From bb65ad5ca69736a1dee9d5d865745a6e11df4124 Mon Sep 17 00:00:00 2001 From: codedogQBY <1369175442@qq.com> Date: Sun, 31 May 2026 01:37:36 +0800 Subject: [PATCH] fix: add startup sync and dark chat text fix --- .../src/components/chat/MarkdownRenderer.tsx | 46 +- .../screens/settings/SyncSettingsScreen.tsx | 507 ++++++++++-------- .../src/components/settings/SyncSettings.tsx | 91 +++- packages/core/src/hooks/use-auto-sync.ts | 62 ++- .../core/src/i18n/locales/en/settings.json | 6 +- .../core/src/i18n/locales/es/settings.json | 2 + .../core/src/i18n/locales/fr/settings.json | 2 + .../core/src/i18n/locales/ja/settings.json | 2 + .../core/src/i18n/locales/ko/settings.json | 2 + .../core/src/i18n/locales/zh-TW/settings.json | 2 + .../core/src/i18n/locales/zh/settings.json | 2 + packages/core/src/stores/sync-store.test.ts | 17 + packages/core/src/stores/sync-store.ts | 46 +- packages/core/src/sync/sync-backend.ts | 8 +- 14 files changed, 521 insertions(+), 274 deletions(-) diff --git a/packages/app-expo/src/components/chat/MarkdownRenderer.tsx b/packages/app-expo/src/components/chat/MarkdownRenderer.tsx index 9ed9ebde..0a51567e 100644 --- a/packages/app-expo/src/components/chat/MarkdownRenderer.tsx +++ b/packages/app-expo/src/components/chat/MarkdownRenderer.tsx @@ -5,7 +5,7 @@ import type { CitationPart } from "@readany/core/types/message"; import * as Clipboard from "expo-clipboard"; import { Fragment, type ReactNode, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { Text, TouchableOpacity, View } from "react-native"; +import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; import Markdown, { type RenderRules, type ASTNode } from "react-native-markdown-display"; interface MarkdownRendererProps { @@ -128,8 +128,11 @@ function renderTextWithCitations( const parts = text.split(/(\[\d+\])/g); const result: React.ReactNode[] = []; + let offset = 0; - parts.forEach((part, i) => { + for (const part of parts) { + const keyOffset = offset; + offset += part.length; const match = part.match(/\[(\d+)\]/); if (match) { const num = Number.parseInt(match[1]); @@ -137,27 +140,26 @@ function renderTextWithCitations( if (citation) { result.push( , ); - return; + continue; } } if (part) { - result.push({part}); + result.push({part}); } - }); + } return result; } export function MarkdownRenderer({ content, - isStreaming, styleOverrides, citations, onCitationClick, @@ -170,7 +172,7 @@ export function MarkdownRenderer({ const rules = useMemo( () => ({ - fence: (node: ASTNode, children: ReactNode[], parentNodes: ASTNode[], style: any) => { + fence: (node: ASTNode, _children: ReactNode[], _parentNodes: ASTNode[], style: any) => { const code = node.content || ""; const lang = getCodeLanguage(node); @@ -180,7 +182,7 @@ export function MarkdownRenderer({ return ; }, - code_block: (node: ASTNode, children: ReactNode[], parentNodes: ASTNode[], style: any) => { + code_block: (node: ASTNode, _children: ReactNode[], _parentNodes: ASTNode[], style: any) => { const code = node.content || ""; const lang = getCodeLanguage(node); @@ -192,17 +194,29 @@ export function MarkdownRenderer({ ); }, - text: (node: ASTNode, children: ReactNode[], parentNodes: ASTNode[], style: any) => { + text: ( + node: ASTNode, + _children: ReactNode[], + _parentNodes: ASTNode[], + style: any, + inheritedStyles: any = {}, + ) => { const text = node.content || ""; + const textStyle = [inheritedStyles, style.text]; + const resolvedTextStyle = StyleSheet.flatten(textStyle); + const themedTextStyle = resolvedTextStyle?.color + ? textStyle + : [...textStyle, { color: colors.foreground }]; + if (citations && citations.length > 0 && /\[\d+\]/.test(text)) { return ( - + {renderTextWithCitations(text, citations, onCitationClick, colors)} ); } return ( - + {text} ); @@ -227,6 +241,14 @@ const makeMarkdownStyles = (colors: ThemeColors) => fontSize: fs.sm, lineHeight: 20, }, + text: { + color: colors.foreground, + fontSize: fs.sm, + lineHeight: 20, + }, + textgroup: { + color: colors.foreground, + }, heading1: { color: colors.foreground, fontSize: fs.lg, diff --git a/packages/app-expo/src/screens/settings/SyncSettingsScreen.tsx b/packages/app-expo/src/screens/settings/SyncSettingsScreen.tsx index 7a56469b..46b1a9e7 100644 --- a/packages/app-expo/src/screens/settings/SyncSettingsScreen.tsx +++ b/packages/app-expo/src/screens/settings/SyncSettingsScreen.tsx @@ -1,16 +1,14 @@ -import { ConfigTransfer } from "../../components/settings/ConfigTransfer"; +import { useResponsiveLayout } from "@/hooks/use-responsive-layout"; import { getPlatformService } from "@readany/core/services"; import { useSyncStore } from "@readany/core/stores"; -import { useResponsiveLayout } from "@/hooks/use-responsive-layout"; -import type { S3Config, WebDavConfig } from "@readany/core/sync/sync-backend"; import { SYNC_SECRET_KEYS } from "@readany/core/sync/sync-backend"; +import Constants from "expo-constants"; /** * SyncSettingsScreen — Multi-backend sync configuration and status panel (mobile). * Supports WebDAV, S3, and LAN sync. */ import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import Constants from "expo-constants"; import { ActivityIndicator, Alert, @@ -24,26 +22,19 @@ import { View, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; +import { ConfigTransfer } from "../../components/settings/ConfigTransfer"; import { spacing, useColors } from "../../styles/theme"; import { SettingsHeader } from "./SettingsHeader"; -import { makeStyles } from "./sync/sync-styles"; -import { WebDavForm } from "./sync/WebDavForm"; -import { S3Form } from "./sync/S3Form"; import { LanSection } from "./sync/LanSection"; +import { S3Form } from "./sync/S3Form"; +import { WebDavForm } from "./sync/WebDavForm"; +import { makeStyles } from "./sync/sync-styles"; type BackendType = "webdav" | "s3" | "lan"; -function isWebDavConfig(config: unknown): config is WebDavConfig { - return ( - typeof config === "object" && config !== null && (config as WebDavConfig).type === "webdav" - ); -} - -function isS3Config(config: unknown): config is S3Config { - return typeof config === "object" && config !== null && (config as S3Config).type === "s3"; -} - -function hasAutoSync(config: unknown): config is { autoSync: boolean; syncIntervalMins?: number } { +function hasAutoSync( + config: unknown, +): config is { autoSync: boolean; syncOnStartup?: boolean; syncIntervalMins?: number } { return typeof config === "object" && config !== null && "autoSync" in config; } @@ -72,6 +63,7 @@ export default function SyncSettingsScreen() { syncWithBackend, forceFullSync, setAutoSync, + setSyncOnStartup, setSyncIntervalMins, resetSync, } = useSyncStore(); @@ -180,9 +172,20 @@ export default function SyncSettingsScreen() { setTesting(false); } }, [ - selectedBackend, url, username, password, allowInsecure, remoteRoot, - s3Endpoint, s3Region, s3Bucket, s3AccessKeyId, s3SecretAccessKey, - testWebDavConnection, testS3Connection, t, + selectedBackend, + url, + username, + password, + allowInsecure, + remoteRoot, + s3Endpoint, + s3Region, + s3Bucket, + s3AccessKeyId, + s3SecretAccessKey, + testWebDavConnection, + testS3Connection, + t, ]); const handleSave = useCallback(async () => { @@ -200,9 +203,19 @@ export default function SyncSettingsScreen() { setSaving(false); } }, [ - selectedBackend, url, username, password, allowInsecure, remoteRoot, - s3Endpoint, s3Region, s3Bucket, s3AccessKeyId, s3SecretAccessKey, - saveWebDavConfig, saveS3Config, + selectedBackend, + url, + username, + password, + allowInsecure, + remoteRoot, + s3Endpoint, + s3Region, + s3Bucket, + s3AccessKeyId, + s3SecretAccessKey, + saveWebDavConfig, + saveS3Config, ]); const handleSync = useCallback(async () => { @@ -219,7 +232,9 @@ export default function SyncSettingsScreen() { }, [syncNow, t]); const handleConflict = useCallback( - (direction: "upload" | "download") => { syncNow(direction); }, + (direction: "upload" | "download") => { + syncNow(direction); + }, [syncNow], ); @@ -258,7 +273,9 @@ export default function SyncSettingsScreen() { { text: t("common.confirm"), style: "destructive", - onPress: () => { void forceFullSync(direction); }, + onPress: () => { + void forceFullSync(direction); + }, }, ], ); @@ -274,24 +291,36 @@ export default function SyncSettingsScreen() { const statusLabel = () => { if (isLanContext) { switch (status) { - case "checking": return t("settings.syncLANPreparingImport"); - case "downloading": return t("settings.syncLANImporting"); - case "syncing-files": return t("settings.syncLANImportingFiles"); - case "error": return t("settings.syncError"); - default: return null; + case "checking": + return t("settings.syncLANPreparingImport"); + case "downloading": + return t("settings.syncLANImporting"); + case "syncing-files": + return t("settings.syncLANImportingFiles"); + case "error": + return t("settings.syncError"); + default: + return null; } } switch (status) { - case "checking": return t("settings.syncChecking"); - case "uploading": return t("settings.syncUploading"); - case "downloading": return t("settings.syncDownloading"); - case "syncing-files": return t("settings.syncSyncingFiles"); - case "error": return t("settings.syncError"); - default: return null; + case "checking": + return t("settings.syncChecking"); + case "uploading": + return t("settings.syncUploading"); + case "downloading": + return t("settings.syncDownloading"); + case "syncing-files": + return t("settings.syncSyncingFiles"); + case "error": + return t("settings.syncError"); + default: + return null; } }; const autoSyncEnabled = hasAutoSync(config) ? config.autoSync : false; + const syncOnStartupEnabled = hasAutoSync(config) ? config.syncOnStartup === true : false; const showScheduledSyncSettings = hasAutoSync(config); const progressLabel = () => { @@ -306,14 +335,16 @@ export default function SyncSettingsScreen() { } return progress.phase === "database" ? t("settings.syncProgressDatabase", { - operation: progress.operation === "upload" - ? t("settings.syncUploading") - : t("settings.syncDownloading"), + operation: + progress.operation === "upload" + ? t("settings.syncUploading") + : t("settings.syncDownloading"), }) : t("settings.syncProgressFiles", { - operation: progress.operation === "upload" - ? t("settings.syncUploading") - : t("settings.syncDownloading"), + operation: + progress.operation === "upload" + ? t("settings.syncUploading") + : t("settings.syncDownloading"), completed: progress.completedFiles, total: progress.totalFiles, }); @@ -343,7 +374,9 @@ export default function SyncSettingsScreen() { keyboardShouldPersistTaps="handled" keyboardDismissMode="on-drag" > - + {t("settings.syncLayoutMigrationNotice")} {/* Backend Type Selector */} @@ -353,7 +386,10 @@ export default function SyncSettingsScreen() { {(["webdav", "s3", "lan"] as const).map((backend) => ( setSelectedBackend(backend)} > - - {t("settings.syncConflictTitle")} - {t("settings.syncConflictDesc")} - - handleConflict("upload")} - activeOpacity={0.7} - > - {t("settings.syncConflictUpload")} - - handleConflict("download")} - activeOpacity={0.7} - > - {t("settings.syncConflictDownload")} - + + + {t("settings.syncConflictTitle")} + {t("settings.syncConflictDesc")} + + handleConflict("upload")} + activeOpacity={0.7} + > + {t("settings.syncConflictUpload")} + + handleConflict("download")} + activeOpacity={0.7} + > + + {t("settings.syncConflictDownload")} + + + - )} {/* Sync Status */} {selectedBackend !== "lan" && (isConfigured || isBusy || lastSyncAt) && ( - - - {isLanContext ? t("settings.syncLANImportStatus") : t("settings.syncStatus")} - - - - - - {isLanContext ? t("settings.syncLANLastImport") : t("settings.syncLastSync")} - - {formatLastSync(lastSyncAt)} - {statusLabel() && {statusLabel()}} - {isBusy && progress && ( - - - {progress.phase === "database" ? ( - - ) : ( - 0 ? Math.round((progress.completedFiles / progress.totalFiles) * 100) : 0}%`, - }, - ]} - /> - )} + + + {isLanContext ? t("settings.syncLANImportStatus") : t("settings.syncStatus")} + + + + + + {isLanContext + ? t("settings.syncLANLastImport") + : t("settings.syncLastSync")} + + {formatLastSync(lastSyncAt)} + {statusLabel() && {statusLabel()}} + {isBusy && progress && ( + + + {progress.phase === "database" ? ( + + ) : ( + 0 ? Math.round((progress.completedFiles / progress.totalFiles) * 100) : 0}%`, + }, + ]} + /> + )} + + {progressLabel()} - {progressLabel()} - + )} + + {!isLanContext && ( + + {isBusy && ( + + )} + + {isBusy ? t("settings.syncSyncing") : t("settings.syncNow")} + + )} - {!isLanContext && ( - - {isBusy && } - - {isBusy ? t("settings.syncSyncing") : t("settings.syncNow")} - - - )} - - {lastResult && ( - - {lastResult.success ? ( - <> - - {isLanContext - ? t("settings.syncLANImportComplete") - : t("settings.syncDirection", { direction: lastResult.direction })} - - {lastResult.filesUploaded > 0 && ( - - {t("settings.syncFilesUp", { count: lastResult.filesUploaded })} - - )} - {lastResult.filesDownloaded > 0 && ( + {lastResult && ( + + {lastResult.success ? ( + <> {isLanContext - ? t("settings.syncLANImportedFiles", { count: lastResult.filesDownloaded }) - : t("settings.syncFilesDown", { count: lastResult.filesDownloaded })} + ? t("settings.syncLANImportComplete") + : t("settings.syncDirection", { direction: lastResult.direction })} - )} - {(lastResult.filesUploadFailed > 0 || lastResult.filesDownloadFailed > 0) && ( - - {t("settings.syncFilesPartialFailed", { - uploadFailed: lastResult.filesUploadFailed, - downloadFailed: lastResult.filesDownloadFailed, - })} - - )} - - ) : ( - - {t("settings.syncFailed", { error: lastResult.error })} - - )} - - )} + {lastResult.filesUploaded > 0 && ( + + {t("settings.syncFilesUp", { count: lastResult.filesUploaded })} + + )} + {lastResult.filesDownloaded > 0 && ( + + {isLanContext + ? t("settings.syncLANImportedFiles", { + count: lastResult.filesDownloaded, + }) + : t("settings.syncFilesDown", { + count: lastResult.filesDownloaded, + })} + + )} + {(lastResult.filesUploadFailed > 0 || + lastResult.filesDownloadFailed > 0) && ( + + {t("settings.syncFilesPartialFailed", { + uploadFailed: lastResult.filesUploadFailed, + downloadFailed: lastResult.filesDownloadFailed, + })} + + )} + + ) : ( + + {t("settings.syncFailed", { error: lastResult.error })} + + )} + + )} - {error && !lastResult && {error}} + {error && !lastResult && {error}} - {showScheduledSyncSettings && ( - <> - - - {t("settings.syncAutoSync")} - {t("settings.syncAutoSyncDesc")} + {showScheduledSyncSettings && ( + <> + + + {t("settings.syncAutoSync")} + {t("settings.syncAutoSyncDesc")} + + setAutoSync(!autoSyncEnabled)} + > + + - setAutoSync(!autoSyncEnabled)} - > - - - - - - {t("settings.syncInterval")} - {t("settings.syncIntervalDesc")} + + + {t("settings.syncOnStartup")} + {t("settings.syncOnStartupDesc")} + + setSyncOnStartup(!syncOnStartupEnabled)} + > + + - - void handleSyncIntervalBlur()} - keyboardType="number-pad" - returnKeyType="done" - /> - - {t("settings.syncIntervalMinutes", { - count: Number.parseInt(syncIntervalInput || "30", 10) || 30, - })} - + + + {t("settings.syncInterval")} + {t("settings.syncIntervalDesc")} + + + void handleSyncIntervalBlur()} + keyboardType="number-pad" + returnKeyType="done" + /> + + {t("settings.syncIntervalMinutes", { + count: Number.parseInt(syncIntervalInput || "30", 10) || 30, + })} + + - - - )} + + )} + - )} {/* Advanced */} {isConfigured && selectedBackend !== "lan" && ( - - setShowAdvanced(!showAdvanced)} - > - {t("settings.syncAdvanced")} - {showAdvanced ? "▲" : "▼"} - - {showAdvanced && ( - - - handleForceFullSync("upload")} - disabled={isBusy} - activeOpacity={0.7} - > - {t("settings.syncForceUpload")} - + + setShowAdvanced(!showAdvanced)} + > + {t("settings.syncAdvanced")} + {showAdvanced ? "▲" : "▼"} + + {showAdvanced && ( + + + handleForceFullSync("upload")} + disabled={isBusy} + activeOpacity={0.7} + > + {t("settings.syncForceUpload")} + + handleForceFullSync("download")} + disabled={isBusy} + activeOpacity={0.7} + > + + {t("settings.syncForceDownload")} + + + + {t("settings.syncForceUploadDesc")} + {t("settings.syncForceDownloadDesc")} handleForceFullSync("download")} - disabled={isBusy} + style={styles.resetBtn} + onPress={handleReset} activeOpacity={0.7} > - - {t("settings.syncForceDownload")} - + {t("settings.syncReset")} + {t("settings.syncResetDesc")} - {t("settings.syncForceUploadDesc")} - {t("settings.syncForceDownloadDesc")} - - {t("settings.syncReset")} - - {t("settings.syncResetDesc")} - - )} - + )} + )} {/* Transfer sync config */} diff --git a/packages/app/src/components/settings/SyncSettings.tsx b/packages/app/src/components/settings/SyncSettings.tsx index bf323bd9..211de35a 100644 --- a/packages/app/src/components/settings/SyncSettings.tsx +++ b/packages/app/src/components/settings/SyncSettings.tsx @@ -35,6 +35,7 @@ export function SyncSettings() { syncNow, forceFullSync, setAutoSync, + setSyncOnStartup, setSyncIntervalMins, resetSync, } = useSyncStore(); @@ -339,6 +340,7 @@ export function SyncSettings() {

{t("settings.syncBackendType")}