Files
Client/lib/widget/downloads/downloads_sheet.dart
T

187 lines
6.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'package:flutter/material.dart';
import '../../routing/app_routes.dart';
import '../../utils/downloads/download_job.dart';
import '../../utils/downloads/download_manager.dart';
import '../../utils/haptics.dart';
import '../../view/pages/files/data/file_type_icon.dart';
import '../centered_leading.dart';
import '../details_bottom_sheet.dart';
import '../info_dialog.dart';
import 'stale_download_guard.dart';
/// Overview of all active and finished-but-unopened downloads. Lets the user
/// open/switch between finished files, cancel running ones and retry failures.
/// Completes when the sheet is dismissed.
Future<void> showDownloadsSheet(BuildContext context) {
final rootContext = context;
return showDetailsBottomSheet(
context,
header: const Padding(
padding: EdgeInsets.fromLTRB(16, 4, 16, 12),
child: Text(
'Downloads',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
children: (sheetContext) => [
_DownloadsList(rootContext: rootContext, sheetContext: sheetContext),
],
);
}
class _DownloadsList extends StatefulWidget {
const _DownloadsList({required this.rootContext, required this.sheetContext});
/// Context under the navigator, used to push the viewer after the sheet closes.
final BuildContext rootContext;
final BuildContext sheetContext;
@override
State<_DownloadsList> createState() => _DownloadsListState();
}
class _DownloadsListState extends State<_DownloadsList> {
ModalRoute<dynamic>? _sheetRoute;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_sheetRoute = ModalRoute.of(context);
}
@override
Widget build(BuildContext context) =>
ValueListenableBuilder<List<DownloadJob>>(
valueListenable: DownloadManager.instance.visibleJobs,
builder: (context, jobs, _) {
if (jobs.isEmpty) {
// Nothing left to manage — close the sheet, but only if it's still
// the top route. When _open drained the tray it already popped the
// sheet and pushed the viewer; without this guard we'd pop that
// viewer straight back off.
WidgetsBinding.instance.addPostFrameCallback((_) {
final route = _sheetRoute;
if (route != null && route.isCurrent) {
Navigator.of(widget.sheetContext).pop();
}
});
return const SizedBox.shrink();
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final job in jobs)
_DownloadRow(
key: ValueKey(job.remotePath),
job: job,
onOpen: () => _open(job),
),
],
);
},
);
void _open(DownloadJob job) {
final path = job.localPath;
if (path == null) return;
// Pop the sheet first so a stale-file dialog lands on the underlying
// screen instead of an emptied sheet.
Navigator.of(widget.sheetContext).pop();
if (!ensureDownloadStillExists(widget.rootContext, job)) return;
Haptics.success();
DownloadManager.instance.markOpened(job);
AppRoutes.openFileViewer(
widget.rootContext,
path,
remoteFile: job.remoteFile,
);
}
}
class _DownloadRow extends StatelessWidget {
const _DownloadRow({required this.job, required this.onOpen, super.key});
final DownloadJob job;
final VoidCallback onOpen;
@override
Widget build(BuildContext context) => AnimatedBuilder(
animation: job.status,
builder: (context, _) {
final status = job.status.value;
final theme = Theme.of(context);
final tile = ListTile(
leading: CenteredLeading(Icon(iconForFileName(job.name))),
title: Text(job.name, maxLines: 2, overflow: TextOverflow.ellipsis),
subtitle: switch (status) {
DownloadInProgress(:final percent) => Row(
children: [
Expanded(
child: LinearProgressIndicator(
value: percent <= 0 ? null : percent / 100,
),
),
const SizedBox(width: 10),
Text('${percent.round()}%'),
],
),
DownloadDone() => const Text('Fertig tippen zum Öffnen'),
DownloadFailed() => Text(
'Fehlgeschlagen',
style: TextStyle(color: theme.colorScheme.error),
),
DownloadCancelled() => const Text('Abgebrochen'),
},
trailing: switch (status) {
DownloadInProgress() => IconButton(
icon: const Icon(Icons.close),
tooltip: 'Abbrechen',
onPressed: () =>
unawaited(DownloadManager.instance.cancel(job)),
),
DownloadFailed() => IconButton(
icon: const Icon(Icons.refresh),
tooltip: 'Erneut versuchen',
onPressed: () => unawaited(DownloadManager.instance.retry(job)),
),
DownloadDone() => const Icon(Icons.open_in_new),
DownloadCancelled() => null,
},
onTap: switch (status) {
DownloadDone() => onOpen,
DownloadFailed() => () => _showError(context, status),
_ => null,
},
);
// Finished rows can be swiped away; running ones stay put.
if (status is DownloadInProgress) return tile;
return Dismissible(
key: ValueKey('dismiss-${job.remotePath}'),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 24),
color: theme.colorScheme.errorContainer,
child: Icon(Icons.delete_outline, color: theme.colorScheme.onErrorContainer),
),
onDismissed: (_) => DownloadManager.instance.dismiss(job),
child: tile,
);
},
);
void _showError(BuildContext context, DownloadFailed status) {
InfoDialog.show(
context,
status.message,
title: 'Download fehlgeschlagen',
copyable: true,
);
}
}