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:
natreex
2026-04-07 16:09:35 -04:00
parent 687c1d582c
commit 5c7e71ce9e
19 changed files with 461 additions and 46 deletions

5
app/HomePage.tsx Normal file
View File

@@ -0,0 +1,5 @@
import BookList from '@/components/book/BookList';
export default function HomePage() {
return <BookList/>;
}

1
app/book/BookLayout.tsx Normal file
View File

@@ -0,0 +1 @@
export {default} from '@/app/book/[bookId]/layout';

1
app/book/BookPage.tsx Normal file
View File

@@ -0,0 +1 @@
export {default} from '@/app/book/[bookId]/page';

1
app/book/ChapterPage.tsx Normal file
View File

@@ -0,0 +1 @@
export {default} from '@/app/book/[bookId]/chapter/[chapterId]/page';

View 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/>;
}

View 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}</>;
}

View 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
View File

@@ -0,0 +1 @@
export {default} from './login/page';

137
app/main.tsx Normal file
View 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>
);

View File

@@ -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>

View File

@@ -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>
)

View File

@@ -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>
);
}

View File

@@ -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);
}
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> {

View File

@@ -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.",

View File

@@ -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é.",

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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,
])