Bump app version to 0.5.1 and add auto-update support
- Implemented auto-update logic in `ScribeTopBar` with update notification and user interaction. - Integrated `@tauri-apps/plugin-updater` and `@tauri-apps/plugin-process` for updater functionality. - Added automatic migration feature with `autoMigrateElectron` support and UI feedback. - Refactored app architecture with new routing, components, and layout for better modularity. - Enhanced JSON response handling in API client for robust data parsing. - Updated locales to include new translations for update and migration-related UI.
This commit is contained in:
137
app/main.tsx
Normal file
137
app/main.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import {BrowserRouter, Routes, Route, Outlet} from 'react-router-dom';
|
||||
import '@/app/globals.css';
|
||||
import '@/lib/i18n';
|
||||
import {listen} from '@tauri-apps/api/event';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import PulseLoader from '@/components/ui/PulseLoader';
|
||||
import {useTranslations} from '@/lib/i18n';
|
||||
|
||||
listen('auth-success', () => window.location.reload());
|
||||
|
||||
type MigrationState = 'pending' | 'error' | 'done';
|
||||
|
||||
function AppInitializer({children}: { children: React.ReactNode }) {
|
||||
const t = useTranslations();
|
||||
const [state, setState] = useState<MigrationState>('pending');
|
||||
const [retryCount, setRetryCount] = useState<number>(0);
|
||||
|
||||
useEffect(function (): void {
|
||||
setState('pending');
|
||||
tauri.autoMigrateElectron().then(function (result): void {
|
||||
if (result.migrated) {
|
||||
window.location.reload();
|
||||
} else if (result.error) {
|
||||
setState('error');
|
||||
} else {
|
||||
setState('done');
|
||||
}
|
||||
}).catch(function (): void {
|
||||
setState('error');
|
||||
});
|
||||
}, [retryCount]);
|
||||
|
||||
if (state === 'pending') {
|
||||
return (
|
||||
<div className="bg-background text-text-primary h-screen flex flex-col items-center justify-center gap-4 font-['Lora']">
|
||||
<PulseLoader text={t('migration.autoMigrating')}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (state === 'error') {
|
||||
return (
|
||||
<div className="bg-background text-text-primary h-screen flex flex-col items-center justify-center gap-6 font-['Lora'] p-8">
|
||||
<div className="flex flex-col items-center gap-3 max-w-md text-center">
|
||||
<p className="text-lg font-semibold">{t('migration.autoErrorTitle')}</p>
|
||||
<p className="text-sm text-text-secondary">{t('migration.autoErrorText')}</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<button
|
||||
className="px-6 py-2 bg-primary text-white rounded-lg hover:opacity-90 transition-opacity"
|
||||
onClick={function (): void { setRetryCount(function (n): number { return n + 1; }); }}
|
||||
>
|
||||
{t('migration.autoErrorRetry')}
|
||||
</button>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<button
|
||||
className="px-6 py-2 text-sm text-text-secondary hover:text-text-primary underline transition-colors"
|
||||
onClick={function (): void { setState('done'); }}
|
||||
>
|
||||
{t('migration.autoErrorContinue')}
|
||||
</button>
|
||||
<p className="text-xs text-text-secondary opacity-70">{t('migration.autoErrorContinueWarning')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
import ScribeShell from '@/components/layout/ScribeShell';
|
||||
import LoginWrapper from '@/app/login/LoginWrapper';
|
||||
|
||||
import HomePage from '@/app/HomePage';
|
||||
import BookPage from '@/app/book/BookPage';
|
||||
import ChapterPage from '@/app/book/ChapterPage';
|
||||
import BookLayout from '@/app/book/BookLayout';
|
||||
|
||||
import LoginPage from '@/app/login/login/page';
|
||||
import RegisterPage from '@/app/login/register/page';
|
||||
import ResetPasswordPage from '@/app/login/reset-password/page';
|
||||
import OfflineLoginPage from '@/app/login/offline/page';
|
||||
|
||||
function LoginShell() {
|
||||
return (
|
||||
<LoginWrapper>
|
||||
<Outlet/>
|
||||
</LoginWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function MainShell() {
|
||||
return (
|
||||
<ScribeShell>
|
||||
<Outlet/>
|
||||
</ScribeShell>
|
||||
);
|
||||
}
|
||||
|
||||
function BookShell() {
|
||||
return (
|
||||
<BookLayout>
|
||||
<Outlet/>
|
||||
</BookLayout>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<AppInitializer>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
{/* Login routes — dedicated window, no ScribeShell */}
|
||||
<Route element={<LoginShell/>}>
|
||||
<Route path="/login" element={<LoginPage/>}/>
|
||||
<Route path="/login/login" element={<LoginPage/>}/>
|
||||
<Route path="/login/register" element={<RegisterPage/>}/>
|
||||
<Route path="/login/reset-password" element={<ResetPasswordPage/>}/>
|
||||
<Route path="/login/offline" element={<OfflineLoginPage/>}/>
|
||||
</Route>
|
||||
|
||||
{/* Main app routes — with ScribeShell */}
|
||||
<Route element={<MainShell/>}>
|
||||
<Route path="/" element={<HomePage/>}/>
|
||||
<Route element={<BookShell/>}>
|
||||
<Route path="/book/:bookId" element={<BookPage/>}/>
|
||||
<Route path="/book/:bookId/chapter/:chapterId" element={<ChapterPage/>}/>
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</AppInitializer>
|
||||
</React.StrictMode>
|
||||
);
|
||||
Reference in New Issue
Block a user