added support for simultan downloads in files and talk, support for background downloads, enhanced loading in files with retry
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
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';
|
||||
|
||||
/// 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;
|
||||
Haptics.success();
|
||||
Navigator.of(widget.sheetContext).pop();
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user