Skip to content

Commit

Permalink
fix: Events Downloading (#256)
Browse files Browse the repository at this point in the history
  • Loading branch information
bdlukaa authored Aug 22, 2024
2 parents 20876d3 + 2d1a268 commit fce7ab0
Showing 11 changed files with 481 additions and 418 deletions.
47 changes: 35 additions & 12 deletions lib/providers/downloads_provider.dart
Original file line number Diff line number Diff line change
@@ -198,8 +198,7 @@ class DownloadsManager extends UnityProvider {
Future<void> save({bool notifyListeners = true}) async {
try {
await downloads.write({
kStorageDownloads:
jsonEncode(downloadedEvents.map((de) => de.toJson()).toList()),
kStorageDownloads: downloadedEvents.map((de) => de.toJson()).toList(),
});
} catch (e) {
debugPrint(e.toString());
@@ -212,15 +211,26 @@ class DownloadsManager extends UnityProvider {
Future<void> restore({bool notifyListeners = true}) async {
final data = await tryReadStorage(() => downloads.read());

downloadedEvents = data[kStorageDownloads] == null
? <DownloadedEvent>{}
: ((await compute(jsonDecode, data[kStorageDownloads] as String) ?? [])
as List)
.map<DownloadedEvent>((item) {
return DownloadedEvent.fromJson(
(item as Map).cast<String, dynamic>(),
);
}).toSet();
// TODO(bdlukaa): Remove this migration in the future.
// Previously, we were unecessarily encoding the downloads
// data as a string. This is no longer necessary.
//
// This migration is to ensure the downloads made on previous
// versions are not lost.
List downloadsData;
if (data[kStorageDownloads] == null) {
downloadsData = [];
} else if (data[kStorageDownloads] is String) {
downloadsData = jsonDecode(data[kStorageDownloads]);
} else {
downloadsData = data[kStorageDownloads];
}

downloadedEvents = downloadsData.map<DownloadedEvent>((item) {
return DownloadedEvent.fromJson(
(item as Map).cast<String, dynamic>(),
);
}).toSet();

super.restore(notifyListeners: notifyListeners);
}
@@ -296,7 +306,10 @@ class DownloadsManager extends UnityProvider {
.value
.$2;
}
debugPrint('Downloading event: $event to $dir');
writeLogToFile(
'downloads(${event.id}): $dir at ${event.mediaURL!}',
print: true,
);
final home = HomeProvider.instance
..loading(UnityLoadingReason.downloadEvent);

@@ -310,6 +323,15 @@ class DownloadsManager extends UnityProvider {
final fileName =
'event_${event.id}_${event.deviceID}_${event.server.name}.mp4';
final downloadPath = path.join(dir, fileName);
final downloadFile = File(downloadPath);
if (!(await downloadFile.exists())) {
await downloadFile.create(recursive: true);
writeLogToFile(
'downloads(${event.id}): Created file: $downloadPath',
print: true,
);
}

await Dio().downloadUri(
event.mediaURL!,
downloadPath,
@@ -319,6 +341,7 @@ class DownloadsManager extends UnityProvider {
onReceiveProgress: (received, total) {
if (total != -1) {
downloading[event] = (received / total, fileName);
writeLogToFile('downloads(${event.id}): ${received / total}');
notifyListeners();
}
},
283 changes: 283 additions & 0 deletions lib/screens/downloads/download_tile.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
/*
* This file is a part of Bluecherry Client (https://github.com/bluecherrydvr/unity).
*
* Copyright 2022 Bluecherry, LLC
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import 'dart:io';

import 'package:bluecherry_client/models/event.dart';
import 'package:bluecherry_client/providers/downloads_provider.dart';
import 'package:bluecherry_client/providers/settings_provider.dart';
import 'package:bluecherry_client/screens/downloads/downloads_manager.dart';
import 'package:bluecherry_client/screens/downloads/indicators.dart';
import 'package:bluecherry_client/utils/date.dart';
import 'package:bluecherry_client/utils/extensions.dart';
import 'package:bluecherry_client/utils/methods.dart';
import 'package:bluecherry_client/utils/theme.dart';
import 'package:bluecherry_client/utils/window.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';

class DownloadTile extends StatefulWidget {
const DownloadTile({
super.key,
required this.size,
required this.event,
this.upcomingEvents = const [],
this.progress = 1.0,
this.downloadPath,
this.initiallyExpanded = true,
});

final Size size;
final Event event;
final Iterable<Event> upcomingEvents;
final double progress;
final String? downloadPath;
final bool initiallyExpanded;

static const _breakpoint = 500.0;

@override
State<DownloadTile> createState() => _DownloadTileState();
}

class _DownloadTileState extends State<DownloadTile> {
/// Whether the event is fully downloaded
bool get isDownloaded =>
widget.progress == 1.0 && widget.downloadPath != null;

@override
void initState() {
super.initState();
if (widget.initiallyExpanded) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!context.mounted) return;
Scrollable.ensureVisible(context);
});
}
}

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final loc = AppLocalizations.of(context);
final settings = context.watch<SettingsProvider>();

final eventType = widget.event.type.locale(context).uppercaseFirst;
final at = settings.formatRawDateAndTime(widget.event.publishedRaw);

final shape = RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
);

return Padding(
padding: const EdgeInsetsDirectional.only(
start: kDownloadsManagerPadding,
end: kDownloadsManagerPadding,
bottom: kDownloadsManagerPadding / 2,
),
child: ClipPath.shape(
shape: shape,
child: Card(
margin: EdgeInsetsDirectional.zero,
shape: shape,
child: ExpansionTile(
clipBehavior: Clip.hardEdge,
shape: shape,
collapsedShape: shape,
initiallyExpanded: widget.initiallyExpanded,
title: Row(children: [
SizedBox(
width: 40.0,
height: 40.0,
child: () {
if (isDownloaded) {
return Padding(
padding: const EdgeInsetsDirectional.only(end: 12.0),
child: Icon(
Icons.download_done,
color: theme.extension<UnityColors>()!.successColor,
),
);
}

return DownloadProgressIndicator(progress: widget.progress);
}(),
),
Expanded(
child: Text(
loc.downloadTitle(
eventType,
widget.event.deviceName,
widget.event.server.name,
at,
),
),
),
]),
childrenPadding: const EdgeInsetsDirectional.symmetric(
vertical: 12.0,
horizontal: 16.0,
),
expandedCrossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: double.infinity,
child: Flex(
direction: widget.size.width >= DownloadTile._breakpoint
? Axis.horizontal
: Axis.vertical,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
wrapExpandedIf(
widget.size.width >= DownloadTile._breakpoint,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DefaultTextStyle.merge(
style: const TextStyle(
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.fade,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(loc.eventType),
Text(loc.device),
Text(loc.server),
Text(loc.duration),
Text(loc.date),
],
),
),
const SizedBox(width: 6.0),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(eventType),
Text(widget.event.deviceName),
Text(widget.event.server.name),
Text(
() {
var fileSize = '';
if (isDownloaded) {
final size =
File(widget.downloadPath!).mbSize;
fileSize =
' (${size.toStringAsFixed(2)} MB)';
}

return widget.event.duration
.humanReadable(context) +
fileSize;
}(),
),
Text(at),
],
),
),
],
),
),
const SizedBox(height: 12.0),
Center(
child: Flex(
direction: widget.size.width >= DownloadTile._breakpoint
? Axis.vertical
: Axis.horizontal,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: [
wrapTooltipIf(
isDownloaded &&
widget.size.width < DownloadTile._breakpoint,
preferBelow: false,
message: loc.play,
child: TextButton(
onPressed: isDownloaded
? () {
Navigator.of(context).pushNamed(
'/events',
arguments: {
'event': widget.event,
'upcoming': widget.upcomingEvents,
},
);
}
: null,
child: Row(children: [
const Icon(Icons.play_arrow, size: 20.0),
if (widget.size.width >=
DownloadTile._breakpoint) ...[
const SizedBox(width: 8.0),
Text(loc.play),
],
]),
),
),
if (isDownloaded)
TextButton.icon(
onPressed: () {
context
.read<DownloadsManager>()
.delete(widget.downloadPath!);
},
icon: const Icon(Icons.delete, size: 20.0),
label: Text(loc.delete),
)
else
TextButton.icon(
onPressed: () {
context
.read<DownloadsManager>()
.cancelEventDownload(widget.event);
},
icon: const Icon(Icons.cancel, size: 20.0),
label: Text(loc.cancel),
),
if (isDesktop)
TextButton.icon(
onPressed: isDownloaded
? () {
launchFileExplorer(
File(widget.downloadPath!).parent.path,
);
}
: null,
icon: const Icon(Icons.folder, size: 20.0),
label: Text(loc.showInFiles),
),
],
),
),
],
),
),
],
),
),
),
);
}
}
Loading

0 comments on commit fce7ab0

Please sign in to comment.