forked from akai/readest
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4005858242 | |||
| c24f951375 | |||
| dbe1efd1e9 | |||
| 0299cce343 | |||
| ca9f504329 | |||
| df3527ceca | |||
| 73d0838291 | |||
| 8e3574509e | |||
| 9bdf375304 | |||
| effa922597 | |||
| fe484257a0 | |||
| cc0cd9a114 | |||
| 916d63806e | |||
| 59d0118067 |
@@ -56,4 +56,4 @@ jobs:
|
||||
- name: build the web App
|
||||
working-directory: apps/readest-app
|
||||
run: |
|
||||
pnpm build-web
|
||||
pnpm build-check
|
||||
|
||||
@@ -4,3 +4,6 @@
|
||||
[submodule "packages/tauri"]
|
||||
path = packages/tauri
|
||||
url = https://github.com/readest/tauri.git
|
||||
[submodule "packages/tauri-plugins"]
|
||||
path = packages/tauri-plugins
|
||||
url = https://github.com/readest/tauri-plugins-workspace.git
|
||||
|
||||
Generated
+344
-189
File diff suppressed because it is too large
Load Diff
+15
-1
@@ -2,9 +2,22 @@
|
||||
members = [
|
||||
"apps/readest-app/src-tauri",
|
||||
"packages/tauri/crates/tauri",
|
||||
"packages/tauri-plugins/plugins/deep-link",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
log = "0.4"
|
||||
tauri = { version = "2.6", default-features = false }
|
||||
tauri-build = "2.3"
|
||||
tauri-plugin = "2.3"
|
||||
tauri-utils = "2.5"
|
||||
serde_json = "1"
|
||||
thiserror = "2"
|
||||
url = "2"
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Bilingify LLC"]
|
||||
homepage = "https://readest.com"
|
||||
@@ -15,4 +28,5 @@ edition = "2021"
|
||||
rust-version = "1.77.2"
|
||||
|
||||
[patch.crates-io]
|
||||
tauri = { path = "packages/tauri/crates/tauri" }
|
||||
tauri = { path = "packages/tauri/crates/tauri" }
|
||||
tauri-plugin-deep-link = { path = "packages/tauri-plugins/plugins/deep-link" }
|
||||
@@ -19,6 +19,7 @@
|
||||
<br>
|
||||
[![Last commit][badge-last-commit]][link-gh-commits]
|
||||
[![Commits][badge-commit-activity]][link-gh-pulse]
|
||||
[![Ask DeepWiki][badge-deepwiki]][link-deepwiki]
|
||||
|
||||
</div>
|
||||
|
||||
@@ -165,6 +166,8 @@ For Windows targets, “Build Tools for Visual Studio 2022” (or a higher editi
|
||||
pnpm tauri dev
|
||||
# or start development for the Web app
|
||||
pnpm dev-web
|
||||
# preview with OpenNext build for the Web app
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
For Android:
|
||||
@@ -304,6 +307,7 @@ The following fonts are utilized in this software, either bundled within the app
|
||||
[badge-discord]: https://img.shields.io/discord/1314226120886976544?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square
|
||||
[badge-hellogithub]: https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=8a5b6ade2aee461a8bd94e59200682a7&claim_uid=eRLUbPOy2qZtDgw&theme=small
|
||||
[badge-donate]: https://donate.readest.com/badge.svg
|
||||
[badge-deepwiki]: https://deepwiki.com/badge.svg
|
||||
[link-donate]: https://donate.readest.com/?tickers=btc%2Ceth%2Csol%2Cusdc
|
||||
[link-appstore]: https://apps.apple.com/app/apple-store/id6738622779?pt=127463130&ct=github&mt=8
|
||||
[link-website]: https://readest.com?utm_source=github&utm_medium=referral&utm_campaign=readme
|
||||
@@ -316,3 +320,4 @@ The following fonts are utilized in this software, either bundled within the app
|
||||
[link-parallel-read]: https://readest.com/#parallel-read
|
||||
[link-koreader]: https://github.com/koreader/koreader
|
||||
[link-hellogithub]: https://hellogithub.com/repository/8a5b6ade2aee461a8bd94e59200682a7
|
||||
[link-deepwiki]: https://deepwiki.com/readest/readest
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import withPWAInit from '@ducanh2912/next-pwa';
|
||||
import { initOpenNextCloudflareForDev } from '@opennextjs/cloudflare';
|
||||
import withBundleAnalyzer from '@next/bundle-analyzer';
|
||||
|
||||
const isDev = process.env['NODE_ENV'] === 'development';
|
||||
const appPlatform = process.env['NEXT_PUBLIC_APP_PLATFORM'];
|
||||
|
||||
if (isDev) {
|
||||
const { initOpenNextCloudflareForDev } = await import('@opennextjs/cloudflare');
|
||||
initOpenNextCloudflareForDev();
|
||||
}
|
||||
|
||||
@@ -25,6 +26,19 @@ const nextConfig = {
|
||||
// Configure assetPrefix or else the server won't properly resolve your assets.
|
||||
assetPrefix: '',
|
||||
reactStrictMode: true,
|
||||
serverExternalPackages: ['isows'],
|
||||
transpilePackages: !isDev
|
||||
? [
|
||||
'i18next-browser-languagedetector',
|
||||
'react-i18next',
|
||||
'i18next',
|
||||
'@ducanh2912/next-pwa',
|
||||
'@tauri-apps',
|
||||
'highlight.js',
|
||||
'foliate-js',
|
||||
'marked',
|
||||
]
|
||||
: [],
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
@@ -55,4 +69,8 @@ const withPWA = withPWAInit({
|
||||
},
|
||||
});
|
||||
|
||||
export default withPWA(nextConfig);
|
||||
const withAnalyzer = withBundleAnalyzer({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
});
|
||||
|
||||
export default withPWA(withAnalyzer(nextConfig));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.65",
|
||||
"version": "0.9.66",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "dotenv -e .env.tauri -- next dev",
|
||||
@@ -35,7 +35,10 @@
|
||||
"preview": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
||||
"deploy": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
||||
"upload": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && opennextjs-cloudflare upload",
|
||||
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
||||
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts",
|
||||
"check:optional-chaining": "count=$(grep -rno '\\?\\.[a-zA-Z_$]' .next/static/chunks/* out/_next/static/chunks/* | wc -l); if [ \"$count\" -gt 0 ]; then echo '❌ Optional chaining found in output!'; exit 1; else echo '✅ No optional chaining found.'; fi",
|
||||
"check:all": "pnpm check:optional-chaining",
|
||||
"build-check": "pnpm build && pnpm build-web && pnpm check:all"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.735.0",
|
||||
@@ -49,18 +52,18 @@
|
||||
"@supabase/auth-ui-shared": "^0.1.8",
|
||||
"@supabase/supabase-js": "^2.50.2",
|
||||
"@tauri-apps/api": "2.6.0",
|
||||
"@tauri-apps/plugin-cli": "^2.3.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2.3.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.2",
|
||||
"@tauri-apps/plugin-fs": "^2.3.0",
|
||||
"@tauri-apps/plugin-haptics": "^2.2.5",
|
||||
"@tauri-apps/plugin-http": "^2.4.4",
|
||||
"@tauri-apps/plugin-log": "^2.5.1",
|
||||
"@tauri-apps/plugin-opener": "^2.3.1",
|
||||
"@tauri-apps/plugin-os": "^2.2.2",
|
||||
"@tauri-apps/plugin-process": "^2.2.2",
|
||||
"@tauri-apps/plugin-shell": "~2.2.2",
|
||||
"@tauri-apps/plugin-updater": "^2.8.1",
|
||||
"@tauri-apps/plugin-cli": "^2.4.0",
|
||||
"@tauri-apps/plugin-deep-link": "^2.4.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.3.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.0",
|
||||
"@tauri-apps/plugin-haptics": "^2.3.0",
|
||||
"@tauri-apps/plugin-http": "^2.5.0",
|
||||
"@tauri-apps/plugin-log": "^2.6.0",
|
||||
"@tauri-apps/plugin-opener": "^2.4.0",
|
||||
"@tauri-apps/plugin-os": "^2.3.0",
|
||||
"@tauri-apps/plugin-process": "^2.3.0",
|
||||
"@tauri-apps/plugin-shell": "~2.3.0",
|
||||
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||
"@zip.js/zip.js": "^2.7.53",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -90,11 +93,12 @@
|
||||
"semver": "^7.7.1",
|
||||
"stripe": "^18.2.1",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"zustand": "5.0.1"
|
||||
"zustand": "5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "^15.4.2",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tauri-apps/cli": "2.6.0",
|
||||
"@tauri-apps/cli": "2.6.2",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/cssbeautify": "^0.3.5",
|
||||
"@types/node": "^22.10.1",
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.9.66": {
|
||||
"date": "2025-07-21",
|
||||
"notes": [
|
||||
"Layout: Fixed nested table of contents items not expanding in long TOC lists",
|
||||
"Reader: Opening files from the OS now takes highest priority",
|
||||
"TTS: Improved voice filtering for some language codes",
|
||||
"App: Fixed loading issues on older browsers",
|
||||
"App: Fixed wrong MIME types generated on Linux"
|
||||
]
|
||||
},
|
||||
"0.9.65": {
|
||||
"date": "2025-07-19",
|
||||
"notes": [
|
||||
|
||||
@@ -252,12 +252,24 @@ pub fn run() {
|
||||
.title("");
|
||||
|
||||
#[cfg(all(not(target_os = "macos"), desktop))]
|
||||
let win_builder = win_builder
|
||||
.decorations(false)
|
||||
.transparent(true)
|
||||
.visible(false)
|
||||
.shadow(true)
|
||||
.title("Readest");
|
||||
let win_builder = {
|
||||
let mut builder = win_builder
|
||||
.decorations(false)
|
||||
.visible(false)
|
||||
.shadow(true)
|
||||
.title("Readest");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
builder = builder.transparent(false);
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
builder = builder.transparent(true);
|
||||
}
|
||||
|
||||
builder
|
||||
};
|
||||
|
||||
win_builder.build().unwrap();
|
||||
// let win = win_builder.build().unwrap();
|
||||
|
||||
@@ -334,66 +334,82 @@ export default function AuthPage() {
|
||||
return isTauriAppPlatform() ? (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-100 fixed inset-0 flex select-none flex-col items-center overflow-hidden overflow-y-auto',
|
||||
'bg-base-100 inset-0 flex select-none flex-col items-center overflow-hidden',
|
||||
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
|
||||
appService?.isLinuxApp && 'window-border',
|
||||
appService?.hasRoundedWindow && 'rounded-window',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={headerRef}
|
||||
className={clsx(
|
||||
'fixed z-10 flex w-full items-center justify-between py-2 pe-6 ps-4',
|
||||
appService?.hasTrafficLight && 'pt-11',
|
||||
'flex h-full w-full flex-col items-center overflow-y-auto',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
|
||||
)}
|
||||
>
|
||||
<button onClick={handleGoBack} className={clsx('btn btn-ghost h-8 min-h-8 w-8 p-0')}>
|
||||
<IoArrowBack className='text-base-content' />
|
||||
</button>
|
||||
<div
|
||||
ref={headerRef}
|
||||
className={clsx(
|
||||
'fixed z-10 flex w-full items-center justify-between py-2 pe-6 ps-4',
|
||||
appService?.hasTrafficLight && 'pt-11',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
onClick={handleGoBack}
|
||||
className={clsx('btn btn-ghost h-12 min-h-12 w-12 p-0 sm:h-8 sm:min-h-8 sm:w-8')}
|
||||
>
|
||||
<IoArrowBack className='text-base-content' />
|
||||
</button>
|
||||
|
||||
{appService?.hasWindowBar && (
|
||||
<WindowButtons
|
||||
headerRef={headerRef}
|
||||
showMinimize={!isTrafficLightVisible}
|
||||
showMaximize={!isTrafficLightVisible}
|
||||
showClose={!isTrafficLightVisible}
|
||||
onClose={handleGoBack}
|
||||
{appService?.hasWindowBar && (
|
||||
<WindowButtons
|
||||
headerRef={headerRef}
|
||||
showMinimize={!isTrafficLightVisible}
|
||||
showMaximize={!isTrafficLightVisible}
|
||||
showClose={!isTrafficLightVisible}
|
||||
onClose={handleGoBack}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'z-20 flex flex-col items-center pb-8',
|
||||
appService?.hasTrafficLight ? 'mt-24' : 'mt-12',
|
||||
)}
|
||||
style={{ maxWidth: '420px' }}
|
||||
>
|
||||
<ProviderLogin
|
||||
provider='google'
|
||||
handleSignIn={tauriSignIn}
|
||||
Icon={FcGoogle}
|
||||
label={_('Sign in with Google')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={clsx('z-20 pb-8', appService?.hasTrafficLight ? 'mt-24' : 'mt-12')}
|
||||
style={{ maxWidth: '420px' }}
|
||||
>
|
||||
<ProviderLogin
|
||||
provider='google'
|
||||
handleSignIn={tauriSignIn}
|
||||
Icon={FcGoogle}
|
||||
label={_('Sign in with Google')}
|
||||
/>
|
||||
<ProviderLogin
|
||||
provider='apple'
|
||||
handleSignIn={appService?.isIOSApp || USE_APPLE_SIGN_IN ? tauriSignInApple : tauriSignIn}
|
||||
Icon={FaApple}
|
||||
label={_('Sign in with Apple')}
|
||||
/>
|
||||
<ProviderLogin
|
||||
provider='github'
|
||||
handleSignIn={tauriSignIn}
|
||||
Icon={FaGithub}
|
||||
label={_('Sign in with GitHub')}
|
||||
/>
|
||||
<hr className='border-base-300 my-3 mt-6 w-64 border-t' />
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
appearance={{ theme: ThemeSupa }}
|
||||
theme={isDarkMode ? 'dark' : 'light'}
|
||||
magicLink={true}
|
||||
providers={[]}
|
||||
redirectTo={getTauriRedirectTo(false)}
|
||||
localization={getAuthLocalization()}
|
||||
/>
|
||||
<ProviderLogin
|
||||
provider='apple'
|
||||
handleSignIn={
|
||||
appService?.isIOSApp || USE_APPLE_SIGN_IN ? tauriSignInApple : tauriSignIn
|
||||
}
|
||||
Icon={FaApple}
|
||||
label={_('Sign in with Apple')}
|
||||
/>
|
||||
<ProviderLogin
|
||||
provider='github'
|
||||
handleSignIn={tauriSignIn}
|
||||
Icon={FaGithub}
|
||||
label={_('Sign in with GitHub')}
|
||||
/>
|
||||
<hr className='border-base-300 my-3 mt-6 w-64 border-t' />
|
||||
<div className='w-full'>
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
appearance={{ theme: ThemeSupa }}
|
||||
theme={isDarkMode ? 'dark' : 'light'}
|
||||
magicLink={true}
|
||||
providers={[]}
|
||||
redirectTo={getTauriRedirectTo(false)}
|
||||
localization={getAuthLocalization()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { parseWebViewVersion } from '@/utils/ua';
|
||||
import { handleGlobalError } from '@/utils/error';
|
||||
|
||||
interface ErrorPageProps {
|
||||
error: Error & { digest?: string };
|
||||
@@ -19,6 +20,7 @@ export default function Error({ error, reset }: ErrorPageProps) {
|
||||
useEffect(() => {
|
||||
setBrowserInfo(parseWebViewVersion(appService));
|
||||
posthog.captureException(error);
|
||||
handleGlobalError(error);
|
||||
}, [appService, error]);
|
||||
|
||||
const handleGoHome = () => {
|
||||
|
||||
@@ -344,12 +344,15 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
|
||||
// Reuse the library from the store when we return from the reader
|
||||
const library = libraryBooks.length > 0 ? libraryBooks : await appService.loadLibraryBooks();
|
||||
setCheckOpenWithBooks(checkOpenWithBooks && (await handleOpenWithBooks(appService, library)));
|
||||
setCheckLastOpenBooks(
|
||||
checkLastOpenBooks &&
|
||||
settings.openLastBooks &&
|
||||
(await handleOpenLastBooks(appService, settings.lastOpenBooks, library)),
|
||||
);
|
||||
let opened = false;
|
||||
if (checkOpenWithBooks) {
|
||||
opened = await handleOpenWithBooks(appService, library);
|
||||
}
|
||||
setCheckOpenWithBooks(opened);
|
||||
if (!opened && checkLastOpenBooks && settings.openLastBooks) {
|
||||
opened = await handleOpenLastBooks(appService, settings.lastOpenBooks, library);
|
||||
}
|
||||
setCheckLastOpenBooks(opened);
|
||||
|
||||
setLibrary(library);
|
||||
setLibraryLoaded(true);
|
||||
@@ -508,9 +511,9 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
}
|
||||
};
|
||||
|
||||
const handleBookDownload = async (book: Book) => {
|
||||
const handleBookDownload = async (book: Book, redownload = false) => {
|
||||
try {
|
||||
await appService?.downloadBook(book, false, (progress) => {
|
||||
await appService?.downloadBook(book, false, redownload, (progress) => {
|
||||
updateBookTransferProgress(book.hash, progress);
|
||||
});
|
||||
await updateBook(envConfig, book);
|
||||
|
||||
@@ -117,7 +117,7 @@ const TOCView: React.FC<{
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [expandedItems]);
|
||||
|
||||
const activeHref = useMemo(() => progress?.sectionHref || null, [progress?.sectionHref]);
|
||||
const flatItems = useFlattenedTOC(toc, expandedItems);
|
||||
@@ -213,7 +213,11 @@ const TOCView: React.FC<{
|
||||
}, [flatItems, scrollToActiveItem]);
|
||||
|
||||
return flatItems.length > 256 ? (
|
||||
<div className='rounded' data-overlayscrollbars-initialize='' ref={containerRef}>
|
||||
<div
|
||||
className='virtual-list rounded pt-2'
|
||||
data-overlayscrollbars-initialize=''
|
||||
ref={containerRef}
|
||||
>
|
||||
<VirtualList
|
||||
ref={vitualListRef}
|
||||
outerRef={listOuterRef}
|
||||
@@ -233,7 +237,7 @@ const TOCView: React.FC<{
|
||||
</VirtualList>
|
||||
</div>
|
||||
) : (
|
||||
<div className='rounded pt-2' ref={staticListRef}>
|
||||
<div className='static-list rounded pt-2' ref={staticListRef}>
|
||||
{flatItems.map((flatItem, index) => (
|
||||
<StaticListRow
|
||||
key={`static-row-${index}`}
|
||||
|
||||
@@ -330,7 +330,7 @@ const TTSPanel = ({
|
||||
</span>
|
||||
<span
|
||||
className={clsx(
|
||||
'text-base sm:text-sm',
|
||||
'max-w-[180px] overflow-hidden text-ellipsis text-base sm:text-sm',
|
||||
voice.disabled && 'text-gray-400',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -22,7 +22,10 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({ onGoBack }) => {
|
||||
appService?.hasTrafficLight && 'pt-11',
|
||||
)}
|
||||
>
|
||||
<button onClick={onGoBack} className={clsx('btn btn-ghost h-8 min-h-8 w-8 p-0')}>
|
||||
<button
|
||||
onClick={onGoBack}
|
||||
className={clsx('btn btn-ghost h-12 min-h-12 w-12 p-0 sm:h-8 sm:min-h-8 sm:w-8')}
|
||||
>
|
||||
<IoArrowBack className='text-base-content' />
|
||||
</button>
|
||||
|
||||
|
||||
@@ -44,13 +44,16 @@ const PlansComparison: React.FC<PlansComparisonProps> = ({
|
||||
};
|
||||
|
||||
const handleTouchStart = (e: React.TouchEvent) => {
|
||||
const touchStart = e.touches[0]!.clientX;
|
||||
const touchStartX = e.touches[0]!.clientX;
|
||||
const touchStartY = e.touches[0]!.clientY;
|
||||
const handleTouchMove = (moveEvent: TouchEvent) => {
|
||||
const touchEnd = moveEvent.touches[0]!.clientX;
|
||||
const diff = touchStart - touchEnd;
|
||||
const touchEndX = moveEvent.touches[0]!.clientX;
|
||||
const touchEndY = moveEvent.touches[0]!.clientY;
|
||||
const diffX = touchStartX - touchEndX;
|
||||
const diffY = touchStartY - touchEndY;
|
||||
|
||||
if (Math.abs(diff) > 50) {
|
||||
if (diff > 0) {
|
||||
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
|
||||
if (diffX > 0) {
|
||||
handlePlanSwipe('left');
|
||||
} else {
|
||||
handlePlanSwipe('right');
|
||||
|
||||
@@ -191,7 +191,7 @@ const ProfilePage = () => {
|
||||
useEffect(() => {
|
||||
fetch(WEB_STRIPE_PLANS_URL)
|
||||
.then((res) => res.json())
|
||||
.then((data) => setAvailablePlans(data));
|
||||
.then((data) => setAvailablePlans(data || []));
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
@@ -219,14 +219,18 @@ const ProfilePage = () => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-100 fixed inset-0 select-none overflow-hidden',
|
||||
'bg-base-100 inset-0 select-none overflow-hidden',
|
||||
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
|
||||
appService?.isLinuxApp && 'window-border',
|
||||
appService?.hasRoundedWindow && 'rounded-window',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
|
||||
)}
|
||||
>
|
||||
<div className='flex h-full w-full flex-col items-center overflow-y-auto'>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex h-full w-full flex-col items-center overflow-y-auto',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
|
||||
)}
|
||||
>
|
||||
<ProfileHeader onGoBack={handleGoBack} />
|
||||
<div className='w-full min-w-60 max-w-4xl py-10'>
|
||||
{loading && (
|
||||
|
||||
@@ -364,7 +364,8 @@ const BookDetailEdit: React.FC<BookDetailEditProps> = ({
|
||||
</button>
|
||||
<button
|
||||
onClick={onSave}
|
||||
className='rounded-md bg-green-500 px-4 py-2 text-white hover:bg-green-600'
|
||||
disabled={fieldErrors && Object.keys(fieldErrors).length > 0}
|
||||
className='rounded-md bg-green-500 px-4 py-2 text-white hover:bg-green-600 disabled:opacity-50'
|
||||
>
|
||||
{_('Save')}
|
||||
</button>
|
||||
|
||||
@@ -18,7 +18,7 @@ interface BookDetailModalProps {
|
||||
book: Book;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
handleBookDownload?: (book: Book) => void;
|
||||
handleBookDownload?: (book: Book, redownload?: boolean) => void;
|
||||
handleBookUpload?: (book: Book) => void;
|
||||
handleBookDelete?: (book: Book) => void;
|
||||
handleBookDeleteCloudBackup?: (book: Book) => void;
|
||||
@@ -132,7 +132,7 @@ const BookDetailModal: React.FC<BookDetailModalProps> = ({
|
||||
const handleRedownload = async () => {
|
||||
handleClose();
|
||||
if (handleBookDownload) {
|
||||
handleBookDownload(book);
|
||||
handleBookDownload(book, true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -45,5 +45,5 @@ export function middleware(request: NextRequest) {
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/api/stripe/:path*', '/api/metadata/:path*'],
|
||||
matcher: ['/api/:path*', '/api/stripe/:path*', '/api/metadata/:path*'],
|
||||
};
|
||||
|
||||
@@ -381,13 +381,19 @@ export abstract class BaseAppService implements AppService {
|
||||
}
|
||||
}
|
||||
|
||||
async downloadBook(book: Book, onlyCover = false, onProgress?: ProgressHandler): Promise<void> {
|
||||
async downloadBook(
|
||||
book: Book,
|
||||
onlyCover = false,
|
||||
redownload = false,
|
||||
onProgress?: ProgressHandler,
|
||||
): Promise<void> {
|
||||
let bookDownloaded = false;
|
||||
let bookCoverDownloaded = false;
|
||||
const completedFiles = { count: 0 };
|
||||
let toDownloadFpCount = 0;
|
||||
const needDownCover = !(await this.fs.exists(getCoverFilename(book), 'Books'));
|
||||
const needDownBook = !onlyCover && !(await this.fs.exists(getLocalBookFilename(book), 'Books'));
|
||||
const needDownCover = !(await this.fs.exists(getCoverFilename(book), 'Books')) || redownload;
|
||||
const needDownBook =
|
||||
(!onlyCover && !(await this.fs.exists(getLocalBookFilename(book), 'Books'))) || redownload;
|
||||
if (needDownCover) {
|
||||
toDownloadFpCount++;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,12 @@ export interface AppService {
|
||||
): Promise<Book | null>;
|
||||
deleteBook(book: Book, includingUploaded?: boolean, includingLocal?: boolean): Promise<void>;
|
||||
uploadBook(book: Book, onProgress?: ProgressHandler): Promise<void>;
|
||||
downloadBook(book: Book, onlyCover?: boolean, onProgress?: ProgressHandler): Promise<void>;
|
||||
downloadBook(
|
||||
book: Book,
|
||||
onlyCover?: boolean,
|
||||
redownload?: boolean,
|
||||
onProgress?: ProgressHandler,
|
||||
): Promise<void>;
|
||||
isBookAvailable(book: Book): Promise<boolean>;
|
||||
getBookFileSize(book: Book): Promise<number | null>;
|
||||
loadBookConfig(book: Book, settings: SystemSettings): Promise<BookConfig>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { EXTS } from '@/libs/document';
|
||||
import { Book, BookConfig, BookProgress, WritingMode } from '@/types/book';
|
||||
import { getUserLang, isContentURI, isFileURI, isValidURL, makeSafeFilename } from './misc';
|
||||
import { getStorageType } from './object';
|
||||
import { getStorageType } from './storage';
|
||||
import { getDirFromLanguage } from './rtl';
|
||||
import { SUPPORTED_LANGS } from '@/services/constants';
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export const handleGlobalError = (e: Error) => {
|
||||
const isChunkError = e?.message?.includes('Loading chunk');
|
||||
|
||||
if (!isChunkError) {
|
||||
const now = Date.now();
|
||||
const lastReload = Number(sessionStorage.getItem('lastErrorReload') || '0');
|
||||
if (now - lastReload > 60_000) {
|
||||
sessionStorage.setItem('lastErrorReload', String(now));
|
||||
window.location.reload();
|
||||
} else {
|
||||
console.warn('Error detected, but reload suppressed (rate limit)');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,21 +1,8 @@
|
||||
import { s3Storage } from './s3';
|
||||
import { r2Storage } from './r2';
|
||||
import { getStorageType } from './storage';
|
||||
|
||||
type ObjectStorageType = 'r2' | 's3';
|
||||
|
||||
export const getStorageType = (): ObjectStorageType => {
|
||||
// TODO: do not expose storage type to client
|
||||
if (process.env['NEXT_PUBLIC_OBJECT_STORAGE_TYPE']) {
|
||||
return process.env['NEXT_PUBLIC_OBJECT_STORAGE_TYPE'] as ObjectStorageType;
|
||||
} else {
|
||||
return 'r2';
|
||||
}
|
||||
};
|
||||
|
||||
export const getDownloadSignedUrl = async (
|
||||
fileKey: string,
|
||||
expiresIn: number,
|
||||
) => {
|
||||
export const getDownloadSignedUrl = async (fileKey: string, expiresIn: number) => {
|
||||
const storageType = getStorageType();
|
||||
if (storageType === 'r2') {
|
||||
const bucketName = process.env['R2_BUCKET_NAME'] || '';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TTSMark } from '@/services/tts/types';
|
||||
import { inferLangFromScript, isSameLang, isValidLang } from './lang';
|
||||
import { code6392to6391, inferLangFromScript, isSameLang, isValidLang } from './lang';
|
||||
|
||||
const cleanTextContent = (text: string) =>
|
||||
text.replace(/\r\n/g, ' ').replace(/\r/g, ' ').replace(/\n/g, ' ').trimStart();
|
||||
@@ -27,10 +27,12 @@ export const parseSSMLLang = (ssml: string, primaryLang?: string): string => {
|
||||
? `${parts[0]!.toLowerCase()}-${parts[1]!.toUpperCase()}`
|
||||
: parts[0]!.toLowerCase();
|
||||
|
||||
lang = code6392to6391(lang) || lang;
|
||||
if (!isValidLang(lang)) {
|
||||
lang = 'en';
|
||||
}
|
||||
}
|
||||
primaryLang = code6392to6391(primaryLang?.toLowerCase() || '') || primaryLang;
|
||||
if (lang === 'en' && primaryLang && !isSameLang(lang, primaryLang)) {
|
||||
lang = primaryLang.split('-')[0]!.toLowerCase();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
type ObjectStorageType = 'r2' | 's3';
|
||||
|
||||
export const getStorageType = (): ObjectStorageType => {
|
||||
// TODO: do not expose storage type to client
|
||||
if (process.env['NEXT_PUBLIC_OBJECT_STORAGE_TYPE']) {
|
||||
return process.env['NEXT_PUBLIC_OBJECT_STORAGE_TYPE'] as ObjectStorageType;
|
||||
} else {
|
||||
return 'r2';
|
||||
}
|
||||
};
|
||||
+1
-1
Submodule packages/tauri updated: 8e592e7ad5...8f1fcc1479
Submodule
+1
Submodule packages/tauri-plugins added at f1202ecfff
Generated
+393
-277
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user