chore: bump eslint-config-next to version 16 (#2294)

This commit is contained in:
Huang Xin
2025-10-22 21:57:04 +08:00
committed by GitHub
parent 50d7f9a9dd
commit 001e836ae3
45 changed files with 1097 additions and 451 deletions
+17 -21
View File
@@ -1,25 +1,21 @@
import path from 'node:path';
import js from '@eslint/js';
import { defineConfig, globalIgnores } from 'eslint/config';
import nextVitals from 'eslint-config-next/core-web-vitals';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import { fileURLToPath } from 'node:url';
import { FlatCompat } from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [
...compat.extends('next/core-web-vitals', 'next/typescript'),
const eslintConfig = defineConfig([
...nextVitals,
{
plugins: {
'jsx-a11y': jsxA11y,
},
rules: {
...jsxA11y.configs.recommended.rules,
},
rules: jsxA11y.configs.recommended.rules,
},
];
globalIgnores([
'node_modules/**',
'.next/**',
'.open-next/**',
'out/**',
'build/**',
'public/**',
'next-env.d.ts',
]),
]);
export default eslintConfig;
+2 -2
View File
@@ -10,7 +10,7 @@
"build-web": "dotenv -e .env.web -- next build",
"start-web": "dotenv -e .env.web -- next start",
"i18n:extract": "i18next-scanner",
"lint": "next lint",
"lint": "eslint .",
"test": "dotenv -e .env -e .env.test.local vitest",
"tauri": "tauri",
"prepare-public-vendor": "mkdirp ./public/vendor/pdfjs",
@@ -126,7 +126,7 @@
"daisyui": "^4.12.24",
"dotenv-cli": "^7.4.4",
"eslint": "^9.16.0",
"eslint-config-next": "15.0.3",
"eslint-config-next": "16.0.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"i18next-scanner": "^4.6.0",
"jsdom": "^26.1.0",
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { vi } from 'vitest';
export const setupSupabaseMocks = async (
+3
View File
@@ -19,6 +19,9 @@ export default function Error({ error, reset }: ErrorPageProps) {
useEffect(() => {
setBrowserInfo(parseWebViewInfo(appService));
}, [appService]);
useEffect(() => {
posthog.captureException(error);
handleGlobalError(error);
}, [appService, error]);
@@ -20,6 +20,7 @@ import { tauriHandleSetAlwaysOnTop, tauriHandleToggleFullScreen } from '@/utils/
import { optInTelemetry, optOutTelemetry } from '@/utils/telemetry';
import { setAboutDialogVisible } from '@/components/AboutWindow';
import { setMigrateDataDirDialogVisible } from '@/app/library/components/MigrateDataWindow';
import { saveSysSettings } from '@/helpers/settings';
import UserAvatar from '@/components/UserAvatar';
import MenuItem from '@/components/MenuItem';
import Quota from '@/components/Quota';
@@ -40,8 +41,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
const { user } = useAuth();
const { userPlan, quotas } = useQuotaStats(true);
const { themeMode, setThemeMode } = useThemeStore();
const { settings, setSettings, saveSettings } = useSettingsStore();
const { setFontLayoutSettingsDialogOpen } = useSettingsStore();
const { settings, setSettingsDialogOpen } = useSettingsStore();
const [isAutoUpload, setIsAutoUpload] = useState(settings.autoUpload);
const [isAutoCheckUpdates, setIsAutoCheckUpdates] = useState(settings.autoCheckUpdates);
const [isAlwaysOnTop, setIsAlwaysOnTop] = useState(settings.alwaysOnTop);
@@ -91,77 +91,67 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
};
const toggleOpenInNewWindow = () => {
settings.openBookInNewWindow = !settings.openBookInNewWindow;
setSettings(settings);
saveSettings(envConfig, settings);
saveSysSettings(envConfig, 'openBookInNewWindow', !settings.openBookInNewWindow);
setIsDropdownOpen?.(false);
};
const toggleAlwaysOnTop = () => {
settings.alwaysOnTop = !settings.alwaysOnTop;
setSettings(settings);
saveSettings(envConfig, settings);
setIsAlwaysOnTop(settings.alwaysOnTop);
tauriHandleSetAlwaysOnTop(settings.alwaysOnTop);
const newValue = !settings.alwaysOnTop;
saveSysSettings(envConfig, 'alwaysOnTop', newValue);
setIsAlwaysOnTop(newValue);
tauriHandleSetAlwaysOnTop(newValue);
setIsDropdownOpen?.(false);
};
const toggleAlwaysShowStatusBar = () => {
settings.alwaysShowStatusBar = !settings.alwaysShowStatusBar;
setSettings(settings);
saveSettings(envConfig, settings);
setIsAlwaysShowStatusBar(settings.alwaysShowStatusBar);
const newValue = !settings.alwaysShowStatusBar;
saveSysSettings(envConfig, 'alwaysShowStatusBar', newValue);
setIsAlwaysShowStatusBar(newValue);
};
const toggleAutoUploadBooks = () => {
settings.autoUpload = !settings.autoUpload;
setSettings(settings);
saveSettings(envConfig, settings);
setIsAutoUpload(settings.autoUpload);
const newValue = !settings.autoUpload;
saveSysSettings(envConfig, 'autoUpload', newValue);
setIsAutoUpload(newValue);
if (settings.autoUpload && !user) {
if (newValue && !user) {
navigateToLogin(router);
}
};
const toggleAutoImportBooksOnOpen = () => {
settings.autoImportBooksOnOpen = !settings.autoImportBooksOnOpen;
setSettings(settings);
saveSettings(envConfig, settings);
setIsAutoImportBooksOnOpen(settings.autoImportBooksOnOpen);
const newValue = !settings.autoImportBooksOnOpen;
saveSysSettings(envConfig, 'autoImportBooksOnOpen', newValue);
setIsAutoImportBooksOnOpen(newValue);
};
const toggleAutoCheckUpdates = () => {
settings.autoCheckUpdates = !settings.autoCheckUpdates;
setSettings(settings);
saveSettings(envConfig, settings);
setIsAutoCheckUpdates(settings.autoCheckUpdates);
const newValue = !settings.autoCheckUpdates;
saveSysSettings(envConfig, 'autoCheckUpdates', newValue);
setIsAutoCheckUpdates(newValue);
};
const toggleScreenWakeLock = () => {
settings.screenWakeLock = !settings.screenWakeLock;
setSettings(settings);
saveSettings(envConfig, settings);
setIsScreenWakeLock(settings.screenWakeLock);
const newValue = !settings.screenWakeLock;
saveSysSettings(envConfig, 'screenWakeLock', newValue);
setIsScreenWakeLock(newValue);
};
const toggleOpenLastBooks = () => {
settings.openLastBooks = !settings.openLastBooks;
setSettings(settings);
saveSettings(envConfig, settings);
setIsOpenLastBooks(settings.openLastBooks);
const newValue = !settings.openLastBooks;
saveSysSettings(envConfig, 'openLastBooks', newValue);
setIsOpenLastBooks(newValue);
};
const toggleTelemetry = () => {
settings.telemetryEnabled = !settings.telemetryEnabled;
if (settings.telemetryEnabled) {
const newValue = !settings.telemetryEnabled;
saveSysSettings(envConfig, 'telemetryEnabled', newValue);
setIsTelemetryEnabled(newValue);
if (newValue) {
optInTelemetry();
} else {
optOutTelemetry();
}
setSettings(settings);
saveSettings(envConfig, settings);
setIsTelemetryEnabled(settings.telemetryEnabled);
};
const handleUpgrade = () => {
@@ -176,7 +166,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
const openSettingsDialog = () => {
setIsDropdownOpen?.(false);
setFontLayoutSettingsDialogOpen(true);
setSettingsDialogOpen(true);
};
const toggleAlwaysInForeground = async () => {
@@ -192,10 +182,8 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
if (permission.postNotification !== 'granted') return;
}
settings.alwaysInForeground = requestAlwaysInForeground;
setSettings(settings);
saveSettings(envConfig, settings);
setAlwaysInForeground(settings.alwaysInForeground);
saveSysSettings(envConfig, 'alwaysInForeground', requestAlwaysInForeground);
setAlwaysInForeground(requestAlwaysInForeground);
};
const avatarUrl = user?.user_metadata?.['picture'] || user?.user_metadata?.['avatar_url'];
@@ -4,6 +4,7 @@ import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { LibraryCoverFitType, LibrarySortByType, LibraryViewModeType } from '@/types/settings';
import { saveSysSettings } from '@/helpers/settings';
import { navigateToLibrary } from '@/utils/nav';
import MenuItem from '@/components/MenuItem';
import Menu from '@/components/Menu';
@@ -17,7 +18,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
const router = useRouter();
const searchParams = useSearchParams();
const { envConfig } = useEnv();
const { settings, setSettings, saveSettings } = useSettingsStore();
const { settings } = useSettingsStore();
const viewMode = settings.libraryViewMode;
const sortBy = settings.librarySortBy;
@@ -48,9 +49,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
];
const handleSetViewMode = (value: LibraryViewModeType) => {
settings.libraryViewMode = value;
setSettings(settings);
saveSettings(envConfig, settings);
saveSysSettings(envConfig, 'libraryViewMode', value);
setIsDropdownOpen?.(false);
const params = new URLSearchParams(searchParams?.toString());
@@ -59,9 +58,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
};
const handleToggleCropCovers = (value: LibraryCoverFitType) => {
settings.libraryCoverFit = value;
setSettings(settings);
saveSettings(envConfig, settings);
saveSysSettings(envConfig, 'libraryCoverFit', value);
setIsDropdownOpen?.(false);
const params = new URLSearchParams(searchParams?.toString());
@@ -70,9 +67,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
};
const handleSetSortBy = (value: LibrarySortByType) => {
settings.librarySortBy = value;
setSettings(settings);
saveSettings(envConfig, settings);
saveSysSettings(envConfig, 'librarySortBy', value);
setIsDropdownOpen?.(false);
const params = new URLSearchParams(searchParams?.toString());
@@ -81,9 +76,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
};
const handleSetSortAscending = (value: boolean) => {
settings.librarySortAscending = value;
setSettings(settings);
saveSettings(envConfig, settings);
saveSysSettings(envConfig, 'librarySortAscending', value);
setIsDropdownOpen?.(false);
const params = new URLSearchParams(searchParams?.toString());
+4 -5
View File
@@ -79,10 +79,8 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
const { selectFiles } = useFileSelector(appService, _);
const { safeAreaInsets: insets, isRoundedWindow } = useThemeStore();
const { settings, setSettings, saveSettings } = useSettingsStore();
const { isFontLayoutSettingsDialogOpen } = useSettingsStore();
const { setFontLayoutSettingsDialogOpen } = useSettingsStore();
const { isSettingsDialogOpen, setSettingsDialogOpen } = useSettingsStore();
const [loading, setLoading] = useState(false);
const isInitiating = useRef(false);
const [libraryLoaded, setLibraryLoaded] = useState(false);
const [isSelectMode, setIsSelectMode] = useState(false);
const [isSelectAll, setIsSelectAll] = useState(false);
@@ -93,6 +91,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
}>({});
const [pendingNavigationBookIds, setPendingNavigationBookIds] = useState<string[] | null>(null);
const [isDragging, setIsDragging] = useState(false);
const isInitiating = useRef(false);
const demoBooks = useDemoBooks();
const osRef = useRef<OverlayScrollbarsComponentRef>(null);
@@ -129,7 +128,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
}
},
onOpenFontLayoutSettings: () => {
setFontLayoutSettingsDialogOpen(true);
setSettingsDialogOpen(true);
},
onOpenBooks: () => {
handleImportBooks();
@@ -772,7 +771,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
<AboutWindow />
<UpdaterWindow />
<MigrateDataWindow />
{isFontLayoutSettingsDialogOpen && <SettingsDialog bookKey={''} />}
{isSettingsDialogOpen && <SettingsDialog bookKey={''} />}
<Toast />
</div>
);
@@ -34,7 +34,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
const { getProgress, getViewState, getViewSettings } = useReaderStore();
const { setGridInsets, hoveredBookKey } = useReaderStore();
const { sideBarBookKey } = useSidebarStore();
const { isFontLayoutSettingsDialogOpen } = useSettingsStore();
const { isSettingsDialogOpen } = useSettingsStore();
const { safeAreaInsets: screenInsets } = useThemeStore();
const aspectRatio = window.innerWidth / window.innerHeight;
@@ -202,7 +202,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
isHoveredAnim={false}
gridInsets={gridInsets}
/>
{isFontLayoutSettingsDialogOpen && <SettingsDialog bookKey={bookKey} />}
{isSettingsDialogOpen && <SettingsDialog bookKey={bookKey} />}
</div>
);
})}
@@ -9,10 +9,10 @@ import Button from '@/components/Button';
const SettingsToggler = () => {
const _ = useTranslation();
const { setHoveredBookKey } = useReaderStore();
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useSettingsStore();
const { isSettingsDialogOpen, setSettingsDialogOpen } = useSettingsStore();
const handleToggleSettings = () => {
setHoveredBookKey('');
setFontLayoutSettingsDialogOpen(!isFontLayoutSettingsDialogOpen);
setSettingsDialogOpen(!isSettingsDialogOpen);
};
return (
<Button
@@ -5,7 +5,7 @@ import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useBookDataStore } from '@/store/bookDataStore';
import { saveViewSettings } from '@/helpers/viewSettings';
import { saveViewSettings } from '@/helpers/settings';
import { isTranslationAvailable } from '@/services/translators/utils';
import Button from '@/components/Button';
@@ -22,7 +22,7 @@ import { getStyles } from '@/utils/style';
import { navigateToLogin } from '@/utils/nav';
import { eventDispatcher } from '@/utils/event';
import { getMaxInlineSize } from '@/utils/config';
import { saveViewSettings } from '@/helpers/viewSettings';
import { saveViewSettings } from '@/helpers/settings';
import { tauriHandleToggleFullScreen } from '@/utils/window';
import MenuItem from '@/components/MenuItem';
import Menu from '@/components/Menu';
@@ -38,7 +38,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ bookKey, setIsDropdownOpen }) => {
const { user } = useAuth();
const { envConfig, appService } = useEnv();
const { getConfig, getBookData } = useBookDataStore();
const { setFontLayoutSettingsDialogOpen } = useSettingsStore();
const { setSettingsDialogOpen } = useSettingsStore();
const { getView, getViewSettings, getViewState, setViewSettings } = useReaderStore();
const config = getConfig(bookKey)!;
const bookData = getBookData(bookKey)!;
@@ -62,7 +62,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ bookKey, setIsDropdownOpen }) => {
const openFontLayoutMenu = () => {
setIsDropdownOpen?.(false);
setFontLayoutSettingsDialogOpen(true);
setSettingsDialogOpen(true);
};
const cycleThemeMode = () => {
@@ -79,6 +79,14 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const annotPopupHeight = useResponsiveSize(44);
const androidSelectionHandlerHeight = 0;
useEffect(() => {
setSelectedStyle(settings.globalReadSettings.highlightStyle);
}, [settings.globalReadSettings.highlightStyle]);
useEffect(() => {
setSelectedColor(settings.globalReadSettings.highlightStyles[selectedStyle]);
}, [settings.globalReadSettings.highlightStyles, selectedStyle]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleDismissPopup = useCallback(
throttle(() => {
@@ -143,7 +151,8 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const { draw, annotation, doc, range } = detail;
const { style, color } = annotation as BookNote;
const customColors = settings.globalReadSettings.customHighlightColors;
const hexColor = color && customColors ? customColors[color] : color ? HIGHLIGHT_COLOR_HEX[color] : color;
const hexColor =
color && customColors ? customColors[color] : color ? HIGHLIGHT_COLOR_HEX[color] : color;
if (style === 'highlight') {
draw(Overlayer.highlight, { color: hexColor });
} else if (['underline', 'squiggly'].includes(style as string)) {
@@ -1,9 +1,11 @@
import clsx from 'clsx';
import React from 'react';
import React, { useState } from 'react';
import { FaCheckCircle } from 'react-icons/fa';
import { HighlightColor, HighlightStyle } from '@/types/book';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { saveSysSettings } from '@/helpers/settings';
const styles = ['highlight', 'underline', 'squiggly'] as HighlightStyle[];
const colors = ['red', 'violet', 'blue', 'green', 'yellow'] as HighlightColor[];
@@ -23,26 +25,30 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
selectedColor: _selectedColor,
onHandleHighlight,
}) => {
const { settings, setSettings } = useSettingsStore();
const { envConfig } = useEnv();
const { settings } = useSettingsStore();
const globalReadSettings = settings.globalReadSettings;
const customColors = globalReadSettings.customHighlightColors;
const [selectedStyle, setSelectedStyle] = React.useState<HighlightStyle>(_selectedStyle);
const [selectedColor, setSelectedColor] = React.useState<HighlightColor>(_selectedColor);
const [selectedStyle, setSelectedStyle] = useState<HighlightStyle>(_selectedStyle);
const [selectedColor, setSelectedColor] = useState<HighlightColor>(_selectedColor);
const size16 = useResponsiveSize(16);
const size18 = useResponsiveSize(18);
const size28 = useResponsiveSize(28);
const handleSelectStyle = (style: HighlightStyle) => {
globalReadSettings.highlightStyle = style;
setSettings(settings);
const newGlobalReadSettings = { ...globalReadSettings, highlightStyle: style };
saveSysSettings(envConfig, 'globalReadSettings', newGlobalReadSettings);
setSelectedStyle(style);
setSelectedColor(globalReadSettings.highlightStyles[style]);
onHandleHighlight(true);
};
const handleSelectColor = (color: HighlightColor) => {
globalReadSettings.highlightStyle = selectedStyle;
globalReadSettings.highlightStyles[selectedStyle] = color;
setSettings(settings);
const newGlobalReadSettings = {
...globalReadSettings,
highlightStyle: selectedStyle,
highlightStyles: { ...globalReadSettings.highlightStyles, [selectedStyle]: color },
};
saveSysSettings(envConfig, 'globalReadSettings', newGlobalReadSettings);
setSelectedColor(color);
onHandleHighlight(true);
};
@@ -7,6 +7,7 @@ import { useThemeStore } from '@/store/themeStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useSettingsStore } from '@/store/settingsStore';
import { useDeviceControlStore } from '@/store/deviceStore';
import { saveSysSettings } from '@/helpers/settings';
import { themes } from '@/styles/themes';
import { debounce } from '@/utils/debounce';
import Slider from '@/components/Slider';
@@ -25,7 +26,7 @@ interface ColorPanelProps {
export const ColorPanel: React.FC<ColorPanelProps> = ({ actionTab, bottomOffset }) => {
const _ = useTranslation();
const { envConfig, appService } = useEnv();
const { settings, setSettings, saveSettings } = useSettingsStore();
const { settings, setSettings } = useSettingsStore();
const { getScreenBrightness, setScreenBrightness } = useDeviceControlStore();
const { themeMode, themeColor, isDarkMode, setThemeMode, setThemeColor } = useThemeStore();
@@ -48,12 +49,10 @@ export const ColorPanel: React.FC<ColorPanelProps> = ({ actionTab, bottomOffset
const debouncedSetScreenBrightness = useMemo(
() =>
debounce(async (value: number) => {
settings.screenBrightness = value;
setSettings(settings);
saveSettings(envConfig, settings);
saveSysSettings(envConfig, 'screenBrightness', value);
await setScreenBrightness(value / 100);
}, 100),
[envConfig, settings, setSettings, saveSettings, setScreenBrightness],
[envConfig, setScreenBrightness],
);
const handleScreenBrightnessChange = useCallback(
@@ -29,6 +29,7 @@ const DesktopFooterBar: React.FC<FooterBarChildProps> = ({
useEffect(() => {
if (progressValid) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setProgressValue(progressFraction * 100);
}
}, [progressValid, progressFraction]);
@@ -5,7 +5,7 @@ import { RxLineHeight } from 'react-icons/rx';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { saveViewSettings } from '@/helpers/viewSettings';
import { saveViewSettings } from '@/helpers/settings';
import Slider from '@/components/Slider';
const FONT_SIZE_LIMITS = {
@@ -1,5 +1,5 @@
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import clsx from 'clsx';
import React, { useState, useCallback, useMemo } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
@@ -28,14 +28,14 @@ const FooterBar: React.FC<FooterBarProps> = ({
const { getView, getViewState, getProgress, getViewSettings } = useReaderStore();
const { isSideBarVisible, setSideBarVisible } = useSidebarStore();
const [actionTab, setActionTab] = useState('');
const view = getView(bookKey);
const config = getConfig(bookKey);
const viewState = getViewState(bookKey);
const progress = getProgress(bookKey);
const viewSettings = getViewSettings(bookKey);
const [userSelectedTab, setUserSelectedTab] = useState('');
const actionTab = hoveredBookKey === bookKey ? userSelectedTab : '';
const isVisible = hoveredBookKey === bookKey;
const progressInfo = useMemo(
@@ -43,12 +43,12 @@ const FooterBar: React.FC<FooterBarProps> = ({
[bookFormat, section, pageinfo],
);
const progressValid = !!progressInfo;
const progressValid = !!progressInfo && progressInfo.total > 0 && progressInfo.current >= 0;
const progressFraction = useMemo(() => {
if (!progressValid || !progressInfo?.total || progressInfo.total <= 0) {
return 0;
if (progressValid && progressInfo.total > 0 && progressInfo.current >= 0) {
return (progressInfo.current + 1) / progressInfo.total;
}
return (progressInfo.current + 1) / progressInfo.total;
return 0;
}, [progressValid, progressInfo]);
const handleProgressChange = useMemo(
@@ -92,7 +92,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
const handleSetActionTab = useCallback(
(tab: string) => {
setActionTab((prevTab) => (prevTab === tab ? '' : tab));
setUserSelectedTab((prevTab) => (prevTab === tab ? '' : tab));
if (tab === 'tts') {
if (viewState?.ttsEnabled) {
@@ -102,16 +102,16 @@ const FooterBar: React.FC<FooterBarProps> = ({
} else if (tab === 'toc') {
setHoveredBookKey('');
if (config?.viewSettings) {
config.viewSettings.sideBarTab = 'toc';
setConfig(bookKey, config);
setConfig(bookKey, { viewSettings: { ...config.viewSettings, sideBarTab: 'toc' } });
}
setSideBarVisible(true);
} else if (tab === 'note') {
setHoveredBookKey('');
setSideBarVisible(true);
if (config?.viewSettings) {
config.viewSettings.sideBarTab = 'annotations';
setConfig(bookKey, config);
setConfig(bookKey, {
viewSettings: { ...config.viewSettings, sideBarTab: 'annotations' },
});
}
}
},
@@ -126,12 +126,6 @@ const FooterBar: React.FC<FooterBarProps> = ({
],
);
useEffect(() => {
if (hoveredBookKey !== bookKey) {
setActionTab('');
}
}, [hoveredBookKey, bookKey]);
const navigationHandlers: NavigationHandlers = useMemo(
() => ({
onPrevPage: handleGoPrevPage,
@@ -37,6 +37,7 @@ export const NavigationPanel: React.FC<NavigationPanelProps> = ({
useEffect(() => {
if (progressValid) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setProgressValue(progressFraction * 100);
}
}, [progressValid, progressFraction]);
@@ -31,7 +31,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
const { text, cfi, note } = item;
const editorRef = useRef<TextEditorRef>(null);
const editorDraftRef = useRef<string>(text || '');
const [editorDraft, setEditorDraft] = useState(text || '');
const [inlineEditMode, setInlineEditMode] = useState(false);
const separatorWidth = useResponsiveSize(3);
@@ -72,19 +72,20 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
};
const editBookmark = () => {
setEditorDraft(text || '');
setInlineEditMode(true);
};
const handleSaveBookmark = () => {
setInlineEditMode(false);
const config = getConfig(bookKey);
if (!config || !editorDraftRef.current) return;
if (!config || !editorDraft) return;
const { booknotes: annotations = [] } = config;
const existingIndex = annotations.findIndex((annotation) => item.id === annotation.id);
if (existingIndex === -1) return;
annotations[existingIndex]!.updatedAt = Date.now();
annotations[existingIndex]!.text = editorDraftRef.current;
annotations[existingIndex]!.text = editorDraft;
const updatedConfig = updateBooknotes(bookKey, annotations);
if (updatedConfig) {
saveConfig(envConfig, bookKey, updatedConfig, settings);
@@ -104,8 +105,8 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
<TextEditor
className='!leading-normal'
ref={editorRef}
value={editorDraftRef.current}
onChange={(value) => (editorDraftRef.current = value)}
value={editorDraft}
onChange={setEditorDraft}
onSave={handleSaveBookmark}
onEscape={() => setInlineEditMode(false)}
spellCheck={false}
@@ -113,7 +114,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
</div>
<div className='flex justify-end space-x-3 p-2' dir='ltr'>
<TextButton onClick={() => setInlineEditMode(false)}>{_('Cancel')}</TextButton>
<TextButton onClick={handleSaveBookmark} disabled={!editorDraftRef.current}>
<TextButton onClick={handleSaveBookmark} disabled={!editorDraft}>
{_('Save')}
</TextButton>
</div>
@@ -20,7 +20,7 @@ interface UseBookShortcutsProps {
const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) => {
const { getView, getViewState, getViewSettings, setViewSettings } = useReaderStore();
const { toggleSideBar, setSideBarBookKey } = useSidebarStore();
const { setFontLayoutSettingsDialogOpen } = useSettingsStore();
const { setSettingsDialogOpen } = useSettingsStore();
const { getBookData } = useBookDataStore();
const { toggleNotebook } = useNotebookStore();
const { getNextBookKey } = useBooksManager();
@@ -183,7 +183,7 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
onToggleNotebook: toggleNotebook,
onToggleScrollMode: toggleScrollMode,
onToggleBookmark: toggleBookmark,
onOpenFontLayoutSettings: () => setFontLayoutSettingsDialogOpen(true),
onOpenFontLayoutSettings: () => setSettingsDialogOpen(true),
onToggleSearchBar: showSearchBar,
onToggleFullscreen: toggleFullscreen,
onToggleTTS: toggleTTS,
@@ -139,7 +139,6 @@ export const useProgressSync = (bookKey: string) => {
}
}
const filteredSyncedConfig = Object.fromEntries(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Object.entries(syncedConfig).filter(([_, value]) => value !== null && value !== undefined),
);
if (syncedConfig.updatedAt >= config.updatedAt) {
@@ -1,34 +1,33 @@
import { useEffect, useRef, useState } from 'react';
import * as CFI from 'foliate-js/epubcfi.js';
import { useEffect, useMemo, useRef } from 'react';
import { BookProgress } from '@/types/book';
import * as CFI from 'foliate-js/epubcfi.js';
const useScrollToItem = (cfi: string, progress: BookProgress | null) => {
const viewRef = useRef<HTMLLIElement | null>(null);
const [isCurrent, setIsCurrent] = useState(false);
useEffect(() => {
if (!viewRef.current || !progress) return;
const isCurrent = useMemo(() => {
if (!progress) return false;
// Calculate if the current item is in the progress range
const { location } = progress;
const start = CFI.collapse(location);
const end = CFI.collapse(location, true);
const isCurrent = CFI.compare(cfi, start) >= 0 && CFI.compare(cfi, end) <= 0;
setIsCurrent(isCurrent);
return CFI.compare(cfi, start) >= 0 && CFI.compare(cfi, end) <= 0;
}, [cfi, progress]);
useEffect(() => {
if (!viewRef.current || !isCurrent) return;
// Scroll to the item if it's the current one and not visible
if (isCurrent) {
const element = viewRef.current;
const rect = element.getBoundingClientRect();
const isVisible = rect.top >= 0 && rect.bottom <= window.innerHeight;
const element = viewRef.current;
const rect = element.getBoundingClientRect();
const isVisible = rect.top >= 0 && rect.bottom <= window.innerHeight;
if (!isVisible) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
element.setAttribute('aria-current', 'page');
if (!isVisible) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, [cfi, progress, viewRef]);
element.setAttribute('aria-current', 'page');
}, [isCurrent]);
return { isCurrent, viewRef };
};
+18 -19
View File
@@ -8,16 +8,18 @@ export type ToastType = 'info' | 'success' | 'warning' | 'error';
export const Toast = () => {
const { safeAreaInsets } = useThemeStore();
const [toastMessage, setToastMessage] = useState('');
const toastType = useRef<ToastType>('info');
const toastTimeout = useRef(5000);
const messageClass = useRef('');
const [toastType, setToastType] = useState<ToastType>('info');
const [toastTimeout, setToastTimeout] = useState(5000);
const [messageClass, setMessageClass] = useState('');
const toastDismissTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
const toastClassMap = {
info: 'toast-info toast-center toast-middle',
success: 'toast-success toast-top sm:toast-end toast-center',
warning: 'toast-warning toast-top sm:toast-end toast-center',
error: 'toast-error toast-top sm:toast-end toast-center',
};
const alertClassMap = {
info: 'alert-primary border-base-300',
success: 'alert-success border-0',
@@ -27,18 +29,20 @@ export const Toast = () => {
useEffect(() => {
if (toastDismissTimeout.current) clearTimeout(toastDismissTimeout.current);
toastDismissTimeout.current = setTimeout(() => setToastMessage(''), toastTimeout.current);
const timeout = setTimeout(() => setToastMessage(''), toastTimeout);
toastDismissTimeout.current = timeout;
return () => {
if (toastDismissTimeout.current) clearTimeout(toastDismissTimeout.current);
if (timeout) clearTimeout(timeout);
};
}, [toastMessage]);
}, [toastMessage, toastTimeout]);
const handleShowToast = async (event: CustomEvent) => {
const { message, type = 'info', timeout, className = '' } = event.detail;
setToastMessage(message);
toastType.current = type;
if (timeout) toastTimeout.current = timeout;
messageClass.current = className;
setToastType(type);
if (timeout) setToastTimeout(timeout);
setMessageClass(className);
};
useEffect(() => {
@@ -53,28 +57,23 @@ export const Toast = () => {
<div
className={clsx(
'toast toast-center toast-middle z-50 w-auto max-w-screen-sm',
toastClassMap[toastType.current],
toastClassMap[toastType],
)}
style={{
top: toastClassMap[toastType.current].includes('toast-top')
top: toastClassMap[toastType].includes('toast-top')
? `${(safeAreaInsets?.top || 0) + 44}px`
: undefined,
}}
>
<div
className={clsx(
'alert flex items-center justify-center',
alertClassMap[toastType.current],
)}
>
<div className={clsx('alert flex items-center justify-center', alertClassMap[toastType])}>
<span
className={clsx(
'max-h-[50vh] min-w-32 overflow-y-auto',
'text-center font-sans text-base font-normal sm:text-sm',
toastType.current === 'info'
toastType === 'info'
? 'max-w-[80vw]'
: 'min-w-[60vw] max-w-[80vw] whitespace-normal break-words sm:min-w-40 sm:max-w-80',
messageClass.current,
messageClass,
)}
>
{toastMessage.split('\n').map((line, idx) => (
+13 -5
View File
@@ -11,6 +11,10 @@ interface UserAvatarProps {
borderClassName?: string;
}
const getStorageKey = (url: string) => {
return `avatar_${btoa(url).replace(/[^a-zA-Z0-9]/g, '')}`;
};
const UserAvatar: React.FC<UserAvatarProps> = ({
url,
size,
@@ -18,15 +22,19 @@ const UserAvatar: React.FC<UserAvatarProps> = ({
borderClassName,
DefaultIcon,
}) => {
const [cachedImageUrl, setCachedImageUrl] = useState<string | null>(null);
const [cachedImageUrl, setCachedImageUrl] = useState<string | null>(() => {
if (!url) return null;
return localStorage.getItem(getStorageKey(url));
});
useEffect(() => {
if (!url) return;
const storageKey = `avatar_${btoa(url).replace(/[^a-zA-Z0-9]/g, '')}`;
const storageKey = getStorageKey(url);
const cached = localStorage.getItem(storageKey);
if (cached) {
setCachedImageUrl(cached);
if (cached && cached === cachedImageUrl) {
return;
}
@@ -51,7 +59,7 @@ const UserAvatar: React.FC<UserAvatarProps> = ({
};
cacheImage();
}, [url]);
}, [url, cachedImageUrl]);
return (
<div
@@ -1,5 +1,5 @@
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import { MdEdit, MdDelete, MdLock, MdLockOpen, MdOutlineSearch } from 'react-icons/md';
import { Book } from '@/types/book';
@@ -18,7 +18,7 @@ interface BookDetailEditProps {
lockedFields: Record<string, boolean>;
fieldErrors: Record<string, string>;
searchLoading: boolean;
onFieldChange: (field: string, value: string) => void;
onFieldChange: (field: string, value: string | undefined) => void;
onToggleFieldLock: (field: string) => void;
onAutoRetrieve: () => void;
onLockAll: () => void;
@@ -53,13 +53,8 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
const hasLockedFields = Object.values(lockedFields).some((locked) => locked);
const allFieldsLocked = Object.values(lockedFields).every((locked) => locked);
const isCoverLocked = lockedFields['coverImageUrl'] || false;
const [newCoverImageUrl, setNewCoverImageUrl] = useState<string | null>(null);
useEffect(() => {
if (metadata.coverImageUrl) {
setNewCoverImageUrl(metadata.coverImageUrl);
}
}, [metadata.coverImageUrl]);
const coverImageUrl = metadata.coverImageUrl || null;
const [newCoverImageUrl, setNewCoverImageUrl] = useState<string | null>(coverImageUrl);
const titleAuthorFields = [
{
@@ -156,12 +151,13 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
const selectedFile = result.files[0]!;
if (selectedFile.path && appService) {
const filePath = selectedFile.path;
metadata.coverImageFile = filePath;
metadata.coverImageUrl = await appService.getCachedImageUrl(filePath);
setNewCoverImageUrl(metadata.coverImageUrl!);
onFieldChange('coverImageFile', filePath);
onFieldChange('coverImageUrl', await appService.getCachedImageUrl(filePath));
setNewCoverImageUrl(filePath);
} else if (selectedFile.file) {
metadata.coverImageBlobUrl = URL.createObjectURL(selectedFile.file);
setNewCoverImageUrl(metadata.coverImageBlobUrl!);
const coverImageBlobUrl = URL.createObjectURL(selectedFile.file);
onFieldChange('coverImageBlobUrl', coverImageBlobUrl);
setNewCoverImageUrl(coverImageBlobUrl);
}
});
};
@@ -209,9 +205,9 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
<button
onClick={() => {
setNewCoverImageUrl(emptyCoverImageUrl);
metadata.coverImageUrl = emptyCoverImageUrl;
metadata.coverImageFile = undefined;
metadata.coverImageBlobUrl = undefined;
onFieldChange('coverImageUrl', emptyCoverImageUrl);
onFieldChange('coverImageFile', undefined);
onFieldChange('coverImageBlobUrl', undefined);
}}
disabled={isCoverLocked}
className={clsx(
@@ -67,7 +67,7 @@ const SourceSelector: React.FC<SourceSelectorProps> = ({ sources, isOpen, onSele
{
title: formatTitle(source.data.title),
author: formatAuthors(source.data.author),
coverImageUrl: (source.data as Metadata)['coverImageUrl'] ?? undefined,
coverImageUrl: (source.data as Metadata)['coverImageUrl'] || '_blank',
} as Book
}
/>
@@ -52,7 +52,7 @@ export const useMetadataEdit = (metadata: BookMetadata | null) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleFieldChange = (field: string, value: string) => {
const handleFieldChange = (field: string, value: string | undefined) => {
if (lockedFields[field]) {
return;
}
@@ -67,7 +67,9 @@ export const useMetadataEdit = (metadata: BookMetadata | null) => {
}
setEditedMeta(newMeta as BookMetadata);
handleFieldValidation(field, value);
if (value !== undefined) {
handleFieldValidation(field, value);
}
if (fieldSources[field]) {
const newSources = { ...fieldSources };
@@ -20,7 +20,7 @@ import { useSettingsStore } from '@/store/settingsStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useResetViewSettings } from '@/hooks/useResetSettings';
import { useCustomTextureStore } from '@/store/customTextureStore';
import { saveViewSettings } from '@/helpers/viewSettings';
import { saveViewSettings } from '@/helpers/settings';
import { CODE_LANGUAGES, CodeLanguage, manageSyntaxHighlighting } from '@/utils/highlightjs';
import { SettingsPanelPanelProp } from './SettingsDialog';
import { useFileSelector } from '@/hooks/useFileSelector';
@@ -10,7 +10,7 @@ import { useEinkMode } from '@/hooks/useEinkMode';
import { getStyles } from '@/utils/style';
import { saveAndReload } from '@/utils/reload';
import { getMaxInlineSize } from '@/utils/config';
import { saveViewSettings } from '@/helpers/viewSettings';
import { saveViewSettings } from '@/helpers/settings';
import { SettingsPanelPanelProp } from './SettingsDialog';
import NumberInput from './NumberInput';
@@ -8,7 +8,7 @@ import { useTranslation } from '@/hooks/useTranslation';
import { useSettingsStore } from '@/store/settingsStore';
import { useCustomFontStore } from '@/store/customFontStore';
import { useFileSelector } from '@/hooks/useFileSelector';
import { saveViewSettings } from '@/helpers/viewSettings';
import { saveViewSettings } from '@/helpers/settings';
import { CustomFont, mountCustomFont } from '@/styles/fonts';
interface CustomFontsProps {
@@ -22,11 +22,10 @@ const DialogMenu: React.FC<DialogMenuProps> = ({
}) => {
const _ = useTranslation();
const iconSize = useResponsiveSize(16);
const { setFontPanelView, isFontLayoutSettingsGlobal, setFontLayoutSettingsGlobal } =
useSettingsStore();
const { setFontPanelView, isSettingsGlobal, setSettingsGlobal } = useSettingsStore();
const handleToggleGlobal = () => {
setFontLayoutSettingsGlobal(!isFontLayoutSettingsGlobal);
setSettingsGlobal(!isSettingsGlobal);
setIsDropdownOpen?.(false);
};
@@ -49,13 +48,9 @@ const DialogMenu: React.FC<DialogMenuProps> = ({
>
<MenuItem
label={_('Global Settings')}
tooltip={isFontLayoutSettingsGlobal ? _('Apply to All Books') : _('Apply to This Book')}
tooltip={isSettingsGlobal ? _('Apply to All Books') : _('Apply to This Book')}
buttonClass='lg:tooltip'
Icon={
isFontLayoutSettingsGlobal ? (
<MdCheck size={iconSize} className='text-base-content' />
) : null
}
Icon={isSettingsGlobal ? <MdCheck size={iconSize} className='text-base-content' /> : null}
onClick={handleToggleGlobal}
/>
<MenuItem label={resetLabel || _('Reset Settings')} onClick={handleResetToDefaults} />
@@ -28,7 +28,7 @@ import { getSysFontsList } from '@/utils/bridge';
import { isCJKStr } from '@/utils/lang';
import { isTauriAppPlatform } from '@/services/environment';
import { useResetViewSettings } from '@/hooks/useResetSettings';
import { saveViewSettings } from '@/helpers/viewSettings';
import { saveViewSettings } from '@/helpers/settings';
import { SettingsPanelPanelProp } from './SettingsDialog';
import NumberInput from './NumberInput';
import FontDropdown from './FontDropDown';
@@ -215,7 +215,6 @@ const FontPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
if (!fontName || isSymbolicFontName(fontName)) return;
const fontsInFamily = Object.entries(res.fonts).filter(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
([_, family]) => family === fontFamily,
);
@@ -5,7 +5,7 @@ import { useAuth } from '@/context/AuthContext';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useSettingsStore } from '@/store/settingsStore';
import { saveViewSettings } from '@/helpers/viewSettings';
import { saveViewSettings } from '@/helpers/settings';
import { getTranslators } from '@/services/translators';
import { useResetViewSettings } from '@/hooks/useResetSettings';
import { TRANSLATED_LANGS, TRANSLATOR_LANGS } from '@/services/constants';
@@ -15,7 +15,7 @@ import { getStyles } from '@/utils/style';
import { saveAndReload } from '@/utils/reload';
import { getMaxInlineSize } from '@/utils/config';
import { lockScreenOrientation } from '@/utils/bridge';
import { saveViewSettings } from '@/helpers/viewSettings';
import { saveViewSettings } from '@/helpers/settings';
import { getBookDirFromWritingMode, getBookLangCode } from '@/utils/book';
import { MIGHT_BE_RTL_LANGS } from '@/services/constants';
import { SettingsPanelPanelProp } from './SettingsDialog';
@@ -15,7 +15,7 @@ type CSSType = 'book' | 'reader';
const MiscPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { appService } = useEnv();
const { settings, isFontLayoutSettingsGlobal, setSettings } = useSettingsStore();
const { settings, isSettingsGlobal, setSettings } = useSettingsStore();
const { getView, getViewSettings, setViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey) || settings.globalViewSettings;
@@ -90,7 +90,7 @@ const MiscPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
setDraftContentStylesheetSaved(true);
viewSettings.userStylesheet = formattedCSS;
if (isFontLayoutSettingsGlobal) {
if (isSettingsGlobal) {
settings.globalViewSettings.userStylesheet = formattedCSS;
setSettings(settings);
}
@@ -99,7 +99,7 @@ const MiscPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
setDraftUIStylesheetSaved(true);
viewSettings.userUIStylesheet = formattedCSS;
if (isFontLayoutSettingsGlobal) {
if (isSettingsGlobal) {
settings.globalViewSettings.userUIStylesheet = formattedCSS;
setSettings(settings);
}
@@ -41,7 +41,7 @@ const SettingsDialog: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [isRtl] = useState(() => getDirFromUILanguage() === 'rtl');
const tabsRef = useRef<HTMLDivElement | null>(null);
const [showAllTabLabels, setShowAllTabLabels] = useState(false);
const { setFontPanelView, setFontLayoutSettingsDialogOpen } = useSettingsStore();
const { setFontPanelView, setSettingsDialogOpen } = useSettingsStore();
const tabConfig = [
{
@@ -113,7 +113,7 @@ const SettingsDialog: React.FC<{ bookKey: string }> = ({ bookKey }) => {
};
const handleClose = () => {
setFontLayoutSettingsDialogOpen(false);
setSettingsDialogOpen(false);
};
useEffect(() => {
@@ -14,38 +14,14 @@ type ThemeEditorProps = {
onCancel: () => void;
};
const ThemeEditor: React.FC<ThemeEditorProps> = ({ customTheme, onSave, onDelete, onCancel }) => {
const ThemePreview: React.FC<{
textColor: string;
backgroundColor: string;
primaryColor: string;
label: string;
}> = ({ textColor, backgroundColor, primaryColor, label }) => {
const _ = useTranslation();
const { settings } = useSettingsStore();
const template =
CUSTOM_THEME_TEMPLATES[Math.floor(Math.random() * CUSTOM_THEME_TEMPLATES.length)]!;
const [lightTextColor, setLightTextColor] = useState(
customTheme?.colors.light.fg || template.light.fg,
);
const [lightBackgroundColor, setLightBackgroundColor] = useState(
customTheme?.colors.light.bg || template.light.bg,
);
const [lightPrimaryColor, setLightPrimaryColor] = useState(
customTheme?.colors.light.primary || template.light.primary,
);
const [darkTextColor, setDarkTextColor] = useState(
customTheme?.colors.dark.fg || template.dark.fg,
);
const [darkBackgroundColor, setDarkBackgroundColor] = useState(
customTheme?.colors.dark.bg || template.dark.bg,
);
const [darkPrimaryColor, setDarkPrimaryColor] = useState(
customTheme?.colors.dark.primary || template.dark.primary,
);
const [themeName, setThemeName] = useState(customTheme?.label || _('Custom'));
const ThemePreview: React.FC<{
textColor: string;
backgroundColor: string;
primaryColor: string;
label: string;
}> = ({ textColor, backgroundColor, primaryColor, label }) => (
return (
<div className='mb-2 mt-4'>
<label className='mb-1 block text-sm font-medium'>{label}</label>
<div
@@ -72,6 +48,34 @@ const ThemeEditor: React.FC<ThemeEditorProps> = ({ customTheme, onSave, onDelete
</div>
</div>
);
};
const ThemeEditor: React.FC<ThemeEditorProps> = ({ customTheme, onSave, onDelete, onCancel }) => {
const _ = useTranslation();
const { settings } = useSettingsStore();
const [template] = useState(
() => CUSTOM_THEME_TEMPLATES[Math.floor(Math.random() * CUSTOM_THEME_TEMPLATES.length)]!,
);
const [lightTextColor, setLightTextColor] = useState(
customTheme?.colors.light.fg || template.light.fg,
);
const [lightBackgroundColor, setLightBackgroundColor] = useState(
customTheme?.colors.light.bg || template.light.bg,
);
const [lightPrimaryColor, setLightPrimaryColor] = useState(
customTheme?.colors.light.primary || template.light.primary,
);
const [darkTextColor, setDarkTextColor] = useState(
customTheme?.colors.dark.fg || template.dark.fg,
);
const [darkBackgroundColor, setDarkBackgroundColor] = useState(
customTheme?.colors.dark.bg || template.dark.bg,
);
const [darkPrimaryColor, setDarkPrimaryColor] = useState(
customTheme?.colors.dark.primary || template.dark.primary,
);
const [themeName, setThemeName] = useState(customTheme?.label || _('Custom'));
const getCustomTheme = () => {
return {
@@ -1,4 +1,5 @@
import { ViewSettings } from '@/types/book';
import { SystemSettings } from '@/types/settings';
import { EnvConfigType } from '@/services/environment';
import { useBookDataStore } from '@/store/bookDataStore';
import { useReaderStore } from '@/store/readerStore';
@@ -13,8 +14,7 @@ export const saveViewSettings = async <K extends keyof ViewSettings>(
skipGlobal = false,
applyStyles = true,
) => {
const { settings, isFontLayoutSettingsGlobal, setSettings, saveSettings } =
useSettingsStore.getState();
const { settings, isSettingsGlobal, setSettings, saveSettings } = useSettingsStore.getState();
const { getView, getViewSettings, setViewSettings } = useReaderStore.getState();
const { getConfig, saveConfig } = useBookDataStore.getState();
const viewSettings = getViewSettings(bookKey);
@@ -27,7 +27,7 @@ export const saveViewSettings = async <K extends keyof ViewSettings>(
}
}
if (isFontLayoutSettingsGlobal && !skipGlobal) {
if (isSettingsGlobal && !skipGlobal) {
settings.globalViewSettings[key] = value;
setSettings(settings);
await saveSettings(envConfig, settings);
@@ -38,3 +38,16 @@ export const saveViewSettings = async <K extends keyof ViewSettings>(
await saveConfig(envConfig, bookKey, config, settings);
}
};
export const saveSysSettings = async <K extends keyof SystemSettings>(
envConfig: EnvConfigType,
key: K,
value: SystemSettings[K],
) => {
const { settings, setSettings, saveSettings } = useSettingsStore.getState();
if (settings[key] !== value) {
settings[key] = value;
setSettings(settings);
await saveSettings(envConfig, settings);
}
};
@@ -727,7 +727,6 @@ export abstract class BaseAppService implements AppService {
}
async saveLibraryBooks(books: Book[]): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const libraryBooks = books.map(({ coverImageUrl, ...rest }) => rest);
const jsonData = JSON.stringify(libraryBooks, null, 2);
const libraryFilename = getLibraryFilename();
@@ -113,7 +113,6 @@ export class TTSController extends EventTarget {
async preloadSSML(ssml: string | undefined, signal: AbortSignal) {
if (!ssml) return;
const iter = await this.ttsClient.speak(ssml, signal, true);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const _ of iter);
}
@@ -31,7 +31,6 @@ interface FontStoreState {
}
function toSettingsFont(font: CustomFont): CustomFont {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { blobUrl, loaded, error, ...settingsFont } = font;
return settingsFont;
}
@@ -37,7 +37,6 @@ interface TextureStoreState {
}
function toSettingsTexture(texture: CustomTexture): CustomTexture {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { blobUrl, loaded, error, ...settingsTexture } = texture;
return settingsTexture;
}
+8 -8
View File
@@ -8,13 +8,13 @@ export type FontPanelView = 'main-fonts' | 'custom-fonts';
interface SettingsState {
settings: SystemSettings;
isFontLayoutSettingsDialogOpen: boolean;
isFontLayoutSettingsGlobal: boolean;
isSettingsDialogOpen: boolean;
isSettingsGlobal: boolean;
fontPanelView: FontPanelView;
setSettings: (settings: SystemSettings) => void;
saveSettings: (envConfig: EnvConfigType, settings: SystemSettings) => void;
setFontLayoutSettingsDialogOpen: (open: boolean) => void;
setFontLayoutSettingsGlobal: (global: boolean) => void;
setSettingsDialogOpen: (open: boolean) => void;
setSettingsGlobal: (global: boolean) => void;
setFontPanelView: (view: FontPanelView) => void;
applyUILanguage: (uiLanguage?: string) => void;
@@ -22,16 +22,16 @@ interface SettingsState {
export const useSettingsStore = create<SettingsState>((set) => ({
settings: {} as SystemSettings,
isFontLayoutSettingsDialogOpen: false,
isFontLayoutSettingsGlobal: true,
isSettingsDialogOpen: false,
isSettingsGlobal: true,
fontPanelView: 'main-fonts',
setSettings: (settings) => set({ settings }),
saveSettings: async (envConfig: EnvConfigType, settings: SystemSettings) => {
const appService = await envConfig.getAppService();
await appService.saveSettings(settings);
},
setFontLayoutSettingsDialogOpen: (open) => set({ isFontLayoutSettingsDialogOpen: open }),
setFontLayoutSettingsGlobal: (global) => set({ isFontLayoutSettingsGlobal: global }),
setSettingsDialogOpen: (open) => set({ isSettingsDialogOpen: open }),
setSettingsGlobal: (global) => set({ isSettingsGlobal: global }),
setFontPanelView: (view) => set({ fontPanelView: view }),
applyUILanguage: (uiLanguage?: string) => {
-1
View File
@@ -3,7 +3,6 @@ import type { NextApiRequest, NextApiResponse } from 'next';
// Helper method to wait for a middleware to execute before continuing
// And to throw an error when an error happens in a middleware
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
export const runMiddleware = (req: NextApiRequest, res: NextApiResponse, fn: Function) => {
return new Promise((resolve, reject) => {
fn(req, res, (result: unknown) => {
+831 -183
View File
File diff suppressed because it is too large Load Diff