feature: Re-implemented syncing

This commit is contained in:
PartyDonut 2025-07-27 10:54:29 +02:00
parent c5c7f71b84
commit 86ff355e21
51 changed files with 3067 additions and 1147 deletions

View file

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/book_model.dart';
import 'package:fladder/models/items/images_models.dart';

View file

@ -6,7 +6,10 @@ import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/routes/auto_router.gr.dart';
import 'package:fladder/screens/syncing/sync_button.dart';
import 'package:fladder/screens/syncing/sync_item_details.dart';
import 'package:fladder/theme.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/fladder_image.dart';
@ -196,6 +199,19 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
mainAxisSize: MainAxisSize.min,
children: [
if (widget.item != null) ...[
ref.watch(syncedItemProvider(widget.item)).when(
error: (error, stackTrace) => const SizedBox.shrink(),
data: (syncedItem) {
if (syncedItem == null) {
return const SizedBox.shrink();
}
return IconButton(
onPressed: () => showSyncItemDetails(context, syncedItem, ref),
icon: SyncButton(item: widget.item!, syncedItem: syncedItem),
);
},
loading: () => const SizedBox.shrink(),
),
Builder(
builder: (context) {
final newActions = widget.actions?.call(context);

View file

@ -10,6 +10,7 @@ void fladderSnackbar(
bool showCloseButton = false,
Duration duration = const Duration(seconds: 3),
}) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
title,

View file

@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/details_screens/components/media_stream_information.dart';
import 'package:fladder/screens/shared/media/episode_posters.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
@ -31,7 +30,7 @@ class NextUpEpisode extends ConsumerWidget {
Opacity(
opacity: 0.75,
child: SelectableText(
"${context.localized.season(1)} ${nextEpisode.season} - ${context.localized.episode(1)} ${nextEpisode.episode}",
nextEpisode.seasonEpisodeLabelFull(context),
style: Theme.of(context).textTheme.titleMedium,
),
),
@ -42,13 +41,11 @@ class NextUpEpisode extends ConsumerWidget {
const SizedBox(height: 16),
LayoutBuilder(
builder: (context, constraints) {
final syncedItem = ref.read(syncProvider.notifier).getSyncedItem(nextEpisode);
if (constraints.maxWidth < 550) {
return Column(
children: [
EpisodePoster(
episode: nextEpisode,
syncedItem: syncedItem,
showLabel: false,
onTap: () => nextEpisode.navigateTo(context),
actions: const [],
@ -71,7 +68,6 @@ class NextUpEpisode extends ConsumerWidget {
maxWidth: MediaQuery.of(context).size.width / 2),
child: EpisodePoster(
episode: nextEpisode,
syncedItem: syncedItem,
showLabel: false,
onTap: () => nextEpisode.navigateTo(context),
actions: const [],

View file

@ -1,16 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/shared/media/episode_posters.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/humanize_duration.dart';
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/util/humanize_duration.dart';
enum EpisodeDetailsViewType {
list(icon: IconsaxPlusBold.grid_6),
@ -48,14 +48,12 @@ class EpisodeDetailsList extends ConsumerWidget {
itemCount: episodes.length,
itemBuilder: (context, index) {
final episode = episodes[index];
final syncedItem = ref.watch(syncProvider.notifier).getSyncedItem(episode);
List<Widget> children = [
Flexible(
flex: 1,
child: EpisodePoster(
episode: episode,
showLabel: false,
syncedItem: syncedItem,
actions: episode.generateActions(context, ref),
onTap: () => episode.navigateTo(context),
isCurrentEpisode: false,

View file

@ -3,10 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/syncing/sync_button.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
@ -88,11 +86,9 @@ class _EpisodePosterState extends ConsumerState<EpisodePosters> {
itemBuilder: (context, index) {
final episode = episodes[index];
final isCurrentEpisode = index == indexOfCurrent;
final syncedItem = ref.watch(syncProvider.notifier).getSyncedItem(episode);
return EpisodePoster(
episode: episode,
blur: allPlayed ? false : indexOfCurrent < index,
syncedItem: syncedItem,
onTap: widget.onEpisodeTap != null
? () {
widget.onEpisodeTap?.call(
@ -130,7 +126,6 @@ class _EpisodePosterState extends ConsumerState<EpisodePosters> {
class EpisodePoster extends ConsumerWidget {
final EpisodeModel episode;
final SyncedItem? syncedItem;
final bool showLabel;
final Function()? onTap;
final Function()? onLongPress;
@ -141,7 +136,6 @@ class EpisodePoster extends ConsumerWidget {
const EpisodePoster({
super.key,
required this.episode,
this.syncedItem,
this.showLabel = true,
this.onTap,
this.onLongPress,
@ -156,7 +150,6 @@ class EpisodePoster extends ConsumerWidget {
color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: const Icon(Icons.local_movies_outlined),
);
final SyncedItem? iSyncedItem = syncedItem;
bool episodeAvailable = episode.status == EpisodeStatus.available;
return AspectRatio(
aspectRatio: 1.76,
@ -203,15 +196,18 @@ class EpisodePoster extends ConsumerWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (iSyncedItem != null)
Consumer(builder: (context, ref, child) {
final SyncStatus syncStatus =
ref.watch(syncStatusesProvider(iSyncedItem, null)).value ?? SyncStatus.partially;
return StatusCard(
color: syncStatus.color,
child: SyncButton(item: episode, syncedItem: syncedItem),
);
}),
ref.watch(syncedItemProvider(episode)).when(
error: (error, stackTrace) => const SizedBox.shrink(),
data: (syncedItem) {
if (syncedItem == null) {
return const SizedBox.shrink();
}
return StatusCard(
child: SyncButton(item: episode, syncedItem: syncedItem),
);
},
loading: () => const SizedBox.shrink(),
),
if (episode.userData.isFavourite)
const StatusCard(
color: Colors.red,
@ -259,7 +255,7 @@ class EpisodePoster extends ConsumerWidget {
child: Align(
alignment: Alignment.bottomRight,
child: PopupMenuButton(
tooltip: "Options",
tooltip: context.localized.options,
icon: const Icon(
Icons.more_vert,
color: Colors.white,

View file

@ -19,6 +19,7 @@ class PosterWidget extends ConsumerWidget {
final int maxLines;
final double? aspectRatio;
final bool inlineTitle;
final bool underTitle;
final Set<ItemActions> excludeActions;
final List<ItemAction> otherActions;
final Function(String id, UserData? newData)? onUserDataChanged;
@ -33,6 +34,7 @@ class PosterWidget extends ConsumerWidget {
this.heroTag,
this.aspectRatio,
this.inlineTitle = false,
this.underTitle = true,
this.excludeActions = const {},
this.otherActions = const [],
this.onUserDataChanged,
@ -64,7 +66,7 @@ class PosterWidget extends ConsumerWidget {
onPressed: onPressed,
),
),
if (!inlineTitle)
if (!inlineTitle && underTitle)
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,

View file

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/syncing/sync_button.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
@ -58,7 +58,6 @@ class SeasonPoster extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final syncedItem = ref.watch(syncProvider.notifier).getSyncedItem(season);
Padding placeHolder(String title) {
return Padding(
padding: const EdgeInsets.all(4),
@ -100,19 +99,25 @@ class SeasonPoster extends ConsumerWidget {
alignment: Alignment.topLeft,
child: placeHolder(season.name),
),
if (season.userData.unPlayedItemCount != 0)
Align(
alignment: Alignment.topRight,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (syncedItem != null)
StatusCard(
child: SyncButton(
item: season,
syncedItem: syncedItem,
),
Align(
alignment: Alignment.topRight,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ref.watch(syncedItemProvider(season)).when(
error: (error, stackTrace) => const SizedBox.shrink(),
data: (syncedItem) {
if (syncedItem == null) {
return const SizedBox.shrink();
}
return StatusCard(
child: SyncButton(item: season, syncedItem: syncedItem),
);
},
loading: () => const SizedBox.shrink(),
),
if (season.userData.unPlayedItemCount != 0)
StatusCard(
color: Theme.of(context).colorScheme.primary,
useFittedBox: true,
@ -122,20 +127,20 @@ class SeasonPoster extends ConsumerWidget {
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 14),
),
),
)
else
Align(
alignment: Alignment.topRight,
child: StatusCard(
color: Theme.of(context).colorScheme.primary,
child: const Icon(
Icons.check_rounded,
),
),
),
],
),
)
else
Align(
alignment: Alignment.topRight,
child: StatusCard(
color: Theme.of(context).colorScheme.primary,
child: const Icon(
Icons.check_rounded,
),
),
],
),
),
LayoutBuilder(
builder: (context, constraints) {
return FlatButton(

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
@ -7,48 +8,44 @@ import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
class SyncButton extends ConsumerStatefulWidget {
class SyncButton extends ConsumerWidget {
final ItemBaseModel item;
final SyncedItem? syncedItem;
final SyncedItem syncedItem;
const SyncButton({required this.item, required this.syncedItem, super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _SyncButtonState();
}
Widget build(BuildContext context, WidgetRef ref) {
final nested = ref.watch(syncedNestedChildrenProvider(syncedItem));
return nested.when(
loading: () => const SizedBox.shrink(),
error: (err, stack) => const SizedBox.shrink(),
data: (children) {
final download = ref.watch(syncDownloadStatusProvider(syncedItem, children));
final status = download?.status ?? TaskStatus.notFound;
final progress = download?.progress ?? 0.0;
class _SyncButtonState extends ConsumerState<SyncButton> {
@override
Widget build(BuildContext context) {
final syncedItem = widget.syncedItem;
final status = syncedItem != null ? ref.watch(syncStatusesProvider(syncedItem, null)).value : null;
final progress = syncedItem != null ? ref.watch(syncDownloadStatusProvider(syncedItem, [])) : null;
return Stack(
alignment: Alignment.center,
children: [
Icon(
syncedItem != null
? status == SyncStatus.partially
? (progress?.progress ?? 0) > 0
? IconsaxPlusLinear.arrow_down
: IconsaxPlusLinear.more_circle
: IconsaxPlusLinear.tick_circle
: IconsaxPlusLinear.arrow_down_2,
color: status?.color,
size: (progress?.progress ?? 0) > 0 ? 16 : null,
),
if ((progress?.progress ?? 0) > 0)
IgnorePointer(
child: SizedBox.fromSize(
return Stack(
alignment: Alignment.center,
children: [
Icon(
status == TaskStatus.notFound
? (progress > 0 ? IconsaxPlusLinear.arrow_down_1 : IconsaxPlusLinear.more_circle)
: status.icon,
color: status.color(context),
size: status == TaskStatus.running && progress > 0 ? 16 : null,
),
SizedBox.fromSize(
size: const Size.fromRadius(10),
child: CircularProgressIndicator(
strokeCap: StrokeCap.round,
strokeWidth: 2,
color: status?.color,
value: progress?.progress,
strokeWidth: 1.5,
color: status.color(context),
value: status == TaskStatus.running ? progress.clamp(0.0, 1.0) : 0,
),
),
)
],
],
);
},
);
}
}

View file

@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/syncing/widgets/synced_season_poster.dart';
import 'widgets/synced_episode_item.dart';
@ -26,8 +25,7 @@ class _ChildSyncWidgetState extends ConsumerState<ChildSyncWidget> {
@override
Widget build(BuildContext context) {
final baseItem = ref.read(syncProvider.notifier).getItem(syncedItem);
final hasFile = syncedItem.videoFile.existsSync();
final baseItem = syncedItem.itemModel;
if (baseItem == null) {
return Container();
}
@ -47,7 +45,6 @@ class _ChildSyncWidgetState extends ConsumerState<ChildSyncWidget> {
EpisodeModel episode => SyncedEpisodeItem(
episode: episode,
syncedItem: syncedItem,
hasFile: hasFile,
),
_ => Container(),
},

View file

@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
@ -13,26 +13,29 @@ import 'package:fladder/screens/shared/default_alert_dialog.dart';
import 'package:fladder/screens/shared/media/poster_widget.dart';
import 'package:fladder/screens/syncing/sync_child_item.dart';
import 'package:fladder/screens/syncing/sync_widgets.dart';
import 'package:fladder/screens/syncing/widgets/sync_options_button.dart';
import 'package:fladder/screens/syncing/widgets/sync_progress_builder.dart';
import 'package:fladder/screens/syncing/widgets/sync_status_overlay.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/refresh_state.dart';
import 'package:fladder/util/size_formatting.dart';
import 'package:fladder/widgets/shared/alert_content.dart';
import 'package:fladder/widgets/shared/icon_button_await.dart';
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
Future<void> showSyncItemDetails(
BuildContext context,
SyncedItem syncItem,
WidgetRef ref,
) {
return showDialogAdaptive(
) async {
await showDialogAdaptive(
context: context,
builder: (context) => SyncItemDetails(
syncItem: syncItem,
),
);
context.refreshData();
}
class SyncItemDetails extends ConsumerStatefulWidget {
@ -48,166 +51,196 @@ class _SyncItemDetailsState extends ConsumerState<SyncItemDetails> {
@override
Widget build(BuildContext context) {
final baseItem = ref.read(syncProvider.notifier).getItem(syncedItem);
final baseItem = syncedItem.itemModel;
final hasFile = syncedItem.videoFile.existsSync();
final syncChildren = ref.read(syncProvider.notifier).getChildren(syncedItem);
final downloadTask = ref.read(downloadTasksProvider(syncedItem.id));
return SyncStatusOverlay(
final downloadTask = ref.watch(downloadTasksProvider(syncedItem.id));
final syncedChildren = ref.watch(syncedChildrenProvider(syncedItem));
final nestedChildren = ref.watch(syncedNestedChildrenProvider(syncedItem));
return PullToRefresh(
refreshOnStart: false,
onRefresh: () async {
final newItem = await ref.read(syncProvider.notifier).refreshSyncItem(syncedItem);
setState(() {
syncedItem = newItem;
});
},
child: SyncStatusOverlay(
syncedItem: syncedItem,
child: ActionContent(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Card(
elevation: 1,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Text(baseItem?.type.label(context) ?? ""),
)),
Text(
context.localized.navigationSync,
style: Theme.of(context).textTheme.titleMedium,
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(IconsaxPlusBold.close_circle),
)
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (baseItem != null) ...{
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: (AdaptiveLayout.poster(context).size *
ref.watch(clientSettingsProvider.select((value) => value.posterSize))) *
0.6,
child: IgnorePointer(
child: PosterWidget(
aspectRatio: 0.7,
poster: baseItem,
inlineTitle: true,
),
),
),
Expanded(
child: SyncProgressBuilder(
item: syncedItem,
children: syncChildren,
builder: (context, combinedStream) {
return Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
baseItem.detailedName(context) ?? "",
style: Theme.of(context).textTheme.titleMedium,
),
SyncSubtitle(syncItem: syncedItem),
SyncLabel(
label: context.localized.totalSize(
ref.watch(syncSizeProvider(syncedItem, syncChildren)).byteFormat ?? '--'),
status: ref.watch(syncStatusesProvider(syncedItem, syncChildren)).value ??
SyncStatus.partially,
),
if (combinedStream?.task != null && combinedStream != null) ...{
SyncProgressBar(item: syncedItem, task: combinedStream)
},
].addInBetween(const SizedBox(height: 8)),
),
),
],
);
},
),
),
if (!hasFile && !downloadTask.hasDownload && syncedItem.hasVideoFile)
IconButtonAwait(
onPressed: () async => await ref.read(syncProvider.notifier).syncFile(syncedItem, false),
icon: const Icon(IconsaxPlusLinear.cloud_change),
)
else if (hasFile)
IconButtonAwait(
color: Theme.of(context).colorScheme.error,
onPressed: () {
showDefaultAlertDialog(
context,
context.localized.syncRemoveDataTitle,
context.localized.syncRemoveDataDesc,
(context) {
ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem, downloadTask.task);
Navigator.of(context).pop();
},
context.localized.delete,
(context) => Navigator.of(context).pop(),
context.localized.cancel,
);
},
icon: const Icon(IconsaxPlusLinear.trash),
),
].addInBetween(const SizedBox(width: 16)),
),
},
const Divider(),
if (syncChildren.isNotEmpty == true)
Flexible(
child: ListView(
shrinkWrap: true,
children: [
...syncChildren.map(
(e) => ChildSyncWidget(syncedChild: e),
),
],
child: switch (syncedChildren) {
AsyncValue<List<SyncedItem>>(value: final children) => ActionContent(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Card(
elevation: 1,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Text(baseItem?.type.label(context) ?? ""),
)),
Text(
context.localized.navigationSync,
style: Theme.of(context).textTheme.titleMedium,
),
),
],
),
actions: [
if (baseItem is! EpisodeModel)
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.errorContainer,
foregroundColor: Theme.of(context).colorScheme.onErrorContainer,
iconColor: Theme.of(context).colorScheme.onErrorContainer,
),
onPressed: () {
showDefaultAlertDialog(
context,
context.localized.syncDeleteItemTitle,
context.localized.syncDeleteItemDesc(baseItem?.detailedName(context) ?? ""),
(context) async {
await ref.read(syncProvider.notifier).removeSync(context, syncedItem);
Navigator.pop(context);
Navigator.pop(context);
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(IconsaxPlusBold.close_circle),
)
],
),
child: ListView(
shrinkWrap: true,
children: [
if (baseItem != null) ...{
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 16,
children: [
SizedBox(
height: (AdaptiveLayout.poster(context).size *
ref.watch(clientSettingsProvider.select((value) => value.posterSize))) *
0.6,
child: IgnorePointer(
child: PosterWidget(
aspectRatio: 0.70,
poster: baseItem,
underTitle: false,
),
),
),
Expanded(
child: switch (nestedChildren) {
AsyncValue<List<SyncedItem>>(:final value) => Builder(
builder: (context) {
final nestedChildren = value ?? [];
return SyncProgressBuilder(
item: syncedItem,
children: nestedChildren,
builder: (context, combinedStream) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
spacing: 4,
children: [
Flexible(
child: Text(
baseItem.detailedName(context) ?? "",
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
),
),
Flexible(
child: SyncSubtitle(
syncItem: syncedItem,
children: nestedChildren,
),
),
Flexible(
child: Consumer(
builder: (context, ref, child) => SyncLabel(
label: context.localized.totalSize(ref
.watch(syncSizeProvider(syncedItem, nestedChildren))
.byteFormat ??
'--'),
status: combinedStream?.status ?? TaskStatus.notFound,
),
),
),
if (combinedStream != null && combinedStream.hasDownload == true)
SyncProgressBar(item: syncedItem, task: combinedStream)
],
);
},
);
},
),
},
),
if (syncedItem.hasVideoFile && !hasFile && !downloadTask.hasDownload)
IconButtonAwait(
onPressed: () async => await ref.read(syncProvider.notifier).syncFile(syncedItem, false),
icon: const Icon(IconsaxPlusLinear.cloud_change),
)
else if (hasFile)
IconButtonAwait(
color: Theme.of(context).colorScheme.error,
onPressed: () {
showDefaultAlertDialog(
context,
context.localized.syncRemoveDataTitle,
context.localized.syncRemoveDataDesc,
(context) {
ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem, downloadTask.task);
Navigator.of(context).pop();
},
context.localized.delete,
(context) => Navigator.of(context).pop(),
context.localized.cancel,
);
},
icon: const Icon(IconsaxPlusLinear.trash),
),
nestedChildren.when(
data: (data) => SyncOptionsButton(
syncedItem: syncedItem,
children: data,
),
error: (error, stackTrace) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
)
],
),
},
if (children?.isNotEmpty == true) ...[
const Divider(),
...children!.map(
(e) => ChildSyncWidget(syncedChild: e),
),
],
],
),
actions: [
if (syncedItem.parentId == null)
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.errorContainer,
foregroundColor: Theme.of(context).colorScheme.onErrorContainer,
iconColor: Theme.of(context).colorScheme.onErrorContainer,
),
onPressed: () {
showDefaultAlertDialog(
context,
context.localized.syncDeleteItemTitle,
context.localized.syncDeleteItemDesc(baseItem?.detailedName(context) ?? ""),
(localContext) async {
await ref.read(syncProvider.notifier).removeSync(context, syncedItem);
Navigator.pop(localContext);
Navigator.pop(context);
},
context.localized.delete,
(context) => Navigator.pop(context),
context.localized.cancel,
);
},
context.localized.delete,
(context) => Navigator.pop(context),
context.localized.cancel,
);
},
child: Text(context.localized.delete),
)
else if (syncedItem.parentId != null)
ElevatedButton(
onPressed: () {
final parentItem = ref.read(syncProvider.notifier).getParentItem(syncedItem.parentId!);
setState(() {
if (parentItem != null) {
syncedItem = parentItem;
}
});
},
child: Text(context.localized.syncOpenParent),
)
],
));
child: Text(context.localized.delete),
)
else if (baseItem?.parentBaseModel != null)
ElevatedButton(
onPressed: () async {
final parentItem = await ref.read(syncProvider.notifier).getSyncedItem(baseItem!.parentBaseModel);
setState(() {
if (parentItem != null) {
syncedItem = parentItem;
}
});
},
child: Text(context.localized.syncOpenParent),
)
],
),
},
),
);
}
}

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
@ -12,7 +13,6 @@ import 'package:fladder/screens/syncing/sync_widgets.dart';
import 'package:fladder/screens/syncing/widgets/sync_progress_builder.dart';
import 'package:fladder/screens/syncing/widgets/sync_status_overlay.dart';
import 'package:fladder/util/fladder_image.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/size_formatting.dart';
@ -28,8 +28,7 @@ class SyncListItemState extends ConsumerState<SyncListItem> {
@override
Widget build(BuildContext context) {
final syncedItem = widget.syncedItem;
final baseItem = ref.read(syncProvider.notifier).getItem(syncedItem);
final children = ref.watch(syncChildrenProvider(syncedItem));
final baseItem = syncedItem.itemModel;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: SyncStatusOverlay(
@ -67,87 +66,99 @@ class SyncListItemState extends ConsumerState<SyncListItem> {
context.localized.cancel);
return false;
},
child: LayoutBuilder(builder: (context, constraints) {
return IntrinsicHeight(
child: InkWell(
onTap: () => baseItem?.navigateTo(context),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
ConstrainedBox(
constraints: BoxConstraints(maxHeight: 125, maxWidth: constraints.maxWidth * 0.2),
child: Card(
child: AspectRatio(
aspectRatio: baseItem?.primaryRatio ?? 1.0,
child: FladderImage(
image: baseItem?.getPosters?.primary,
fit: BoxFit.cover,
)),
),
),
Expanded(
child: SyncProgressBuilder(
item: syncedItem,
children: children,
builder: (context, combinedStream) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
baseItem?.detailedName(context) ?? "",
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
),
),
Flexible(
child: SyncSubtitle(
syncItem: syncedItem,
children: children,
),
),
Flexible(
child: SyncLabel(
label: context.localized.totalSize(
ref.watch(syncSizeProvider(syncedItem, children)).byteFormat ?? '--'),
status: ref.watch(syncStatusesProvider(syncedItem, children)).value ??
SyncStatus.partially,
),
),
if (combinedStream != null && combinedStream.hasDownload == true)
SyncProgressBar(item: syncedItem, task: combinedStream)
].addInBetween(const SizedBox(height: 4)),
);
},
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Card(
elevation: 0,
shadowColor: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: Text(baseItem != null ? baseItem.type.label(context) : ""),
)),
IconButton(
onPressed: () => showSyncItemDetails(context, syncedItem, ref),
icon: const Icon(IconsaxPlusLinear.more_square),
child: LayoutBuilder(
builder: (context, constraints) {
return IntrinsicHeight(
child: InkWell(
onTap: () => baseItem?.navigateTo(context),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 16,
children: [
ConstrainedBox(
constraints: BoxConstraints(maxHeight: 125, maxWidth: constraints.maxWidth * 0.2),
child: Card(
child: AspectRatio(
aspectRatio: baseItem?.primaryRatio ?? 1.0,
child: FladderImage(
image: baseItem?.getPosters?.primary,
fit: BoxFit.cover,
)),
),
],
),
].addInBetween(const SizedBox(width: 16)),
),
Expanded(
child: FutureBuilder(
future: ref.read(syncProvider.notifier).getNestedChildren(syncedItem),
builder: (context, asyncSnapshot) {
final nestedChildren = asyncSnapshot.data ?? [];
return SyncProgressBuilder(
item: syncedItem,
children: nestedChildren,
builder: (context, combinedStream) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
spacing: 4,
children: [
Flexible(
child: Text(
baseItem?.detailedName(context) ?? "",
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
),
),
Flexible(
child: SyncSubtitle(
syncItem: syncedItem,
children: nestedChildren,
),
),
Flexible(
child: Consumer(
builder: (context, ref, child) => SyncLabel(
label: context.localized.totalSize(
ref.watch(syncSizeProvider(syncedItem, nestedChildren)).byteFormat ??
'--'),
status: combinedStream?.status ?? TaskStatus.notFound,
),
),
),
if (combinedStream != null && combinedStream.hasDownload == true)
SyncProgressBar(item: syncedItem, task: combinedStream)
],
);
},
);
},
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Card(
elevation: 0,
shadowColor: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: Text(baseItem != null ? baseItem.type.label(context) : ""),
)),
IconButton(
onPressed: () => showSyncItemDetails(context, syncedItem, ref),
icon: const Icon(IconsaxPlusLinear.more_square),
),
],
),
],
),
),
),
),
);
}),
);
},
),
),
),
),

View file

@ -12,7 +12,6 @@ import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/sync/background_download_provider.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
const _cancellableStatuses = {
@ -24,23 +23,23 @@ const _cancellableStatuses = {
class SyncLabel extends ConsumerWidget {
final String? label;
final SyncStatus status;
final TaskStatus status;
const SyncLabel({this.label, required this.status, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Container(
decoration: BoxDecoration(
color: status.color.withValues(alpha: 0.15),
color: status.color(context).withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
child: Text(
label ?? status.label(context),
label ?? status.name(context),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
color: status.color,
color: status.color(context),
),
),
),
@ -68,6 +67,7 @@ class SyncProgressBar extends ConsumerWidget {
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
Flexible(
child: LinearProgressIndicator(
@ -79,7 +79,7 @@ class SyncProgressBar extends ConsumerWidget {
),
Opacity(opacity: 0.75, child: Text("${(downloadProgress * 100).toStringAsFixed(0)}%")),
if (downloadTask != null) ...{
if (downloadStatus != TaskStatus.paused)
if (downloadStatus != TaskStatus.paused && downloadStatus != TaskStatus.enqueued)
IconButton(
onPressed: () => ref.read(backgroundDownloaderProvider).pause(downloadTask),
icon: const Icon(IconsaxPlusBold.pause),
@ -101,7 +101,7 @@ class SyncProgressBar extends ConsumerWidget {
),
],
},
].addInBetween(const SizedBox(width: 8)),
],
),
const SizedBox(width: 6),
],
@ -120,33 +120,46 @@ class SyncSubtitle extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final baseItem = ref.read(syncProvider.notifier).getItem(syncItem);
final syncStatus = ref.watch(syncStatusesProvider(syncItem, children)).value ?? SyncStatus.partially;
final baseItem = syncItem.itemModel;
final syncStatus = ref
.watch(syncDownloadStatusProvider(syncItem, children).select((value) => value?.status ?? TaskStatus.notFound));
return Container(
decoration:
BoxDecoration(color: syncStatus.color.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10)),
decoration: BoxDecoration(
color: syncStatus.color(context).withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10)),
child: Material(
color: const Color.fromARGB(0, 208, 130, 130),
textStyle:
Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold, color: syncStatus.color),
textStyle: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(fontWeight: FontWeight.bold, color: syncStatus.color(context)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
child: switch (baseItem) {
SeriesModel _ => Builder(
SeasonModel _ => Builder(
builder: (context) {
final itemBaseModels = children.map((e) => ref.read(syncProvider.notifier).getItem(e));
final seriesItemsSyncLeft = children.where((element) => element.taskId != null).length;
final seasons = itemBaseModels.whereType<SeasonModel>().length;
final itemBaseModels = children.map((e) => e.itemModel);
final episodes = itemBaseModels.whereType<EpisodeModel>().length;
return Text(
[
"${context.localized.season(seasons)}: $seasons",
"${context.localized.episode(seasons)}: $episodes | ${context.localized.sync}: ${children.where((element) => element.videoFile.existsSync()).length}${seriesItemsSyncLeft > 0 ? " | Syncing: $seriesItemsSyncLeft" : ""}"
"${context.localized.episode(2)}: $episodes | ${context.localized.syncStatusSynced}: ${children.where((element) => element.videoFile.existsSync()).length}"
].join('\n'),
);
},
),
_ => Text(syncStatus.label(context)),
SeriesModel _ => Builder(
builder: (context) {
final itemBaseModels = children.map((e) => e.itemModel);
final seasons = itemBaseModels.whereType<SeasonModel>().length;
final episodes = itemBaseModels.whereType<EpisodeModel>().length;
return Text(
[
"${context.localized.season(2)}: $seasons",
"${context.localized.episode(2)}: $episodes | ${context.localized.syncStatusSynced}: ${children.where((element) => element.videoFile.existsSync()).length}"
].join('\n'),
);
},
),
_ => Text(syncStatus.name(context)),
},
),
),

View file

@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
@ -52,6 +53,30 @@ class _SyncedScreenState extends ConsumerState<SyncedScreen> {
)
else
const DefaultSliverTopBadding(),
if (kDebugMode)
SliverToBoxAdapter(
child: Padding(
padding: padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 12,
children: [
ElevatedButton(
onPressed: () => ref.read(syncProvider.notifier).viewDatabase(context),
child: const Text("View Database"),
),
ElevatedButton(
onPressed: () => ref.read(syncProvider.notifier).db.clearDatabase(),
child: const Text("Clear drift database"),
),
ElevatedButton(
onPressed: () => ref.read(syncProvider.notifier).migrateFromIsar(),
child: const Text("Migrate Isar to Drift"),
),
],
),
),
),
if (items.isNotEmpty) ...[
SliverToBoxAdapter(
child: Padding(

View file

@ -0,0 +1,139 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/refresh_state.dart';
import 'package:fladder/widgets/shared/filled_button_await.dart';
class SyncOptionsButton extends ConsumerWidget {
final SyncedItem syncedItem;
final List<SyncedItem> children;
const SyncOptionsButton({
required this.syncedItem,
required this.children,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return PopupMenuButton(
itemBuilder: (context) {
final unSyncedChildren = children.where((element) {
final hasDownload = ref.read(syncDownloadStatusProvider(element, []));
return element.hasVideoFile && !element.videoFile.existsSync() && hasDownload?.status == TaskStatus.notFound;
}).toList();
final syncedChildren =
children.where((element) => element.hasVideoFile && element.videoFile.existsSync()).toList();
return [
PopupMenuItem(
child: Row(
spacing: 12,
children: [
const Icon(IconsaxPlusLinear.refresh_2),
Text(context.localized.refreshMetadata),
],
),
onTap: () => context.refreshData(),
),
if (children.isNotEmpty) ...[
PopupMenuItem(
enabled: unSyncedChildren.isNotEmpty,
child: Row(
spacing: 12,
children: [
const Icon(IconsaxPlusLinear.cloud_add),
Text(context.localized.sync),
],
),
onTap: () async => _syncRemainingItems(context, syncedItem, unSyncedChildren, ref),
),
PopupMenuItem(
enabled: syncedChildren.isNotEmpty,
child: Row(
spacing: 12,
children: [
const Icon(IconsaxPlusLinear.trash),
Text(context.localized.delete),
],
),
onTap: () async => _deleteSyncedItems(context, syncedItem, syncedChildren, ref),
)
]
];
},
);
}
}
Future<dynamic> _deleteSyncedItems(
BuildContext context, SyncedItem syncedItem, List<SyncedItem> syncedChildren, WidgetRef ref) {
return showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text(context.localized.syncDeleteAllItemsTitle(syncedItem.itemModel?.name ?? "")),
content: Text(
context.localized.syncDeleteAllItemsDesc(syncedItem.itemModel?.name ?? "", syncedChildren.length),
),
scrollable: true,
actions: [
ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text(context.localized.cancel)),
FilledButtonAwait(
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.errorContainer,
foregroundColor: Theme.of(context).colorScheme.onErrorContainer,
iconColor: Theme.of(context).colorScheme.onErrorContainer,
),
onPressed: () async {
final deleteList = syncedChildren.map((e) => ref.read(syncProvider.notifier).deleteFullSyncFiles(e, null));
await Future.wait(deleteList);
Navigator.of(context).pop();
},
child: Text(
context.localized.delete,
),
)
],
),
);
}
Future<dynamic> _syncRemainingItems(
BuildContext context, SyncedItem syncedItem, List<SyncedItem> unSyncedChildren, WidgetRef ref) {
return showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text(context.localized.syncAllItemsTitle(syncedItem.itemModel?.name ?? "")),
content: Text(
context.localized.syncAllItemsDesc(
syncedItem.itemModel?.name ?? "",
unSyncedChildren.length,
),
),
scrollable: true,
actions: [
ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text(context.localized.cancel)),
FilledButtonAwait(
onPressed: () async {
final syncList = unSyncedChildren.map((e) => ref.read(syncProvider.notifier).syncFile(e, false));
await Future.wait(syncList);
Navigator.of(context).pop();
},
child: Text(
context.localized.sync,
),
)
],
),
);
}

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
@ -22,12 +23,10 @@ class SyncedEpisodeItem extends ConsumerStatefulWidget {
super.key,
required this.episode,
required this.syncedItem,
required this.hasFile,
});
final EpisodeModel episode;
final SyncedItem syncedItem;
final bool hasFile;
@override
ConsumerState<SyncedEpisodeItem> createState() => _SyncedEpisodeItemState();
@ -40,91 +39,94 @@ class _SyncedEpisodeItemState extends ConsumerState<SyncedEpisodeItem> {
final downloadTask = ref.watch(downloadTasksProvider(syncedItem.id));
final hasFile = widget.syncedItem.videoFile.existsSync();
return Row(
children: [
ConstrainedBox(
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.3),
child: FlatButton(
onTap: () {
widget.episode.navigateTo(context);
return context.maybePop();
},
child: SizedBox(
width: 175,
child: EpisodePoster(
episode: widget.episode,
syncedItem: syncedItem,
actions: [],
showLabel: false,
isCurrentEpisode: false,
return IntrinsicHeight(
child: Row(
children: [
ConstrainedBox(
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.3),
child: FlatButton(
onTap: () {
widget.episode.navigateTo(context);
return context.maybePop();
},
child: SizedBox(
width: 175,
child: EpisodePoster(
episode: widget.episode,
actions: [],
showLabel: false,
isCurrentEpisode: false,
),
),
),
),
),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.episode.name,
style: Theme.of(context).textTheme.titleMedium,
),
Opacity(
opacity: 0.75,
child: Text(
widget.episode.seasonEpisodeLabel(context),
style: Theme.of(context).textTheme.bodyLarge,
Expanded(
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.episode.name,
style: Theme.of(context).textTheme.titleMedium,
),
),
],
),
),
if (!widget.hasFile && downloadTask.hasDownload)
Flexible(
child: SyncProgressBar(item: syncedItem, task: downloadTask),
)
else
Flexible(
child: SyncLabel(
label: context.localized.totalSize(ref.watch(syncSizeProvider(syncedItem, [])).byteFormat ?? '--'),
status: ref.watch(syncStatusesProvider(syncedItem, [])).value ?? SyncStatus.partially,
Opacity(
opacity: 0.75,
child: Text(
widget.episode.seasonEpisodeLabel(context),
style: Theme.of(context).textTheme.bodyLarge,
),
),
],
),
)
],
),
if (!hasFile && downloadTask.hasDownload)
Flexible(
child: SyncProgressBar(item: syncedItem, task: downloadTask),
)
else
Flexible(
child: SyncLabel(
label:
context.localized.totalSize(ref.watch(syncSizeProvider(syncedItem, [])).byteFormat ?? '--'),
status: ref.watch(syncDownloadStatusProvider(syncedItem, [])
.select((value) => value?.status ?? TaskStatus.notFound)),
),
)
],
),
),
),
if (!hasFile && !downloadTask.hasDownload)
IconButtonAwait(
onPressed: () async => await ref.read(syncProvider.notifier).syncFile(syncedItem, false),
icon: const Icon(IconsaxPlusLinear.cloud_change),
)
else if (hasFile)
IconButtonAwait(
color: Theme.of(context).colorScheme.error,
onPressed: () async {
await showDefaultAlertDialog(
context,
context.localized.syncRemoveDataTitle,
context.localized.syncRemoveDataDesc,
(context) async {
await ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem, downloadTask.task);
Navigator.pop(context);
},
context.localized.delete,
(context) => Navigator.pop(context),
context.localized.cancel,
);
},
icon: const Icon(IconsaxPlusLinear.trash),
)
].addInBetween(const SizedBox(width: 16)),
if (!hasFile && !downloadTask.hasDownload)
IconButtonAwait(
onPressed: () async => await ref.read(syncProvider.notifier).syncFile(syncedItem, false),
icon: const Icon(IconsaxPlusLinear.cloud_change),
)
else if (hasFile)
IconButtonAwait(
color: Theme.of(context).colorScheme.error,
onPressed: () async {
await showDefaultAlertDialog(
context,
context.localized.syncRemoveDataTitle,
context.localized.syncRemoveDataDesc,
(context) async {
await ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem, downloadTask.task);
Navigator.pop(context);
},
context.localized.delete,
(context) => Navigator.pop(context),
context.localized.cancel,
);
},
icon: const Icon(IconsaxPlusLinear.trash),
)
].addInBetween(const SizedBox(width: 16)),
),
);
}
}

View file

@ -1,17 +1,21 @@
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/syncing/sync_widgets.dart';
import 'package:fladder/screens/syncing/widgets/sync_options_button.dart';
import 'package:fladder/screens/syncing/widgets/sync_progress_builder.dart';
import 'package:fladder/screens/syncing/widgets/synced_episode_item.dart';
import 'package:fladder/util/fladder_image.dart';
import 'package:fladder/widgets/shared/icon_button_await.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/size_formatting.dart';
class SyncedSeasonPoster extends ConsumerStatefulWidget {
const SyncedSeasonPoster({
@ -32,72 +36,96 @@ class _SyncedSeasonPosterState extends ConsumerState<SyncedSeasonPoster> {
@override
Widget build(BuildContext context) {
final season = widget.season;
final children = ref.read(syncProvider.notifier).getChildren(widget.syncedItem);
final unSyncedChildren = children.where((child) => child.status == SyncStatus.partially).toList();
return ExpansionTile(
tilePadding: EdgeInsets.zero,
title: Row(
spacing: 6,
children: [
SizedBox(
width: 75,
child: AspectRatio(
aspectRatio: 0.65,
child: FlatButton(
onTap: () {
season.navigateTo(context);
return context.maybePop();
},
child: Card(
child: FladderImage(
image: season.getPosters?.primary ??
season.parentImages?.backDrop?.firstOrNull ??
season.parentImages?.primary,
final nestedChildren = ref.watch(syncedNestedChildrenProvider(widget.syncedItem));
return nestedChildren.when(
data: (children) => Builder(
builder: (context) {
final syncedItem = widget.syncedItem;
return ExpansionTile(
tilePadding: EdgeInsets.zero,
shape: const Border(),
title: Row(
spacing: 12,
children: [
SizedBox(
width: 75,
child: AspectRatio(
aspectRatio: 0.65,
child: FlatButton(
onTap: () {
season.navigateTo(context);
return context.maybePop();
},
child: Card(
child: FladderImage(
image: season.getPosters?.primary ??
season.parentImages?.backDrop?.firstOrNull ??
season.parentImages?.primary,
),
),
),
),
),
),
Flexible(
child: SyncProgressBuilder(
item: syncedItem,
children: children,
builder: (context, combinedStream) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
spacing: 4,
children: [
Flexible(
child: Text(
season.name,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
),
),
Flexible(
child: SyncSubtitle(
syncItem: syncedItem,
children: children,
),
),
Flexible(
child: Consumer(
builder: (context, ref, child) => SyncLabel(
label: context.localized
.totalSize(ref.watch(syncSizeProvider(syncedItem, children))?.byteFormat ?? '--'),
status: combinedStream?.status ?? TaskStatus.notFound,
),
),
),
if (combinedStream != null && combinedStream.hasDownload == true)
SyncProgressBar(item: syncedItem, task: combinedStream)
],
);
},
),
),
],
),
),
Column(
children: [
Text(
season.name,
style: Theme.of(context).textTheme.titleMedium,
)
],
),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (unSyncedChildren.isNotEmpty)
IconButtonAwait(
onPressed: () async {
for (var i = 0; i < unSyncedChildren.length; i++) {
final childSyncedItem = unSyncedChildren[i];
await ref.read(syncProvider.notifier).syncFile(childSyncedItem, false);
}
trailing: SyncOptionsButton(syncedItem: syncedItem, children: children),
children: children.map(
(item) {
final baseItem = item.itemModel;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: SyncedEpisodeItem(
episode: baseItem as EpisodeModel,
syncedItem: item,
),
);
},
icon: const Icon(IconsaxPlusLinear.cloud_change),
),
],
),
children: children.map(
(item) {
final baseItem = ref.read(syncProvider.notifier).getItem(item);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: IntrinsicHeight(
child: SyncedEpisodeItem(
episode: baseItem as EpisodeModel,
syncedItem: item,
hasFile: item.videoFile.existsSync(),
),
),
).toList(),
);
},
).toList(),
),
error: (error, stackTrace) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
);
}
}