forked from akai/readest
chore: bump eslint-config-next to version 16 (#2294)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+16
-3
@@ -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,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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Generated
+831
-183
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user