update
BIN
i18n-2.xlsm
@@ -14,15 +14,12 @@
|
|||||||
<meta name="robots" content="noindex, nofollow"/>
|
<meta name="robots" content="noindex, nofollow"/>
|
||||||
<meta name="msapplication-tap-highlight" content="no">
|
<meta name="msapplication-tap-highlight" content="no">
|
||||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||||
<script src="http://localhost:8098"></script>
|
|
||||||
<!--
|
<!--
|
||||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
|
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
|
||||||
-->
|
-->
|
||||||
<link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png">
|
<link rel="icon" href="icons/favicon.svg" type="image/svg+xml">
|
||||||
<link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png">
|
<link rel="icon" type="image/ico" href="icons/favicon.ico">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
|
<link rel="apple-touch-icon" href="icons/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
|
|
||||||
<link rel="icon" type="image/ico" href="favicon.ico">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- quasar:entry-point -->
|
<!-- quasar:entry-point -->
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 63 KiB |
BIN
public/icons/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/icons/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
public/icons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 859 B After Width: | Height: | Size: 705 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
BIN
public/icons/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
64
public/icons/favicon.svg
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<svg
|
||||||
|
viewBox="0 0 8.4666662 8.4666662"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<defs id="defs1" />
|
||||||
|
<g id="layer1">
|
||||||
|
<rect
|
||||||
|
style="fill: #27A7E7 ;stroke-width:0.233149"
|
||||||
|
id="rect5"
|
||||||
|
width="6.9885192"
|
||||||
|
height="0.35581663"
|
||||||
|
x="3.114475"
|
||||||
|
y="0.86827624"
|
||||||
|
transform="matrix(0.77578367,0.63099897,-0.77578367,0.63099897,0,0)"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
style="fill: #27A7E7 ;stroke-width:0.24961"
|
||||||
|
id="rect5-7"
|
||||||
|
width="7.4819207"
|
||||||
|
height="0.3809379"
|
||||||
|
x="-3.9267058"
|
||||||
|
y="5.7988153"
|
||||||
|
transform="matrix(-0.70756824,0.70664502,0.70756824,0.70664502,0,0)"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
style="fill: #27A7E7 ;stroke-width:0.134869"
|
||||||
|
id="path5-8"
|
||||||
|
cx="1.5875"
|
||||||
|
cy="6.8791666"
|
||||||
|
r="1.0583333"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
style="fill: #27A7E7 ;stroke-width:0.168586"
|
||||||
|
id="path5-8-5"
|
||||||
|
cx="7.1437502"
|
||||||
|
cy="7.1437502"
|
||||||
|
r="1.3229166"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
style="fill: #27A7E7 ;stroke-width:0.118011"
|
||||||
|
id="path5-8-5-1"
|
||||||
|
cx="1.4552083"
|
||||||
|
cy="2.5135417"
|
||||||
|
r="0.92604166"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
style="fill: #27A7E7 ;stroke-width:0.101152"
|
||||||
|
id="path5-8-5-1-7"
|
||||||
|
cx="7.1437502"
|
||||||
|
cy="1.3229166"
|
||||||
|
r="0.79374999"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
style="fill: #F36D3A; stroke-width:0.23602"
|
||||||
|
id="path5"
|
||||||
|
cx="3.96875"
|
||||||
|
cy="4.4979167"
|
||||||
|
r="1.8520833"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -233,4 +233,4 @@ export default defineConfig((ctx) => {
|
|||||||
extraScripts: []
|
extraScripts: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|||||||
39
src/App.vue
@@ -3,14 +3,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inject, onMounted, ref } from 'vue'
|
import { inject, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from 'stores/auth'
|
import { useAuthStore } from 'stores/auth'
|
||||||
import { useSettingsStore } from 'stores/settings'
|
import { useSettingsStore } from 'stores/settings'
|
||||||
import { useQuasar } from 'quasar'
|
import { useQuasar } from 'quasar'
|
||||||
import type { WebApp } from '@twa-dev/types'
|
import type { WebApp } from '@twa-dev/types'
|
||||||
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const tg = inject('tg') as WebApp
|
const tg = inject('tg') as WebApp
|
||||||
|
|
||||||
@@ -30,39 +29,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseIdString (input: string): { id: number; taskId?: number; meetingId?: number } | null {
|
onMounted(async () => {
|
||||||
const pattern = /^p(?<id>\d+)(?:t(?<taskId>\d+))?(?:m(?<meetingId>\d+))?$/
|
try {
|
||||||
const match = input.match(pattern)
|
|
||||||
|
|
||||||
if (!match?.groups?.id) return null
|
|
||||||
|
|
||||||
const id = parseInt(match.groups.id, 10)
|
|
||||||
const taskId = match.groups.taskId ? parseInt(match.groups.taskId, 10) : undefined
|
|
||||||
const meetingId = match.groups.meetingId ? parseInt(match.groups.meetingId, 10) : undefined
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
...(taskId !== undefined && { taskId }),
|
|
||||||
...(meetingId !== undefined && { meetingId })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
const startRouteInfo = ref<{ id: number; taskId?: number; meetingId?: number } | null>(null)
|
if (tg) {
|
||||||
if (tg.initDataUnsafe.start_param) {
|
|
||||||
startRouteInfo.value = parseIdString(tg.initDataUnsafe.start_param)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
if (startRouteInfo.value) authStore.setStartRouteInfo(startRouteInfo.value)
|
|
||||||
if (!authStore.isInit) await authStore.init(tg)
|
if (!authStore.isInit) await authStore.init(tg)
|
||||||
if (!settingsStore.isInit) await settingsStore.init()
|
if (!settingsStore.isInit) await settingsStore.init()
|
||||||
} catch {
|
}
|
||||||
// await router.push({ name: 'server_error'})
|
} catch (error) {
|
||||||
|
console.error('App initialization failed:', error)
|
||||||
|
alert(error)
|
||||||
|
await router.push({ name: '404' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
124
src/components/BaseLogo.vue
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex row items-center no-wrap logo-component"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="iconcolor"
|
||||||
|
viewBox="0 0 8.4666662 8.4666662"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<defs id="defs1" />
|
||||||
|
<g id="layer1">
|
||||||
|
<rect
|
||||||
|
class="fill-brand"
|
||||||
|
style="stroke-width:0.233149"
|
||||||
|
id="rect5"
|
||||||
|
width="6.9885192"
|
||||||
|
height="0.35581663"
|
||||||
|
x="3.114475"
|
||||||
|
y="0.86827624"
|
||||||
|
transform="matrix(0.77578367,0.63099897,-0.77578367,0.63099897,0,0)"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
class="fill-brand"
|
||||||
|
style="stroke-width:0.24961"
|
||||||
|
id="rect5-7"
|
||||||
|
width="7.4819207"
|
||||||
|
height="0.3809379"
|
||||||
|
x="-3.9267058"
|
||||||
|
y="5.7988153"
|
||||||
|
transform="matrix(-0.70756824,0.70664502,0.70756824,0.70664502,0,0)"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
class="fill-brand"
|
||||||
|
style="stroke-width:0.134869"
|
||||||
|
id="path5-8"
|
||||||
|
cx="1.5875"
|
||||||
|
cy="6.8791666"
|
||||||
|
r="1.0583333"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
class="fill-brand"
|
||||||
|
style="stroke-width:0.168586"
|
||||||
|
id="path5-8-5"
|
||||||
|
cx="7.1437502"
|
||||||
|
cy="7.1437502"
|
||||||
|
r="1.3229166"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
class="fill-brand"
|
||||||
|
style="stroke-width:0.118011"
|
||||||
|
id="path5-8-5-1"
|
||||||
|
cx="1.4552083"
|
||||||
|
cy="2.5135417"
|
||||||
|
r="0.92604166"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
class="fill-brand"
|
||||||
|
style="stroke-width:0.101152"
|
||||||
|
id="path5-8-5-1-7"
|
||||||
|
cx="7.1437502"
|
||||||
|
cy="1.3229166"
|
||||||
|
r="0.79374999"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
style="stroke-width:0.23602"
|
||||||
|
id="path5"
|
||||||
|
cx="3.96875"
|
||||||
|
cy="4.4979167"
|
||||||
|
r="1.8520833"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="text-brand"
|
||||||
|
style="margin-right: 0.075em;"
|
||||||
|
>
|
||||||
|
tg
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="text-brand2 text-bold">
|
||||||
|
Crew
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.logo-component {
|
||||||
|
svg {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
margin-right: 0.125em;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-brand {
|
||||||
|
fill: $brand;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
100%,
|
||||||
|
0% {
|
||||||
|
fill: $brand2;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
fill: $brand2;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#path5 {
|
||||||
|
animation: blink 3s infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<q-btn
|
<q-btn
|
||||||
rounded color="primary"
|
rounded color="primary"
|
||||||
class="w100 q-mt-md q-mb-xs"
|
class="w100 q-mt-md q-mb-xs fix-disabled-btn"
|
||||||
:disable="!(isFormValid && (isDirty(initialMeeting, modelValue) || newFiles.length !== 0))"
|
:disable="!(isFormValid && (isDirty(initialMeeting, modelValue) || newFiles.length !== 0))"
|
||||||
@click = "emit('update', newFiles)"
|
@click = "emit('update', newFiles)"
|
||||||
>
|
>
|
||||||
|
|||||||
87
src/components/pnActionBar.vue
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex row q-mb-sm q-mt-md q-mx-sm justify-between items-center no-wrap">
|
||||||
|
<q-btn
|
||||||
|
v-if="showCalendarBtn"
|
||||||
|
flat round
|
||||||
|
:color="calendarActive ? 'primary' : 'grey'"
|
||||||
|
@click="emit('toggle-calendar')"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<q-icon name="mdi-calendar-month-outline" size="sm"/>
|
||||||
|
<q-badge
|
||||||
|
color="red"
|
||||||
|
rounded
|
||||||
|
floating
|
||||||
|
transparent
|
||||||
|
style="position: relative; top: -6px; margin-left: -12px"
|
||||||
|
:style="{ opacity: calendarBadge ? 0.8 : 0 }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
v-model="search"
|
||||||
|
clearable
|
||||||
|
clear-icon="close"
|
||||||
|
borderless
|
||||||
|
filled
|
||||||
|
:placeholder="$t(placeholder)"
|
||||||
|
dense
|
||||||
|
class="col-grow q-pt-xs q-mx-sm"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="mdi-magnify" color="grey"/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
v-if="showFilterBtn"
|
||||||
|
@click="emit('open-filters')"
|
||||||
|
flat round
|
||||||
|
:color="filterActive ? 'primary' : 'grey'"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<q-icon name="mdi-filter-outline" size="sm"/>
|
||||||
|
<q-badge
|
||||||
|
color="red"
|
||||||
|
rounded
|
||||||
|
floating
|
||||||
|
transparent
|
||||||
|
style="position: relative; top: -6px; margin-left: -12px"
|
||||||
|
:style="{ opacity: filterBadge ? 0.8 : 0 }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const search = defineModel<string>({
|
||||||
|
required: true,
|
||||||
|
default: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: 'Search...'
|
||||||
|
},
|
||||||
|
showCalendarBtn: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
showFilterBtn: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
calendarActive: Boolean,
|
||||||
|
filterActive: Boolean,
|
||||||
|
calendarBadge: Boolean,
|
||||||
|
filterBadge: Boolean
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'toggle-calendar',
|
||||||
|
'open-filters'
|
||||||
|
])
|
||||||
|
</script>
|
||||||
125
src/components/pnItemBtmDialog.vue
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<q-item
|
||||||
|
@click="showDialog=true"
|
||||||
|
clickable
|
||||||
|
v-ripple
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar
|
||||||
|
rounded
|
||||||
|
text-color="white"
|
||||||
|
:icon
|
||||||
|
:color="iconColor"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>
|
||||||
|
{{ $t(title) }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label caption>
|
||||||
|
<slot name="value"/>
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<q-icon name="mdi-chevron-right" color="grey"/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-dialog
|
||||||
|
v-model="showDialog"
|
||||||
|
maximized
|
||||||
|
transition-show="slide-up"
|
||||||
|
transition-hide="slide-down"
|
||||||
|
position="bottom"
|
||||||
|
>
|
||||||
|
<q-card
|
||||||
|
class="fix-card-width flex column no-scroll no-wrap q-px-none"
|
||||||
|
style="
|
||||||
|
border-top-left-radius: var(--top-raduis) !important;
|
||||||
|
border-top-right-radius: var(--top-raduis) !important;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref="cardHeaderRef"
|
||||||
|
class="flex items-center no-wrap justify-between w100 q-my-none q-pa-md"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class="flex column q-mx-xs">
|
||||||
|
<span class="text-h6 ellipsis">{{ $t(title) }}</span>
|
||||||
|
<span v-if="caption" class="text-grey text-caption">{{ $t(caption) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between no-wrap">
|
||||||
|
<q-btn
|
||||||
|
icon="mdi-close"
|
||||||
|
@click="showDialog=false"
|
||||||
|
flat round
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref="cardBodyRef"
|
||||||
|
class="q-px-none q-ma-none"
|
||||||
|
>
|
||||||
|
<pn-shadow-scroll
|
||||||
|
:hideShadows="false"
|
||||||
|
:height="bodyHeight"
|
||||||
|
>
|
||||||
|
<div ref="cardBodyInnerRef" class="q-px-md q-ma-none">
|
||||||
|
<q-resize-observer @resize="updateDimensions" />
|
||||||
|
<slot/>
|
||||||
|
</div>
|
||||||
|
</pn-shadow-scroll>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref="cardFooterRef"
|
||||||
|
class="q-pa-md"
|
||||||
|
>
|
||||||
|
<slot name="footer"/>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useSlots } from 'vue'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
title: string
|
||||||
|
caption?: string
|
||||||
|
icon: string
|
||||||
|
iconColor: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const showDialog=ref<boolean>(false)
|
||||||
|
|
||||||
|
const slots = useSlots()
|
||||||
|
const cardHeaderRef = ref<HTMLElement | null>(null)
|
||||||
|
const cardFooterRef = ref<HTMLElement | null>(null)
|
||||||
|
const cardBodyRef = ref<HTMLElement | null>(null)
|
||||||
|
const cardBodyInnerRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const headerHeight = ref(0)
|
||||||
|
const footerHeight = ref(0)
|
||||||
|
const bodyInnerHeight = ref(0)
|
||||||
|
const bodyHeight = ref(0)
|
||||||
|
|
||||||
|
const updateDimensions = () => {
|
||||||
|
headerHeight.value = cardHeaderRef.value?.offsetHeight || 0
|
||||||
|
footerHeight.value = cardFooterRef.value?.offsetHeight || 0
|
||||||
|
bodyInnerHeight.value = cardBodyInnerRef.value?.offsetHeight || 0
|
||||||
|
bodyHeight.value = window.innerHeight - headerHeight.value - footerHeight.value - 48
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => slots.body?.(), updateDimensions, { flush: 'post' })
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fix-card-width {
|
||||||
|
width: var(--body-width) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
53
src/components/pnListSelector.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<q-list class="q-gutter-y-sm">
|
||||||
|
<q-btn
|
||||||
|
v-for="(option, index) in options"
|
||||||
|
:key="index"
|
||||||
|
flat
|
||||||
|
no-caps dense
|
||||||
|
class="w100"
|
||||||
|
align="left"
|
||||||
|
@click="selectItem(option)"
|
||||||
|
:class="isSelected(option.value) ? 'text-primary' : ''"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
:name="isSelected(option.value) ? 'mdi-check' : ''"
|
||||||
|
color="primary"
|
||||||
|
class="q-pr-sm"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
:class="!isSelected(option.value) ? 'text-weight-regular' : ''"
|
||||||
|
>
|
||||||
|
{{ $te(option.label) ? $t(option.label) : option.label }}
|
||||||
|
</span>
|
||||||
|
</q-btn>
|
||||||
|
</q-list>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
interface ListOption {
|
||||||
|
label: string
|
||||||
|
value: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
options: ListOption[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const model = defineModel<string | number | null>({
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const isSelected = computed(() => (value: string | number) => {
|
||||||
|
return model.value === value
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectItem = (option: ListOption) => {
|
||||||
|
model.value = option.value
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<q-btn
|
<q-btn
|
||||||
rounded color="primary"
|
rounded color="primary"
|
||||||
class="w100 q-mt-md q-mb-xs"
|
class="w100 q-mt-md q-mb-xs fix-disabled-btn"
|
||||||
@click = "emit('update', newFiles)"
|
@click = "emit('update', newFiles)"
|
||||||
:disable="!(isFormValid && (isDirty(initialTask, modelValue) || newFiles.length !== 0))"
|
:disable="!(isFormValid && (isDirty(initialTask, modelValue) || newFiles.length !== 0))"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
class="text-caption flex items-center w100"
|
class="text-caption flex items-center w100"
|
||||||
:class="'text-' + taskStatus.color"
|
:class="'text-' + taskStatus.color"
|
||||||
>
|
>
|
||||||
<q-icon :name="taskStatus.icon"/>
|
<q-icon :name="taskStatus.icon" class="q-pr-xs"/>
|
||||||
<span>
|
<span>
|
||||||
{{ $t(taskStatus.text) }}
|
{{ $t(taskStatus.text) }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
// app global css in SCSS form
|
// app global css in SCSS form
|
||||||
.text-brand {
|
.text-brand {
|
||||||
color: $green-14 !important;
|
color: $brand !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-brand {
|
.bg-brand {
|
||||||
background: $green-14 !important;
|
background: $brand !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-brand2 {
|
||||||
|
color: $brand2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-brand2 {
|
||||||
|
background: $brand2 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
$base-width: 100;
|
$base-width: 100;
|
||||||
@@ -13,10 +21,14 @@ $base-width: 100;
|
|||||||
$base-width: $base-width - 10;
|
$base-width: $base-width - 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
$base-height: 100;
|
body, html, #q-app {
|
||||||
@while $base-height > 0 {
|
font-family: $typography-font-family;
|
||||||
.h#{$base-height} { height: #{$base-height}+'%'; }
|
-webkit-font-smoothing: antialiased;
|
||||||
$base-height: $base-height - 10;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@@ -66,6 +78,20 @@ body {
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'myFont';
|
||||||
|
src: url(./fonts/Inter-Regular.woff2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fix-disabled-btn.q-btn[disabled]:not(.q-btn--flat) {
|
||||||
|
background-color: $grey-5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fix-disabled-btn.q-btn.q-btn--flat[disabled] {
|
||||||
|
color: $grey-9 !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.pn-icon {
|
.pn-icon {
|
||||||
font-family: 'pn-icon';
|
font-family: 'pn-icon';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@@ -73,7 +99,7 @@ body {
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'pn-icon';
|
font-family: 'pn-icon';
|
||||||
src: url("./fonts/pn.woff") format("woff")
|
src: url(./fonts/pn.woff) format("woff")
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-file-default:before {
|
.icon-file-default:before {
|
||||||
|
|||||||
BIN
src/css/fonts/Inter-Regular.woff2
Normal file
@@ -1,18 +1,4 @@
|
|||||||
// Quasar SCSS (& Sass) Variables
|
$primary : #27A7E7;
|
||||||
// --------------------------------------------------
|
|
||||||
// To customize the look and feel of this app, you can override
|
|
||||||
// the Sass/SCSS variables found in Quasar's source Sass/SCSS files.
|
|
||||||
|
|
||||||
// Check documentation for full list of Quasar variables
|
|
||||||
|
|
||||||
// Your own variables (that are declared here) and Quasar's own
|
|
||||||
// ones will be available out of the box in your .vue/.scss/.sass files
|
|
||||||
|
|
||||||
// It's highly recommended to change the default colors
|
|
||||||
// to match your app's branding.
|
|
||||||
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
|
||||||
|
|
||||||
$primary : #1976D2;
|
|
||||||
$secondary : #26A69A;
|
$secondary : #26A69A;
|
||||||
$accent : #9C27B0;
|
$accent : #9C27B0;
|
||||||
|
|
||||||
@@ -26,4 +12,9 @@ $warning : #F2C037;
|
|||||||
|
|
||||||
$lightgrey : #DCDCDC;
|
$lightgrey : #DCDCDC;
|
||||||
|
|
||||||
$body-font-size: var(--dynamic-font-size)
|
$brand: #27A7E7;
|
||||||
|
$brand2: #F36D3A;
|
||||||
|
|
||||||
|
$body-font-size: var(--dynamic-font-size);
|
||||||
|
|
||||||
|
$typography-font-family: 'myFont', Roboto !default;
|
||||||
|
|||||||
@@ -92,7 +92,25 @@ function parseIntString (s: string | string[] | undefined) :number | null {
|
|||||||
return regex.test(s) ? Number(s) : null
|
return regex.test(s) ? Number(s) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseStartParams (input: string): { id: number; taskId?: number; meetingId?: number } | null {
|
||||||
|
const pattern = /^p(?<id>\d+)(?:t(?<taskId>\d+))?(?:m(?<meetingId>\d+))?$/
|
||||||
|
const match = input.match(pattern)
|
||||||
|
|
||||||
|
if (!match?.groups?.id) return null
|
||||||
|
|
||||||
|
const id = parseInt(match.groups.id, 10)
|
||||||
|
const taskId = match.groups.taskId ? parseInt(match.groups.taskId, 10) : undefined
|
||||||
|
const meetingId = match.groups.meetingId ? parseInt(match.groups.meetingId, 10) : undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
...(taskId !== undefined && { taskId }),
|
||||||
|
...(meetingId !== undefined && { meetingId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
isDirty,
|
isDirty,
|
||||||
parseIntString
|
parseIntString,
|
||||||
|
parseStartParams
|
||||||
}
|
}
|
||||||
149
src/pages/AcceptTermsPage.vue
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex column w100 q-pa-none q-ma-none no-wrap" style="height: 100vh">
|
||||||
|
<div class="flex justify-center items-center w100 q-pb-md">
|
||||||
|
{{'👋 '+ $t('accept_terms__welcome_title')}}
|
||||||
|
<base-logo class="q-pa-xs"/>
|
||||||
|
!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-grow">
|
||||||
|
<q-resize-observer @resize="onResize"/>
|
||||||
|
<pn-shadow-scroll :height="sectionHeight" :hideShadows="false">
|
||||||
|
<div class="q-px-md text-caption">
|
||||||
|
<div>
|
||||||
|
{{$t('accept_terms__welcome')}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-bold q-pt-md">
|
||||||
|
{{$t('accept_terms__section_privacy_data_title')}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{$t('accept_terms__section_privacy_data')}}
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<li> {{$t('accept_terms__section_privacy_data_option1')}}</li>
|
||||||
|
<li> {{$t('accept_terms__section_privacy_data_option2')}}</li>
|
||||||
|
<li> {{$t('accept_terms__section_privacy_data_option3')}}</li>
|
||||||
|
<li> {{$t('accept_terms__section_privacy_data_option4')}}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="text-bold q-pt-md">
|
||||||
|
{{$t('accept_terms__section_choise_title')}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{$t('accept_terms__section_choise_agree')}}
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<li> {{$t('accept_terms__section_choise_agree_option1')}}</li>
|
||||||
|
<li> {{$t('accept_terms__section_choise_agree_option2')}}</li>
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
{{$t('accept_terms__section_choise_decline')}}
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<li> {{$t('accept_terms__section_choise_decline_option1')}}</li>
|
||||||
|
<li> {{$t('accept_terms__section_choise_decline_option2')}}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{$t('accept_terms__section_revoke')}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</pn-shadow-scroll>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex column w100 q-pt-md">
|
||||||
|
<div class="text-caption q-pb-md">
|
||||||
|
<div class="flex column q-gutter-y-md">
|
||||||
|
<div class="flex items-center no-wrap">
|
||||||
|
<q-checkbox v-model="agreement" val="1" dense class="q-px-sm"/>
|
||||||
|
<span>
|
||||||
|
{{$t('accept_terms__section_checkbox_agreement') + ' '}}
|
||||||
|
<a href="">{{ $t('accept_terms__section_checkbox_agreement_doc') }}</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center no-wrap">
|
||||||
|
<q-checkbox v-model="agreement" val="2" dense class="q-px-sm"/>
|
||||||
|
<span>
|
||||||
|
{{$t('accept_terms__section_checkbox_privacy') + ' '}}
|
||||||
|
<a href="">{{ $t('accept_terms__section_checkbox_privacy_doc') }}</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex no-wrap justify-center w100 q-gutter-x-md">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
color="negative"
|
||||||
|
no-caps
|
||||||
|
style="opacity: 0.8"
|
||||||
|
@click="onDecline"
|
||||||
|
class="w40"
|
||||||
|
>
|
||||||
|
{{$t('accept_terms__btn_decline')}}
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
color="primary"
|
||||||
|
no-caps
|
||||||
|
:disable="agreement.length !== 2"
|
||||||
|
@click="onSubmit"
|
||||||
|
class="w40 fix-disabled"
|
||||||
|
>
|
||||||
|
{{$t('accept_terms__btn_agree')}}
|
||||||
|
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, inject } from 'vue'
|
||||||
|
import { parseStartParams } from 'helpers/helpers'
|
||||||
|
import BaseLogo from 'components/BaseLogo.vue'
|
||||||
|
import { useAuthStore } from 'stores/auth'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import type { WebApp } from '@twa-dev/types'
|
||||||
|
const tg = inject('tg') as WebApp
|
||||||
|
|
||||||
|
function onDecline () {
|
||||||
|
tg.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const startParams = typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.start_param
|
||||||
|
? parseStartParams(window.Telegram.WebApp.initDataUnsafe.start_param)
|
||||||
|
: null
|
||||||
|
|
||||||
|
async function onSubmit () {
|
||||||
|
await authStore.termsAccepted()
|
||||||
|
const route = startParams === null
|
||||||
|
? { name: '404' }
|
||||||
|
: startParams?.taskId
|
||||||
|
? { name: 'task_info', params: { id: startParams.id, taskId: startParams.taskId }}
|
||||||
|
: startParams?.meetingId
|
||||||
|
? { name: 'meeting_info', params: { id: startParams.id, meetingId: startParams.meetingId }}
|
||||||
|
: { name: 'files', params: { id: startParams.id }}
|
||||||
|
await router.push(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
const agreement = ref([])
|
||||||
|
|
||||||
|
interface sizeParams {
|
||||||
|
height: number,
|
||||||
|
width: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const sectionHeight = ref(0)
|
||||||
|
function onResize(size: sizeParams) {
|
||||||
|
sectionHeight.value = size.height
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -3,78 +3,70 @@
|
|||||||
<template #title>
|
<template #title>
|
||||||
{{ $t('settings__title') }}
|
{{ $t('settings__title') }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<pn-scroll-list>
|
<pn-scroll-list>
|
||||||
<q-list separator>
|
<q-list separator>
|
||||||
<q-item>
|
<pn-item-btm-dialog
|
||||||
<q-item-section avatar>
|
title="settings__language"
|
||||||
<q-avatar color="primary" rounded text-color="white" icon="mdi-translate" size="md" />
|
icon="mdi-translate"
|
||||||
</q-item-section>
|
iconColor="primary"
|
||||||
<q-item-section>
|
>
|
||||||
<span>{{ $t('settings__language') }}</span>
|
<template #value>
|
||||||
</q-item-section>
|
{{ localeOptions.find(el => el.value === locale)?.label }}
|
||||||
<q-item-section>
|
</template>
|
||||||
<q-select
|
<pn-list-selector
|
||||||
class="fix-input-right text-body1"
|
|
||||||
v-model="locale"
|
v-model="locale"
|
||||||
:options="localeOptions"
|
:options="localeOptions"
|
||||||
dense
|
|
||||||
borderless
|
|
||||||
emit-value
|
|
||||||
map-options
|
|
||||||
hide-bottom-space
|
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</pn-item-btm-dialog>
|
||||||
</q-item>
|
|
||||||
<q-item>
|
<pn-item-btm-dialog
|
||||||
<q-item-section avatar>
|
title="settings__font_size"
|
||||||
<q-avatar color="primary" rounded text-color="white" icon="mdi-format-size" size="md" />
|
icon="mdi-format-size"
|
||||||
</q-item-section>
|
iconColor="primary"
|
||||||
<q-item-section>
|
>
|
||||||
<span>{{ $t('settings__font_size') }}</span>
|
<template #value>
|
||||||
</q-item-section>
|
{{ $t(fontSizeLabel) }}
|
||||||
<q-item-section>
|
</template>
|
||||||
<div class="flex justify-end">
|
<pn-list-selector
|
||||||
<q-btn
|
v-model="fontSize"
|
||||||
@click="settingsStore.decreaseFontSize()"
|
:options="fontSizeOptions"
|
||||||
color="negative" flat
|
|
||||||
icon="mdi-format-font-size-decrease"
|
|
||||||
class="q-pa-sm q-mx-xs"
|
|
||||||
:disable="!settingsStore.canDecrease"
|
|
||||||
/>
|
/>
|
||||||
<q-btn
|
</pn-item-btm-dialog>
|
||||||
@click="settingsStore.increaseFontSize()"
|
|
||||||
color="positive" flat
|
|
||||||
icon="mdi-format-font-size-increase"
|
|
||||||
class="q-pa-sm q-mx-xs"
|
|
||||||
:disable="!settingsStore.canIncrease"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
</q-list>
|
||||||
</pn-scroll-list>
|
</pn-scroll-list>
|
||||||
</pn-page-card>
|
</pn-page-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, watch, ref, onMounted } from 'vue'
|
||||||
import { useSettingsStore } from 'stores/settings'
|
import { useSettingsStore } from 'stores/settings'
|
||||||
|
import pnItemBtmDialog from 'components/pnItemBtmDialog.vue'
|
||||||
|
import pnListSelector from 'components/pnListSelector.vue'
|
||||||
|
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
|
const locale = ref('')
|
||||||
const localeOptions = settingsStore.supportLocale
|
const localeOptions = settingsStore.supportLocale
|
||||||
|
|
||||||
const locale = computed({
|
watch(locale, async (newValue) => {
|
||||||
get: () => settingsStore.settings.locale,
|
await settingsStore.updateSettings({ locale: newValue })
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
||||||
set: (value: string) => settingsStore.updateLocale(value)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const fontSize = ref(16)
|
||||||
|
const fontSizeOptions = settingsStore.supportFontSizes
|
||||||
|
const fontSizeLabel = computed(() =>
|
||||||
|
fontSizeOptions.find(el => el.value === fontSize.value)?.label ?? ''
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(fontSize, async (newValue) => {
|
||||||
|
await settingsStore.updateSettings({ fontSize: newValue })
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
locale.value = settingsStore.settings.locale
|
||||||
|
fontSize.value = settingsStore.settings.fontSize
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.fix-input-right :deep(.q-field__native) {
|
|
||||||
justify-content: end;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -45,11 +45,15 @@
|
|||||||
readonly
|
readonly
|
||||||
filled
|
filled
|
||||||
class="w100"
|
class="w100"
|
||||||
:label = "$t('user_card__' + key)"
|
:label = "(key!=='email' && key!=='phone') ? $t('user_card__' + key) : undefined"
|
||||||
>
|
>
|
||||||
<template #control>
|
<template #control>
|
||||||
{{displayUser[key]}}
|
{{displayUser[key]}}
|
||||||
</template>
|
</template>
|
||||||
|
<template #prepend v-if="(key==='email' || key==='phone')">
|
||||||
|
<q-icon v-if="key==='email'" name="mdi-email-outline"/>
|
||||||
|
<q-icon v-if="key==='phone'" name="mdi-phone-outline"/>
|
||||||
|
</template>
|
||||||
</q-field>
|
</q-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -73,15 +77,13 @@
|
|||||||
const userId = parseIntString(route.params.userId)
|
const userId = parseIntString(route.params.userId)
|
||||||
const user = computed(() => userId && usersStore.userById(userId))
|
const user = computed(() => userId && usersStore.userById(userId))
|
||||||
|
|
||||||
const tname = computed(() => {
|
const tname = computed(() =>
|
||||||
return (!user.value)
|
user.value
|
||||||
? ''
|
? [user.value?.firstname, user.value?.lastname]
|
||||||
: user.value.firstname
|
.filter(Boolean)
|
||||||
? user.value.lastname
|
.join(' ')
|
||||||
? user.value.firstname + ' ' + user.value.lastname
|
: ''
|
||||||
: user.value.firstname
|
)
|
||||||
: user.value.lastname ?? ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const userPosition = computed(() => {
|
const userPosition = computed(() => {
|
||||||
return (!user.value)
|
return (!user.value)
|
||||||
|
|||||||
@@ -2,21 +2,12 @@
|
|||||||
<div class="q-pa-none flex column col-grow no-scroll">
|
<div class="q-pa-none flex column col-grow no-scroll">
|
||||||
<pn-scroll-list>
|
<pn-scroll-list>
|
||||||
<template #card-body-header>
|
<template #card-body-header>
|
||||||
|
<pn-action-bar
|
||||||
<div class="flex row q-ma-md justify-between">
|
|
||||||
<q-input
|
|
||||||
v-model="search"
|
v-model="search"
|
||||||
clearable
|
placeholder="chats__search"
|
||||||
clear-icon="close"
|
:show-filter-btn="false"
|
||||||
:placeholder="$t('chats__search')"
|
:show-calendar-btn="false"
|
||||||
dense
|
/>
|
||||||
class="col-grow"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="mdi-magnify" />
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<q-list separator>
|
<q-list separator>
|
||||||
<q-item
|
<q-item
|
||||||
@@ -59,6 +50,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, inject } from 'vue'
|
import { ref, computed, inject } from 'vue'
|
||||||
import { useChatsStore } from 'stores/chats'
|
import { useChatsStore } from 'stores/chats'
|
||||||
|
import pnActionBar from 'components/pnActionBar.vue'
|
||||||
import type { WebApp } from '@twa-dev/types'
|
import type { WebApp } from '@twa-dev/types'
|
||||||
const tg = inject('tg') as WebApp
|
const tg = inject('tg') as WebApp
|
||||||
|
|
||||||
|
|||||||
@@ -2,60 +2,16 @@
|
|||||||
<div class="q-pa-none flex column col-grow no-scroll">
|
<div class="q-pa-none flex column col-grow no-scroll">
|
||||||
<pn-scroll-list>
|
<pn-scroll-list>
|
||||||
<template #card-body-header>
|
<template #card-body-header>
|
||||||
|
<pn-action-bar
|
||||||
<div class="flex row q-mb-xs q-mt-md q-mx-sm justify-between">
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-calendar-month-outline"
|
|
||||||
flat dense round
|
|
||||||
class="q-mr-sm"
|
|
||||||
size="lg"
|
|
||||||
:color="showCalendar ? 'primary' : 'grey'"
|
|
||||||
@click="showCalendar = !showCalendar"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<q-badge
|
|
||||||
color="red"
|
|
||||||
rounded
|
|
||||||
floating
|
|
||||||
transparent
|
|
||||||
style="position: relative; top: -16px; margin-left: -12px"
|
|
||||||
:style="{ opacity: datesRange ? 0.8 : 0 }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-btn>
|
|
||||||
<q-input
|
|
||||||
v-model="search"
|
v-model="search"
|
||||||
clearable
|
placeholder="files__search"
|
||||||
clear-icon="close"
|
:calendar-active="showCalendar"
|
||||||
:placeholder="$t('files__search')"
|
:filter-active="showFiltersDialog"
|
||||||
dense
|
:calendar-badge="!!datesRange"
|
||||||
class="col-grow"
|
:filter-badge="!checkFiltersSelect"
|
||||||
>
|
@toggle-calendar="showCalendar = !showCalendar"
|
||||||
<template #prepend>
|
@open-filters="showFiltersDialog = true"
|
||||||
<q-icon name="mdi-magnify" />
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
@click="showFiltersDialog = true"
|
|
||||||
icon="mdi-filter-outline"
|
|
||||||
dense round flat
|
|
||||||
size="lg"
|
|
||||||
:color="showFiltersDialog ? 'primary' : 'grey'"
|
|
||||||
class="q-mr-xs"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<q-badge
|
|
||||||
color="red"
|
|
||||||
rounded
|
|
||||||
floating
|
|
||||||
transparent
|
|
||||||
style="position: relative; top: -16px; margin-left: -12px"
|
|
||||||
:style="{ opacity: !checkFiltersSelect ? 0.8 : 0 }"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
<q-slide-transition>
|
<q-slide-transition>
|
||||||
<div v-show="showCalendar">
|
<div v-show="showCalendar">
|
||||||
<q-date
|
<q-date
|
||||||
@@ -241,6 +197,7 @@
|
|||||||
import { useFilesStore } from 'stores/files'
|
import { useFilesStore } from 'stores/files'
|
||||||
import { useUsersStore } from 'stores/users'
|
import { useUsersStore } from 'stores/users'
|
||||||
import { useChatsStore } from 'stores/chats'
|
import { useChatsStore } from 'stores/chats'
|
||||||
|
import pnActionBar from 'components/pnActionBar.vue'
|
||||||
import { date } from 'quasar'
|
import { date } from 'quasar'
|
||||||
import { parseFileName, fileIcon, fileSize } from 'helpers/files-functions'
|
import { parseFileName, fileIcon, fileSize } from 'helpers/files-functions'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|||||||
@@ -2,41 +2,14 @@
|
|||||||
<div class="q-pa-none flex column col-grow no-scroll">
|
<div class="q-pa-none flex column col-grow no-scroll">
|
||||||
<pn-scroll-list>
|
<pn-scroll-list>
|
||||||
<template #card-body-header>
|
<template #card-body-header>
|
||||||
|
<pn-action-bar
|
||||||
<div class="flex row q-mb-xs q-mt-md q-mx-sm justify-between">
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-calendar-month-outline"
|
|
||||||
flat dense round
|
|
||||||
class="q-mr-sm"
|
|
||||||
size="lg"
|
|
||||||
:color="showCalendar ? 'primary' : 'grey'"
|
|
||||||
@click="showCalendar = !showCalendar"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<q-badge
|
|
||||||
color="red"
|
|
||||||
rounded
|
|
||||||
floating
|
|
||||||
transparent
|
|
||||||
style="position: relative; top: -16px; margin-left: -12px"
|
|
||||||
:style="{ opacity: datesRange ? 0.8 : 0 }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-btn>
|
|
||||||
<q-input
|
|
||||||
v-model="search"
|
v-model="search"
|
||||||
clearable
|
placeholder="meetings__search"
|
||||||
clear-icon="close"
|
:calendar-active="showCalendar"
|
||||||
:placeholder="$t('meetings__search')"
|
:calendar-badge="!!datesRange"
|
||||||
dense
|
@toggle-calendar="showCalendar = !showCalendar"
|
||||||
class="col-grow"
|
:show-filter-btn="false"
|
||||||
>
|
/>
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="mdi-magnify" />
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-slide-transition>
|
<q-slide-transition>
|
||||||
<div v-show="showCalendar">
|
<div v-show="showCalendar">
|
||||||
<q-date
|
<q-date
|
||||||
@@ -65,7 +38,7 @@
|
|||||||
<q-btn flat dense no-caps v-if ="showReset" @click="resetDisplayPreviousMeetings">
|
<q-btn flat dense no-caps v-if ="showReset" @click="resetDisplayPreviousMeetings">
|
||||||
<div class="flex items-center text-caption text-grey">
|
<div class="flex items-center text-caption text-grey">
|
||||||
{{$t('meetings__previous_hide')}}
|
{{$t('meetings__previous_hide')}}
|
||||||
<q-icon name="close" size="xs"/>
|
<q-icon name="mdi-close" size="xs"/>
|
||||||
</div>
|
</div>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
@@ -227,6 +200,7 @@
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import type { Meeting } from 'types/Meeting'
|
import type { Meeting } from 'types/Meeting'
|
||||||
import { date } from 'quasar'
|
import { date } from 'quasar'
|
||||||
|
import pnActionBar from 'components/pnActionBar.vue'
|
||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const showCalendar = ref<boolean>(false)
|
const showCalendar = ref<boolean>(false)
|
||||||
|
|||||||
@@ -2,60 +2,16 @@
|
|||||||
<div class="q-pa-none flex column col-grow no-scroll">
|
<div class="q-pa-none flex column col-grow no-scroll">
|
||||||
<pn-scroll-list>
|
<pn-scroll-list>
|
||||||
<template #card-body-header>
|
<template #card-body-header>
|
||||||
|
<pn-action-bar
|
||||||
<div class="flex row q-mb-xs q-mt-md q-mx-sm justify-between">
|
|
||||||
<q-btn
|
|
||||||
icon="mdi-calendar-month-outline"
|
|
||||||
flat dense round
|
|
||||||
class="q-mr-sm"
|
|
||||||
size="lg"
|
|
||||||
:color="showCalendar ? 'primary' : 'grey'"
|
|
||||||
@click="showCalendar = !showCalendar"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<q-badge
|
|
||||||
color="red"
|
|
||||||
rounded
|
|
||||||
floating
|
|
||||||
transparent
|
|
||||||
style="position: relative; top: -16px; margin-left: -12px"
|
|
||||||
:style="{ opacity: datesRange ? 0.8 : 0 }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</q-btn>
|
|
||||||
<q-input
|
|
||||||
v-model="search"
|
v-model="search"
|
||||||
clearable
|
placeholder="tasks__search"
|
||||||
clear-icon="close"
|
:calendar-active="showCalendar"
|
||||||
:placeholder="$t('tasks__search')"
|
:filter-active="showFiltersDialog"
|
||||||
dense
|
:calendar-badge="!!datesRange"
|
||||||
class="col-grow"
|
:filter-badge="!checkFiltersSelect"
|
||||||
>
|
@toggle-calendar="showCalendar = !showCalendar"
|
||||||
<template #prepend>
|
@open-filters="showFiltersDialog = true"
|
||||||
<q-icon name="mdi-magnify" />
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
@click="showFiltersDialog = true"
|
|
||||||
icon="mdi-filter-outline"
|
|
||||||
dense round flat
|
|
||||||
size="lg"
|
|
||||||
:color="showFiltersDialog ? 'primary' : 'grey'"
|
|
||||||
class="q-mr-xs"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<q-badge
|
|
||||||
color="red"
|
|
||||||
rounded
|
|
||||||
floating
|
|
||||||
transparent
|
|
||||||
style="position: relative; top: -16px; margin-left: -12px"
|
|
||||||
:style="{ opacity: !checkFiltersSelect ? 0.8 : 0 }"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
<q-slide-transition>
|
<q-slide-transition>
|
||||||
<div v-show="showCalendar">
|
<div v-show="showCalendar">
|
||||||
<q-date
|
<q-date
|
||||||
@@ -236,6 +192,7 @@
|
|||||||
import taskItem from 'components/taskItem.vue'
|
import taskItem from 'components/taskItem.vue'
|
||||||
import type { Task } from 'types/Task'
|
import type { Task } from 'types/Task'
|
||||||
import { date } from 'quasar'
|
import { date } from 'quasar'
|
||||||
|
import pnActionBar from 'components/pnActionBar.vue'
|
||||||
|
|
||||||
const search = ref('')
|
const search = ref('')
|
||||||
const showCalendar = ref<boolean>(false)
|
const showCalendar = ref<boolean>(false)
|
||||||
|
|||||||
@@ -2,20 +2,12 @@
|
|||||||
<div class="q-pa-none flex column col-grow no-scroll">
|
<div class="q-pa-none flex column col-grow no-scroll">
|
||||||
<pn-scroll-list>
|
<pn-scroll-list>
|
||||||
<template #card-body-header>
|
<template #card-body-header>
|
||||||
<div class="flex row q-ma-md justify-between">
|
<pn-action-bar
|
||||||
<q-input
|
|
||||||
v-model="search"
|
v-model="search"
|
||||||
clearable
|
placeholder="users__search"
|
||||||
clear-icon="close"
|
:show-filter-btn="false"
|
||||||
:placeholder="$t('users__search')"
|
:show-calendar-btn="false"
|
||||||
dense
|
/>
|
||||||
class="col-grow"
|
|
||||||
>
|
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="mdi-magnify" />
|
|
||||||
</template>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<q-list separator>
|
<q-list separator>
|
||||||
@@ -57,6 +49,7 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useUsersStore } from 'stores/users'
|
import { useUsersStore } from 'stores/users'
|
||||||
|
import pnActionBar from 'components/pnActionBar.vue'
|
||||||
import type { User } from 'types/User'
|
import type { User } from 'types/User'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -8,12 +8,17 @@ import {
|
|||||||
import routes from './routes'
|
import routes from './routes'
|
||||||
import { useProjectsStore } from 'stores/projects'
|
import { useProjectsStore } from 'stores/projects'
|
||||||
import { useAuthStore } from 'stores/auth'
|
import { useAuthStore } from 'stores/auth'
|
||||||
|
import { parseStartParams } from 'helpers/helpers'
|
||||||
|
|
||||||
export default defineRouter(function (/* { store, ssrContext } */) {
|
export default defineRouter(function (/* { store, ssrContext } */) {
|
||||||
const createHistory = process.env.SERVER
|
const createHistory = process.env.SERVER
|
||||||
? createMemoryHistory
|
? createMemoryHistory
|
||||||
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)
|
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)
|
||||||
|
|
||||||
|
const startRouteInfo = typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.start_param
|
||||||
|
? parseStartParams(window.Telegram.WebApp.initDataUnsafe.start_param)
|
||||||
|
: null
|
||||||
|
|
||||||
const Router = createRouter({
|
const Router = createRouter({
|
||||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||||
routes,
|
routes,
|
||||||
@@ -21,25 +26,26 @@ export default defineRouter(function (/* { store, ssrContext } */) {
|
|||||||
// Leave this as is and make changes in quasar.conf.js instead!
|
// Leave this as is and make changes in quasar.conf.js instead!
|
||||||
// quasar.conf.js -> build -> vueRouterMode
|
// quasar.conf.js -> build -> vueRouterMode
|
||||||
// quasar.conf.js -> build -> publicPath
|
// quasar.conf.js -> build -> publicPath
|
||||||
history: createHistory(process.env.VUE_ROUTER_BASE),
|
history: createHistory(process.env.VUE_ROUTER_BASE)
|
||||||
})
|
})
|
||||||
|
|
||||||
Router.beforeEach(async (to) => {
|
Router.beforeEach(async (to) => {
|
||||||
|
console.log(window.Telegram.WebApp.initDataUnsafe.start_param, startRouteInfo)
|
||||||
if (to.name === 'settings') return
|
console.log(112, to)
|
||||||
if (to.name === '404') return
|
if (to.name === 'settings' || to.name === '404' || to.name === 'accept-terms') return true
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const projectsStore = useProjectsStore()
|
const projectsStore = useProjectsStore()
|
||||||
|
|
||||||
|
if (!authStore.isInit) await authStore.init(window.Telegram.WebApp)
|
||||||
|
// if (!authStore.isTermsAccepted) return { name: 'accept-terms' }
|
||||||
|
|
||||||
if (authStore.startRouteInfo && to.path === '/') {
|
if (to.path === '/' && startRouteInfo) {
|
||||||
const { id, taskId, meetingId } = authStore.startRouteInfo
|
console.log(222, startRouteInfo)
|
||||||
authStore.setStartRouteInfo(null)
|
const { id, taskId, meetingId } = startRouteInfo
|
||||||
|
|
||||||
if (!projectsStore.isInit) await projectsStore.init()
|
if (!projectsStore.isInit) await projectsStore.init()
|
||||||
const project = projectsStore.projectById(id)
|
|
||||||
|
|
||||||
|
const project = projectsStore.projectById(id)
|
||||||
if (!project) return { name: '404' }
|
if (!project) return { name: '404' }
|
||||||
|
|
||||||
return taskId
|
return taskId
|
||||||
|
|||||||
@@ -85,6 +85,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'accept-terms',
|
||||||
|
path: '/accept-terms-of-use',
|
||||||
|
component: () => import('pages/AcceptTermsPage.vue'),
|
||||||
|
meta: { hideBackButton: true }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '404',
|
name: '404',
|
||||||
path: '/:catchAll(.*)*',
|
path: '/:catchAll(.*)*',
|
||||||
|
|||||||
@@ -5,25 +5,32 @@ import type { WebApp } from '@twa-dev/types'
|
|||||||
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
const isInit = ref(false)
|
const isInit = ref(false)
|
||||||
|
const isTermsAccepted = ref(false)
|
||||||
const telegramUserData = ref()
|
const telegramUserData = ref()
|
||||||
|
|
||||||
async function init (tg: WebApp) {
|
async function init (tg: WebApp) {
|
||||||
await api.post('/auth?' + tg?.initData)
|
const { data } = await api.post('/auth?' + tg?.initData)
|
||||||
telegramUserData.value = tg?.initDataUnsafe.user
|
telegramUserData.value = tg?.initDataUnsafe.user
|
||||||
|
isTermsAccepted.value = data.data?.is_terms_accepted
|
||||||
isInit.value = true
|
isInit.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const startRouteInfo = ref<{ id: number; taskId?: number; meetingId?: number } | null>(null)
|
async function termsAccepted () {
|
||||||
|
const { data } = await api.post('/terms/accept')
|
||||||
|
if (data.success) isTermsAccepted.value = true
|
||||||
|
}
|
||||||
|
|
||||||
function setStartRouteInfo (info: { id: number; taskId?: number; meetingId?: number } | null) {
|
async function termsRevoked () {
|
||||||
startRouteInfo.value = info
|
const { data } = await api.post('/terms/revoke')
|
||||||
|
if (data.success) isTermsAccepted.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isInit,
|
isInit,
|
||||||
|
isTermsAccepted,
|
||||||
telegramUserData,
|
telegramUserData,
|
||||||
startRouteInfo,
|
init,
|
||||||
setStartRouteInfo,
|
termsAccepted,
|
||||||
init
|
termsRevoked
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ interface AppSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultFontSize = 16
|
const defaultFontSize = 16
|
||||||
const minFontSize = 10
|
|
||||||
const maxFontSize = 22
|
|
||||||
const fontSizeStep = 2
|
|
||||||
|
|
||||||
const defaultSettings: AppSettings = {
|
const defaultSettings: AppSettings = {
|
||||||
fontSize: defaultFontSize,
|
fontSize: defaultFontSize,
|
||||||
@@ -29,14 +26,18 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
const isInit = ref(false)
|
const isInit = ref(false)
|
||||||
|
|
||||||
const currentFontSize = computed(() => settings.value?.fontSize ?? defaultFontSize)
|
const currentFontSize = computed(() => settings.value?.fontSize ?? defaultFontSize)
|
||||||
const canIncrease = computed(() => currentFontSize.value < maxFontSize)
|
|
||||||
const canDecrease = computed(() => currentFontSize.value > minFontSize)
|
|
||||||
|
|
||||||
const supportLocale = [
|
const supportLocale = [
|
||||||
{ value: 'en-US', label: 'English' },
|
{ value: 'en-US', label: 'English' },
|
||||||
{ value: 'ru-RU', label: 'Русский' }
|
{ value: 'ru-RU', label: 'Русский' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const supportFontSizes = [
|
||||||
|
{ value: 12, label: 'settings__fontsize_small' },
|
||||||
|
{ value: 16, label: 'settings__fontsize_medium' },
|
||||||
|
{ value: 20, label: 'settings__fontsize_large' }
|
||||||
|
]
|
||||||
|
|
||||||
const quasarLangMap: Record<string, string> = {
|
const quasarLangMap: Record<string, string> = {
|
||||||
'en-US': 'en-US',
|
'en-US': 'en-US',
|
||||||
'ru-RU': 'ru'
|
'ru-RU': 'ru'
|
||||||
@@ -89,32 +90,12 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateLocale = async (newLocale: string) => {
|
|
||||||
if (i18nLocale) {
|
|
||||||
i18nLocale.value = newLocale
|
|
||||||
await updateQuasarLang(newLocale)
|
|
||||||
settings.value.locale = newLocale
|
|
||||||
await saveSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveSettings = async () => {
|
|
||||||
await api.put('/settings', { settings: settings.value })
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSettings = async (newSettings: Partial<AppSettings>) => {
|
|
||||||
settings.value = { ...settings.value, ...newSettings }
|
|
||||||
updateCssVariable()
|
|
||||||
await applyLocale()
|
|
||||||
await saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get('/settings')
|
const { data } = await api.get('/settings')
|
||||||
settings.value = {
|
settings.value = {
|
||||||
fontSize: data.data.settings.fontSize || defaultSettings.fontSize,
|
fontSize: data.data.fontSize || defaultSettings.fontSize,
|
||||||
locale: data.data.settings.locale || detectLocale()
|
locale: data.data.locale || detectLocale()
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
settings.value.locale = detectLocale()
|
settings.value.locale = detectLocale()
|
||||||
@@ -124,30 +105,25 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
isInit.value = true
|
isInit.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const clampFontSize = (size: number) =>
|
const saveSettings = async () => {
|
||||||
Math.max(minFontSize, Math.min(size, maxFontSize))
|
await api.put('/settings', settings.value)
|
||||||
|
|
||||||
const increaseFontSize = async () => {
|
|
||||||
const newSize = clampFontSize(currentFontSize.value + fontSizeStep)
|
|
||||||
await updateSettings({ fontSize: newSize })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const decreaseFontSize = async () => {
|
const updateSettings = async (newSettings: Partial<AppSettings>) => {
|
||||||
const newSize = clampFontSize(currentFontSize.value - fontSizeStep)
|
settings.value = { ...settings.value, ...newSettings }
|
||||||
await updateSettings({ fontSize: newSize })
|
updateCssVariable()
|
||||||
|
await applyLocale()
|
||||||
|
await saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
settings,
|
settings,
|
||||||
supportLocale,
|
supportLocale,
|
||||||
|
supportFontSizes,
|
||||||
isInit,
|
isInit,
|
||||||
currentFontSize,
|
currentFontSize,
|
||||||
canIncrease,
|
|
||||||
canDecrease,
|
|
||||||
init,
|
init,
|
||||||
increaseFontSize,
|
updateSettings
|
||||||
decreaseFontSize,
|
|
||||||
updateSettings,
|
|
||||||
updateLocale
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||