Planungen android
All checks were successful
Deploy to production / deploy (push) Successful in 2m17s

This commit is contained in:
Torsten Schulz (local)
2026-07-08 22:28:51 +02:00
parent f5b3f174e6
commit 53becb6630
90 changed files with 5901 additions and 42 deletions

View File

@@ -0,0 +1,8 @@
# Android/Capacitor build configuration.
# Copy to .env.android and adjust values before building.
VITE_API_BASE_URL=https://www.your-part.de
VITE_SOCKET_IO_URL=https://www.your-part.de
VITE_DAEMON_SOCKET=wss://www.your-part.de
VITE_PUBLIC_BASE_URL=https://www.your-part.de
VITE_DISABLE_3D=true

View File

@@ -81,6 +81,7 @@ export default {
display: flex;
flex-direction: column;
height: 100vh;
height: 100dvh;
overflow: hidden;
}
</style>

View File

@@ -233,26 +233,38 @@ export default {
@media (max-width: 640px) {
.app-footer__inner {
gap: 10px;
padding: 8px 10px 10px;
gap: 6px;
min-height: 0;
padding: 6px 10px calc(6px + env(safe-area-inset-bottom, 0px));
}
.footer-system {
flex-wrap: wrap;
display: none;
}
.window-bar {
display: none;
}
.window-bar:not(.window-bar--empty) {
display: flex;
border-radius: var(--radius-md);
padding: 6px;
padding: 4px 6px;
}
.dialog-button {
min-height: 34px;
min-height: 30px;
}
.static-block {
flex-wrap: wrap;
justify-content: flex-start;
flex-wrap: nowrap;
justify-content: space-between;
width: 100%;
gap: 8px;
padding-top: 0;
border-top: 0;
font-size: 0.78rem;
overflow-x: auto;
}
}
</style>

View File

@@ -515,25 +515,60 @@ export default {
}
@media (max-width: 640px) {
.app-header {
padding: 4px 8px;
}
.app-header__inner {
align-items: flex-start;
align-items: center;
gap: 8px;
}
.brand {
width: 100%;
flex: 1 1 auto;
width: auto;
}
.logo {
width: 34px;
height: 34px;
border-radius: 10px;
}
.brand-copy strong {
font-size: 0.94rem;
}
.brand-copy span {
font-size: 0.68rem;
}
.header-meta {
width: 100%;
gap: 8px;
}
.header-lang {
width: 100%;
justify-content: space-between;
}
.header-lang__select {
min-width: 8.5rem;
}
.connection-status {
width: 100%;
flex-wrap: wrap;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.status-indicator {
min-height: 32px;
min-height: 30px;
justify-content: center;
padding: 0 8px;
font-size: 0.68rem;
}
}
</style>

View File

@@ -2,9 +2,27 @@
<nav
ref="navRoot"
class="app-navigation"
:class="{ 'app-navigation--suppress-hover': suppressHover }"
:class="{
'app-navigation--suppress-hover': suppressHover,
'app-navigation--mobile-open': mobileMenuOpen
}"
>
<div class="nav-primary">
<div class="mobile-nav-bar">
<button
type="button"
class="mobile-nav-toggle"
:aria-expanded="String(mobileMenuOpen)"
aria-controls="app-navigation-menu"
@click.stop="toggleMobileMenu"
>
<span class="mobile-nav-toggle__icon" aria-hidden="true"></span>
<span>{{ mobileMenuOpen ? 'Schließen' : 'Menü' }}</span>
</button>
<span class="mobile-nav-current">{{ activeMenuLabel }}</span>
<button type="button" @click="accessMailbox" class="mailbox mobile-nav-mailbox" aria-label="Mailbox"></button>
</div>
<div id="app-navigation-menu" class="nav-primary">
<ul>
<!-- Hauptmenü -->
<li
@@ -228,11 +246,20 @@ export default {
_moderationReportChangedHandler: null,
_userAccessChangedHandler: null,
moderationOpenCount: 0,
accountMenuOpen: false
accountMenuOpen: false,
mobileMenuOpen: false
};
},
computed: {
...mapGetters(['menu', 'user', 'menuNeedsUpdate', 'socket'])
...mapGetters(['menu', 'user', 'menuNeedsUpdate', 'socket']),
activeMenuLabel() {
const entries = Object.entries(this.menu || {});
const activeEntry = entries.find(([, item]) => this.containsActiveItem(item));
if (!activeEntry) {
return 'Navigation';
}
return this.$t(`navigation.${activeEntry[0]}`);
}
},
watch: {
menuNeedsUpdate(newVal) {
@@ -337,11 +364,21 @@ export default {
updateViewportState() {
this.isMobileNav = window.innerWidth <= 960;
if (!this.isMobileNav) {
this.mobileMenuOpen = false;
this.expandedMainKey = null;
this.expandedSubKey = null;
}
},
toggleMobileMenu() {
this.mobileMenuOpen = !this.mobileMenuOpen;
if (!this.mobileMenuOpen) {
this.expandedMainKey = null;
this.expandedSubKey = null;
this.closeAccountMenu();
}
},
isMainExpanded(key) {
return this.isMobileNav
? this.expandedMainKey === key
@@ -379,6 +416,7 @@ export default {
this.pinnedMainKey = null;
this.pinnedSubKey = null;
this.accountMenuOpen = false;
this.mobileMenuOpen = false;
this.suppressHover = true;
if (this.hoverReleaseTimer) {
clearTimeout(this.hoverReleaseTimer);
@@ -477,6 +515,16 @@ export default {
return this.$route.path === item.path || this.$route.path.startsWith(`${item.path}/`);
},
containsActiveItem(item) {
if (this.isItemActive(item)) {
return true;
}
if (!item?.children) {
return false;
}
return Object.values(item.children).some((child) => this.containsActiveItem(child));
},
openMultiChat() {
const ref = this.$root.$refs.multiChatDialog;
if (ref && typeof ref.open === 'function') {
@@ -701,6 +749,10 @@ export default {
z-index: 999;
}
.mobile-nav-bar {
display: none;
}
.nav-primary {
flex: 1;
min-width: 0;
@@ -1078,44 +1130,137 @@ a {
@media (max-width: 960px) {
.app-navigation {
position: sticky;
top: 0;
margin: 0;
flex-direction: column;
flex-wrap: nowrap;
padding: 8px 10px;
padding: 0;
align-items: stretch;
gap: 0;
box-shadow: 0 8px 18px rgba(71, 52, 35, 0.12);
}
.mobile-nav-bar {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
min-height: 54px;
padding: 8px 10px;
}
.mobile-nav-toggle {
min-height: 38px;
padding: 0 12px;
border-radius: 999px;
background: rgba(255, 250, 242, 0.94);
border: 1px solid rgba(248, 162, 43, 0.32);
color: var(--color-text-primary);
box-shadow: none;
font-size: 0.84rem;
font-weight: 800;
}
.mobile-nav-toggle:hover {
transform: none;
box-shadow: none;
}
.mobile-nav-toggle__icon,
.mobile-nav-toggle__icon::before,
.mobile-nav-toggle__icon::after {
display: inline-block;
width: 15px;
height: 2px;
border-radius: 999px;
background: currentColor;
}
.mobile-nav-toggle__icon {
position: relative;
margin-right: 8px;
transform: translateY(-1px);
}
.mobile-nav-toggle__icon::before,
.mobile-nav-toggle__icon::after {
position: absolute;
left: 0;
content: '';
}
.mobile-nav-toggle__icon::before {
top: -5px;
}
.mobile-nav-toggle__icon::after {
top: 5px;
}
.mobile-nav-current {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--color-text-secondary);
font-size: 0.86rem;
font-weight: 800;
}
.mobile-nav-mailbox {
display: block;
width: 38px;
height: 38px;
}
.nav-primary,
.nav-primary > ul,
.right-block {
display: none;
width: 100%;
}
.app-navigation--mobile-open .nav-primary,
.app-navigation--mobile-open .right-block {
display: flex;
}
.nav-primary {
overflow-x: auto;
overflow-y: visible;
flex-direction: column;
max-height: min(62vh, 560px);
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
padding: 6px 10px 10px;
border-top: 1px solid rgba(93, 64, 55, 0.1);
}
.nav-primary > ul {
min-width: 0;
flex-wrap: wrap;
flex-direction: column;
flex-wrap: nowrap;
gap: 8px;
}
.right-block {
justify-content: space-between;
padding-left: 0;
justify-content: flex-start;
padding: 8px 10px 10px;
margin-left: 0;
border-left: 0;
padding-top: 6px;
border-top: 1px solid rgba(93, 64, 55, 0.1);
background: rgba(246, 236, 220, 0.99);
}
.mainmenuitem {
min-height: 42px;
width: calc(50% - 4px);
width: 100%;
justify-content: flex-start;
padding: 0 14px;
background: rgba(255, 252, 247, 0.68);
}
.mainmenuitem--active {
background: rgba(255, 250, 242, 0.98);
}
.submenu1,
@@ -1150,21 +1295,26 @@ a {
height: 42px;
}
.right-block > .mailbox {
display: none;
}
.account-menu__label {
max-width: 180px;
}
}
@media (max-width: 640px) {
.mainmenuitem {
width: 100%;
}
.right-block {
display: none;
flex-wrap: wrap;
gap: 10px;
}
.app-navigation--mobile-open .right-block {
display: flex;
}
.account-menu {
width: 100%;
}

View File

@@ -154,9 +154,10 @@ export default {
computed: {
isDev() {
return !import.meta.env.PROD;
}
},
computed: {
},
is3DDisabled() {
return import.meta.env.VITE_DISABLE_3D === 'true';
},
actualGender() {
if (this.gender) {
return this.gender;
@@ -214,13 +215,20 @@ export default {
},
watch: {
async actualGender() {
if (this.is3DDisabled) return;
await this.loadModel();
},
async ageGroup() {
if (this.is3DDisabled) return;
await this.loadModel();
}
},
async mounted() {
if (this.is3DDisabled) {
this.showFallback = true;
return;
}
const container = this.$refs.container;
// expose runtime props for debug overlay
this.debugInfo.lightweight = !!this.lightweight;
@@ -290,6 +298,11 @@ export default {
},
async init3D() {
if (this.is3DDisabled) {
this.showFallback = true;
return;
}
const container = this.$refs.container;
if (!container) return;
this.showFallback = false;
@@ -397,6 +410,11 @@ export default {
},
async loadModel() {
if (this.is3DDisabled) {
this.showFallback = true;
return;
}
if (!this.scene) return;
const modelRuntime = await this.ensureThreeModelRuntime();
const loaders = await this.ensureThreeLoaders();

View File

@@ -36,6 +36,7 @@ import apiClient from '@/utils/axios.js';
import DialogWidget from '@/components/DialogWidget.vue';
import SelectDropdownWidget from '@/components/form/SelectDropdownWidget.vue';
import { showApiError, showError } from '@/utils/feedback.js';
import { toTranslatedParamOptions } from '@/utils/paramValues.js';
export default {
name: 'RegisterDialog',
@@ -166,7 +167,7 @@ export default {
const response = await apiClient.post('/api/settings/getparamvalues', {
type: 'language'
});
this.languages = response.data.map(item => { return { value: item.id, captionTr: `settings.personal.language.${item.name}` } });
this.languages = toTranslatedParamOptions(response.data, 'settings.personal.language');
} catch (err) {
console.error('Error loading languages:', err);
}

View File

@@ -2,6 +2,7 @@
"error": {
"title": "Adunay nahitabo nga sayop",
"close": "Isira",
"credentialsinvalid": "Dili husto ang imong login data."
"credentialsinvalid": "Dili husto ang imong login data.",
"network": "Napakyas ang koneksyon sa server. Palihug susiha ang internet connection ug server access."
}
}

View File

@@ -2,6 +2,7 @@
"error": {
"title": "Fehler aufgetreten",
"close": "Schließen",
"credentialsinvalid": "Die Zugangsdaten sind nicht korrekt."
"credentialsinvalid": "Die Zugangsdaten sind nicht korrekt.",
"network": "Die Verbindung zum Server ist fehlgeschlagen. Bitte Internetverbindung und Serverfreigabe prüfen."
}
}
}

View File

@@ -2,6 +2,7 @@
"error": {
"title": "An Error Occurred",
"close": "Close",
"credentialsinvalid": "The credentials are not correct."
"credentialsinvalid": "The credentials are not correct.",
"network": "The server connection failed. Please check your internet connection and server access."
}
}
}

View File

@@ -1,7 +1,8 @@
{
"error": {
"title": "Error",
"close": "Cerrar",
"credentialsinvalid": "Las credenciales no son correctas."
"close": "Cerrar",
"credentialsinvalid": "Las credenciales no son correctas.",
"network": "La conexión con el servidor falló. Comprueba la conexión a internet y el acceso al servidor."
}
}

View File

@@ -2,6 +2,7 @@
"error": {
"title": "Fehler aufgetreten",
"close": "Fermer",
"credentialsinvalid": "Die Zugangsdaten sind nicht korrekt."
"credentialsinvalid": "Die Zugangsdaten sind nicht korrekt.",
"network": "La connexion au serveur a échoué. Vérifie la connexion internet et l'accès au serveur."
}
}

View File

@@ -0,0 +1,22 @@
export function normalizeParamValues(data) {
if (Array.isArray(data)) {
return data;
}
if (Array.isArray(data?.values)) {
return data.values;
}
if (Array.isArray(data?.paramValues)) {
return data.paramValues;
}
return [];
}
export function toTranslatedParamOptions(data, captionPrefix) {
return normalizeParamValues(data).map((item) => ({
value: item.id ?? item.value,
captionTr: `${captionPrefix}.${item.name ?? item.value}`
}));
}

View File

@@ -49,6 +49,7 @@
import apiClient from '@/utils/axios.js';
import { mapGetters } from 'vuex';
import MessageboxWidget from '../../components/MessageboxWidget.vue';
import { toTranslatedParamOptions } from '@/utils/paramValues.js';
export default {
name: "AdminInterestsView",
@@ -120,7 +121,7 @@ export default {
const response = await apiClient.post('/api/settings/getparamvalues', {
type: 'language'
});
this.languages = response.data.map(item => { return { value: item.id, captionTr: `settings.personal.language.${item.name}` } });
this.languages = toTranslatedParamOptions(response.data, 'settings.personal.language');
} catch (err) {
console.error('Error loading languages:', err);
}

View File

@@ -72,6 +72,7 @@
import Multiselect from 'vue-multiselect';
import 'vue-multiselect/dist/vue-multiselect.min.css';
import apiClient from '@/utils/axios.js';
import { normalizeParamValues } from '@/utils/paramValues.js';
export default {
components: {
@@ -98,7 +99,7 @@ export default {
const response = await apiClient.post('/api/settings/getparamvalues', {
type: 'gender'
});
this.genderOptions = response.data.map(g => ({ name: g.name, value: g.value }));
this.genderOptions = normalizeParamValues(response.data).map(g => ({ name: g.name, value: g.value ?? g.id }));
} catch (error) {
console.error('Fehler beim Laden der Geschlechtsoptionen:', error);
}