Init repo

This commit is contained in:
PartyDonut 2024-09-15 14:12:28 +02:00
commit 764b6034e3
566 changed files with 212335 additions and 0 deletions

View file

@ -0,0 +1,71 @@
import 'package:ficonsax/ficonsax.dart';
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';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/shared/default_alert_dialog.dart';
import 'package:fladder/screens/syncing/sync_item_details.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SyncButton extends ConsumerStatefulWidget {
final ItemBaseModel item;
final SyncedItem? syncedItem;
const SyncButton({required this.item, required this.syncedItem, super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _SyncButtonState();
}
class _SyncButtonState extends ConsumerState<SyncButton> {
@override
Widget build(BuildContext context) {
final syncedItem = widget.syncedItem;
final status = syncedItem != null ? ref.watch(syncStatusesProvider(syncedItem)).value : null;
final progress = syncedItem != null ? ref.watch(syncDownloadStatusProvider(syncedItem)) : null;
return Stack(
alignment: Alignment.center,
children: [
InkWell(
onTap: syncedItem != null
? () => showSyncItemDetails(context, syncedItem, ref)
: () => showDefaultActionDialog(
context,
'Sync ${widget.item.detailedName}?',
null,
(context) async {
await ref.read(syncProvider.notifier).addSyncItem(context, widget.item);
Navigator.of(context).pop();
},
"Sync",
(context) => Navigator.of(context).pop(),
"Cancel",
),
child: Icon(
syncedItem != null
? status == SyncStatus.partially
? (progress?.progress ?? 0) > 0
? IconsaxOutline.arrow_down
: IconsaxOutline.more_circle
: IconsaxOutline.tick_circle
: IconsaxOutline.arrow_down_2,
color: status?.color,
size: (progress?.progress ?? 0) > 0 ? 16 : null,
),
),
if ((progress?.progress ?? 0) > 0)
IgnorePointer(
child: SizedBox.fromSize(
size: Size.fromRadius(10),
child: CircularProgressIndicator(
strokeCap: StrokeCap.round,
strokeWidth: 2,
color: status?.color,
value: progress?.progress,
),
),
)
],
);
}
}

View file

@ -0,0 +1,66 @@
import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/screens/syncing/widgets/synced_season_poster.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'widgets/synced_episode_item.dart';
class ChildSyncWidget extends ConsumerStatefulWidget {
final SyncedItem syncedChild;
const ChildSyncWidget({
required this.syncedChild,
super.key,
});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _ChildSyncWidgetState();
}
class _ChildSyncWidgetState extends ConsumerState<ChildSyncWidget> {
late SyncedItem syncedItem = widget.syncedChild;
@override
Widget build(BuildContext context) {
final baseItem = ref.read(syncProvider.notifier).getItem(syncedItem);
final hasFile = syncedItem.videoFile.existsSync();
if (baseItem == null) {
return Container();
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Card(
child: InkWell(
onTap: () {
Navigator.of(context).pop();
baseItem.navigateTo(context);
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Flexible(
child: switch (baseItem) {
SeasonModel season => SyncedSeasonPoster(
syncedItem: syncedItem,
season: season,
),
EpisodeModel episode => SyncedEpisodeItem(
episode: episode,
syncedItem: syncedItem,
hasFile: hasFile,
),
_ => Container(),
},
),
],
),
),
),
),
);
}
}

View file

@ -0,0 +1,256 @@
import 'package:background_downloader/background_downloader.dart';
import 'package:ficonsax/ficonsax.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/background_download_provider.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/shared/adaptive_dialog.dart';
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_markedfordelete.dart';
import 'package:fladder/screens/syncing/widgets/sync_progress_builder.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/size_formatting.dart';
import 'package:fladder/widgets/shared/icon_button_await.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Future<void> showSyncItemDetails(
BuildContext context,
SyncedItem syncItem,
WidgetRef ref,
) {
return showDialogAdaptive(
context: context,
useSafeArea: false,
builder: (context) => SyncItemDetails(
syncItem: syncItem,
),
);
}
class SyncItemDetails extends ConsumerStatefulWidget {
final SyncedItem syncItem;
const SyncItemDetails({required this.syncItem, super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _SyncItemDetailsState();
}
class _SyncItemDetailsState extends ConsumerState<SyncItemDetails> {
late SyncedItem syncedItem = widget.syncItem;
@override
Widget build(BuildContext context) {
final baseItem = ref.read(syncProvider.notifier).getItem(syncedItem);
final hasFile = syncedItem.videoFile.existsSync();
final syncChildren = ref.read(syncProvider.notifier).getChildren(syncedItem);
final downloadTask = ref.read(downloadTasksProvider(syncedItem.id));
return SyncMarkedForDelete(
syncedItem: syncedItem,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Card(
elevation: 1,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(baseItem?.type.label(context) ?? ""),
)),
Text(
context.localized.navigationSync,
style: Theme.of(context).textTheme.titleMedium,
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: Icon(IconsaxBold.close_circle),
)
],
),
if (baseItem != null) ...{
Divider(),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: 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,
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)).byteFormat ?? '--'),
status: ref.watch(syncStatusesProvider(syncedItem)).value ?? SyncStatus.partially,
),
].addInBetween(const SizedBox(height: 8)),
),
),
if (combinedStream?.task != null) ...{
if (combinedStream?.status != TaskStatus.paused)
IconButton(
onPressed: () =>
ref.read(backgroundDownloaderProvider).pause(combinedStream!.task!),
icon: Icon(IconsaxBold.pause),
),
if (combinedStream?.status == TaskStatus.paused) ...[
IconButton(
onPressed: () =>
ref.read(backgroundDownloaderProvider).resume(combinedStream!.task!),
icon: Icon(IconsaxBold.play),
),
IconButton(
onPressed: () => ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem),
icon: Icon(IconsaxBold.stop),
),
],
const SizedBox(width: 16)
},
if (combinedStream != null && combinedStream.hasDownload)
SizedBox.fromSize(
size: Size.fromRadius(35),
child: Stack(
fit: StackFit.expand,
alignment: Alignment.center,
children: [
CircularProgressIndicator(
value: combinedStream.progress,
strokeWidth: 8,
backgroundColor: Theme.of(context).colorScheme.surface.withOpacity(0.5),
strokeCap: StrokeCap.round,
color: combinedStream.status.color(context),
),
Center(child: Text("${((combinedStream.progress) * 100).toStringAsFixed(0)}%"))
],
)),
],
);
},
),
),
if (!hasFile && !downloadTask.hasDownload && syncedItem.hasVideoFile)
IconButtonAwait(
onPressed: () async => await ref.read(syncProvider.notifier).syncVideoFile(syncedItem, false),
icon: Icon(IconsaxOutline.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);
Navigator.of(context).pop();
},
context.localized.delete,
(context) => Navigator.of(context).pop(),
context.localized.cancel,
);
},
icon: Icon(IconsaxOutline.trash),
),
].addInBetween(const SizedBox(width: 16)),
),
),
},
Divider(),
if (syncChildren.isNotEmpty == true)
Flexible(
child: ListView(
shrinkWrap: true,
children: [
...syncChildren.map(
(e) => ChildSyncWidget(syncedChild: e),
),
],
),
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (baseItem is! EpisodeModel)
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.errorContainer,
foregroundColor: 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(syncedItem);
Navigator.pop(context);
Navigator.pop(context);
},
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),
)
],
),
)
],
),
),
);
}
}

View file

@ -0,0 +1,148 @@
import 'package:ficonsax/ficonsax.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/screens/shared/default_alert_dialog.dart';
import 'package:fladder/screens/syncing/sync_item_details.dart';
import 'package:fladder/screens/syncing/sync_widgets.dart';
import 'package:fladder/screens/syncing/widgets/sync_markedfordelete.dart';
import 'package:fladder/screens/syncing/widgets/sync_progress_builder.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';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SyncListItem extends ConsumerStatefulWidget {
final SyncedItem syncedItem;
const SyncListItem({required this.syncedItem, super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => SyncListItemState();
}
class SyncListItemState extends ConsumerState<SyncListItem> {
@override
Widget build(BuildContext context) {
final syncedItem = widget.syncedItem;
final baseItem = ref.read(syncProvider.notifier).getItem(syncedItem);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: SyncMarkedForDelete(
syncedItem: syncedItem,
child: Card(
elevation: 1,
color: Theme.of(context).colorScheme.secondaryContainer.withOpacity(0.2),
shadowColor: Colors.transparent,
child: Dismissible(
background: Container(
color: Theme.of(context).colorScheme.errorContainer,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [Icon(IconsaxBold.trash)],
),
),
),
key: Key(syncedItem.id),
direction: DismissDirection.startToEnd,
confirmDismiss: (direction) async {
await showDefaultAlertDialog(
context,
context.localized.deleteItem(baseItem?.detailedName(context) ?? ""),
context.localized.syncDeletePopupPermanent,
(context) async {
ref.read(syncProvider.notifier).removeSync(syncedItem);
Navigator.of(context).pop();
return true;
},
context.localized.delete,
(context) async {
Navigator.of(context).pop();
},
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,
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),
),
Flexible(
child: SyncLabel(
label: context.localized
.totalSize(ref.watch(syncSizeProvider(syncedItem)).byteFormat ?? '--'),
status: ref.watch(syncStatusesProvider(syncedItem)).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: Icon(IconsaxOutline.more_square),
),
],
),
].addInBetween(SizedBox(width: 16)),
),
),
),
);
}),
),
),
),
);
}
}

View file

@ -0,0 +1,138 @@
import 'package:background_downloader/background_downloader.dart';
import 'package:ficonsax/ficonsax.dart';
import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/models/items/series_model.dart';
import 'package:fladder/models/syncing/download_stream.dart';
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';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SyncLabel extends ConsumerWidget {
final String? label;
final SyncStatus status;
const SyncLabel({this.label, required this.status, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Container(
decoration: BoxDecoration(
color: status.color.withOpacity(0.15),
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
child: Text(
label ?? status.label,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
color: status.color,
),
),
),
);
}
}
class SyncProgressBar extends ConsumerWidget {
final SyncedItem item;
final DownloadStream task;
const SyncProgressBar({required this.item, required this.task, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final downloadStatus = task.status;
final downloadProgress = task.progress;
final downloadTask = task.task;
if (!task.hasDownload) {
return SizedBox.shrink();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(downloadStatus.name),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: LinearProgressIndicator(
minHeight: 8,
value: downloadProgress,
color: downloadStatus.color(context),
borderRadius: BorderRadius.circular(8),
),
),
Opacity(opacity: 0.75, child: Text("${(downloadProgress * 100).toStringAsFixed(0)}%")),
if (downloadTask != null) ...{
if (downloadStatus != TaskStatus.paused)
IconButton(
onPressed: () => ref.read(backgroundDownloaderProvider).pause(downloadTask),
icon: Icon(IconsaxBold.pause),
)
},
if (downloadStatus == TaskStatus.paused && downloadTask != null) ...[
IconButton(
onPressed: () => ref.read(backgroundDownloaderProvider).resume(downloadTask),
icon: Icon(IconsaxBold.play),
),
IconButton(
onPressed: () => ref.read(syncProvider.notifier).deleteFullSyncFiles(item),
icon: Icon(IconsaxBold.stop),
)
],
].addInBetween(SizedBox(width: 8)),
),
const SizedBox(width: 6),
],
);
}
}
class SyncSubtitle extends ConsumerWidget {
final SyncedItem syncItem;
const SyncSubtitle({
required this.syncItem,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final baseItem = ref.read(syncProvider.notifier).getItem(syncItem);
final children = syncItem.nestedChildren(ref);
final syncStatus = ref.watch(syncStatusesProvider(syncItem)).value ?? SyncStatus.partially;
return Container(
decoration: BoxDecoration(color: syncStatus.color.withOpacity(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),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
child: switch (baseItem) {
SeriesModel _ => 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 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" : ""}"
].join('\n'),
);
},
),
_ => Text(syncStatus.label),
},
),
),
);
}
}

View file

@ -0,0 +1,91 @@
import 'package:ficonsax/ficonsax.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/routes/build_routes/home_routes.dart';
import 'package:fladder/screens/shared/nested_scaffold.dart';
import 'package:fladder/screens/shared/nested_sliver_appbar.dart';
import 'package:fladder/screens/syncing/sync_list_item.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/widgets/shared/pinch_poster_zoom.dart';
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/util/sliver_list_padding.dart';
class SyncedScreen extends ConsumerStatefulWidget {
final ScrollController navigationScrollController;
const SyncedScreen({required this.navigationScrollController, super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _SyncedScreenState();
}
class _SyncedScreenState extends ConsumerState<SyncedScreen> {
@override
Widget build(BuildContext context) {
final items = ref.watch(syncProvider.select((value) => value.items));
return PullToRefresh(
refreshOnStart: true,
onRefresh: () => ref.read(syncProvider.notifier).refresh(),
child: NestedScaffold(
body: PinchPosterZoom(
scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference / 2),
child: items.isNotEmpty
? CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
controller: widget.navigationScrollController,
slivers: [
if (AdaptiveLayout.of(context).layout == LayoutState.phone)
NestedSliverAppBar(
searchTitle: "${context.localized.search} ...",
parent: context,
route: LibrarySearchRoute(),
)
else
const DefaultSliverTopBadding(),
SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text(
context.localized.syncedItems,
style: Theme.of(context).textTheme.titleLarge,
),
),
),
SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 16),
sliver: SliverList.builder(
itemBuilder: (context, index) {
final item = items[index];
return SyncListItem(syncedItem: item);
},
itemCount: items.length,
),
),
const DefautlSliverBottomPadding(),
],
)
: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.localized.noItemsSynced,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(width: 16),
Icon(
IconsaxOutline.cloud_cross,
)
],
),
),
),
),
);
}
}

View file

@ -0,0 +1,41 @@
import 'package:ficonsax/ficonsax.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SyncMarkedForDelete extends ConsumerWidget {
final SyncedItem syncedItem;
final Widget child;
const SyncMarkedForDelete({required this.syncedItem, required this.child, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Stack(
children: [
child,
if (syncedItem.markedForDelete)
Positioned.fill(
child: Card(
elevation: 0,
semanticContainer: false,
color: Colors.black.withOpacity(0.6),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CircularProgressIndicator.adaptive(
strokeCap: StrokeCap.round,
valueColor: AlwaysStoppedAnimation(Theme.of(context).colorScheme.error),
),
Text("Deleting"),
Icon(IconsaxOutline.trash)
].addPadding(EdgeInsets.symmetric(horizontal: 16)),
),
),
)
],
);
}
}

View file

@ -0,0 +1,17 @@
import 'package:fladder/models/syncing/download_stream.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SyncProgressBuilder extends ConsumerWidget {
final SyncedItem item;
final Widget Function(BuildContext context, DownloadStream? combinedStream) builder;
const SyncProgressBuilder({required this.item, required this.builder, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final syncStatus = ref.watch(syncDownloadStatusProvider(item));
return builder(context, syncStatus);
}
}

View file

@ -0,0 +1,122 @@
import 'package:ficonsax/ficonsax.dart';
import 'package:fladder/models/items/episode_model.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/screens/shared/default_alert_dialog.dart';
import 'package:fladder/screens/shared/media/episode_posters.dart';
import 'package:fladder/screens/syncing/sync_widgets.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/size_formatting.dart';
import 'package:fladder/widgets/shared/icon_button_await.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SyncedEpisodeItem extends ConsumerStatefulWidget {
const SyncedEpisodeItem({
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();
}
class _SyncedEpisodeItemState extends ConsumerState<SyncedEpisodeItem> {
late SyncedItem syncedItem = widget.syncedItem;
@override
Widget build(BuildContext context) {
final downloadTask = ref.watch(downloadTasksProvider(syncedItem.id));
final hasFile = widget.syncedItem.videoFile.existsSync();
return Row(
children: [
IgnorePointer(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.3),
child: SizedBox(
width: 250,
child: EpisodePoster(
episode: widget.episode,
syncedItem: syncedItem,
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,
),
),
],
),
),
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,
),
)
],
),
),
if (!hasFile && !downloadTask.hasDownload)
IconButtonAwait(
onPressed: () async => await ref.read(syncProvider.notifier).syncVideoFile(syncedItem, false),
icon: Icon(IconsaxOutline.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);
Navigator.pop(context);
},
context.localized.delete,
(context) => Navigator.pop(context),
context.localized.cancel,
);
},
icon: Icon(IconsaxOutline.trash),
)
].addInBetween(const SizedBox(width: 16)),
);
}
}

View file

@ -0,0 +1,95 @@
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/shared/animated_fade_size.dart';
import 'package:fladder/screens/syncing/widgets/synced_episode_item.dart';
import 'package:fladder/util/fladder_image.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SyncedSeasonPoster extends ConsumerStatefulWidget {
const SyncedSeasonPoster({
super.key,
required this.syncedItem,
required this.season,
});
final SyncedItem syncedItem;
final SeasonModel season;
@override
ConsumerState<SyncedSeasonPoster> createState() => _SyncedSeasonPosterState();
}
class _SyncedSeasonPosterState extends ConsumerState<SyncedSeasonPoster> {
bool expanded = false;
@override
Widget build(BuildContext context) {
final season = widget.season;
final children = ref.read(syncProvider.notifier).getChildren(widget.syncedItem);
return Column(
children: [
Row(
children: [
SizedBox(
width: 125,
child: AspectRatio(
aspectRatio: 0.65,
child: Card(
child: FladderImage(
image: season.getPosters?.primary ??
season.parentImages?.backDrop?.firstOrNull ??
season.parentImages?.primary,
),
),
),
),
Column(
children: [
Text(
season.name,
style: Theme.of(context).textTheme.titleMedium,
)
],
),
Spacer(),
IconButton(
onPressed: () {
setState(() {
expanded = !expanded;
});
},
icon: Icon(!expanded ? Icons.keyboard_arrow_down_rounded : Icons.keyboard_arrow_up_rounded),
)
].addPadding(EdgeInsets.symmetric(horizontal: 6)),
),
AnimatedFadeSize(
duration: const Duration(milliseconds: 250),
child: expanded && children.isNotEmpty
? ListView(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
children: <Widget>[
Divider(),
...children.map(
(item) {
final baseItem = ref.read(syncProvider.notifier).getItem(item);
return IntrinsicHeight(
child: SyncedEpisodeItem(
episode: baseItem as EpisodeModel,
syncedItem: item,
hasFile: item.videoFile.existsSync(),
),
);
},
)
].addPadding(EdgeInsets.symmetric(vertical: 10)),
)
: Container(),
)
].addPadding(EdgeInsets.only(top: 10, bottom: expanded ? 10 : 0)),
);
}
}