@@ -6,10 +6,9 @@ import {
66 StyleSheet ,
77 TouchableOpacity ,
88 Image ,
9- ActivityIndicator ,
109 Modal ,
1110 StatusBar ,
12- useWindowDimensions ,
11+ Alert ,
1312} from 'react-native' ;
1413import { SafeAreaView } from 'react-native-safe-area-context' ;
1514import { useRouter } from 'expo-router' ;
@@ -19,15 +18,15 @@ let ScreenOrientation: typeof import('expo-screen-orientation') | null = null;
1918try { ScreenOrientation = require ( 'expo-screen-orientation' ) ; } catch { }
2019import { useDownloadStore , DownloadTask } from '../store/downloadStore' ;
2120import { LanShareModal } from '../components/LanShareModal' ;
21+ import { proxyImageUrl } from '../utils/imageUrl' ;
22+ import { useTheme } from '../utils/theme' ;
2223
2324function formatFileSize ( bytes ?: number ) : string {
2425 if ( ! bytes || bytes <= 0 ) return '' ;
2526 if ( bytes < 1024 * 1024 ) return `${ ( bytes / 1024 ) . toFixed ( 0 ) } KB` ;
2627 if ( bytes < 1024 * 1024 * 1024 ) return `${ ( bytes / ( 1024 * 1024 ) ) . toFixed ( 1 ) } MB` ;
2728 return `${ ( bytes / ( 1024 * 1024 * 1024 ) ) . toFixed ( 2 ) } GB` ;
2829}
29- import { proxyImageUrl } from '../utils/imageUrl' ;
30- import { useTheme } from '../utils/theme' ;
3130
3231export default function DownloadsScreen ( ) {
3332 const router = useRouter ( ) ;
@@ -36,8 +35,6 @@ export default function DownloadsScreen() {
3635 const [ playingUri , setPlayingUri ] = useState < string | null > ( null ) ;
3736 const [ playingTitle , setPlayingTitle ] = useState ( '' ) ;
3837 const [ shareTask , setShareTask ] = useState < ( DownloadTask & { key : string } ) | null > ( null ) ;
39- const { width, height } = useWindowDimensions ( ) ;
40- const isLandscape = width > height ;
4138
4239 async function openPlayer ( uri : string , title : string ) {
4340 setPlayingTitle ( title ) ;
@@ -50,6 +47,18 @@ export default function DownloadsScreen() {
5047 await ScreenOrientation ?. lockAsync ( ScreenOrientation . OrientationLock . PORTRAIT_UP ) ;
5148 }
5249
50+ function confirmDelete ( key : string , status : DownloadTask [ 'status' ] ) {
51+ const isDownloading = status === 'downloading' ;
52+ Alert . alert (
53+ isDownloading ? '取消下载' : '删除下载' ,
54+ isDownloading ? '确定取消该下载任务?' : '确定删除该文件?删除后不可恢复。' ,
55+ [
56+ { text : '取消' , style : 'cancel' } ,
57+ { text : isDownloading ? '取消下载' : '删除' , style : 'destructive' , onPress : ( ) => removeTask ( key ) } ,
58+ ] ,
59+ ) ;
60+ }
61+
5362 useEffect ( ( ) => {
5463 loadFromStorage ( ) ;
5564 } , [ ] ) ;
@@ -74,29 +83,33 @@ export default function DownloadsScreen() {
7483
7584 { sections . length === 0 ? (
7685 < View style = { styles . empty } >
77- < Ionicons name = "cloud-download-outline" size = { 56 } color = "#ccc" />
78- < Text style = { styles . emptyTxt } > 暂无下载记录</ Text >
86+ < Ionicons name = "cloud-download-outline" size = { 56 } color = { theme . textSub } />
87+ < Text style = { [ styles . emptyTxt , { color : theme . textSub } ] } > 暂无下载记录</ Text >
7988 </ View >
8089 ) : (
8190 < SectionList
8291 sections = { sections }
8392 keyExtractor = { ( item ) => item . key }
8493 renderSectionHeader = { ( { section } ) => (
85- < View style = { styles . sectionHeader } >
86- < Text style = { styles . sectionTitle } > { section . title } </ Text >
94+ < View style = { [ styles . sectionHeader , { backgroundColor : theme . bg } ] } >
95+ < Text style = { [ styles . sectionTitle , { color : theme . textSub } ] } > { section . title } </ Text >
8796 </ View >
8897 ) }
8998 renderItem = { ( { item } ) => (
9099 < DownloadRow
91100 task = { item }
101+ theme = { theme }
92102 onPlay = { ( ) => {
93103 if ( item . localUri ) openPlayer ( item . localUri , item . title ) ;
94104 } }
95- onDelete = { ( ) => removeTask ( item . key ) }
105+ onDelete = { ( ) => confirmDelete ( item . key , item . status ) }
96106 onShare = { ( ) => setShareTask ( item ) }
107+ onRetry = { ( ) => router . push ( `/video/${ item . bvid } ` as any ) }
97108 />
98109 ) }
99- ItemSeparatorComponent = { ( ) => < View style = { styles . separator } /> }
110+ ItemSeparatorComponent = { ( ) => (
111+ < View style = { [ styles . separator , { backgroundColor : theme . border , marginLeft : 108 } ] } />
112+ ) }
100113 contentContainerStyle = { { paddingBottom : 32 } }
101114 />
102115 ) }
@@ -119,22 +132,18 @@ export default function DownloadsScreen() {
119132 { playingUri && (
120133 < Video
121134 source = { { uri : playingUri } }
122- style = { isLandscape
123- ? { width, height }
124- : { width, height : width * 0.5625 } }
135+ style = { StyleSheet . absoluteFillObject }
125136 resizeMode = "contain"
126137 controls
127138 paused = { false }
128139 />
129140 ) }
130- { ! isLandscape && (
131- < View style = { styles . playerBar } >
132- < TouchableOpacity onPress = { closePlayer } style = { styles . closeBtn } >
133- < Ionicons name = "chevron-back" size = { 24 } color = "#fff" />
134- </ TouchableOpacity >
135- < Text style = { styles . playerTitle } numberOfLines = { 1 } > { playingTitle } </ Text >
136- </ View >
137- ) }
141+ < View style = { styles . playerBar } >
142+ < TouchableOpacity onPress = { closePlayer } style = { styles . closeBtn } hitSlop = { { top : 8 , bottom : 8 , left : 8 , right : 8 } } >
143+ < Ionicons name = "chevron-back" size = { 24 } color = "#fff" />
144+ </ TouchableOpacity >
145+ < Text style = { styles . playerTitle } numberOfLines = { 1 } > { playingTitle } </ Text >
146+ </ View >
138147 </ View >
139148 </ Modal >
140149 </ SafeAreaView >
@@ -143,98 +152,114 @@ export default function DownloadsScreen() {
143152
144153function DownloadRow ( {
145154 task,
155+ theme,
146156 onPlay,
147157 onDelete,
148158 onShare,
159+ onRetry,
149160} : {
150161 task : DownloadTask & { key : string } ;
162+ theme : ReturnType < typeof useTheme > ;
151163 onPlay : ( ) => void ;
152164 onDelete : ( ) => void ;
153165 onShare : ( ) => void ;
166+ onRetry : ( ) => void ;
154167} ) {
155- return (
156- < View style = { styles . row } >
157- < Image
158- source = { { uri : proxyImageUrl ( task . cover ) } }
159- style = { styles . cover }
160- />
168+ const isDone = task . status === 'done' ;
169+ const isError = task . status === 'error' ;
170+ const isDownloading = task . status === 'downloading' ;
171+
172+ const rowContent = (
173+ < View style = { [ styles . row , { backgroundColor : theme . card } ] } >
174+ < Image source = { { uri : proxyImageUrl ( task . cover ) } } style = { styles . cover } />
161175 < View style = { styles . info } >
162- < Text style = { styles . title } numberOfLines = { 2 } > { task . title } </ Text >
163- < Text style = { styles . qdesc } >
176+ < Text style = { [ styles . title , { color : theme . text } ] } numberOfLines = { 2 } > { task . title } </ Text >
177+ < Text style = { [ styles . qdesc , { color : theme . textSub } ] } >
164178 { task . qdesc } { task . fileSize ? ` · ${ formatFileSize ( task . fileSize ) } ` : '' }
165179 </ Text >
166- { task . status === 'downloading' && (
180+ { isDownloading && (
167181 < View style = { styles . progressWrap } >
168182 < View style = { styles . progressTrack } >
169183 < View style = { [ styles . progressFill , { width : `${ Math . round ( task . progress * 100 ) } %` as any } ] } />
170184 </ View >
171- < ActivityIndicator size = "small" color = "#00AEEC" style = { { marginLeft : 6 } } />
172185 < Text style = { styles . progressTxt } > { Math . round ( task . progress * 100 ) } %</ Text >
173186 </ View >
174187 ) }
175- { task . status === 'error' && (
176- < Text style = { styles . errorTxt } numberOfLines = { 1 } > { task . error ?? '下载失败' } </ Text >
188+ { isError && (
189+ < View style = { styles . errorRow } >
190+ < Text style = { styles . errorTxt } numberOfLines = { 1 } > { task . error ?? '下载失败' } </ Text >
191+ < TouchableOpacity onPress = { onRetry } style = { styles . retryBtn } >
192+ < Text style = { styles . retryTxt } > 重新下载</ Text >
193+ </ TouchableOpacity >
194+ </ View >
177195 ) }
178196 </ View >
179197 < View style = { styles . actions } >
180- { task . status === 'done' && (
181- < >
182- < TouchableOpacity style = { styles . playBtn } onPress = { onPlay } >
183- < Ionicons name = "play-circle" size = { 20 } color = "#00AEEC" />
184- < Text style = { styles . playTxt } > 播放</ Text >
185- </ TouchableOpacity >
186- < TouchableOpacity style = { styles . shareBtn } onPress = { onShare } >
187- < Ionicons name = "share-social-outline" size = { 20 } color = "#00AEEC" />
188- </ TouchableOpacity >
189- </ >
198+ { isDone && (
199+ < TouchableOpacity style = { styles . actionBtn } onPress = { onShare } >
200+ < Ionicons name = "share-social-outline" size = { 20 } color = "#00AEEC" />
201+ </ TouchableOpacity >
190202 ) }
191- < TouchableOpacity style = { styles . deleteBtn } onPress = { onDelete } >
192- < Ionicons name = "trash-outline" size = { 18 } color = "#bbb" />
203+ < TouchableOpacity
204+ style = { styles . actionBtn }
205+ onPress = { isDownloading ? onDelete : onDelete }
206+ >
207+ < Ionicons
208+ name = { isDownloading ? 'close-circle-outline' : 'trash-outline' }
209+ size = { 20 }
210+ color = { isDownloading ? '#bbb' : '#bbb' }
211+ />
193212 </ TouchableOpacity >
194213 </ View >
195214 </ View >
196215 ) ;
216+
217+ if ( isDone ) {
218+ return (
219+ < TouchableOpacity activeOpacity = { 0.85 } onPress = { onPlay } >
220+ { rowContent }
221+ </ TouchableOpacity >
222+ ) ;
223+ }
224+
225+ return rowContent ;
197226}
198227
199228const styles = StyleSheet . create ( {
200- safe : { flex : 1 , backgroundColor : '#fff' } ,
229+ safe : { flex : 1 } ,
201230 topBar : {
202231 flexDirection : 'row' ,
203232 alignItems : 'center' ,
204233 paddingHorizontal : 8 ,
205234 paddingVertical : 8 ,
206235 borderBottomWidth : StyleSheet . hairlineWidth ,
207- borderBottomColor : '#eee' ,
208236 } ,
209237 backBtn : { padding : 4 } ,
210238 topTitle : {
211239 flex : 1 ,
212240 fontSize : 16 ,
213241 fontWeight : '700' ,
214- color : '#212121' ,
215242 marginLeft : 4 ,
216243 } ,
217244 empty : { flex : 1 , alignItems : 'center' , justifyContent : 'center' , gap : 12 } ,
218- emptyTxt : { fontSize : 14 , color : '#bbb' } ,
245+ emptyTxt : { fontSize : 14 } ,
219246 sectionHeader : {
220- backgroundColor : '#f4f4f4' ,
221247 paddingHorizontal : 16 ,
222248 paddingVertical : 8 ,
223249 } ,
224- sectionTitle : { fontSize : 13 , fontWeight : '600' , color : '#555' } ,
250+ sectionTitle : { fontSize : 13 , fontWeight : '600' } ,
225251 row : {
226252 flexDirection : 'row' ,
227253 alignItems : 'center' ,
228254 paddingHorizontal : 16 ,
229255 paddingVertical : 12 ,
230- backgroundColor : '#fff' ,
231256 gap : 12 ,
232257 } ,
233- cover : { width : 80 , height : 54 , borderRadius : 6 , backgroundColor : '#eee' } ,
258+ cover : { width : 80 , height : 54 , borderRadius : 6 , backgroundColor : '#eee' , flexShrink : 0 } ,
234259 info : { flex : 1 } ,
235- title : { fontSize : 13 , color : '#212121' , lineHeight : 18 , marginBottom : 4 } ,
236- qdesc : { fontSize : 12 , color : '#999' , marginBottom : 4 } ,
237- progressWrap : { flexDirection : 'row' , alignItems : 'center' , marginTop : 2 } ,
260+ title : { fontSize : 13 , lineHeight : 18 , marginBottom : 4 } ,
261+ qdesc : { fontSize : 12 , marginBottom : 4 } ,
262+ progressWrap : { flexDirection : 'row' , alignItems : 'center' , marginTop : 2 , gap : 6 } ,
238263 progressTrack : {
239264 flex : 1 ,
240265 height : 3 ,
@@ -243,14 +268,19 @@ const styles = StyleSheet.create({
243268 overflow : 'hidden' ,
244269 } ,
245270 progressFill : { height : 3 , backgroundColor : '#00AEEC' , borderRadius : 2 } ,
246- progressTxt : { fontSize : 11 , color : '#999' , marginLeft : 4 } ,
247- errorTxt : { fontSize : 12 , color : '#f44' , marginTop : 2 } ,
248- actions : { alignItems : 'center' , gap : 8 } ,
249- playBtn : { flexDirection : 'row' , alignItems : 'center' , gap : 3 } ,
250- playTxt : { fontSize : 13 , color : '#00AEEC' } ,
251- shareBtn : { padding : 4 } ,
252- deleteBtn : { padding : 4 } ,
253- separator : { height : StyleSheet . hairlineWidth , backgroundColor : '#f0f0f0' , marginLeft : 108 } ,
271+ progressTxt : { fontSize : 11 , color : '#999' , minWidth : 30 } ,
272+ errorRow : { flexDirection : 'row' , alignItems : 'center' , gap : 8 , marginTop : 2 } ,
273+ errorTxt : { fontSize : 12 , color : '#f44' , flex : 1 } ,
274+ retryBtn : {
275+ paddingHorizontal : 8 ,
276+ paddingVertical : 2 ,
277+ borderRadius : 10 ,
278+ backgroundColor : '#e8f7fd' ,
279+ } ,
280+ retryTxt : { fontSize : 12 , color : '#00AEEC' , fontWeight : '600' } ,
281+ actions : { alignItems : 'center' , gap : 12 } ,
282+ actionBtn : { padding : 4 } ,
283+ separator : { height : StyleSheet . hairlineWidth } ,
254284 // player modal
255285 playerBg : { flex : 1 , backgroundColor : '#000' , justifyContent : 'center' } ,
256286 playerBar : {
@@ -261,6 +291,8 @@ const styles = StyleSheet.create({
261291 flexDirection : 'row' ,
262292 alignItems : 'center' ,
263293 paddingHorizontal : 8 ,
294+ backgroundColor : 'rgba(0,0,0,0.4)' ,
295+ paddingVertical : 8 ,
264296 } ,
265297 closeBtn : { padding : 6 } ,
266298 playerTitle : { flex : 1 , color : '#fff' , fontSize : 14 , fontWeight : '600' , marginLeft : 4 } ,
0 commit comments