From f880598ee51405c2c3eed8418cf46cfed640246d Mon Sep 17 00:00:00 2001 From: BookDock Dev Date: Sun, 14 Jun 2026 01:08:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/mobile/app.json | 2 +- apps/mobile/package.json | 4 +- apps/mobile/src/screens/LibraryScreen.tsx | 12 ----- apps/mobile/src/screens/ProfileScreen.tsx | 17 +------ apps/mobile/src/screens/ReaderScreen.tsx | 10 ---- apps/mobile/src/screens/TTSScreen.tsx | 62 ++++++++++++++++++++++- apps/mobile/src/stores/themeStore.ts | 4 +- pnpm-lock.yaml | 12 ++--- 8 files changed, 73 insertions(+), 50 deletions(-) diff --git a/apps/mobile/app.json b/apps/mobile/app.json index 879a3cb4..171b7eb4 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -2,7 +2,7 @@ "expo": { "name": "BookDock", "slug": "bookdock", - "version": "0.0.64", + "version": "0.0.65", "orientation": "default", "icon": "./assets/logo.png", "userInterfaceStyle": "automatic", diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 2f5b998d..f0848393 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@bookdock/mobile", - "version": "0.0.64", + "version": "0.0.65", "private": true, "main": "index.js", "scripts": { @@ -18,7 +18,7 @@ "@bookdock/tts": "workspace:*", "@bookdock/ui": "workspace:*", "@expo/metro-runtime": "~4.0.0", - "@expo/vector-icons": "^14.0.4", + "@expo/vector-icons": "^15.1.1", "@react-native-async-storage/async-storage": "1.23.1", "@react-native-community/netinfo": "^12.0.1", "@react-native-community/slider": "^4.5.5", diff --git a/apps/mobile/src/screens/LibraryScreen.tsx b/apps/mobile/src/screens/LibraryScreen.tsx index 48d963ad..e2ed9d57 100644 --- a/apps/mobile/src/screens/LibraryScreen.tsx +++ b/apps/mobile/src/screens/LibraryScreen.tsx @@ -169,18 +169,6 @@ export function LibraryScreen() { }, [navigation]); const handleTTSPress = useCallback(async (book: Book) => { - const token = await AsyncStorage.getItem('bookdock_plus_token'); - if (!token) { - navigation.navigate('MemberLogin'); - return; - } - - const vip = await useAuthStore.getState().refreshVipStatus(); - if (!vip) { - navigation.navigate('MemberBenefits'); - return; - } - navigation.navigate('TTSScreen', { book }); }, [navigation]); diff --git a/apps/mobile/src/screens/ProfileScreen.tsx b/apps/mobile/src/screens/ProfileScreen.tsx index 7723a1ce..ad6dd030 100644 --- a/apps/mobile/src/screens/ProfileScreen.tsx +++ b/apps/mobile/src/screens/ProfileScreen.tsx @@ -15,7 +15,7 @@ import { import { useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RootStackParamList } from '../navigation/types'; -import { Ionicons } from '@expo/vector-icons'; +import { Ionicons, AntDesign } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { useAuthStore, useLibraryStore, useThemeStore } from '../stores'; import { getTheme, spacing, fontSizes, borderRadius } from '../utils/theme'; @@ -175,24 +175,11 @@ export function ProfileScreen() { ), headerRight: () => ( - {progress > 0 && ( - { - setIsUpdateModalVisible(true); - }} - style={{ marginRight: 10, flexDirection: 'row', alignItems: 'center' }} - > - - - 更新 - - - )} navigation.navigate('ScanLogin')} > - + { - const token = await AsyncStorage.getItem('bookdock_plus_token'); - if (!token) { - navigation.navigate('MemberLogin'); - return; - } - const vip = await useAuthStore.getState().refreshVipStatus(); - if (!vip) { - navigation.navigate('MemberBenefits'); - return; - } navigation.navigate('TTSScreen', { book }); }, [navigation, book]); diff --git a/apps/mobile/src/screens/TTSScreen.tsx b/apps/mobile/src/screens/TTSScreen.tsx index 2f87c6f0..5e4e1f04 100644 --- a/apps/mobile/src/screens/TTSScreen.tsx +++ b/apps/mobile/src/screens/TTSScreen.tsx @@ -428,6 +428,43 @@ export function TTSScreen() { return () => sub.remove(); }, [currentParagraph, paragraphs.length, chapterIndex, voiceId, provider, ttsStore]); + // ── TrackPlayer event: track changed (user clicked next/prev in system UI) ── + useEffect(() => { + const sub = TrackPlayer.addEventListener(Event.PlaybackTrackChanged, async (event) => { + // event.nextTrack is the new track index + // When user clicks "next" in system UI, we need to update currentParagraph + if (event.nextTrack !== undefined && event.nextTrack !== null) { + // The track index corresponds to the paragraph index within the queue + // Our queue has: [currentParagraph chunks..., nextParagraph chunks...] + // So we need to map track index back to paragraph index + const trackIndex = event.nextTrack; + // Count how many tracks belong to current paragraph + const currentParaTrackCount = await TrackPlayer.getQueue().then(q => { + let count = 0; + for (let i = 0; i < q.length; i++) { + if (q[i].id.startsWith(`${paragraphs[currentParagraph]?.id}`)) { + count++; + } else { + break; + } + } + return count; + }).catch(() => 1); + + if (trackIndex >= currentParaTrackCount) { + // User moved to next paragraph + const nextParagraphIdx = currentParagraph + 1; + if (nextParagraphIdx < paragraphs.length) { + ttsStore.setCurrentParagraph(nextParagraphIdx); + // Pre-load next next paragraph for continuous playback + prefetchParagraph(nextParagraphIdx + 1); + } + } + } + }); + return () => sub.remove(); + }, [currentParagraph, paragraphs, ttsStore]); + // Auto-scroll to current paragraph when it changes useEffect(() => { @@ -648,7 +685,7 @@ export function TTSScreen() { const uris = await synthesizeParagraph(idx); if (uris.length === 0) return; - // Build tracks for RNTP + // Build tracks for RNTP - current paragraph + next paragraph (if available) const coverUri = getCoverImageUrl(book.coverUrl); const tracks = uris.map((uri, i) => ({ id: `${paragraphs[idx].id}-${i}`, @@ -659,8 +696,29 @@ export function TTSScreen() { duration: 0, })); + // Pre-synthesize next paragraph and add to queue if available + const nextIdx = idx + 1; + let nextTracks: any[] = []; + if (nextIdx < paragraphs.length) { + try { + const nextUris = await synthesizeParagraph(nextIdx); + if (nextUris.length > 0) { + nextTracks = nextUris.map((uri, i) => ({ + id: `${paragraphs[nextIdx].id}-${i}`, + url: uri, + title: `${book.title} - ${chapterTitle}`, + artist: book.author || "未知作者", + artwork: coverUri, + duration: 0, + })); + } + } catch (e) { + console.warn('[TTSScreen] Pre-synthesize next paragraph failed:', e); + } + } + await TrackPlayer.reset(); - await TrackPlayer.add(tracks); + await TrackPlayer.add([...tracks, ...nextTracks]); if (startOffsetMs > 0) { await TrackPlayer.seekTo(startOffsetMs / 1000); } diff --git a/apps/mobile/src/stores/themeStore.ts b/apps/mobile/src/stores/themeStore.ts index 782b5b64..173eadb1 100644 --- a/apps/mobile/src/stores/themeStore.ts +++ b/apps/mobile/src/stores/themeStore.ts @@ -1,6 +1,7 @@ import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import AsyncStorage from '@react-native-async-storage/async-storage'; +import { Appearance } from 'react-native'; export type AppTheme = 'light' | 'dark' | 'system'; @@ -13,8 +14,7 @@ interface ThemeState { } function getSystemTheme(): 'light' | 'dark' { - // In React Native, we use a listener for system theme changes - return 'light'; // Default, will be updated by listener + return Appearance.getColorScheme() === 'dark' ? 'dark' : 'light'; } function getEffectiveTheme(theme: AppTheme, systemTheme: 'light' | 'dark'): 'light' | 'dark' { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ae5c39c..b8406119 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -178,8 +178,8 @@ importers: specifier: ~4.0.0 version: 4.0.1(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1)) '@expo/vector-icons': - specifier: ^14.0.4 - version: 14.1.0(expo-font@13.0.4(expo@52.0.49(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + specifier: ^15.1.1 + version: 15.1.1(expo-font@13.0.4(expo@52.0.49(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) '@react-native-async-storage/async-storage': specifier: 1.23.1 version: 1.23.1(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1)) @@ -1643,10 +1643,10 @@ packages: '@expo/vector-icons@14.0.4': resolution: {integrity: sha512-+yKshcbpDfbV4zoXOgHxCwh7lkE9VVTT5T03OUlBsqfze1PLy6Hi4jp1vSb1GVbY6eskvMIivGVc9SKzIv0oEQ==} - '@expo/vector-icons@14.1.0': - resolution: {integrity: sha512-7T09UE9h8QDTsUeMGymB4i+iqvtEeaO5VvUjryFB4tugDTG/bkzViWA74hm5pfjjDEhYMXWaX112mcvhccmIwQ==} + '@expo/vector-icons@15.1.1': + resolution: {integrity: sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw==} peerDependencies: - expo-font: '*' + expo-font: '>=14.0.4' react: '*' react-native: '*' @@ -9588,7 +9588,7 @@ snapshots: dependencies: prop-types: 15.8.1 - '@expo/vector-icons@14.1.0(expo-font@13.0.4(expo@52.0.49(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)': + '@expo/vector-icons@15.1.1(expo-font@13.0.4(expo@52.0.49(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)': dependencies: expo-font: 13.0.4(expo@52.0.49(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@expo/metro-runtime@4.0.1(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1)))(encoding@0.1.13)(react-native-webview@13.12.5(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.9(@babel/core@7.29.0)(@babel/preset-env@7.29.2(@babel/core@7.29.0))(@types/react@18.3.28)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1) react: 18.3.1