feat: add books sorting by title and author in the library page, closes #540 (#887)

This commit is contained in:
Huang Xin
2025-04-14 23:38:41 +08:00
committed by GitHub
parent 8efad90932
commit 59f875f590
28 changed files with 342 additions and 34 deletions
@@ -283,5 +283,12 @@
"DOWNLOAD & INSTALL": "تنزيل وتثبيت",
"Changelog:": "سجل التغييرات:",
"Redownload": "إعادة التنزيل",
"Software Update": "تحديث البرنامج"
"Software Update": "تحديث البرنامج",
"Title": "العنوان",
"Date Read": "تاريخ القراءة",
"Date Added": "تاريخ الإضافة",
"Format": "التنسيق",
"Ascending": "تصاعدي",
"Descending": "تنازلي",
"Sort by...": "ترتيب حسب..."
}
@@ -283,5 +283,12 @@
"DOWNLOAD & INSTALL": "HERUNTERLADEN & INSTALLIEREN",
"Changelog:": "Änderungsprotokoll:",
"Redownload": "Neu herunterladen",
"Software Update": "Software-Update"
"Software Update": "Software-Update",
"Title": "Titel",
"Date Read": "Datum gelesen",
"Date Added": "Datum hinzugefügt",
"Format": "Format",
"Ascending": "Aufsteigend",
"Descending": "Absteigend",
"Sort by...": "Sortieren nach..."
}
@@ -283,5 +283,12 @@
"DOWNLOAD & INSTALL": "ΛΗΨΗ & ΕΓΚΑΤΑΣΤΑΣΗ",
"Changelog:": "Αλλαγές:",
"Redownload": "Επαναφόρτωση",
"Software Update": "Ενημέρωση λογισμικού"
"Software Update": "Ενημέρωση λογισμικού",
"Title": "Τίτλος",
"Date Read": "Ημερομηνία ανάγνωσης",
"Date Added": "Ημερομηνία προσθήκης",
"Format": "Μορφή",
"Ascending": "Αύξουσα",
"Descending": "Φθίνουσα",
"Sort by...": "Ταξινόμηση κατά..."
}
@@ -283,5 +283,12 @@
"DOWNLOAD & INSTALL": "DESCARGAR E INSTALAR",
"Changelog:": "Notas de la versión:",
"Redownload": "Volver a descargar",
"Software Update": "Actualización de software"
"Software Update": "Actualización de software",
"Title": "Título",
"Date Read": "Fecha de lectura",
"Date Added": "Fecha añadida",
"Format": "Formato",
"Ascending": "Ascendente",
"Descending": "Descendente",
"Sort by...": "Ordenar por..."
}
@@ -283,5 +283,12 @@
"DOWNLOAD & INSTALL": "TÉLÉCHARGER ET INSTALLER",
"Changelog:": "Journal des modifications :",
"Redownload": "Télécharger à nouveau",
"Software Update": "Mise à jour logicielle"
"Software Update": "Mise à jour logicielle",
"Title": "Titre",
"Date Read": "Date de lecture",
"Date Added": "Date ajoutée",
"Format": "Format",
"Ascending": "Ascendant",
"Descending": "Descendant",
"Sort by...": "Trier par..."
}
@@ -283,5 +283,12 @@
"DOWNLOAD & INSTALL": "डाउनलोड और इंस्टॉल करें",
"Changelog:": "चेंजलॉग:",
"Redownload": "री-डाउनलोड करें",
"Software Update": "सॉफ़्टवेयर अपडेट"
"Software Update": "सॉफ़्टवेयर अपडेट",
"Title": "शीर्षक",
"Date Read": "पढ़ने की तारीख",
"Date Added": "जोड़ा गया दिनांक",
"Format": "फॉर्मेट",
"Ascending": "आरोही",
"Descending": "अवरोही",
"Sort by...": "द्वारा क्रमबद्ध करें..."
}
@@ -283,5 +283,12 @@
"DOWNLOAD & INSTALL": "UNDUH & INSTAL",
"Changelog:": "Catatan Perubahan:",
"Redownload": "Unduh Ulang",
"Software Update": "Pembaruan Perangkat Lunak"
"Software Update": "Pembaruan Perangkat Lunak",
"Title": "Judul",
"Date Read": "Tanggal Dibaca",
"Date Added": "Tanggal Ditambahkan",
"Format": "Format",
"Ascending": "Naik",
"Descending": "Turun",
"Sort by...": "Urutkan berdasarkan..."
}
@@ -283,5 +283,12 @@
"DOWNLOAD & INSTALL": "SCARICA E INSTALLA",
"Changelog:": "Note di rilascio:",
"Redownload": "Scarica di nuovo",
"Software Update": "Aggiornamento software"
"Software Update": "Aggiornamento software",
"Title": "Titolo",
"Date Read": "Data di lettura",
"Date Added": "Data aggiunta",
"Format": "Formato",
"Ascending": "Crescente",
"Descending": "Decrescente",
"Sort by...": "Ordina per..."
}
@@ -283,5 +283,12 @@
"DOWNLOAD & INSTALL": "更新",
"Changelog:": "変更ログ:",
"Redownload": "再ダウンロード",
"Software Update": "ソフトウェアの更新"
"Software Update": "ソフトウェアの更新",
"Title": "タイトル",
"Date Read": "読書日",
"Date Added": "追加日",
"Format": "形式",
"Ascending": "昇順",
"Descending": "降順",
"Sort by...": "並べ替え..."
}
@@ -283,5 +283,12 @@
"DOWNLOAD & INSTALL": "다운로드 및 설치",
"Changelog:": "변경 로그:",
"Redownload": "다시 다운로드",
"Software Update": "소프트웨어 업데이트"
"Software Update": "소프트웨어 업데이트",
"Title": "제목",
"Date Read": "읽은 날짜",
"Date Added": "추가된 날짜",
"Format": "형식",
"Ascending": "오름차순",
"Descending": "내림차순",
"Sort by...": "정렬 기준..."
}
@@ -283,5 +283,12 @@
"DOWNLOAD & INSTALL": "POBIERZ I ZAINSTALUJ",
"Changelog:": "Dziennik zmian:",
"Redownload": "Pobierz ponownie",
"Software Update": "Aktualizacja oprogramowania"
"Software Update": "Aktualizacja oprogramowania",
"Title": "Tytuł",
"Date Read": "Data przeczytania",
"Date Added": "Data dodania",
"Format": "Format",
"Ascending": "Rosnąco",
"Descending": "Malejąco",
"Sort by...": "Sortuj według..."
}
@@ -283,5 +283,12 @@
"DOWNLOAD & INSTALL": "BAIXAR E INSTALAR",
"Changelog:": "Notas de versão:",
"Redownload": "Baixar novamente",
"Software Update": "Atualização de Software"
"Software Update": "Atualização de Software",
"Title": "Título",
"Date Read": "Data de leitura",
"Date Added": "Data de adição",
"Format": "Formato",
"Ascending": "Crescente",
"Descending": "Decrescente",
"Sort by...": "Ordenar por..."
}
@@ -283,5 +283,12 @@
"DOWNLOAD & INSTALL": "СКАЧАТЬ И УСТАНОВИТЬ",
"Changelog:": "Изменения:",
"Redownload": "Переустановить",
"Software Update": "Обновление программного обеспечения"
"Software Update": "Обновление программного обеспечения",
"Title": "Название",
"Date Read": "Дата прочтения",
"Date Added": "Дата добавления",
"Format": "Формат",
"Ascending": "По возрастанию",
"Descending": "По убыванию",
"Sort by...": "Сортировать по..."
}
@@ -283,5 +283,12 @@
"DOWNLOAD & INSTALL": "İNDİR & YÜKLE",
"Changelog:": "Değişiklik Günlüğü:",
"Redownload": "Yeniden İndir",
"Software Update": "Yazılım Güncellemesi"
"Software Update": "Yazılım Güncellemesi",
"Title": "Başlık",
"Date Read": "Okunma Tarihi",
"Date Added": "Eklenme Tarihi",
"Format": "Biçim",
"Ascending": "Artan",
"Descending": "Azalan",
"Sort by...": "Şuna göre sırala..."
}
@@ -283,5 +283,12 @@
"DOWNLOAD & INSTALL": "ЗАВАНТАЖИТИ І ВСТАНОВИТИ",
"Changelog:": "Журнал змін:",
"Redownload": "Завантажити знову",
"Software Update": "Оновлення програмного забезпечення"
"Software Update": "Оновлення програмного забезпечення",
"Title": "Назва",
"Date Read": "Дата прочитання",
"Date Added": "Дата додавання",
"Format": "Формат",
"Ascending": "За зростанням",
"Descending": "За спаданням",
"Sort by...": "Сортувати за..."
}
@@ -283,5 +283,12 @@
"DOWNLOAD & INSTALL": "TẢI XUỐNG & CÀI ĐẶT",
"Changelog:": "Thay đổi:",
"Redownload": "Tải lại",
"Software Update": "Cập nhật phần mềm"
"Software Update": "Cập nhật phần mềm",
"Title": "Tiêu đề",
"Date Read": "Ngày đã đọc",
"Date Added": "Ngày thêm",
"Format": "Định dạng",
"Ascending": "Tăng dần",
"Descending": "Giảm dần",
"Sort by...": "Sắp xếp theo..."
}
@@ -283,5 +283,12 @@
"DOWNLOAD & INSTALL": "下载并安装",
"Changelog:": "更新日志:",
"Redownload": "重新下载",
"Software Update": "软件更新"
"Software Update": "软件更新",
"Title": "书名",
"Date Read": "阅读日期",
"Date Added": "添加日期",
"Format": "格式",
"Ascending": "升序",
"Descending": "降序",
"Sort by...": "排序方式..."
}
@@ -283,5 +283,12 @@
"DOWNLOAD & INSTALL": "下載並安裝",
"Changelog:": "更新日誌:",
"Redownload": "重新下載",
"Software Update": "軟體更新"
"Software Update": "軟體更新",
"Title": "書名",
"Date Read": "閱讀日期",
"Date Added": "添加日期",
"Format": "格式",
"Ascending": "升序",
"Descending": "降序",
"Sort by...": "排序方式..."
}
@@ -7,6 +7,7 @@ import { LuFolderPlus } from 'react-icons/lu';
import { PiPlus } from 'react-icons/pi';
import { Book, BooksGroup } from '@/types/book';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useLibraryStore } from '@/store/libraryStore';
import { useTranslation } from '@/hooks/useTranslation';
import { navigateToLibrary, navigateToReader } from '@/utils/nav';
@@ -45,6 +46,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const router = useRouter();
const searchParams = useSearchParams();
const { appService } = useEnv();
const { settings } = useSettingsStore();
const [loading, setLoading] = useState(false);
const [selectedBooks, setSelectedBooks] = useState<string[]>([]);
const [showSelectModeActions, setShowSelectModeActions] = useState(false);
@@ -53,6 +55,8 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const [queryTerm, setQueryTerm] = useState<string | null>(null);
const [navBooksGroup, setNavBooksGroup] = useState<BooksGroup | null>(null);
const [importBookUrl] = useState(searchParams?.get('url') || '');
const [sortBy, setSortBy] = useState(searchParams?.get('sort') || 'updated');
const [sortOrder, setSortOrder] = useState(searchParams?.get('order') || 'desc');
const isImportingBook = useRef(false);
const { setLibrary } = useLibraryStore();
@@ -89,23 +93,47 @@ const Bookshelf: React.FC<BookshelfProps> = ({
useEffect(() => {
const group = searchParams?.get('group') || '';
const query = searchParams?.get('q') || '';
const sort = searchParams?.get('sort') || settings.librarySortBy;
const order = searchParams?.get('order') || (settings.librarySortAscending ? 'asc' : 'desc');
const params = new URLSearchParams(searchParams?.toString());
if (query) {
params.set('q', query);
setQueryTerm(query);
} else {
params.delete('q');
setQueryTerm(null);
}
if (sort) {
params.set('sort', sort);
setSortBy(sort);
} else {
params.delete('sort');
}
if (order) {
params.set('order', order);
setSortOrder(order);
} else {
params.delete('order');
}
if (sort === 'updated' && order === 'desc') {
params.delete('sort');
params.delete('order');
}
if (group) {
const booksGroup = allBookshelfItems.find(
(item) => 'name' in item && item.id === group,
) as BooksGroup;
if (booksGroup) {
setNavBooksGroup(booksGroup);
params.set('group', group);
} else {
navigateToLibrary(router, query ? `q=${query}` : undefined);
params.delete('group');
navigateToLibrary(router, `${params.toString()}`);
}
} else {
setNavBooksGroup(null);
navigateToLibrary(router, query ? `q=${query}` : undefined);
params.delete('group');
navigateToLibrary(router, `${params.toString()}`);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams, libraryBooks, showGroupingModal]);
@@ -152,11 +180,50 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const authors = formatAuthors(item.author);
return searchTerm.test(title) || searchTerm.test(authors);
};
const filteredBookshelfItems = currentBookshelfItems.filter((item) => {
if ('name' in item) return item.books.some((book) => bookFilter(book, queryTerm || ''));
else if (queryTerm) return bookFilter(item, queryTerm);
return true;
});
const bookSorter = (a: Book, b: Book) => {
const uiLanguage = localStorage?.getItem('i18nextLng') || '';
switch (sortBy) {
case 'title':
const aTitle = formatTitle(a.title);
const bTitle = formatTitle(b.title);
return aTitle.localeCompare(bTitle, uiLanguage || navigator.language);
case 'author':
const aAuthors = formatAuthors(a.author);
const bAuthors = formatAuthors(b.author);
return aAuthors.localeCompare(bAuthors, uiLanguage || navigator.language);
case 'updated':
return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
case 'created':
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
case 'format':
return a.format.localeCompare(b.format, uiLanguage || navigator.language);
default:
return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
}
};
const sortOrderMultiplier = sortOrder === 'asc' ? 1 : -1;
const filteredBookshelfItems = currentBookshelfItems
.filter((item) => {
if ('name' in item) return item.books.some((book) => bookFilter(book, queryTerm || ''));
else if (queryTerm) return bookFilter(item, queryTerm);
return true;
})
.sort((a, b) => {
const uiLanguage = localStorage?.getItem('i18nextLng') || '';
if (sortBy === 'updated') {
return (
(new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()) * sortOrderMultiplier
);
} else if ('name' in a || 'name' in b) {
const aName = 'name' in a ? a.name : formatTitle(a.title);
const bName = 'name' in b ? b.name : formatTitle(b.title);
return aName.localeCompare(bName, uiLanguage || navigator.language) * sortOrderMultiplier;
} else if (!('name' in a || 'name' in b)) {
return bookSorter(a, b) * sortOrderMultiplier;
} else {
return 0;
}
});
return (
<div className='bookshelf'>
@@ -4,6 +4,7 @@ import { useRouter, useSearchParams } from 'next/navigation';
import { FaSearch } from 'react-icons/fa';
import { PiPlus } from 'react-icons/pi';
import { PiSelectionAllDuotone } from 'react-icons/pi';
import { PiDotsThreeCircle } from 'react-icons/pi';
import { MdOutlineMenu, MdArrowBackIosNew } from 'react-icons/md';
import { IoMdCloseCircle } from 'react-icons/io';
@@ -13,11 +14,12 @@ import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useTrafficLightStore } from '@/store/trafficLightStore';
import { navigateToLibrary } from '@/utils/nav';
import { throttle } from '@/utils/throttle';
import useShortcuts from '@/hooks/useShortcuts';
import WindowButtons from '@/components/WindowButtons';
import Dropdown from '@/components/Dropdown';
import SettingsMenu from './SettingsMenu';
import ImportMenu from './ImportMenu';
import useShortcuts from '@/hooks/useShortcuts';
import SortMenu from './SortMenu';
interface LibraryHeaderProps {
isSelectMode: boolean;
@@ -44,7 +46,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
const [searchQuery, setSearchQuery] = useState(searchParams?.get('q') ?? '');
const headerRef = useRef<HTMLDivElement>(null);
const iconSize16 = useResponsiveSize(16);
const iconSize18 = useResponsiveSize(18);
const iconSize20 = useResponsiveSize(20);
useShortcuts({
@@ -173,10 +175,17 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<MdOutlineMenu size={iconSize16} />}
toggleButton={<MdOutlineMenu size={iconSize18} />}
>
<SettingsMenu />
</Dropdown>
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<PiDotsThreeCircle size={iconSize18} />}
>
<SortMenu />
</Dropdown>
{appService?.hasWindowBar && (
<WindowButtons
headerRef={headerRef}
@@ -19,11 +19,11 @@ import { QuotaType } from '@/types/user';
import MenuItem from '@/components/MenuItem';
import Quota from '@/components/Quota';
interface BookMenuProps {
interface SettingsMenuProps {
setIsDropdownOpen?: (isOpen: boolean) => void;
}
const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
const _ = useTranslation();
const router = useRouter();
const { envConfig, appService } = useEnv();
@@ -132,7 +132,7 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
return (
<div
tabIndex={0}
className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-3 w-72 shadow-2xl'
className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-2 w-72 shadow-2xl'
>
{user ? (
<MenuItem
@@ -0,0 +1,96 @@
import React from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { MdCheck } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { LibrarySortByType } from '@/types/settings';
import { navigateToLibrary } from '@/utils/nav';
import MenuItem from '@/components/MenuItem';
interface SortMenuProps {
setIsDropdownOpen?: (isOpen: boolean) => void;
}
const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
const _ = useTranslation();
const router = useRouter();
const searchParams = useSearchParams();
const { envConfig } = useEnv();
const { settings, setSettings, saveSettings } = useSettingsStore();
const sortBy = settings.librarySortBy;
const isAscending = settings.librarySortAscending;
const sortByOptions = [
{ label: _('Title'), value: 'title' },
{ label: _('Author'), value: 'author' },
{ label: _('Format'), value: 'format' },
{ label: _('Date Read'), value: 'updated' },
{ label: _('Date Added'), value: 'created' },
];
const sortingOptions = [
{ label: _('Ascending'), value: true },
{ label: _('Descending'), value: false },
];
const handleSetSortBy = (value: LibrarySortByType) => {
settings.librarySortBy = value;
setSettings(settings);
saveSettings(envConfig, settings);
setIsDropdownOpen?.(false);
const params = new URLSearchParams(searchParams?.toString());
params.set('sort', value);
navigateToLibrary(router, `${params.toString()}`);
};
const handleSetSortAscending = (value: boolean) => {
settings.librarySortAscending = value;
setSettings(settings);
saveSettings(envConfig, settings);
setIsDropdownOpen?.(false);
const params = new URLSearchParams(searchParams?.toString());
params.set('order', value ? 'asc' : 'desc');
navigateToLibrary(router, `${params.toString()}`);
};
return (
<div
tabIndex={0}
className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-2 shadow-2xl'
>
<MenuItem
label={_('Sort by...')}
buttonClass='h-8'
labelClass='text-sm sm:text-xs'
disabled
/>
{sortByOptions.map((option) => (
<MenuItem
key={option.value}
label={option.label}
buttonClass='h-8'
icon={sortBy === option.value ? <MdCheck className='text-base-content' /> : undefined}
onClick={() => handleSetSortBy(option.value as LibrarySortByType)}
/>
))}
<hr className='border-base-200 my-1' />
{sortingOptions.map((option) => (
<MenuItem
key={option.value.toString()}
label={option.label}
buttonClass='h-8'
icon={
isAscending === option.value ? <MdCheck className='text-base-content' /> : undefined
}
onClick={() => handleSetSortAscending(option.value)}
/>
))}
</div>
);
};
export default SortMenu;
+1 -1
View File
@@ -578,7 +578,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
/>
)}
<AboutWindow />
<UpdaterWindow />
{appService?.isAndroidApp && <UpdaterWindow />}
<Toast />
</div>
);
@@ -54,7 +54,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
<Suspense>
<ReaderContent ids={ids} settings={settings} />
<AboutWindow />
<UpdaterWindow />
{appService?.isAndroidApp && <UpdaterWindow />}
<Toast />
</Suspense>
</div>
@@ -4,6 +4,7 @@ import { useDefaultIconSize } from '@/hooks/useResponsiveSize';
interface MenuItemProps {
label: string;
buttonClass?: string;
labelClass?: string;
shortcut?: string;
disabled?: boolean;
@@ -15,6 +16,7 @@ interface MenuItemProps {
const MenuItem: React.FC<MenuItemProps> = ({
label,
buttonClass,
labelClass,
shortcut,
disabled,
@@ -29,6 +31,7 @@ const MenuItem: React.FC<MenuItemProps> = ({
className={clsx(
'hover:bg-base-300 text-base-content flex h-10 w-full items-center justify-between rounded-md p-2',
disabled && 'btn-disabled text-gray-400',
buttonClass,
)}
onClick={onClick}
disabled={disabled}
@@ -69,6 +69,7 @@ export const UpdaterContent = ({ version }: { version?: string }) => {
const [changelogs, setChangelogs] = useState<Changelog[]>([]);
const [progress, setProgress] = useState<number | null>(null);
const [contentLength, setContentLength] = useState<number | null>(null);
const [isDownloading, setIsDownloading] = useState(false);
const [downloaded, setDownloaded] = useState<number | null>(null);
useEffect(() => {
@@ -214,6 +215,7 @@ export const UpdaterContent = ({ version }: { version?: string }) => {
let contentLength = 0;
let lastLogged = 0;
setProgress(0);
setIsDownloading(true);
await update.downloadAndInstall?.((event) => {
switch (event.event) {
case 'Started':
@@ -293,7 +295,7 @@ export const UpdaterContent = ({ version }: { version?: string }) => {
<button
className={clsx(
'btn btn-warning text-base-100 px-6 font-bold',
!update && 'btn-disabled',
(!update || isDownloading) && 'btn-disabled',
)}
onClick={handleDownloadInstall}
>
@@ -38,6 +38,8 @@ export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
autoCheckUpdates: true,
screenWakeLock: true,
autoImportBooksOnOpen: false,
librarySortBy: 'updated',
librarySortAscending: false,
lastSyncedAtBooks: 0,
lastSyncedAtConfigs: 0,
+3
View File
@@ -2,6 +2,7 @@ import { CustomTheme } from '@/styles/themes';
import { HighlightColor, HighlightStyle, ViewSettings } from './book';
export type ThemeType = 'light' | 'dark' | 'auto';
export type LibrarySortByType = 'title' | 'author' | 'updated' | 'created' | 'size' | 'format';
export interface ReadSettings {
sideBarWidth: string;
@@ -26,6 +27,8 @@ export interface SystemSettings {
autoCheckUpdates: boolean;
screenWakeLock: boolean;
autoImportBooksOnOpen: boolean;
librarySortBy: LibrarySortByType;
librarySortAscending: boolean;
lastSyncedAtBooks: number;
lastSyncedAtConfigs: number;