@cavulsqa/create 2.9.2 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/bin/create.mjs +18 -25
- package/lib/templates.mjs +56 -0
- package/package.json +13 -5
- package/templates/f7-app/CLAUDE.md +3 -0
- package/templates/f7-app/auto-imports.d.ts +15 -0
- package/templates/f7-app/package.json +6 -3
- package/templates/f7-app/src/App.vue +1 -0
- package/templates/f7-app/src/env.d.ts +1 -0
- package/templates/f7-app/src/main.ts +3 -0
- package/templates/f7-app/src/modules/demo/router/routes/demo.routes.ts +4 -16
- package/templates/f7-app/src/modules/home/router/routes/home.routes.ts +3 -11
- package/templates/f7-app/src/modules/settings/router/routes/settings.routes.ts +2 -6
- package/templates/f7-app/src/plugins/bootstrapError.ts +2 -0
- package/templates/f7-app/src/plugins/recorder.plugin.ts +155 -0
- package/templates/f7-app/src/router/global/global.routes.ts +2 -6
- package/templates/f7-app/src/shared/composables/useNavigationGuard.ts +48 -0
- package/templates/f7-app/src/shared/recorder/capacitorSink.ts +124 -0
- package/templates/f7-app/src/shared/utils/lazyRoute.ts +25 -0
- package/templates/f7-app/tests/lazyRoute.test.ts +30 -0
- package/templates/f7-app/tests/navigationGuard.test.ts +82 -0
- package/templates/f7-app/vite.config.ts +1 -0
- package/templates/m3e-app/.claude/rules/data-fetching.md +68 -0
- package/templates/m3e-app/.claude/rules/database.md +106 -0
- package/templates/m3e-app/.claude/rules/m3e-ui.md +48 -0
- package/templates/m3e-app/.claude/rules/modules.md +43 -0
- package/templates/m3e-app/.claude/rules/native.md +60 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/SKILL.md +68 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/color.md +44 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/components.md +325 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/layout.md +42 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/motion.md +51 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/shapes-type.md +48 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/sources.md +53 -0
- package/templates/m3e-app/.claude/skills/module-architecture/SKILL.md +65 -0
- package/templates/m3e-app/.claude/skills/module-architecture/file-templates.md +178 -0
- package/templates/m3e-app/.claude/skills/reactive-data/SKILL.md +90 -0
- package/templates/m3e-app/.claude/skills/reactive-data/testing.md +55 -0
- package/templates/m3e-app/.env.example +32 -0
- package/templates/m3e-app/CLAUDE.md +93 -0
- package/templates/m3e-app/auto-imports.d.ts +851 -0
- package/templates/m3e-app/capacitor.config.ts +44 -0
- package/templates/m3e-app/components.d.ts +252 -0
- package/templates/m3e-app/index.html +16 -0
- package/templates/m3e-app/package.json +107 -0
- package/templates/m3e-app/src/App.vue +129 -0
- package/templates/m3e-app/src/app/pragmas.config.ts +35 -0
- package/templates/m3e-app/src/app/scroll.config.ts +20 -0
- package/templates/m3e-app/src/app/storage.config.ts +65 -0
- package/templates/m3e-app/src/app/tabs.ts +33 -0
- package/templates/m3e-app/src/app/theme.config.ts +51 -0
- package/templates/m3e-app/src/assets/css/app.css +18 -0
- package/templates/m3e-app/src/assets/css/base.css +53 -0
- package/templates/m3e-app/src/assets/css/layout/container-transform.css +113 -0
- package/templates/m3e-app/src/assets/css/layout/shell.css +69 -0
- package/templates/m3e-app/src/assets/css/layout/transitions.css +121 -0
- package/templates/m3e-app/src/assets/css/m3e.css +6 -0
- package/templates/m3e-app/src/assets/css/theme/framework7.css +18 -0
- package/templates/m3e-app/src/assets/css/theme/tailwind.css +365 -0
- package/templates/m3e-app/src/domains/benchmark/benchmark.dataset.ts +209 -0
- package/templates/m3e-app/src/domains/benchmark/benchmark.suite.ts +662 -0
- package/templates/m3e-app/src/domains/sales/sales.repository.ts +458 -0
- package/templates/m3e-app/src/env.d.ts +40 -0
- package/templates/m3e-app/src/locales/ar.json +1260 -0
- package/templates/m3e-app/src/locales/en.json +1260 -0
- package/templates/m3e-app/src/locales/fr.json +1260 -0
- package/templates/m3e-app/src/main.ts +41 -0
- package/templates/m3e-app/src/modules/demo/components/DemoBenchmark.vue +119 -0
- package/templates/m3e-app/src/modules/demo/components/DemoBusLog.vue +63 -0
- package/templates/m3e-app/src/modules/demo/components/DemoCreateOrderSheet.vue +203 -0
- package/templates/m3e-app/src/modules/demo/components/DemoMetricsSheet.vue +75 -0
- package/templates/m3e-app/src/modules/demo/components/DemoOrderList.vue +75 -0
- package/templates/m3e-app/src/modules/demo/components/DemoPipelineBenchmark.vue +65 -0
- package/templates/m3e-app/src/modules/demo/components/DemoStatCards.vue +71 -0
- package/templates/m3e-app/src/modules/demo/composables/useBenchmark.ts +139 -0
- package/templates/m3e-app/src/modules/demo/composables/useOrderStatus.ts +64 -0
- package/templates/m3e-app/src/modules/demo/composables/useReactiveDemo.ts +171 -0
- package/templates/m3e-app/src/modules/demo/router/routes/demo.routes.ts +22 -0
- package/templates/m3e-app/src/modules/demo/views/DemoView.vue +210 -0
- package/templates/m3e-app/src/modules/demo/views/OrderDetailView.vue +136 -0
- package/templates/m3e-app/src/modules/demo/views/OrderSearchView.vue +74 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryBlock.vue +16 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryCarouselTile.vue +71 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryCustomerForm.vue +76 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryProofOfDelivery.vue +69 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaDay.vue +106 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaMonth.vue +40 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaVisitList.vue +45 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatAttachSheet.vue +106 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatContactPicker.vue +85 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatInviteComposer.vue +131 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatPollComposer.vue +148 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatRecentPhotos.vue +62 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsAccount.vue +79 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsTags.vue +53 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsVerification.vue +73 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/PasswordStrength.vue +37 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryButtons.vue +128 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCarousels.vue +236 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCharts.vue +84 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryFabs.vue +70 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryInputs.vue +219 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryNavigation.vue +141 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryOverlays.vue +368 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryPickers.vue +175 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryProgress.vue +118 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryScale.vue +103 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GallerySelection.vue +219 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryShapes.vue +48 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GallerySurfaces.vue +213 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryTables.vue +185 -0
- package/templates/m3e-app/src/modules/gallery/components/surfaces/SurfacesContainerTransform.vue +46 -0
- package/templates/m3e-app/src/modules/gallery/composables/chatTeam.ts +18 -0
- package/templates/m3e-app/src/modules/gallery/composables/composeKind.ts +2 -0
- package/templates/m3e-app/src/modules/gallery/composables/currentPosition.ts +35 -0
- package/templates/m3e-app/src/modules/gallery/composables/demoOrders.ts +29 -0
- package/templates/m3e-app/src/modules/gallery/composables/featuredStories.ts +73 -0
- package/templates/m3e-app/src/modules/gallery/composables/fieldFormats.ts +36 -0
- package/templates/m3e-app/src/modules/gallery/composables/openExternal.ts +11 -0
- package/templates/m3e-app/src/modules/gallery/composables/routePlan.ts +90 -0
- package/templates/m3e-app/src/modules/gallery/composables/useAgenda.ts +61 -0
- package/templates/m3e-app/src/modules/gallery/composables/useChatCustomers.ts +31 -0
- package/templates/m3e-app/src/modules/gallery/composables/useChatDemo.ts +299 -0
- package/templates/m3e-app/src/modules/gallery/composables/useGallerySections.ts +230 -0
- package/templates/m3e-app/src/modules/gallery/composables/useLocalAttachments.ts +71 -0
- package/templates/m3e-app/src/modules/gallery/composables/useLocationShare.ts +38 -0
- package/templates/m3e-app/src/modules/gallery/composables/usePhotoScenes.ts +82 -0
- package/templates/m3e-app/src/modules/gallery/composables/useTreeDemo.ts +67 -0
- package/templates/m3e-app/src/modules/gallery/composables/wilayas.ts +67 -0
- package/templates/m3e-app/src/modules/gallery/router/routes/gallery.routes.ts +52 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryAgendaView.vue +117 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryChatView.vue +292 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryContactsView.vue +77 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryFeaturedView.vue +60 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryLoginView.vue +135 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryOnboardingView.vue +116 -0
- package/templates/m3e-app/src/modules/gallery/views/GallerySectionView.vue +27 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryTabsView.vue +135 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryView.vue +50 -0
- package/templates/m3e-app/src/modules/home/components/HomeHero.vue +57 -0
- package/templates/m3e-app/src/modules/home/composables/useHomeFeatures.ts +128 -0
- package/templates/m3e-app/src/modules/home/composables/useOpenFeature.ts +22 -0
- package/templates/m3e-app/src/modules/home/router/routes/home.routes.ts +21 -0
- package/templates/m3e-app/src/modules/home/views/FeatureDetailView.vue +66 -0
- package/templates/m3e-app/src/modules/home/views/HomeView.vue +47 -0
- package/templates/m3e-app/src/modules/settings/components/RolePalette.vue +38 -0
- package/templates/m3e-app/src/modules/settings/components/SeedSwatches.vue +41 -0
- package/templates/m3e-app/src/modules/settings/components/SettingsChoice.vue +43 -0
- package/templates/m3e-app/src/modules/settings/components/StudioPreview.vue +52 -0
- package/templates/m3e-app/src/modules/settings/router/routes/settings.routes.ts +17 -0
- package/templates/m3e-app/src/modules/settings/types.ts +7 -0
- package/templates/m3e-app/src/modules/settings/views/ColorStudioView.vue +166 -0
- package/templates/m3e-app/src/modules/settings/views/SettingsView.vue +157 -0
- package/templates/m3e-app/src/plugins/bootstrapError.ts +62 -0
- package/templates/m3e-app/src/plugins/capacitor/index.ts +14 -0
- package/templates/m3e-app/src/plugins/capacitor/useAndroidBackButton.ts +36 -0
- package/templates/m3e-app/src/plugins/capacitor/useKeyboard.ts +48 -0
- package/templates/m3e-app/src/plugins/capacitor/useSplashScreen.ts +43 -0
- package/templates/m3e-app/src/plugins/capacitor/useStatusBar.ts +16 -0
- package/templates/m3e-app/src/plugins/framework7.plugin.ts +36 -0
- package/templates/m3e-app/src/plugins/i18n.plugin.ts +87 -0
- package/templates/m3e-app/src/plugins/m3e.plugin.ts +22 -0
- package/templates/m3e-app/src/plugins/seed.plugin.ts +7 -0
- package/templates/m3e-app/src/plugins/sqlite.plugin.ts +32 -0
- package/templates/m3e-app/src/router/global/global.routes.ts +12 -0
- package/templates/m3e-app/src/router/index.ts +20 -0
- package/templates/m3e-app/src/shared/components/error/404.vue +13 -0
- package/templates/m3e-app/src/shared/components/layout/EmptyState.vue +30 -0
- package/templates/m3e-app/src/shared/components/layout/SectionHeader.vue +10 -0
- package/templates/m3e-app/src/shared/components/page/AppPage.vue +104 -0
- package/templates/m3e-app/src/shared/composables/navigation/useActiveTab.ts +39 -0
- package/templates/m3e-app/src/shared/composables/navigation/useContainerTransform.ts +117 -0
- package/templates/m3e-app/src/shared/composables/navigation/useNavigationGuard.ts +48 -0
- package/templates/m3e-app/src/shared/composables/navigation/useNavigationVisibility.ts +64 -0
- package/templates/m3e-app/src/shared/composables/navigation/useViewRouter.ts +25 -0
- package/templates/m3e-app/src/shared/composables/navigation/useWindowClass.ts +22 -0
- package/templates/m3e-app/src/shared/composables/theme/useThemeSettings.ts +155 -0
- package/templates/m3e-app/src/shared/database/candidates/capacitorSqlite.ts +36 -0
- package/templates/m3e-app/src/shared/database/candidates/index.ts +4 -0
- package/templates/m3e-app/src/shared/database/candidates/opfsSahPool.ts +25 -0
- package/templates/m3e-app/src/shared/database/candidates/types.ts +56 -0
- package/templates/m3e-app/src/shared/database/candidates/waSqlite.ts +56 -0
- package/templates/m3e-app/src/shared/database/database.ts +219 -0
- package/templates/m3e-app/src/shared/database/index.ts +3 -0
- package/templates/m3e-app/src/shared/database/migrations.ts +223 -0
- package/templates/m3e-app/src/shared/database/opfs.worker.ts +5 -0
- package/templates/m3e-app/src/shared/database/queries.ts +26 -0
- package/templates/m3e-app/src/shared/database/schema.ts +153 -0
- package/templates/m3e-app/src/shared/database/storage.ts +64 -0
- package/templates/m3e-app/src/shared/database/wa.worker.ts +5 -0
- package/templates/m3e-app/src/shared/utils/lazyRoute.ts +41 -0
- package/templates/m3e-app/src/shared/utils/resolvers/resolvers.ts +42 -0
- package/templates/m3e-app/src/shared/utils/textDirection.ts +22 -0
- package/templates/m3e-app/src/shared/utils/theme/themeSettings.ts +59 -0
- package/templates/m3e-app/src/shared/utils/tone.ts +8 -0
- package/templates/m3e-app/tests/benchmark.suite.test.ts +104 -0
- package/templates/m3e-app/tests/containerTransform.test.ts +58 -0
- package/templates/m3e-app/tests/fieldFormats.test.ts +25 -0
- package/templates/m3e-app/tests/lazyRoute.test.ts +30 -0
- package/templates/m3e-app/tests/locales.test.ts +73 -0
- package/templates/m3e-app/tests/migrations.test.ts +74 -0
- package/templates/m3e-app/tests/navigationGuard.test.ts +82 -0
- package/templates/m3e-app/tests/openDatabase.test.ts +54 -0
- package/templates/m3e-app/tests/sales.repository.test.ts +329 -0
- package/templates/m3e-app/tests/storage.test.ts +117 -0
- package/templates/m3e-app/tests/textDirection.test.ts +11 -0
- package/templates/m3e-app/tooling/linkedPackages.ts +112 -0
- package/templates/m3e-app/tsconfig.json +21 -0
- package/templates/m3e-app/vite.config.ts +144 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<AppPage :title="t('settings.title')" name="settings">
|
|
3
|
+
<SectionHeader :title="t('settings.appearance')" />
|
|
4
|
+
<M3List variant="segmented" inset>
|
|
5
|
+
<M3ListItem
|
|
6
|
+
clickable
|
|
7
|
+
:headline="t('settings.colors')"
|
|
8
|
+
:supporting="t(`studio.variants.${settings.variant}`)"
|
|
9
|
+
@click="f7router.navigate('/settings/studio/')"
|
|
10
|
+
>
|
|
11
|
+
<template #leading>
|
|
12
|
+
<M3Shape shape="cookie9Sided" class="size-10 bg-primary text-on-primary">
|
|
13
|
+
<i-ms-palette-outline-rounded class="size-5" />
|
|
14
|
+
</M3Shape>
|
|
15
|
+
</template>
|
|
16
|
+
<template #trailing><i-ms-chevron-right-rounded class="rtl:-scale-x-100" /></template>
|
|
17
|
+
</M3ListItem>
|
|
18
|
+
</M3List>
|
|
19
|
+
|
|
20
|
+
<div class="mx-4 mt-0.5 flex flex-col gap-0.5">
|
|
21
|
+
<SettingsChoice
|
|
22
|
+
:model-value="settings.mode"
|
|
23
|
+
:label="t('settings.darkMode')"
|
|
24
|
+
:options="modeOptions"
|
|
25
|
+
@update:model-value="(mode) => theme.update({ mode })"
|
|
26
|
+
>
|
|
27
|
+
<template #icon><component :is="modeIcon" /></template>
|
|
28
|
+
</SettingsChoice>
|
|
29
|
+
<SettingsChoice
|
|
30
|
+
:model-value="settings.motion"
|
|
31
|
+
:label="t('settings.motion')"
|
|
32
|
+
:supporting="t('settings.motionNote')"
|
|
33
|
+
:options="motionOptions"
|
|
34
|
+
@update:model-value="(motion) => theme.update({ motion })"
|
|
35
|
+
>
|
|
36
|
+
<template #icon><i-ms-animation-rounded /></template>
|
|
37
|
+
</SettingsChoice>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<SectionHeader :title="t('settings.general')" />
|
|
41
|
+
<M3List variant="segmented" inset>
|
|
42
|
+
<M3ListItem
|
|
43
|
+
clickable
|
|
44
|
+
:headline="t('settings.language')"
|
|
45
|
+
:supporting="t(`settings.languages.${locale}`)"
|
|
46
|
+
@click="chooseLanguage"
|
|
47
|
+
>
|
|
48
|
+
<template #leading><i-ms-language-rounded /></template>
|
|
49
|
+
</M3ListItem>
|
|
50
|
+
</M3List>
|
|
51
|
+
|
|
52
|
+
<SectionHeader :title="t('settings.storage')" />
|
|
53
|
+
<M3List variant="segmented" inset>
|
|
54
|
+
<M3ListItem :headline="t('settings.engine')" :supporting="storage">
|
|
55
|
+
<template #leading><i-ms-database-outline-rounded /></template>
|
|
56
|
+
</M3ListItem>
|
|
57
|
+
<M3ListItem
|
|
58
|
+
v-if="!durable"
|
|
59
|
+
:headline="t('settings.notDurable')"
|
|
60
|
+
:supporting="tradeoff"
|
|
61
|
+
multiline
|
|
62
|
+
tone="destructive"
|
|
63
|
+
>
|
|
64
|
+
<template #leading><i-ms-info-outline-rounded /></template>
|
|
65
|
+
</M3ListItem>
|
|
66
|
+
<M3ListItem
|
|
67
|
+
v-for="skip in skipped"
|
|
68
|
+
:key="skip.id"
|
|
69
|
+
:headline="skip.id"
|
|
70
|
+
:supporting="skip.detail"
|
|
71
|
+
multiline
|
|
72
|
+
>
|
|
73
|
+
<template #leading><i-ms-chevron-right-rounded class="rtl:-scale-x-100" /></template>
|
|
74
|
+
</M3ListItem>
|
|
75
|
+
</M3List>
|
|
76
|
+
|
|
77
|
+
<SectionHeader :title="t('settings.about')" />
|
|
78
|
+
<M3List variant="segmented" inset>
|
|
79
|
+
<M3ListItem :headline="t('settings.appName')" :trailing-text="appName">
|
|
80
|
+
<template #leading><i-ms-info-outline-rounded /></template>
|
|
81
|
+
</M3ListItem>
|
|
82
|
+
<M3ListItem :headline="t('settings.version')" :trailing-text="appVersion">
|
|
83
|
+
<template #leading><i-ms-verified-outline-rounded /></template>
|
|
84
|
+
</M3ListItem>
|
|
85
|
+
<M3ListItem :headline="t('settings.platform')" :trailing-text="platform">
|
|
86
|
+
<template #leading><i-ms-phone-android-outline-rounded /></template>
|
|
87
|
+
</M3ListItem>
|
|
88
|
+
<M3ListItem :headline="t('settings.webview')" :trailing-text="webview">
|
|
89
|
+
<template #leading><i-ms-web-rounded /></template>
|
|
90
|
+
</M3ListItem>
|
|
91
|
+
</M3List>
|
|
92
|
+
</AppPage>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<script setup lang="ts">
|
|
96
|
+
import { Capacitor } from "@capacitor/core";
|
|
97
|
+
import { markRaw } from "vue";
|
|
98
|
+
import type { Router } from "framework7/types";
|
|
99
|
+
import AutoIcon from "~icons/material-symbols/brightness-auto-outline-rounded";
|
|
100
|
+
import DarkIcon from "~icons/material-symbols/dark-mode-outline-rounded";
|
|
101
|
+
import LightIcon from "~icons/material-symbols/light-mode-outline-rounded";
|
|
102
|
+
import SettingsChoice from "@/modules/settings/components/SettingsChoice.vue";
|
|
103
|
+
import type { ChoiceOption } from "@/modules/settings/types";
|
|
104
|
+
import { LOCALES, setLocale, type AppLocale } from "@/plugins/i18n.plugin";
|
|
105
|
+
import { activeStorage, activeStorageLabel, storageAttempts } from "@/shared/database/database";
|
|
106
|
+
import { webviewVersion } from "@/shared/database/storage";
|
|
107
|
+
import type { ModeChoice, MotionChoice } from "@/shared/utils/theme/themeSettings";
|
|
108
|
+
|
|
109
|
+
defineProps<{ f7router: Router.Router }>();
|
|
110
|
+
|
|
111
|
+
const { t, locale } = useI18n();
|
|
112
|
+
const theme = useThemeSettings();
|
|
113
|
+
const settings = computed(() => theme.settings.value);
|
|
114
|
+
const actionSheet = useActionSheet();
|
|
115
|
+
|
|
116
|
+
const appName = __APP_NAME__;
|
|
117
|
+
const appVersion = __APP_VERSION__;
|
|
118
|
+
const platform = Capacitor.getPlatform();
|
|
119
|
+
const storage = activeStorageLabel();
|
|
120
|
+
const durable = activeStorage().durable;
|
|
121
|
+
const tradeoff = activeStorage().tradeoff;
|
|
122
|
+
const skipped = storageAttempts().filter((attempt) => attempt.outcome !== "opened");
|
|
123
|
+
const chromium = webviewVersion();
|
|
124
|
+
const webview = chromium === null ? t("settings.unknown") : `Chromium ${String(chromium)}`;
|
|
125
|
+
|
|
126
|
+
const modeOptions = computed<ChoiceOption<ModeChoice>[]>(() => [
|
|
127
|
+
{ value: "system", label: t("settings.modeSystem"), icon: markRaw(AutoIcon) },
|
|
128
|
+
{ value: "light", label: t("settings.modeLight"), icon: markRaw(LightIcon) },
|
|
129
|
+
{ value: "dark", label: t("settings.modeDark"), icon: markRaw(DarkIcon) },
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
const modeIcon = computed(
|
|
133
|
+
() => modeOptions.value.find((option) => option.value === settings.value.mode)?.icon ?? AutoIcon,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const motionOptions = computed<ChoiceOption<MotionChoice>[]>(() => [
|
|
137
|
+
{ value: "system", label: t("settings.motionSystem") },
|
|
138
|
+
{ value: "reduced", label: t("settings.motionReduced") },
|
|
139
|
+
{ value: "full", label: t("settings.motionFull") },
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
async function chooseLanguage() {
|
|
143
|
+
const choice = await actionSheet.open({
|
|
144
|
+
title: t("settings.language"),
|
|
145
|
+
groups: [
|
|
146
|
+
{
|
|
147
|
+
items: LOCALES.map((code) => ({
|
|
148
|
+
id: code,
|
|
149
|
+
label: t(`settings.languages.${code}`),
|
|
150
|
+
selected: locale.value === code,
|
|
151
|
+
})),
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
if (choice) setLocale(choice as AppLocale);
|
|
156
|
+
}
|
|
157
|
+
</script>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MINIMUM_CHROMIUM_FOR_OPFS,
|
|
3
|
+
webviewLikelyTooOld,
|
|
4
|
+
webviewVersion,
|
|
5
|
+
} from "@/shared/database/storage";
|
|
6
|
+
import { hideSplashScreen } from "@/plugins/capacitor/useSplashScreen";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The screen shown when the app cannot start, which in practice means the database did not open.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately framework-free. Mounting the app to report the failure would run screens that assume
|
|
12
|
+
* a working database, so a second failure would replace the first and the person would learn
|
|
13
|
+
* nothing. Plain DOM cannot fail for the same reason twice.
|
|
14
|
+
*
|
|
15
|
+
* It replaces a red sentence in a bare div, which is what shipped before and told a field user
|
|
16
|
+
* nothing they could act on.
|
|
17
|
+
*/
|
|
18
|
+
export function renderBootstrapError(error: unknown): void {
|
|
19
|
+
hideSplashScreen();
|
|
20
|
+
const root = document.getElementById("app");
|
|
21
|
+
if (!root) return;
|
|
22
|
+
|
|
23
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
24
|
+
const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
25
|
+
const version = webviewVersion();
|
|
26
|
+
|
|
27
|
+
const advice = webviewLikelyTooOld()
|
|
28
|
+
? `This WebView is Chromium ${String(version)}. The app needs ${String(MINIMUM_CHROMIUM_FOR_OPFS)} or newer for local storage. Update Android System WebView in the Play Store - it updates separately from Android itself.`
|
|
29
|
+
: "Closing the app completely and opening it again usually clears this. Your data has not been deleted.";
|
|
30
|
+
|
|
31
|
+
root.replaceChildren();
|
|
32
|
+
root.insertAdjacentHTML(
|
|
33
|
+
"afterbegin",
|
|
34
|
+
`<div style="font:16px/1.5 var(--md-ref-typeface-plain,system-ui),sans-serif;padding:max(32px,env(safe-area-inset-top)) 24px 32px;max-width:38rem;margin:0 auto;min-height:100dvh;box-sizing:border-box;background:var(--md-sys-color-surface,#fff);color:var(--md-sys-color-on-surface,#1c1b1f)">
|
|
35
|
+
<div style="width:56px;height:56px;border-radius:28px;display:grid;place-items:center;font-size:28px;background:var(--md-sys-color-error-container,#ffdad6);color:var(--md-sys-color-on-error-container,#410002)">!</div>
|
|
36
|
+
<h1 style="font:400 28px/36px var(--md-ref-typeface-brand,system-ui),sans-serif;margin:20px 0 8px">The app could not start</h1>
|
|
37
|
+
<p style="margin:0 0 12px;color:var(--md-sys-color-on-surface-variant,#49454f)">${escapeHtml(message)}</p>
|
|
38
|
+
<p style="margin:0 0 24px;color:var(--md-sys-color-on-surface-variant,#49454f)">${escapeHtml(advice)}</p>
|
|
39
|
+
<button id="bootstrap-retry" style="appearance:none;border:0;border-radius:28px;height:56px;width:100%;background:var(--md-sys-color-primary,#6750a4);color:var(--md-sys-color-on-primary,#fff);font:500 16px var(--md-ref-typeface-plain,system-ui),sans-serif">
|
|
40
|
+
Try again
|
|
41
|
+
</button>
|
|
42
|
+
<details style="margin-top:24px;color:var(--md-sys-color-on-surface-variant,#49454f)">
|
|
43
|
+
<summary style="cursor:pointer;font-size:13px">Technical details</summary>
|
|
44
|
+
<p style="font-size:12px;margin:8px 0 0">WebView: Chromium ${version === null ? "unknown" : String(version)}</p>
|
|
45
|
+
<pre style="font-size:11px;white-space:pre-wrap;word-break:break-word;margin:8px 0 0">${escapeHtml(detail)}</pre>
|
|
46
|
+
</details>
|
|
47
|
+
</div>`,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
document.getElementById("bootstrap-retry")?.addEventListener("click", () => {
|
|
51
|
+
window.location.reload();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The message can carry an engine's own text, and that text is not trusted markup. */
|
|
56
|
+
function escapeHtml(value: string): string {
|
|
57
|
+
return value
|
|
58
|
+
.replace(/&/g, "&")
|
|
59
|
+
.replace(/</g, "<")
|
|
60
|
+
.replace(/>/g, ">")
|
|
61
|
+
.replace(/"/g, """);
|
|
62
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type Framework7 from "framework7";
|
|
2
|
+
import { useAndroidBackButton } from "./useAndroidBackButton";
|
|
3
|
+
import { useKeyboard } from "./useKeyboard";
|
|
4
|
+
import { useStatusBar } from "./useStatusBar";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Native wiring that needs the Framework7 instance, so it runs once the shell exists rather than at
|
|
8
|
+
* module load. Every handler no-ops off-device, which is what keeps `vp dev` in a browser working.
|
|
9
|
+
*/
|
|
10
|
+
export async function initCapacitor(f7: Framework7): Promise<void> {
|
|
11
|
+
await useAndroidBackButton(f7);
|
|
12
|
+
useKeyboard();
|
|
13
|
+
await useStatusBar();
|
|
14
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { App } from "@capacitor/app";
|
|
2
|
+
import { Capacitor } from "@capacitor/core";
|
|
3
|
+
import type Framework7 from "framework7";
|
|
4
|
+
import { m3e } from "@/plugins/m3e.plugin";
|
|
5
|
+
import { useActiveTab } from "@/shared/composables/navigation/useActiveTab";
|
|
6
|
+
import { START_TAB } from "@/app/tabs";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Android back closes what is on top before it navigates, in this order:
|
|
10
|
+
* 1. the topmost M3 overlay - a sheet, a dialog, a menu, the FAB menu. A persistent one swallows
|
|
11
|
+
* back instead of closing, so a required decision cannot be backed out of by accident;
|
|
12
|
+
* 2. the current tab's own history;
|
|
13
|
+
* 3. from the root of any other destination, the start destination, as M3 navigation does;
|
|
14
|
+
* 4. otherwise the app goes to the background rather than exiting, so it reopens where it was.
|
|
15
|
+
*/
|
|
16
|
+
export async function useAndroidBackButton(f7: Framework7): Promise<void> {
|
|
17
|
+
if (!Capacitor.isNativePlatform()) return;
|
|
18
|
+
const tabs = useActiveTab();
|
|
19
|
+
|
|
20
|
+
await App.addListener("backButton", () => {
|
|
21
|
+
if (m3e.config.overlays.closeTop()) return;
|
|
22
|
+
|
|
23
|
+
const view = f7.views.current;
|
|
24
|
+
if (view?.router && view.router.history.length > 1) {
|
|
25
|
+
view.router.back();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!tabs.isStart()) {
|
|
30
|
+
tabs.show(START_TAB);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
void App.minimizeApp();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Capacitor } from "@capacitor/core";
|
|
2
|
+
import { Keyboard } from "@capacitor/keyboard";
|
|
3
|
+
import { useNavigationVisibility } from "@/shared/composables/navigation/useNavigationVisibility";
|
|
4
|
+
|
|
5
|
+
const EDITABLE = "input, textarea, [contenteditable='true']";
|
|
6
|
+
const KEEPS_KEYBOARD = `${EDITABLE}, [data-keeps-keyboard]`;
|
|
7
|
+
|
|
8
|
+
function scrollFocusedIntoView(): void {
|
|
9
|
+
const focused = document.activeElement;
|
|
10
|
+
if (focused instanceof HTMLElement && focused.matches(EDITABLE)) {
|
|
11
|
+
focused.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The focused field scrolls out of view as the keyboard animates in, so it is scrolled back on
|
|
17
|
+
* every phase: the layout is still settling at `keyboardWillShow` and only `keyboardDidShow` sees
|
|
18
|
+
* the final height. The navigation bar leaves while the keyboard is up - it would otherwise sit on
|
|
19
|
+
* the keyboard and steal a row from the field being typed into. A tap outside any field dismisses
|
|
20
|
+
* the keyboard instead of leaving it over half the screen - except inside `[data-keeps-keyboard]`,
|
|
21
|
+
* such as a message composer, whose send button must not close it.
|
|
22
|
+
*/
|
|
23
|
+
export function useKeyboard(): void {
|
|
24
|
+
if (!Capacitor.isNativePlatform()) return;
|
|
25
|
+
const { setKeyboardOpen } = useNavigationVisibility();
|
|
26
|
+
|
|
27
|
+
void Keyboard.addListener("keyboardWillShow", () => {
|
|
28
|
+
setKeyboardOpen(true);
|
|
29
|
+
scrollFocusedIntoView();
|
|
30
|
+
});
|
|
31
|
+
void Keyboard.addListener("keyboardDidShow", scrollFocusedIntoView);
|
|
32
|
+
void Keyboard.addListener("keyboardDidHide", () => setKeyboardOpen(false));
|
|
33
|
+
|
|
34
|
+
document.addEventListener(
|
|
35
|
+
"touchstart",
|
|
36
|
+
(event) => {
|
|
37
|
+
const target = event.target;
|
|
38
|
+
if (!(target instanceof Element) || target.closest(KEEPS_KEYBOARD)) return;
|
|
39
|
+
if (
|
|
40
|
+
document.activeElement instanceof HTMLElement &&
|
|
41
|
+
document.activeElement.matches(EDITABLE)
|
|
42
|
+
) {
|
|
43
|
+
document.activeElement.blur();
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{ passive: true },
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Capacitor } from "@capacitor/core";
|
|
2
|
+
import { SplashScreen } from "@capacitor/splash-screen";
|
|
3
|
+
import type Framework7 from "framework7/lite";
|
|
4
|
+
|
|
5
|
+
const PAGE_TIMEOUT_MS = 4000;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `launchAutoHide` is off in `capacitor.config.ts`, so the splash stays up until this is called.
|
|
9
|
+
* Hiding it on a frame boundary avoids a flash of an unpainted shell between the two.
|
|
10
|
+
*/
|
|
11
|
+
export function hideSplashScreen(): void {
|
|
12
|
+
if (!Capacitor.isNativePlatform()) return;
|
|
13
|
+
requestAnimationFrame(() => void SplashScreen.hide());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const inActiveTab = (element: Element | null | undefined) =>
|
|
17
|
+
Boolean(element?.closest(".tab-active, .view-main"));
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Keeps the splash up until the visible tab's first page has rendered - route components load
|
|
21
|
+
* lazily, so the shell alone would show an empty screen with a navigation bar for a beat. A page
|
|
22
|
+
* that never arrives (a failed chunk) still lets the splash go after a few seconds.
|
|
23
|
+
*/
|
|
24
|
+
export function hideSplashWhenPageReady(app: Framework7): void {
|
|
25
|
+
if (!Capacitor.isNativePlatform()) return;
|
|
26
|
+
if (document.querySelector(".tab-active .page-current, .view-main .page-current")) {
|
|
27
|
+
hideSplashScreen();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
let done = false;
|
|
31
|
+
const finish = () => {
|
|
32
|
+
if (done) return;
|
|
33
|
+
done = true;
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
app.off("pageInit", onPage);
|
|
36
|
+
hideSplashScreen();
|
|
37
|
+
};
|
|
38
|
+
const onPage = (page: { el?: Element }) => {
|
|
39
|
+
if (inActiveTab(page.el)) finish();
|
|
40
|
+
};
|
|
41
|
+
const timer = window.setTimeout(finish, PAGE_TIMEOUT_MS);
|
|
42
|
+
app.on("pageInit", onPage);
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Capacitor } from "@capacitor/core";
|
|
2
|
+
import { StatusBar } from "@capacitor/status-bar";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Edge to edge: the status bar is transparent over the web view and the top app bar pads itself
|
|
6
|
+
* with the safe-area inset. Its icon colour follows the theme's dark mode, which the theme sets -
|
|
7
|
+
* see `useThemeSettings`.
|
|
8
|
+
*/
|
|
9
|
+
export async function useStatusBar(): Promise<void> {
|
|
10
|
+
if (!Capacitor.isNativePlatform()) return;
|
|
11
|
+
try {
|
|
12
|
+
await StatusBar.setOverlaysWebView({ overlay: true });
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.warn("[status-bar] overlay not applied", error);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Framework7Parameters } from "framework7/types";
|
|
2
|
+
import routes from "@/router";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Framework7 runs as the navigation engine: routing, the per-tab view stacks and the page
|
|
6
|
+
* lifecycle. The theme is pinned to Material because its Material page metrics are what the M3
|
|
7
|
+
* components sit in; nothing visual of Framework7's own is used. Pages move with `m3e-axis`, the
|
|
8
|
+
* shared-axis transition in `assets/css/layout/transitions.css`, and Framework7 plays it backwards
|
|
9
|
+
* on back.
|
|
10
|
+
*/
|
|
11
|
+
export function framework7Parameters(): Framework7Parameters {
|
|
12
|
+
return {
|
|
13
|
+
name: "App",
|
|
14
|
+
theme: "md",
|
|
15
|
+
// Framework7 sets or clears `dark` on <html> at init, and F7App turns an unset value into
|
|
16
|
+
// `false`, which cleared the dark mode `startTheme` applied before mount. Handing it the mode
|
|
17
|
+
// already applied keeps them in agreement; the theme's own watcher owns the class afterwards.
|
|
18
|
+
darkMode:
|
|
19
|
+
typeof document !== "undefined" && document.documentElement.classList.contains("dark"),
|
|
20
|
+
routes,
|
|
21
|
+
touch: {
|
|
22
|
+
tapHold: true,
|
|
23
|
+
tapHoldDelay: 500,
|
|
24
|
+
tapHoldPreventClicks: true,
|
|
25
|
+
touchRipple: false,
|
|
26
|
+
activeState: false,
|
|
27
|
+
},
|
|
28
|
+
statusbar: { enabled: false },
|
|
29
|
+
view: {
|
|
30
|
+
animate: true,
|
|
31
|
+
transition: "m3e-axis",
|
|
32
|
+
browserHistory: false,
|
|
33
|
+
mdSwipeBack: false,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { f7 } from "framework7-vue";
|
|
2
|
+
import { createI18n } from "vue-i18n";
|
|
3
|
+
import { textDirection } from "@/shared/utils/textDirection";
|
|
4
|
+
import ar from "@/locales/ar.json";
|
|
5
|
+
import en from "@/locales/en.json";
|
|
6
|
+
import fr from "@/locales/fr.json";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Arabic is `ar-DZ`, not `ar`: Algeria writes Latin digits (bare `ar` formats ٠١٢) and its own month
|
|
10
|
+
* names (جانفي, فيفري ...), and every date and number in the app is formatted with this tag.
|
|
11
|
+
*/
|
|
12
|
+
export const LOCALES = ["en", "fr", "ar-DZ"] as const;
|
|
13
|
+
export type AppLocale = (typeof LOCALES)[number];
|
|
14
|
+
|
|
15
|
+
const STORAGE_KEY = "app-locale";
|
|
16
|
+
|
|
17
|
+
const isLocale = (value: unknown): value is AppLocale =>
|
|
18
|
+
typeof value === "string" && (LOCALES as readonly string[]).includes(value);
|
|
19
|
+
|
|
20
|
+
function initialLocale(): AppLocale {
|
|
21
|
+
try {
|
|
22
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
23
|
+
if (isLocale(stored)) return stored;
|
|
24
|
+
} catch {
|
|
25
|
+
return "en";
|
|
26
|
+
}
|
|
27
|
+
return fromDevice(navigator.language) ?? "en";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The app locale for a device language: the same language, whatever its region. */
|
|
31
|
+
export function fromDevice(tag: string): AppLocale | null {
|
|
32
|
+
const language = tag.split("-")[0]?.toLowerCase();
|
|
33
|
+
return LOCALES.find((locale) => locale.split("-")[0] === language) ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SIX_FORMS = ["zero", "one", "two", "few", "many", "other"] as const;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `"{count} line | {count} lines"` picks its form from the language's own plural rules: French
|
|
40
|
+
* counts 0 as singular, English does not. A three-form message is `zero | one | other`, and a
|
|
41
|
+
* six-form one is CLDR's full set, `zero | one | two | few | many | other`, which Arabic needs:
|
|
42
|
+
* 3 to 10 take a plural, 11 to 99 a singular accusative, 100 the singular again.
|
|
43
|
+
*/
|
|
44
|
+
export function pluralRule(locale: AppLocale) {
|
|
45
|
+
const rules = new Intl.PluralRules(locale);
|
|
46
|
+
return (count: number, forms: number) => {
|
|
47
|
+
const category = rules.select(count);
|
|
48
|
+
if (forms === 6) return SIX_FORMS.indexOf(category as (typeof SIX_FORMS)[number]);
|
|
49
|
+
const one = category === "one";
|
|
50
|
+
if (forms === 3) return count === 0 ? 0 : one ? 1 : 2;
|
|
51
|
+
return one ? 0 : 1;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The document's language and direction follow the app's locale: `lang` picks fonts, hyphenation
|
|
57
|
+
* and the screen reader's voice, and `dir` mirrors the layout, which the components read through
|
|
58
|
+
* logical properties and `[dir="rtl"]`. A right-to-left locale needs no other change.
|
|
59
|
+
*/
|
|
60
|
+
function applyToDocument(locale: AppLocale): void {
|
|
61
|
+
if (typeof document === "undefined") return;
|
|
62
|
+
const direction = textDirection(locale);
|
|
63
|
+
document.documentElement.lang = locale;
|
|
64
|
+
document.documentElement.dir = direction;
|
|
65
|
+
if (f7) f7.rtl = direction === "rtl";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const i18n = createI18n({
|
|
69
|
+
legacy: false,
|
|
70
|
+
locale: initialLocale(),
|
|
71
|
+
fallbackLocale: "en",
|
|
72
|
+
messages: { en, fr, "ar-DZ": ar },
|
|
73
|
+
pluralRules: Object.fromEntries(LOCALES.map((locale) => [locale, pluralRule(locale)])),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
applyToDocument(i18n.global.locale.value as AppLocale);
|
|
77
|
+
|
|
78
|
+
/** Switches language and remembers it; the device language is only the first-run default. */
|
|
79
|
+
export function setLocale(locale: AppLocale): void {
|
|
80
|
+
i18n.global.locale.value = locale;
|
|
81
|
+
applyToDocument(locale);
|
|
82
|
+
try {
|
|
83
|
+
localStorage.setItem(STORAGE_KEY, locale);
|
|
84
|
+
} catch {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createM3e } from "@cavulsqa/m3e-vue";
|
|
2
|
+
import { Capacitor } from "@capacitor/core";
|
|
3
|
+
import { Haptics, ImpactStyle } from "@capacitor/haptics";
|
|
4
|
+
|
|
5
|
+
const native = Capacitor.isNativePlatform();
|
|
6
|
+
|
|
7
|
+
function impact(style: ImpactStyle): void {
|
|
8
|
+
if (!native) return;
|
|
9
|
+
Haptics.impact({ style }).catch(() => undefined);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The one m3e instance: components inject it, the Android back handler closes its overlays, and
|
|
14
|
+
* the theme writes the motion preference into it. Haptics are best-effort - a device without a
|
|
15
|
+
* vibrator rejects, and a missed tick is not worth an error.
|
|
16
|
+
*/
|
|
17
|
+
export const m3e = createM3e({
|
|
18
|
+
haptics: {
|
|
19
|
+
tick: () => impact(ImpactStyle.Light),
|
|
20
|
+
confirm: () => impact(ImpactStyle.Medium),
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ensureReferenceData } from "@/domains/sales/sales.repository";
|
|
2
|
+
import { rdb } from "@/shared/database";
|
|
3
|
+
|
|
4
|
+
/** Runs on every start: `ensureReferenceData` is idempotent and does nothing after the first. */
|
|
5
|
+
export async function seedPlugin(): Promise<void> {
|
|
6
|
+
await ensureReferenceData(rdb);
|
|
7
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { openDatabase } from "@/shared/database";
|
|
2
|
+
|
|
3
|
+
const OPEN_TIMEOUT_MS = 10_000;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Opening the database is the one thing that must succeed before the first screen renders, and the
|
|
7
|
+
* one thing that used to fail silently: awaited at the top level of `main.ts`, a rejection left an
|
|
8
|
+
* empty `#app` and nothing in the console. The timeout turns a hang into a message.
|
|
9
|
+
*/
|
|
10
|
+
export async function sqlitePlugin(): Promise<void> {
|
|
11
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await Promise.race([
|
|
15
|
+
openDatabase(),
|
|
16
|
+
new Promise<never>((_, reject) => {
|
|
17
|
+
timer = setTimeout(
|
|
18
|
+
() =>
|
|
19
|
+
reject(
|
|
20
|
+
new Error(`opening the database did not finish in ${String(OPEN_TIMEOUT_MS)}ms`),
|
|
21
|
+
),
|
|
22
|
+
OPEN_TIMEOUT_MS,
|
|
23
|
+
);
|
|
24
|
+
}),
|
|
25
|
+
]);
|
|
26
|
+
} finally {
|
|
27
|
+
// Losing the race does not stop the timer, which then held the event loop for the full timeout
|
|
28
|
+
// after a fast open. The walk itself is not cancellable, but `openDatabase` memoises it, so a
|
|
29
|
+
// retry joins the attempt already running instead of racing it for the same OPFS directory.
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Router } from "framework7/types";
|
|
2
|
+
import { lazyRoute } from "@/shared/utils/lazyRoute";
|
|
3
|
+
|
|
4
|
+
const globalRoutes: Router.RouteParameters[] = [
|
|
5
|
+
{
|
|
6
|
+
name: "not-found",
|
|
7
|
+
path: "(.*)",
|
|
8
|
+
async: lazyRoute(() => import("@/shared/components/error/404.vue")),
|
|
9
|
+
},
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export default globalRoutes;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Router } from "framework7/types";
|
|
2
|
+
import homeRoutes from "@/modules/home/router/routes/home.routes";
|
|
3
|
+
import demoRoutes from "@/modules/demo/router/routes/demo.routes";
|
|
4
|
+
import galleryRoutes from "@/modules/gallery/router/routes/gallery.routes";
|
|
5
|
+
import settingsRoutes from "@/modules/settings/router/routes/settings.routes";
|
|
6
|
+
import globalRoutes from "./global/global.routes";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Each module owns its routes; this only orders them. The catch-all is last because Framework7
|
|
10
|
+
* takes the first match.
|
|
11
|
+
*/
|
|
12
|
+
const routes: Router.RouteParameters[] = [
|
|
13
|
+
...homeRoutes,
|
|
14
|
+
...demoRoutes,
|
|
15
|
+
...galleryRoutes,
|
|
16
|
+
...settingsRoutes,
|
|
17
|
+
...globalRoutes,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export default routes;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<AppPage :title="t('errors.notFoundTitle')" back>
|
|
3
|
+
<EmptyState
|
|
4
|
+
shape="ghostish"
|
|
5
|
+
:headline="t('errors.notFoundTitle')"
|
|
6
|
+
:text="t('errors.notFound')"
|
|
7
|
+
/>
|
|
8
|
+
</AppPage>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup lang="ts">
|
|
12
|
+
const { t } = useI18n();
|
|
13
|
+
</script>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="flex flex-col items-center gap-4 px-8 py-12 text-center">
|
|
3
|
+
<M3Shape
|
|
4
|
+
:shape="props.shape"
|
|
5
|
+
class="size-24 bg-secondary-container text-on-secondary-container"
|
|
6
|
+
>
|
|
7
|
+
<slot name="icon" />
|
|
8
|
+
</M3Shape>
|
|
9
|
+
<h3 class="type-title-large m-0 text-on-surface">{{ props.headline }}</h3>
|
|
10
|
+
<p v-if="props.text" class="type-body-medium m-0 max-w-80 text-on-surface-variant">
|
|
11
|
+
{{ props.text }}
|
|
12
|
+
</p>
|
|
13
|
+
<slot />
|
|
14
|
+
</section>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import type { MaterialShapeName } from "@cavulsqa/m3e";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* What a screen shows when it has nothing to show: a shape, one line that explains, and the one
|
|
22
|
+
* action that fills it. Never a paragraph, never placeholder rows.
|
|
23
|
+
*/
|
|
24
|
+
const props = withDefaults(
|
|
25
|
+
defineProps<{ headline: string; text?: string; shape?: MaterialShapeName }>(),
|
|
26
|
+
{
|
|
27
|
+
shape: "cookie9Sided",
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
</script>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<h2 class="type-title-small-emphasized px-6 pt-6 pb-2 text-primary">
|
|
3
|
+
<slot>{{ props.title }}</slot>
|
|
4
|
+
</h2>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
/** The label above a group of content: M3's list subheader, in the primary role. */
|
|
9
|
+
const props = defineProps<{ title?: string }>();
|
|
10
|
+
</script>
|