Make dialogs responsive for mobile devices (#209)

* Make dialogs responsive for mobile devices

* Support mouse backward/forward buttons

* Use back icon to close layer 2 components on mobile devices
Currently layer 2 components are: 1. sidebar 2. notebook 3. dialogs
They are all fullscreen on mobile devices.

* Add translations
This commit is contained in:
Huang Xin
2025-01-21 19:49:41 +01:00
committed by GitHub
parent 07116bdae8
commit fc2cad3980
30 changed files with 472 additions and 284 deletions
@@ -135,5 +135,8 @@
"Apply to this book": "Auf dieses Buch anwenden",
"Unable to fetch the translation. Try again later.": "Übersetzung konnte nicht abgerufen werden. Versuchen Sie es später erneut.",
"Update Now!": "Jetzt aktualisieren!",
"Update": "Aktualisieren"
"Update": "Aktualisieren",
"Check update": "Aktualisierung prüfen",
"Already the latest version": "Bereits die neueste Version",
"Book Details": "Buchdetails"
}
@@ -135,5 +135,8 @@
"Apply to this book": "Εφαρμογή σε αυτό το βιβλίο",
"Unable to fetch the translation. Try again later.": "Αδυναμία λήψης της μετάφρασης. Δοκιμάστε ξανά αργότερα.",
"Update Now!": "Ενημέρωση τώρα!",
"Update": "Ενημέρωση"
"Update": "Ενημέρωση",
"Check update": "Έλεγχος ενημέρωσης",
"Already the latest version": "Ήδη η τελευταία έκδοση",
"Book Details": "Λεπτομέρειες βιβλίου"
}
@@ -135,5 +135,8 @@
"Apply to this book": "Aplicar a este libro",
"Unable to fetch the translation. Try again later.": "No se puede obtener la traducción. Inténtalo de nuevo más tarde.",
"Update Now!": "¡Actualizar ahora!",
"Update": "Actualizar"
"Update": "Actualizar",
"Check update": "Comprobar actualización",
"Already the latest version": "Ya es la última versión",
"Book Details": "Detalles del libro"
}
@@ -135,5 +135,8 @@
"Apply to this book": "Appliquer à ce livre",
"Unable to fetch the translation. Try again later.": "Impossible de récupérer la traduction. Réessayez plus tard.",
"Update Now!": "Mettre à jour maintenant !",
"Update": "Mettre à jour"
"Update": "Mettre à jour",
"Check update": "Vérifier la mise à jour",
"Already the latest version": "Déjà la dernière version",
"Book Details": "Détails du livre"
}
@@ -135,5 +135,8 @@
"Apply to this book": "इस पुस्तक पर लागू करें",
"Unable to fetch the translation. Try again later.": "अनुवाद प्राप्त करने में असमर्थ। बाद में पुनः प्रयास करें।",
"Update Now!": "अभी अपडेट करें!",
"Update": "अपडेट करें"
"Update": "अपडेट करें",
"Check update": "अपडेट जांचें",
"Already the latest version": "पहले से ही नवीनतम संस्करण",
"Book Details": "पुस्तक विवरण"
}
@@ -135,5 +135,8 @@
"Apply to this book": "Terapkan ke buku ini",
"Unable to fetch the translation. Try again later.": "Tidak dapat mengambil terjemahan. Coba lagi nanti.",
"Update Now!": "Perbarui Sekarang!",
"Update": "Perbarui"
"Update": "Perbarui",
"Check update": "Periksa pembaruan",
"Already the latest version": "Sudah versi terbaru",
"Book Details": "Detail Buku"
}
@@ -135,5 +135,8 @@
"Apply to this book": "Applica a questo libro",
"Unable to fetch the translation. Try again later.": "Impossibile recuperare la traduzione. Riprova più tardi.",
"Update Now!": "Aggiorna ora!",
"Update": "Aggiorna"
"Update": "Aggiorna",
"Check update": "Controlla aggiornamento",
"Already the latest version": "Già l'ultima versione",
"Book Details": "Dettagli libro"
}
@@ -135,5 +135,8 @@
"Apply to this book": "この書籍に適用",
"Unable to fetch the translation. Try again later.": "翻訳を取得できません。後で再試行してください。",
"Update Now!": "今すぐ更新!",
"Update": "更新"
"Update": "更新",
"Check update": "更新を確認",
"Already the latest version": "すでに最新バージョン",
"Book Details": "書籍の詳細"
}
@@ -135,5 +135,8 @@
"Apply to this book": "이 책에 적용",
"Unable to fetch the translation. Try again later.": "번역을 가져올 수 없습니다. 나중에 다시 시도하세요.",
"Update Now!": "지금 업데이트!",
"Update": "업데이트"
"Update": "업데이트",
"Check update": "업데이트 확인",
"Already the latest version": "이미 최신 버전",
"Book Details": "책 세부 정보"
}
@@ -135,5 +135,8 @@
"Apply to this book": "Zastosuj do tej książki",
"Unable to fetch the translation. Try again later.": "Nie udało się pobrać tłumaczenia. Spróbuj ponownie później.",
"Update Now!": "Zaktualizuj teraz!",
"Update": "Zaktualizuj"
"Update": "Zaktualizuj",
"Check update": "Sprawdź aktualizację",
"Already the latest version": "Już najnowsza wersja",
"Book Details": "Szczegóły książki"
}
@@ -135,5 +135,8 @@
"Apply to this book": "Aplicar a este livro",
"Unable to fetch the translation. Try again later.": "Não foi possível buscar a tradução. Tente novamente mais tarde.",
"Update Now!": "Atualizar Agora!",
"Update": "Atualizar"
"Update": "Atualizar",
"Check update": "Verificar atualização",
"Already the latest version": "Já é a versão mais recente",
"Book Details": "Detalhes do Livro"
}
@@ -135,5 +135,8 @@
"Apply to this book": "Применить к этой книге",
"Unable to fetch the translation. Try again later.": "Не удалось получить перевод. Попробуйте позже.",
"Update Now!": "Обновить сейчас!",
"Update": "Обновить"
"Update": "Обновить",
"Check update": "Проверить обновление",
"Already the latest version": "Уже последняя версия",
"Book Details": "Детали книги"
}
@@ -135,5 +135,8 @@
"Apply to this book": "Bu kitaba uygula",
"Unable to fetch the translation. Try again later.": "Çeviri alınamıyor. Daha sonra tekrar deneyin.",
"Update Now!": "Şimdi Güncelle!",
"Update": "Güncelle"
"Update": "Güncelle",
"Check update": "Güncellemeyi kontrol et",
"Already the latest version": "Zaten en son sürüm",
"Book Details": "Kitap Detayları"
}
@@ -135,5 +135,8 @@
"Apply to this book": "Застосувати до цієї книги",
"Unable to fetch the translation. Try again later.": "Не вдалося отримати переклад. Спробуйте пізніше.",
"Update Now!": "Оновити зараз!",
"Update": "Оновити"
"Update": "Оновити",
"Check update": "Перевірити оновлення",
"Already the latest version": "Вже остання версія",
"Book Details": "Деталі книги"
}
@@ -135,5 +135,8 @@
"Apply to this book": "Áp dụng cho sách này",
"Unable to fetch the translation. Try again later.": "Không thể tải dịch vụ. Vui lòng thử lại sau.",
"Update Now!": "Cập nhật ngay!",
"Update": "Cập nhật"
"Update": "Cập nhật",
"Check update": "Kiểm tra cập nhật",
"Already the latest version": "Đã là phiên bản mới nhất",
"Book Details": "Chi tiết sách"
}
@@ -135,5 +135,8 @@
"Apply to this book": "应用于此书籍",
"Unable to fetch the translation. Try again later.": "无法获取翻译,请稍后重试。",
"Update Now!": "立即更新!",
"Update": "更新"
"Update": "更新",
"Check update": "检查更新",
"Already the latest version": "已经是最新版本",
"Book Details": "书籍详情"
}
@@ -135,5 +135,8 @@
"Apply to this book": "應用於此書籍",
"Unable to fetch the translation. Try again later.": "無法獲取翻譯,請稍後再試。",
"Update Now!": "立即更新!",
"Update": "更新"
"Update": "更新",
"Check update": "檢查更新",
"Already the latest version": "已經是最新版本",
"Book Details": "書籍詳情"
}
@@ -48,8 +48,8 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
isTrafficLightVisible ? 'pl-16' : 'pl-2',
)}
>
<div className='flex items-center justify-between space-x-6'>
<div className='exclude-title-bar-mousedown sm:w relative flex w-full items-center pl-4'>
<div className='flex items-center justify-between space-x-12'>
<div className='exclude-title-bar-mousedown relative flex w-full items-center pl-4'>
<span className='absolute left-8 text-gray-500'>
<FaSearch className='h-4 w-4' />
</span>
@@ -97,20 +97,22 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
</button>
</div>
</div>
<div className='flex h-full items-center'>
<div className='flex h-full items-center gap-x-4'>
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end mr-2'
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={16} />}
>
<SettingsMenu />
</Dropdown>
<WindowButtons
headerRef={headerRef}
showMinimize={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
showMaximize={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
showClose={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
/>
{!isTrafficLightVisible && appService?.appPlatform !== 'web' && (
<WindowButtons
headerRef={headerRef}
showMinimize={true}
showMaximize={true}
showClose={true}
/>
)}
</div>
</div>
</div>
@@ -84,8 +84,12 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
<WindowButtons
className='window-buttons flex h-full items-center'
headerRef={headerRef}
showMinimize={bookKeys.length == 1 && appService?.appPlatform !== 'web'}
showMaximize={bookKeys.length == 1 && appService?.appPlatform !== 'web'}
showMinimize={
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
}
showMaximize={
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
}
onClose={() => onCloseBook(bookKey)}
/>
</div>
@@ -305,6 +305,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
if (!selection || !selection.text) return;
const { sectionHref: href } = progress;
selection.href = href;
setShowAnnotPopup(false);
setHighlightOptionsVisible(false);
setNotebookVisible(true);
setNotebookNewAnnotation(selection);
@@ -3,7 +3,7 @@ import React from 'react';
import { FiSearch } from 'react-icons/fi';
import { LuNotebookPen } from 'react-icons/lu';
import { MdOutlinePushPin, MdPushPin } from 'react-icons/md';
import { MdArrowBackIosNew, MdOutlinePushPin, MdPushPin } from 'react-icons/md';
import { useTranslation } from '@/hooks/useTranslation';
const NotebookHeader: React.FC<{
@@ -18,7 +18,7 @@ const NotebookHeader: React.FC<{
<LuNotebookPen size={14} />
<div className='notebook-title text-sm font-medium'>{_('Notebook')}</div>
</div>
<div className='z-10 flex items-center gap-x-3'>
<div className='z-10 flex items-center gap-x-4'>
<button
onClick={handleTogglePin}
className={clsx(
@@ -30,14 +30,9 @@ const NotebookHeader: React.FC<{
</button>
<button
onClick={handleClose}
className={'bg-base-300/65 btn btn-ghost btn-circle h-6 min-h-6 w-6 sm:hidden'}
className={'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent sm:hidden'}
>
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'>
<path
fill='currentColor'
d='M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z'
/>
</svg>
<MdArrowBackIosNew size={20} />
</button>
<button className='btn btn-ghost left-0 h-8 min-h-8 w-8 p-0'>
<FiSearch size={18} />
@@ -57,7 +57,7 @@ const FontDropdown: React.FC<DropdownProps> = ({
tabIndex={0}
className={clsx(
'dropdown-content bgcolor-base-200 menu rounded-box relative z-[1] overflow-y-scroll shadow',
'!mr-5 mb-[-46px] inline max-h-80 w-[250px] overflow-y-scroll',
'!mr-5 mb-[-46px] inline max-h-80 w-[220px] overflow-y-scroll',
)}
>
{moreOptions.map((option) => (
@@ -1,5 +1,5 @@
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import { BookConfig } from '@/types/book';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
@@ -8,12 +8,13 @@ import { RiDashboardLine } from 'react-icons/ri';
import { VscSymbolColor } from 'react-icons/vsc';
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
import { IoAccessibilityOutline } from 'react-icons/io5';
import { MdArrowBackIosNew } from 'react-icons/md';
import FontPanel from './FontPanel';
import LayoutPanel from './LayoutPanel';
import ColorPanel from './ColorPanel';
import WindowButtons from '@/components/WindowButtons';
import Dropdown from '@/components/Dropdown';
import Dialog from '@/components/Dialog';
import DialogMenu from './DialogMenu';
import MiscPanel from './MiscPanel';
@@ -30,20 +31,6 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
const [activePanel, setActivePanel] = useState<SettingsPanelType>('Font');
const { setFontLayoutSettingsDialogOpen } = useSettingsStore();
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setFontLayoutSettingsDialogOpen(false);
}
};
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const tabConfig = [
{
tab: 'Font',
@@ -67,50 +54,77 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
},
] as TabConfig[];
return (
<dialog className='modal modal-open sm:min-w-90 h-full w-full !bg-[rgba(0,0,0,0.2)] sm:w-full'>
<div className='modal-box settings-content flex h-full max-h-full w-full max-w-full flex-col rounded-none p-0 sm:h-[65%] sm:w-1/2 sm:min-w-[480px] sm:max-w-full sm:rounded-2xl'>
<div className='dialog-header bg-base-100 sticky top-0 z-10 flex items-center justify-center px-4 pt-2'>
<div className='dialog-tabs flex h-10 max-w-[75%] flex-grow items-center justify-around sm:max-w-[80%]'>
{tabConfig.map(({ tab, icon: Icon, label }) => (
<button
key={tab}
className={clsx(
'btn btn-ghost text-base-content h-8 min-h-8',
activePanel === tab ? 'btn-active' : '',
)}
onClick={() => setActivePanel(tab)}
>
<Icon size={20} className='mr-0' />
{window.innerWidth >= 500 ? label : ''}
</button>
))}
</div>
<div className='flex h-full items-center justify-end'>
<Dropdown
className='dropdown-bottom dropdown-end'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<PiDotsThreeVerticalBold size={16} />}
>
<DialogMenu />
</Dropdown>
<WindowButtons
className='window-buttons absolute right-4 !ml-2 flex h-full items-center'
showMinimize={false}
showMaximize={false}
onClose={() => setFontLayoutSettingsDialogOpen(false)}
/>
</div>
</div>
const handleClose = () => {
setFontLayoutSettingsDialogOpen(false);
};
<div className='text-base-content mt-2 flex-grow overflow-y-auto px-[10%]'>
{activePanel === 'Font' && <FontPanel bookKey={bookKey} />}
{activePanel === 'Layout' && <LayoutPanel bookKey={bookKey} />}
{activePanel === 'Color' && <ColorPanel bookKey={bookKey} />}
{activePanel === 'Misc' && <MiscPanel bookKey={bookKey} />}
</div>
</div>
</dialog>
return (
<>
<Dialog
isOpen={true}
onClose={handleClose}
className='modal-open'
boxClassName='sm:w-1/2 sm:min-w-[480px]'
header={
<div className='flex w-full items-center justify-between'>
<button
tabIndex={-1}
onClick={handleClose}
className={
'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent focus:outline-none sm:hidden'
}
>
<MdArrowBackIosNew size={20} />
</button>
<div className='dialog-tabs flex h-10 max-w-[100%] flex-grow items-center justify-around pl-4'>
{tabConfig.map(({ tab, icon: Icon, label }) => (
<button
key={tab}
className={clsx(
'btn btn-ghost text-base-content h-8 min-h-8',
activePanel === tab ? 'btn-active' : '',
)}
onClick={() => setActivePanel(tab)}
>
<Icon size={20} className='mr-0' />
{window.innerWidth >= 500 ? label : ''}
</button>
))}
</div>
<div className='flex h-full items-center justify-end gap-x-2'>
<Dropdown
className='dropdown-bottom dropdown-end'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<PiDotsThreeVerticalBold size={16} />}
>
<DialogMenu />
</Dropdown>
<button
onClick={handleClose}
className={'bg-base-300/65 btn btn-ghost btn-circle hidden h-6 min-h-6 w-6 sm:flex'}
>
<svg
xmlns='http://www.w3.org/2000/svg'
width='1em'
height='1em'
viewBox='0 0 24 24'
>
<path
fill='currentColor'
d='M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z'
/>
</svg>
</button>
</div>
</div>
}
>
{activePanel === 'Font' && <FontPanel bookKey={bookKey} />}
{activePanel === 'Layout' && <LayoutPanel bookKey={bookKey} />}
{activePanel === 'Color' && <ColorPanel bookKey={bookKey} />}
{activePanel === 'Misc' && <MiscPanel bookKey={bookKey} />}
</Dialog>
</>
);
};
@@ -3,6 +3,7 @@ import React from 'react';
import { GiBookshelf } from 'react-icons/gi';
import { FiSearch } from 'react-icons/fi';
import { MdOutlineMenu, MdOutlinePushPin, MdPushPin } from 'react-icons/md';
import { MdArrowBackIosNew } from 'react-icons/md';
import useTrafficLight from '@/hooks/useTrafficLight';
import Dropdown from '@/components/Dropdown';
@@ -24,12 +25,18 @@ const SidebarHeader: React.FC<{
isTrafficLightVisible ? 'pl-20' : 'pl-1.5',
)}
>
<div className='flex items-center'>
<div className='flex items-center gap-x-4'>
<button
onClick={onClose}
className={'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent sm:hidden'}
>
<MdArrowBackIosNew size={20} />
</button>
<button className='btn btn-ghost h-8 min-h-8 w-8 p-0' onClick={onGoToLibrary}>
<GiBookshelf size={20} className='fill-base-content' />
</button>
</div>
<div className='flex size-[70%] min-w-24 max-w-32 items-center justify-between'>
<div className='flex min-w-24 max-w-32 items-center justify-between sm:size-[70%]'>
<button
onClick={onToggleSearchBar}
className={clsx(
@@ -50,7 +57,7 @@ const SidebarHeader: React.FC<{
>
<BookMenu />
</Dropdown>
<div className='right-0 flex h-8 w-8 items-center justify-center'>
<div className='right-0 hidden h-8 w-8 items-center justify-center sm:flex'>
<button
onClick={onTogglePin}
className={clsx(
@@ -60,17 +67,6 @@ const SidebarHeader: React.FC<{
>
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
</button>
<button
onClick={onClose}
className={'bg-base-300/65 btn btn-ghost btn-circle h-6 min-h-6 w-6 sm:hidden'}
>
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'>
<path
fill='currentColor'
d='M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z'
/>
</svg>
</button>
</div>
</div>
</div>
@@ -46,6 +46,12 @@ export const useClickEvent = (
} else if (deltaY < 0) {
viewRef.current?.prev(1);
}
} else if (msg.data.type === 'iframe-mouseup') {
if (msg.data.button === 3) {
viewRef.current?.history.back();
} else if (msg.data.button === 4) {
viewRef.current?.history.forward();
}
}
}
} else {
@@ -115,14 +121,18 @@ export const useTouchEvent = (
const touch = e.targetTouches[0];
if (touch) {
touchEnd = touch;
};
}
const windowWidth = window.innerWidth;
if (touchEnd) {
const deltaY = touchEnd.screenY - touchStart.screenY;
const deltaX = touchEnd.screenX - touchStart.screenX;
// also check for deltaX to prevent swipe page turn from triggering the toggle
if (deltaY < -10 && Math.abs(deltaY) > Math.abs(deltaX) && Math.abs(deltaX) < windowWidth * 0.3) {
if (
deltaY < -10 &&
Math.abs(deltaY) > Math.abs(deltaX) &&
Math.abs(deltaX) < windowWidth * 0.3
) {
// swipe up to toggle the header bar and the footer bar
setHoveredBookKey(hoveredBookKey ? null : bookKey);
}
@@ -151,4 +161,4 @@ export const useTouchEvent = (
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hoveredBookKey, viewRef]);
}
};
@@ -30,6 +30,7 @@ export const handleMousedown = (bookKey: string, event: MouseEvent) => {
{
type: 'iframe-mousedown',
bookKey,
button: event.button,
screenX: event.screenX,
screenY: event.screenY,
clientX: event.clientX,
@@ -46,6 +47,7 @@ export const handleMouseup = (bookKey: string, event: MouseEvent) => {
{
type: 'iframe-mouseup',
bookKey,
button: event.button,
screenX: event.screenX,
screenY: event.screenY,
clientX: event.clientX,
@@ -150,7 +152,7 @@ const handleTouchEv = (bookKey: string, event: TouchEvent, type: string) => {
},
'*',
);
}
};
export const handleTouchStart = (bookKey: string, event: TouchEvent) => {
handleTouchEv(bookKey, event, 'iframe-touchstart');
+59 -59
View File
@@ -1,10 +1,10 @@
import React from 'react';
import Image from 'next/image';
import packageJson from '../../package.json';
import WindowButtons from './WindowButtons';
import { useTranslation } from '@/hooks/useTranslation';
import { hasUpdater } from '@/services/environment';
import { checkForAppUpdates } from '@/helpers/updater';
import Dialog from './Dialog';
export const setAboutDialogVisible = (visible: boolean) => {
const dialog = document.getElementById('about_window');
@@ -27,67 +27,67 @@ export const AboutWindow = () => {
};
return (
<dialog id='about_window' className='modal'>
<form method='dialog' className='modal-box w-96 max-w-lg p-4'>
<div className='dialog-header bg-base-100 sticky top-0 z-10 flex items-center justify-center p-2'>
<WindowButtons
className='window-buttons absolute right-0 flex h-full items-center'
showMinimize={false}
showMaximize={false}
onClose={() => setAboutDialogVisible(false)}
/>
</div>
<div className='flex flex-col items-center px-8'>
<div className='mb-4'>
<Image src='/icon.png' alt='App Logo' className='h-24 w-24' width={64} height={64} />
<>
<Dialog
id='about_window'
isOpen={false}
title={_('About Readest')}
onClose={() => setAboutDialogVisible(false)}
boxClassName='sm:!w-96'
>
<div className='about-content flex h-full flex-col items-center justify-center'>
<div className='flex flex-col items-center px-8'>
<div className='mb-4'>
<Image src='/icon.png' alt='App Logo' className='h-24 w-24' width={64} height={64} />
</div>
<h2 className='text-2xl font-bold'>Readest</h2>
<p className='text-neutral-content text-sm'>
{_('Version {{version}}', { version: packageJson.version })}
</p>
{hasUpdater() && !isUpdated && (
<span className='badge badge-primary mt-2' onClick={handleCheckUpdate}>
{_('Check update')}
</span>
)}
{isUpdated && (
<p className='text-neutral-content mt-2 text-xs'>{_('Already the latest version')}</p>
)}
</div>
<h2 className='text-2xl font-bold'>Readest</h2>
<p className='text-neutral-content text-sm'>
{_('Version {{version}}', { version: packageJson.version })}
</p>
{hasUpdater() && !isUpdated && (
<span className='badge badge-primary mt-2' onClick={handleCheckUpdate}>
{_('Check update')}
</span>
)}
{isUpdated && (
<p className='text-neutral-content mt-2 text-xs'>{_('Already the latest version')}</p>
)}
</div>
<div className='divider'></div>
<div className='divider py-12 sm:py-2'></div>
<div className='flex flex-col items-center px-4 text-center'>
<p className='text-neutral-content text-sm'>
© {new Date().getFullYear()} Bilingify LLC. All rights reserved.
</p>
<p className='text-neutral-content mt-2 text-xs'>
This software is licensed under the{' '}
<a
href='https://www.gnu.org/licenses/agpl-3.0.html'
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 underline'
>
GNU Affero General Public License v3.0
</a>
. You are free to use, modify, and distribute this software under the terms of the AGPL
v3 license. Please see the license for more details.
</p>
<p className='text-neutral-content mt-2 text-xs'>
Source code is available at{' '}
<a
href='https://github.com/readest/readest'
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 underline'
>
GitHub
</a>
.
</p>
<div className='flex flex-col items-center px-4 text-center'>
<p className='text-neutral-content text-sm'>
© {new Date().getFullYear()} Bilingify LLC. All rights reserved.
</p>
<p className='text-neutral-content mt-2 text-xs'>
This software is licensed under the{' '}
<a
href='https://www.gnu.org/licenses/agpl-3.0.html'
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 underline'
>
GNU Affero General Public License v3.0
</a>
. You are free to use, modify, and distribute this software under the terms of the
AGPL v3 license. Please see the license for more details.
</p>
<p className='text-neutral-content mt-2 text-xs'>
Source code is available at{' '}
<a
href='https://github.com/readest/readest'
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 underline'
>
GitHub
</a>
.
</p>
</div>
</div>
</form>
</dialog>
</Dialog>
</>
);
};
+1 -1
View File
@@ -13,7 +13,7 @@ const Alert: React.FC<{
<div
role='alert'
className={clsx(
'alert fixed bottom-4 left-1/2 flex -translate-x-1/2 transform items-center justify-between',
'alert fixed bottom-4 left-1/2 z-[100] flex -translate-x-1/2 transform items-center justify-between',
'rounded-lg bg-gray-100 p-4 shadow-2xl',
'w-full max-w-[90vw] sm:max-w-[70vw] md:max-w-[50vw] lg:max-w-[40vw] xl:max-w-[40vw]',
)}
@@ -2,7 +2,6 @@ import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import Image from 'next/image';
import Alert from '@/components/Alert';
import { Book } from '@/types/book';
import { BookDoc } from '@/libs/document';
import { useEnv } from '@/context/EnvContext';
@@ -10,8 +9,9 @@ import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useLibraryStore } from '@/store/libraryStore';
import { formatDate, formatLanguage, formatPublisher, formatSubject } from '@/utils/book';
import WindowButtons from '@/components/WindowButtons';
import Alert from '@/components/Alert';
import Spinner from './Spinner';
import Dialog from './Dialog';
interface BookDetailModalProps {
book: Book;
@@ -28,21 +28,6 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
const { settings } = useSettingsStore();
const { deleteBook } = useLibraryStore();
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen, onClose]);
useEffect(() => {
const loadingTimeout = setTimeout(() => setLoading(true), 300);
const fetchBookDetails = async () => {
@@ -63,7 +48,7 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
const handleDelete = () => {
setShowDeleteAlert(true);
}
};
const confirmDelete = () => {
deleteBook(envConfig, book);
@@ -71,8 +56,6 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
setShowDeleteAlert(false);
};
if (!isOpen) return null;
if (!bookMeta)
return (
loading && (
@@ -83,113 +66,132 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
);
return (
<div className='fixed inset-0 z-50 flex w-full select-text items-center justify-center'>
<div className='min-w-[480px] max-w-md'>
<div className='fixed inset-0 bg-gray-800 bg-opacity-70' onClick={handleClose} />
<>
<Dialog
title={_('Book Details')}
isOpen={isOpen}
onClose={handleClose}
boxClassName='sm:min-w-[480px]'
contentClassName='!p-6'
>
<div className='z-50 flex w-full select-text items-center justify-center'>
<div className='relative w-full rounded-lg'>
<div className='mb-10 flex h-40 items-start'>
<div className='book-cover relative mr-10 aspect-[28/41] h-40 items-end shadow-lg'>
<Image
src={book.coverImageUrl!}
alt={book.title}
fill={true}
className='w-10 object-cover'
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
(e.target as HTMLImageElement).nextElementSibling?.classList.remove(
'invisible',
);
}}
/>
<div
className={clsx(
'invisible absolute inset-0 flex items-center justify-center p-1',
'text-neutral-content rounded-none text-center font-serif text-base font-medium',
)}
>
{book.title}
</div>
</div>
<div className='bg-base-200 relative z-50 w-full rounded-lg p-6 shadow-xl'>
<div className='absolute right-4 top-4 flex space-x-2'>
<WindowButtons
className='window-buttons flex'
showMinimize={false}
showMaximize={false}
onClose={handleClose}
/>
</div>
<div className='mb-6 flex h-40 items-start'>
<div className='book-cover relative mr-10 aspect-[28/41] h-40 items-end shadow-lg'>
<Image
src={book.coverImageUrl!}
alt={book.title}
fill={true}
className='w-10 object-cover'
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
(e.target as HTMLImageElement).nextElementSibling?.classList.remove('invisible');
}}
/>
<div
className={clsx(
'invisible absolute inset-0 flex items-center justify-center p-1',
'text-neutral-content rounded-none text-center font-serif text-base font-medium',
<div className='title-author flex h-40 flex-col justify-between'>
<div>
<p className='text-base-content mb-2 line-clamp-2 break-all text-2xl font-bold'>
{book.title || _('Untitled')}
</p>
<p className='text-neutral-content line-clamp-1'>{book.author || _('Unknown')}</p>
</div>
{window.innerWidth >= 400 && (
<div className='flex flex-wrap items-center gap-x-4 gap-y-2 py-2'>
<button
className='btn rounded bg-red-600 text-white hover:bg-red-700'
onClick={handleDelete}
>
{_('Delete')}
</button>
<button className='btn btn-disabled bg-primary/25 hover:bg-primary/85 rounded px-4 text-white'>
{_('More Info')}
</button>
</div>
)}
>
{book.title}
</div>
</div>
<div className='title-author flex h-40 max-w-[60%] flex-col justify-between pr-4'>
<div>
<h2 className='text-base-content mb-2 line-clamp-2 break-all text-2xl font-bold'>
{book.title || _('Untitled')}
</h2>
<p className='text-neutral-content line-clamp-1'>{book.author || _('Unknown')}</p>
{window.innerWidth < 400 && (
<div className='flex flex-wrap items-center gap-x-4 gap-y-2 py-2'>
<button
className='btn rounded bg-red-600 text-white hover:bg-red-700'
onClick={handleDelete}
>
{_('Delete')}
</button>
<button className='btn btn-disabled bg-primary/25 hover:bg-primary/85 rounded px-4 text-white'>
{_('More Info')}
</button>
</div>
<button
className='w-36 rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700'
onClick={handleDelete}
>
{_('Delete Book')}
</button>
<button className='btn-disabled bg-primary/25 hover:bg-primary/85 w-36 rounded px-4 py-2 text-white'>
{_('More Info')}
</button>
</div>
</div>
)}
<div className='text-base-content mb-4'>
<div className='mb-4 grid grid-cols-3 gap-4'>
<div className='overflow-hidden'>
<span className='font-bold'>{_('Publisher:')}</span>
<p className='text-neutral-content line-clamp-1 text-sm'>
{formatPublisher(bookMeta.publisher || '') || _('Unknown')}
</p>
</div>
<div className='overflow-hidden'>
<span className='font-bold'>{_('Published:')}</span>
<p className='text-neutral-content max-w-28 text-ellipsis text-sm'>
{formatDate(bookMeta.published) || _('Unknown')}
</p>
</div>
<div className='overflow-hidden'>
<span className='font-bold'>{_('Updated:')}</span>
<p className='text-neutral-content text-sm'>{formatDate(book.lastUpdated) || ''}</p>
</div>
</div>
<div className='text-base-content my-4'>
<div className='mb-4 grid grid-cols-2 gap-4 sm:grid-cols-3'>
<div className='overflow-hidden'>
<span className='font-bold'>{_('Publisher:')}</span>
<p className='text-neutral-content line-clamp-1 text-sm'>
{formatPublisher(bookMeta.publisher || '') || _('Unknown')}
</p>
</div>
<div className='overflow-hidden'>
<span className='font-bold'>{_('Published:')}</span>
<p className='text-neutral-content max-w-28 text-ellipsis text-sm'>
{formatDate(bookMeta.published) || _('Unknown')}
</p>
</div>
<div className='overflow-hidden'>
<span className='font-bold'>{_('Updated:')}</span>
<p className='text-neutral-content text-sm'>
{formatDate(book.lastUpdated) || ''}
</p>
</div>
<div className='grid grid-cols-3 gap-4'>
<div className='overflow-hidden'>
<span className='font-bold'>{_('Language:')}</span>
<p className='text-neutral-content text-sm'>
{formatLanguage(bookMeta.language) || _('Unknown')}
</p>
</div>
<div className='overflow-hidden'>
<span className='font-bold'>{_('Identifier:')}</span>
<p className='text-neutral-content line-clamp-1 text-sm'>
{bookMeta.identifier || 'N/A'}
</p>
</div>
<div className='overflow-hidden'>
<span className='font-bold'>{_('Subjects:')}</span>
<p className='text-neutral-content line-clamp-1 text-sm'>
{formatSubject(bookMeta.subject) || _('Unknown')}
</p>
<div className='overflow-hidden'>
<span className='font-bold'>{_('Language:')}</span>
<p className='text-neutral-content text-sm'>
{formatLanguage(bookMeta.language) || _('Unknown')}
</p>
</div>
<div className='overflow-hidden'>
<span className='font-bold'>{_('Identifier:')}</span>
<p className='text-neutral-content line-clamp-1 text-sm'>
{bookMeta.identifier || 'N/A'}
</p>
</div>
<div className='overflow-hidden'>
<span className='font-bold'>{_('Subjects:')}</span>
<p className='text-neutral-content line-clamp-1 text-sm'>
{formatSubject(bookMeta.subject) || _('Unknown')}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</Dialog>
{showDeleteAlert && (
<Alert
title={_('Confirm Deletion')}
message={_('Are you sure to delete the selected books?')}
onClickCancel={() => { setShowDeleteAlert(false); handleClose(); }}
onClickCancel={() => {
setShowDeleteAlert(false);
}}
onClickConfirm={confirmDelete}
/>
)}
</div>
</>
);
};
+111
View File
@@ -0,0 +1,111 @@
import clsx from 'clsx';
import React, { ReactNode, useEffect } from 'react';
import { MdArrowBackIosNew } from 'react-icons/md';
interface DialogProps {
id?: string;
isOpen: boolean;
children: ReactNode;
header?: ReactNode;
title?: string;
className?: string;
boxClassName?: string;
contentClassName?: string;
onClose: () => void;
}
const Dialog: React.FC<DialogProps> = ({
id,
isOpen,
children,
header,
title,
className,
boxClassName,
contentClassName,
onClose,
}) => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<dialog
id={id ?? 'dialog'}
open={isOpen}
className={clsx(
'modal sm:min-w-90 z-50 h-full w-full !bg-[rgba(0,0,0,0.2)] sm:w-full',
className,
)}
>
<div
className={clsx(
'modal-box settings-content flex h-full max-h-full w-full max-w-full flex-col rounded-none p-0 sm:rounded-2xl',
'sm:h-[60%] sm:w-[65%] sm:max-w-[600px]',
boxClassName,
)}
>
<div className='dialog-header bg-base-100 sticky top-2 z-10 flex items-center justify-between px-4'>
{header ? (
header
) : (
<div className='flex h-11 w-full items-center justify-between'>
<button
tabIndex={-1}
onClick={onClose}
className={
'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent focus:outline-none sm:hidden'
}
>
<MdArrowBackIosNew size={20} />
</button>
<div className='z-15 pointer-events-none absolute inset-0 flex h-11 items-center justify-center'>
<span className='line-clamp-1 text-center font-bold'>{title ?? ''}</span>
</div>
<button
tabIndex={-1}
onClick={onClose}
className={
'bg-base-300/65 btn btn-ghost btn-circle ml-auto hidden h-6 min-h-6 w-6 focus:outline-none sm:flex'
}
>
<svg
xmlns='http://www.w3.org/2000/svg'
width='1em'
height='1em'
viewBox='0 0 24 24'
>
<path
fill='currentColor'
d='M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z'
/>
</svg>
</button>
</div>
)}
</div>
<div
className={clsx(
'text-base-content my-2 flex-grow overflow-y-auto px-[10%]',
contentClassName,
)}
>
{children}
</div>
</div>
</dialog>
);
};
export default Dialog;