Add foundational components and logic for migration, UI design, and input handling

- Introduced foundational UI components (`Badge`, `LockCard`, `SectionHeader`, `AvatarIcon`, etc.) for flexible layouts and consistent design.
- Added migration support with the `MigrationModal` component and backend integration for exporting/importing data between Electron and Tauri.
- Extended form components with `TextAreaInput`, `OrderInput`, `ToggleField`, and `ToolbarSelect` for improved input handling.
- Updated `ScribeShell` with migration popup logic to prompt users for data migration.
- Integrated `AlertStack` for better alert handling and notification management.
- Enhanced Rust/Tauri services with migration command implementations.
- Added translations and styles for new components.
This commit is contained in:
natreex
2026-04-05 12:52:54 -04:00
parent 2b6d4cc48b
commit d4765e6576
49 changed files with 3115 additions and 2 deletions

101
lib/crashReporter.ts Normal file
View File

@@ -0,0 +1,101 @@
import axios from 'axios';
import {configs, isDesktop} from '@/lib/configs';
interface CrashReportPayload {
appName: string;
appVersion: string;
platform: string;
osVersion?: string;
errorType: string;
errorMessage: string;
stackTrace?: string;
screenName?: string;
userId?: string;
breadcrumbs?: Breadcrumb[];
extraData?: Record<string, unknown>;
}
interface Breadcrumb {
timestamp: number;
action: string;
detail?: string;
}
const MAX_BREADCRUMBS = 30;
const breadcrumbs: Breadcrumb[] = [];
export function addBreadcrumb(action: string, detail?: string): void {
breadcrumbs.push({timestamp: Date.now(), action, detail});
if (breadcrumbs.length > MAX_BREADCRUMBS) breadcrumbs.shift();
}
function getPlatform(): string {
if (!isDesktop) return 'web';
const ua: string = navigator.userAgent.toLowerCase();
if (ua.includes('mac')) return 'desktop-macos';
if (ua.includes('win')) return 'desktop-windows';
if (ua.includes('linux')) return 'desktop-linux';
return 'desktop';
}
function getUserId(): string | undefined {
try {
const raw: string | null = localStorage.getItem('userId');
return raw ?? undefined;
} catch {
return undefined;
}
}
async function sendCrashReport(payload: CrashReportPayload): Promise<void> {
try {
await axios.post(configs.apiUrl + 'crash-report', payload, {
headers: {'Content-Type': 'application/json'},
timeout: 5000,
});
} catch {
// Silently fail — we can't crash while reporting a crash
}
}
function buildReport(errorType: string, errorMessage: string, stackTrace?: string): CrashReportPayload {
return {
appName: configs.appName,
appVersion: configs.appVersion,
platform: getPlatform(),
osVersion: navigator.userAgent,
errorType,
errorMessage,
stackTrace,
screenName: window.location.pathname,
userId: getUserId(),
breadcrumbs: [...breadcrumbs],
};
}
export function reportError(error: Error, extraData?: Record<string, unknown>): void {
const report: CrashReportPayload = buildReport(
error.name || 'Error',
error.message,
error.stack,
);
if (extraData) report.extraData = extraData;
sendCrashReport(report);
}
export function initCrashReporter(): void {
window.onerror = (_message, _source, _lineno, _colno, error: Error | undefined): void => {
if (error) {
sendCrashReport(buildReport('UncaughtError', error.message, error.stack));
}
};
window.onunhandledrejection = (event: PromiseRejectionEvent): void => {
const reason: unknown = event.reason;
if (reason instanceof Error) {
sendCrashReport(buildReport('UnhandledRejection', reason.message, reason.stack));
} else {
sendCrashReport(buildReport('UnhandledRejection', String(reason)));
}
};
}

View File

@@ -187,9 +187,38 @@
"title": "Discord",
"description": "Join our community on Discord.",
"badge": "DISCORD"
},
"migration": {
"title": "Import from Electron",
"description": "Migrate your data from the old version."
}
}
},
"migration": {
"title": "Migration from Electron",
"introText": "We detected this is your first launch. If you were using the old version of ERitors Scribe (Electron), you can import your local data (books, characters, chapters, etc.).",
"steps": "How to proceed:",
"step1": "In the old Electron app, go to the menu and click \"Export for migration\".",
"step2": "A .json file and a copy of your database will be created on your Desktop.",
"step3": "Come back here and enter the path to the exported .json file.",
"later": "Later",
"haveFile": "I have the file",
"selectText": "Paste the full path to the migration file exported from Electron.",
"filePath": "Migration file path",
"back": "Back",
"import": "Import",
"importing": "Migrating...",
"successTitle": "Migration successful!",
"successText": "Your data has been imported successfully. Log in again to access it.",
"deleteReminder": "Remember to delete the migration file and the database copy from your Desktop.",
"done": "Done",
"errorTitle": "Migration error",
"close": "Close",
"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."
},
"quillSense": {
"needSubscription": "Please subscribe to QuillSense or bring your keys to access this feature.",
"subscriptionDescription": "Unlock powerful writing tools to enrich your prose.",

View File

@@ -187,9 +187,38 @@
"title": "Discord",
"description": "Rejoignez notre communauté sur Discord.",
"badge": "DISCORD"
},
"migration": {
"title": "Importer depuis Electron",
"description": "Migrer vos données depuis l'ancienne version."
}
}
},
"migration": {
"title": "Migration depuis Electron",
"introText": "Nous avons détecté que c'est votre premier lancement. Si vous utilisiez l'ancienne version d'ERitors Scribe (Electron), vous pouvez importer vos données locales (livres, personnages, chapitres, etc.).",
"steps": "Comment procéder :",
"step1": "Dans l'ancienne app Electron, allez dans le menu et cliquez sur « Exporter pour migration ».",
"step2": "Un fichier .json et une copie de votre base de données seront créés sur votre Bureau.",
"step3": "Revenez ici et indiquez le chemin du fichier .json exporté.",
"later": "Plus tard",
"haveFile": "J'ai le fichier",
"selectText": "Collez le chemin complet du fichier de migration exporté depuis Electron.",
"filePath": "Chemin du fichier de migration",
"back": "Retour",
"import": "Importer",
"importing": "Migration en cours...",
"successTitle": "Migration réussie !",
"successText": "Vos données ont été importées avec succès. Reconnectez-vous pour y accéder.",
"deleteReminder": "Pensez à supprimer le fichier de migration et la copie de la base de données de votre Bureau.",
"done": "Terminé",
"errorTitle": "Erreur de migration",
"close": "Fermer",
"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."
},
"quillSense": {
"needSubscription": "Veuillez vous abonner à QuillSense ou Amenez vos clés pour accéder à cette fonctionnalité.",
"subscriptionDescription": "Débloquez des outils d'aide à l'écriture puissants pour enrichir votre prose.",

View File

@@ -677,6 +677,29 @@ export async function applySeriesTombstones(tombstones: TombstoneRecord[]): Prom
return invoke<void>('apply_series_tombstones', {tombstones});
}
// ─── Migration ────────────────────────────────────────────────
export interface MigrationCheckResult {
found: boolean;
userId: string | null;
hasDb: boolean;
migrationPath: string | null;
}
export interface MigrationResult {
success: boolean;
userId: string | null;
error: string | null;
}
export async function checkElectronMigration(migrationFilePath: string): Promise<MigrationCheckResult> {
return invoke<MigrationCheckResult>('check_electron_migration', {migrationFilePath});
}
export async function importFromElectron(migrationFilePath: string): Promise<MigrationResult> {
return invoke<MigrationResult>('import_from_electron', {data: {migrationFilePath}});
}
// ─── Window Management ──────────────────────────────────────
let loginWindowOpening = false;

View File

@@ -0,0 +1,4 @@
export function isWebKitWithoutIndentFix(): boolean {
const ua: string = navigator.userAgent;
return /AppleWebKit/.test(ua) && !/Chrome|Chromium|Edg/.test(ua);
}