diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index 0d8b22a..fc9b4cc 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -73,6 +73,7 @@ jobs: run: | npm ci npm run build:android + zip -r dist.zip dist - name: Sync Capacitor to Android working-directory: client @@ -105,4 +106,5 @@ jobs: - Client: `${{ vars.REGISTRY }}/${{ vars.CLIENT_IMAGE }}:latest` files: | client/android/app/build/outputs/apk/release/*.apk + client/dist.zip api_key: ${{ secrets.GITHUB_TOKEN }} diff --git a/client/android/app/capacitor.build.gradle b/client/android/app/capacitor.build.gradle index bbfb44f..fded3bd 100644 --- a/client/android/app/capacitor.build.gradle +++ b/client/android/app/capacitor.build.gradle @@ -9,7 +9,8 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { - + implementation project(':capacitor-app') + implementation project(':capgo-capacitor-updater') } diff --git a/client/android/capacitor.settings.gradle b/client/android/capacitor.settings.gradle index 9a5fa87..94cddac 100644 --- a/client/android/capacitor.settings.gradle +++ b/client/android/capacitor.settings.gradle @@ -1,3 +1,9 @@ // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN include ':capacitor-android' project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') + +include ':capacitor-app' +project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android') + +include ':capgo-capacitor-updater' +project(':capgo-capacitor-updater').projectDir = new File('../node_modules/@capgo/capacitor-updater/android') diff --git a/client/capacitor.config.json b/client/capacitor.config.json index 5a765a2..8a70336 100644 --- a/client/capacitor.config.json +++ b/client/capacitor.config.json @@ -5,5 +5,10 @@ "server": { "androidScheme": "https", "cleartext": false + }, + "plugins": { + "CapacitorUpdater": { + "autoUpdate": false + } } } diff --git a/client/package-lock.json b/client/package-lock.json index c716ec0..a9f86dc 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,7 +9,9 @@ "version": "0.0.0", "dependencies": { "@capacitor/android": "^8.0.0", + "@capacitor/app": "^8.0.0", "@capacitor/core": "^8.0.0", + "@capgo/capacitor-updater": "^8.40.6", "@eslint/eslintrc": "^3.3.3", "@eslint/js": "^9.39.2", "@mdi/font": "^7.3.67", @@ -639,6 +641,15 @@ "@capacitor/core": "^8.0.0" } }, + "node_modules/@capacitor/app": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@capacitor/app/-/app-8.0.0.tgz", + "integrity": "sha512-OwzIkUs4w433Bu9WWAEbEYngXEfJXZ9Wmdb8eoaqzYBgB0W9/3Ed/mh6sAYPNBAZlpyarmewgP7Nb+d3Vrh+xA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, "node_modules/@capacitor/cli": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-7.4.4.tgz", @@ -681,6 +692,15 @@ "tslib": "^2.1.0" } }, + "node_modules/@capgo/capacitor-updater": { + "version": "8.40.6", + "resolved": "https://registry.npmjs.org/@capgo/capacitor-updater/-/capacitor-updater-8.40.6.tgz", + "integrity": "sha512-Z2uG8E9VKClYeZRhvkN8p/w3tXqxj5DB61M3uKwuHyHgPWEOoBv+drlRtnwDL8+XhSljNLG7yGPWUg8WAi8cBQ==", + "license": "MPL-2.0", + "peerDependencies": { + "@capacitor/core": "^8.0.0" + } + }, "node_modules/@csstools/css-parser-algorithms": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", diff --git a/client/package.json b/client/package.json index 970e77d..86fc5c7 100644 --- a/client/package.json +++ b/client/package.json @@ -14,7 +14,9 @@ }, "dependencies": { "@capacitor/android": "^8.0.0", + "@capacitor/app": "^8.0.0", "@capacitor/core": "^8.0.0", + "@capgo/capacitor-updater": "^8.40.6", "@eslint/eslintrc": "^3.3.3", "@eslint/js": "^9.39.2", "@mdi/font": "^7.3.67", diff --git a/client/src/App.vue b/client/src/App.vue index 888fb94..81e7750 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -287,7 +287,9 @@ import { ref, watch, onMounted, computed, onUnmounted, } from 'vue'; import { useI18n } from 'vue-i18n'; +import { App as CapacitorApp } from '@capacitor/app'; import { useAppStore } from '@/stores/appStore'; +import { checkForUpdates } from '@/utils/autoUpdate'; import logo from '@/assets/icon.svg'; const drawer = ref(false); @@ -388,6 +390,9 @@ const handleMouseMove = (e) => { }; onMounted(() => { + CapacitorApp.addListener('appStateChange', ({ isActive }) => { + if (isActive) checkForUpdates(); + }); if (store.token) { viewState.value = 'app'; store.fetchStats(); diff --git a/client/src/main.js b/client/src/main.js index e222d66..d1cc415 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -25,10 +25,10 @@ const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', component: Dashboard }, - { path: '/dashboard', component: Dashboard }, { path: '/collection', component: Collection }, { path: '/review', component: Review }, { path: '/lesson', component: Lesson }, + { path: '/:pathMatch(.*)*', redirect: '/' }, ], }); diff --git a/client/src/utils/autoUpdate.js b/client/src/utils/autoUpdate.js new file mode 100644 index 0000000..d3da0b0 --- /dev/null +++ b/client/src/utils/autoUpdate.js @@ -0,0 +1,55 @@ +import { Capacitor } from '@capacitor/core'; +import { App } from '@capacitor/app'; + +export async function checkForUpdates() { + if (!Capacitor.isNativePlatform()) { + console.log('Auto-update skipped (running on web)'); + return; + } + + function isNewer(current, target) { + const c = current.replace(/^v/, '').split('.').map(Number); + const t = target.replace(/^v/, '').split('.').map(Number); + + for (let i = 0; i < 3; i++) { + if (t[i] > c[i]) return true; + if (t[i] < c[i]) return false; + } + return false; + } + + try { + const { CapacitorUpdater } = await import('@capgo/capacitor-updater'); + + await CapacitorUpdater.notifyAppReady(); + + const GITEA_API_URL = 'https://git.crylia.de/Crylia/zen-kanji/releases/latest'; + + const appInfo = await App.getInfo(); + const currentVersion = appInfo.version; + + const response = await fetch(GITEA_API_URL); + if (!response.ok) return; + + const release = await response.json(); + const latestTag = release.tag_name; + + if (!isNewer(currentVersion, latestTag)) { + return; + } + + const asset = release.assets.find((a) => a.name === 'dist.zip'); + if (!asset) return; + + console.log(`Downloading update: ${latestTag}`); + + const version = await CapacitorUpdater.download({ + url: asset.browser_download_url, + version: latestTag, + }); + + await CapacitorUpdater.set(version); + } catch (error) { + console.error('Auto-update failed:', error); + } +} diff --git a/client/src/views/Lesson.vue b/client/src/views/Lesson.vue index 96eda6c..6bd0ce0 100644 --- a/client/src/views/Lesson.vue +++ b/client/src/views/Lesson.vue @@ -8,7 +8,7 @@ v-else-if="currentItem" ) .d-flex.align-center.pa-4.border-b-subtle - v-btn(icon="mdi-close" variant="text" to="/dashboard" color="grey") + v-btn(icon="mdi-close" variant="text" to="/" color="grey") v-spacer .text-caption.text-grey-darken-1.font-weight-bold | {{ sessionDone + 1 }} / {{ sessionTotal }} @@ -95,7 +95,7 @@ .text-h4.font-weight-bold.text-white.mb-2 {{ $t('lesson.completeTitle') }} .text-body-1.text-grey.mb-8 {{ $t('lesson.learned', { n: sessionDone }) }} v-btn.glow-btn.text-black.font-weight-bold( - to="/dashboard" + to="/" block color="cyan" height="50" diff --git a/client/src/views/Review.vue b/client/src/views/Review.vue index 094cde9..314a038 100644 --- a/client/src/views/Review.vue +++ b/client/src/views/Review.vue @@ -96,7 +96,7 @@ .text-caption.text-grey.text-uppercase.mt-1 {{ $t('stats.correct') }} v-btn.text-black.font-weight-bold.glow-btn( - to="/dashboard" + to="/" block color="primary" height="50" @@ -108,7 +108,7 @@ .text-h4.mb-2.text-white {{ $t('review.caughtUp') }} .text-grey.mb-6 {{ $t('review.noReviews') }} v-btn.font-weight-bold( - to="/dashboard" + to="/" color="primary" variant="tonal" ) {{ $t('review.viewCollection') }} diff --git a/server/src/services/stats.service.js b/server/src/services/stats.service.js index 7c146d9..eca06a3 100644 --- a/server/src/services/stats.service.js +++ b/server/src/services/stats.service.js @@ -93,7 +93,7 @@ export const getUserStats = async (user) => { accuracy: Math.round((item.stats.correct / item.stats.total) * 100) })).filter(i => i.accuracy < 85) .sort((a, b) => a.accuracy - b.accuracy) - .slice(0, 4); + .slice(0, 10); return { distribution: dist,