From 0a6eb906495b95b785837c9daad4e34efa830c99 Mon Sep 17 00:00:00 2001 From: ghostcoder42 Date: Fri, 4 Sep 2026 18:53:50 +0800 Subject: [PATCH 1/3] feat: parse release metadata (date, title, notes, apk size) from GitHub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend parseGithubRelease with the release title, publication date, notes body and APK asset size, and add stripMarkdown/truncateText helpers to flatten release-note markdown to plain text — CRLF is normalized because a stray \r glues lines together in Android text views. --- src/lib/updates.test.ts | 53 ++++++++++++++++++++++++++++++++++++++--- src/lib/updates.ts | 44 +++++++++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/lib/updates.test.ts b/src/lib/updates.test.ts index 493ba77..0ba665f 100644 --- a/src/lib/updates.test.ts +++ b/src/lib/updates.test.ts @@ -1,4 +1,4 @@ -import { compareVersions, parseGithubRelease } from './updates'; +import { compareVersions, parseGithubRelease, stripMarkdown, truncateText } from './updates'; describe('compareVersions', () => { it.each([ @@ -18,9 +18,16 @@ describe('compareVersions', () => { describe('parseGithubRelease', () => { const payload = { tag_name: 'v0.2.0', + name: 'v0.2.0 — fixes', + published_at: '2026-08-26T10:00:00Z', + body: '## Fixes\n- crash on open\n- **dark mode**', html_url: 'https://github.com/ghostcoder42/hanime1/releases/tag/v0.2.0', assets: [ - { name: 'HAnime1-0.2.0.apk', browser_download_url: 'https://example.com/app.apk' }, + { + name: 'HAnime1-0.2.0.apk', + size: 77013559, + browser_download_url: 'https://example.com/app.apk', + }, { name: 'HAnime1-0.2.0.ipa', browser_download_url: 'https://example.com/app.ipa' }, ], }; @@ -30,12 +37,25 @@ describe('parseGithubRelease', () => { version: '0.2.0', releaseUrl: 'https://github.com/ghostcoder42/hanime1/releases/tag/v0.2.0', apkUrl: 'https://example.com/app.apk', + name: 'v0.2.0 — fixes', + publishedAt: '2026-08-26T10:00:00Z', + notes: '## Fixes\n- crash on open\n- **dark mode**', + apkSize: 77013559, }); }); it('returns apkUrl null when there is no apk asset', () => { const noApk = { ...payload, assets: [{ name: 'x.ipa', browser_download_url: 'u' }] }; - expect(parseGithubRelease(noApk)?.apkUrl).toBeNull(); + const parsed = parseGithubRelease(noApk); + expect(parsed?.apkUrl).toBeNull(); + expect(parsed?.apkSize).toBeUndefined(); + }); + + it('omits optional fields when the release has none', () => { + const parsed = parseGithubRelease({ tag_name: '0.3.0', name: ' ', body: '' }); + expect(parsed?.name).toBeUndefined(); + expect(parsed?.notes).toBeUndefined(); + expect(parsed?.publishedAt).toBeUndefined(); }); it('falls back to the canonical release page when html_url is missing', () => { @@ -50,3 +70,30 @@ describe('parseGithubRelease', () => { expect(parseGithubRelease('string')).toBeNull(); }); }); + +describe('stripMarkdown / truncateText', () => { + it('flattens headings, emphasis, links and list bullets', () => { + const md = + '## What changed\nFixed [the bug](https://ex.com/a) and **crash** `on open`\n- item one\n- item two'; + expect(stripMarkdown(md)).toBe( + 'What changed\nFixed the bug and crash on open\n• item one\n• item two' + ); + }); + + it('normalizes CRLF so lines never glue together (GitHub body shape)', () => { + // GitHub release notes come with \r\n line endings; stray \r made list + // items render on one line in the Alert. + const body = + "## What's Changed\r\n* PR one by @user in https://ex.com/1\r\n* PR two by @user in https://ex.com/2"; + const out = stripMarkdown(body); + expect(out).not.toContain('\r'); + expect(out).toBe( + "What's Changed\n• PR one by @user in https://ex.com/1\n• PR two by @user in https://ex.com/2" + ); + }); + + it('truncates long text with an ellipsis and keeps short text intact', () => { + expect(truncateText('abc', 10)).toBe('abc'); + expect(truncateText('a'.repeat(20), 10)).toBe(`${'a'.repeat(10)}…`); + }); +}); diff --git a/src/lib/updates.ts b/src/lib/updates.ts index 2dd7045..4aebc75 100644 --- a/src/lib/updates.ts +++ b/src/lib/updates.ts @@ -18,10 +18,25 @@ export type LatestRelease = { releaseUrl: string; /** Direct APK download URL when the release has one (null otherwise). */ apkUrl: string | null; + /** Release title as published (often just the tag, e.g. "v0.3.0"). */ + name?: string; + /** ISO timestamp of publication, for the update dialog. */ + publishedAt?: string; + /** Release notes body (raw markdown from GitHub). */ + notes?: string; + /** APK asset size in bytes when an APK asset exists. */ + apkSize?: number; }; -type GithubReleaseAsset = { name?: unknown; browser_download_url?: unknown }; -type GithubReleaseJson = { tag_name?: unknown; html_url?: unknown; assets?: unknown }; +type GithubReleaseAsset = { name?: unknown; size?: unknown; browser_download_url?: unknown }; +type GithubReleaseJson = { + tag_name?: unknown; + name?: unknown; + published_at?: unknown; + body?: unknown; + html_url?: unknown; + assets?: unknown; +}; /** Compare dotted numeric versions ("v0.2.0" vs "0.1.1") → -1 | 0 | 1. */ export function compareVersions(a: string, b: string): number { @@ -55,6 +70,7 @@ export function parseGithubRelease(json: unknown): LatestRelease | null { : `https://github.com/${GITHUB_REPO}/releases/latest`; let apkUrl: string | null = null; + let apkSize: number | undefined; if (Array.isArray(data.assets)) { for (const asset of data.assets as GithubReleaseAsset[]) { if ( @@ -63,11 +79,33 @@ export function parseGithubRelease(json: unknown): LatestRelease | null { typeof asset.browser_download_url === 'string' ) { apkUrl = asset.browser_download_url; + if (typeof asset.size === 'number' && asset.size > 0) apkSize = asset.size; break; } } } - return { version: tag, releaseUrl, apkUrl }; + const name = typeof data.name === 'string' && data.name.trim() ? data.name.trim() : undefined; + const publishedAt = + typeof data.published_at === 'string' && data.published_at ? data.published_at : undefined; + const notes = typeof data.body === 'string' && data.body.trim() ? data.body : undefined; + return { version: tag, releaseUrl, apkUrl, name, publishedAt, notes, apkSize }; +} + +/** Flatten release-note markdown to plain text (Alert shows no formatting). */ +export function stripMarkdown(text: string): string { + return text + .replace(/\r\n?/g, '\n') // normalize CRLF — stray \r glues lines together on Android + .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') // [label](url) -> label + .replace(/^#{1,6}[ \t]*/gm, '') // headings (keep the line break after) + .replace(/(\*\*|__|`+)/g, '') // emphasis / code + .replace(/^[ \t]*[-*+][ \t]+/gm, '• ') // list items + .replace(/\n{3,}/g, '\n\n') + .trim(); +} + +/** Clamp a text to `max` characters with an ellipsis. */ +export function truncateText(text: string, max: number): string { + return text.length <= max ? text : `${text.slice(0, max).trimEnd()}…`; } /** Fetch the latest published release from GitHub (null on any failure). */ From e105dd355cf5a42f9bf08eb5d99edab19386692d Mon Sep 17 00:00:00 2001 From: ghostcoder42 Date: Fri, 4 Sep 2026 18:54:41 +0800 Subject: [PATCH 2/3] fix: give the update-check row a real button and highlight the version hint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The row's action was muted-gray text ('檢查') that read as disabled. Render a pill button (primary background + refresh icon) that swaps to a spinner with a localized 'checking…' label while the request is in flight, and disable the row during the check. Keep the new version number in the hint line — split the hint string into prefix/suffix keys so the version itself renders rose-highlighted instead of replacing the button label with it. --- src/app/(app)/settings.tsx | 45 +++++++++++++++++++++++++++---------- src/translations/en.json | 4 +++- src/translations/ja.json | 4 +++- src/translations/ko.json | 4 +++- src/translations/zh-CN.json | 4 +++- src/translations/zh.json | 4 +++- 6 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/app/(app)/settings.tsx b/src/app/(app)/settings.tsx index a628e32..1f986ba 100644 --- a/src/app/(app)/settings.tsx +++ b/src/app/(app)/settings.tsx @@ -1,5 +1,6 @@ import { queryClient } from '@/api/common/query-client'; import { ScreenErrorBoundary } from '@/components/error-boundary'; +import { Icon } from '@/components/icon'; import { SafeAreaView } from '@/components/safe-area-view'; import { SITE_DOMAINS, SITE_DOMAIN_KEY, type SiteDomain } from '@/lib/hanime1/endpoints'; import { type ThemeMode, useThemeConfig } from '@/lib/hooks'; @@ -51,7 +52,8 @@ function Row({ children, }: { label: string; - description?: string; + /** Plain string gets the standard muted style; a node renders as-is (rich text). */ + description?: React.ReactNode; children: React.ReactNode; }) { return ( @@ -59,7 +61,11 @@ function Row({ {label} {description ? ( - {description} + typeof description === 'string' ? ( + {description} + ) : ( + description + ) ) : null} {children} @@ -207,18 +213,33 @@ export default function SettingsScreen() { + {t('settings.updateAvailablePrefix')}{' '} + {update.latestVersion}{' '} + {t('settings.updateAvailableSuffix')} + + ) : undefined } > - {update.checking ? ( - - ) : update.hasUpdate ? ( - {update.latestVersion} - ) : ( - {t('settings.checkNow')} - )} + {/* Pill-shaped button so the row reads as actionable (the old muted + text looked disabled); swaps to a spinner + "checking" label + while the request is in flight. View, not Pressable — the whole + row is the tap target. */} + + {update.checking ? ( + + ) : ( + + )} + + {t(update.checking ? 'settings.checking' : 'settings.checkNow')} + + diff --git a/src/translations/en.json b/src/translations/en.json index 0ef62cd..fccc4fe 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -83,12 +83,14 @@ "version": "Version", "checkUpdates": "Check for updates", "checkNow": "Check", + "checking": "Checking…", "upToDate": "You're on the latest version", "updateCheckFailed": "Failed to check for updates", "updateAvailableTitle": "Update available", "updateAvailableMsg": "Version {{version}} is available (current: {{current}}). Download it now?", "updateNow": "Update now", - "updateAvailableHint": "New version {{version}} available" + "updateAvailablePrefix": "New version", + "updateAvailableSuffix": "available" }, "actions": { "addToFavorites": "Add to favorites", diff --git a/src/translations/ja.json b/src/translations/ja.json index ceb6234..c564c68 100644 --- a/src/translations/ja.json +++ b/src/translations/ja.json @@ -83,12 +83,14 @@ "version": "バージョン", "checkUpdates": "アップデートを確認", "checkNow": "確認", + "checking": "確認中…", "upToDate": "すでに最新バージョンです", "updateCheckFailed": "アップデートの確認に失敗しました", "updateAvailableTitle": "新しいバージョンがあります", "updateAvailableMsg": "新しいバージョン {{version}} が利用可能です(現在 {{current}})。今すぐダウンロードしますか?", "updateNow": "今すぐ更新", - "updateAvailableHint": "新しいバージョン {{version}} があります" + "updateAvailablePrefix": "新しいバージョン", + "updateAvailableSuffix": "があります" }, "actions": { "addToFavorites": "お気に入りに追加", diff --git a/src/translations/ko.json b/src/translations/ko.json index f71f38b..67e56a5 100644 --- a/src/translations/ko.json +++ b/src/translations/ko.json @@ -83,12 +83,14 @@ "version": "버전", "checkUpdates": "업데이트 확인", "checkNow": "확인", + "checking": "확인 중…", "upToDate": "이미 최신 버전입니다", "updateCheckFailed": "업데이트 확인 실패", "updateAvailableTitle": "새 버전 있음", "updateAvailableMsg": "새 버전 {{version}} 사용 가능(현재 {{current}}). 지금 다운로드하시겠습니까?", "updateNow": "지금 업데이트", - "updateAvailableHint": "새 버전 {{version}} 사용 가능" + "updateAvailablePrefix": "새 버전", + "updateAvailableSuffix": "사용 가능" }, "actions": { "addToFavorites": "즐겨찾기에 추가", diff --git a/src/translations/zh-CN.json b/src/translations/zh-CN.json index 3f8682a..a09e85c 100644 --- a/src/translations/zh-CN.json +++ b/src/translations/zh-CN.json @@ -83,12 +83,14 @@ "version": "版本", "checkUpdates": "检查更新", "checkNow": "检查", + "checking": "检查中", "upToDate": "已是最新版本", "updateCheckFailed": "检查更新失败", "updateAvailableTitle": "发现新版本", "updateAvailableMsg": "新版本 {{version}} 已发布(当前 {{current}}),是否前往下载?", "updateNow": "立即更新", - "updateAvailableHint": "有新版本 {{version}} 可供更新" + "updateAvailablePrefix": "有新版本", + "updateAvailableSuffix": "可供更新" }, "actions": { "addToFavorites": "加入收藏", diff --git a/src/translations/zh.json b/src/translations/zh.json index 3026f6e..266f914 100644 --- a/src/translations/zh.json +++ b/src/translations/zh.json @@ -83,12 +83,14 @@ "version": "版本", "checkUpdates": "檢查更新", "checkNow": "檢查", + "checking": "檢查中", "upToDate": "已是最新版本", "updateCheckFailed": "檢查更新失敗", "updateAvailableTitle": "發現新版本", "updateAvailableMsg": "新版本 {{version}} 已發布(目前 {{current}}),是否前往下載?", "updateNow": "立即更新", - "updateAvailableHint": "有新版本 {{version}} 可供更新" + "updateAvailablePrefix": "有新版本", + "updateAvailableSuffix": "可供更新" }, "actions": { "addToFavorites": "加入收藏", From 0ddf0ddbac7069212c9acc9e3b1949b6a7379bc7 Mon Sep 17 00:00:00 2001 From: ghostcoder42 Date: Fri, 4 Sep 2026 18:55:06 +0800 Subject: [PATCH 3/3] feat: rich in-app update dialog capped at 80% of the screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The system Alert cannot cap its height and GitHub release notes can get arbitrarily long. Replace it with an in-app modal (only ever opened by a manual check — the silent auto-check still just refreshes the row hint) showing the release date, APK size, title and notes in a scrollable card clamped to 80% of the screen, with cancel/update actions styled like the rest of the app. --- src/app/(app)/settings.tsx | 8 +++ src/components/update-dialog.test.tsx | 55 +++++++++++++++++ src/components/update-dialog.tsx | 88 +++++++++++++++++++++++++++ src/lib/hooks/use-update-check.ts | 55 +++++++++-------- src/translations/en.json | 4 +- src/translations/ja.json | 4 +- src/translations/ko.json | 4 +- src/translations/zh-CN.json | 4 +- src/translations/zh.json | 4 +- 9 files changed, 194 insertions(+), 32 deletions(-) create mode 100644 src/components/update-dialog.test.tsx create mode 100644 src/components/update-dialog.tsx diff --git a/src/app/(app)/settings.tsx b/src/app/(app)/settings.tsx index 1f986ba..1ce03fe 100644 --- a/src/app/(app)/settings.tsx +++ b/src/app/(app)/settings.tsx @@ -2,6 +2,7 @@ import { queryClient } from '@/api/common/query-client'; import { ScreenErrorBoundary } from '@/components/error-boundary'; import { Icon } from '@/components/icon'; import { SafeAreaView } from '@/components/safe-area-view'; +import { UpdateDialog } from '@/components/update-dialog'; import { SITE_DOMAINS, SITE_DOMAIN_KEY, type SiteDomain } from '@/lib/hanime1/endpoints'; import { type ThemeMode, useThemeConfig } from '@/lib/hooks'; import { useSecuritySettings } from '@/lib/hooks/use-security-settings'; @@ -246,6 +247,13 @@ export default function SettingsScreen() { {VERSION} + + ); } diff --git a/src/components/update-dialog.test.tsx b/src/components/update-dialog.test.tsx new file mode 100644 index 0000000..15161ec --- /dev/null +++ b/src/components/update-dialog.test.tsx @@ -0,0 +1,55 @@ +import type { LatestRelease } from '@/lib/updates'; +import { fireEvent, render } from '@testing-library/react-native'; +import { UpdateDialog } from './update-dialog'; + +const release: LatestRelease = { + version: '0.3.0', + releaseUrl: 'https://github.com/ghostcoder42/hanime1/releases/tag/v0.3.0', + apkUrl: 'https://example.com/app.apk', + name: 'v0.3.0 — big release', + publishedAt: '2026-08-27T02:23:26Z', + notes: "## What's Changed\r\n* Fix A\r\n* Fix B", + apkSize: 52967745, +}; + +async function setup(overrides: Partial[0]> = {}) { + const onClose = jest.fn(); + const onDownload = jest.fn(); + const utils = await render( + + ); + return { utils, onClose, onDownload }; +} + +describe('UpdateDialog', () => { + it('renders nothing when there is no release', async () => { + const { utils } = await setup({ release: null }); + expect(utils.toJSON()).toBeNull(); + }); + + it('shows the release date, apk size, title and notes with proper line breaks', async () => { + const { utils } = await setup(); + expect(utils.getByText('2026-08-27', { exact: false })).toBeTruthy(); + expect(utils.getByText(/APK ≈ 50\.5 MB/)).toBeTruthy(); + expect(utils.getByText('v0.3.0 — big release')).toBeTruthy(); + // CRLF normalized (per-line matching — the matcher folds newlines; the + // exact \n layout is covered by the stripMarkdown unit tests) + expect(utils.getByText(/What's Changed/)).toBeTruthy(); + expect(utils.getByText(/• Fix A/)).toBeTruthy(); + expect(utils.getByText(/• Fix B/)).toBeTruthy(); + }); + + it('closes via cancel and hands the release to onDownload', async () => { + const { utils, onClose, onDownload } = await setup(); + fireEvent.press(utils.getByText('Cancel')); + expect(onClose).toHaveBeenCalledTimes(1); + fireEvent.press(utils.getByText('Update now')); + expect(onDownload).toHaveBeenCalledWith(release); + }); +}); diff --git a/src/components/update-dialog.tsx b/src/components/update-dialog.tsx new file mode 100644 index 0000000..736b90c --- /dev/null +++ b/src/components/update-dialog.tsx @@ -0,0 +1,88 @@ +import { useTranslate } from '@/lib/i18n/utils'; +import { type LatestRelease, stripMarkdown, truncateText } from '@/lib/updates'; +import { Modal, Pressable, ScrollView, Text, View } from 'react-native'; + +type UpdateDialogProps = { + /** Release to present; null keeps the dialog closed. */ + release: LatestRelease | null; + currentVersion: string; + onClose: () => void; + onDownload: (release: LatestRelease) => void; +}; + +/** + * In-app replacement for the system Alert on a positive manual update check. + * The system dialog cannot cap its height, and GitHub release notes can get + * arbitrarily long — this card is clamped to 80% of the screen with the body + * scrolling inside, so it stays usable no matter how much text a release has. + */ +export function UpdateDialog({ release, currentVersion, onClose, onDownload }: UpdateDialogProps) { + const t = useTranslate(); + if (!release) return null; + + const showName = + !!release.name && release.name !== `v${release.version}` && release.name !== release.version; + const notes = release.notes ? truncateText(stripMarkdown(release.notes), 2000) : ''; + + const meta: string[] = []; + if (release.publishedAt) { + meta.push(`${t('settings.releaseDate')} ${release.publishedAt.slice(0, 10)}`); + } + if (release.apkSize) meta.push(`APK ≈ ${(release.apkSize / 1024 / 1024).toFixed(1)} MB`); + + return ( + + + {/* Pressable card with no onPress = tap sink, so only the backdrop + dismisses while scrolls/buttons inside still work. */} + + + {t('settings.updateAvailableTitle')} + + + + + {t('settings.updateAvailableMsg', { + version: release.version, + current: currentVersion, + })} + + {meta.length > 0 ? ( + {meta.join(' · ')} + ) : null} + {showName ? ( + {release.name} + ) : null} + {notes ? ( + + {t('settings.releaseNotes')} + {'\n'} + {notes} + + ) : null} + + + + + {t('common.cancel')} + + onDownload(release)} + className="rounded-full bg-primary px-4 py-2" + accessibilityRole="button" + > + {t('settings.updateNow')} + + + + + + ); +} diff --git a/src/lib/hooks/use-update-check.ts b/src/lib/hooks/use-update-check.ts index fe6a7ee..57d471a 100644 --- a/src/lib/hooks/use-update-check.ts +++ b/src/lib/hooks/use-update-check.ts @@ -1,10 +1,10 @@ import { useLatestRelease } from '@/api/update-queries'; import { useTranslate } from '@/lib/i18n/utils'; import { useUpdateStore } from '@/lib/stores/update-store'; -import { compareVersions } from '@/lib/updates'; +import { type LatestRelease, compareVersions } from '@/lib/updates'; import { Env } from '@env'; -import { useCallback, useEffect } from 'react'; -import { Alert, Linking, Platform } from 'react-native'; +import { useCallback, useEffect, useState } from 'react'; +import { Linking, Platform } from 'react-native'; import { showMessage } from 'react-native-flash-message'; export type UpdateCheckState = { @@ -15,17 +15,27 @@ export type UpdateCheckState = { lastCheckedAt: number | undefined; /** True when the persisted latest version is newer than the running app. */ hasUpdate: boolean; - /** Manual check: prompts when an update is available, toasts otherwise. */ + /** + * Release found by the last manual check, when it is newer than the running + * app. Render it with ; null means no dialog. + */ + pendingRelease: LatestRelease | null; + /** Manual check: opens the update dialog when newer, toasts otherwise. */ check: () => Promise; - /** Open the download (direct APK on Android, release page on iOS). */ + dismissUpdateDialog: () => void; + /** Open the download for a release (direct APK on Android, page on iOS). */ + openReleaseDownload: (release: LatestRelease) => void; + /** Open the download for the persisted latest release (banner-style use). */ openDownload: () => void; }; /** * GitHub-release update checker for the settings screen. The TanStack Query * mounts as a silent auto-check (dedup + cache), and every successful fetch - * is persisted to the update store so the hint survives restarts. Declining - * the prompt keeps the row hint until the installed version catches up. + * is persisted to the update store so the hint survives restarts. The dialog + * only ever opens from a manual check; the silent auto-check just refreshes + * the stored hint. Declining keeps the row hint until the installed version + * catches up. */ export function useUpdateCheck(): UpdateCheckState { const t = useTranslate(); @@ -35,6 +45,7 @@ export function useUpdateCheck(): UpdateCheckState { const releaseUrl = useUpdateStore((s) => s.releaseUrl); const apkUrl = useUpdateStore((s) => s.apkUrl); const recordCheck = useUpdateStore((s) => s.recordCheck); + const [pendingRelease, setPendingRelease] = useState(null); const currentVersion = Env.VERSION ?? '0.0.0'; const hasUpdate = !!latestVersion && compareVersions(latestVersion, currentVersion) > 0; @@ -45,6 +56,12 @@ export function useUpdateCheck(): UpdateCheckState { if (data) recordCheck(data, dataUpdatedAt); }, [data, dataUpdatedAt, recordCheck]); + const openReleaseDownload = useCallback((release: LatestRelease) => { + const url = + Platform.OS === 'android' ? (release.apkUrl ?? release.releaseUrl) : release.releaseUrl; + if (url) void Linking.openURL(url); + }, []); + const openDownload = useCallback(() => { const url = Platform.OS === 'android' ? (apkUrl ?? releaseUrl) : releaseUrl; if (url) void Linking.openURL(url); @@ -62,26 +79,7 @@ export function useUpdateCheck(): UpdateCheckState { return; } if (compareVersions(release.version, currentVersion) > 0) { - Alert.alert( - t('settings.updateAvailableTitle'), - t('settings.updateAvailableMsg', { - version: release.version, - current: currentVersion, - }), - [ - { text: t('common.cancel'), style: 'cancel' }, - { - text: t('settings.updateNow'), - onPress: () => { - const url = - Platform.OS === 'android' - ? (release.apkUrl ?? release.releaseUrl) - : release.releaseUrl; - void Linking.openURL(url); - }, - }, - ] - ); + setPendingRelease(release); } else { showMessage({ message: t('settings.upToDate'), @@ -97,7 +95,10 @@ export function useUpdateCheck(): UpdateCheckState { latestVersion: latestVersion ?? undefined, lastCheckedAt: lastCheckedAt ?? undefined, hasUpdate, + pendingRelease, check, + dismissUpdateDialog: () => setPendingRelease(null), + openReleaseDownload, openDownload, }; } diff --git a/src/translations/en.json b/src/translations/en.json index fccc4fe..169c459 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -90,7 +90,9 @@ "updateAvailableMsg": "Version {{version}} is available (current: {{current}}). Download it now?", "updateNow": "Update now", "updateAvailablePrefix": "New version", - "updateAvailableSuffix": "available" + "updateAvailableSuffix": "available", + "releaseDate": "Released", + "releaseNotes": "What's new:" }, "actions": { "addToFavorites": "Add to favorites", diff --git a/src/translations/ja.json b/src/translations/ja.json index c564c68..85a0d1b 100644 --- a/src/translations/ja.json +++ b/src/translations/ja.json @@ -90,7 +90,9 @@ "updateAvailableMsg": "新しいバージョン {{version}} が利用可能です(現在 {{current}})。今すぐダウンロードしますか?", "updateNow": "今すぐ更新", "updateAvailablePrefix": "新しいバージョン", - "updateAvailableSuffix": "があります" + "updateAvailableSuffix": "があります", + "releaseDate": "リリース日", + "releaseNotes": "更新内容:" }, "actions": { "addToFavorites": "お気に入りに追加", diff --git a/src/translations/ko.json b/src/translations/ko.json index 67e56a5..d312dd5 100644 --- a/src/translations/ko.json +++ b/src/translations/ko.json @@ -90,7 +90,9 @@ "updateAvailableMsg": "새 버전 {{version}} 사용 가능(현재 {{current}}). 지금 다운로드하시겠습니까?", "updateNow": "지금 업데이트", "updateAvailablePrefix": "새 버전", - "updateAvailableSuffix": "사용 가능" + "updateAvailableSuffix": "사용 가능", + "releaseDate": "릴리스", + "releaseNotes": "업데이트 내용:" }, "actions": { "addToFavorites": "즐겨찾기에 추가", diff --git a/src/translations/zh-CN.json b/src/translations/zh-CN.json index a09e85c..ff3dcec 100644 --- a/src/translations/zh-CN.json +++ b/src/translations/zh-CN.json @@ -90,7 +90,9 @@ "updateAvailableMsg": "新版本 {{version}} 已发布(当前 {{current}}),是否前往下载?", "updateNow": "立即更新", "updateAvailablePrefix": "有新版本", - "updateAvailableSuffix": "可供更新" + "updateAvailableSuffix": "可供更新", + "releaseDate": "发布于", + "releaseNotes": "更新说明:" }, "actions": { "addToFavorites": "加入收藏", diff --git a/src/translations/zh.json b/src/translations/zh.json index 266f914..5c56cdd 100644 --- a/src/translations/zh.json +++ b/src/translations/zh.json @@ -90,7 +90,9 @@ "updateAvailableMsg": "新版本 {{version}} 已發布(目前 {{current}}),是否前往下載?", "updateNow": "立即更新", "updateAvailablePrefix": "有新版本", - "updateAvailableSuffix": "可供更新" + "updateAvailableSuffix": "可供更新", + "releaseDate": "發布於", + "releaseNotes": "更新說明:" }, "actions": { "addToFavorites": "加入收藏",