Jelajahi Sumber

Set up the Options view bound to the prefs & settings stores

Bubka 1 tahun lalu
induk
melakukan
c448628e1b

+ 9 - 0
resources/js_vue3/App.vue

@@ -1,5 +1,14 @@
 <script setup>
     import { RouterView } from 'vue-router'
+
+    onMounted(async () => {
+        const { useUserStore } = await import('./stores/user.js');
+        const { language } = useNavigatorLanguage()
+
+        watch(language, () => {
+            useUserStore().applyLanguage()
+        })
+    })
 </script>
 
 <template>

+ 11 - 9
resources/js_vue3/app.js

@@ -38,9 +38,11 @@ app.use(i18nVue, {
         }
     }
 })
+
+// Notifications
 app.use(Notifications)
 
-// Components registration
+// Global components registration
 import ResponsiveWidthWrapper from '@/layouts/ResponsiveWidthWrapper.vue'
 import FormWrapper from '@/layouts/FormWrapper.vue'
 import Footer from '@/layouts/Footer.vue'
@@ -49,10 +51,10 @@ import VueButton           from '@/components/formElements/Button.vue'
 import FieldError       from '@/components/formElements/FieldError.vue'
 import FormField        from '@/components/formElements/FormField.vue'
 import FormPasswordField        from '@/components/formElements/FormPasswordField.vue'
-// import FormSelect       from './FormSelect'
+import FormSelect       from '@/components/formElements/FormSelect.vue'
 // import FormSwitch       from './FormSwitch'
-// import FormToggle       from './FormToggle'
-// import FormCheckbox     from './FormCheckbox'
+import FormToggle       from '@/components/formElements/FormToggle.vue'
+import FormCheckbox     from '@/components/formElements/FormCheckbox.vue'
 import FormButtons      from '@/components/formElements/FormButtons.vue'
 // import Kicker           from './Kicker'
 // import SettingTabs      from './SettingTabs'
@@ -67,6 +69,9 @@ app
     .component('FieldError', FieldError)
     .component('FormField', FormField)
     .component('FormPasswordField', FormPasswordField)
+    .component('FormSelect', FormSelect)
+    .component('FormToggle', FormToggle)
+    .component('FormCheckbox', FormCheckbox)
     .component('FormButtons', FormButtons)
 
 // Global error handling
@@ -81,8 +86,5 @@ if (process.env.NODE_ENV != 'development') {
 app.mount('#app')
 
 // Theme
-import { useColorMode } from '@vueuse/core'
-
-const mode = useColorMode({
-    attribute: 'data-theme',
-  })
+import { useUserStore } from '@/stores/user'
+useUserStore().applyUserPrefs()

+ 37 - 0
resources/js_vue3/components/VersionChecker.vue

@@ -0,0 +1,37 @@
+<script setup>
+    import systemService from '@/services/systemService'
+    import { useAppSettingsStore } from '@/stores/appSettings'
+
+    const appSettings = useAppSettingsStore()
+    const isScanning = ref(false)
+    const isUpToDate = ref(null)
+
+    async function getLatestRelease() {
+        isScanning.value = true;
+
+        await systemService.getLastRelease()
+        .then(response => {
+            appSettings.latestRelease = response.data.newRelease
+            isUpToDate.value = response.data.newRelease === false
+        })
+
+        isScanning.value = false;
+    }
+
+</script>
+
+<template>
+    <div class="columns is-mobile is-vcentered">
+        <div class="column is-narrow">
+            <button type="button" :class="isScanning ? 'is-loading' : ''" class="button is-link is-rounded is-small" @click="getLatestRelease">Check now</button>
+        </div>
+        <div class="column">
+            <span v-if="appSettings.latestRelease" class="mt-2 has-text-warning">
+                <span class="release-flag"></span>{{ appSettings.latestRelease }} is available <a class="is-size-7" href="https://github.com/Bubka/2FAuth/releases">View on Github</a>
+            </span>
+            <span v-if="isUpToDate" class="has-text-grey">
+                {{ $t('commons.you_are_up_to_date') }}
+            </span>
+        </div>
+    </div>
+</template>

+ 44 - 0
resources/js_vue3/components/formElements/FormCheckbox.vue

@@ -0,0 +1,44 @@
+<script setup>
+    defineOptions({
+        inheritAttrs: false
+    })
+
+    const props = defineProps({
+        modelValue: Boolean,
+        fieldName: {
+            type: String,
+            default: '',
+            required: true
+        },
+        label: {
+            type: String,
+            default: ''
+        },
+        labelClass: {
+            type: String,
+            default: ''
+        },
+        help: {
+            type: String,
+            default: ''
+        },
+    })
+
+    const emit = defineEmits(['update:modelValue'])
+    const attrs = useAttrs()
+    const checked = ref(props.modelValue)
+
+    function setCheckbox() {
+        if (attrs['disabled'] == undefined) {
+            emit('update:modelValue', checked)
+        }
+    }
+</script>
+
+<template>
+    <div class="field">
+        <input :id="fieldName" type="checkbox" :name="fieldName" class="is-checkradio is-info" v-model="checked" v-on:change="setCheckbox" v-bind="$attrs"/>
+        <label tabindex="0" :for="fieldName" class="label" :class="labelClass" v-html="$t(label)" v-on:keypress.space.prevent="setCheckbox" />
+        <p class="help" v-html="$t(help)" v-if="help" />
+    </div>
+</template>

+ 39 - 0
resources/js_vue3/components/formElements/FormSelect.vue

@@ -0,0 +1,39 @@
+<script setup>
+    const props = defineProps({
+        modelValue: [String, Number, Boolean],
+        label: {
+            type: String,
+            default: ''
+        },
+        fieldName: {
+            type: String,
+            default: '',
+            required: true
+        },
+        options: {
+            type: Array,
+            required: true
+        },
+        help: {
+            type: String,
+            default: ''
+        },
+    })
+
+    const selected = ref(props.modelValue)
+</script>
+
+<template>
+    <div class="field">
+        <label class="label" v-html="$t(label)"></label>
+        <div class="control">
+            <div class="select">
+                <select v-model="selected" v-on:change="$emit('update:modelValue', $event.target.value)">
+                    <option v-for="option in options" :value="option.value">{{ $t(option.text) }}</option>
+                </select>
+            </div>
+        </div>
+        <!-- <FieldError :form="form" :field="fieldName" /> -->
+        <p class="help" v-html="$t(help)" v-if="help"></p>
+    </div>
+</template>

+ 73 - 0
resources/js_vue3/components/formElements/FormToggle.vue

@@ -0,0 +1,73 @@
+<script setup>
+    import { useIdGenerator } from '@/composables/helpers'
+    import { UseColorMode } from '@vueuse/components'
+
+    const props = defineProps({
+        modelValue: [String, Number, Boolean],
+        choices: {
+            type: Array,
+            required: true
+        },
+        fieldName: {
+            type: String,
+            required: true
+        },
+        hasOffset: Boolean,
+        isDisabled: Boolean,
+        label: {
+            type: String,
+            default: ''
+        },
+        help: {
+            type: String,
+            default: ''
+        },
+    })
+
+    // defines what events our component emits
+    const emit = defineEmits('update:modelValue')
+
+    function setRadio(event) {
+        emit('update:modelValue', event)
+    }
+    
+</script>
+
+<template>
+    <div class="field" :class="{ 'pt-3': hasOffset }" role="radiogroup"
+        :aria-labelledby="useIdGenerator('label',fieldName).inputId">
+        <label v-if="label" :id="useIdGenerator('label',fieldName).inputId" class="label" v-html="$t(label)" />
+        <div class="is-toggle buttons">
+            <UseColorMode v-slot="{ mode }">
+                <button
+                    v-for="choice in choices"
+                    :key="choice.value"
+                    :id="useIdGenerator('button',fieldName+choice.value).inputId"
+                    role="radio"
+                    type="button"
+                    class="button"
+                    :aria-checked="modelValue===choice.value"
+                    :disabled="isDisabled"
+                    :class="{
+                        'is-link': modelValue===choice.value,
+                        'is-dark': mode==='dark',
+                        'is-multiline': choice.legend,
+                    }"
+                    v-on:click.stop="setRadio(choice.value)"
+                    :title="choice.title? choice.title:''">
+                    <input
+                        :id="useIdGenerator('radio',choice.value).inputId"
+                        type="radio"
+                        class="is-hidden"
+                        :checked="modelValue===choice.value"
+                        :value="choice.value"
+                        :disabled="isDisabled" />
+                    <span v-if="choice.legend" v-html="$t(choice.legend)" class="is-block is-size-7" />
+                    <FontAwesomeIcon :icon="['fas',choice.icon]" v-if="choice.icon" class="mr-2" /> {{ $t(choice.text) }}
+                </button>
+            </UseColorMode>
+        </div>
+        <!-- <FieldError :form="form" :field="fieldName" /> -->
+        <p class="help" v-html="$t(help)" v-if="help" />
+    </div>
+</template>

+ 45 - 0
resources/js_vue3/layouts/SettingTabs.vue

@@ -0,0 +1,45 @@
+<script setup>
+    const tabs = ref([
+        {
+            'name' : wTrans('settings.options'),
+            'view' : 'settings.options',
+            'id'   : 'lnkTabOptions'
+        },
+        // {
+        //     'name' : wTrans('settings.account'),
+        //     'view' : 'settings.account',
+        //     'id'   : 'lnkTabAccount'
+        // },
+        // {
+        //     'name' : wTrans('settings.oauth'),
+        //     'view' : 'settings.oauth.tokens',
+        //     'id'   : 'lnkTabOAuth'
+        // },
+        // {
+        //     'name' : wTrans('settings.webauthn'),
+        //     'view' : 'settings.webauthn.devices',
+        //     'id'   : 'lnkTabWebauthn'
+        // },
+    ])
+
+    const props = defineProps({
+        activeTab: {
+            type: String,
+            default: ''
+        },
+    })
+</script>
+
+<template>
+    <div class="options-header">
+        <ResponsiveWidthWrapper>
+            <div class="tabs is-centered is-fullwidth">
+                <ul>
+                    <li v-for="tab in tabs" :key="tab.view" :class="{ 'is-active': tab.view === props.activeTab }">
+                        <RouterLink :id="tab.id" :to="{ name: tab.view }">{{ tab.name }}</RouterLink>
+                    </li>
+                </ul>
+            </div>
+        </ResponsiveWidthWrapper>
+    </div>
+</template>

+ 16 - 16
resources/js_vue3/router/index.js

@@ -34,25 +34,25 @@ import noEmptyError from './middlewares/noEmptyError'
 const router = createRouter({
 	history: createWebHistory('/'),
 	routes: [
-		// { path: '/start', name: 'start', component: Start, meta: { requiresAuth: true }, props: true },
-        // { path: '/capture', name: 'capture', component: Capture, meta: { requiresAuth: true }, props: true },
+		// { path: '/start', name: 'start', component: Start, meta: { middlewares: [authGuard] }, props: true },
+        // { path: '/capture', name: 'capture', component: Capture, meta: { middlewares: [authGuard] }, props: true },
 
-        { path: '/accounts', name: 'accounts', component: Accounts, meta: { middlewares: [authGuard], requiresAuth: true }, alias: '/', props: true },
-        // { path: '/account/create', name: 'createAccount', component: CreateAccount, meta: { requiresAuth: true } },
-        // { path: '/account/import', name: 'importAccounts', component: ImportAccount, meta: { requiresAuth: true } },
-        // { path: '/account/:twofaccountId/edit', name: 'editAccount', component: EditAccount, meta: { requiresAuth: true } },
-        // { path: '/account/:twofaccountId/qrcode', name: 'showQRcode', component: QRcodeAccount, meta: { requiresAuth: true } },
+        { path: '/accounts', name: 'accounts', component: Accounts, meta: { middlewares: [authGuard] }, alias: '/', props: true },
+        // { path: '/account/create', name: 'createAccount', component: CreateAccount, meta: { middlewares: [authGuard] } },
+        // { path: '/account/import', name: 'importAccounts', component: ImportAccount, meta: { middlewares: [authGuard] } },
+        // { path: '/account/:twofaccountId/edit', name: 'editAccount', component: EditAccount, meta: { middlewares: [authGuard] } },
+        // { path: '/account/:twofaccountId/qrcode', name: 'showQRcode', component: QRcodeAccount, meta: { middlewares: [authGuard] } },
 
-        // { path: '/groups', name: 'groups', component: Groups, meta: { requiresAuth: true }, props: true },
-        // { path: '/group/create', name: 'createGroup', component: CreateGroup, meta: { requiresAuth: true } },
-        // { path: '/group/:groupId/edit', name: 'editGroup', component: EditGroup, meta: { requiresAuth: true }, props: true },
+        // { path: '/groups', name: 'groups', component: Groups, meta: { middlewares: [authGuard] }, props: true },
+        // { path: '/group/create', name: 'createGroup', component: CreateGroup, meta: { middlewares: [authGuard] } },
+        // { path: '/group/:groupId/edit', name: 'editGroup', component: EditGroup, meta: { middlewares: [authGuard] }, props: true },
 
-        { path: '/settings/options', name: 'settings.options', component: SettingsOptions, meta: { requiresAuth: true, showAbout: true } },
-        // { path: '/settings/account', name: 'settings.account', component: SettingsAccount, meta: { requiresAuth: true, showAbout: true } },
-        // { path: '/settings/oauth', name: 'settings.oauth.tokens', component: SettingsOAuth, meta: { requiresAuth: true, showAbout: true } },
-        // { path: '/settings/oauth/pat/create', name: 'settings.oauth.generatePAT', component: GeneratePAT, meta: { requiresAuth: true, showAbout: true } },
-        // { path: '/settings/webauthn/:credentialId/edit', name: 'settings.webauthn.editCredential', component: EditCredential, meta: { requiresAuth: true, showAbout: true }, props: true },
-        // { path: '/settings/webauthn', name: 'settings.webauthn.devices', component: SettingsWebAuthn, meta: { requiresAuth: true, showAbout: true } },
+        { path: '/settings/options', name: 'settings.options', component: SettingsOptions, meta: { middlewares: [authGuard], showAbout: true } },
+        // { path: '/settings/account', name: 'settings.account', component: SettingsAccount, meta: { middlewares: [authGuard], showAbout: true } },
+        // { path: '/settings/oauth', name: 'settings.oauth.tokens', component: SettingsOAuth, meta: { middlewares: [authGuard], showAbout: true } },
+        // { path: '/settings/oauth/pat/create', name: 'settings.oauth.generatePAT', component: GeneratePAT, meta: { middlewares: [authGuard], showAbout: true } },
+        // { path: '/settings/webauthn/:credentialId/edit', name: 'settings.webauthn.editCredential', component: EditCredential, meta: { middlewares: [authGuard], showAbout: true }, props: true },
+        // { path: '/settings/webauthn', name: 'settings.webauthn.devices', component: SettingsWebAuthn, meta: { middlewares: [authGuard], showAbout: true } },
 
         { path: '/login', name: 'login', component: Login, meta: { disabledWithAuthProxy: true, showAbout: true } },
         { path: '/register', name: 'register', component: Register, meta: { disabledWithAuthProxy: true, showAbout: true } },

+ 1 - 0
resources/js_vue3/router/middlewares/authGuard.js

@@ -13,6 +13,7 @@ export default async function auth({ to, next, stores }) {
                 preferences: currentUser.preferences,
                 isAdmin: currentUser.is_admin,
             })
+            user.applyUserPrefs()
         }
     }
 

+ 14 - 0
resources/js_vue3/services/appSettingService.js

@@ -0,0 +1,14 @@
+import { httpClientFactory } from '@/services/httpClientFactory'
+
+const apiClient = httpClientFactory('api')
+
+export default {
+    /**
+     * 
+     * @returns 
+     */
+    update(name, value) {
+        return apiClient.put('/settings/' + name, { value: value })
+    },
+    
+}

+ 14 - 0
resources/js_vue3/services/groupService.js

@@ -0,0 +1,14 @@
+import { httpClientFactory } from '@/services/httpClientFactory'
+
+const apiClient = httpClientFactory('api')
+
+export default {
+    /**
+     * 
+     * @returns 
+     */
+    getAll() {
+        return apiClient.get('groups')
+    },
+    
+}

+ 9 - 1
resources/js_vue3/services/systemService.js

@@ -5,10 +5,18 @@ const webClient = httpClientFactory('web')
 export default {
     /**
      * 
-     * @returns 
+     * @returns Promise
      */
     getSystemInfos() {
         return webClient.get('infos')
     },
+
+    /**
+     * 
+     * @returns Promise
+     */
+    getLastRelease() {
+        return webClient.get('latestRelease')
+    }
     
 }

+ 14 - 0
resources/js_vue3/services/userPreferenceService.js

@@ -0,0 +1,14 @@
+import { httpClientFactory } from '@/services/httpClientFactory'
+
+const apiClient = httpClientFactory('api')
+
+export default {
+    /**
+     * 
+     * @returns 
+     */
+    update(name, value) {
+        return apiClient.put('/user/preferences/' + name, { value: value })
+    },
+    
+}

+ 6 - 10
resources/js_vue3/stores/appSettings.js

@@ -1,18 +1,14 @@
 import { defineStore } from 'pinia'
-// import { useApi } from '@/api/useAPI.js'
-
-// const api = useApi()
+import appSettingService from '@/services/appSettingService'
 
 export const useAppSettingsStore = defineStore({
-	id: 'settings',
+    id: 'appSettings',
 
-	state: () => {
+    state: () => {
         return { ...window.appSettings }
     },
 
-	actions: {
-		updateSetting(setting) {
-			this.settings = { ...this.state.settings, ...setting }
-		},
-	},
+    actions: {
+        
+    },
 })

+ 23 - 3
resources/js_vue3/stores/user.js

@@ -1,6 +1,7 @@
 import { defineStore } from 'pinia'
 import authService from '@/services/authService'
 import router from '@/router'
+import { useColorMode } from '@vueuse/core'
 
 export const useUserStore = defineStore({
     id: 'user',
@@ -20,9 +21,6 @@ export const useUserStore = defineStore({
     },
 
     actions: {
-        updatePreference(preference) {
-            this.preferences = { ...this.state.preferences, ...preference }
-        },
 
         logout() {
             // async appLogout(evt) {
@@ -45,7 +43,29 @@ export const useUserStore = defineStore({
         reset() {
             localStorage.clear()
             this.$reset()
+            this.applyUserPrefs()
             router.push({ name: 'login' })
+        },
+
+        applyTheme() {
+            const mode = useColorMode({
+                attribute: 'data-theme',
+            })
+            mode.value = this.preferences.theme == 'system' ? 'auto' : this.preferences.theme
+        },
+
+        applyLanguage() {
+            const { isSupported, language } = useNavigatorLanguage()
+
+            if (isSupported) {
+                loadLanguageAsync(this.preferences.lang == 'browser' ? language.value.slice(0, 2)  : this.preferences.lang)
+            }
+            else loadLanguageAsync('en')
+        },
+
+        applyUserPrefs() {
+            this.applyTheme()
+            this.applyLanguage()
         }
 
     },

+ 1 - 0
resources/js_vue3/views/auth/Login.vue

@@ -34,6 +34,7 @@
                 preferences: response.data.preferences,
                 isAdmin: response.data.is_admin,
             })
+            user.applyTheme()
 
             router.push({ name: 'accounts', params: { toRefresh: true } })
         })

+ 195 - 2
resources/js_vue3/views/settings/Options.vue

@@ -1,7 +1,200 @@
-<script>
+<script setup>
+    import SettingTabs from '@/layouts/SettingTabs.vue'
+    import groupService from '@/services/groupService'
+    import userPreferenceService from '@/services/userPreferenceService'
+    import { useUserStore } from '@/stores/user'
+    import { useAppSettingsStore } from '@/stores/appSettings'
+    import { useNotifyStore } from '@/stores/notify'
+    import { UseColorMode } from '@vueuse/components'
+    import VersionChecker from '@/components/VersionChecker.vue'
 
+    const $2fauth = inject('2fauth')
+    const user = useUserStore()
+    const notify = useNotifyStore()
+    const appSettings = useAppSettingsStore()
+    const returnTo = useStorage($2fauth.prefix + 'returnTo', 'accounts')
+
+    const layouts = [
+        { text: 'settings.forms.grid', value: 'grid', icon: 'th' },
+        { text: 'settings.forms.list', value: 'list', icon: 'list' },
+    ]
+    const themes = [
+        { text: 'settings.forms.light', value: 'light', icon: 'sun' },
+        { text: 'settings.forms.dark', value: 'dark', icon: 'moon' },
+        { text: 'settings.forms.automatic', value: 'system', icon: 'desktop' },
+    ]
+    const passwordFormats = [
+        { text: '12 34 56', value: 2, legend: 'settings.forms.pair', title: 'settings.forms.pair_legend' },
+        { text: '123 456', value: 3, legend: 'settings.forms.trio', title: 'settings.forms.trio_legend' },
+        { text: '1234 5678', value: 0.5, legend: 'settings.forms.half', title: 'settings.forms.half_legend' },
+    ]
+    const kickUserAfters = [
+        { text: 'settings.forms.never', value: 0 },
+        { text: 'settings.forms.on_otp_copy', value: -1 },
+        { text: 'settings.forms.1_minutes', value: 1 },
+        { text: 'settings.forms.5_minutes', value: 5 },
+        { text: 'settings.forms.10_minutes', value: 10 },
+        { text: 'settings.forms.15_minutes', value: 15 },
+        { text: 'settings.forms.30_minutes', value: 30 },
+        { text: 'settings.forms.1_hour', value: 60 },
+        { text: 'settings.forms.1_day', value: 1440 }, 
+    ]
+    const groups = [
+        { text: 'groups.no_group', value: 0 },
+        { text: 'groups.active_group', value: -1 },
+    ]
+    const captureModes = [
+        { text: 'settings.forms.livescan', value: 'livescan' },
+        { text: 'settings.forms.upload', value: 'upload' },
+        { text: 'settings.forms.advanced_form', value: 'advancedForm' },
+    ]
+    const getOtpTriggers = [
+        { text: 'settings.forms.otp_generation_on_request', value: true, legend: 'settings.forms.otp_generation_on_request_legend', title: 'settings.forms.otp_generation_on_request_title' },
+        { text: 'settings.forms.otp_generation_on_home', value: false, legend: 'settings.forms.otp_generation_on_home_legend', title: 'settings.forms.otp_generation_on_home_title' },
+    ]
+
+    const langs = computed(() => {
+        let locales = [{
+            text: 'languages.browser_preference',
+            value: 'browser'
+        }];
+
+        for (const locale of $2fauth.langs) {
+            locales.push({
+                text: 'languages.' + locale,
+                value: locale
+            })
+        }
+        return locales
+    })
+
+    user.$subscribe((mutation) => {
+        userPreferenceService.update(mutation.events.key, mutation.events.newValue).then(response => {
+            useNotifyStore().info({ type: 'is-success', text: trans('settings.forms.setting_saved') })
+            
+            if(mutation.events.key === 'lang' && getActiveLanguage() !== mutation.events.newValue) {
+                user.applyLanguage()
+            }
+            else if(mutation.events.key === 'theme') {
+                user.applyTheme()
+            }
+        })
+    })
+
+    appSettings.$subscribe((mutation) => {
+        appSettingService.update(mutation.events.key, mutation.events.newValue).then(response => {
+            useNotifyStore().info({ type: 'is-success', text: trans('settings.forms.setting_saved') })
+        })
+    })
+
+    onMounted(() => {
+        groupService.getAll().then(response => {
+            response.data.forEach((data) => {
+                if( data.id >0 ) {
+                    groups.push({
+                        text: data.name,
+                        value: data.id
+                    })
+                }
+            })
+        })
+    })
+
+    onBeforeRouteLeave((to) => {
+        if (! to.name.startsWith('settings.')) {
+            notify.clear()
+        }
+    })
 </script>
 
 <template>
-    <p>toto</p>
+    <div>
+        <SettingTabs activeTab="settings.options"></SettingTabs>
+        <div class="options-tabs">
+            <FormWrapper>
+                <form>
+                    <!-- <input type="hidden" name="isReady" id="isReady" :value="isReady" /> -->
+                    <!-- user preferences -->
+                    <div class="block">
+                        <h4 class="title is-4 has-text-grey-light">{{ $t('settings.general') }}</h4>
+                        <!-- Language -->
+                        <FormSelect v-model="user.preferences.lang" :options="langs" fieldName="lang" label="settings.forms.language.label" help="settings.forms.language.help" />
+                        <div class="field help">
+                            {{ $t('settings.forms.some_translation_are_missing') }}
+                            <a class="ml-2" href="https://crowdin.com/project/2fauth">
+                                {{ $t('settings.forms.help_translate_2fauth') }}
+                                <FontAwesomeIcon :icon="['fas', 'external-link-alt']" />
+                            </a>
+                        </div>
+                        <!-- display mode -->
+                        <FormToggle v-model="user.preferences.displayMode" :choices="layouts" fieldName="displayMode" label="settings.forms.display_mode.label" help="settings.forms.display_mode.help"/>
+                        <!-- theme -->
+                        <FormToggle v-model="user.preferences.theme" :choices="themes" fieldName="theme" label="settings.forms.theme.label" help="settings.forms.theme.help"/>
+                        <!-- show icon -->
+                        <FormCheckbox v-model="user.preferences.showAccountsIcons" fieldName="showAccountsIcons" label="settings.forms.show_accounts_icons.label" help="settings.forms.show_accounts_icons.help" />
+                        <!-- Official icons -->
+                        <FormCheckbox v-model="user.preferences.getOfficialIcons" fieldName="getOfficialIcons" label="settings.forms.get_official_icons.label" help="settings.forms.get_official_icons.help" />
+                        <!-- password format -->
+                        <FormCheckbox v-model="user.preferences.formatPassword" fieldName="formatPassword" label="settings.forms.password_format.label" help="settings.forms.password_format.help" />
+                        <FormToggle v-model="user.preferences.formatPasswordBy" :choices="passwordFormats" fieldName="formatPasswordBy" />
+
+                        <h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('groups.groups') }}</h4>
+                        <!-- default group -->
+                        <FormSelect v-model="user.preferences.defaultGroup" :options="groups" fieldName="defaultGroup" label="settings.forms.default_group.label" help="settings.forms.default_group.help" />
+                        <!-- retain active group -->
+                        <FormCheckbox v-model="user.preferences.rememberActiveGroup" fieldName="rememberActiveGroup" label="settings.forms.remember_active_group.label" help="settings.forms.remember_active_group.help" />
+
+                        <h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.security') }}</h4>
+                        <!-- auto lock -->
+                        <FormSelect v-model="user.preferences.kickUserAfter" :options="kickUserAfters" fieldName="kickUserAfter" label="settings.forms.auto_lock.label" help="settings.forms.auto_lock.help" />
+                        <!-- get OTP on request -->
+                        <FormToggle v-model="user.preferences.getOtpOnRequest" :choices="getOtpTriggers" fieldName="getOtpOnRequest" label="settings.forms.otp_generation.label" help="settings.forms.otp_generation.help"/>
+                        <!-- otp as dot -->
+                        <FormCheckbox v-model="user.preferences.showOtpAsDot" fieldName="showOtpAsDot" label="settings.forms.show_otp_as_dot.label" help="settings.forms.show_otp_as_dot.help" />
+                        <!-- close otp on copy -->
+                        <FormCheckbox v-model="user.preferences.closeOtpOnCopy" fieldName="closeOtpOnCopy" label="settings.forms.close_otp_on_copy.label" help="settings.forms.close_otp_on_copy.help" :disabled="!user.preferences.getOtpOnRequest" />
+                        <!-- copy otp on get -->
+                        <FormCheckbox v-model="user.preferences.copyOtpOnDisplay" fieldName="copyOtpOnDisplay" label="settings.forms.copy_otp_on_display.label" help="settings.forms.copy_otp_on_display.help" :disabled="!user.preferences.getOtpOnRequest" />
+
+                        <h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.data_input') }}</h4>
+                        <!-- basic qrcode -->
+                        <FormCheckbox v-model="user.preferences.useBasicQrcodeReader" fieldName="useBasicQrcodeReader" label="settings.forms.use_basic_qrcode_reader.label" help="settings.forms.use_basic_qrcode_reader.help" />
+                        <!-- direct capture -->
+                        <FormCheckbox v-model="user.preferences.useDirectCapture" fieldName="useDirectCapture" label="settings.forms.useDirectCapture.label" help="settings.forms.useDirectCapture.help" />
+                        <!-- default capture mode -->
+                        <FormSelect v-model="user.preferences.defaultCaptureMode" :options="captureModes" fieldName="defaultCaptureMode" label="settings.forms.defaultCaptureMode.label" help="settings.forms.defaultCaptureMode.help" />
+                    </div>
+                    <!-- Admin settings -->
+                    <div v-if="user.isAdmin">
+                        <h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.administration') }}</h4>
+                        <div class="is-size-7-mobile block" v-html="$t('settings.administration_legend')"></div>
+                        <!-- Check for update -->
+                        <FormCheckbox v-model="appSettings.checkForUpdate" fieldName="checkForUpdate" label="commons.check_for_update" help="commons.check_for_update_help" />
+                        <VersionChecker />
+                        <!-- protect db -->
+                        <FormCheckbox v-model="appSettings.useEncryption" fieldName="useEncryption" label="settings.forms.use_encryption.label" help="settings.forms.use_encryption.help" />
+                        <!-- disable registration -->
+                        <FormCheckbox v-model="appSettings.disableRegistration" fieldName="disableRegistration" label="settings.forms.disable_registration.label" help="settings.forms.disable_registration.help" />
+                    </div>
+                </form>
+            </FormWrapper>
+        </div>
+        <VueFooter :showButtons="true">
+            <!-- Close button -->
+            <p class="control">
+                <UseColorMode v-slot="{ mode }">
+                    <RouterLink
+                        id="btnClose"
+                        :to="{ name: returnTo }"
+                        class="button is-rounded"
+                        :class="{'is-dark' : mode === 'dark'}"
+                        tabindex="0"
+                        role="button"
+                        :aria-label="$t('commons.close_the_x_page', {pagetitle: $route.meta.title})">
+                        {{ $t('commons.close') }}
+                    </RouterLink>
+                </UseColorMode>
+            </p>
+        </VueFooter>
+    </div>
 </template>

+ 6 - 1
vite.config.js

@@ -38,10 +38,15 @@ export default defineConfig({
                     '@vueuse/core': [
                         'useStorage',
                         'useClipboard',
+                        'useNavigatorLanguage'
                     ],
                     'laravel-vue-i18n': [
                         'i18nVue',
-                        'trans'
+                        'trans',
+                        'wTrans',
+                        'getActiveLanguage',
+                        'loadLanguageAsync',
+                        'getActiveLanguage'
                     ],
                     '@kyvg/vue3-notification': [
                         'useNotification'