import 'dart:async'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/appbar/appbar.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/select_mask.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models_new/download/bili_download_entry_info.dart'; import 'package:PiliPlus/models_new/download/download_info.dart'; import 'package:PiliPlus/pages/download/controller.dart'; import 'package:PiliPlus/pages/download/detail/view.dart'; import 'package:PiliPlus/pages/download/detail/widgets/item.dart'; import 'package:PiliPlus/pages/download/search/view.dart'; import 'package:PiliPlus/services/download/download_service.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart' show IterableExt; import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:flutter/material.dart' hide SliverGridDelegateWithMaxCrossAxisExtent; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; class DownloadPage extends StatefulWidget { const DownloadPage({super.key}); @override State createState() => _DownloadPageState(); } class _DownloadPageState extends State { final _downloadService = Get.find(); final _controller = Get.put(DownloadPageController()); final _progress = ChangeNotifier(); @override void dispose() { _progress.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final padding = MediaQuery.viewPaddingOf(context); return Obx(() { final enableMultiSelect = _controller.enableMultiSelect.value; return PopScope( canPop: !enableMultiSelect, onPopInvokedWithResult: (didPop, result) { if (enableMultiSelect) { _controller.handleSelect(); } }, child: Scaffold( resizeToAvoidBottomInset: false, appBar: MultiSelectAppBarWidget( ctr: _controller, actions: [ TextButton( style: TextButton.styleFrom( visualDensity: VisualDensity.compact, ), onPressed: () async { final allChecked = _controller.allChecked.toSet(); _controller.handleSelect(); final list = []; for (final page in allChecked) { list.addAll(page.entries); } final res = await Future.wait( list.map( (e) => _downloadService.downloadDanmaku( entry: e, isUpdate: true, ), ), ); if (res.every((e) => e)) { SmartDialog.showToast('更新成功'); } else { SmartDialog.showToast('更新失败'); } }, child: Text( '更新', style: TextStyle(color: theme.colorScheme.onSurface), ), ), ], child: AppBar( title: const Text('离线缓存'), actions: [ IconButton( tooltip: '搜索', onPressed: () async { await _downloadService.waitForInitialization; if (!mounted) return; Get.to(DownloadSearchPage(progress: _progress)); }, icon: const Icon(Icons.search), ), IconButton( tooltip: '多选', onPressed: () { if (enableMultiSelect) { _controller.handleSelect(); } else { _controller.enableMultiSelect.value = true; } }, icon: const Icon(Icons.edit_note), ), const SizedBox(width: 6), ], ), ), body: Padding( padding: EdgeInsets.only(left: padding.left, right: padding.right), child: CustomScrollView( slivers: [ Obx(() { final entry = _downloadService.waitDownloadQueue.firstWhereOrNull( (e) => e.cid == _downloadService.curCid, ) ?? _downloadService.waitDownloadQueue.firstOrNull; if (entry != null) { return SliverMainAxisGroup( slivers: [ SliverPadding( padding: const EdgeInsets.only(left: 12, bottom: 7), sliver: SliverToBoxAdapter( child: Text( '正在缓存 (${_downloadService.waitDownloadQueue.length})', ), ), ), SliverToBoxAdapter( child: SizedBox( height: 100, child: DetailItem( entry: entry, progress: _progress, downloadService: _downloadService, showTitle: true, isCurr: true, controller: _controller, ), ), ), ], ); } return const SliverToBoxAdapter(); }), Obx(() { if (_controller.pages.isNotEmpty) { return SliverMainAxisGroup( slivers: [ SliverPadding( padding: EdgeInsets.only( left: 12, bottom: 7, top: _downloadService.waitDownloadQueue.isEmpty ? 0 : 7, ), sliver: const SliverToBoxAdapter( child: Text('已缓存视频'), ), ), SliverGrid.builder( gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( mainAxisSpacing: 2, mainAxisExtent: 100, maxCrossAxisExtent: Grid.smallCardWidth * 2, ), itemBuilder: (context, index) { final item = _controller.pages[index]; if (item.entries.length == 1) { final entry = item.entries.first; return DetailItem( entry: entry, progress: _progress, downloadService: _downloadService, showTitle: true, onDelete: () { _downloadService.deleteDownload( entry: entry, removeList: true, ); GStorage.watchProgress.delete( entry.cid.toString(), ); }, checked: item.checked, onSelect: (_) => _controller.onSelect(item), controller: _controller, ); } return _buildItem(theme, item, enableMultiSelect); }, itemCount: _controller.pages.length, ), ], ); } if (_downloadService.waitDownloadQueue.isNotEmpty) { return const SliverToBoxAdapter(); } return const HttpError(); }), SliverToBoxAdapter( child: SizedBox(height: padding.bottom + 100), ), ], ), ), ), ); }); } Widget _buildItem( ThemeData theme, DownloadPageInfo pageInfo, bool enableMultiSelect, ) { void onLongPress() => enableMultiSelect ? null : showDialog( context: context, builder: (context) => AlertDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), content: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( onTap: () { Get.back(); showConfirmDialog( context: context, title: '确定删除?', onConfirm: () async { await GStorage.watchProgress.deleteAll( pageInfo.entries.map((e) => e.cid.toString()), ); _downloadService.deletePage( pageDirPath: pageInfo.dirPath, ); }, ); }, dense: true, title: const Text( '删除', style: TextStyle(fontSize: 14), ), ), ListTile( onTap: () async { Get.back(); final res = await Future.wait( pageInfo.entries.map( (e) => _downloadService.downloadDanmaku( entry: e, isUpdate: true, ), ), ); if (res.every((e) => e)) { SmartDialog.showToast('更新成功'); } else { SmartDialog.showToast('更新失败'); } }, dense: true, title: const Text( '更新弹幕', style: TextStyle(fontSize: 14), ), ), ], ), ), ); final first = pageInfo.entries.first; return Material( type: MaterialType.transparency, child: InkWell( onTap: () { if (_controller.enableMultiSelect.value) { _controller.onSelect(pageInfo); return; } Get.to( DownloadDetailPage( pageId: pageInfo.pageId, title: pageInfo.title, progress: _progress, ), ); }, onLongPress: onLongPress, onSecondaryTap: PlatformUtils.isMobile ? null : onLongPress, child: Padding( padding: const EdgeInsets.symmetric( horizontal: StyleString.safeSpace, vertical: 5, ), child: Row( spacing: 10, children: [ Stack( clipBehavior: Clip.none, children: [ AspectRatio( aspectRatio: StyleString.aspectRatio, child: LayoutBuilder( builder: (context, constraints) => NetworkImgLayer( src: pageInfo.cover, width: constraints.maxWidth, height: constraints.maxHeight, ), ), ), PBadge( text: '${pageInfo.entries.length}个视频', right: 6.0, bottom: 6.0, isBold: false, type: PBadgeType.gray, ), if (pageInfo.seasonType case final pgcType?) PBadge( text: switch (pgcType) { -1 => '课程', 1 => '番剧', 2 => '电影', 3 => '纪录片', 4 => '国创', 5 => '电视剧', 7 => '综艺', _ => null, }, right: 6.0, top: 6.0, ), Positioned.fill( child: selectMask(theme, pageInfo.checked), ), ], ), Expanded( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( pageInfo.title, textAlign: TextAlign.start, style: TextStyle( fontSize: theme.textTheme.bodyMedium!.fontSize, height: 1.42, letterSpacing: 0.3, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), Row( crossAxisAlignment: .end, mainAxisAlignment: .spaceBetween, children: [ if (first.ownerName case final ownerName?) Text( ownerName, style: TextStyle( fontSize: 12, height: 1.6, color: theme.colorScheme.outline, ), ) else const Spacer(), pageInfo.entries.first.moreBtn(theme), ], ), ], ), ), ], ), ), ), ); } }