Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 80 additions & 38 deletions lib/components/DownloadsScreen/downloaded_items_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,56 +107,98 @@ class DownloadedChildrenList extends ConsumerStatefulWidget {
}

class _DownloadedChildrenListState extends ConsumerState<DownloadedChildrenList> {
final _downloadsService = GetIt.instance<DownloadsService>();
final downloadsService = GetIt.instance<DownloadsService>();

@override
Widget build(BuildContext context) {
var items = _downloadsService.getVisibleChildren(widget.parent);

// If we're displaying an artist, we have to filter out tracks that are
// children of albums we already have in the list
if ((widget.parent.type == DownloadItemType.collection && widget.parent.baseItemType == BaseItemDtoType.artist) ||
(widget.parent.type == DownloadItemType.finampCollection &&
widget.parent.finampCollection!.type == FinampCollectionType.collectionWithLibraryFilter &&
BaseItemDtoType.fromItem(widget.parent.finampCollection!.item!) == BaseItemDtoType.artist)) {
// Collect album names
final albumIds = <BaseItemId>{};
for (var stub in items) {
if (BaseItemDtoType.fromItem(stub.baseItem!) == BaseItemDtoType.album) {
final albumId = stub.baseItem?.id;
if (albumId != null) albumIds.add(albumId);
List<DownloadStub> filterTracksByAlbum(List<DownloadStub> unfilteredItems) {
if ((widget.parent.type == DownloadItemType.collection && widget.parent.baseItemType == BaseItemDtoType.artist) ||
(widget.parent.type == DownloadItemType.finampCollection &&
widget.parent.finampCollection!.type == FinampCollectionType.collectionWithLibraryFilter &&
BaseItemDtoType.fromItem(widget.parent.finampCollection!.item!) == BaseItemDtoType.artist)) {
// Collect album names
final albumIds = <BaseItemId>{};
for (var stub in unfilteredItems) {
if (BaseItemDtoType.fromItem(stub.baseItem!) == BaseItemDtoType.album) {
final albumId = stub.baseItem?.id;
if (albumId != null) albumIds.add(albumId);
}
}
// Filter out tracks with matching album id
unfilteredItems = unfilteredItems.where((stub) {
final type = BaseItemDtoType.fromItem(stub.baseItem!);
if (type == BaseItemDtoType.track) {
final albumId = stub.baseItem?.albumId;
return !albumIds.contains(albumId);
}
return true;
}).toList();
}
// Filter out tracks with matching album id
items = items.where((stub) {
final type = BaseItemDtoType.fromItem(stub.baseItem!);
if (type == BaseItemDtoType.track) {
final albumId = stub.baseItem?.albumId;
return !albumIds.contains(albumId);
}
return true;
}).toList();

return unfilteredItems;
}

return Container(
var unfilteredItems = downloadsService.getVisibleChildren(widget.parent);
final items = filterTracksByAlbum(unfilteredItems);

return ColoredBox(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: Column(
children: [
// TODO use a list builder here
for (final stub in items)
ListTile(
title: Text(stub.baseItem?.name ?? stub.name),
leading: AlbumImage(item: stub.baseItem),
subtitle: ItemFileSize(stub: stub),
trailing: ref.watch(_downloadsService.statusProvider((stub, null))).isRequired
? IconButton(
icon: const Icon(Icons.delete),
onPressed: () => askBeforeDeleteDownloadFromDevice(context, stub),
)
: null,
),
],
children: [for (final stub in items) DownloadedItemListTile(stub: stub, downloadsService: downloadsService)],
),
);
}
}

class DownloadedItemListTile extends ConsumerWidget {
const DownloadedItemListTile({super.key, required this.stub, required this.downloadsService});

final DownloadStub stub;
final DownloadsService downloadsService;

@override
Widget build(BuildContext context, WidgetRef ref) {
final itemDownloadProgress = ref.watch(downloadsService.progressProvider(stub.isarId));
final isAvailableToDelete = ref.watch(downloadsService.statusProvider((stub, null))).isRequired;

return ListTile(
title: Text(stub.baseItem?.name ?? stub.name),
leading: AlbumImage(item: stub.baseItem),
subtitle: AnimatedSize(
alignment: Alignment.topCenter,
curve: Curves.easeOut,
duration: Duration(milliseconds: 200),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
ItemFileSize(stub: stub),
if (itemDownloadProgress?.progress != null)
Row(
spacing: 16,
children: [
Flexible(
child: LinearProgressIndicator(
value: itemDownloadProgress!.progress,
minHeight: 12,
borderRadius: BorderRadius.circular(8),
),
),
if (itemDownloadProgress.hasNetworkSpeed) Text(itemDownloadProgress.networkSpeedAsString),
],
),
],
),
),
trailing: isAvailableToDelete
? IconButton(
icon: const Icon(Icons.delete),
onPressed: () => askBeforeDeleteDownloadFromDevice(context, stub),
)
: null,
isThreeLine: true,
);
}
}
Expand Down
44 changes: 43 additions & 1 deletion lib/services/downloads_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ class DownloadsService {
late final Stream<Map<DownloadItemState, int>> downloadStatusesStream;
final StreamController<Map<DownloadItemState, int>> _downloadStatusesStreamController = StreamController.broadcast();

final Map<int, TaskProgressUpdate> _downloadsProgress = {};
late final Stream<void> downloadProgressStream;
final StreamController<void> _downloadProgressStreamController = StreamController.broadcast();

// This triggers refresh of music/artist screens on item deletion
late final Stream<void> offlineDeletesStream;
final StreamController<void> _offlineDeletesStreamController = StreamController.broadcast();
Expand Down Expand Up @@ -160,11 +164,22 @@ class DownloadsService {
.toList();
});

// Gets download progress for invididual downloading files
late final progressProvider = Provider.family.autoDispose<TaskProgressUpdate?, int>((ref, isarId) {
var subscription = downloadProgressStream.listen((_) {
ref.state = _downloadsProgress[isarId];
});
ref.onDispose(subscription.cancel);
return _downloadsProgress[isarId];
});

/// Constructs the service. startQueues should also be called to complete initialization.
DownloadsService() {
// Initialize downloadStatuses dict with actual counts of items in isar with
// that state. Calls to updateItemState will keep this up to date as the
// state of an item is changed.
const kStreamThrottleTime = Duration(milliseconds: 200);

for (var state in DownloadItemState.values) {
downloadStatuses[state] = _isar.downloadItems
.where()
Expand All @@ -178,10 +193,17 @@ class DownloadsService {
}

downloadStatusesStream = _downloadStatusesStreamController.stream.throttleTime(
const Duration(milliseconds: 200),
kStreamThrottleTime,
leading: false,
trailing: true,
);

downloadProgressStream = _downloadProgressStreamController.stream.throttleTime(
kStreamThrottleTime,
leading: false,
trailing: true,
);

offlineDeletesStream = _offlineDeletesStreamController.stream;
downloadCountsStream = _downloadCountsStreamController.stream;

Expand Down Expand Up @@ -282,6 +304,21 @@ class DownloadsService {
_downloadsLogger.severe("Could not determine item for id ${event.task.taskId}, event:${event.toString()}");
}
});
} else if (event is TaskProgressUpdate) {
final taskId = event.task.taskId;
final isarId = int.tryParse(taskId);
if (isarId == null) {
_downloadsLogger.severe("Unabled to find item to download with id $taskId");
return;
}

if (event.progress < 0 || event.progress >= 1.0) {
_downloadsProgress.remove(isarId);
} else {
_downloadsProgress[isarId] = event;
}

_downloadProgressStreamController.add(null);
}
});

Expand Down Expand Up @@ -901,6 +938,11 @@ class DownloadsService {
}
}
item.state = newState;

if (newState.isFinal && item.type.hasFiles) {
_downloadsProgress.remove(item.isarId);
}

_isar.downloadItems.putSync(item, saveLinks: false);
List<DownloadItem> parents = _isar.downloadItems
.where()
Expand Down
1 change: 1 addition & 0 deletions lib/services/downloads_service_backend.dart
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ class IsarTaskQueue implements TaskQueue {
directory: path_helper.dirname(task.path!),
headers: {"Authorization": _finampUserHelper.authorizationHeader},
filename: path_helper.basename(task.path!),
updates: Updates.statusAndProgress,
);
return Future.sync(() async {
//bool success = await FileDownloader().resume(downloadTask);
Expand Down
2 changes: 1 addition & 1 deletion lib/services/music_providers.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading