updater: show update notes for new releases (#1552)

This commit is contained in:
Huang Xin
2025-07-08 18:13:47 +08:00
committed by GitHub
parent f5b686abd9
commit 9556e1305f
27 changed files with 284 additions and 121 deletions
@@ -433,5 +433,6 @@
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "احفظ واوصل لمجموعة قراءتك كاملة بـ 10 جيجابايت تخزين سحابي آمن.",
"Deleted cloud backup of the book: {{title}}": "تم حذف النسخة الاحتياطية السحابية للكتاب: {{title}}",
"Failed to delete cloud backup of the book": "فشل في حذف النسخة الاحتياطية السحابية للكتاب",
"Are you sure to delete the cloud backup of the selected book?": "هل أنت متأكد من حذف النسخة الاحتياطية السحابية للكتاب المحدد؟"
"Are you sure to delete the cloud backup of the selected book?": "هل أنت متأكد من حذف النسخة الاحتياطية السحابية للكتاب المحدد؟",
"What's New in Readest": "ما الجديد في Readest"
}
@@ -417,5 +417,6 @@
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Speichern und greifen Sie sicher auf Ihre gesamte Lesekollektion mit bis zu 10 GB Cloud-Speicher zu.",
"Deleted cloud backup of the book: {{title}}": "Gelöschte Cloud-Sicherung des Buches: {{title}}",
"Failed to delete cloud backup of the book": "Fehler beim Löschen der Cloud-Sicherung des Buches",
"Are you sure to delete the cloud backup of the selected book?": "Sind Sie sicher, dass Sie die Cloud-Sicherung des ausgewählten Buches löschen möchten?"
"Are you sure to delete the cloud backup of the selected book?": "Sind Sie sicher, dass Sie die Cloud-Sicherung des ausgewählten Buches löschen möchten?",
"What's New in Readest": "Was gibt es Neues in Readest"
}
@@ -417,5 +417,6 @@
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Αποθηκεύστε και αποκτήστε πρόσβαση στη συλλογή ανάγνωσης με έως 10 GB ασφαλή cloud αποθήκευση.",
"Deleted cloud backup of the book: {{title}}": "Διαγράφηκε το αντίγραφο ασφαλείας του βιβλίου: {{title}}",
"Failed to delete cloud backup of the book": "Αποτυχία διαγραφής του αντιγράφου ασφαλείας του βιβλίου",
"Are you sure to delete the cloud backup of the selected book?": "Είστε σίγουροι ότι θέλετε να διαγράψετε το αντίγραφο ασφαλείας του επιλεγμένου βιβλίου;"
"Are you sure to delete the cloud backup of the selected book?": "Είστε σίγουροι ότι θέλετε να διαγράψετε το αντίγραφο ασφαλείας του επιλεγμένου βιβλίου;",
"What's New in Readest": "Τι νέο υπάρχει στο Readest"
}
@@ -421,5 +421,6 @@
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Almacena y accede de forma segura a toda tu colección de lectura con hasta 10 GB de almacenamiento en la nube.",
"Deleted cloud backup of the book: {{title}}": "Respaldo en la nube del libro eliminado: {{title}}",
"Failed to delete cloud backup of the book": "Fallo al eliminar el respaldo en la nube del libro",
"Are you sure to delete the cloud backup of the selected book?": "¿Estás seguro de que deseas eliminar el respaldo en la nube del libro seleccionado?"
"Are you sure to delete the cloud backup of the selected book?": "¿Estás seguro de que deseas eliminar el respaldo en la nube del libro seleccionado?",
"What's New in Readest": "¿Qué hay de nuevo en Readest"
}
@@ -421,5 +421,6 @@
"Deleted cloud backup of the book: {{title}}": "Suppression de la sauvegarde cloud du livre : {{title}}",
"Failed to delete cloud backup of the book": "Échec de la suppression de la sauvegarde cloud du livre",
"Are you sure to delete the cloud backup of the selected book?": "Êtes-vous sûr de vouloir supprimer la sauvegarde cloud du livre sélectionné ?",
"Don't have an account? Sign up": "Vous n'avez pas de compte ? Inscrivez-vous"
"Don't have an account? Sign up": "Vous n'avez pas de compte ? Inscrivez-vous",
"What's New in Readest": "Quoi de neuf dans Readest"
}
@@ -417,5 +417,6 @@
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "10 GB सुरक्षित क्लाउड स्टोरेज के साथ अपना संपूर्ण पठन संग्रह संग्रहीत करें।",
"Deleted cloud backup of the book: {{title}}": "पुस्तक का क्लाउड बैकअप हटाया गया: {{title}}",
"Failed to delete cloud backup of the book": "पुस्तक का क्लाउड बैकअप हटाने में विफल",
"Are you sure to delete the cloud backup of the selected book?": "क्या आप सुनिश्चित हैं कि आप चयनित पुस्तक का क्लाउड बैकअप हटाना चाहते हैं?"
"Are you sure to delete the cloud backup of the selected book?": "क्या आप सुनिश्चित हैं कि आप चयनित पुस्तक का क्लाउड बैकअप हटाना चाहते हैं?",
"What's New in Readest": "Readest में नया क्या है"
}
@@ -413,5 +413,6 @@
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Simpan dan akses koleksi bacaan lengkap dengan aman menggunakan penyimpanan cloud 10 GB.",
"Deleted cloud backup of the book: {{title}}": "Cadangan cloud buku dihapus: {{title}}",
"Failed to delete cloud backup of the book": "Cadangan cloud buku gagal dihapus",
"Are you sure to delete the cloud backup of the selected book?": "Apakah Anda yakin ingin menghapus cadangan cloud dari buku yang dipilih?"
"Are you sure to delete the cloud backup of the selected book?": "Apakah Anda yakin ingin menghapus cadangan cloud dari buku yang dipilih?",
"What's New in Readest": "Yang Baru di Readest"
}
@@ -421,5 +421,6 @@
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Archivia e accedi in sicurezza alla tua intera collezione di lettura con fino a 10 GB di storage cloud.",
"Deleted cloud backup of the book: {{title}}": "Backup cloud del libro eliminato: {{title}}",
"Failed to delete cloud backup of the book": "Impossibile eliminare il backup cloud del libro",
"Are you sure to delete the cloud backup of the selected book?": "Sei sicuro di voler eliminare il backup cloud del libro selezionato?"
"Are you sure to delete the cloud backup of the selected book?": "Sei sicuro di voler eliminare il backup cloud del libro selezionato?",
"What's New in Readest": "Novità in Readest"
}
@@ -413,5 +413,6 @@
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "最大10GBの安全なクラウドストレージで読書コレクション全体を保存・アクセス。",
"Deleted cloud backup of the book: {{title}}": "書籍のクラウドバックアップを削除しました:{{title}}",
"Failed to delete cloud backup of the book": "書籍のクラウドバックアップを削除できませんでした",
"Are you sure to delete the cloud backup of the selected book?": "選択した書籍のクラウドバックアップを削除してもよろしいですか?"
"Are you sure to delete the cloud backup of the selected book?": "選択した書籍のクラウドバックアップを削除してもよろしいですか?",
"What's New in Readest": "Readestの新機能"
}
@@ -413,5 +413,6 @@
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "최대 10GB의 안전한 클라우드 저장소로 전체 독서 컬렉션을 저장하고 액세스하세요.",
"Deleted cloud backup of the book: {{title}}": "책의 클라우드 백업이 삭제됨: {{title}}",
"Failed to delete cloud backup of the book": "책의 클라우드 백업을 삭제하지 못했습니다.",
"Are you sure to delete the cloud backup of the selected book?": "선택한 책의 클라우드 백업을 삭제하시겠습니까?"
"Are you sure to delete the cloud backup of the selected book?": "선택한 책의 클라우드 백업을 삭제하시겠습니까?",
"What's New in Readest": "Readest의 새로운 기능"
}
@@ -417,5 +417,6 @@
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Sla je hele leesverzameling veilig op en benader deze met tot 10 GB cloudopslag.",
"Deleted cloud backup of the book: {{title}}": "Verwijderde cloudback-up van het boek: {{title}}",
"Failed to delete cloud backup of the book": "Verwijderen van cloudback-up van het boek is mislukt.",
"Are you sure to delete the cloud backup of the selected book?": "Weet je zeker dat je de cloudback-up van het geselecteerde boek wilt verwijderen?"
"Are you sure to delete the cloud backup of the selected book?": "Weet je zeker dat je de cloudback-up van het geselecteerde boek wilt verwijderen?",
"What's New in Readest": "Wat is er nieuw in Readest"
}
@@ -425,5 +425,6 @@
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Bezpiecznie przechowuj i uzyskuj dostęp do całej kolekcji czytelniczej z do 10 GB pamięci w chmurze.",
"Deleted cloud backup of the book: {{title}}": "Usunięto kopię zapasową książki w chmurze: {{title}}",
"Failed to delete cloud backup of the book": "Nie udało się usunąć kopii zapasowej książki w chmurze.",
"Are you sure to delete the cloud backup of the selected book?": "Czy na pewno chcesz usunąć kopię zapasową chmury wybranej książki?"
"Are you sure to delete the cloud backup of the selected book?": "Czy na pewno chcesz usunąć kopię zapasową chmury wybranej książki?",
"What's New in Readest": "Co nowego w Readest"
}
@@ -421,5 +421,6 @@
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Armazene e acesse com segurança sua coleção completa de leitura com até 10 GB de armazenamento na nuvem.",
"Deleted cloud backup of the book: {{title}}": "Backup na nuvem do livro excluído: {{title}}",
"Failed to delete cloud backup of the book": "Falha ao excluir o backup na nuvem do livro.",
"Are you sure to delete the cloud backup of the selected book?": "Tem certeza de que deseja excluir o backup na nuvem do livro selecionado?"
"Are you sure to delete the cloud backup of the selected book?": "Tem certeza de que deseja excluir o backup na nuvem do livro selecionado?",
"What's New in Readest": "Novidades no Readest"
}
@@ -425,5 +425,6 @@
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Безопасно храните и получайте доступ ко всей коллекции чтения с до 10 ГБ облачного хранилища.",
"Deleted cloud backup of the book: {{title}}": "Удалено облачное резервное копирование книги: {{title}}",
"Failed to delete cloud backup of the book": "Не удалось удалить облачное резервное копирование книги.",
"Are you sure to delete the cloud backup of the selected book?": "Вы уверены, что хотите удалить облачное резервное копирование выбранной книги?"
"Are you sure to delete the cloud backup of the selected book?": "Вы уверены, что хотите удалить облачное резервное копирование выбранной книги?",
"What's New in Readest": "Что нового в Readest"
}
@@ -413,5 +413,6 @@
"Reveal in File Explorer": "แสดงใน File Explorer",
"Reveal in Folder": "แสดงในโฟลเดอร์",
"Don't have an account? Sign up": "ยังไม่มีบัญชี? สมัครสมาชิก",
"All the world's a stage,\nAnd all the men and women merely players;\nThey have their exits and their entrances,\nAnd one man in his time plays many parts,\nHis acts being seven ages.\n\n— William Shakespeare": "โลกทั้งใบเป็นเวที\nและชายหญิงทั้งปวงเป็นนักแสดง\nมีทางเข้าและทางออก\nแต่ละคนในชีวิตเล่นหลายบท\nการกระทำของเขาแบ่งเป็นเจ็ดวัย\n\n— วิลเลียม เชกสเปียร์"
"All the world's a stage,\nAnd all the men and women merely players;\nThey have their exits and their entrances,\nAnd one man in his time plays many parts,\nHis acts being seven ages.\n\n— William Shakespeare": "โลกทั้งใบเป็นเวที\nและชายหญิงทั้งปวงเป็นนักแสดง\nมีทางเข้าและทางออก\nแต่ละคนในชีวิตเล่นหลายบท\nการกระทำของเขาแบ่งเป็นเจ็ดวัย\n\n— วิลเลียม เชกสเปียร์",
"What's New in Readest": "มีอะไรใหม่ใน Readest"
}
@@ -417,5 +417,6 @@
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Tüm okuma koleksiyonunuzu 10 GB'a kadar güvenli bulut depolamayla saklayın ve erişin.",
"Deleted cloud backup of the book: {{title}}": "Kitabın bulut yedeği silindi: {{title}}",
"Failed to delete cloud backup of the book": "Kitabın bulut yedeğini silme işlemi başarısız oldu.",
"Are you sure to delete the cloud backup of the selected book?": "Seçilen kitabın bulut yedeğini silmek istediğinize emin misiniz?"
"Are you sure to delete the cloud backup of the selected book?": "Seçilen kitabın bulut yedeğini silmek istediğinize emin misiniz?",
"What's New in Readest": "Readest'te Neler Yeni"
}
@@ -425,5 +425,6 @@
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Безпечно зберігайте та отримуйте доступ до всієї колекції читання з до 10 ГБ хмарного сховища.",
"Deleted cloud backup of the book: {{title}}": "Видалено хмарну резервну копію книги: {{title}}",
"Failed to delete cloud backup of the book": "Не вдалося видалити хмарну резервну копію книги.",
"Are you sure to delete the cloud backup of the selected book?": "Ви впевнені, що хочете видалити хмарну резервну копію вибраної книги?"
"Are you sure to delete the cloud backup of the selected book?": "Ви впевнені, що хочете видалити хмарну резервну копію вибраної книги?",
"What's New in Readest": "Що нового в Readest"
}
@@ -413,5 +413,6 @@
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "Lưu trữ và truy cập an toàn toàn bộ bộ sưu tập đọc với tới 10 GB lưu trữ đám mây.",
"Deleted cloud backup of the book: {{title}}": "Đã xóa bản sao lưu đám mây của sách: {{title}}",
"Failed to delete cloud backup of the book": "Không thể xóa bản sao lưu đám mây của sách.",
"Are you sure to delete the cloud backup of the selected book?": "Bạn có chắc chắn muốn xóa bản sao lưu đám mây của sách đã chọn không?"
"Are you sure to delete the cloud backup of the selected book?": "Bạn có chắc chắn muốn xóa bản sao lưu đám mây của sách đã chọn không?",
"What's New in Readest": "Những gì mới trong Readest"
}
@@ -413,5 +413,6 @@
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "使用多达 10GB 的安全云存储保存和访问您的整个阅读收藏。",
"Deleted cloud backup of the book: {{title}}": "已删除书籍云备份:{{title}}",
"Failed to delete cloud backup of the book": "无法删除书籍云备份。",
"Are you sure to delete the cloud backup of the selected book?": "您确定要删除所选书籍的云备份吗?"
"Are you sure to delete the cloud backup of the selected book?": "您确定要删除所选书籍的云备份吗?",
"What's New in Readest": "Readest 更新功能"
}
@@ -413,5 +413,6 @@
"Securely store and access your entire reading collection with up to 10 GB of cloud storage.": "使用多達 10GB 的安全雲端儲存保存和存取您的整個閱讀收藏。",
"Deleted cloud backup of the book: {{title}}": "已刪除書籍的雲端備份:{{title}}",
"Failed to delete cloud backup of the book": "無法刪除書籍的雲端備份。",
"Are you sure to delete the cloud backup of the selected book?": "您確定要刪除所選書籍的雲端備份嗎?"
"Are you sure to delete the cloud backup of the selected book?": "您確定要刪除所選書籍的雲端備份嗎?",
"What's New in Readest": "Readest 更新功能"
}
+1 -1
View File
@@ -8,7 +8,7 @@
"Library: You can now delete only the cloud backup of a book without removing the local copy",
"Performance: Fonts in the font list now load only when displayed for faster startup",
"Performance: Opening large MOBI books is now significantly faster",
"Layout: Fixed overlay scrollbar display in virtualized book lists",
"Layout: Fixed overlay scrollbar display in longer TOC items list",
"Layout: Improved grid cover image height in crop mode",
"Layout: Removed unsupported screen orientation lock option on iPad",
"Layout: System context menu no longer appears when selecting text on iPad",
+5 -3
View File
@@ -16,7 +16,7 @@ import { ProgressPayload } from '@/utils/transfer';
import { throttle } from '@/utils/throttle';
import { parseOpenWithFiles } from '@/helpers/openWith';
import { isTauriAppPlatform } from '@/services/environment';
import { checkForAppUpdates } from '@/helpers/updater';
import { checkForAppUpdates, checkAppReleaseNotes } from '@/helpers/updater';
import { FILE_ACCEPT_FORMATS, SUPPORTED_FILE_EXTS } from '@/services/constants';
import { impactFeedback } from '@tauri-apps/plugin-haptics';
import { getCurrentWebview } from '@tauri-apps/api/webview';
@@ -118,6 +118,8 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
const doCheckAppUpdates = async () => {
if (appService?.hasUpdater && settings.autoCheckUpdates) {
await checkForAppUpdates(_);
} else if (appService?.hasUpdater === false) {
checkAppReleaseNotes();
}
};
if (settings.alwaysOnTop) {
@@ -125,7 +127,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
}
doCheckAppUpdates();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settings]);
}, [appService?.hasUpdater, settings]);
useEffect(() => {
if (appService?.isMobileApp) {
@@ -683,7 +685,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
/>
)}
<AboutWindow />
{appService?.isAndroidApp && <UpdaterWindow />}
<UpdaterWindow />
<Toast />
</div>
);
@@ -118,7 +118,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
<Suspense>
<ReaderContent ids={ids} settings={settings} />
<AboutWindow />
{appService?.isAndroidApp && <UpdaterWindow />}
<UpdaterWindow />
<Toast />
</Suspense>
</div>
+4 -2
View File
@@ -5,7 +5,7 @@ import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
import { useOpenWithBooks } from '@/hooks/useOpenWithBooks';
import { useSettingsStore } from '@/store/settingsStore';
import { checkForAppUpdates } from '@/helpers/updater';
import { checkForAppUpdates, checkAppReleaseNotes } from '@/helpers/updater';
import Reader from './components/Reader';
// This is only used for the Tauri app in the app router
@@ -20,11 +20,13 @@ export default function Page() {
const doCheckAppUpdates = async () => {
if (appService?.hasUpdater && settings.autoCheckUpdates) {
await checkForAppUpdates(_);
} else if (appService?.hasUpdater === false) {
checkAppReleaseNotes();
}
};
doCheckAppUpdates();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settings]);
}, [appService?.hasUpdater, settings.autoCheckUpdates]);
return <Reader />;
}
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import Image from 'next/image';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
import { checkForAppUpdates } from '@/helpers/updater';
import { checkForAppUpdates, checkAppReleaseNotes } from '@/helpers/updater';
import { parseWebViewVersion } from '@/utils/ua';
import { getAppVersion } from '@/utils/version';
import Dialog from './Dialog';
@@ -50,9 +50,9 @@ export const AboutWindow = () => {
const handleCheckUpdate = async () => {
setUpdateStatus('checking');
try {
const update = await checkForAppUpdates(_, false);
if (update) {
setIsOpen(false);
const hasUpdate = await checkForAppUpdates(_, false);
if (hasUpdate) {
handleClose();
} else {
setUpdateStatus('updated');
}
@@ -62,6 +62,15 @@ export const AboutWindow = () => {
}
};
const handleShowRecentUpdates = async () => {
const hasNotes = await checkAppReleaseNotes(false);
if (hasNotes) {
handleClose();
} else {
setUpdateStatus('error');
}
};
const handleClose = () => {
setIsOpen(false);
setUpdateStatus(null);
@@ -87,8 +96,11 @@ export const AboutWindow = () => {
</p>
</div>
<div className='h-5'>
{appService?.hasUpdater && !updateStatus && (
<span className='badge badge-primary cursor-pointer p-1' onClick={handleCheckUpdate}>
{!updateStatus && (
<span
className='badge badge-primary cursor-pointer p-2'
onClick={appService?.hasUpdater ? handleCheckUpdate : handleShowRecentUpdates}
>
{_('Check Update')}
</span>
)}
+174 -82
View File
@@ -6,7 +6,8 @@ import { useEffect, useState } from 'react';
import { type as osType, arch as osArch } from '@tauri-apps/plugin-os';
import { check, Update } from '@tauri-apps/plugin-updater';
import { relaunch } from '@tauri-apps/plugin-process';
import { fetch } from '@tauri-apps/plugin-http';
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
import { isTauriAppPlatform } from '@/services/environment';
import { useTranslator } from '@/hooks/useTranslator';
import { useTranslation } from '@/hooks/useTranslation';
import { useSearchParams } from 'next/navigation';
@@ -14,8 +15,10 @@ import { getAppVersion } from '@/utils/version';
import { tauriDownload } from '@/utils/transfer';
import { installPackage } from '@/utils/bridge';
import { getLocale } from '@/utils/misc';
import { setLastShownReleaseNotesVersion } from '@/helpers/updater';
import { READEST_UPDATER_FILE, READEST_CHANGELOG_FILE } from '@/services/constants';
import Dialog from '@/components/Dialog';
import Link from './Link';
interface ReleaseNotes {
releases: Record<
@@ -58,7 +61,15 @@ interface GenericUpdate {
downloadAndInstall?(onEvent?: (progress: DownloadEvent) => void): Promise<void>;
}
export const UpdaterContent = ({ version }: { version?: string }) => {
export const UpdaterContent = ({
latestVersion,
lastVersion,
checkUpdate = true,
}: {
latestVersion?: string;
lastVersion?: string;
checkUpdate?: boolean;
}) => {
const _ = useTranslation();
const [targetLang, setTargetLang] = useState('EN');
const { translate } = useTranslator({
@@ -69,15 +80,19 @@ export const UpdaterContent = ({ version }: { version?: string }) => {
const { appService } = useEnv();
const searchParams = useSearchParams();
const currentVersion = getAppVersion();
const resolvedVersion = version ?? searchParams?.get('version') ?? '';
const [newVersion, setNewVersion] = useState(
latestVersion ?? searchParams?.get('latestVersion') ?? '',
);
const [oldVersion, setOldVersion] = useState(
lastVersion ?? searchParams?.get('lastVersion') ?? '',
);
const [update, setUpdate] = useState<GenericUpdate | Update | null>(null);
const [isMounted, setIsMounted] = useState(false);
const [newVersion, setNewVersion] = useState(resolvedVersion);
const [changelogs, setChangelogs] = useState<Changelog[]>([]);
const [progress, setProgress] = useState<number | null>(null);
const [contentLength, setContentLength] = useState<number | null>(null);
const [isDownloading, setIsDownloading] = useState(false);
const [downloaded, setDownloaded] = useState<number | null>(null);
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setTargetLang(getLocale());
@@ -91,6 +106,7 @@ export const UpdaterContent = ({ version }: { version?: string }) => {
}
};
const checkAndroidUpdate = async () => {
const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;
const response = await fetch(READEST_UPDATER_FILE);
const data = await response.json();
if (semver.gt(data.version, currentVersion)) {
@@ -156,30 +172,46 @@ export const UpdaterContent = ({ version }: { version?: string }) => {
checkAndroidUpdate();
}
};
checkForUpdates();
if (appService?.hasUpdater && checkUpdate) {
checkForUpdates();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [appService?.hasUpdater]);
useEffect(() => {
const fetchChangelogs = async (
fromVersion: string,
toVersion: string,
): Promise<Changelog[]> => {
if (latestVersion) {
setNewVersion(latestVersion);
}
if (lastVersion) {
setOldVersion(lastVersion);
}
if (!checkUpdate) {
setUpdate({
currentVersion,
version: latestVersion,
date: '',
body: '',
} as GenericUpdate);
}
}, [latestVersion, lastVersion, checkUpdate]);
useEffect(() => {
const fetchChangelogs = async (fromVersion: string): Promise<Changelog[]> => {
try {
const fetch = isTauriAppPlatform() ? tauriFetch : window.fetch;
const res = await fetch(READEST_CHANGELOG_FILE);
const data: ReleaseNotes = await res.json();
const releases = data.releases;
const entries = Object.entries(releases)
.filter(([ver]) => semver.gt(ver, fromVersion) && semver.lte(ver, toVersion))
.sort(([a], [b]) => semver.rcompare(a, b))
.map(([version, info]) => ({
version,
date: new Date(info.date).toLocaleDateString(),
notes: info.notes,
}));
return entries;
let entries = Object.entries(releases)
.filter(([ver]) => semver.gt(ver, fromVersion))
.sort(([a], [b]) => semver.rcompare(a, b));
entries = entries.length ? entries : Object.entries(releases).slice(0, 3);
return entries.map(([version, info]) => ({
version,
date: new Date(info.date).toLocaleDateString(),
notes: info.notes,
}));
} catch (error) {
console.error('Failed to fetch changelog:', error);
return [];
@@ -193,25 +225,28 @@ export const UpdaterContent = ({ version }: { version?: string }) => {
};
const updateChangelogs = async (update: GenericUpdate) => {
setNewVersion(update.version);
let changelogs = await fetchChangelogs(currentVersion, update.version);
if (changelogs.length === 0) {
let changelogs = await fetchChangelogs(oldVersion || currentVersion);
if (changelogs.length === 0 && update.date && update.body) {
changelogs = [
{
version: update.version,
date: new Date(update.date!).toLocaleDateString(),
date: new Date(update.date).toLocaleDateString(),
notes: parseNumberedList(update.body ?? ''),
},
];
}
for (const entry of changelogs) {
try {
entry.notes = await translate(entry.notes, { useCache: true });
} catch (error) {
console.log('Failed to translate changelog:', error);
if (!targetLang.toLowerCase().startsWith('en')) {
for (const entry of changelogs) {
try {
entry.notes = await translate(entry.notes, { useCache: true });
} catch (error) {
console.log('Failed to translate changelog:', error);
}
}
}
setChangelogs(changelogs);
setLastShownReleaseNotesVersion(newVersion);
};
if (update) {
updateChangelogs(update);
@@ -272,54 +307,94 @@ export const UpdaterContent = ({ version }: { version?: string }) => {
<Image src='/icon.png' alt='Logo' className='h-20 w-20' width={64} height={64} />
</div>
<div className='text-base-content flex-grow text-sm'>
<h2 className='mb-4 text-center font-bold sm:text-start'>
{_('A new version of Readest is available!')}
</h2>
<p className='mb-2'>
{_('Readest {{newVersion}} is available (installed version {{currentVersion}}).', {
newVersion,
currentVersion,
})}
</p>
<p className='mb-2'>{_('Download and install now?')}</p>
{checkUpdate ? (
<div className='text-base-content flex-grow text-sm'>
<h2 className='mb-4 text-center font-bold sm:text-start'>
{_('A new version of Readest is available!')}
</h2>
<p className='mb-2'>
{_('Readest {{newVersion}} is available (installed version {{currentVersion}}).', {
newVersion,
currentVersion,
})}
</p>
<p className='mb-2'>{_('Download and install now?')}</p>
<div className='flex w-full flex-row items-center justify-end gap-4'>
{progress !== null && (
<div className='flex flex-grow flex-col'>
<progress
className='progress my-1 h-4 w-full'
value={progress}
max='100'
></progress>
<p className='text-base-content/75 flex items-center justify-center text-sm'>
{progress < 100
? _('Downloading {{downloaded}} of {{contentLength}}', {
downloaded: downloaded
? `${Math.floor(downloaded / 1024 / 1024)} MB`
: '0 MB',
contentLength: contentLength
? `${Math.floor(contentLength / 1024 / 1024)} MB`
: '0 MB',
})
: _('Download finished')}
</p>
<div className='flex w-full flex-row items-center justify-end gap-4'>
{progress !== null && (
<div className='flex flex-grow flex-col'>
<progress
className='progress my-1 h-4 w-full'
value={progress}
max='100'
></progress>
<p className='text-base-content/75 flex items-center justify-center text-sm'>
{progress < 100
? _('Downloading {{downloaded}} of {{contentLength}}', {
downloaded: downloaded
? `${Math.floor(downloaded / 1024 / 1024)} MB`
: '0 MB',
contentLength: contentLength
? `${Math.floor(contentLength / 1024 / 1024)} MB`
: '0 MB',
})
: _('Download finished')}
</p>
</div>
)}
<div className='card-actions'>
<button
className={clsx(
'btn btn-warning text-base-100 px-6 font-bold',
(!update || isDownloading) && 'btn-disabled',
)}
onClick={handleDownloadInstall}
>
{_('DOWNLOAD & INSTALL')}
</button>
</div>
)}
<div className='card-actions'>
<button
className={clsx(
'btn btn-warning text-base-100 px-6 font-bold',
(!update || isDownloading) && 'btn-disabled',
)}
onClick={handleDownloadInstall}
>
{_('DOWNLOAD & INSTALL')}
</button>
</div>
</div>
</div>
) : (
<div className='text-base-content flex h-full flex-grow flex-col text-sm sm:flex-row'>
<div className='flex flex-col items-center justify-center gap-4 p-1 sm:items-start sm:gap-2'>
<h2 className='text-center font-bold sm:text-start'>
{_('Version {{version}}', { version: currentVersion })}
</h2>
{changelogs.length > 0 && semver.gt(changelogs[0]!.version, currentVersion) ? (
<div className='flex gap-2'>
{(appService?.isIOSApp || appService?.isMacOSApp) && (
<Link
href='https://apps.apple.com/app/id6738622779'
target='_blank'
rel='noopener noreferrer'
className='btn btn-primary btn-sm'
>
{_('Check Update')}
</Link>
)}
{appService?.isAndroidApp && (
<Link
href='https://play.google.com/store/apps/details?id=com.bilingify.readest'
target='_blank'
rel='noopener noreferrer'
className='btn btn-primary btn-sm'
>
{_('Check Update')}
</Link>
)}
</div>
) : (
<div className='flex'>
<p className='text-sm font-bold'>{_('Already the latest version')}</p>
</div>
)}
</div>
</div>
)}
</div>
<div className='text-base-content text-sm'>
<h3 className='mb-2 font-bold'>{_('Changelog:')}</h3>
@@ -351,11 +426,16 @@ export const UpdaterContent = ({ version }: { version?: string }) => {
);
};
export const setUpdaterWindowVisible = (visible: boolean, newVersion?: string) => {
export const setUpdaterWindowVisible = (
visible: boolean,
latestVersion: string,
lastVersion?: string,
checkUpdate = true,
) => {
const dialog = document.getElementById('updater_window');
if (dialog) {
const event = new CustomEvent('setDialogVisibility', {
detail: { visible, newVersion },
detail: { visible, latestVersion, lastVersion, checkUpdate },
});
dialog.dispatchEvent(event);
}
@@ -363,15 +443,21 @@ export const setUpdaterWindowVisible = (visible: boolean, newVersion?: string) =
export const UpdaterWindow = () => {
const _ = useTranslation();
const [newVersion, setNewVersion] = useState('');
const [latestVersion, setLatestVersion] = useState('');
const [lastVersion, setLastVersion] = useState('');
const [checkUpdate, setCheckUpdate] = useState(true);
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const handleCustomEvent = (event: CustomEvent) => {
const { visible, newVersion } = event.detail;
const { visible, latestVersion, lastVersion, checkUpdate } = event.detail;
setIsOpen(visible);
if (newVersion) {
setNewVersion(newVersion);
setCheckUpdate(checkUpdate);
if (latestVersion) {
setLatestVersion(latestVersion);
}
if (lastVersion) {
setLastVersion(lastVersion);
}
};
@@ -391,11 +477,17 @@ export const UpdaterWindow = () => {
<Dialog
id='updater_window'
isOpen={isOpen}
title={_('Software Update')}
title={checkUpdate ? _('Software Update') : _("What's New in Readest")}
onClose={() => setIsOpen(false)}
boxClassName='sm:!w-[80%] sm:h-auto'
boxClassName='sm:!w-[75%] sm:h-auto sm:!max-h-[85vh] sm:!max-w-2xl'
>
<UpdaterContent version={newVersion ?? undefined} />
{isOpen && (
<UpdaterContent
latestVersion={latestVersion ?? undefined}
lastVersion={lastVersion ?? undefined}
checkUpdate={checkUpdate}
/>
)}
</Dialog>
);
};
+41 -6
View File
@@ -5,14 +5,19 @@ import { fetch } from '@tauri-apps/plugin-http';
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
import { TranslationFunc } from '@/hooks/useTranslation';
import { setUpdaterWindowVisible } from '@/components/UpdaterWindow';
import { isTauriAppPlatform } from '@/services/environment';
import { getAppVersion } from '@/utils/version';
import { CHECK_UPDATE_INTERVAL_SEC, READEST_UPDATER_FILE } from '@/services/constants';
import {
CHECK_UPDATE_INTERVAL_SEC,
READEST_CHANGELOG_FILE,
READEST_UPDATER_FILE,
} from '@/services/constants';
const LAST_CHECK_KEY = 'lastAppUpdateCheck';
const showUpdateWindow = (newVersion: string) => {
const showUpdateWindow = (latestVersion: string) => {
const win = new WebviewWindow('updater', {
url: `/updater?version=${newVersion}`,
url: `/updater?latestVersion=${latestVersion}`,
title: 'Software Update',
width: 626,
height: 406,
@@ -29,11 +34,11 @@ const showUpdateWindow = (newVersion: string) => {
export const checkForAppUpdates = async (
_: TranslationFunc,
autoCheck = true,
isAutoCheck = true,
): Promise<boolean> => {
const lastCheck = localStorage.getItem(LAST_CHECK_KEY);
const now = Date.now();
if (autoCheck && lastCheck && now - parseInt(lastCheck, 10) < CHECK_UPDATE_INTERVAL_SEC * 1000)
if (isAutoCheck && lastCheck && now - parseInt(lastCheck, 10) < CHECK_UPDATE_INTERVAL_SEC * 1000)
return false;
localStorage.setItem(LAST_CHECK_KEY, now.toString());
@@ -51,7 +56,7 @@ export const checkForAppUpdates = async (
const data = await response.json();
const isNewer = semver.gt(data.version, getAppVersion());
if (isNewer && ('android-arm64' in data.platforms || 'android-universal' in data.platforms)) {
setUpdaterWindowVisible(true, data.version);
setUpdaterWindowVisible(true, data.version!, getAppVersion());
}
return isNewer;
} catch (err) {
@@ -62,3 +67,33 @@ export const checkForAppUpdates = async (
return false;
};
const LAST_SHOWN_RELEASE_NOTES_KEY = 'lastShownReleaseNotesVersion';
export const setLastShownReleaseNotesVersion = (version: string) => {
localStorage.setItem(LAST_SHOWN_RELEASE_NOTES_KEY, version);
};
export const getLastShownReleaseNotesVersion = () => {
return localStorage.getItem(LAST_SHOWN_RELEASE_NOTES_KEY) || '';
};
export const checkAppReleaseNotes = async (isAutoCheck = true) => {
const currentVersion = getAppVersion();
const lastShownVersion = getLastShownReleaseNotesVersion();
if ((lastShownVersion && semver.gt(currentVersion, lastShownVersion)) || !isAutoCheck) {
try {
const fetchFunc = isTauriAppPlatform() ? fetch : window.fetch;
const res = await fetchFunc(READEST_CHANGELOG_FILE);
if (res.ok) {
setUpdaterWindowVisible(true, currentVersion, lastShownVersion, false);
return true;
}
} catch (err) {
console.warn('Failed to fetch release notes', err);
}
} else if (!lastShownVersion) {
setLastShownReleaseNotesVersion(currentVersion);
}
return false;
};