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,250 @@
import 'package:ficonsax/ficonsax.dart';
import 'package:fladder/models/book_model.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/photos_model.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/collections/add_to_collection.dart';
import 'package:fladder/screens/metadata/edit_item.dart';
import 'package:fladder/screens/metadata/identifty_screen.dart';
import 'package:fladder/screens/metadata/info_screen.dart';
import 'package:fladder/screens/playlists/add_to_playlists.dart';
import 'package:fladder/screens/metadata/refresh_metadata.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/screens/syncing/sync_button.dart';
import 'package:fladder/screens/syncing/sync_item_details.dart';
import 'package:fladder/util/item_base_model/play_item_helpers.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/refresh_state.dart';
import 'package:fladder/widgets/pop_up/delete_file.dart';
import 'package:fladder/widgets/shared/item_actions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
extension ItemBaseModelsBooleans on List<ItemBaseModel> {
Map<FladderItemType, List<ItemBaseModel>> get groupedItems {
Map<FladderItemType, List<ItemBaseModel>> groupedItems = {};
for (int i = 0; i < length; i++) {
FladderItemType type = this[i].type;
if (!groupedItems.containsKey(type)) {
groupedItems[type] = [this[i]];
} else {
groupedItems[type]?.add(this[i]);
}
}
return groupedItems;
}
}
enum ItemActions {
play,
openShow,
openParent,
details,
showAlbum,
playFromStart,
addCollection,
addPlaylist,
markPlayed,
markUnplayed,
setFavorite,
refreshMetaData,
editMetaData,
mediaInfo,
identify,
download,
}
extension ItemBaseModelExtensions on ItemBaseModel {
List<ItemAction> generateActions(
BuildContext context,
WidgetRef ref, {
List<ItemAction> otherActions = const [],
Set<ItemActions> exclude = const {},
Function(UserData? newData)? onUserDataChanged,
Function(ItemBaseModel item)? onItemUpdated,
Function(ItemBaseModel item)? onDeleteSuccesFully,
}) {
final isAdmin = ref.read(userProvider)?.policy?.isAdministrator ?? false;
final downloadEnabled = ref.read(userProvider.select(
(value) => value?.canDownload ?? false,
)) &&
syncAble &&
(canDownload ?? false);
final syncedItem = ref.read(syncProvider.notifier).getSyncedItem(this);
return [
if (!exclude.contains(ItemActions.play))
if (playAble)
ItemActionButton(
action: () => play(context, ref),
icon: Icon(IconsaxOutline.play),
label: Text(playButtonLabel(context)),
),
if (parentId?.isNotEmpty == true) ...[
if (!exclude.contains(ItemActions.openShow) && this is EpisodeModel)
ItemActionButton(
icon: Icon(FladderItemType.series.icon),
action: () => parentBaseModel.navigateTo(context),
label: Text(context.localized.openShow),
),
if (!exclude.contains(ItemActions.openParent) && this is! EpisodeModel && !galleryItem)
ItemActionButton(
icon: Icon(FladderItemType.folder.icon),
action: () => parentBaseModel.navigateTo(context),
label: Text(context.localized.openParent),
),
],
if (!galleryItem && !exclude.contains(ItemActions.details))
ItemActionButton(
action: () async => await navigateTo(context),
icon: Icon(IconsaxOutline.main_component),
label: Text(context.localized.showDetails),
)
else if (!exclude.contains(ItemActions.showAlbum) && galleryItem)
ItemActionButton(
icon: Icon(FladderItemType.photoalbum.icon),
action: () => (this as PhotoModel).navigateToAlbum(context),
label: Text(context.localized.showAlbum),
),
if (!exclude.contains(ItemActions.playFromStart))
if ((userData.progress) > 0)
ItemActionButton(
icon: Icon(IconsaxOutline.refresh),
action: (this is BookModel)
? () => ((this as BookModel).play(context, ref, currentPage: 0))
: () => play(context, ref, startPosition: Duration.zero),
label: Text((this is BookModel)
? context.localized.readFromStart(name)
: context.localized.playFromStart(subTextShort(context) ?? name)),
),
ItemActionDivider(),
if (!exclude.contains(ItemActions.addCollection))
if (type != FladderItemType.boxset)
ItemActionButton(
icon: Icon(IconsaxOutline.archive_add),
action: () async {
await addItemToCollection(context, [this]);
if (context.mounted) {
context.refreshData();
}
},
label: Text(context.localized.addToCollection),
),
if (!exclude.contains(ItemActions.addPlaylist))
if (type != FladderItemType.playlist)
ItemActionButton(
icon: Icon(IconsaxOutline.archive_add),
action: () async {
await addItemToPlaylist(context, [this]);
if (context.mounted) {
context.refreshData();
}
},
label: Text(context.localized.addToPlaylist),
),
if (!exclude.contains(ItemActions.markPlayed))
ItemActionButton(
icon: Icon(IconsaxOutline.eye),
action: () async {
final userData = await ref.read(userProvider.notifier).markAsPlayed(true, id);
onUserDataChanged?.call(userData?.bodyOrThrow);
context.refreshData();
},
label: Text(context.localized.markAsWatched),
),
if (!exclude.contains(ItemActions.markUnplayed))
ItemActionButton(
icon: Icon(IconsaxOutline.eye_slash),
label: Text(context.localized.markAsUnwatched),
action: () async {
final userData = await ref.read(userProvider.notifier).markAsPlayed(false, id);
onUserDataChanged?.call(userData?.bodyOrThrow);
context.refreshData();
},
),
if (!exclude.contains(ItemActions.setFavorite))
ItemActionButton(
icon: Icon(userData.isFavourite ? IconsaxOutline.heart_remove : IconsaxOutline.heart_add),
action: () async {
final newData = await ref.read(userProvider.notifier).setAsFavorite(!userData.isFavourite, id);
onUserDataChanged?.call(newData?.bodyOrThrow);
context.refreshData();
},
label: Text(userData.isFavourite ? context.localized.removeAsFavorite : context.localized.addAsFavorite),
),
...otherActions,
ItemActionDivider(),
if (!exclude.contains(ItemActions.editMetaData) && isAdmin)
ItemActionButton(
icon: Icon(IconsaxOutline.edit),
action: () async {
final newItem = await showEditItemPopup(context, id);
if (newItem != null) {
onItemUpdated?.call(newItem);
}
},
label: Text(context.localized.editMetadata),
),
if (!exclude.contains(ItemActions.refreshMetaData) && isAdmin)
ItemActionButton(
icon: Icon(IconsaxOutline.global_refresh),
action: () async {
showRefreshPopup(context, id, detailedName(context) ?? name);
},
label: Text(context.localized.refreshMetadata),
),
if (!exclude.contains(ItemActions.download) && downloadEnabled) ...{
if (syncedItem == null)
ItemActionButton(
icon: Icon(IconsaxOutline.arrow_down_2),
label: Text(context.localized.sync),
action: () => ref.read(syncProvider.notifier).addSyncItem(context, this),
)
else
ItemActionButton(
icon: IgnorePointer(child: SyncButton(item: this, syncedItem: syncedItem)),
action: () => showSyncItemDetails(context, syncedItem, ref),
label: Text(context.localized.syncDetails),
)
},
if (canDelete == true)
ItemActionButton(
icon: Container(
child: Icon(
IconsaxOutline.trash,
),
),
action: () async {
final response = await showDeleteDialog(context, this, ref);
if (response?.isSuccessful == true) {
onDeleteSuccesFully?.call(this);
if (context.mounted) {
context.refreshData();
}
} else {
fladderSnackbarResponse(context, response);
}
},
label: Text(context.localized.delete),
),
if (!exclude.contains(ItemActions.identify) && identifiable && isAdmin)
ItemActionButton(
icon: Icon(IconsaxOutline.search_normal),
action: () async {
showIdentifyScreen(context, this);
},
label: Text(context.localized.identify),
),
if (!exclude.contains(ItemActions.mediaInfo))
ItemActionButton(
icon: Icon(IconsaxOutline.info_circle),
action: () async {
showInfoScreen(context, this);
},
label: Text("${type.label(context)} ${context.localized.info}"),
),
];
}
}

View file

@ -0,0 +1,340 @@
import 'package:collection/collection.dart';
import 'package:fladder/models/book_model.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/photos_model.dart';
import 'package:fladder/models/media_playback_model.dart';
import 'package:fladder/models/playback/playback_model.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/models/video_stream_model.dart';
import 'package:fladder/providers/api_provider.dart';
import 'package:fladder/providers/book_viewer_provider.dart';
import 'package:fladder/providers/items/book_details_provider.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/screens/book_viewer/book_viewer_screen.dart';
import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart';
import 'package:fladder/screens/shared/adaptive_dialog.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/screens/video_player/video_player.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/list_extensions.dart';
import 'package:fladder/util/refresh_state.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:window_manager/window_manager.dart';
Future<void> _showLoadingIndicator(BuildContext context) async {
return showDialog(
barrierDismissible: kDebugMode,
useRootNavigator: true,
context: context,
builder: (context) => const LoadIndicator(),
);
}
class LoadIndicator extends StatelessWidget {
const LoadIndicator({super.key});
@override
Widget build(BuildContext context) {
return Dialog(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 32),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(strokeCap: StrokeCap.round),
const SizedBox(width: 70),
Text(
"Loading",
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(width: 20),
],
),
),
);
}
}
Future<void> _playVideo(
BuildContext context, {
required PlaybackModel? current,
Duration? startPosition,
List<ItemBaseModel>? queue,
required WidgetRef ref,
VoidCallback? onPlayerExit,
}) async {
if (current == null) {
if (context.mounted) {
Navigator.of(context, rootNavigator: true).pop();
fladderSnackbar(context, title: "No video found to play");
}
return;
}
final loadedCorrectly = await ref.read(videoPlayerProvider.notifier).loadPlaybackItem(
current,
startPosition: startPosition,
);
if (!loadedCorrectly) {
if (context.mounted) {
Navigator.of(context, rootNavigator: true).pop();
fladderSnackbar(context, title: "An error occurred loading media");
}
return;
}
//Pop loading screen
Navigator.of(context, rootNavigator: true).pop();
ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(state: VideoPlayerState.fullScreen));
if (context.mounted) {
await Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(
builder: (context) => const VideoPlayer(),
),
);
if (AdaptiveLayout.of(context).isDesktop) {
final fullScreen = await windowManager.isFullScreen();
if (fullScreen) {
await windowManager.setFullScreen(false);
}
}
if (context.mounted) {
context.refreshData();
}
onPlayerExit?.call();
}
}
extension BookBaseModelExtension on BookModel? {
Future<void> play(
BuildContext context,
WidgetRef ref, {
int? currentPage,
AutoDisposeStateNotifierProvider<BookDetailsProviderNotifier, BookProviderModel>? provider,
BuildContext? parentContext,
}) async {
if (kIsWeb) {
fladderSnackbar(context, title: "Books are not supported on web for now.");
return;
}
if (this == null) {
fladderSnackbar(context, title: "Not a selected book");
return;
}
var newProvider = provider;
if (newProvider == null) {
newProvider = bookDetailsProvider(this?.id ?? "");
await ref.watch(bookDetailsProvider(this?.id ?? "").notifier).fetchDetails(this!);
}
ref.read(bookViewerProvider.notifier).fetchBook(this);
await openBookViewer(
context,
newProvider,
initialPage: currentPage ?? this?.currentPage,
);
parentContext?.refreshData();
if (context.mounted) {
context.refreshData();
}
}
}
extension PhotoAlbumExtension on PhotoAlbumModel? {
Future<void> play(
BuildContext context,
WidgetRef ref, {
int? currentPage,
AutoDisposeStateNotifierProvider<BookDetailsProviderNotifier, BookProviderModel>? provider,
BuildContext? parentContext,
}) async {
_showLoadingIndicator(context);
final albumModel = this;
if (albumModel == null) return;
final api = ref.read(jellyApiProvider);
final getChildItems = await api.itemsGet(
parentId: albumModel.id,
includeItemTypes: FladderItemType.galleryItem.map((e) => e.dtoKind).toList(),
recursive: true);
final photos = getChildItems.body?.items.whereType<PhotoModel>() ?? [];
Navigator.of(context, rootNavigator: true).pop();
if (photos.isEmpty) {
return;
}
await Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(
builder: (context) => PhotoViewerScreen(
items: photos.toList(),
),
),
);
if (context.mounted) {
context.refreshData();
}
return;
}
}
extension ItemBaseModelExtensions on ItemBaseModel? {
Future<void> play(
BuildContext context,
WidgetRef ref, {
Duration? startPosition,
bool showPlaybackOption = false,
}) async =>
switch (this) {
PhotoAlbumModel album => album.play(context, ref),
BookModel book => book.play(context, ref),
_ => _default(context, this, ref, startPosition: startPosition),
};
Future<void> _default(
BuildContext context,
ItemBaseModel? itemModel,
WidgetRef ref, {
Duration? startPosition,
bool showPlaybackOption = false,
}) async {
if (itemModel == null) return;
_showLoadingIndicator(context);
SyncedItem? syncedItem = ref.read(syncProvider.notifier).getSyncedItem(this);
final options = {
PlaybackType.directStream,
PlaybackType.transcode,
if (syncedItem != null && syncedItem.status == SyncStatus.complete) PlaybackType.offline,
};
PlaybackModel? model;
if (showPlaybackOption) {
final playbackType = await _showPlaybackTypeSelection(
context: context,
options: options,
);
model = switch (playbackType) {
PlaybackType.directStream || PlaybackType.transcode => await ref
.read(playbackModelHelper)
.createServerPlaybackModel(itemModel, playbackType, startPosition: startPosition),
PlaybackType.offline => await ref.read(playbackModelHelper).createOfflinePlaybackModel(itemModel, syncedItem),
null => null
};
} else {
model = (await ref.read(playbackModelHelper).createServerPlaybackModel(itemModel, PlaybackType.directStream)) ??
await ref.read(playbackModelHelper).createOfflinePlaybackModel(itemModel, syncedItem);
}
if (model == null) {
return;
}
await _playVideo(context, startPosition: startPosition, current: model, ref: ref);
}
}
extension ItemBaseModelsBooleans on List<ItemBaseModel> {
Future<void> playLibraryItems(BuildContext context, WidgetRef ref, {bool shuffle = false}) async {
if (isEmpty) return;
_showLoadingIndicator(context);
// Replace all shows/seasons with all episodes
List<List<ItemBaseModel>> newList = await Future.wait(map((element) async {
switch (element.type) {
case FladderItemType.series:
return await ref.read(jellyApiProvider).fetchEpisodeFromShow(seriesId: element.id);
default:
return [element];
}
}));
var expandedList =
newList.expand((element) => element).toList().where((element) => element.playAble).toList().uniqueBy(
(value) => value.id,
);
if (shuffle) {
expandedList.shuffle();
}
PlaybackModel? model = await ref.read(playbackModelHelper).createServerPlaybackModel(
expandedList.firstOrNull,
PlaybackType.directStream,
libraryQueue: expandedList,
);
if (context.mounted) {
await _playVideo(context, ref: ref, queue: expandedList, current: model);
if (context.mounted) {
RefreshState.of(context).refresh();
}
}
}
}
Future<PlaybackType?> _showPlaybackTypeSelection({
required BuildContext context,
required Set<PlaybackType> options,
}) async {
PlaybackType? playbackType;
await showDialogAdaptive(
context: context,
builder: (context) {
return PlaybackDialogue(
options: options,
onClose: (type) {
playbackType = type;
Navigator.of(context).pop();
},
);
},
);
return playbackType;
}
class PlaybackDialogue extends StatelessWidget {
final Set<PlaybackType> options;
final Function(PlaybackType type) onClose;
const PlaybackDialogue({required this.options, required this.onClose, super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16).add(EdgeInsets.only(top: 16, bottom: 8)),
child: Text(
"Playback type",
style: Theme.of(context).textTheme.titleLarge,
),
),
const Divider(),
...options.map((type) => ListTile(
title: Text(type.name),
leading: Icon(type.icon),
onTap: () {
onClose(type);
},
))
],
);
}
}