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:
5
app/HomePage.tsx
Normal file
5
app/HomePage.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import BookList from '@/components/book/BookList';
|
||||
|
||||
export default function HomePage() {
|
||||
return <BookList/>;
|
||||
}
|
||||
1
app/book/BookLayout.tsx
Normal file
1
app/book/BookLayout.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export {default} from '@/app/book/[bookId]/layout';
|
||||
1
app/book/BookPage.tsx
Normal file
1
app/book/BookPage.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export {default} from '@/app/book/[bookId]/page';
|
||||
1
app/book/ChapterPage.tsx
Normal file
1
app/book/ChapterPage.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export {default} from '@/app/book/[bookId]/chapter/[chapterId]/page';
|
||||
77
app/book/[bookId]/chapter/[chapterId]/page.tsx
Normal file
77
app/book/[bookId]/chapter/[chapterId]/page.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
'use client';
|
||||
import React, {useContext, useEffect, useRef} from 'react';
|
||||
import {useParams} from '@/lib/navigation';
|
||||
import {ChapterContext, ChapterContextProps} from '@/context/ChapterContext';
|
||||
import {SessionContext, SessionContextProps} from '@/context/SessionContext';
|
||||
import {LangContext, LangContextProps} from '@/context/LangContext';
|
||||
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
|
||||
import {apiGet} from '@/lib/api/client';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {BookContext, BookContextProps} from '@/context/BookContext';
|
||||
import {ChapterProps} from '@/lib/types/chapter';
|
||||
import {useTranslations} from '@/lib/i18n';
|
||||
import TextEditor from '@/components/editor/TextEditor';
|
||||
|
||||
export default function ChapterPage() {
|
||||
const params: { bookId: string; chapterId: string } = useParams<{ bookId: string; chapterId: string }>();
|
||||
const {chapter, setChapter}: ChapterContextProps = useContext<ChapterContextProps>(ChapterContext);
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
|
||||
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
|
||||
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
||||
const t = useTranslations();
|
||||
const hasFetched = useRef<string>('');
|
||||
|
||||
useEffect((): void => {
|
||||
if (!session.accessToken || !params.chapterId) return;
|
||||
if (chapter && chapter.chapterId === params.chapterId) {
|
||||
hasFetched.current = params.chapterId;
|
||||
return;
|
||||
}
|
||||
if (hasFetched.current === params.chapterId) return;
|
||||
hasFetched.current = params.chapterId;
|
||||
fetchChapter().then();
|
||||
}, [params.chapterId, session.accessToken, chapter]);
|
||||
|
||||
async function fetchChapter(): Promise<void> {
|
||||
try {
|
||||
const isFirstLoad: boolean = !chapter;
|
||||
let response: ChapterProps | null;
|
||||
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
||||
if (isFirstLoad) {
|
||||
response = await tauri.getLastChapter(params.bookId);
|
||||
} else {
|
||||
response = await tauri.getWholeChapter(params.chapterId, chapter!.chapterContent.version, params.bookId);
|
||||
}
|
||||
} else {
|
||||
const endpoint: string = isFirstLoad ? 'chapter/last-chapter' : 'chapter/whole';
|
||||
const queryParams: Record<string, string | number> = isFirstLoad
|
||||
? {bookid: params.bookId}
|
||||
: {bookid: params.bookId, id: params.chapterId, version: chapter!.chapterContent.version};
|
||||
response = await apiGet<ChapterProps | null>(
|
||||
endpoint, session.accessToken, lang, queryParams
|
||||
);
|
||||
}
|
||||
if (!response) {
|
||||
errorMessage(t('scribeChapterComponent.errorFetchChapter'));
|
||||
return;
|
||||
}
|
||||
setChapter(response);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('scribeChapterComponent.errorFetchChapter'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!chapter || chapter.chapterId !== params.chapterId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <TextEditor/>;
|
||||
}
|
||||
86
app/book/[bookId]/layout.tsx
Normal file
86
app/book/[bookId]/layout.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
import React, {ReactNode, useContext, useEffect} from 'react';
|
||||
import {useParams} from '@/lib/navigation';
|
||||
import {BookContext, BookContextProps} from '@/context/BookContext';
|
||||
import {SessionContext, SessionContextProps} from '@/context/SessionContext';
|
||||
import {LangContext, LangContextProps} from '@/context/LangContext';
|
||||
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
|
||||
import {apiGet} from '@/lib/api/client';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {BookProps} from '@/lib/types/book';
|
||||
import {useTranslations} from '@/lib/i18n';
|
||||
import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext';
|
||||
import {SyncedBook} from '@/lib/types/synced-book';
|
||||
|
||||
export default function BookLayout({children}: { children: ReactNode }) {
|
||||
const params: { bookId: string } = useParams<{ bookId: string }>();
|
||||
const {book, setBook}: BookContextProps = useContext<BookContextProps>(BookContext);
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
|
||||
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
||||
const {localOnlyBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
const t = useTranslations();
|
||||
|
||||
useEffect((): void => {
|
||||
if (session.accessToken && params.bookId) {
|
||||
if (!book || book.bookId !== params.bookId) {
|
||||
fetchBook().then();
|
||||
}
|
||||
}
|
||||
}, [params.bookId, session.accessToken]);
|
||||
|
||||
async function fetchBook(): Promise<void> {
|
||||
try {
|
||||
let localBookOnly: boolean = false;
|
||||
let bookResponse: BookProps | null = null;
|
||||
|
||||
if (isCurrentlyOffline()) {
|
||||
bookResponse = await tauri.getBookBasicInformation(params.bookId);
|
||||
if (bookResponse) localBookOnly = true;
|
||||
} else {
|
||||
const isOfflineBook = localOnlyBooks.find((b: SyncedBook): boolean => b.id === params.bookId);
|
||||
if (isOfflineBook) {
|
||||
bookResponse = await tauri.getBookBasicInformation(params.bookId);
|
||||
localBookOnly = true;
|
||||
}
|
||||
if (!bookResponse) {
|
||||
bookResponse = await apiGet<BookProps>(
|
||||
'book/basic-information', session.accessToken, lang, {id: params.bookId}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!bookResponse) {
|
||||
errorMessage(t('controllerBar.bookNotFound'));
|
||||
return;
|
||||
}
|
||||
setBook({
|
||||
bookId: bookResponse.bookId,
|
||||
type: bookResponse.type,
|
||||
title: bookResponse.title,
|
||||
subTitle: bookResponse.subTitle,
|
||||
summary: bookResponse.summary,
|
||||
publicationDate: bookResponse.publicationDate,
|
||||
desiredWordCount: bookResponse.desiredWordCount,
|
||||
totalWordCount: bookResponse.totalWordCount ?? 0,
|
||||
quillsenseEnabled: bookResponse.quillsenseEnabled,
|
||||
tools: bookResponse.tools,
|
||||
seriesId: bookResponse.seriesId,
|
||||
serie: bookResponse.serie,
|
||||
coverImage: bookResponse.coverImage ? 'data:image/jpeg;base64,' + bookResponse.coverImage : '',
|
||||
localBook: localBookOnly,
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('controllerBar.unknownBookError'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
67
app/book/[bookId]/page.tsx
Normal file
67
app/book/[bookId]/page.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
'use client';
|
||||
import React, {useContext, useEffect, useState} from 'react';
|
||||
import {useParams, useRouter} from '@/lib/navigation';
|
||||
import {SessionContext, SessionContextProps} from '@/context/SessionContext';
|
||||
import {LangContext, LangContextProps} from '@/context/LangContext';
|
||||
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
|
||||
import {ChapterContext, ChapterContextProps} from '@/context/ChapterContext';
|
||||
import {apiGet} from '@/lib/api/client';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {BookContext, BookContextProps} from '@/context/BookContext';
|
||||
import {ChapterProps} from '@/lib/types/chapter';
|
||||
import {useTranslations} from '@/lib/i18n';
|
||||
import NoBookHome from '@/components/editor/NoBookHome';
|
||||
|
||||
export default function BookPage() {
|
||||
const params: { bookId: string } = useParams<{ bookId: string }>();
|
||||
const router = useRouter();
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
|
||||
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {setChapter}: ChapterContextProps = useContext<ChapterContextProps>(ChapterContext);
|
||||
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
|
||||
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
||||
const t = useTranslations();
|
||||
|
||||
const [isRedirecting, setIsRedirecting] = useState<boolean>(true);
|
||||
|
||||
useEffect((): void => {
|
||||
if (session.accessToken && params.bookId) {
|
||||
redirectToLastChapter().then();
|
||||
}
|
||||
}, [session.accessToken, params.bookId]);
|
||||
|
||||
async function redirectToLastChapter(): Promise<void> {
|
||||
try {
|
||||
let response: ChapterProps | null;
|
||||
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
||||
response = await tauri.getLastChapter(params.bookId);
|
||||
} else {
|
||||
response = await apiGet<ChapterProps | null>(
|
||||
'chapter/last-chapter', session.accessToken, lang, {bookid: params.bookId}
|
||||
);
|
||||
}
|
||||
if (response) {
|
||||
setChapter(response);
|
||||
router.replace(`/book/${params.bookId}/chapter/${response.chapterId}`);
|
||||
return;
|
||||
}
|
||||
setIsRedirecting(false);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('homePage.errors.lastChapterError'));
|
||||
}
|
||||
setIsRedirecting(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (isRedirecting) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <NoBookHome/>;
|
||||
}
|
||||
1
app/login/page.tsx
Normal file
1
app/login/page.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export {default} from './login/page';
|
||||
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>
|
||||
);
|
||||
@@ -38,7 +38,6 @@ import OfflineProvider from '@/context/OfflineProvider';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import OfflinePinSetup from '@/components/offline/OfflinePinSetup';
|
||||
import OfflinePinVerify from '@/components/offline/OfflinePinVerify';
|
||||
import MigrationModal from '@/components/migration/MigrationModal';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import useSyncBooks from '@/hooks/useSyncBooks';
|
||||
@@ -137,16 +136,6 @@ function ScribeContent({children}: { children: ReactNode }) {
|
||||
const [currentChapter, setCurrentChapter] = useState<ChapterProps | undefined>(undefined);
|
||||
const [currentBook, setCurrentBook] = useState<BookProps | null>(null);
|
||||
const [bookSettingId, setBookSettingId] = useState<string>('');
|
||||
const [showMigrationPopup, setShowMigrationPopup] = useState<boolean>(false);
|
||||
|
||||
useEffect(function (): void {
|
||||
if (!isDesktop) return;
|
||||
const done: boolean = localStorage.getItem('electron_migration_done') === 'true';
|
||||
const dismissed: boolean = localStorage.getItem('electron_migration_dismissed') === 'true';
|
||||
if (!done && !dismissed) {
|
||||
setShowMigrationPopup(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [serverSyncedBooks, setServerSyncedBooks] = useState<SyncedBook[]>([]);
|
||||
const [localSyncedBooks, setLocalSyncedBooks] = useState<SyncedBook[]>([]);
|
||||
@@ -437,12 +426,6 @@ function ScribeContent({children}: { children: ReactNode }) {
|
||||
onCancel={(): void => {}}
|
||||
/>
|
||||
)}
|
||||
{showMigrationPopup && (
|
||||
<MigrationModal
|
||||
onClose={function (): void { setShowMigrationPopup(false); }}
|
||||
onSuccess={function (): void { setShowMigrationPopup(false); window.location.reload(); }}
|
||||
/>
|
||||
)}
|
||||
</SettingBookContext.Provider>
|
||||
</AIUsageContext.Provider>
|
||||
</ChapterContext.Provider>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
const logo = "/eritors-favicon-white.png";
|
||||
import React, {useContext} from "react";
|
||||
import {Download, X} from "lucide-react";
|
||||
import {BookContext, BookContextProps} from "@/context/BookContext";
|
||||
import {useTranslations} from '@/lib/i18n';
|
||||
import {useAutoUpdate} from "@/hooks/useAutoUpdate";
|
||||
|
||||
export default function ScribeTopBar() {
|
||||
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
|
||||
const t = useTranslations();
|
||||
const update = useAutoUpdate();
|
||||
return (
|
||||
<div className="flex items-center justify-between px-6 py-3 bg-tertiary border-b border-secondary">
|
||||
<div className="flex items-center space-x-4 group">
|
||||
@@ -33,6 +36,21 @@ export default function ScribeTopBar() {
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center space-x-2 min-w-[120px] justify-end">
|
||||
{update.available && (
|
||||
<div className="flex items-center gap-2 bg-primary/15 border border-primary/30 rounded-lg px-3 py-1.5">
|
||||
<span className="text-xs text-text-secondary">v{update.version}</span>
|
||||
<button onClick={update.install} disabled={update.downloading}
|
||||
className="flex items-center gap-1 text-xs font-medium text-primary hover:text-primary-light transition-colors">
|
||||
<Download className="w-3.5 h-3.5" strokeWidth={2}/>
|
||||
{update.downloading ? t("scribeTopBar.updating") : t("scribeTopBar.update")}
|
||||
</button>
|
||||
{!update.downloading && (
|
||||
<button onClick={update.dismiss} className="text-text-secondary hover:text-text-primary transition-colors">
|
||||
<X className="w-3.5 h-3.5" strokeWidth={2}/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import {ArrowRightLeft, ExternalLink, Feather, Globe, Info, MapPin, MessageCircle, Users, Wand2, X} from 'lucide-react';
|
||||
import {ExternalLink, Feather, Globe, Info, MapPin, MessageCircle, Users, Wand2, X} from 'lucide-react';
|
||||
import React, {lazy, Suspense, useContext, useEffect, useState} from "react";
|
||||
import {BookContext, BookContextProps} from "@/context/BookContext";
|
||||
import {PanelComponent} from "@/lib/types/editor";
|
||||
@@ -12,8 +12,6 @@ import InsetPanel from "@/components/ui/InsetPanel";
|
||||
import IconButton from "@/components/ui/IconButton";
|
||||
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import MigrationModal from '@/components/migration/MigrationModal';
|
||||
|
||||
// Lazy loaded Editor components
|
||||
const WorldEditor = lazy(function () {
|
||||
return import('@/components/book/settings/world/editor/WorldEditor');
|
||||
@@ -54,11 +52,6 @@ export default function ComposerRightBar(): React.JSX.Element {
|
||||
const [panelHidden, setPanelHidden] = useState<boolean>(false);
|
||||
const [currentPanel, setCurrentPanel] = useState<PanelComponent | undefined>();
|
||||
const [showAbout, setShowAbout] = useState<boolean>(false);
|
||||
const [showMigration, setShowMigration] = useState<boolean>(false);
|
||||
|
||||
const migrationDone: boolean = localStorage.getItem('electron_migration_done') === 'true';
|
||||
const migrationDismissed: boolean = localStorage.getItem('electron_migration_dismissed') === 'true';
|
||||
const showMigrationButton: boolean = isDesktop && !migrationDone;
|
||||
|
||||
function togglePanel(component: PanelComponent): void {
|
||||
if (panelHidden) {
|
||||
@@ -110,16 +103,6 @@ export default function ComposerRightBar(): React.JSX.Element {
|
||||
];
|
||||
|
||||
const homeComponents: PanelComponent[] = [
|
||||
...(showMigrationButton ? [{
|
||||
id: 0,
|
||||
title: t("composerRightBar.homeComponents.migration.title"),
|
||||
description: t("composerRightBar.homeComponents.migration.description"),
|
||||
badge: 'IMPORT',
|
||||
icon: ArrowRightLeft,
|
||||
action: function (): void {
|
||||
setShowMigration(true);
|
||||
}
|
||||
}] : []),
|
||||
{
|
||||
id: 1,
|
||||
title: t("composerRightBar.homeComponents.about.title"),
|
||||
@@ -241,10 +224,6 @@ export default function ComposerRightBar(): React.JSX.Element {
|
||||
{showAbout && <AboutEditors onClose={function (): void {
|
||||
setShowAbout(false);
|
||||
}}/>}
|
||||
{showMigration && <MigrationModal
|
||||
onClose={function (): void { setShowMigration(false); }}
|
||||
onSuccess={function (): void { setShowMigration(false); window.location.reload(); }}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,11 @@ async function handleResponse<T>(response: Response): Promise<T> {
|
||||
const body = await response.json().catch(() => ({message: response.statusText}));
|
||||
throw new ApiError(body.message || body || response.statusText, response.status);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
const contentType = response.headers.get('content-type') ?? '';
|
||||
if (contentType.includes('application/json')) {
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
return response.text() as unknown as Promise<T>;
|
||||
}
|
||||
|
||||
export async function apiGet<T>(url: string, auth: string, lang: string = "fr", params: Record<string, unknown> = {}): Promise<T> {
|
||||
|
||||
@@ -55,7 +55,9 @@
|
||||
"scribeTopBar": {
|
||||
"logoAlt": "Logo",
|
||||
"scribe": "Scribe",
|
||||
"separator": " - "
|
||||
"separator": " - ",
|
||||
"update": "Update",
|
||||
"updating": "Updating..."
|
||||
},
|
||||
"scribeLeftBar": {
|
||||
"editorComponents": {
|
||||
@@ -217,7 +219,13 @@
|
||||
"retry": "Retry",
|
||||
"fileNotFound": "The migration file was not found at this path.",
|
||||
"dbNotFound": "The database was not found next to the migration file.",
|
||||
"importFailed": "Import failed. Please check that the files are valid."
|
||||
"importFailed": "Import failed. Please check that the files are valid.",
|
||||
"autoMigrating": "Migrating your data...",
|
||||
"autoErrorTitle": "Automatic migration failed",
|
||||
"autoErrorText": "Your data from the previous version could not be recovered.",
|
||||
"autoErrorContinue": "Continue without migrating",
|
||||
"autoErrorContinueWarning": "Warning: your local data will not be recovered.",
|
||||
"autoErrorRetry": "Retry"
|
||||
},
|
||||
"quillSense": {
|
||||
"needSubscription": "Please subscribe to QuillSense or bring your keys to access this feature.",
|
||||
|
||||
@@ -55,7 +55,9 @@
|
||||
"scribeTopBar": {
|
||||
"logoAlt": "Logo",
|
||||
"scribe": "Scribe",
|
||||
"separator": " - "
|
||||
"separator": " - ",
|
||||
"update": "Mettre à jour",
|
||||
"updating": "Mise à jour..."
|
||||
},
|
||||
"scribeLeftBar": {
|
||||
"editorComponents": {
|
||||
@@ -217,7 +219,13 @@
|
||||
"retry": "Réessayer",
|
||||
"fileNotFound": "Le fichier de migration est introuvable à ce chemin.",
|
||||
"dbNotFound": "La base de données n'a pas été trouvée à côté du fichier de migration.",
|
||||
"importFailed": "L'importation a échoué. Vérifiez que les fichiers sont valides."
|
||||
"importFailed": "L'importation a échoué. Vérifiez que les fichiers sont valides.",
|
||||
"autoMigrating": "Migration des données en cours...",
|
||||
"autoErrorTitle": "La migration automatique a échoué",
|
||||
"autoErrorText": "Vos données de l'ancienne version n'ont pas pu être récupérées.",
|
||||
"autoErrorContinue": "Continuer sans migrer",
|
||||
"autoErrorContinueWarning": "Attention : vos données locales ne seront pas récupérées.",
|
||||
"autoErrorRetry": "Réessayer"
|
||||
},
|
||||
"quillSense": {
|
||||
"needSubscription": "Veuillez vous abonner à QuillSense ou Amenez vos clés pour accéder à cette fonctionnalité.",
|
||||
|
||||
14
lib/tauri.ts
14
lib/tauri.ts
@@ -700,6 +700,20 @@ export async function importFromElectron(migrationFilePath: string): Promise<Mig
|
||||
return invoke<MigrationResult>('import_from_electron', {data: {migrationFilePath}});
|
||||
}
|
||||
|
||||
export interface AutoMigrationResult {
|
||||
migrated: boolean;
|
||||
userId: string | null;
|
||||
tokenMigrated: boolean;
|
||||
keyMigrated: boolean;
|
||||
pinMigrated: boolean;
|
||||
dbMigrated: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export async function autoMigrateElectron(): Promise<AutoMigrationResult> {
|
||||
return invoke<AutoMigrationResult>('auto_migrate_electron');
|
||||
}
|
||||
|
||||
// ─── Window Management ──────────────────────────────────────
|
||||
|
||||
let loginWindowOpening = false;
|
||||
|
||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -12,7 +12,9 @@
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-http": "^2.5.8",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-shell": "^2.3.5",
|
||||
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||
"@tiptap/extension-color": "^3.10.7",
|
||||
"@tiptap/extension-gapcursor": "^3.10.7",
|
||||
"@tiptap/extension-highlight": "^3.10.7",
|
||||
@@ -1013,6 +1015,15 @@
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-process": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.1.tgz",
|
||||
"integrity": "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-shell": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz",
|
||||
@@ -1022,6 +1033,15 @@
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-updater": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.10.1.tgz",
|
||||
"integrity": "sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/core": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.19.0.tgz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eritorsscribe",
|
||||
"productName": "ERitors Scribe",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 4000",
|
||||
@@ -32,7 +32,9 @@
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-http": "^2.5.8",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-shell": "^2.3.5",
|
||||
"@tauri-apps/plugin-updater": "^2.10.1",
|
||||
"@tiptap/extension-color": "^3.10.7",
|
||||
"@tiptap/extension-gapcursor": "^3.10.7",
|
||||
"@tiptap/extension-highlight": "^3.10.7",
|
||||
|
||||
@@ -23,6 +23,8 @@ pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.manage(db_manager)
|
||||
.manage(session)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
@@ -172,7 +174,8 @@ pub fn run() {
|
||||
domains::tombstone::commands::get_tombstones_since,
|
||||
domains::tombstone::commands::apply_book_tombstones,
|
||||
domains::tombstone::commands::apply_series_tombstones,
|
||||
// ─── Migration ────────────<EFBFBD><EFBFBD>───────────────────
|
||||
// ─── Migration ─────────────────────────────────
|
||||
domains::migration::commands::auto_migrate_electron,
|
||||
domains::migration::commands::check_electron_migration,
|
||||
domains::migration::commands::import_from_electron,
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user