Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/mobile/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bookdock/mobile",
"version": "0.0.64",
"version": "0.0.65",
"private": true,
"main": "index.js",
"scripts": {
Expand All @@ -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",
Expand Down
12 changes: 0 additions & 12 deletions apps/mobile/src/screens/LibraryScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]);

Expand Down
17 changes: 2 additions & 15 deletions apps/mobile/src/screens/ProfileScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -175,24 +175,11 @@ export function ProfileScreen() {
),
headerRight: () => (
<View style={{ flexDirection: 'row', alignItems: 'center', marginRight: 16 }}>
{progress > 0 && (
<TouchableOpacity
onPress={() => {
setIsUpdateModalVisible(true);
}}
style={{ marginRight: 10, flexDirection: 'row', alignItems: 'center' }}
>
<Ionicons name="download-outline" size={22} color={theme.colors.text} />
<Text style={{ color: theme.colors.text }}>
更新
</Text>
</TouchableOpacity>
)}
<TouchableOpacity
style={{ width: 40, height: 40, alignItems: 'center', justifyContent: 'center' }}
onPress={() => navigation.navigate('ScanLogin')}
>
<Ionicons name="qr-code-outline" size={24} color={theme.colors.text} />
<AntDesign name="scan" size={22} color={theme.colors.text} />
</TouchableOpacity>
<TouchableOpacity
style={{ width: 40, height: 40, alignItems: 'center', justifyContent: 'center' }}
Expand Down
10 changes: 0 additions & 10 deletions apps/mobile/src/screens/ReaderScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2254,16 +2254,6 @@ export function ReaderScreen() {
}, [book.fileType, currentChapter, chapters.length, loadChapter, pdfCurrentPage, pdfTotalPages]);

const handleTTS = useCallback(async () => {
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]);

Expand Down
62 changes: 60 additions & 2 deletions apps/mobile/src/screens/TTSScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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}`,
Expand All @@ -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);
}
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/src/stores/themeStore.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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' {
Expand Down
12 changes: 6 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading