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

53
lib/android_tv/main.dart Normal file
View file

@ -0,0 +1,53 @@
import 'package:fladder/providers/shared_provider.dart';
import 'package:fladder/util/application_info.dart';
import 'package:fladder/util/string_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:media_kit/media_kit.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() async {
_setupLogging();
WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized();
final sharedPreferences = await SharedPreferences.getInstance();
PackageInfo packageInfo = await PackageInfo.fromPlatform();
runApp(
ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWith((ref) => sharedPreferences),
applicationInfoProvider.overrideWith((ref) => ApplicationInfo(
name: packageInfo.appName, version: packageInfo.version, os: defaultTargetPlatform.name.capitalize())),
],
child: const Main(),
),
);
}
void _setupLogging() {
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((rec) {
if (kDebugMode) {
print('${rec.level.name}: ${rec.time}: ${rec.message}');
}
});
}
class Main extends ConsumerWidget {
const Main({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return const MaterialApp(
home: Scaffold(
body: Center(child: Text("AndroidTV")),
),
);
}
}

View file

@ -0,0 +1 @@
export 'jellyfin_open_api.swagger.dart' show JellyfinOpenApi;

View file

@ -0,0 +1 @@
final Map<Type, Object Function(Map<String, dynamic>)> generatedMapping = {};

View file

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/util/localization_helper.dart';
enum MetadataRefresh {
defaultRefresh(MetadataRefreshMode.$default),
validation(MetadataRefreshMode.validationonly),
fullRefresh(MetadataRefreshMode.fullrefresh);
const MetadataRefresh(this.api);
final MetadataRefreshMode api;
String label(BuildContext context) {
return switch (this) {
defaultRefresh => context.localized.metadataRefreshDefault,
validation => context.localized.metadataRefreshValidation,
fullRefresh => context.localized.metadataRefreshFull,
};
}
ItemsItemIdRefreshPostMetadataRefreshMode? get metadataRefreshMode => switch (this) {
MetadataRefresh.fullRefresh => ItemsItemIdRefreshPostMetadataRefreshMode.fullrefresh,
MetadataRefresh.validation => ItemsItemIdRefreshPostMetadataRefreshMode.validationonly,
_ => ItemsItemIdRefreshPostMetadataRefreshMode.$default
};
ItemsItemIdRefreshPostImageRefreshMode? get imageRefreshMode => switch (this) {
MetadataRefresh.fullRefresh => ItemsItemIdRefreshPostImageRefreshMode.fullrefresh,
MetadataRefresh.validation => ItemsItemIdRefreshPostImageRefreshMode.validationonly,
_ => ItemsItemIdRefreshPostImageRefreshMode.$default
};
}
enum ItemLocation {
filesystem('FileSystem'),
remote('Remote'),
virtual('Virtual'),
offline('Offline');
final String? value;
factory ItemLocation.fromDto(LocationType? type) =>
ItemLocation.values.firstWhereOrNull((element) => type?.value == element.value) ?? ItemLocation.filesystem;
const ItemLocation(this.value);
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

669
lib/l10n/app_en.arb Normal file
View file

@ -0,0 +1,669 @@
{
"@@locale": "en",
"switchUser": "Switch user",
"userName": "Username",
"password": "Password",
"login": "Login",
"logout": "Logout",
"cancel": "Cancel",
"accept": "Accept",
"code": "Code",
"error": "Error",
"clear": "Clear",
"days": "Days",
"search": "Search",
"loggedIn": "Logged-in",
"change": "Change",
"other": "Other",
"dynamicText": "Dynamic",
"enabled": "Enabled",
"disabled": "Disabled",
"dashboard": "Dashboard",
"advanced": "Advanced",
"refresh": "Refresh",
"delete": "Delete",
"goTo": "Go To",
"loop": "Loop",
"empty": "Empty",
"noRating": "No rating",
"backgroundBlur": "Background blur",
"autoPlay": "Auto-play",
"resume": "Resume {item}",
"@resume": {
"description": "resume",
"placeholders": {
"item": {
"type": "String"
}
}
},
"play": "Play {item}",
"@play": {
"description": "Play with",
"placeholders": {
"item": {
"type": "String"
}
}
},
"read": "Read {item}",
"@read": {
"description": "read",
"placeholders": {
"item": {
"type": "String"
}
}
},
"readFromStart": "Read {item} from start",
"@readFromStart": {
"description": "Read book from start",
"placeholders": {
"item": {
"type": "String"
}
}
},
"playFrom": "Play from {name}",
"@playFrom": {
"description": "playFrom",
"placeholders": {
"name": {
"type": "String"
}
}
},
"playFromStart": "Play {name} from the start",
"@playFromStart": {
"description": "speel vanaf het begin",
"placeholders": {
"name": {
"type": "String"
}
}
},
"moreFrom": "More from {info}",
"@moreFrom": {
"description": "More from",
"placeholders": {
"info": {
"type": "String"
}
}
},
"selectedWith": "Selected {info}",
"@selectedWith": {
"description": "selected",
"placeholders": {
"info": {
"type": "String"
}
}
},
"selected": "Selected",
"restart": "Restart",
"reWatch": "Rewatch",
"options": "Options",
"list": "List",
"grid": "Grid",
"masonry": "Masonry",
"start": "Start",
"none": "None",
"chapter": "{count, plural, other{Chapters} one{Chapter}}",
"@chapter": {
"description": "chapter",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"watchOn": "Watch on",
"sync": "Sync",
"moreOptions": "More options",
"continuePage": "Continue - page {page}",
"@continuePage": {
"description": "Continue - page 1",
"placeholders": {
"page": {
"type": "int"
}
}
},
"openShow": "Open show",
"showDetails": "Show details",
"showAlbum": "Show album",
"season": "{count, plural, other{Seasons} one{Season} }",
"@season": {
"description": "season",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"episode": "{count, plural, other{Episodes} one{Episode} }",
"@episode": {
"description": "episode",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"addToCollection": "Add to collection",
"addToPlaylist": "Add to playlist",
"removeFromCollection": "Remove to collection",
"removeFromPlaylist": "Remove to playlist",
"markAsWatched": "Mark as watched",
"markAsUnwatched": "Mark as unwatched",
"removeAsFavorite": "Remove as favorite",
"addAsFavorite": "Add as favorite",
"editMetadata": "Edit metadata",
"refreshMetadata": "Refresh metadata",
"syncDetails": "Sync details",
"identify": "Identify",
"info": "Info",
"clearAllSettings": "Clear all settings",
"clearAllSettingsQuestion": "Clear all settings?",
"unableToReverseAction": "This action can not be reversed, it will remove all settings.",
"navigationDashboard": "Dashboard",
"navigationFavorites": "Favorites",
"navigationSync": "Synced",
"navigation": "Navigation",
"library": "{count, plural, other{Libraries} one{Library}}",
"@library": {
"description": "Plural",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"scanLibrary": "Scan library",
"dashboardContinue": "Continue",
"dashboardContinueWatching": "Continue Watching",
"dashboardContinueReading": "Continue Reading",
"dashboardContinueListening": "Continue Listening",
"dashboardNextUp": "Next-up",
"dashboardRecentlyAdded": "Recently added in {name}",
"@dashboardRecentlyAdded": {
"description": "Recently added on home screen",
"placeholders": {
"name": {
"type": "String"
}
}
},
"settings": "Settings",
"settingsClientTitle": "Fladder",
"settingsClientDesc": "General, Time-out, Layout, Theme",
"settingsQuickConnectTitle": "Quick connect",
"settingsProfileTitle": "Profile",
"settingsProfileDesc": "Lockscreen",
"settingsPlayerTitle": "Player",
"settingsPlayerDesc": "Aspect-ratio, Advanced",
"logoutUserPopupTitle": "Log-out user {userName}?",
"@logoutUserPopupTitle": {
"description": "Pop-up for loging out the user",
"placeholders": {
"userName": {
"type": "String"
}
}
},
"logoutUserPopupContent": "This will log-out {userName} and delete te user from the app.\nYou will have to log back in to {serverName}.",
"@logoutUserPopupContent": {
"description": "Pop-up for loging out the user description",
"placeholders": {
"userName": {
"type": "String"
},
"serverName": {
"type": "String"
}
}
},
"quickConnectTitle": "Quick-connect",
"quickConnectAction": "Enter quick connect code for",
"quickConnectInputACode": "Input a code",
"quickConnectWrongCode": "Wrong code",
"downloadsTitle": "Downloads",
"downloadsPath": "Path",
"pathEditTitle": "Change location",
"pathEditSelect": "Select downloads destination",
"pathClearTitle": "Clear downloads path",
"pathEditDesc": "This location is set for all users, any synced data will no longer be accessible.\nIt will remain on your storage.",
"downloadsSyncedData": "Synced data",
"downloadsClearTitle": "Clear synced data",
"downloadsClearDesc": "Are you sure you want to remove all synced data?\nThis will clear all data for every synced user!",
"lockscreen": "Lockscreen",
"timeOut": "Time-out",
"home": "Home",
"settingsHomeCarouselTitle": "Dashboard carousel",
"settingsHomeCarouselDesc": "Shows a carousel on the dashboard screen",
"settingsHomeNextUpTitle": "Next-up posters",
"settingsHomeNextUpDesc": "Type of posters shown in the dashboard screen",
"settingsVisual": "Visual",
"settingsBlurredPlaceholderTitle": "Blurred placeholder",
"settingsBlurredPlaceholderDesc": "Show blurred background when loading posters",
"settingsBlurEpisodesTitle": "Blur next-up episodes",
"settingsBlurEpisodesDesc": "Blur all upcoming episodes",
"settingsEnableOsMediaControls": "Enable OS media controls",
"settingsNextUpCutoffDays": "Next-up cutoff days",
"settingsShowScaleSlider": "Show poster size slide",
"settingsPosterSize": "Poster size",
"settingsPosterSlider": "Show scale slider",
"settingsPosterPinch": "Pinch-zoom to scale posters",
"theme": "Theme",
"mode": "Mode",
"themeModeSystem": "System",
"themeModeLight": "Light",
"themeModeDark": "Dark",
"themeColor": "Theme color",
"color": "Color",
"amoledBlack": "Amoled black",
"hide": "Hide",
"nextUp": "Next Up",
"settingsContinue": "Continue",
"separate": "Separate",
"combined": "Combined",
"settingsSecurity": "Security",
"settingSecurityApplockTitle": "App lock",
"appLockTitle": "Set the log-in method for {userName}",
"@appLockTitle": {
"description": "Pop-up to pick a login method",
"placeholders": {
"userName": {
"type": "String"
}
}
},
"biometricsFailedCheckAgain": "Biometrics failed check settings and try again",
"appLockAutoLogin": "Auto login",
"appLockPasscode": "Passcode",
"appLockBiometrics": "Biometrics",
"settingsPlayerVideoHWAccelTitle": "Hardware acceleration",
"settingsPlayerVideoHWAccelDesc": "Use the gpu to render video (recommended)",
"settingsPlayerNativeLibassAccelTitle": "Native libass subtitles",
"settingsPlayerNativeLibassAccelDesc": "Use video player libass subtitle renderer",
"settingsPlayerMobileWarning": "Enabling Hardware acceleration and native libass subtitles on Android might cause some subtitles to not render.",
"settingsPlayerCustomSubtitlesTitle": "Customize subtitles",
"settingsPlayerCustomSubtitlesDesc": "Customize Size, Color, Position, Outline",
"videoScalingFillScreenTitle": "Fill screen",
"videoScalingFillScreenDesc": "Fill the navigation and statusbar",
"videoScalingFillScreenNotif": "Fill-screen overwrites video fit, in horizontal rotation",
"videoScaling": "Video scaling",
"videoScalingFill": "Fill",
"videoScalingContain": "Contain",
"videoScalingCover": "Cover",
"videoScalingFitWidth": "Fit Width",
"videoScalingFitHeight": "Fit Height",
"videoScalingScaleDown": "ScaleDown",
"subtitleConfiguratorPlaceHolder": "This is placeholder text, \n nothing to see here.",
"subtitleConfigurator": "Subtitle configurator",
"refreshPopup": "Refresh - {name}",
"@refreshPopup": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"scanningName": "Scanning - {name}",
"@scanningName": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"refreshPopupContentMetadata": "Metadata is refreshed based on settings and internet services that are enabled in the Dashboard.",
"replaceExistingImages": "Replace existing images",
"metadataRefreshDefault": "Scan for new and updated files",
"metadataRefreshValidation": "Search for missing metadata",
"metadataRefreshFull": "Replace all metadata",
"syncedItems": "Synced items",
"noItemsSynced": "No items synced",
"syncDeletePopupPermanent": "This action is permanent and will remove all localy synced files",
"totalSize": "Total size: {size}",
"@totalSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mediaTypeBase": "Base Type",
"mediaTypeMovie": "Movie",
"mediaTypeSeries": "Series",
"mediaTypeSeason": "Season",
"mediaTypeEpisode": "Episode",
"mediaTypePhoto": "Photo",
"mediaTypePerson": "Person",
"mediaTypePhotoAlbum": "Photo Album",
"mediaTypeFolder": "Folder",
"mediaTypeBoxset": "Boxset",
"mediaTypePlaylist": "Playlist",
"mediaTypeBook": "Book",
"actor": "{count, plural, other{Actors} one{Actor}}",
"@actor": {
"description": "actor",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"writer": "{count, plural, other{Writer} two{Writers}}",
"@writer": {
"description": "writer",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"director": "{count, plural, other{Director} two{Directors}}",
"@director": {
"description": "director",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"video": "Video",
"audio": "Audio",
"subtitles": "Subtitles",
"related": "Related",
"all": "All",
"overview": "Overview",
"selectViewType": "Select view type",
"noItemsToShow": "No items to show",
"sortBy": "Sort by",
"groupBy": "Group by",
"scrollToTop": "Scroll to top",
"disableFilters": "Disable filters",
"selectAll": "Select all",
"clearSelection": "Clear selection",
"shuffleVideos": "Shuffle videos",
"shuffleGallery": "Shuffle gallery",
"unknown": "Unknown",
"favorites": "Favorites",
"recursive": "Recursive",
"genre": "{count, plural, other{Genres} one{Genre}}",
"@genre": {
"description": "genre",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"studio": "{count, plural, other{Studios} one{Studio}}",
"@studio": {
"description": "studio",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"label": "{count, plural, other{Labels} one{Label}}",
"@label": {
"description": "label",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"group": "Group",
"type": "{count, plural, other{Types} one{Type}}",
"@type": {
"description": "type",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"filter": "{count, plural, other{Filters} one{Filter}}",
"@filter": {
"description": "filter",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"showEmpty": "Show empty",
"hideEmpty": "Hide empty",
"rating": "{count, plural, other{Ratings} one{Rating}}",
"@rating": {
"description": "rating",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"year": "{count, plural, other{Years} one{Year}}",
"@year": {
"description": "year",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"playVideos": "Play videos",
"playLabel": "Play",
"forceRefresh": "Force refresh",
"itemCount": "Item count: {count}",
"@itemCount": {
"description": "Item count",
"placeholders": {
"count": {
"type": "int"
}
}
},
"invalidUrl": "Invalid url",
"invalidUrlDesc": "Url needs to start with http(s)://",
"incorrectPinTryAgain": "Incorrect pin try again",
"somethingWentWrongPasswordCheck": "Something went wrong, check your password",
"unableToConnectHost": "Unable to connect to host",
"server": "Server",
"retrievePublicListOfUsers": "Retrieve public list of users",
"displayLanguage": "Display language",
"deleteItem": "Delete {item}?",
"@deleteItem": {
"description": "deleteItem",
"placeholders": {
"item": {
"type": "String"
}
}
},
"syncDeleteItemTitle": "Delete synced item",
"syncDeleteItemDesc": "Delete all synced data for?\n{item}",
"@syncDeleteItemDesc": {
"description": "Sync delete item pop-up window",
"placeholders": {
"item": {
"type": "String"
}
}
},
"syncOpenParent": "Open parent",
"syncRemoveDataTitle": "Remove synced data?",
"syncRemoveDataDesc": "Delete synced video data? This is permanent and you will need to re-sync the files",
"collectionFolder": "Collection folder",
"musicAlbum": "Album",
"active": "Active",
"name": "Name",
"result": "Result",
"close": "Close",
"replaceAllImages": "Replace all images",
"noResults": "No results",
"openWebLink": "Open web link",
"setIdentityTo": "Set identity to {name}",
"@setIdentityTo": {
"description": "setIdentityTo",
"placeholders": {
"name": {
"type": "String"
}
}
},
"somethingWentWrong": "Something went wrong",
"clearChanges": "Clear changes",
"useDefaults": "Use defaults",
"light": "Light",
"normal": "Normal",
"bold": "Bold",
"fontSize": "Font size",
"heightOffset": "Height offset",
"fontColor": "Font color",
"outlineColor": "Outline color",
"outlineSize": "Outline size",
"backgroundOpacity": "Background opacity",
"shadow": "Shadow",
"played": "Played",
"unPlayed": "Unplayed",
"resumable": "Resumable",
"sortOrder": "Sort order",
"sortName": "Name",
"communityRating": "Community Rating",
"parentalRating": "Parental Rating",
"dateAdded": "Date added",
"dateLastContentAdded": "Date last content added",
"favorite": "Favorite",
"datePlayed": "Date played",
"folders": "Folders",
"playCount": "Play count",
"releaseDate": "Release date",
"runTime": "Run time",
"ascending": "Ascending",
"descending": "Descending",
"minutes": "{count, plural, other{Minutes} one{Minute} }",
"@minutes": {
"description": "minute",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"seconds": "{count, plural, other{Seconds} one{Second}}",
"@seconds": {
"description": "second",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"page": "Page {index}",
"@page": {
"description": "page",
"placeholders": {
"index": {
"type": "int"
}
}
},
"set": "Set",
"@set": {
"description": "Use for setting a certain value",
"context": "Set 'time'"
},
"never": "Never",
"selectTime": "Select time",
"immediately": "Immediately",
"timeAndAnnotation": "{minutes} and {seconds}",
"@timeAndAnnotation": {
"description": "timeAndAnnotation",
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"scanYourFingerprintToAuthenticate": "Scan your fingerprint to authenticate {user}",
"@scanYourFingerprintToAuthenticate": {
"placeholders": {
"user": {
"type": "String"
}
}
},
"scanBiometricHint": "Verify identity",
"deleteFileFromSystem": "Deleting this item {item} will delete it from both the file system and your media library.\nAre you sure you wish to continue?",
"@deleteFileFromSystem": {
"description": "Delete file from system",
"placeholders": {
"item": {
"type": "String"
}
}
},
"notPartOfAlbum": "Not part of a album",
"retry": "Retry",
"failedToLoadImage": "Failed to load image",
"save": "Save",
"metaDataSavedFor": "Metadata saved for {item}",
"@metaDataSavedFor": {
"description": "metaDataSavedFor",
"placeholders": {
"item": {
"type": "String"
}
}
},
"libraryPageSizeTitle": "Library page size",
"libraryPageSizeDesc": "Set the amount to load at a time. 0 disables paging",
"fetchingLibrary": "Fetching library items",
"libraryFetchNoItemsFound": "No items found, try different settings.",
"noSuggestionsFound": "No suggestions found",
"viewPhotos": "View photos",
"random": "Random",
"tag": "{count, plural, one{Tag} other{Tags}}",
"@tag": {
"description": "tag",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"saved": "Saved",
"discovered": "Discovered",
"noServersFound": "No new servers found",
"about": "About",
"openParent": "Open parent",
"mouseDragSupport": "Drag using mouse",
"controls": "Controls"
}

669
lib/l10n/app_es.arb Normal file
View file

@ -0,0 +1,669 @@
{
"@@locale": "es",
"switchUser": "Cambiar usuario",
"userName": "Nombre de usuario",
"password": "Contraseña",
"login": "Iniciar sesión",
"logout": "Cerrar sesión",
"cancel": "Cancelar",
"accept": "Aceptar",
"code": "Código",
"error": "Error",
"clear": "Borrar",
"days": "Días",
"search": "Buscar",
"loggedIn": "Conectado",
"change": "Cambiar",
"other": "Otro",
"dynamicText": "Dinámico",
"enabled": "Habilitado",
"disabled": "Deshabilitado",
"dashboard": "Tablero",
"advanced": "Avanzado",
"refresh": "Actualizar",
"delete": "Eliminar",
"goTo": "Ir a",
"loop": "Bucle",
"empty": "Vacío",
"noRating": "Sin calificación",
"backgroundBlur": "Desenfoque de fondo",
"autoPlay": "Reproducción automática",
"resume": "Reanudar {item}",
"@resume": {
"description": "reanudar",
"placeholders": {
"item": {
"type": "String"
}
}
},
"play": "Reproducir {item}",
"@play": {
"description": "Reproducir con",
"placeholders": {
"item": {
"type": "String"
}
}
},
"read": "Leer {item}",
"@read": {
"description": "leer",
"placeholders": {
"item": {
"type": "String"
}
}
},
"readFromStart": "Leer {item} desde el principio",
"@readFromStart": {
"description": "Leer libro desde el principio",
"placeholders": {
"item": {
"type": "String"
}
}
},
"playFrom": "Reproducir desde {name}",
"@playFrom": {
"description": "reproducir desde",
"placeholders": {
"name": {
"type": "String"
}
}
},
"moreFrom": "Más de {info}",
"@moreFrom": {
"description": "Más de",
"placeholders": {
"info": {
"type": "String"
}
}
},
"selectedWith": "Seleccionado {info}",
"@selectedWith": {
"description": "seleccionado",
"placeholders": {
"info": {
"type": "String"
}
}
},
"selected": "Seleccionado",
"restart": "Reiniciar",
"reWatch": "Volver a ver",
"options": "Opciones",
"list": "Lista",
"grid": "Cuadrícula",
"masonry": "Mampostería",
"start": "Iniciar",
"none": "Ninguno",
"chapter": "{count, plural, other{Capítulos} one{Capítulo}}",
"@chapter": {
"description": "capítulo",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"watchOn": "Ver en",
"sync": "Sincronizar",
"moreOptions": "Más opciones",
"continuePage": "Continuar - página {page}",
"@continuePage": {
"description": "Continuar - página 1",
"placeholders": {
"page": {
"type": "int"
}
}
},
"openShow": "Abrir espectáculo",
"showDetails": "Mostrar detalles",
"showAlbum": "Mostrar álbum",
"season": "{count, plural, other{Temporadas} one{Temporada}}",
"@season": {
"description": "temporada",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"episode": "{count, plural, other{Episodios} one{Episodio}}",
"@episode": {
"description": "episodio",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"addToCollection": "Agregar a la colección",
"addToPlaylist": "Agregar a la lista de reproducción",
"removeFromCollection": "Eliminar de la colección",
"removeFromPlaylist": "Eliminar de la lista de reproducción",
"markAsWatched": "Marcar como visto",
"markAsUnwatched": "Marcar como no visto",
"removeAsFavorite": "Eliminar como favorito",
"addAsFavorite": "Agregar como favorito",
"editMetadata": "Editar metadatos",
"refreshMetadata": "Actualizar metadatos",
"syncDetails": "Sincronizar detalles",
"identify": "Identificar",
"info": "Información",
"clearAllSettings": "Borrar todos los ajustes",
"clearAllSettingsQuestion": "¿Borrar todos los ajustes?",
"unableToReverseAction": "Esta acción no se puede deshacer, eliminará todos los ajustes.",
"navigationDashboard": "Tablero",
"navigationFavorites": "Favoritos",
"navigationSync": "Sincronizado",
"navigation": "Navegación",
"library": "{count, plural, other{Bibliotecas} one{Biblioteca}}",
"@library": {
"description": "plural",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"scanLibrary": "Escanear biblioteca",
"dashboardContinue": "Continuar",
"dashboardContinueWatching": "Continuar viendo",
"dashboardContinueReading": "Continuar leyendo",
"dashboardContinueListening": "Continuar escuchando",
"dashboardNextUp": "Próximo",
"dashboardRecentlyAdded": "Recientemente añadido en {name}",
"@dashboardRecentlyAdded": {
"description": "Recientemente añadido en la pantalla de inicio",
"placeholders": {
"name": {
"type": "String"
}
}
},
"settings": "Ajustes",
"settingsClientTitle": "Fladder",
"settingsClientDesc": "General, Tiempo de espera, Diseño, Tema",
"settingsQuickConnectTitle": "Conexión rápida",
"settingsProfileTitle": "Perfil",
"settingsProfileDesc": "Pantalla de bloqueo",
"settingsPlayerTitle": "Reproductor",
"settingsPlayerDesc": "Relación de aspecto, Avanzado",
"logoutUserPopupTitle": "¿Cerrar sesión del usuario {userName}?",
"@logoutUserPopupTitle": {
"description": "Pop-up para cerrar sesión del usuario",
"placeholders": {
"userName": {
"type": "String"
}
}
},
"logoutUserPopupContent": "Esto cerrará la sesión de {userName} y eliminará al usuario de la aplicación.\nDeberá volver a iniciar sesión en {serverName}.",
"@logoutUserPopupContent": {
"description": "Pop-up para cerrar sesión del usuario descripción",
"placeholders": {
"userName": {
"type": "String"
},
"serverName": {
"type": "String"
}
}
},
"quickConnectTitle": "Conexión rápida",
"quickConnectAction": "Ingrese el código de conexión rápida para",
"quickConnectInputACode": "Ingrese un código",
"quickConnectWrongCode": "Código incorrecto",
"downloadsTitle": "Descargas",
"downloadsPath": "Ruta",
"pathEditTitle": "Cambiar ubicación",
"pathEditSelect": "Seleccionar destino de descargas",
"pathClearTitle": "Borrar ruta de descargas",
"pathEditDesc": "Esta ubicación se establece para todos los usuarios, los datos sincronizados ya no serán accesibles.\nPermanecerán en su almacenamiento.",
"downloadsSyncedData": "Datos sincronizados",
"downloadsClearTitle": "Borrar datos sincronizados",
"downloadsClearDesc": "¿Está seguro de que desea eliminar todos los datos sincronizados?\n¡Esto eliminará todos los datos de cada usuario sincronizado!",
"lockscreen": "Pantalla de bloqueo",
"timeOut": "Tiempo de espera",
"home": "Inicio",
"settingsHomeCarouselTitle": "Carrusel del tablero",
"settingsHomeCarouselDesc": "Muestra un carrusel en la pantalla del tablero",
"settingsHomeNextUpTitle": "Próximos pósteres",
"settingsHomeNextUpDesc": "Tipo de pósteres mostrados en la pantalla del tablero",
"settingsVisual": "Visual",
"settingsBlurredPlaceholderTitle": "Marcador de posición difuminado",
"settingsBlurredPlaceholderDesc": "Mostrar fondo difuminado al cargar pósteres",
"settingsBlurEpisodesTitle": "Difuminar próximos episodios",
"settingsBlurEpisodesDesc": "Difuminar todos los próximos episodios",
"settingsEnableOsMediaControls": "Habilitar controles multimedia del sistema operativo",
"settingsNextUpCutoffDays": "Días límite de próximos",
"settingsShowScaleSlider": "Mostrar control deslizante de tamaño de póster",
"settingsPosterSize": "Tamaño del póster",
"settingsPosterSlider": "Mostrar control deslizante de escala",
"settingsPosterPinch": "Pellizcar para escalar pósteres",
"theme": "Tema",
"mode": "Modo",
"themeModeSystem": "Sistema",
"themeModeLight": "Claro",
"themeModeDark": "Oscuro",
"themeColor": "Color del tema",
"color": "Color",
"amoledBlack": "Negro amoled",
"hide": "Ocultar",
"nextUp": "Próximos",
"settingsContinue": "Continuar",
"separate": "Separado",
"combined": "Combinado",
"settingsSecurity": "Seguridad",
"settingSecurityApplockTitle": "Bloqueo de aplicación",
"appLockTitle": "Establecer el método de inicio de sesión para {userName}",
"@appLockTitle": {
"description": "Pop-up para elegir un método de inicio de sesión",
"placeholders": {
"userName": {
"type": "String"
}
}
},
"biometricsFailedCheckAgain": "Falló la biometría, verifique la configuración y vuelva a intentarlo",
"appLockAutoLogin": "Inicio de sesión automático",
"appLockPasscode": "Código de acceso",
"appLockBiometrics": "Biometría",
"settingsPlayerVideoHWAccelTitle": "Aceleración de hardware",
"settingsPlayerVideoHWAccelDesc": "Usar la gpu para renderizar video (recomendado)",
"settingsPlayerNativeLibassAccelTitle": "Subtítulos nativos libass",
"settingsPlayerNativeLibassAccelDesc": "Usar el renderizador de subtítulos libass del reproductor de video",
"settingsPlayerMobileWarning": "Habilitar la aceleración de hardware y los subtítulos libass nativos en Android puede hacer que algunos subtítulos no se rendericen.",
"settingsPlayerCustomSubtitlesTitle": "Personalizar subtítulos",
"settingsPlayerCustomSubtitlesDesc": "Personalizar tamaño, color, posición, contorno",
"videoScalingFillScreenTitle": "Llenar pantalla",
"videoScalingFillScreenDesc": "Llenar la barra de navegación y la barra de estado",
"videoScalingFillScreenNotif": "Llenar pantalla sobrescribe ajuste de video, en rotación horizontal",
"videoScaling": "Escalado de video",
"videoScalingFill": "Llenar",
"videoScalingContain": "Contener",
"videoScalingCover": "Cubrir",
"videoScalingFitWidth": "Ajustar ancho",
"videoScalingFitHeight": "Ajustar altura",
"videoScalingScaleDown": "Reducir escala",
"subtitleConfiguratorPlaceHolder": "Este es un texto de marcador de posición,\nnada que ver aquí.",
"subtitleConfigurator": "Configurador de subtítulos",
"refreshPopup": "Actualizar - {name}",
"@refreshPopup": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"scanningName": "Escaneando - {name}",
"@scanningName": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"refreshPopupContentMetadata": "Los metadatos se actualizan según la configuración y los servicios de Internet que están habilitados en el Tablero.",
"replaceExistingImages": "Reemplazar imágenes existentes",
"metadataRefreshDefault": "Buscar archivos nuevos y actualizados",
"metadataRefreshValidation": "Buscar metadatos faltantes",
"metadataRefreshFull": "Reemplazar todos los metadatos",
"syncedItems": "Elementos sincronizados",
"noItemsSynced": "No hay elementos sincronizados",
"syncDeletePopupPermanent": "Esta acción es permanente y eliminará todos los archivos sincronizados localmente",
"totalSize": "Tamaño total: {size}",
"@totalSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mediaTypeBase": "Tipo base",
"mediaTypeMovie": "Película",
"mediaTypeSeries": "Serie",
"mediaTypeSeason": "Temporada",
"mediaTypeEpisode": "Episodio",
"mediaTypePhoto": "Foto",
"mediaTypePerson": "Persona",
"mediaTypePhotoAlbum": "Álbum de fotos",
"mediaTypeFolder": "Carpeta",
"mediaTypeBoxset": "Conjunto de cajas",
"mediaTypePlaylist": "Lista de reproducción",
"mediaTypeBook": "Libro",
"actor": "{count, plural, other{Actores} one{Actor}}",
"@actor": {
"description": "actor",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"writer": "{count, plural, other{Escritores} one{Escritor}}",
"@writer": {
"description": "escritor",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"director": "{count, plural, other{Directores} one{Director}}",
"@director": {
"description": "director",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"video": "Video",
"audio": "Audio",
"subtitles": "Subtítulos",
"related": "Relacionado",
"all": "Todo",
"overview": "Visión general",
"selectViewType": "Seleccionar tipo de vista",
"noItemsToShow": "No hay elementos para mostrar",
"sortBy": "Ordenar por",
"groupBy": "Agrupar por",
"scrollToTop": "Desplazarse hacia arriba",
"disableFilters": "Deshabilitar filtros",
"selectAll": "Seleccionar todo",
"clearSelection": "Borrar selección",
"shuffleVideos": "Videos aleatorios",
"shuffleGallery": "Galería aleatoria",
"unknown": "Desconocido",
"favorites": "Favoritos",
"recursive": "Recursivo",
"genre": "{count, plural, other{Géneros} one{Género}}",
"@genre": {
"description": "género",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"studio": "{count, plural, other{Estudios} one{Estudio}}",
"@studio": {
"description": "estudio",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"label": "{count, plural, other{Etiquetas} one{Etiqueta}}",
"@label": {
"description": "etiqueta",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"group": "Grupo",
"type": "{count, plural, other{Tipos} one{Tipo}}",
"@type": {
"description": "tipo",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"filter": "{count, plural, other{Filtros} one{Filtro}}",
"@filter": {
"description": "filtro",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"showEmpty": "Mostrar vacío",
"hideEmpty": "Ocultar vacío",
"rating": "{count, plural, other{Calificaciones} one{Calificación}}",
"@rating": {
"description": "calificación",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"year": "{count, plural, other{Años} one{Año}}",
"@year": {
"description": "año",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"playVideos": "Reproducir videos",
"playLabel": "Reproducir",
"forceRefresh": "Actualizar a la fuerza",
"itemCount": "Cantidad de elementos: {count}",
"@itemCount": {
"description": "Cantidad de elementos",
"placeholders": {
"count": {
"type": "int"
}
}
},
"invalidUrl": "URL inválida",
"invalidUrlDesc": "La URL debe comenzar con http(s)://",
"incorrectPinTryAgain": "PIN incorrecto, intente nuevamente",
"somethingWentWrongPasswordCheck": "Algo salió mal, verifique su contraseña",
"unableToConnectHost": "No se puede conectar al host",
"server": "Servidor",
"retrievePublicListOfUsers": "Recuperar lista pública de usuarios",
"displayLanguage": "Idioma de visualización",
"deleteItem": "¿Eliminar {item}?",
"@deleteItem": {
"description": "deleteItem",
"placeholders": {
"item": {
"type": "String"
}
}
},
"syncDeleteItemTitle": "Eliminar elemento sincronizado",
"syncDeleteItemDesc": "¿Eliminar todos los datos sincronizados de?\n{item}",
"@syncDeleteItemDesc": {
"description": "Ventana emergente de eliminación de elemento sincronizado",
"placeholders": {
"item": {
"type": "String"
}
}
},
"syncOpenParent": "Abrir padre",
"syncRemoveDataTitle": "¿Eliminar datos sincronizados?",
"syncRemoveDataDesc": "¿Eliminar los datos de video sincronizados? Esto es permanente y necesitarás volver a sincronizar los archivos",
"collectionFolder": "Carpeta de colección",
"musicAlbum": "Álbum",
"active": "Activo",
"name": "Nombre",
"result": "Resultado",
"close": "Cerrar",
"replaceAllImages": "Reemplazar todas las imágenes",
"noResults": "Sin resultados",
"openWebLink": "Abrir enlace web",
"setIdentityTo": "Establecer identidad a {name}",
"@setIdentityTo": {
"description": "setIdentityTo",
"placeholders": {
"name": {
"type": "String"
}
}
},
"somethingWentWrong": "Algo salió mal",
"clearChanges": "Borrar cambios",
"useDefaults": "Usar valores predeterminados",
"light": "Ligero",
"normal": "Normal",
"bold": "Negrita",
"fontSize": "Tamaño de fuente",
"heightOffset": "Desplazamiento de altura",
"fontColor": "Color de fuente",
"outlineColor": "Color del contorno",
"outlineSize": "Tamaño del contorno",
"backgroundOpacity": "Opacidad de fondo",
"shadow": "Sombra",
"played": "Reproducido",
"unPlayed": "No reproducido",
"resumable": "Reanudable",
"sortOrder": "Orden de clasificación",
"sortName": "Nombre",
"communityRating": "Calificación de la comunidad",
"parentalRating": "Calificación parental",
"dateAdded": "Fecha de añadido",
"dateLastContentAdded": "Fecha del último contenido añadido",
"favorite": "Favorito",
"datePlayed": "Fecha de reproducción",
"folders": "Carpetas",
"playCount": "Conteo de reproducciones",
"releaseDate": "Fecha de lanzamiento",
"runTime": "Duración",
"ascending": "Ascendente",
"descending": "Descendente",
"playFromStart": "Reproducir {name} desde el principio",
"@playFromStart": {
"description": "reproducir desde el inicio",
"placeholders": {
"name": {
"type": "String"
}
}
},
"minutes": "{count, plural, other{Minutos} one{Minuto}}",
"@minutes": {
"description": "minuto",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"seconds": "{count, plural, other{Segundos} one{Segundo}}",
"@seconds": {
"description": "segundo",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"page": "Página {index}",
"@page": {
"description": "página",
"placeholders": {
"index": {
"type": "int"
}
}
},
"set": "Establecer",
"@set": {
"description": "Usar para establecer un cierto valor",
"context": "Establecer 'tiempo'"
},
"never": "Nunca",
"selectTime": "Seleccionar hora",
"immediately": "Inmediatamente",
"timeAndAnnotation": "{minutes} y {seconds}",
"@timeAndAnnotation": {
"description": "timeAndAnnotation",
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"scanYourFingerprintToAuthenticate": "Escanea tu huella digital para autenticar a {user}",
"@scanYourFingerprintToAuthenticate": {
"placeholders": {
"user": {
"type": "String"
}
}
},
"scanBiometricHint": "Verificar identidad",
"deleteFileFromSystem": "Eliminar este elemento {item} lo eliminará tanto del sistema de archivos como de su biblioteca multimedia.\n¿Estás seguro de que deseas continuar?",
"@deleteFileFromSystem": {
"description": "Eliminar archivo del sistema",
"placeholders": {
"item": {
"type": "String"
}
}
},
"notPartOfAlbum": "No forma parte de un álbum",
"retry": "Reintentar",
"failedToLoadImage": "Error al cargar la imagen",
"save": "Guardar",
"metaDataSavedFor": "Metadatos guardados para {item}",
"@metaDataSavedFor": {
"description": "metadatosGuardadosPara",
"placeholders": {
"item": {
"type": "String"
}
}
},
"libraryPageSizeTitle": "Tamaño de página de la biblioteca",
"libraryPageSizeDesc": "Establece la cantidad a cargar a la vez. 0 desactiva el paginado.",
"fetchingLibrary": "Cargando elementos de la biblioteca",
"libraryFetchNoItemsFound": "No se encontraron elementos, intenta con diferentes configuraciones.",
"noSuggestionsFound": "No se encontraron sugerencias",
"viewPhotos": "Ver fotos",
"random": "Aleatorio",
"tag": "{count, plural, one{Etiqueta} other{Etiquetas}}",
"@tag": {
"description": "etiqueta",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"saved": "Guardado",
"discovered": "Descubierto",
"noServersFound": "No se encontraron nuevos servidores",
"about": "Acerca de",
"openParent": "Abrir carpeta superior",
"mouseDragSupport": "Arrastrar con el ratón",
"controls": "Controles"
}

669
lib/l10n/app_fr.arb Normal file
View file

@ -0,0 +1,669 @@
{
"@@locale": "fr",
"switchUser": "Changer d'utilisateur",
"userName": "Nom d'utilisateur",
"password": "Mot de passe",
"login": "Connexion",
"logout": "Déconnexion",
"cancel": "Annuler",
"accept": "Accepter",
"code": "Code",
"error": "Erreur",
"clear": "Effacer",
"days": "Jours",
"search": "Recherche",
"loggedIn": "Connecté",
"change": "Changer",
"other": "Autre",
"dynamicText": "Dynamique",
"enabled": "Activé",
"disabled": "Désactivé",
"dashboard": "Tableau de bord",
"advanced": "Avancé",
"refresh": "Rafraîchir",
"delete": "Supprimer",
"goTo": "Aller à",
"loop": "Boucle",
"empty": "Vide",
"noRating": "Pas de note",
"backgroundBlur": "Flou de fond",
"autoPlay": "Lecture automatique",
"resume": "Reprendre {item}",
"@resume": {
"description": "reprendre",
"placeholders": {
"item": {
"type": "String"
}
}
},
"play": "Lire {item}",
"@play": {
"description": "Lire avec",
"placeholders": {
"item": {
"type": "String"
}
}
},
"read": "Lire {item}",
"@read": {
"description": "lire",
"placeholders": {
"item": {
"type": "String"
}
}
},
"readFromStart": "Lire {item} depuis le début",
"@readFromStart": {
"description": "Lire le livre depuis le début",
"placeholders": {
"item": {
"type": "String"
}
}
},
"playFrom": "Lire depuis {name}",
"@playFrom": {
"description": "jouer de",
"placeholders": {
"name": {
"type": "String"
}
}
},
"moreFrom": "Plus de {info}",
"@moreFrom": {
"description": "Plus de",
"placeholders": {
"info": {
"type": "String"
}
}
},
"selectedWith": "Sélectionné {info}",
"@selectedWith": {
"description": "sélectionné",
"placeholders": {
"info": {
"type": "String"
}
}
},
"selected": "Sélectionné",
"restart": "Redémarrer",
"reWatch": "Revoir",
"watchOn": "Regarder sur",
"options": "Options",
"list": "Liste",
"grid": "Grille",
"masonry": "Maçonnerie",
"start": "Commencer",
"none": "Aucun",
"chapter": "{count, plural, other{Chapitres} one{Chapitre}}",
"@chapter": {
"description": "chapitre",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"sync": "Synchroniser",
"moreOptions": "Plus d'options",
"continuePage": "Continuer - page {page}",
"@continuePage": {
"description": "Continuer - page 1",
"placeholders": {
"page": {
"type": "int"
}
}
},
"openShow": "Ouvrir le spectacle",
"showDetails": "Afficher les détails",
"showAlbum": "Afficher l'album",
"season": "{count, plural, other{Saisons} one{Saison}}",
"@season": {
"description": "saison",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"episode": "{count, plural, other{Épisodes} one{Épisode}}",
"@episode": {
"description": "épisode",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"addToCollection": "Ajouter à la collection",
"addToPlaylist": "Ajouter à la playlist",
"removeFromCollection": "Retirer de la collection",
"removeFromPlaylist": "Retirer de la playlist",
"markAsWatched": "Marquer comme regardé",
"markAsUnwatched": "Marquer comme non regardé",
"removeAsFavorite": "Retirer des favoris",
"addAsFavorite": "Ajouter aux favoris",
"editMetadata": "Modifier les métadonnées",
"refreshMetadata": "Rafraîchir les métadonnées",
"syncDetails": "Synchroniser les détails",
"identify": "Identifier",
"info": "Infos",
"clearAllSettings": "Effacer tous les paramètres",
"clearAllSettingsQuestion": "Effacer tous les paramètres?",
"unableToReverseAction": "Cette action ne peut pas être annulée, elle supprimera tous les paramètres.",
"navigationDashboard": "Tableau de bord",
"navigationFavorites": "Favoris",
"navigationSync": "Synchronisé",
"navigation": "Navigation",
"library": "{count, plural, other{Bibliothèques} one{Bibliothèque}}",
"@library": {
"description": "pluriel",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"scanLibrary": "Scanner la bibliothèque",
"dashboardContinue": "Continuer",
"dashboardContinueWatching": "Continuer à regarder",
"dashboardContinueReading": "Continuer à lire",
"dashboardContinueListening": "Continuer à écouter",
"dashboardNextUp": "Suivant",
"dashboardRecentlyAdded": "Récemment ajouté dans {name}",
"@dashboardRecentlyAdded": {
"description": "Récemment ajouté sur l'écran d'accueil",
"placeholders": {
"name": {
"type": "String"
}
}
},
"settings": "Paramètres",
"settingsClientTitle": "Fladder",
"settingsClientDesc": "Général, Timeout, Disposition, Thème",
"settingsQuickConnectTitle": "Connexion rapide",
"settingsProfileTitle": "Profil",
"settingsProfileDesc": "Écran de verrouillage",
"settingsPlayerTitle": "Lecteur",
"settingsPlayerDesc": "Ratio, Avancé",
"logoutUserPopupTitle": "Déconnecter l'utilisateur {userName}?",
"@logoutUserPopupTitle": {
"description": "Pop-up pour déconnecter l'utilisateur",
"placeholders": {
"userName": {
"type": "String"
}
}
},
"logoutUserPopupContent": "Cela déconnectera {userName} et supprimera l'utilisateur de l'application.\nVous devrez vous reconnecter à {serverName}.",
"@logoutUserPopupContent": {
"description": "Pop-up pour déconnecter l'utilisateur description",
"placeholders": {
"userName": {
"type": "String"
},
"serverName": {
"type": "String"
}
}
},
"quickConnectTitle": "Connexion rapide",
"quickConnectAction": "Entrer le code de connexion rapide pour",
"quickConnectInputACode": "Entrer un code",
"quickConnectWrongCode": "Code incorrect",
"downloadsTitle": "Téléchargements",
"downloadsPath": "Chemin",
"pathEditTitle": "Changer l'emplacement",
"pathEditSelect": "Sélectionner la destination des téléchargements",
"pathClearTitle": "Effacer le chemin de téléchargement",
"pathEditDesc": "Cet emplacement est défini pour tous les utilisateurs, toutes les données synchronisées ne seront plus accessibles.\nElles resteront sur votre stockage.",
"downloadsSyncedData": "Données synchronisées",
"downloadsClearTitle": "Effacer les données synchronisées",
"downloadsClearDesc": "Êtes-vous sûr de vouloir supprimer toutes les données synchronisées?\nCela effacera toutes les données pour chaque utilisateur synchronisé!",
"lockscreen": "Écran de verrouillage",
"timeOut": "Timeout",
"home": "Accueil",
"settingsHomeCarouselTitle": "Carrousel du tableau de bord",
"settingsHomeCarouselDesc": "Affiche un carrousel sur l'écran du tableau de bord",
"settingsHomeNextUpTitle": "Affiches à venir",
"settingsHomeNextUpDesc": "Type d'affiches affichées sur l'écran du tableau de bord",
"settingsVisual": "Visuel",
"settingsBlurredPlaceholderTitle": "Placeholder flou",
"settingsBlurredPlaceholderDesc": "Afficher un arrière-plan flou lors du chargement des affiches",
"settingsBlurEpisodesTitle": "Flouter les prochains épisodes",
"settingsBlurEpisodesDesc": "Flouter tous les épisodes à venir",
"settingsEnableOsMediaControls": "Activer les contrôles multimédia de l'OS",
"settingsNextUpCutoffDays": "Jours de coupure suivants",
"settingsShowScaleSlider": "Afficher le curseur de taille des affiches",
"settingsPosterSize": "Taille de l'affiche",
"settingsPosterSlider": "Afficher le curseur de mise à l'échelle",
"settingsPosterPinch": "Pincer pour zoomer pour mettre à l'échelle les affiches",
"theme": "Thème",
"mode": "Mode",
"themeModeSystem": "Système",
"themeModeLight": "Clair",
"themeModeDark": "Sombre",
"themeColor": "Couleur du thème",
"color": "Couleur",
"amoledBlack": "Noir Amoled",
"hide": "Cacher",
"nextUp": "Suivant",
"settingsContinue": "Continuer",
"separate": "Séparer",
"combined": "Combiné",
"settingsSecurity": "Sécurité",
"settingSecurityApplockTitle": "Verrouillage de l'application",
"appLockTitle": "Définir la méthode de connexion pour {userName}",
"@appLockTitle": {
"description": "Pop-up pour choisir une méthode de connexion",
"placeholders": {
"userName": {
"type": "String"
}
}
},
"biometricsFailedCheckAgain": "Échec de la biométrie, vérifiez les paramètres et réessayez",
"appLockAutoLogin": "Connexion automatique",
"appLockPasscode": "Code d'accès",
"appLockBiometrics": "Biométrie",
"settingsPlayerVideoHWAccelTitle": "Accélération matérielle",
"settingsPlayerVideoHWAccelDesc": "Utiliser le GPU pour rendre la vidéo (recommandé)",
"settingsPlayerNativeLibassAccelTitle": "Sous-titres natifs libass",
"settingsPlayerNativeLibassAccelDesc": "Utiliser le rendu des sous-titres libass du lecteur vidéo",
"settingsPlayerMobileWarning": "L'activation de l'accélération matérielle et des sous-titres natifs libass sur Android peut empêcher certains sous-titres de s'afficher.",
"settingsPlayerCustomSubtitlesTitle": "Personnaliser les sous-titres",
"settingsPlayerCustomSubtitlesDesc": "Personnaliser la taille, la couleur, la position, le contour",
"videoScalingFillScreenTitle": "Remplir l'écran",
"videoScalingFillScreenDesc": "Remplir la barre de navigation et la barre d'état",
"videoScalingFillScreenNotif": "Le remplissage de l'écran écrase l'ajustement de la vidéo, en rotation horizontale",
"videoScaling": "Mise à l'échelle de la vidéo",
"videoScalingFill": "Remplir",
"videoScalingContain": "Contenir",
"videoScalingCover": "Couverture",
"videoScalingFitWidth": "Ajuster la largeur",
"videoScalingFitHeight": "Ajuster la hauteur",
"videoScalingScaleDown": "Réduire l'échelle",
"subtitleConfiguratorPlaceHolder": "Ceci est un texte de placeholder,\nrien à voir ici.",
"subtitleConfigurator": "Configurateur de sous-titres",
"refreshPopup": "Rafraîchir - {name}",
"@refreshPopup": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"scanningName": "Analyse - {name}",
"@scanningName": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"refreshPopupContentMetadata": "Les métadonnées sont mises à jour en fonction des paramètres et des services Internet activés dans le tableau de bord.",
"replaceExistingImages": "Remplacer les images existantes",
"metadataRefreshDefault": "Rechercher des fichiers nouveaux et mis à jour",
"metadataRefreshValidation": "Rechercher des métadonnées manquantes",
"metadataRefreshFull": "Remplacer toutes les métadonnées",
"syncedItems": "Éléments synchronisés",
"noItemsSynced": "Aucun élément synchronisé",
"syncDeletePopupPermanent": "Cette action est permanente et supprimera tous les fichiers synchronisés localement",
"totalSize": "Taille totale : {size}",
"@totalSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mediaTypeBase": "Type de base",
"mediaTypeMovie": "Film",
"mediaTypeSeries": "Série",
"mediaTypeSeason": "Saison",
"mediaTypeEpisode": "Épisode",
"mediaTypePhoto": "Photo",
"mediaTypePerson": "Personne",
"mediaTypePhotoAlbum": "Album photo",
"mediaTypeFolder": "Dossier",
"mediaTypeBoxset": "Coffret",
"mediaTypePlaylist": "Liste de lecture",
"mediaTypeBook": "Livre",
"actor": "{count, plural, other{Acteurs} one{Acteur}}",
"@actor": {
"description": "acteur",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"writer": "{count, plural, other{Écrivains} one{Écrivain}}",
"@writer": {
"description": "écrivain",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"director": "{count, plural, other{Réalisateurs} one{Réalisateur}}",
"@director": {
"description": "réalisateur",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"video": "Vidéo",
"audio": "Audio",
"subtitles": "Sous-titres",
"related": "Lié",
"all": "Tout",
"overview": "Aperçu",
"selectViewType": "Sélectionner le type de vue",
"noItemsToShow": "Aucun élément à afficher",
"sortBy": "Trier par",
"groupBy": "Grouper par",
"scrollToTop": "Défiler vers le haut",
"disableFilters": "Désactiver les filtres",
"selectAll": "Tout sélectionner",
"clearSelection": "Effacer la sélection",
"shuffleVideos": "Mélanger les vidéos",
"shuffleGallery": "Mélanger la galerie",
"unknown": "Inconnu",
"favorites": "Favoris",
"recursive": "Récursif",
"genre": "{count, plural, other{Genres} one{Genre}}",
"@genre": {
"description": "genre",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"studio": "{count, plural, other{Studios} one{Studio}}",
"@studio": {
"description": "studio",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"label": "{count, plural, other{Étiquettes} one{Étiquette}}",
"@label": {
"description": "étiquette",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"group": "Groupe",
"type": "{count, plural, other{Types} one{Type}}",
"@type": {
"description": "type",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"filter": "{count, plural, other{Filtres} one{Filtre}}",
"@filter": {
"description": "filtre",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"showEmpty": "Afficher vide",
"hideEmpty": "Cacher vide",
"rating": "{count, plural, other{Notes} one{Note}}",
"@rating": {
"description": "note",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"year": "{count, plural, other{Années} one{Année}}",
"@year": {
"description": "année",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"playVideos": "Lire les vidéos",
"playLabel": "Lire",
"forceRefresh": "Forcer le rafraîchissement",
"itemCount": "Nombre d'éléments: {count}",
"@itemCount": {
"description": "Nombre d'éléments",
"placeholders": {
"count": {
"type": "int"
}
}
},
"invalidUrl": "URL invalide",
"invalidUrlDesc": "L'URL doit commencer par http(s)://",
"incorrectPinTryAgain": "PIN incorrect, réessayez",
"somethingWentWrongPasswordCheck": "Quelque chose s'est mal passé, vérifiez votre mot de passe",
"unableToConnectHost": "Impossible de se connecter à l'hôte",
"server": "Serveur",
"retrievePublicListOfUsers": "Récupérer la liste publique des utilisateurs",
"displayLanguage": "Langue d'affichage",
"deleteItem": "Supprimer {item}?",
"@deleteItem": {
"description": "deleteItem",
"placeholders": {
"item": {
"type": "String"
}
}
},
"syncDeleteItemTitle": "Supprimer l'élément synchronisé",
"syncDeleteItemDesc": "Supprimer toutes les données synchronisées pour?\n{item}",
"@syncDeleteItemDesc": {
"description": "Fenêtre contextuelle de suppression d'élément synchronisé",
"placeholders": {
"item": {
"type": "String"
}
}
},
"syncOpenParent": "Ouvrir le parent",
"syncRemoveDataTitle": "Supprimer les données synchronisées?",
"syncRemoveDataDesc": "Supprimer les données vidéo synchronisées? Ceci est permanent et vous devrez resynchroniser les fichiers",
"collectionFolder": "Dossier de collection",
"musicAlbum": "Album",
"active": "Actif",
"name": "Nom",
"result": "Résultat",
"close": "Fermer",
"replaceAllImages": "Remplacer toutes les images",
"noResults": "Aucun résultat",
"openWebLink": "Ouvrir le lien web",
"setIdentityTo": "Définir l'identité sur {name}",
"@setIdentityTo": {
"description": "setIdentityTo",
"placeholders": {
"name": {
"type": "String"
}
}
},
"somethingWentWrong": "Quelque chose a mal tourné",
"clearChanges": "Effacer les modifications",
"useDefaults": "Utiliser les paramètres par défaut",
"light": "Léger",
"normal": "Normal",
"bold": "Gras",
"fontSize": "Taille de la police",
"heightOffset": "Décalage de hauteur",
"fontColor": "Couleur de la police",
"outlineColor": "Couleur du contour",
"outlineSize": "Taille du contour",
"backgroundOpacity": "Opacité de l'arrière-plan",
"shadow": "Ombre",
"played": "Joué",
"unPlayed": "Non joué",
"resumable": "Reprenable",
"sortOrder": "Ordre de tri",
"sortName": "Nom",
"communityRating": "Évaluation de la communauté",
"parentalRating": "Évaluation parentale",
"dateAdded": "Date d'ajout",
"dateLastContentAdded": "Date du dernier contenu ajouté",
"favorite": "Favori",
"datePlayed": "Date de lecture",
"folders": "Dossiers",
"playCount": "Nombre de lectures",
"releaseDate": "Date de sortie",
"runTime": "Durée",
"ascending": "Ascendant",
"descending": "Descendant",
"playFromStart": "Lire {name} depuis le début",
"@playFromStart": {
"description": "jouer depuis le début",
"placeholders": {
"name": {
"type": "String"
}
}
},
"minutes": "{count, plural, other{Minutes} one{Minute}}",
"@minutes": {
"description": "minute",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"seconds": "{count, plural, other{Secondes} one{Seconde}}",
"@seconds": {
"description": "seconde",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"page": "Page {index}",
"@page": {
"description": "page",
"placeholders": {
"index": {
"type": "int"
}
}
},
"set": "Régler",
"@set": {
"description": "Utiliser pour définir une certaine valeur",
"context": "Régler 'heure'"
},
"never": "Jamais",
"selectTime": "Sélectionner l'heure",
"immediately": "Immédiatement",
"timeAndAnnotation": "{minutes} et {seconds}",
"@timeAndAnnotation": {
"description": "timeAndAnnotation",
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"scanYourFingerprintToAuthenticate": "Scannez votre empreinte digitale pour authentifier {user}",
"@scanYourFingerprintToAuthenticate": {
"placeholders": {
"user": {
"type": "String"
}
}
},
"scanBiometricHint": "Vérifier identité",
"deleteFileFromSystem": "Supprimer cet élément {item} le supprimera à la fois du système de fichiers et de votre bibliothèque multimédia.\nÊtes-vous sûr de vouloir continuer?",
"@deleteFileFromSystem": {
"description": "Supprimer le fichier du système",
"placeholders": {
"item": {
"type": "String"
}
}
},
"notPartOfAlbum": "Ne fait pas partie d'un album",
"retry": "Réessayer",
"failedToLoadImage": "Échec du chargement de l'image",
"save": "Enregistrer",
"metaDataSavedFor": "Métadonnées enregistrées pour {item}",
"@metaDataSavedFor": {
"description": "métadonnéesEnregistréesPour",
"placeholders": {
"item": {
"type": "String"
}
}
},
"libraryPageSizeTitle": "Taille de la page de la bibliothèque",
"libraryPageSizeDesc": "Définir la quantité à charger à la fois. 0 désactive la pagination.",
"fetchingLibrary": "Chargement des éléments de la bibliothèque",
"libraryFetchNoItemsFound": "Aucun élément trouvé, essayez avec des paramètres différents.",
"noSuggestionsFound": "Aucune suggestion trouvée",
"viewPhotos": "Voir les photos",
"random": "Aléatoire",
"tag": "{count, plural, one{Étiquette} other{Étiquettes}}",
"@tag": {
"description": "étiquette",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"saved": "Enregistré",
"discovered": "Découvert",
"noServersFound": "Aucun nouveau serveur trouvé",
"about": "À propos",
"openParent": "Ouvrir le dossier parent",
"mouseDragSupport": "Faire glisser avec la souris",
"controls": "Commandes"
}

669
lib/l10n/app_jp.arb Normal file
View file

@ -0,0 +1,669 @@
{
"@@locale": "ja",
"switchUser": "ユーザーを切り替える",
"userName": "ユーザー名",
"password": "パスワード",
"login": "ログイン",
"logout": "ログアウト",
"cancel": "キャンセル",
"accept": "承認",
"code": "コード",
"error": "エラー",
"clear": "クリア",
"days": "日",
"search": "検索",
"loggedIn": "ログイン済み",
"change": "変更",
"other": "その他",
"dynamicText": "動的",
"enabled": "有効",
"disabled": "無効",
"dashboard": "ダッシュボード",
"advanced": "高度な",
"refresh": "リフレッシュ",
"delete": "削除",
"goTo": "行く",
"loop": "ループ",
"empty": "空",
"noRating": "評価なし",
"backgroundBlur": "背景ぼかし",
"autoPlay": "自動再生",
"resume": "{item}を再開する",
"@resume": {
"description": "再開する",
"placeholders": {
"item": {
"type": "String"
}
}
},
"play": "{item}を再生する",
"@play": {
"description": "再生する",
"placeholders": {
"item": {
"type": "String"
}
}
},
"read": "{item}を読む",
"@read": {
"description": "読む",
"placeholders": {
"item": {
"type": "String"
}
}
},
"readFromStart": "{item}を最初から読む",
"@readFromStart": {
"description": "最初から読む",
"placeholders": {
"item": {
"type": "String"
}
}
},
"playFrom": "{name}から再生する",
"@playFrom": {
"description": "から再生する",
"placeholders": {
"name": {
"type": "String"
}
}
},
"moreFrom": "{info}の詳細",
"@moreFrom": {
"description": "詳細",
"placeholders": {
"info": {
"type": "String"
}
}
},
"selectedWith": "{info}を選択済み",
"@selectedWith": {
"description": "選択済み",
"placeholders": {
"info": {
"type": "String"
}
}
},
"selected": "選択済み",
"restart": "再起動",
"reWatch": "再視聴",
"watchOn": "で見る",
"options": "オプション",
"list": "リスト",
"grid": "グリッド",
"masonry": "石工",
"start": "スタート",
"none": "なし",
"chapter": "{count, plural, other{チャプター} one{チャプター}}",
"@chapter": {
"description": "チャプター",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"sync": "同期",
"moreOptions": "その他のオプション",
"continuePage": "続行 - ページ {page}",
"@continuePage": {
"description": "続行 - ページ 1",
"placeholders": {
"page": {
"type": "int"
}
}
},
"openShow": "番組を開く",
"showDetails": "詳細を表示",
"showAlbum": "アルバムを表示",
"season": "{count, plural, other{シーズン} one{シーズン}}",
"@season": {
"description": "シーズン",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"episode": "{count, plural, other{エピソード} one{エピソード}}",
"@episode": {
"description": "エピソード",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"addToCollection": "コレクションに追加",
"addToPlaylist": "プレイリストに追加",
"removeFromCollection": "コレクションから削除",
"removeFromPlaylist": "プレイリストから削除",
"markAsWatched": "視聴済みにマーク",
"markAsUnwatched": "未視聴にマーク",
"removeAsFavorite": "お気に入りから削除",
"addAsFavorite": "お気に入りに追加",
"editMetadata": "メタデータを編集",
"refreshMetadata": "メタデータを更新",
"syncDetails": "詳細を同期",
"identify": "識別",
"info": "情報",
"clearAllSettings": "すべての設定をクリア",
"clearAllSettingsQuestion": "すべての設定をクリアしますか?",
"unableToReverseAction": "この操作は元に戻せません。すべての設定が削除されます。",
"navigationDashboard": "ダッシュボード",
"navigationFavorites": "お気に入り",
"navigationSync": "同期済み",
"navigation": "ナビゲーション",
"library": "{count, plural, other{ライブラリ} one{ライブラリ}}",
"@library": {
"description": "複数",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"scanLibrary": "ライブラリをスキャン",
"dashboardContinue": "続行",
"dashboardContinueWatching": "視聴を続ける",
"dashboardContinueReading": "読み続ける",
"dashboardContinueListening": "リスニングを続ける",
"dashboardNextUp": "次へ",
"dashboardRecentlyAdded": "{name}に最近追加",
"@dashboardRecentlyAdded": {
"description": "ホーム画面に最近追加",
"placeholders": {
"name": {
"type": "String"
}
}
},
"settings": "設定",
"settingsClientTitle": "Fladder",
"settingsClientDesc": "一般, タイムアウト, レイアウト, テーマ",
"settingsQuickConnectTitle": "クイック接続",
"settingsProfileTitle": "プロフィール",
"settingsProfileDesc": "ロック画面",
"settingsPlayerTitle": "プレーヤー",
"settingsPlayerDesc": "アスペクト比, 高度な",
"logoutUserPopupTitle": "ユーザー {userName} をログアウトしますか?",
"@logoutUserPopupTitle": {
"description": "ユーザーをログアウトするためのポップアップ",
"placeholders": {
"userName": {
"type": "String"
}
}
},
"logoutUserPopupContent": "これにより、{userName} がログアウトされ、アプリからユーザーが削除されます。\n{serverName}に再ログインする必要があります。",
"@logoutUserPopupContent": {
"description": "ユーザーをログアウトするためのポップアップの説明",
"placeholders": {
"userName": {
"type": "String"
},
"serverName": {
"type": "String"
}
}
},
"quickConnectTitle": "クイック接続",
"quickConnectAction": "クイック接続コードを入力",
"quickConnectInputACode": "コードを入力",
"quickConnectWrongCode": "コードが間違っています",
"downloadsTitle": "ダウンロード",
"downloadsPath": "パス",
"pathEditTitle": "場所を変更",
"pathEditSelect": "ダウンロード先を選択",
"pathClearTitle": "ダウンロードパスをクリア",
"pathEditDesc": "この場所はすべてのユーザーに設定されており、同期されたデータはもうアクセスできません。\nストレージに残ります。",
"downloadsSyncedData": "同期データ",
"downloadsClearTitle": "同期データをクリア",
"downloadsClearDesc": "すべての同期データを削除してもよろしいですか?\nこれにより、すべての同期ユーザーのデータがクリアされます",
"lockscreen": "ロック画面",
"timeOut": "タイムアウト",
"home": "ホーム",
"settingsHomeCarouselTitle": "ダッシュボードのカルーセル",
"settingsHomeCarouselDesc": "ダッシュボード画面にカルーセルを表示",
"settingsHomeNextUpTitle": "次のポスター",
"settingsHomeNextUpDesc": "ダッシュボード画面に表示されるポスターの種類",
"settingsVisual": "ビジュアル",
"settingsBlurredPlaceholderTitle": "ぼかしプレースホルダー",
"settingsBlurredPlaceholderDesc": "ポスターの読み込み中にぼかし背景を表示",
"settingsBlurEpisodesTitle": "次のエピソードをぼかす",
"settingsBlurEpisodesDesc": "次のエピソードをすべてぼかす",
"settingsEnableOsMediaControls": "OSメディアコントロールを有効にする",
"settingsNextUpCutoffDays": "次のカットオフ日",
"settingsShowScaleSlider": "ポスターサイズスライダーを表示",
"settingsPosterSize": "ポスターサイズ",
"settingsPosterSlider": "スケールスライダーを表示",
"settingsPosterPinch": "ポスターをスケールするためにピンチズーム",
"theme": "テーマ",
"mode": "モード",
"themeModeSystem": "システム",
"themeModeLight": "ライト",
"themeModeDark": "ダーク",
"themeColor": "テーマカラー",
"color": "カラー",
"amoledBlack": "アモレッドブラック",
"hide": "非表示",
"nextUp": "次へ",
"settingsContinue": "続行",
"separate": "分離",
"combined": "組み合わせ",
"settingsSecurity": "セキュリティ",
"settingSecurityApplockTitle": "アプリロック",
"appLockTitle": "{userName}のログイン方法を設定",
"@appLockTitle": {
"description": "ログイン方法を選択するポップアップ",
"placeholders": {
"userName": {
"type": "String"
}
}
},
"biometricsFailedCheckAgain": "生体認証に失敗しました。設定を確認してもう一度お試しください。",
"appLockAutoLogin": "自動ログイン",
"appLockPasscode": "パスコード",
"appLockBiometrics": "生体認証",
"settingsPlayerVideoHWAccelTitle": "ハードウェアアクセラレーション",
"settingsPlayerVideoHWAccelDesc": "ビデオをレンダリングするためにGPUを使用する推奨",
"settingsPlayerNativeLibassAccelTitle": "ネイティブlibass字幕",
"settingsPlayerNativeLibassAccelDesc": "ビデオプレーヤーlibass字幕レンダラーを使用する",
"settingsPlayerMobileWarning": "Androidでハードウェアアクセラレーションとネイティブlibass字幕を有効にすると、一部の字幕がレンダリングされない場合があります。",
"settingsPlayerCustomSubtitlesTitle": "字幕のカスタマイズ",
"settingsPlayerCustomSubtitlesDesc": "サイズ、色、位置、アウトラインをカスタマイズする",
"videoScalingFillScreenTitle": "画面全体に表示",
"videoScalingFillScreenDesc": "ナビゲーションバーとステータスバーを埋める",
"videoScalingFillScreenNotif": "画面全体はビデオフィットを上書きし、水平回転で",
"videoScaling": "ビデオスケーリング",
"videoScalingFill": "全体に表示",
"videoScalingContain": "含む",
"videoScalingCover": "カバー",
"videoScalingFitWidth": "幅に合わせる",
"videoScalingFitHeight": "高さに合わせる",
"videoScalingScaleDown": "スケールダウン",
"subtitleConfiguratorPlaceHolder": "これはプレースホルダーテキストです。\nここには何もありません。",
"subtitleConfigurator": "字幕コンフィギュレーター",
"refreshPopup": "リフレッシュ - {name}",
"@refreshPopup": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"scanningName": "スキャン中 - {name}",
"@scanningName": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"refreshPopupContentMetadata": "メタデータはダッシュボードで有効にされた設定とインターネットサービスに基づいて更新されます。",
"replaceExistingImages": "既存の画像を置き換える",
"metadataRefreshDefault": "新しいファイルと更新されたファイルをスキャン",
"metadataRefreshValidation": "欠落しているメタデータを検索",
"metadataRefreshFull": "すべてのメタデータを置き換える",
"syncedItems": "同期されたアイテム",
"noItemsSynced": "同期されたアイテムはありません",
"syncDeletePopupPermanent": "この操作は恒久的であり、すべてのローカルに同期されたファイルを削除します",
"totalSize": "合計サイズ: {size}",
"@totalSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mediaTypeBase": "基本タイプ",
"mediaTypeMovie": "映画",
"mediaTypeSeries": "シリーズ",
"mediaTypeSeason": "シーズン",
"mediaTypeEpisode": "エピソード",
"mediaTypePhoto": "写真",
"mediaTypePerson": "人物",
"mediaTypePhotoAlbum": "フォトアルバム",
"mediaTypeFolder": "フォルダー",
"mediaTypeBoxset": "ボックスセット",
"mediaTypePlaylist": "プレイリスト",
"mediaTypeBook": "本",
"actor": "{count, plural, other{俳優} one{俳優}}",
"@actor": {
"description": "俳優",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"writer": "{count, plural, other{作家} one{作家}}",
"@writer": {
"description": "作家",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"director": "{count, plural, other{監督} one{監督}}",
"@director": {
"description": "監督",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"video": "ビデオ",
"audio": "オーディオ",
"subtitles": "字幕",
"related": "関連",
"all": "すべて",
"overview": "概要",
"selectViewType": "ビュータイプを選択",
"noItemsToShow": "表示するアイテムがありません",
"sortBy": "並び替え",
"groupBy": "グループ化",
"scrollToTop": "トップにスクロール",
"disableFilters": "フィルターを無効にする",
"selectAll": "すべて選択",
"clearSelection": "選択をクリア",
"shuffleVideos": "ビデオをシャッフル",
"shuffleGallery": "ギャラリーをシャッフル",
"unknown": "不明",
"favorites": "お気に入り",
"recursive": "再帰的",
"genre": "{count, plural, other{ジャンル} one{ジャンル}}",
"@genre": {
"description": "ジャンル",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"studio": "{count, plural, other{スタジオ} one{スタジオ}}",
"@studio": {
"description": "スタジオ",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"label": "{count, plural, other{ラベル} one{ラベル}}",
"@label": {
"description": "ラベル",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"group": "グループ",
"type": "{count, plural, other{タイプ} one{タイプ}}",
"@type": {
"description": "タイプ",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"filter": "{count, plural, other{フィルター} one{フィルター}}",
"@filter": {
"description": "フィルター",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"showEmpty": "空を表示",
"hideEmpty": "空を非表示",
"rating": "{count, plural, other{評価} one{評価}}",
"@rating": {
"description": "評価",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"year": "{count, plural, other{年} one{年}}",
"@year": {
"description": "年",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"playVideos": "ビデオを再生",
"playLabel": "再生",
"forceRefresh": "強制リフレッシュ",
"itemCount": "アイテム数: {count}",
"@itemCount": {
"description": "アイテム数",
"placeholders": {
"count": {
"type": "int"
}
}
},
"invalidUrl": "無効なURL",
"invalidUrlDesc": "URLはhttp(s)://で始まる必要があります",
"incorrectPinTryAgain": "PINが間違っています、もう一度試してください",
"somethingWentWrongPasswordCheck": "何かがうまくいかなかった、パスワードを確認してください",
"unableToConnectHost": "ホストに接続できません",
"server": "サーバー",
"retrievePublicListOfUsers": "ユーザーの公開リストを取得",
"displayLanguage": "表示言語",
"deleteItem": "{item}を削除しますか?",
"@deleteItem": {
"description": "deleteItem",
"placeholders": {
"item": {
"type": "String"
}
}
},
"syncDeleteItemTitle": "同期されたアイテムを削除",
"syncDeleteItemDesc": "すべての同期されたデータを削除しますか?\n{item}",
"@syncDeleteItemDesc": {
"description": "同期削除アイテムのポップアップウィンドウ",
"placeholders": {
"item": {
"type": "String"
}
}
},
"syncOpenParent": "親を開く",
"syncRemoveDataTitle": "同期データを削除しますか?",
"syncRemoveDataDesc": "同期されたビデオデータを削除しますか?これは永久的なもので、ファイルを再同期する必要があります",
"collectionFolder": "コレクションフォルダ",
"musicAlbum": "アルバム",
"active": "アクティブ",
"name": "名前",
"result": "結果",
"close": "閉じる",
"replaceAllImages": "すべての画像を置き換える",
"noResults": "結果がありません",
"openWebLink": "ウェブリンクを開く",
"setIdentityTo": "{name}にアイデンティティを設定",
"@setIdentityTo": {
"description": "setIdentityTo",
"placeholders": {
"name": {
"type": "String"
}
}
},
"somethingWentWrong": "問題が発生しました",
"clearChanges": "変更をクリア",
"useDefaults": "デフォルトを使用",
"light": "ライト",
"normal": "ノーマル",
"bold": "ボールド",
"fontSize": "フォントサイズ",
"heightOffset": "高さのオフセット",
"fontColor": "フォントの色",
"outlineColor": "アウトラインの色",
"outlineSize": "アウトラインのサイズ",
"backgroundOpacity": "背景の不透明度",
"shadow": "影",
"played": "再生済み",
"unPlayed": "未再生",
"resumable": "再開可能",
"sortOrder": "並び順",
"sortName": "名前",
"communityRating": "コミュニティ評価",
"parentalRating": "ペアレンタル評価",
"dateAdded": "追加日",
"dateLastContentAdded": "最後にコンテンツが追加された日",
"favorite": "お気に入り",
"datePlayed": "再生日",
"folders": "フォルダ",
"playCount": "再生回数",
"releaseDate": "リリース日",
"runTime": "再生時間",
"ascending": "昇順",
"descending": "降順",
"playFromStart": "{name} を最初から再生",
"@playFromStart": {
"description": "最初から再生",
"placeholders": {
"name": {
"type": "String"
}
}
},
"minutes": "分",
"@minutes": {
"description": "分",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"seconds": "秒",
"@seconds": {
"description": "秒",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"page": "{index} ページ",
"@page": {
"description": "ページ",
"placeholders": {
"index": {
"type": "int"
}
}
},
"set": "設定",
"@set": {
"description": "特定の値を設定するために使用",
"context": "時間を設定"
},
"never": "なし",
"selectTime": "時間を選択",
"immediately": "すぐに",
"timeAndAnnotation": "{minutes} と {seconds}",
"@timeAndAnnotation": {
"description": "timeAndAnnotation",
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"scanYourFingerprintToAuthenticate": "指紋をスキャンして{user}を認証してください",
"@scanYourFingerprintToAuthenticate": {
"placeholders": {
"user": {
"type": "String"
}
}
},
"scanBiometricHint": "身元を確認する",
"deleteFileFromSystem": "このアイテム {item} を削除すると、ファイルシステムとメディアライブラリの両方から削除されます。\n続行してもよろしいですか",
"@deleteFileFromSystem": {
"description": "システムからファイルを削除",
"placeholders": {
"item": {
"type": "String"
}
}
},
"notPartOfAlbum": "アルバムの一部ではありません",
"retry": "再試行",
"failedToLoadImage": "画像の読み込みに失敗しました",
"save": "保存",
"metaDataSavedFor": "{item} のメタデータが保存されました",
"@metaDataSavedFor": {
"description": "メタデータが保存されました",
"placeholders": {
"item": {
"type": "String"
}
}
},
"libraryPageSizeTitle": "ライブラリページのサイズ",
"libraryPageSizeDesc": "一度に読み込む量を設定します。0はページングを無効にします。",
"fetchingLibrary": "ライブラリアイテムを取得しています",
"libraryFetchNoItemsFound": "アイテムが見つかりませんでした。設定を変更してみてください。",
"noSuggestionsFound": "提案が見つかりません",
"viewPhotos": "写真を見る",
"random": "ランダム",
"tag": "{count, plural, one{タグ} other{タグ}}",
"@tag": {
"description": "タグ",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"saved": "保存済み",
"discovered": "発見済み",
"noServersFound": "新しいサーバーが見つかりません",
"about": "情報",
"openParent": "親フォルダを開く",
"mouseDragSupport": "マウスでドラッグ",
"controls": "コントロール"
}

669
lib/l10n/app_nl.arb Normal file
View file

@ -0,0 +1,669 @@
{
"@@locale": "nl",
"switchUser": "Wissel gebruiker",
"userName": "Gebruikersnaam",
"password": "Wachtwoord",
"login": "Inloggen",
"logout": "Uitloggen",
"cancel": "Annuleren",
"accept": "Accepteren",
"code": "Code",
"error": "Fout",
"clear": "Wissen",
"days": "Dagen",
"search": "Zoeken",
"loggedIn": "Ingelogd",
"change": "Wijzig",
"other": "Overig",
"dynamicText": "Dynamisch",
"enabled": "Ingeschakeld",
"disabled": "Uitgeschakeld",
"dashboard": "Dashboard",
"advanced": "Geavanceerd",
"refresh": "Vernieuwen",
"delete": "Verwijderen",
"goTo": "Ga naar",
"loop": "Lus",
"empty": "Leeg",
"noRating": "Geen beoordeling",
"backgroundBlur": "Achtergrond vervagen",
"autoPlay": "Automatisch afspelen",
"resume": "{item} Hervatten",
"@resume": {
"description": "hervatten",
"placeholders": {
"item": {
"type": "String"
}
}
},
"play": "{item} Afspelen",
"@play": {
"description": "Speel met",
"placeholders": {
"item": {
"type": "String"
}
}
},
"read": "{item} Lezen",
"@read": {
"description": "lezen",
"placeholders": {
"item": {
"type": "String"
}
}
},
"readFromStart": "{item} vanaf het begin lezen",
"@readFromStart": {
"description": "Lees boek vanaf het begin",
"placeholders": {
"item": {
"type": "String"
}
}
},
"playFrom": "Speel vanaf {name}",
"@playFrom": {
"description": "speel vanaf",
"placeholders": {
"name": {
"type": "String"
}
}
},
"playFromStart": "{name} Vanaf het begin afspelen",
"@playFromStart": {
"description": "speel vanaf het begin",
"placeholders": {
"name": {
"type": "String"
}
}
},
"moreFrom": "Meer van {info}",
"@moreFrom": {
"description": "Meer van",
"placeholders": {
"info": {
"type": "String"
}
}
},
"selectedWith": "Geselecteerd {info}",
"@selectedWith": {
"description": "geselecteerd",
"placeholders": {
"info": {
"type": "String"
}
}
},
"selected": "Geselecteerd",
"restart": "Herstarten",
"reWatch": "Opnieuw bekijken",
"watchOn": "Kijk op",
"options": "Opties",
"list": "Lijst",
"grid": "Raster",
"masonry": "Metseleffect",
"start": "Start",
"none": "Geen",
"chapter": "{count, plural, other{Hoofdstukken} one{Hoofdstuk}}",
"@chapter": {
"description": "hoofdstuk",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"sync": "Synchroniseren",
"moreOptions": "Meer opties",
"continuePage": "Doorgaan - pagina {page}",
"@continuePage": {
"description": "Doorgaan - pagina 1",
"placeholders": {
"page": {
"type": "int"
}
}
},
"openShow": "Open show",
"showDetails": "Toon details",
"showAlbum": "Toon album",
"season": "{count, plural, other{Seizoenen} one{Seizoen}}",
"@season": {
"description": "seizoen",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"episode": "{count, plural, other{Afleveringen} one{Aflevering}}",
"@episode": {
"description": "aflevering",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"addToCollection": "Toevoegen aan collectie",
"addToPlaylist": "Toevoegen aan afspeellijst",
"removeFromCollection": "Verwijderen uit collectie",
"removeFromPlaylist": "Verwijderen uit afspeellijst",
"markAsWatched": "Markeer als bekeken",
"markAsUnwatched": "Markeer als niet bekeken",
"removeAsFavorite": "Verwijderen als favoriet",
"addAsFavorite": "Toevoegen als favoriet",
"editMetadata": "Metadata bewerken",
"refreshMetadata": "Metadata vernieuwen",
"syncDetails": "Details synchroniseren",
"identify": "Identificeren",
"info": "Info",
"clearAllSettings": "Alle instellingen wissen",
"clearAllSettingsQuestion": "Alle instellingen wissen?",
"unableToReverseAction": "Deze actie kan niet ongedaan worden gemaakt, het verwijdert alle instellingen.",
"navigationDashboard": "Dashboard",
"navigationFavorites": "Favorieten",
"navigationSync": "Gesynchroniseerd",
"navigation": "Navigatie",
"library": "{count, plural, other{Bibliotheken} one{Bibliotheek}}",
"@library": {
"description": "meervoud",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"scanLibrary": "Scan bibliotheek",
"dashboardContinue": "Doorgaan",
"dashboardContinueWatching": "Doorgaan met kijken",
"dashboardContinueReading": "Doorgaan met lezen",
"dashboardContinueListening": "Doorgaan met luisteren",
"dashboardNextUp": "Volgende",
"dashboardRecentlyAdded": "Onlangs toegevoegd in {name}",
"@dashboardRecentlyAdded": {
"description": "Onlangs toegevoegd op startscherm",
"placeholders": {
"name": {
"type": "String"
}
}
},
"settings": "Instellingen",
"settingsClientTitle": "Fladder",
"settingsClientDesc": "Algemeen, Time-out, Lay-out, Thema",
"settingsQuickConnectTitle": "Snel verbinden",
"settingsProfileTitle": "Profiel",
"settingsProfileDesc": "Vergrendelscherm",
"settingsPlayerTitle": "Speler",
"settingsPlayerDesc": "Beeldverhouding, Geavanceerd",
"logoutUserPopupTitle": "Gebruiker {userName} uitloggen?",
"@logoutUserPopupTitle": {
"description": "Pop-up voor uitloggen gebruiker",
"placeholders": {
"userName": {
"type": "String"
}
}
},
"logoutUserPopupContent": "Dit zal {userName} uitloggen en de gebruiker uit de app verwijderen.\nJe moet opnieuw inloggen op {serverName}.",
"@logoutUserPopupContent": {
"description": "Pop-up voor uitloggen gebruiker beschrijving",
"placeholders": {
"userName": {
"type": "String"
},
"serverName": {
"type": "String"
}
}
},
"quickConnectTitle": "Snel verbinden",
"quickConnectAction": "Voer snelverbind code in voor",
"quickConnectInputACode": "Voer een code in",
"quickConnectWrongCode": "Verkeerde code",
"downloadsTitle": "Downloads",
"downloadsPath": "Pad",
"pathEditTitle": "Locatie wijzigen",
"pathEditSelect": "Selecteer downloadbestemming",
"pathClearTitle": "Downloadpad wissen",
"pathEditDesc": "Deze locatie is ingesteld voor alle gebruikers, gesynchroniseerde gegevens zijn niet meer toegankelijk.\nHet blijft op je opslag.",
"downloadsSyncedData": "Gesynchroniseerde gegevens",
"downloadsClearTitle": "Gesynchroniseerde gegevens wissen",
"downloadsClearDesc": "Weet je zeker dat je alle gesynchroniseerde gegevens wilt verwijderen?\nDit wist alle gegevens voor elke gesynchroniseerde gebruiker!",
"lockscreen": "Vergrendelscherm",
"timeOut": "Time-out",
"home": "Home",
"settingsHomeCarouselTitle": "Dashboard carrousel",
"settingsHomeCarouselDesc": "Toont een carrousel op het dashboard scherm",
"settingsHomeNextUpTitle": "Volgende posters",
"settingsHomeNextUpDesc": "Soort posters getoond op het dashboard scherm",
"settingsVisual": "Visueel",
"settingsBlurredPlaceholderTitle": "Vervaagde placeholder",
"settingsBlurredPlaceholderDesc": "Toon vervaagde achtergrond bij het laden van posters",
"settingsBlurEpisodesTitle": "Vervagen volgende afleveringen",
"settingsBlurEpisodesDesc": "Vervaag alle komende afleveringen",
"settingsEnableOsMediaControls": "OS media bediening inschakelen",
"settingsNextUpCutoffDays": "Volgende cutoff dagen",
"settingsShowScaleSlider": "Toon posterschaal schuifregelaar",
"settingsPosterSize": "Poster grootte",
"settingsPosterSlider": "Toon schaal schuifregelaar",
"settingsPosterPinch": "Knijp-zoom om posters te schalen",
"theme": "Thema",
"mode": "Modus",
"themeModeSystem": "Systeem",
"themeModeLight": "Licht",
"themeModeDark": "Donker",
"themeColor": "Thema kleur",
"color": "Kleur",
"amoledBlack": "Amoled zwart",
"hide": "Verbergen",
"nextUp": "Volgende",
"settingsContinue": "Doorgaan",
"separate": "Gescheiden",
"combined": "Gecombineerd",
"settingsSecurity": "Beveiliging",
"settingSecurityApplockTitle": "App vergrendelen",
"appLockTitle": "Stel de inlogmethode in voor {userName}",
"@appLockTitle": {
"description": "Pop-up om een inlogmethode te kiezen",
"placeholders": {
"userName": {
"type": "String"
}
}
},
"biometricsFailedCheckAgain": "Biometrie mislukt, controleer de instellingen en probeer opnieuw",
"appLockAutoLogin": "Automatisch inloggen",
"appLockPasscode": "Wachtwoord",
"appLockBiometrics": "Biometrie",
"settingsPlayerVideoHWAccelTitle": "Hardwareversnelling",
"settingsPlayerVideoHWAccelDesc": "Gebruik de gpu om video weer te geven (aanbevolen)",
"settingsPlayerNativeLibassAccelTitle": "Native libass ondertiteling",
"settingsPlayerNativeLibassAccelDesc": "Gebruik videospeler libass ondertitel renderer",
"settingsPlayerMobileWarning": "Hardwareversnelling en native libass ondertitels inschakelen op Android kan ervoor zorgen dat sommige ondertitels niet worden weergegeven.",
"settingsPlayerCustomSubtitlesTitle": "Ondertitels aanpassen",
"settingsPlayerCustomSubtitlesDesc": "Pas grootte, kleur, positie, omtrek aan",
"videoScalingFillScreenTitle": "Vul scherm",
"videoScalingFillScreenDesc": "Vul de navigatie- en statusbalk",
"videoScalingFillScreenNotif": "Volledig scherm overschrijft videopasvorm, in horizontale rotatie",
"videoScaling": "Videoschaling",
"videoScalingFill": "Vullen",
"videoScalingContain": "Bevatten",
"videoScalingCover": "Bedekken",
"videoScalingFitWidth": "Pas breedte aan",
"videoScalingFitHeight": "Pas hoogte aan",
"videoScalingScaleDown": "Schaal omlaag",
"subtitleConfiguratorPlaceHolder": "Dit is placeholder tekst,\nniets te zien hier.",
"subtitleConfigurator": "Ondertitel configurator",
"refreshPopup": "Vernieuwen - {name}",
"@refreshPopup": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"scanningName": "Scannen - {name}",
"@scanningName": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"refreshPopupContentMetadata": "Metadata wordt vernieuwd op basis van instellingen en internetdiensten die zijn ingeschakeld in het Dashboard.",
"replaceExistingImages": "Bestaande afbeeldingen vervangen",
"metadataRefreshDefault": "Scannen naar nieuwe en bijgewerkte bestanden",
"metadataRefreshValidation": "Zoeken naar ontbrekende metadata",
"metadataRefreshFull": "Alle metadata vervangen",
"syncedItems": "Gesynchroniseerde items",
"noItemsSynced": "Geen items gesynchroniseerd",
"syncDeletePopupPermanent": "Deze actie is permanent en verwijdert alle lokaal gesynchroniseerde bestanden",
"totalSize": "Totale grootte: {size}",
"@totalSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mediaTypeBase": "Basis type",
"mediaTypeMovie": "Film",
"mediaTypeSeries": "Serie",
"mediaTypeSeason": "Seizoen",
"mediaTypeEpisode": "Aflevering",
"mediaTypePhoto": "Foto",
"mediaTypePerson": "Persoon",
"mediaTypePhotoAlbum": "Foto album",
"mediaTypeFolder": "Map",
"mediaTypeBoxset": "Boxset",
"mediaTypePlaylist": "Afspeellijst",
"mediaTypeBook": "Boek",
"actor": "{count, plural, other{Acteurs} one{Acteur}}",
"@actor": {
"description": "acteur",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"writer": "{count, plural, other{Schrijvers} one{Schrijver}}",
"@writer": {
"description": "schrijver",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"director": "{count, plural, other{Regisseurs} one{Regisseur}}",
"@director": {
"description": "regisseur",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"video": "Video",
"audio": "Audio",
"subtitles": "Ondertitels",
"related": "Gerelateerd",
"all": "Alles",
"overview": "Overzicht",
"selectViewType": "Selecteer weergavetype",
"noItemsToShow": "Geen items om te tonen",
"sortBy": "Sorteer op",
"groupBy": "Groepeer op",
"scrollToTop": "Scroll naar boven",
"disableFilters": "Filters uitschakelen",
"selectAll": "Alles selecteren",
"clearSelection": "Selectie wissen",
"shuffleVideos": "Video's shuffle",
"shuffleGallery": "Galerij shuffle",
"unknown": "Onbekend",
"favorites": "Favorieten",
"recursive": "Recursief",
"genre": "{count, plural, other{Genres} one{Genre}}",
"@genre": {
"description": "genre",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"studio": "{count, plural, other{Studio's} one{Studio}}",
"@studio": {
"description": "studio",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"label": "{count, plural, other{Labels} one{Label}}",
"@label": {
"description": "label",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"group": "Groeperen",
"type": "{count, plural, other{Types} one{Type}}",
"@type": {
"description": "type",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"filter": "{count, plural, other{Filters} one{Filter}}",
"@filter": {
"description": "filter",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"showEmpty": "Toon leeg",
"hideEmpty": "Verberg leeg",
"rating": "{count, plural, other{Beoordelingen} one{Beoordeling}}",
"@rating": {
"description": "beoordeling",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"year": "{count, plural, other{Jaren} one{Jaar}}",
"@year": {
"description": "jaar",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"playVideos": "Video's afspelen",
"playLabel": "Afspelen",
"forceRefresh": "Geforceerd vernieuwen",
"itemCount": "Aantal items: {count}",
"@itemCount": {
"description": "Aantal items",
"placeholders": {
"count": {
"type": "int"
}
}
},
"invalidUrl": "Ongeldige url",
"invalidUrlDesc": "Url moet beginnen met http(s)://",
"incorrectPinTryAgain": "Onjuiste pin, probeer opnieuw",
"somethingWentWrongPasswordCheck": "Er is iets misgegaan, controleer uw wachtwoord",
"unableToConnectHost": "Kan geen verbinding maken met host",
"server": "Server",
"retrievePublicListOfUsers": "Publieke lijst van gebruikers ophalen",
"displayLanguage": "Weergavetaal",
"deleteItem": "{item} verwijderen?",
"@deleteItem": {
"description": "deleteItem",
"placeholders": {
"item": {
"type": "String"
}
}
},
"syncDeleteItemTitle": "Gesynchroniseerd item verwijderen",
"syncDeleteItemDesc": "Alle gesynchroniseerde gegevens verwijderen voor?\n{item}",
"@syncDeleteItemDesc": {
"description": "Pop-upvenster voor synchronisatie van te verwijderen item",
"placeholders": {
"item": {
"type": "String"
}
}
},
"syncOpenParent": "Ouder openen",
"syncRemoveDataTitle": "Gesynchroniseerde gegevens verwijderen?",
"syncRemoveDataDesc": "Gesynchroniseerde videogegevens verwijderen? Dit is permanent en u moet de bestanden opnieuw synchroniseren",
"collectionFolder": "Collectiemap",
"musicAlbum": "Album",
"active": "Actief",
"name": "Naam",
"result": "Resultaat",
"close": "Sluiten",
"replaceAllImages": "Alle afbeeldingen vervangen",
"noResults": "Geen resultaten",
"openWebLink": "Weblink openen",
"setIdentityTo": "Identiteit instellen op {name}",
"@setIdentityTo": {
"description": "setIdentityTo",
"placeholders": {
"name": {
"type": "String"
}
}
},
"somethingWentWrong": "Er is iets misgegaan",
"clearChanges": "Wijzigingen wissen",
"useDefaults": "Standaardwaarden gebruiken",
"light": "Licht",
"normal": "Normaal",
"bold": "Vet",
"fontSize": "Lettergrootte",
"heightOffset": "Hoogte-offset",
"fontColor": "Letterkleur",
"outlineColor": "Omtreklijnkleur",
"outlineSize": "Omtreklijn grootte",
"backgroundOpacity": "Achtergrond doorzichtigheid",
"shadow": "Schaduw",
"played": "Gespeeld",
"unPlayed": "Ongespeeld",
"resumable": "Hervatbaar",
"sortOrder": "Sorteervolgorde",
"sortName": "Naam",
"communityRating": "Gemeenschapswaardering",
"parentalRating": "Ouderlijk toezicht beoordeling",
"dateAdded": "Datum toegevoegd",
"dateLastContentAdded": "Datum laatste inhoud toegevoegd",
"favorite": "Favoriet",
"datePlayed": "Datum afgespeeld",
"folders": "Mappen",
"playCount": "Aantal keren afgespeeld",
"releaseDate": "Releasedatum",
"runTime": "Looptijd",
"ascending": "Oplopend",
"descending": "Aflopend",
"minutes": "{count, plural, other{Minuten} one{Minuut}}",
"@minutes": {
"description": "minuut",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"seconds": "{count, plural, other{Seconden} one{Seconde}}",
"@seconds": {
"description": "seconde",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"page": "Pagina {index}",
"@page": {
"description": "pagina",
"placeholders": {
"index": {
"type": "int"
}
}
},
"set": "Instellen",
"@set": {
"description": "Gebruik voor het instellen van een bepaalde waarde",
"context": "Instellen 'tijd'"
},
"never": "Nooit",
"selectTime": "Tijd selecteren",
"immediately": "Direct",
"timeAndAnnotation": "{minutes} en {seconds}",
"@timeAndAnnotation": {
"description": "timeAndAnnotation",
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"scanYourFingerprintToAuthenticate": "Scan je vingerafdruk om {user} te verifiëren",
"@scanYourFingerprintToAuthenticate": {
"placeholders": {
"user": {
"type": "String"
}
}
},
"scanBiometricHint": "Identiteit verifiëren",
"deleteFileFromSystem": "Het verwijderen van dit item {item} zal het zowel uit het bestandssysteem als uit je mediatheek verwijderen.\nWeet je zeker dat je wilt doorgaan?",
"@deleteFileFromSystem": {
"description": "Bestand uit systeem verwijderen",
"placeholders": {
"item": {
"type": "String"
}
}
},
"notPartOfAlbum": "Maakt geen deel uit van een album",
"retry": "Opnieuw proberen",
"failedToLoadImage": "Kan afbeelding niet laden",
"save": "Opslaan",
"metaDataSavedFor": "Metadata opgeslagen voor {item}",
"@metaDataSavedFor": {
"description": "metadataOpgeslagenVoor",
"placeholders": {
"item": {
"type": "String"
}
}
},
"libraryPageSizeTitle": "Bibliotheek paginagrootte",
"libraryPageSizeDesc": "Stel de hoeveelheid in om per keer te laden. 0 schakelt paginering uit.",
"fetchingLibrary": "Bibliotheek items ophalen",
"libraryFetchNoItemsFound": "Geen items gevonden, probeer andere instellingen.",
"noSuggestionsFound": "Geen suggesties gevonden",
"viewPhotos": "Foto's bekijken",
"random": "Willekeurig",
"tag": "{count, plural, one{Label} other{Labels}}",
"@tag": {
"description": "label",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"saved": "Opgeslagen",
"discovered": "Ontdekt",
"noServersFound": "Geen nieuwe servers gevonden",
"about": "Over",
"openParent": "Bovenliggende map openen",
"mouseDragSupport": "Slepen met de muis",
"controls": "Bediening"
}

669
lib/l10n/app_zh.arb Normal file
View file

@ -0,0 +1,669 @@
{
"@@locale": "zh",
"switchUser": "切换用户",
"userName": "用户名",
"password": "密码",
"login": "登录",
"logout": "登出",
"cancel": "取消",
"accept": "接受",
"code": "代码",
"error": "错误",
"clear": "清除",
"days": "天",
"search": "搜索",
"loggedIn": "已登录",
"change": "更改",
"other": "其他",
"dynamicText": "动态",
"enabled": "启用",
"disabled": "禁用",
"dashboard": "仪表板",
"advanced": "高级",
"refresh": "刷新",
"delete": "删除",
"goTo": "去往",
"loop": "循环",
"empty": "空",
"noRating": "无评分",
"backgroundBlur": "背景模糊",
"autoPlay": "自动播放",
"resume": "继续 {item}",
"@resume": {
"description": "继续",
"placeholders": {
"item": {
"type": "String"
}
}
},
"play": "播放 {item}",
"@play": {
"description": "播放",
"placeholders": {
"item": {
"type": "String"
}
}
},
"read": "阅读 {item}",
"@read": {
"description": "阅读",
"placeholders": {
"item": {
"type": "String"
}
}
},
"readFromStart": "从头开始阅读 {item}",
"@readFromStart": {
"description": "从头开始阅读书籍",
"placeholders": {
"item": {
"type": "String"
}
}
},
"playFrom": "从 {name} 开始播放",
"@playFrom": {
"description": "从...开始播放",
"placeholders": {
"name": {
"type": "String"
}
}
},
"moreFrom": "更多来自 {info}",
"@moreFrom": {
"description": "更多来自",
"placeholders": {
"info": {
"type": "String"
}
}
},
"selectedWith": "选择了 {info}",
"@selectedWith": {
"description": "选择",
"placeholders": {
"info": {
"type": "String"
}
}
},
"selected": "已选择",
"restart": "重新开始",
"reWatch": "重新观看",
"watchOn": "观看于",
"options": "选项",
"list": "列表",
"grid": "网格",
"masonry": "砖石",
"start": "开始",
"none": "无",
"chapter": "{count, plural, other{章节} one{章节}}",
"@chapter": {
"description": "章节",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"sync": "同步",
"moreOptions": "更多选项",
"continuePage": "继续 - 第 {page} 页",
"@continuePage": {
"description": "继续 - 第 1 页",
"placeholders": {
"page": {
"type": "int"
}
}
},
"openShow": "打开节目",
"showDetails": "显示详情",
"showAlbum": "显示相册",
"season": "{count, plural, other{季} one{季}}",
"@season": {
"description": "季",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"episode": "{count, plural, other{集} one{集}}",
"@episode": {
"description": "集",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"addToCollection": "添加到收藏",
"addToPlaylist": "添加到播放列表",
"removeFromCollection": "从收藏中删除",
"removeFromPlaylist": "从播放列表中删除",
"markAsWatched": "标记为已观看",
"markAsUnwatched": "标记为未观看",
"removeAsFavorite": "取消收藏",
"addAsFavorite": "添加为收藏",
"editMetadata": "编辑元数据",
"refreshMetadata": "刷新元数据",
"syncDetails": "同步详情",
"identify": "识别",
"info": "信息",
"clearAllSettings": "清除所有设置",
"clearAllSettingsQuestion": "清除所有设置?",
"unableToReverseAction": "此操作无法撤销,它将删除所有设置。",
"navigationDashboard": "仪表板",
"navigationFavorites": "收藏夹",
"navigationSync": "已同步",
"navigation": "导航",
"library": "{count, plural, other{库} one{库}}",
"@library": {
"description": "复数",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"scanLibrary": "扫描库",
"dashboardContinue": "继续",
"dashboardContinueWatching": "继续观看",
"dashboardContinueReading": "继续阅读",
"dashboardContinueListening": "继续听",
"dashboardNextUp": "下一个",
"dashboardRecentlyAdded": "最近添加于 {name}",
"@dashboardRecentlyAdded": {
"description": "最近添加到首页",
"placeholders": {
"name": {
"type": "String"
}
}
},
"settings": "设置",
"settingsClientTitle": "Fladder",
"settingsClientDesc": "常规,超时,布局,主题",
"settingsQuickConnectTitle": "快速连接",
"settingsProfileTitle": "个人资料",
"settingsProfileDesc": "锁屏",
"settingsPlayerTitle": "播放器",
"settingsPlayerDesc": "纵横比,高级",
"logoutUserPopupTitle": "登出用户 {userName}",
"@logoutUserPopupTitle": {
"description": "登出用户的弹窗",
"placeholders": {
"userName": {
"type": "String"
}
}
},
"logoutUserPopupContent": "这将登出 {userName} 并从应用程序中删除用户。\n你需要重新登录到 {serverName}。",
"@logoutUserPopupContent": {
"description": "登出用户的弹窗描述",
"placeholders": {
"userName": {
"type": "String"
},
"serverName": {
"type": "String"
}
}
},
"quickConnectTitle": "快速连接",
"quickConnectAction": "输入快速连接代码",
"quickConnectInputACode": "输入代码",
"quickConnectWrongCode": "错误的代码",
"downloadsTitle": "下载",
"downloadsPath": "路径",
"pathEditTitle": "更改位置",
"pathEditSelect": "选择下载目标",
"pathClearTitle": "清除下载路径",
"pathEditDesc": "此位置设置适用于所有用户,任何同步的数据将不再可访问。\n它将保留在你的存储中。",
"downloadsSyncedData": "同步数据",
"downloadsClearTitle": "清除同步数据",
"downloadsClearDesc": "你确定要删除所有同步数据吗?\n这将清除每个同步用户的所有数据",
"lockscreen": "锁屏",
"timeOut": "超时",
"home": "主页",
"settingsHomeCarouselTitle": "仪表板轮播",
"settingsHomeCarouselDesc": "在仪表板屏幕上显示轮播",
"settingsHomeNextUpTitle": "下一个海报",
"settingsHomeNextUpDesc": "仪表板屏幕上显示的海报类型",
"settingsVisual": "视觉",
"settingsBlurredPlaceholderTitle": "模糊的占位符",
"settingsBlurredPlaceholderDesc": "加载海报时显示模糊的背景",
"settingsBlurEpisodesTitle": "模糊下一个集数",
"settingsBlurEpisodesDesc": "模糊所有即将播放的集数",
"settingsEnableOsMediaControls": "启用操作系统媒体控制",
"settingsNextUpCutoffDays": "下一个截止天数",
"settingsShowScaleSlider": "显示海报大小滑块",
"settingsPosterSize": "海报大小",
"settingsPosterSlider": "显示比例滑块",
"settingsPosterPinch": "捏合缩放以调整海报大小",
"theme": "主题",
"mode": "模式",
"themeModeSystem": "系统",
"themeModeLight": "浅色",
"themeModeDark": "深色",
"themeColor": "主题颜色",
"color": "颜色",
"amoledBlack": "Amoled 黑色",
"hide": "隐藏",
"nextUp": "下一个",
"settingsContinue": "继续",
"separate": "分开",
"combined": "组合",
"settingsSecurity": "安全",
"settingSecurityApplockTitle": "应用锁",
"appLockTitle": "设置 {userName} 的登录方式",
"@appLockTitle": {
"description": "选择登录方式的弹窗",
"placeholders": {
"userName": {
"type": "String"
}
}
},
"biometricsFailedCheckAgain": "生物识别失败,请检查设置并重试",
"appLockAutoLogin": "自动登录",
"appLockPasscode": "密码",
"appLockBiometrics": "生物识别",
"settingsPlayerVideoHWAccelTitle": "硬件加速",
"settingsPlayerVideoHWAccelDesc": "使用 GPU 渲染视频(推荐)",
"settingsPlayerNativeLibassAccelTitle": "本地 libass 字幕",
"settingsPlayerNativeLibassAccelDesc": "使用视频播放器 libass 字幕渲染器",
"settingsPlayerMobileWarning": "在 Android 上启用硬件加速和本地 libass 字幕可能会导致某些字幕无法渲染。",
"settingsPlayerCustomSubtitlesTitle": "自定义字幕",
"settingsPlayerCustomSubtitlesDesc": "自定义大小、颜色、位置、轮廓",
"videoScalingFillScreenTitle": "填满屏幕",
"videoScalingFillScreenDesc": "填充导航栏和状态栏",
"videoScalingFillScreenNotif": "全屏覆盖视频适应,在水平旋转中",
"videoScaling": "视频缩放",
"videoScalingFill": "填充",
"videoScalingContain": "包含",
"videoScalingCover": "覆盖",
"videoScalingFitWidth": "适应宽度",
"videoScalingFitHeight": "适应高度",
"videoScalingScaleDown": "缩小",
"subtitleConfiguratorPlaceHolder": "这是占位符文本,\n这里没有什么可看的。",
"subtitleConfigurator": "字幕配置器",
"refreshPopup": "刷新 - {name}",
"@refreshPopup": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"scanningName": "扫描 - {name}",
"@scanningName": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"refreshPopupContentMetadata": "根据仪表板中启用的设置和互联网服务刷新元数据。",
"replaceExistingImages": "替换现有图像",
"metadataRefreshDefault": "扫描新文件和更新的文件",
"metadataRefreshValidation": "搜索缺失的元数据",
"metadataRefreshFull": "替换所有元数据",
"syncedItems": "同步项目",
"noItemsSynced": "没有同步项目",
"syncDeletePopupPermanent": "此操作是永久性的,将删除所有本地同步的文件",
"totalSize": "总大小:{size}",
"@totalSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mediaTypeBase": "基础类型",
"mediaTypeMovie": "电影",
"mediaTypeSeries": "系列",
"mediaTypeSeason": "季",
"mediaTypeEpisode": "集",
"mediaTypePhoto": "照片",
"mediaTypePerson": "人物",
"mediaTypePhotoAlbum": "相册",
"mediaTypeFolder": "文件夹",
"mediaTypeBoxset": "套装",
"mediaTypePlaylist": "播放列表",
"mediaTypeBook": "书籍",
"actor": "{count, plural, other{演员} one{演员}}",
"@actor": {
"description": "演员",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"writer": "{count, plural, other{作家} one{作家}}",
"@writer": {
"description": "作家",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"director": "{count, plural, other{导演} one{导演}}",
"@director": {
"description": "导演",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"video": "视频",
"audio": "音频",
"subtitles": "字幕",
"related": "相关",
"all": "全部",
"overview": "概述",
"selectViewType": "选择查看类型",
"noItemsToShow": "没有可显示的项目",
"sortBy": "排序方式",
"groupBy": "分组依据",
"scrollToTop": "滚动到顶部",
"disableFilters": "禁用过滤器",
"selectAll": "全选",
"clearSelection": "清除选择",
"shuffleVideos": "随机播放视频",
"shuffleGallery": "随机播放图库",
"unknown": "未知",
"favorites": "收藏",
"recursive": "递归",
"genre": "{count, plural, other{类型} one{类型}}",
"@genre": {
"description": "类型",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"studio": "{count, plural, other{工作室} one{工作室}}",
"@studio": {
"description": "工作室",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"label": "{count, plural, other{标签} one{标签}}",
"@label": {
"description": "标签",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"group": "组",
"type": "{count, plural, other{类型} one{类型}}",
"@type": {
"description": "类型",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"filter": "{count, plural, other{过滤器} one{过滤器}}",
"@filter": {
"description": "过滤器",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"showEmpty": "显示空",
"hideEmpty": "隐藏空",
"rating": "{count, plural, other{评分} one{评分}}",
"@rating": {
"description": "评分",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"year": "{count, plural, other{年} one{年}}",
"@year": {
"description": "年",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"playVideos": "播放视频",
"playLabel": "播放",
"forceRefresh": "强制刷新",
"itemCount": "项目数:{count}",
"@itemCount": {
"description": "项目数",
"placeholders": {
"count": {
"type": "int"
}
}
},
"invalidUrl": "无效的 URL",
"invalidUrlDesc": "URL 必须以 http(s):// 开头",
"incorrectPinTryAgain": "错误的 PIN请重试",
"somethingWentWrongPasswordCheck": "出了点问题,请检查您的密码",
"unableToConnectHost": "无法连接到主机",
"server": "服务器",
"retrievePublicListOfUsers": "检索公共用户列表",
"displayLanguage": "显示语言 (显示语言)",
"deleteItem": "删除 {item}?",
"@deleteItem": {
"description": "deleteItem",
"placeholders": {
"item": {
"type": "String"
}
}
},
"syncDeleteItemTitle": "删除同步的项目",
"syncDeleteItemDesc": "删除所有同步数据?\n{item}",
"@syncDeleteItemDesc": {
"description": "同步删除项目弹出窗口",
"placeholders": {
"item": {
"type": "String"
}
}
},
"syncOpenParent": "打开父级",
"syncRemoveDataTitle": "删除同步的数据?",
"syncRemoveDataDesc": "删除同步的视频数据?这是永久性的,您需要重新同步文件",
"collectionFolder": "收藏夹",
"musicAlbum": "专辑",
"active": "活跃",
"name": "名称",
"result": "结果",
"close": "关闭",
"replaceAllImages": "替换所有图像",
"noResults": "没有结果",
"openWebLink": "打开网页链接",
"setIdentityTo": "设置身份为 {name}",
"@setIdentityTo": {
"description": "setIdentityTo",
"placeholders": {
"name": {
"type": "String"
}
}
},
"somethingWentWrong": "出了些问题",
"clearChanges": "清除更改",
"useDefaults": "使用默认值",
"light": "浅色",
"normal": "正常",
"bold": "粗体",
"fontSize": "字体大小",
"heightOffset": "高度偏移",
"fontColor": "字体颜色",
"outlineColor": "轮廓颜色",
"outlineSize": "轮廓大小",
"backgroundOpacity": "背景不透明度",
"shadow": "阴影",
"played": "已播放",
"unPlayed": "未播放",
"resumable": "可恢复",
"sortOrder": "排序顺序",
"sortName": "名称",
"communityRating": "社区评分",
"parentalRating": "家长评级",
"dateAdded": "添加日期",
"dateLastContentAdded": "最后添加内容的日期",
"favorite": "收藏",
"datePlayed": "播放日期",
"folders": "文件夹",
"playCount": "播放次数",
"releaseDate": "发布日期",
"runTime": "运行时间",
"ascending": "升序",
"descending": "降序",
"playFromStart": "从头播放 {name}",
"@playFromStart": {
"description": "从头播放",
"placeholders": {
"name": {
"type": "String"
}
}
},
"minutes": "分钟",
"@minutes": {
"description": "分钟",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"seconds": "秒",
"@seconds": {
"description": "秒",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"page": "第 {index} 页",
"@page": {
"description": "页数",
"placeholders": {
"index": {
"type": "int"
}
}
},
"set": "设置",
"@set": {
"description": "用于设置某个值",
"context": "设置时间"
},
"never": "从不",
"selectTime": "选择时间",
"immediately": "立即",
"timeAndAnnotation": "{minutes} 分 {seconds} 秒",
"@timeAndAnnotation": {
"description": "时间和注释",
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"scanYourFingerprintToAuthenticate": "扫描您的指纹以验证 {user}",
"@scanYourFingerprintToAuthenticate": {
"placeholders": {
"user": {
"type": "String"
}
}
},
"scanBiometricHint": "验证身份",
"deleteFileFromSystem": "删除此项目 {item} 将从文件系统和您的媒体库中删除它。\n您确定要继续吗",
"@deleteFileFromSystem": {
"description": "从系统中删除文件",
"placeholders": {
"item": {
"type": "String"
}
}
},
"notPartOfAlbum": "不属于专辑的一部分",
"retry": "重试",
"failedToLoadImage": "加载图像失败",
"save": "保存",
"metaDataSavedFor": "{item} 的元数据已保存",
"@metaDataSavedFor": {
"description": "元数据已保存",
"placeholders": {
"item": {
"type": "String"
}
}
},
"libraryPageSizeTitle": "库页面大小",
"libraryPageSizeDesc": "设置每次加载的数量。0 禁用分页。",
"fetchingLibrary": "正在获取库项目",
"libraryFetchNoItemsFound": "未找到项目,请尝试不同的设置。",
"noSuggestionsFound": "未找到建议",
"viewPhotos": "查看照片",
"random": "随机",
"tag": "{count, plural, one{标签} other{标签}}",
"@tag": {
"description": "标签",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"saved": "已保存",
"discovered": "已发现",
"noServersFound": "未找到新服务器",
"about": "关于",
"openParent": "打开上级文件夹",
"mouseDragSupport": "使用鼠标拖动",
"controls": "控制"
}

1
lib/l10n/l10n_errors.txt Normal file
View file

@ -0,0 +1 @@
{}

339
lib/main.dart Normal file
View file

@ -0,0 +1,339 @@
import 'dart:io';
import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:fladder/models/syncing/i_synced_item.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ignore: depend_on_referenced_packages
import 'package:flutter_web_plugins/url_strategy.dart';
import 'package:go_router/go_router.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:media_kit/media_kit.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:window_manager/window_manager.dart';
import 'package:fladder/models/account_model.dart';
import 'package:fladder/providers/shared_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/routes/app_routes.dart';
import 'package:fladder/routes/build_routes/route_builder.dart';
import 'package:fladder/screens/login/lock_screen.dart';
import 'package:fladder/theme.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/application_info.dart';
import 'package:fladder/util/string_extensions.dart';
import 'package:fladder/util/themes_data.dart';
import 'package:universal_html/html.dart' as html;
bool get _isDesktop {
if (kIsWeb) return false;
return [
TargetPlatform.windows,
TargetPlatform.linux,
TargetPlatform.macOS,
].contains(defaultTargetPlatform);
}
class CustomCacheManager {
static const key = 'customCacheKey';
static CacheManager instance = CacheManager(
Config(
key,
stalePeriod: const Duration(days: 3),
maxNrOfCacheObjects: 500,
repo: JsonCacheInfoRepository(databaseName: key),
fileService: HttpFileService(),
),
);
}
void main() async {
if (kIsWeb) {
usePathUrlStrategy();
html.document.onContextMenu.listen((event) => event.preventDefault());
GoRouter.optionURLReflectsImperativeAPIs = true;
}
_setupLogging();
WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized();
final sharedPreferences = await SharedPreferences.getInstance();
PackageInfo packageInfo = await PackageInfo.fromPlatform();
Directory isarPath = Directory("");
Directory applicationDirectory = Directory("");
if (!kIsWeb) {
applicationDirectory = await getApplicationDocumentsDirectory();
isarPath = Directory(path.joinAll([applicationDirectory.path, 'Fladder', 'Database']));
await isarPath.create(recursive: true);
}
if (_isDesktop) {
await WindowManager.instance.ensureInitialized();
}
final applicationInfo = ApplicationInfo(
name: kIsWeb ? "${packageInfo.appName.capitalize()} Web" : packageInfo.appName.capitalize(),
version: "${packageInfo.version}(${packageInfo.buildNumber})",
os: defaultTargetPlatform.name.capitalize(),
);
runApp(
ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWith((ref) => sharedPreferences),
applicationInfoProvider.overrideWith((ref) => applicationInfo),
syncProvider.overrideWith((ref) => SyncNotifier(
ref,
!kIsWeb
? Isar.open(
schemas: [ISyncedItemSchema],
directory: isarPath.path,
)
: null,
applicationDirectory,
))
],
child: AdaptiveLayoutBuilder(
fallBack: LayoutState.phone,
layoutPoints: [
LayoutPoints(start: 0, end: 599, type: LayoutState.phone),
LayoutPoints(start: 600, end: 1919, type: LayoutState.tablet),
LayoutPoints(start: 1920, end: 3180, type: LayoutState.desktop),
],
child: const Main(),
),
),
);
}
void _setupLogging() {
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((rec) {
if (kDebugMode) {
print('${rec.level.name}: ${rec.time}: ${rec.message}');
}
});
}
class Main extends ConsumerStatefulWidget with WindowListener {
const Main({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _MainState();
}
class _MainState extends ConsumerState<Main> with WindowListener, WidgetsBindingObserver {
DateTime dateTime = DateTime.now();
bool hidden = false;
@override
void didChangeAppLifecycleState(AppLifecycleState state) async {
if (ref.read(lockScreenActiveProvider) || ref.read(userProvider) == null) {
dateTime = DateTime.now();
return;
}
switch (state) {
case AppLifecycleState.resumed:
enableTimeOut();
break;
case AppLifecycleState.hidden:
break;
case AppLifecycleState.paused:
hidden = true;
dateTime = DateTime.now();
break;
default:
break;
}
}
void enableTimeOut() async {
final timeOut = ref.read(clientSettingsProvider).timeOut;
if (timeOut == null) return;
final difference = DateTime.now().difference(dateTime).abs();
if (difference > timeOut && ref.read(userProvider)?.authMethod != Authentication.autoLogin && hidden) {
hidden = false;
dateTime = DateTime.now();
// Stop playback if the user was still watching a video
await ref.read(videoPlayerProvider).pause();
if (context.mounted) {
AdaptiveLayout.of(context).router.push(LockScreenRoute().route);
}
}
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
windowManager.addListener(this);
_init();
}
@override
void dispose() {
super.dispose();
WidgetsBinding.instance.removeObserver(this);
windowManager.removeListener(this);
}
@override
void onWindowClose() {
ref.read(videoPlayerProvider).stop();
super.onWindowClose();
}
@override
void onWindowResize() async {
final size = await windowManager.getSize();
ref.read(clientSettingsProvider.notifier).setWindowSize(size);
super.onWindowResize();
}
@override
void onWindowResized() async {
final size = await windowManager.getSize();
ref.read(clientSettingsProvider.notifier).setWindowSize(size);
super.onWindowResized();
}
@override
void onWindowMove() async {
final position = await windowManager.getPosition();
ref.read(clientSettingsProvider.notifier).setWindowPosition(position);
super.onWindowMove();
}
@override
void onWindowMoved() async {
final position = await windowManager.getPosition();
ref.read(clientSettingsProvider.notifier).setWindowPosition(position);
super.onWindowMoved();
}
void _init() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
ref.read(sharedUtilityProvider).loadSettings();
final clientSettings = ref.read(clientSettingsProvider);
if (_isDesktop) {
WindowOptions windowOptions = WindowOptions(
size: Size(clientSettings.size.x, clientSettings.size.y),
center: true,
backgroundColor: Colors.transparent,
skipTaskbar: false,
titleBarStyle: TitleBarStyle.hidden,
title: packageInfo.appName.capitalize());
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: []);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
));
}
}
@override
Widget build(BuildContext context) {
final themeMode = ref.watch(clientSettingsProvider.select((value) => value.themeMode));
final themeColor = ref.watch(clientSettingsProvider.select((value) => value.themeColor));
final amoledBlack = ref.watch(clientSettingsProvider.select((value) => value.amoledBlack));
final mouseDrag = ref.watch(clientSettingsProvider.select((value) => value.mouseDragSupport));
final language = ref.watch(clientSettingsProvider
.select((value) => value.selectedLocale ?? WidgetsBinding.instance.platformDispatcher.locale));
final scrollBehaviour = const MaterialScrollBehavior();
return Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
},
child: DynamicColorBuilder(builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
final lightTheme = themeColor == null
? FladderTheme.theme(lightDynamic ?? FladderTheme.defaultScheme(Brightness.light))
: FladderTheme.theme(themeColor.schemeLight);
final darkTheme = (themeColor == null
? FladderTheme.theme(darkDynamic ?? FladderTheme.defaultScheme(Brightness.dark))
: FladderTheme.theme(themeColor.schemeDark));
final amoledOverwrite = amoledBlack ? Colors.black : null;
return ThemesData(
light: lightTheme,
dark: darkTheme,
child: MaterialApp.router(
onGenerateTitle: (context) => ref.watch(currentTitleProvider),
theme: lightTheme,
scrollBehavior: scrollBehaviour.copyWith(
dragDevices: {
...scrollBehaviour.dragDevices,
mouseDrag ? PointerDeviceKind.mouse : null,
}.whereNotNull().toSet(),
),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
builder: (context, child) => Localizations.override(
context: context,
locale: AppLocalizations.supportedLocales.firstWhere(
(element) => element.languageCode == language.languageCode,
orElse: () => Locale('en', "GB"),
),
child: ScaffoldMessenger(child: child ?? Container()),
),
darkTheme: darkTheme.copyWith(
scaffoldBackgroundColor: amoledOverwrite,
cardColor: amoledOverwrite,
canvasColor: amoledOverwrite,
colorScheme: darkTheme.colorScheme.copyWith(
surface: amoledOverwrite,
surfaceContainerHighest: amoledOverwrite,
),
),
themeMode: themeMode,
routerConfig: AdaptiveLayout.of(context).router,
),
);
}),
);
}
List<RouteBase> getRoutes(LayoutState state) {
switch (state) {
case LayoutState.phone:
return AppRoutes.linearRoutes;
case LayoutState.tablet:
case LayoutState.desktop:
return AppRoutes.nestedRoutes;
}
}
}
final currentTitleProvider = StateProvider<String>((ref) {
return "Fladder";
});

View file

@ -0,0 +1,107 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first, invalid_annotation_target
import 'package:ficonsax/ficonsax.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/credentials_model.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'account_model.freezed.dart';
part 'account_model.g.dart';
@freezed
class AccountModel with _$AccountModel {
const AccountModel._();
const factory AccountModel({
required String name,
required String id,
required String avatar,
required DateTime lastUsed,
@Default(Authentication.autoLogin) Authentication authMethod,
@Default("") String localPin,
required CredentialsModel credentials,
@Default([]) List<String> latestItemsExcludes,
@Default([]) List<String> searchQueryHistory,
@Default(false) bool quickConnectState,
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
@JsonKey(includeFromJson: false, includeToJson: false) ServerConfiguration? serverConfiguration,
}) = _AccountModel;
factory AccountModel.fromJson(Map<String, dynamic> json) => _$AccountModelFromJson(json);
String get server {
return credentials.server;
}
bool get canDownload {
return (policy?.enableContentDownloading ?? false) && !kIsWeb;
}
//Check if it's the same account on the same server
bool sameIdentity(AccountModel other) {
if (identical(this, other)) return true;
return other.id == id && other.credentials.serverId == credentials.serverId;
}
}
enum Authentication {
autoLogin(0),
biometrics(1),
passcode(2),
none(3);
const Authentication(this.value);
final int value;
bool available(BuildContext context) {
switch (this) {
case Authentication.none:
case Authentication.autoLogin:
case Authentication.passcode:
return true;
case Authentication.biometrics:
return !AdaptiveLayout.of(context).isDesktop;
}
}
String name(BuildContext context) {
switch (this) {
case Authentication.none:
return context.localized.none;
case Authentication.autoLogin:
return context.localized.appLockAutoLogin;
case Authentication.biometrics:
return context.localized.appLockBiometrics;
case Authentication.passcode:
return context.localized.appLockPasscode;
}
}
IconData get icon {
switch (this) {
case Authentication.none:
return IconsaxBold.arrow_bottom;
case Authentication.autoLogin:
return IconsaxOutline.login_1;
case Authentication.biometrics:
return IconsaxOutline.finger_scan;
case Authentication.passcode:
return IconsaxOutline.password_check;
}
}
static Authentication fromMap(int value) {
return Authentication.values[value];
}
int toMap() {
return value;
}
}

View file

@ -0,0 +1,450 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'account_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
AccountModel _$AccountModelFromJson(Map<String, dynamic> json) {
return _AccountModel.fromJson(json);
}
/// @nodoc
mixin _$AccountModel {
String get name => throw _privateConstructorUsedError;
String get id => throw _privateConstructorUsedError;
String get avatar => throw _privateConstructorUsedError;
DateTime get lastUsed => throw _privateConstructorUsedError;
Authentication get authMethod => throw _privateConstructorUsedError;
String get localPin => throw _privateConstructorUsedError;
CredentialsModel get credentials => throw _privateConstructorUsedError;
List<String> get latestItemsExcludes => throw _privateConstructorUsedError;
List<String> get searchQueryHistory => throw _privateConstructorUsedError;
bool get quickConnectState => throw _privateConstructorUsedError;
@JsonKey(includeFromJson: false, includeToJson: false)
UserPolicy? get policy => throw _privateConstructorUsedError;
@JsonKey(includeFromJson: false, includeToJson: false)
ServerConfiguration? get serverConfiguration =>
throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$AccountModelCopyWith<AccountModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AccountModelCopyWith<$Res> {
factory $AccountModelCopyWith(
AccountModel value, $Res Function(AccountModel) then) =
_$AccountModelCopyWithImpl<$Res, AccountModel>;
@useResult
$Res call(
{String name,
String id,
String avatar,
DateTime lastUsed,
Authentication authMethod,
String localPin,
CredentialsModel credentials,
List<String> latestItemsExcludes,
List<String> searchQueryHistory,
bool quickConnectState,
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
@JsonKey(includeFromJson: false, includeToJson: false)
ServerConfiguration? serverConfiguration});
}
/// @nodoc
class _$AccountModelCopyWithImpl<$Res, $Val extends AccountModel>
implements $AccountModelCopyWith<$Res> {
_$AccountModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? id = null,
Object? avatar = null,
Object? lastUsed = null,
Object? authMethod = null,
Object? localPin = null,
Object? credentials = null,
Object? latestItemsExcludes = null,
Object? searchQueryHistory = null,
Object? quickConnectState = null,
Object? policy = freezed,
Object? serverConfiguration = freezed,
}) {
return _then(_value.copyWith(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
avatar: null == avatar
? _value.avatar
: avatar // ignore: cast_nullable_to_non_nullable
as String,
lastUsed: null == lastUsed
? _value.lastUsed
: lastUsed // ignore: cast_nullable_to_non_nullable
as DateTime,
authMethod: null == authMethod
? _value.authMethod
: authMethod // ignore: cast_nullable_to_non_nullable
as Authentication,
localPin: null == localPin
? _value.localPin
: localPin // ignore: cast_nullable_to_non_nullable
as String,
credentials: null == credentials
? _value.credentials
: credentials // ignore: cast_nullable_to_non_nullable
as CredentialsModel,
latestItemsExcludes: null == latestItemsExcludes
? _value.latestItemsExcludes
: latestItemsExcludes // ignore: cast_nullable_to_non_nullable
as List<String>,
searchQueryHistory: null == searchQueryHistory
? _value.searchQueryHistory
: searchQueryHistory // ignore: cast_nullable_to_non_nullable
as List<String>,
quickConnectState: null == quickConnectState
? _value.quickConnectState
: quickConnectState // ignore: cast_nullable_to_non_nullable
as bool,
policy: freezed == policy
? _value.policy
: policy // ignore: cast_nullable_to_non_nullable
as UserPolicy?,
serverConfiguration: freezed == serverConfiguration
? _value.serverConfiguration
: serverConfiguration // ignore: cast_nullable_to_non_nullable
as ServerConfiguration?,
) as $Val);
}
}
/// @nodoc
abstract class _$$AccountModelImplCopyWith<$Res>
implements $AccountModelCopyWith<$Res> {
factory _$$AccountModelImplCopyWith(
_$AccountModelImpl value, $Res Function(_$AccountModelImpl) then) =
__$$AccountModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String name,
String id,
String avatar,
DateTime lastUsed,
Authentication authMethod,
String localPin,
CredentialsModel credentials,
List<String> latestItemsExcludes,
List<String> searchQueryHistory,
bool quickConnectState,
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
@JsonKey(includeFromJson: false, includeToJson: false)
ServerConfiguration? serverConfiguration});
}
/// @nodoc
class __$$AccountModelImplCopyWithImpl<$Res>
extends _$AccountModelCopyWithImpl<$Res, _$AccountModelImpl>
implements _$$AccountModelImplCopyWith<$Res> {
__$$AccountModelImplCopyWithImpl(
_$AccountModelImpl _value, $Res Function(_$AccountModelImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? id = null,
Object? avatar = null,
Object? lastUsed = null,
Object? authMethod = null,
Object? localPin = null,
Object? credentials = null,
Object? latestItemsExcludes = null,
Object? searchQueryHistory = null,
Object? quickConnectState = null,
Object? policy = freezed,
Object? serverConfiguration = freezed,
}) {
return _then(_$AccountModelImpl(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
avatar: null == avatar
? _value.avatar
: avatar // ignore: cast_nullable_to_non_nullable
as String,
lastUsed: null == lastUsed
? _value.lastUsed
: lastUsed // ignore: cast_nullable_to_non_nullable
as DateTime,
authMethod: null == authMethod
? _value.authMethod
: authMethod // ignore: cast_nullable_to_non_nullable
as Authentication,
localPin: null == localPin
? _value.localPin
: localPin // ignore: cast_nullable_to_non_nullable
as String,
credentials: null == credentials
? _value.credentials
: credentials // ignore: cast_nullable_to_non_nullable
as CredentialsModel,
latestItemsExcludes: null == latestItemsExcludes
? _value._latestItemsExcludes
: latestItemsExcludes // ignore: cast_nullable_to_non_nullable
as List<String>,
searchQueryHistory: null == searchQueryHistory
? _value._searchQueryHistory
: searchQueryHistory // ignore: cast_nullable_to_non_nullable
as List<String>,
quickConnectState: null == quickConnectState
? _value.quickConnectState
: quickConnectState // ignore: cast_nullable_to_non_nullable
as bool,
policy: freezed == policy
? _value.policy
: policy // ignore: cast_nullable_to_non_nullable
as UserPolicy?,
serverConfiguration: freezed == serverConfiguration
? _value.serverConfiguration
: serverConfiguration // ignore: cast_nullable_to_non_nullable
as ServerConfiguration?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
const _$AccountModelImpl(
{required this.name,
required this.id,
required this.avatar,
required this.lastUsed,
this.authMethod = Authentication.autoLogin,
this.localPin = "",
required this.credentials,
final List<String> latestItemsExcludes = const [],
final List<String> searchQueryHistory = const [],
this.quickConnectState = false,
@JsonKey(includeFromJson: false, includeToJson: false) this.policy,
@JsonKey(includeFromJson: false, includeToJson: false)
this.serverConfiguration})
: _latestItemsExcludes = latestItemsExcludes,
_searchQueryHistory = searchQueryHistory,
super._();
factory _$AccountModelImpl.fromJson(Map<String, dynamic> json) =>
_$$AccountModelImplFromJson(json);
@override
final String name;
@override
final String id;
@override
final String avatar;
@override
final DateTime lastUsed;
@override
@JsonKey()
final Authentication authMethod;
@override
@JsonKey()
final String localPin;
@override
final CredentialsModel credentials;
final List<String> _latestItemsExcludes;
@override
@JsonKey()
List<String> get latestItemsExcludes {
if (_latestItemsExcludes is EqualUnmodifiableListView)
return _latestItemsExcludes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_latestItemsExcludes);
}
final List<String> _searchQueryHistory;
@override
@JsonKey()
List<String> get searchQueryHistory {
if (_searchQueryHistory is EqualUnmodifiableListView)
return _searchQueryHistory;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_searchQueryHistory);
}
@override
@JsonKey()
final bool quickConnectState;
@override
@JsonKey(includeFromJson: false, includeToJson: false)
final UserPolicy? policy;
@override
@JsonKey(includeFromJson: false, includeToJson: false)
final ServerConfiguration? serverConfiguration;
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'AccountModel(name: $name, id: $id, avatar: $avatar, lastUsed: $lastUsed, authMethod: $authMethod, localPin: $localPin, credentials: $credentials, latestItemsExcludes: $latestItemsExcludes, searchQueryHistory: $searchQueryHistory, quickConnectState: $quickConnectState, policy: $policy, serverConfiguration: $serverConfiguration)';
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('type', 'AccountModel'))
..add(DiagnosticsProperty('name', name))
..add(DiagnosticsProperty('id', id))
..add(DiagnosticsProperty('avatar', avatar))
..add(DiagnosticsProperty('lastUsed', lastUsed))
..add(DiagnosticsProperty('authMethod', authMethod))
..add(DiagnosticsProperty('localPin', localPin))
..add(DiagnosticsProperty('credentials', credentials))
..add(DiagnosticsProperty('latestItemsExcludes', latestItemsExcludes))
..add(DiagnosticsProperty('searchQueryHistory', searchQueryHistory))
..add(DiagnosticsProperty('quickConnectState', quickConnectState))
..add(DiagnosticsProperty('policy', policy))
..add(DiagnosticsProperty('serverConfiguration', serverConfiguration));
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AccountModelImpl &&
(identical(other.name, name) || other.name == name) &&
(identical(other.id, id) || other.id == id) &&
(identical(other.avatar, avatar) || other.avatar == avatar) &&
(identical(other.lastUsed, lastUsed) ||
other.lastUsed == lastUsed) &&
(identical(other.authMethod, authMethod) ||
other.authMethod == authMethod) &&
(identical(other.localPin, localPin) ||
other.localPin == localPin) &&
(identical(other.credentials, credentials) ||
other.credentials == credentials) &&
const DeepCollectionEquality()
.equals(other._latestItemsExcludes, _latestItemsExcludes) &&
const DeepCollectionEquality()
.equals(other._searchQueryHistory, _searchQueryHistory) &&
(identical(other.quickConnectState, quickConnectState) ||
other.quickConnectState == quickConnectState) &&
(identical(other.policy, policy) || other.policy == policy) &&
(identical(other.serverConfiguration, serverConfiguration) ||
other.serverConfiguration == serverConfiguration));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
name,
id,
avatar,
lastUsed,
authMethod,
localPin,
credentials,
const DeepCollectionEquality().hash(_latestItemsExcludes),
const DeepCollectionEquality().hash(_searchQueryHistory),
quickConnectState,
policy,
serverConfiguration);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$AccountModelImplCopyWith<_$AccountModelImpl> get copyWith =>
__$$AccountModelImplCopyWithImpl<_$AccountModelImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$AccountModelImplToJson(
this,
);
}
}
abstract class _AccountModel extends AccountModel {
const factory _AccountModel(
{required final String name,
required final String id,
required final String avatar,
required final DateTime lastUsed,
final Authentication authMethod,
final String localPin,
required final CredentialsModel credentials,
final List<String> latestItemsExcludes,
final List<String> searchQueryHistory,
final bool quickConnectState,
@JsonKey(includeFromJson: false, includeToJson: false)
final UserPolicy? policy,
@JsonKey(includeFromJson: false, includeToJson: false)
final ServerConfiguration? serverConfiguration}) = _$AccountModelImpl;
const _AccountModel._() : super._();
factory _AccountModel.fromJson(Map<String, dynamic> json) =
_$AccountModelImpl.fromJson;
@override
String get name;
@override
String get id;
@override
String get avatar;
@override
DateTime get lastUsed;
@override
Authentication get authMethod;
@override
String get localPin;
@override
CredentialsModel get credentials;
@override
List<String> get latestItemsExcludes;
@override
List<String> get searchQueryHistory;
@override
bool get quickConnectState;
@override
@JsonKey(includeFromJson: false, includeToJson: false)
UserPolicy? get policy;
@override
@JsonKey(includeFromJson: false, includeToJson: false)
ServerConfiguration? get serverConfiguration;
@override
@JsonKey(ignore: true)
_$$AccountModelImplCopyWith<_$AccountModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,50 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'account_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$AccountModelImpl _$$AccountModelImplFromJson(Map<String, dynamic> json) =>
_$AccountModelImpl(
name: json['name'] as String,
id: json['id'] as String,
avatar: json['avatar'] as String,
lastUsed: DateTime.parse(json['lastUsed'] as String),
authMethod:
$enumDecodeNullable(_$AuthenticationEnumMap, json['authMethod']) ??
Authentication.autoLogin,
localPin: json['localPin'] as String? ?? "",
credentials: CredentialsModel.fromJson(json['credentials'] as String),
latestItemsExcludes: (json['latestItemsExcludes'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
searchQueryHistory: (json['searchQueryHistory'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
quickConnectState: json['quickConnectState'] as bool? ?? false,
);
Map<String, dynamic> _$$AccountModelImplToJson(_$AccountModelImpl instance) =>
<String, dynamic>{
'name': instance.name,
'id': instance.id,
'avatar': instance.avatar,
'lastUsed': instance.lastUsed.toIso8601String(),
'authMethod': _$AuthenticationEnumMap[instance.authMethod]!,
'localPin': instance.localPin,
'credentials': instance.credentials,
'latestItemsExcludes': instance.latestItemsExcludes,
'searchQueryHistory': instance.searchQueryHistory,
'quickConnectState': instance.quickConnectState,
};
const _$AuthenticationEnumMap = {
Authentication.autoLogin: 'autoLogin',
Authentication.biometrics: 'biometrics',
Authentication.passcode: 'passcode',
Authentication.none: 'none',
};

View file

@ -0,0 +1,72 @@
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/overview_model.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class BookModel extends ItemBaseModel {
final String? parentName;
final List<ItemBaseModel> items;
BookModel(
{this.items = const [],
required this.parentName,
required super.name,
required super.id,
required super.overview,
required super.parentId,
required super.playlistId,
required super.images,
required super.childCount,
required super.primaryRatio,
required super.userData,
super.jellyType,
required super.canDownload,
required super.canDelete});
@override
String? get subText => parentName;
@override
String? detailedName(BuildContext context) => "$name ${parentName != null ? "\n ($parentName)" : ""} ";
@override
ItemBaseModel get parentBaseModel => copyWith(id: parentId);
@override
bool get playAble => true;
int get currentPage => userData.playbackPositionTicks ~/ 10000;
@override
String playText(BuildContext context) => context.localized.read(name);
@override
double get progress => userData.progress != 0 ? 100 : 0;
@override
String playButtonLabel(BuildContext context) => progress != 0
? context.localized.continuePage(currentPage)
: userData.played == true
? "${context.localized.restart} $name"
: context.localized.read(name);
factory BookModel.fromBaseDto(BaseItemDto item, Ref ref) {
return BookModel(
name: item.name ?? "",
id: item.id ?? "",
parentName: item.seriesName ?? item.seasonName,
childCount: item.childCount,
overview: OverviewModel.fromBaseItemDto(item, ref),
userData: UserData.fromDto(item.userData),
parentId: item.parentId,
playlistId: item.playlistItemId,
images: ImagesData.fromBaseItem(item, ref),
canDelete: item.canDelete,
canDownload: item.canDownload,
primaryRatio: item.primaryImageAspectRatio,
);
}
}

View file

@ -0,0 +1,47 @@
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/overview_model.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'boxset_model.mapper.dart';
@MappableClass()
class BoxSetModel extends ItemBaseModel with BoxSetModelMappable {
final List<ItemBaseModel> items;
const BoxSetModel({
this.items = const [],
required super.name,
required super.id,
required super.overview,
required super.parentId,
required super.playlistId,
required super.images,
required super.childCount,
required super.primaryRatio,
required super.userData,
required super.canDelete,
required super.canDownload,
super.jellyType,
});
factory BoxSetModel.fromBaseDto(BaseItemDto item, Ref ref) {
return BoxSetModel(
name: item.name ?? "",
id: item.id ?? "",
childCount: item.childCount,
overview: OverviewModel.fromBaseItemDto(item, ref),
userData: UserData.fromDto(item.userData),
parentId: item.parentId,
playlistId: item.playlistItemId,
images: ImagesData.fromBaseItem(item, ref),
primaryRatio: item.primaryImageAspectRatio,
canDelete: item.canDelete,
canDownload: item.canDownload,
jellyType: item.type,
);
}
}

View file

@ -0,0 +1,242 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
part of 'boxset_model.dart';
class BoxSetModelMapper extends SubClassMapperBase<BoxSetModel> {
BoxSetModelMapper._();
static BoxSetModelMapper? _instance;
static BoxSetModelMapper ensureInitialized() {
if (_instance == null) {
MapperContainer.globals.use(_instance = BoxSetModelMapper._());
ItemBaseModelMapper.ensureInitialized().addSubMapper(_instance!);
ItemBaseModelMapper.ensureInitialized();
OverviewModelMapper.ensureInitialized();
UserDataMapper.ensureInitialized();
}
return _instance!;
}
@override
final String id = 'BoxSetModel';
static List<ItemBaseModel> _$items(BoxSetModel v) => v.items;
static const Field<BoxSetModel, List<ItemBaseModel>> _f$items =
Field('items', _$items, opt: true, def: const []);
static String _$name(BoxSetModel v) => v.name;
static const Field<BoxSetModel, String> _f$name = Field('name', _$name);
static String _$id(BoxSetModel v) => v.id;
static const Field<BoxSetModel, String> _f$id = Field('id', _$id);
static OverviewModel _$overview(BoxSetModel v) => v.overview;
static const Field<BoxSetModel, OverviewModel> _f$overview =
Field('overview', _$overview);
static String? _$parentId(BoxSetModel v) => v.parentId;
static const Field<BoxSetModel, String> _f$parentId =
Field('parentId', _$parentId);
static String? _$playlistId(BoxSetModel v) => v.playlistId;
static const Field<BoxSetModel, String> _f$playlistId =
Field('playlistId', _$playlistId);
static ImagesData? _$images(BoxSetModel v) => v.images;
static const Field<BoxSetModel, ImagesData> _f$images =
Field('images', _$images);
static int? _$childCount(BoxSetModel v) => v.childCount;
static const Field<BoxSetModel, int> _f$childCount =
Field('childCount', _$childCount);
static double? _$primaryRatio(BoxSetModel v) => v.primaryRatio;
static const Field<BoxSetModel, double> _f$primaryRatio =
Field('primaryRatio', _$primaryRatio);
static UserData _$userData(BoxSetModel v) => v.userData;
static const Field<BoxSetModel, UserData> _f$userData =
Field('userData', _$userData);
static bool? _$canDelete(BoxSetModel v) => v.canDelete;
static const Field<BoxSetModel, bool> _f$canDelete =
Field('canDelete', _$canDelete);
static bool? _$canDownload(BoxSetModel v) => v.canDownload;
static const Field<BoxSetModel, bool> _f$canDownload =
Field('canDownload', _$canDownload);
static BaseItemKind? _$jellyType(BoxSetModel v) => v.jellyType;
static const Field<BoxSetModel, BaseItemKind> _f$jellyType =
Field('jellyType', _$jellyType, opt: true);
@override
final MappableFields<BoxSetModel> fields = const {
#items: _f$items,
#name: _f$name,
#id: _f$id,
#overview: _f$overview,
#parentId: _f$parentId,
#playlistId: _f$playlistId,
#images: _f$images,
#childCount: _f$childCount,
#primaryRatio: _f$primaryRatio,
#userData: _f$userData,
#canDelete: _f$canDelete,
#canDownload: _f$canDownload,
#jellyType: _f$jellyType,
};
@override
final bool ignoreNull = true;
@override
final String discriminatorKey = 'type';
@override
final dynamic discriminatorValue = 'BoxSetModel';
@override
late final ClassMapperBase superMapper =
ItemBaseModelMapper.ensureInitialized();
static BoxSetModel _instantiate(DecodingData data) {
return BoxSetModel(
items: data.dec(_f$items),
name: data.dec(_f$name),
id: data.dec(_f$id),
overview: data.dec(_f$overview),
parentId: data.dec(_f$parentId),
playlistId: data.dec(_f$playlistId),
images: data.dec(_f$images),
childCount: data.dec(_f$childCount),
primaryRatio: data.dec(_f$primaryRatio),
userData: data.dec(_f$userData),
canDelete: data.dec(_f$canDelete),
canDownload: data.dec(_f$canDownload),
jellyType: data.dec(_f$jellyType));
}
@override
final Function instantiate = _instantiate;
static BoxSetModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<BoxSetModel>(map);
}
static BoxSetModel fromJson(String json) {
return ensureInitialized().decodeJson<BoxSetModel>(json);
}
}
mixin BoxSetModelMappable {
String toJson() {
return BoxSetModelMapper.ensureInitialized()
.encodeJson<BoxSetModel>(this as BoxSetModel);
}
Map<String, dynamic> toMap() {
return BoxSetModelMapper.ensureInitialized()
.encodeMap<BoxSetModel>(this as BoxSetModel);
}
BoxSetModelCopyWith<BoxSetModel, BoxSetModel, BoxSetModel> get copyWith =>
_BoxSetModelCopyWithImpl(this as BoxSetModel, $identity, $identity);
@override
String toString() {
return BoxSetModelMapper.ensureInitialized()
.stringifyValue(this as BoxSetModel);
}
}
extension BoxSetModelValueCopy<$R, $Out>
on ObjectCopyWith<$R, BoxSetModel, $Out> {
BoxSetModelCopyWith<$R, BoxSetModel, $Out> get $asBoxSetModel =>
$base.as((v, t, t2) => _BoxSetModelCopyWithImpl(v, t, t2));
}
abstract class BoxSetModelCopyWith<$R, $In extends BoxSetModel, $Out>
implements ItemBaseModelCopyWith<$R, $In, $Out> {
ListCopyWith<$R, ItemBaseModel,
ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>> get items;
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview;
@override
UserDataCopyWith<$R, UserData, UserData> get userData;
@override
$R call(
{List<ItemBaseModel>? items,
String? name,
String? id,
OverviewModel? overview,
String? parentId,
String? playlistId,
ImagesData? images,
int? childCount,
double? primaryRatio,
UserData? userData,
bool? canDelete,
bool? canDownload,
BaseItemKind? jellyType});
BoxSetModelCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t);
}
class _BoxSetModelCopyWithImpl<$R, $Out>
extends ClassCopyWithBase<$R, BoxSetModel, $Out>
implements BoxSetModelCopyWith<$R, BoxSetModel, $Out> {
_BoxSetModelCopyWithImpl(super.value, super.then, super.then2);
@override
late final ClassMapperBase<BoxSetModel> $mapper =
BoxSetModelMapper.ensureInitialized();
@override
ListCopyWith<$R, ItemBaseModel,
ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>>
get items => ListCopyWith(
$value.items, (v, t) => v.copyWith.$chain(t), (v) => call(items: v));
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview =>
$value.overview.copyWith.$chain((v) => call(overview: v));
@override
UserDataCopyWith<$R, UserData, UserData> get userData =>
$value.userData.copyWith.$chain((v) => call(userData: v));
@override
$R call(
{List<ItemBaseModel>? items,
String? name,
String? id,
OverviewModel? overview,
Object? parentId = $none,
Object? playlistId = $none,
Object? images = $none,
Object? childCount = $none,
Object? primaryRatio = $none,
UserData? userData,
Object? canDelete = $none,
Object? canDownload = $none,
Object? jellyType = $none}) =>
$apply(FieldCopyWithData({
if (items != null) #items: items,
if (name != null) #name: name,
if (id != null) #id: id,
if (overview != null) #overview: overview,
if (parentId != $none) #parentId: parentId,
if (playlistId != $none) #playlistId: playlistId,
if (images != $none) #images: images,
if (childCount != $none) #childCount: childCount,
if (primaryRatio != $none) #primaryRatio: primaryRatio,
if (userData != null) #userData: userData,
if (canDelete != $none) #canDelete: canDelete,
if (canDownload != $none) #canDownload: canDownload,
if (jellyType != $none) #jellyType: jellyType
}));
@override
BoxSetModel $make(CopyWithData data) => BoxSetModel(
items: data.get(#items, or: $value.items),
name: data.get(#name, or: $value.name),
id: data.get(#id, or: $value.id),
overview: data.get(#overview, or: $value.overview),
parentId: data.get(#parentId, or: $value.parentId),
playlistId: data.get(#playlistId, or: $value.playlistId),
images: data.get(#images, or: $value.images),
childCount: data.get(#childCount, or: $value.childCount),
primaryRatio: data.get(#primaryRatio, or: $value.primaryRatio),
userData: data.get(#userData, or: $value.userData),
canDelete: data.get(#canDelete, or: $value.canDelete),
canDownload: data.get(#canDownload, or: $value.canDownload),
jellyType: data.get(#jellyType, or: $value.jellyType));
@override
BoxSetModelCopyWith<$R2, BoxSetModel, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t) =>
_BoxSetModelCopyWithImpl($value, $cast, t);
}

View file

@ -0,0 +1,49 @@
import 'package:ficonsax/ficonsax.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:flutter/material.dart';
extension CollectionTypeExtension on CollectionType {
IconData get iconOutlined {
return getIconType(true);
}
IconData get icon {
return getIconType(false);
}
Set<FladderItemType> get itemKinds {
switch (this) {
case CollectionType.movies:
return {FladderItemType.movie};
case CollectionType.tvshows:
return {FladderItemType.series};
case CollectionType.homevideos:
return {FladderItemType.photoalbum, FladderItemType.folder, FladderItemType.photo, FladderItemType.video};
case CollectionType.boxsets:
case CollectionType.folders:
case CollectionType.books:
default:
return {};
}
}
IconData getIconType(bool outlined) {
switch (this) {
case CollectionType.movies:
return outlined ? IconsaxOutline.video_horizontal : IconsaxBold.video_horizontal;
case CollectionType.tvshows:
return outlined ? IconsaxOutline.video_vertical : IconsaxBold.video_vertical;
case CollectionType.boxsets:
case CollectionType.folders:
return outlined ? IconsaxOutline.folder : IconsaxBold.folder;
case CollectionType.homevideos:
return outlined ? IconsaxOutline.gallery : IconsaxBold.gallery;
case CollectionType.books:
return outlined ? IconsaxOutline.book : Icons.book_rounded;
default:
return IconsaxOutline.info_circle;
}
}
}

View file

@ -0,0 +1,81 @@
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/util/application_info.dart';
import 'package:xid/xid.dart';
class CredentialsModel {
final String token;
final String server;
final String serverName;
final String serverId;
final String deviceId;
CredentialsModel({
this.token = "",
this.server = "",
this.serverName = "",
this.serverId = "",
required this.deviceId,
});
factory CredentialsModel.createNewCredentials() {
return CredentialsModel(deviceId: Xid().toString());
}
Map<String, String> header(Ref ref) {
final application = ref.read(applicationInfoProvider);
final headers = {
'content-type': 'application/json',
'x-emby-token': token,
'x-emby-authorization':
'MediaBrowser Client="${application.name}", Device="${application.os}", DeviceId="$deviceId", Version="${application.version}"'
};
return headers;
}
CredentialsModel copyWith({
String? token,
String? server,
String? serverName,
String? serverId,
String? deviceId,
}) {
return CredentialsModel(
token: token ?? this.token,
server: server ?? this.server,
serverName: serverName ?? this.serverName,
serverId: serverId ?? this.serverId,
deviceId: deviceId ?? this.deviceId,
);
}
Map<String, dynamic> toMap() {
return {
'token': token,
'server': server,
'serverName': serverName,
'serverId': serverId,
'deviceId': deviceId,
};
}
factory CredentialsModel.fromMap(Map<String, dynamic> map) {
return CredentialsModel(
token: map['token'] ?? '',
server: map['server'] ?? '',
serverName: map['serverName'] ?? '',
serverId: map['serverId'] ?? '',
deviceId: map['deviceId'] ?? '',
);
}
String toJson() => json.encode(toMap());
factory CredentialsModel.fromJson(String source) => CredentialsModel.fromMap(json.decode(source));
@override
String toString() {
return 'CredentialsModel(token: $token, server: $server, serverName: $serverName, serverId: $serverId, header: $header)';
}
}

View file

@ -0,0 +1,26 @@
import 'package:fladder/models/item_base_model.dart';
class FavouritesModel {
final bool loading;
final Map<FladderItemType, List<ItemBaseModel>> favourites;
final List<ItemBaseModel> people;
FavouritesModel({
this.loading = false,
this.favourites = const {},
this.people = const [],
});
FavouritesModel copyWith({
bool? loading,
String? searchQuery,
Map<FladderItemType, List<ItemBaseModel>>? favourites,
List<ItemBaseModel>? people,
}) {
return FavouritesModel(
loading: loading ?? this.loading,
favourites: favourites ?? this.favourites,
people: people ?? this.people,
);
}
}

View file

@ -0,0 +1,34 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:fladder/models/item_base_model.dart';
class HomeModel {
final bool loading;
final List<ItemBaseModel> resumeVideo;
final List<ItemBaseModel> resumeAudio;
final List<ItemBaseModel> resumeBooks;
final List<ItemBaseModel> nextUp;
HomeModel({
this.loading = false,
this.resumeVideo = const [],
this.resumeAudio = const [],
this.resumeBooks = const [],
this.nextUp = const [],
});
HomeModel copyWith({
bool? loading,
List<ItemBaseModel>? resumeVideo,
List<ItemBaseModel>? resumeAudio,
List<ItemBaseModel>? resumeBooks,
List<ItemBaseModel>? nextUp,
List<ItemBaseModel>? nextUpBooks,
}) {
return HomeModel(
loading: loading ?? this.loading,
resumeVideo: resumeVideo ?? this.resumeVideo,
resumeAudio: resumeAudio ?? this.resumeAudio,
resumeBooks: resumeBooks ?? this.resumeBooks,
nextUp: nextUp ?? this.nextUp,
);
}
}

View file

@ -0,0 +1,107 @@
// ignore_for_file: constant_identifier_names
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/util/size_formatting.dart';
class InformationModel {
final Map<String, dynamic> baseInformation;
final List<Map<String, dynamic>> videoStreams;
final List<Map<String, dynamic>> audioStreams;
final List<Map<String, dynamic>> subStreams;
InformationModel({
required this.baseInformation,
required this.videoStreams,
required this.audioStreams,
required this.subStreams,
});
static InformationModel? fromResponse(BaseItemDto? item) {
if (item == null) return null;
var videoStreams = item.mediaStreams?.where((element) => element.type == MediaStreamType.video).toList() ?? [];
var audioStreams = item.mediaStreams?.where((element) => element.type == MediaStreamType.audio).toList() ?? [];
var subStreams = item.mediaStreams?.where((element) => element.type == MediaStreamType.subtitle).toList() ?? [];
return InformationModel(
baseInformation: {
"Title": item.name,
"Container": item.container,
"Path": item.path,
"Size": item.mediaSources?.firstOrNull?.size.byteFormat,
},
videoStreams: videoStreams
.map(
(e) => {
"Title": e.displayTitle,
"Codec": e.codec,
"Profile": e.profile,
"Level": e.level,
"Resolution": "${e.width}x${e.height}",
"Aspect Ration": e.aspectRatio,
"Interlaced": e.isInterlaced,
"FrameRate": e.realFrameRate,
"Bitrate": "${e.bitRate} kbps",
"Bit depth": e.bitDepth,
"Video range": e.videoRange,
"Video range type": e.videoRangeType,
"Ref frames": e.refFrames,
},
)
.toList(),
audioStreams: audioStreams
.map(
(e) => {
"Title": e.displayTitle,
"Language": e.language,
"Codec": e.codec,
"Layout": e.channelLayout,
"Bitrate": "${e.bitRate} kbps",
"Sample Rate": "${e.sampleRate} Hz",
"Default": e.isDefault,
"Forced": e.isForced,
"External": e.isExternal,
},
)
.toList(),
subStreams: subStreams
.map(
(e) => {
"Title": e.displayTitle,
"Language": e.language,
"Codec": e.codec,
"Profile": e.profile,
"Default": e.isDefault,
"Forced": e.isForced,
"External": e.isExternal,
},
)
.toList(),
);
}
InformationModel copyWith({
Map<String, dynamic>? baseInformation,
List<Map<String, dynamic>>? videoStreams,
List<Map<String, dynamic>>? audioStreams,
List<Map<String, dynamic>>? subStreams,
}) {
return InformationModel(
baseInformation: baseInformation ?? this.baseInformation,
videoStreams: videoStreams ?? this.videoStreams,
audioStreams: audioStreams ?? this.audioStreams,
subStreams: subStreams ?? this.subStreams,
);
}
static String mapToString(Map<String, dynamic> map) {
return map.entries.map((e) => "${e.key}: ${e.value}").join("\n");
}
static String streamsToString(List<Map<String, dynamic>> streams) {
return streams.map((e) => mapToString(e)).join("\n");
}
@override
String toString() => "${mapToString(baseInformation)}\n\n"
"${streamsToString(videoStreams)}\n\n"
"${streamsToString(audioStreams)}\n\n"
"${streamsToString(subStreams)}\n\n";
}

View file

@ -0,0 +1,363 @@
import 'package:dart_mappable/dart_mappable.dart';
import 'package:ficonsax/ficonsax.dart';
import 'package:fladder/models/book_model.dart';
import 'package:fladder/models/boxset_model.dart';
import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/library_search/library_search_options.dart';
import 'package:fladder/models/playlist_model.dart';
import 'package:fladder/routes/build_routes/home_routes.dart';
import 'package:fladder/routes/build_routes/route_builder.dart';
import 'package:fladder/screens/details_screens/book_detail_screen.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/string_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/folder_model.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/movie_model.dart';
import 'package:fladder/models/items/overview_model.dart';
import 'package:fladder/models/items/person_model.dart';
import 'package:fladder/models/items/photos_model.dart';
import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/models/items/series_model.dart';
import 'package:fladder/screens/details_screens/details_screens.dart';
import 'package:fladder/screens/details_screens/episode_detail_screen.dart';
import 'package:fladder/screens/details_screens/season_detail_screen.dart';
import 'package:fladder/screens/library_search/library_search_screen.dart';
part 'item_base_model.mapper.dart';
@MappableClass()
class ItemBaseModel with ItemBaseModelMappable {
final String name;
final String id;
final OverviewModel overview;
final String? parentId;
final String? playlistId;
final ImagesData? images;
final int? childCount;
final double? primaryRatio;
final UserData userData;
final bool? canDownload;
final bool? canDelete;
final dto.BaseItemKind? jellyType;
const ItemBaseModel({
required this.name,
required this.id,
required this.overview,
required this.parentId,
required this.playlistId,
required this.images,
required this.childCount,
required this.primaryRatio,
required this.userData,
required this.canDownload,
required this.canDelete,
required this.jellyType,
});
String get title => name;
ItemBaseModel? setProgress(double progress) {
return copyWith(userData: userData.copyWith(progress: progress));
}
Widget? subTitle(SortingOptions options) => switch (options) {
SortingOptions.parentalRating => overview.parentalRating != null
? Row(
children: [
Icon(
IconsaxBold.star_1,
size: 14,
color: Colors.yellowAccent,
),
const SizedBox(width: 6),
Text((overview.parentalRating ?? 0.0).toString())
],
)
: null,
SortingOptions.communityRating => overview.communityRating != null
? Row(
children: [
Icon(
IconsaxBold.star_1,
size: 14,
color: Colors.yellowAccent,
),
const SizedBox(width: 6),
Text((overview.communityRating ?? 0.0).toString())
],
)
: null,
_ => null,
};
///Used for retrieving the correct id when fetching queue
String get streamId => id;
ItemBaseModel get parentBaseModel => copyWith(id: parentId);
bool get emptyShow => false;
bool get identifiable => false;
int? get unPlayedItemCount => userData.unPlayedItemCount;
bool get unWatched => !userData.played && userData.progress <= 0 && userData.unPlayedItemCount == 0;
String? detailedName(BuildContext context) => null;
String? get subText => null;
String? subTextShort(BuildContext context) => null;
String? label(BuildContext context) => null;
ImagesData? get getPosters => images;
ImageData? get bannerImage => images?.primary ?? getPosters?.randomBackDrop ?? getPosters?.primary;
bool get playAble => false;
bool get syncAble => false;
bool get galleryItem => false;
MediaStreamsModel? get streamModel => null;
String playText(BuildContext context) => context.localized.play(name);
double get progress => userData.progress;
String playButtonLabel(BuildContext context) =>
progress != 0 ? context.localized.resume(name.maxLength()) : context.localized.play(name.maxLength());
Widget get detailScreenWidget {
switch (this) {
case PersonModel _:
return PersonDetailScreen(person: Person(id: id, image: images?.primary));
case SeasonModel _:
return SeasonDetailScreen(item: this);
case FolderModel _:
case PhotoAlbumModel _:
case BoxSetModel _:
case PlaylistModel _:
return LibrarySearchScreen(folderId: [id]);
case PhotoModel _:
final photo = this as PhotoModel;
return LibrarySearchScreen(
folderId: [photo.albumId ?? photo.parentId ?? ""],
photoToView: photo,
);
case BookModel book:
return BookDetailScreen(item: book);
case MovieModel _:
return MovieDetailScreen(item: this);
case EpisodeModel _:
return EpisodeDetailScreen(item: this);
case SeriesModel series:
return SeriesDetailScreen(item: series);
default:
return EmptyItem(item: this);
}
}
Future<void> navigateTo(BuildContext context) async => context.routePush(DetailsRoute(id: id), extra: this);
factory ItemBaseModel.fromBaseDto(dto.BaseItemDto item, Ref ref) {
return switch (item.type) {
BaseItemKind.photo || BaseItemKind.video => PhotoModel.fromBaseDto(item, ref),
BaseItemKind.photoalbum => PhotoAlbumModel.fromBaseDto(item, ref),
BaseItemKind.folder ||
BaseItemKind.collectionfolder ||
BaseItemKind.aggregatefolder =>
FolderModel.fromBaseDto(item, ref),
BaseItemKind.episode => EpisodeModel.fromBaseDto(item, ref),
BaseItemKind.movie => MovieModel.fromBaseDto(item, ref),
BaseItemKind.series => SeriesModel.fromBaseDto(item, ref),
BaseItemKind.person => PersonModel.fromBaseDto(item, ref),
BaseItemKind.season => SeasonModel.fromBaseDto(item, ref),
BaseItemKind.boxset => BoxSetModel.fromBaseDto(item, ref),
BaseItemKind.book => BookModel.fromBaseDto(item, ref),
BaseItemKind.playlist => PlaylistModel.fromBaseDto(item, ref),
_ => ItemBaseModel._fromBaseDto(item, ref)
};
}
factory ItemBaseModel._fromBaseDto(dto.BaseItemDto item, Ref ref) {
return ItemBaseModel(
name: item.name ?? "",
id: item.id ?? "",
childCount: item.childCount,
overview: OverviewModel.fromBaseItemDto(item, ref),
userData: UserData.fromDto(item.userData),
parentId: item.parentId,
playlistId: item.playlistItemId,
images: ImagesData.fromBaseItem(item, ref),
primaryRatio: item.primaryImageAspectRatio,
canDelete: item.canDelete,
canDownload: item.canDownload,
jellyType: item.type,
);
}
FladderItemType get type => switch (this) {
MovieModel _ => FladderItemType.movie,
SeriesModel _ => FladderItemType.series,
SeasonModel _ => FladderItemType.season,
PhotoAlbumModel _ => FladderItemType.photoalbum,
PhotoModel model => model.internalType,
EpisodeModel _ => FladderItemType.episode,
BookModel _ => FladderItemType.book,
PlaylistModel _ => FladderItemType.playlist,
FolderModel _ => FladderItemType.folder,
ItemBaseModel _ => FladderItemType.baseType,
};
@override
bool operator ==(covariant ItemBaseModel other) {
if (identical(this, other)) return true;
return other.id == id;
}
@override
int get hashCode {
return id.hashCode ^ type.hashCode;
}
}
// Currently supported types
enum FladderItemType {
baseType(
icon: IconsaxOutline.folder_2,
selectedicon: IconsaxBold.folder_2,
),
audio(
icon: IconsaxOutline.music,
selectedicon: IconsaxBold.music,
),
musicAlbum(
icon: IconsaxOutline.music,
selectedicon: IconsaxBold.music,
),
musicVideo(
icon: IconsaxOutline.music,
selectedicon: IconsaxBold.music,
),
collectionFolder(
icon: IconsaxOutline.music,
selectedicon: IconsaxBold.music,
),
video(
icon: IconsaxOutline.video,
selectedicon: IconsaxBold.video,
),
movie(
icon: IconsaxOutline.video_horizontal,
selectedicon: IconsaxBold.video_horizontal,
),
series(
icon: IconsaxOutline.video_vertical,
selectedicon: IconsaxBold.video_vertical,
),
season(
icon: IconsaxOutline.video_vertical,
selectedicon: IconsaxBold.video_vertical,
),
episode(
icon: IconsaxOutline.video_vertical,
selectedicon: IconsaxBold.video_vertical,
),
photo(
icon: IconsaxOutline.picture_frame,
selectedicon: IconsaxBold.picture_frame,
),
person(
icon: IconsaxOutline.user,
selectedicon: IconsaxBold.user,
),
photoalbum(
icon: IconsaxOutline.gallery,
selectedicon: IconsaxBold.gallery,
),
folder(
icon: IconsaxOutline.folder,
selectedicon: IconsaxBold.folder,
),
boxset(
icon: IconsaxOutline.bookmark,
selectedicon: IconsaxBold.bookmark,
),
playlist(
icon: IconsaxOutline.archive_book,
selectedicon: IconsaxBold.archive_book,
),
book(
icon: IconsaxOutline.book,
selectedicon: IconsaxBold.book,
);
const FladderItemType({required this.icon, required this.selectedicon});
static Set<FladderItemType> get playable => {
FladderItemType.series,
FladderItemType.episode,
FladderItemType.season,
FladderItemType.movie,
FladderItemType.musicVideo,
};
static Set<FladderItemType> get galleryItem => {
FladderItemType.photo,
FladderItemType.video,
};
String label(BuildContext context) {
return switch (this) {
FladderItemType.baseType => context.localized.mediaTypeBase,
FladderItemType.audio => context.localized.audio,
FladderItemType.collectionFolder => context.localized.collectionFolder,
FladderItemType.musicAlbum => context.localized.musicAlbum,
FladderItemType.musicVideo => context.localized.video,
FladderItemType.video => context.localized.video,
FladderItemType.movie => context.localized.mediaTypeMovie,
FladderItemType.series => context.localized.mediaTypeSeries,
FladderItemType.season => context.localized.mediaTypeSeason,
FladderItemType.episode => context.localized.mediaTypeEpisode,
FladderItemType.photo => context.localized.mediaTypePhoto,
FladderItemType.person => context.localized.mediaTypePerson,
FladderItemType.photoalbum => context.localized.mediaTypePhotoAlbum,
FladderItemType.folder => context.localized.mediaTypeFolder,
FladderItemType.boxset => context.localized.mediaTypeBoxset,
FladderItemType.playlist => context.localized.mediaTypePlaylist,
FladderItemType.book => context.localized.mediaTypeBook,
};
}
BaseItemKind get dtoKind => switch (this) {
FladderItemType.baseType => BaseItemKind.userrootfolder,
FladderItemType.audio => BaseItemKind.audio,
FladderItemType.collectionFolder => BaseItemKind.collectionfolder,
FladderItemType.musicAlbum => BaseItemKind.musicalbum,
FladderItemType.musicVideo => BaseItemKind.video,
FladderItemType.video => BaseItemKind.video,
FladderItemType.movie => BaseItemKind.movie,
FladderItemType.series => BaseItemKind.series,
FladderItemType.season => BaseItemKind.season,
FladderItemType.episode => BaseItemKind.episode,
FladderItemType.photo => BaseItemKind.photo,
FladderItemType.person => BaseItemKind.person,
FladderItemType.photoalbum => BaseItemKind.photoalbum,
FladderItemType.folder => BaseItemKind.folder,
FladderItemType.boxset => BaseItemKind.boxset,
FladderItemType.playlist => BaseItemKind.playlist,
FladderItemType.book => BaseItemKind.book,
};
final IconData icon;
final IconData selectedicon;
}

View file

@ -0,0 +1,214 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
part of 'item_base_model.dart';
class ItemBaseModelMapper extends ClassMapperBase<ItemBaseModel> {
ItemBaseModelMapper._();
static ItemBaseModelMapper? _instance;
static ItemBaseModelMapper ensureInitialized() {
if (_instance == null) {
MapperContainer.globals.use(_instance = ItemBaseModelMapper._());
OverviewModelMapper.ensureInitialized();
UserDataMapper.ensureInitialized();
}
return _instance!;
}
@override
final String id = 'ItemBaseModel';
static String _$name(ItemBaseModel v) => v.name;
static const Field<ItemBaseModel, String> _f$name = Field('name', _$name);
static String _$id(ItemBaseModel v) => v.id;
static const Field<ItemBaseModel, String> _f$id = Field('id', _$id);
static OverviewModel _$overview(ItemBaseModel v) => v.overview;
static const Field<ItemBaseModel, OverviewModel> _f$overview =
Field('overview', _$overview);
static String? _$parentId(ItemBaseModel v) => v.parentId;
static const Field<ItemBaseModel, String> _f$parentId =
Field('parentId', _$parentId);
static String? _$playlistId(ItemBaseModel v) => v.playlistId;
static const Field<ItemBaseModel, String> _f$playlistId =
Field('playlistId', _$playlistId);
static ImagesData? _$images(ItemBaseModel v) => v.images;
static const Field<ItemBaseModel, ImagesData> _f$images =
Field('images', _$images);
static int? _$childCount(ItemBaseModel v) => v.childCount;
static const Field<ItemBaseModel, int> _f$childCount =
Field('childCount', _$childCount);
static double? _$primaryRatio(ItemBaseModel v) => v.primaryRatio;
static const Field<ItemBaseModel, double> _f$primaryRatio =
Field('primaryRatio', _$primaryRatio);
static UserData _$userData(ItemBaseModel v) => v.userData;
static const Field<ItemBaseModel, UserData> _f$userData =
Field('userData', _$userData);
static bool? _$canDownload(ItemBaseModel v) => v.canDownload;
static const Field<ItemBaseModel, bool> _f$canDownload =
Field('canDownload', _$canDownload);
static bool? _$canDelete(ItemBaseModel v) => v.canDelete;
static const Field<ItemBaseModel, bool> _f$canDelete =
Field('canDelete', _$canDelete);
static dto.BaseItemKind? _$jellyType(ItemBaseModel v) => v.jellyType;
static const Field<ItemBaseModel, dto.BaseItemKind> _f$jellyType =
Field('jellyType', _$jellyType);
@override
final MappableFields<ItemBaseModel> fields = const {
#name: _f$name,
#id: _f$id,
#overview: _f$overview,
#parentId: _f$parentId,
#playlistId: _f$playlistId,
#images: _f$images,
#childCount: _f$childCount,
#primaryRatio: _f$primaryRatio,
#userData: _f$userData,
#canDownload: _f$canDownload,
#canDelete: _f$canDelete,
#jellyType: _f$jellyType,
};
@override
final bool ignoreNull = true;
static ItemBaseModel _instantiate(DecodingData data) {
return ItemBaseModel(
name: data.dec(_f$name),
id: data.dec(_f$id),
overview: data.dec(_f$overview),
parentId: data.dec(_f$parentId),
playlistId: data.dec(_f$playlistId),
images: data.dec(_f$images),
childCount: data.dec(_f$childCount),
primaryRatio: data.dec(_f$primaryRatio),
userData: data.dec(_f$userData),
canDownload: data.dec(_f$canDownload),
canDelete: data.dec(_f$canDelete),
jellyType: data.dec(_f$jellyType));
}
@override
final Function instantiate = _instantiate;
static ItemBaseModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<ItemBaseModel>(map);
}
static ItemBaseModel fromJson(String json) {
return ensureInitialized().decodeJson<ItemBaseModel>(json);
}
}
mixin ItemBaseModelMappable {
String toJson() {
return ItemBaseModelMapper.ensureInitialized()
.encodeJson<ItemBaseModel>(this as ItemBaseModel);
}
Map<String, dynamic> toMap() {
return ItemBaseModelMapper.ensureInitialized()
.encodeMap<ItemBaseModel>(this as ItemBaseModel);
}
ItemBaseModelCopyWith<ItemBaseModel, ItemBaseModel, ItemBaseModel>
get copyWith => _ItemBaseModelCopyWithImpl(
this as ItemBaseModel, $identity, $identity);
@override
String toString() {
return ItemBaseModelMapper.ensureInitialized()
.stringifyValue(this as ItemBaseModel);
}
}
extension ItemBaseModelValueCopy<$R, $Out>
on ObjectCopyWith<$R, ItemBaseModel, $Out> {
ItemBaseModelCopyWith<$R, ItemBaseModel, $Out> get $asItemBaseModel =>
$base.as((v, t, t2) => _ItemBaseModelCopyWithImpl(v, t, t2));
}
abstract class ItemBaseModelCopyWith<$R, $In extends ItemBaseModel, $Out>
implements ClassCopyWith<$R, $In, $Out> {
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview;
UserDataCopyWith<$R, UserData, UserData> get userData;
$R call(
{String? name,
String? id,
OverviewModel? overview,
String? parentId,
String? playlistId,
ImagesData? images,
int? childCount,
double? primaryRatio,
UserData? userData,
bool? canDownload,
bool? canDelete,
dto.BaseItemKind? jellyType});
ItemBaseModelCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t);
}
class _ItemBaseModelCopyWithImpl<$R, $Out>
extends ClassCopyWithBase<$R, ItemBaseModel, $Out>
implements ItemBaseModelCopyWith<$R, ItemBaseModel, $Out> {
_ItemBaseModelCopyWithImpl(super.value, super.then, super.then2);
@override
late final ClassMapperBase<ItemBaseModel> $mapper =
ItemBaseModelMapper.ensureInitialized();
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview =>
$value.overview.copyWith.$chain((v) => call(overview: v));
@override
UserDataCopyWith<$R, UserData, UserData> get userData =>
$value.userData.copyWith.$chain((v) => call(userData: v));
@override
$R call(
{String? name,
String? id,
OverviewModel? overview,
Object? parentId = $none,
Object? playlistId = $none,
Object? images = $none,
Object? childCount = $none,
Object? primaryRatio = $none,
UserData? userData,
Object? canDownload = $none,
Object? canDelete = $none,
Object? jellyType = $none}) =>
$apply(FieldCopyWithData({
if (name != null) #name: name,
if (id != null) #id: id,
if (overview != null) #overview: overview,
if (parentId != $none) #parentId: parentId,
if (playlistId != $none) #playlistId: playlistId,
if (images != $none) #images: images,
if (childCount != $none) #childCount: childCount,
if (primaryRatio != $none) #primaryRatio: primaryRatio,
if (userData != null) #userData: userData,
if (canDownload != $none) #canDownload: canDownload,
if (canDelete != $none) #canDelete: canDelete,
if (jellyType != $none) #jellyType: jellyType
}));
@override
ItemBaseModel $make(CopyWithData data) => ItemBaseModel(
name: data.get(#name, or: $value.name),
id: data.get(#id, or: $value.id),
overview: data.get(#overview, or: $value.overview),
parentId: data.get(#parentId, or: $value.parentId),
playlistId: data.get(#playlistId, or: $value.playlistId),
images: data.get(#images, or: $value.images),
childCount: data.get(#childCount, or: $value.childCount),
primaryRatio: data.get(#primaryRatio, or: $value.primaryRatio),
userData: data.get(#userData, or: $value.userData),
canDownload: data.get(#canDownload, or: $value.canDownload),
canDelete: data.get(#canDelete, or: $value.canDelete),
jellyType: data.get(#jellyType, or: $value.jellyType));
@override
ItemBaseModelCopyWith<$R2, ItemBaseModel, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t) =>
_ItemBaseModelCopyWithImpl($value, $cast, t);
}

View file

@ -0,0 +1,318 @@
import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:fladder/models/items/series_model.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as jelly;
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/providers/image_provider.dart';
class EditItemsProvider {
final List<EditingImageModel> serverImages;
final List<EditingImageModel> images;
final List<EditingImageModel> customImages;
final EditingImageModel? selected;
final List<EditingImageModel> selection;
const EditItemsProvider({
this.serverImages = const [],
this.images = const [],
this.customImages = const [],
this.selected,
this.selection = const [],
});
Future<void> setImage(
jelly.ImageType type, {
required Function(EditingImageModel? imageModel) uploadData,
required Function(EditingImageModel? imageModel) uploadUrl,
}) async {
switch (type) {
case jelly.ImageType.primary:
case jelly.ImageType.logo:
{
if (selected == null) return;
if (selected?.imageData != null) {
await uploadData(selected!.copyWith(type: type));
} else if (selected?.url != null) {
await uploadUrl(selected!.copyWith(type: type));
}
}
case jelly.ImageType.backdrop:
{
for (var element in selection) {
if (element.imageData != null) {
await uploadData(element.copyWith(type: type));
} else if (element.url != null) {
await uploadUrl(element.copyWith(type: type));
}
}
}
default:
}
return;
}
EditItemsProvider copyWith({
List<EditingImageModel>? serverImages,
List<EditingImageModel>? images,
List<EditingImageModel>? customImages,
ValueGetter<EditingImageModel?>? selected,
List<EditingImageModel>? selection,
}) {
return EditItemsProvider(
serverImages: serverImages ?? this.serverImages,
images: images ?? this.images,
customImages: customImages ?? this.customImages,
selected: selected != null ? selected() : this.selected,
selection: selection ?? this.selection,
);
}
}
class ItemEditingModel {
final ItemBaseModel? item;
final jelly.MetadataEditorInfo? editorInfo;
final Map<String, dynamic>? json;
final Map<String, dynamic>? editedJson;
final bool includeAllImages;
final EditItemsProvider primary;
final EditItemsProvider logo;
final EditItemsProvider backdrop;
final bool saving;
ItemEditingModel({
this.item,
this.editorInfo,
this.json,
this.editedJson,
this.includeAllImages = false,
this.primary = const EditItemsProvider(),
this.logo = const EditItemsProvider(),
this.backdrop = const EditItemsProvider(),
this.saving = false,
});
Map<String, dynamic>? editAbleFields() {
return editedJson == null
? {}
: {
"Name": editedJson?["Name"] as String?,
"OriginalTitle": editedJson?["OriginalTitle"] as String?,
"PremiereDate": editedJson?["PremiereDate"] != null ? DateTime.tryParse(editedJson!["PremiereDate"]) : null,
"DateCreated": editedJson?["DateCreated"] != null ? DateTime.tryParse(editedJson!["DateCreated"]) : null,
"ProductionYear": editedJson?["ProductionYear"] as int?,
"Path": editedJson?["Path"] as String?,
"Overview": editedJson?["Overview"] as String? ?? "",
}
..removeWhere((key, value) => value == null);
}
Map<String, dynamic>? editAdvancedAbleFields(Ref ref) => editedJson == null
? {}
: {
if (item is SeriesModel) "DisplayOrder": DisplayOrder.fromMap(editedJson?["DisplayOrder"]),
if (item is SeriesModel) ...{
"OfficialRating": {
for (String element in (editorInfo?.parentalRatingOptions?.map((e) => e.name).toSet()
?..add(json?["OfficialRating"] as String?))
?.whereNotNull()
.toList() ??
[])
element: (editedJson?["OfficialRating"] as String?) == element
},
"CustomRating": {
for (String element in (editorInfo?.parentalRatingOptions?.map((e) => e.name).toSet()
?..add(json?["CustomRating"] as String?))
?.whereNotNull()
.toList() ??
[])
element: (editedJson?["CustomRating"] as String?) == element
},
},
"People": editedJson?["People"] != null
? (editedJson!["People"] as List<dynamic>)
.map((e) => Person.fromBasePerson(jelly.BaseItemPerson.fromJson(e), ref))
.toList()
: null,
"ExternalUrls": editedJson?["ExternalUrls"] != null
? (editedJson!["ExternalUrls"] as List<dynamic>).map((e) => ExternalUrls.fromMap(e)).toList()
: null,
"CommunityRating": double.tryParse((editedJson?["CommunityRating"] as num?).toString()),
"SeriesName": editedJson?["SeriesName"] as String?,
"IndexNumber": editedJson?["IndexNumber"] as int?,
"RunTimeTicks": (editedJson?["RunTimeTicks"] == null)
? null
: Duration(milliseconds: editedJson?["RunTimeTicks"] ~/ 10000),
"ParentIndexNumber": editedJson?["ParentIndexNumber"] as int?,
if (item is SeriesModel) "Status": ShowStatus.fromMap(editedJson?["Status"] as String?),
"Genres": editedJson?["Genres"] != null ? (List<String>.from(editedJson!["Genres"])) : null,
"Tags": editedJson?["Tags"] != null ? (List<String>.from(editedJson?["Tags"])) : null,
"Studios": editedJson?["Studios"] != null
? (editedJson!["Studios"] as List<dynamic>).map((e) => Studio.fromMap(e)).toList()
: null,
"SeriesStudio": editedJson?["SeriesStudio"] as String?,
"LockData": editedJson?["LockData"] as bool? ?? false,
"LockedFields": ((editedJson?["LockData"] as bool?) == false)
? EditorLockedFields.enabled(List<String>.from(editedJson?["LockedFields"]))
: null,
}
..removeWhere((key, value) => value == null);
ItemEditingModel copyWith({
ValueGetter<ItemBaseModel?>? item,
ValueGetter<jelly.MetadataEditorInfo?>? editorInfo,
ValueGetter<Map<String, dynamic>?>? json,
ValueGetter<Map<String, dynamic>?>? editedJson,
bool? includeAllImages,
EditItemsProvider? primary,
EditItemsProvider? logo,
EditItemsProvider? backdrop,
bool? saving,
}) {
return ItemEditingModel(
item: item != null ? item() : this.item,
editorInfo: editorInfo != null ? editorInfo() : this.editorInfo,
json: json != null ? json() : this.json,
editedJson: editedJson != null ? editedJson() : this.editedJson,
includeAllImages: includeAllImages ?? this.includeAllImages,
primary: primary ?? this.primary,
logo: logo ?? this.logo,
backdrop: backdrop ?? this.backdrop,
saving: saving ?? this.saving,
);
}
}
class EditingImageModel {
final String providerName;
final String? url;
final Uint8List? imageData;
final int? index;
final int height;
final int width;
final double communityRating;
final int voteCount;
final String language;
final jelly.ImageType type;
final jelly.RatingType ratingType;
EditingImageModel({
required this.providerName,
this.url,
this.imageData,
this.index,
this.height = 0,
this.width = 0,
this.communityRating = 0.0,
this.voteCount = 0,
this.language = "",
this.type = jelly.ImageType.primary,
this.ratingType = jelly.RatingType.likes,
});
double get ratio {
if (width == 0 && height == 0) return 1;
final ratio = (width.toDouble() / height.toDouble()).clamp(0.1, 5).toDouble();
if (ratio < 0) {
return 1;
} else {
return ratio;
}
}
factory EditingImageModel.fromDto(jelly.RemoteImageInfo info) {
return EditingImageModel(
providerName: info.providerName ?? "",
url: info.url ?? "",
height: info.height ?? 0,
width: info.width ?? 0,
communityRating: info.communityRating ?? 0.0,
voteCount: info.voteCount ?? 0,
language: info.language ?? "",
type: info.type ?? jelly.ImageType.primary,
ratingType: info.ratingType ?? jelly.RatingType.likes,
);
}
factory EditingImageModel.fromImage(jelly.ImageInfo info, String itemId, Ref ref) {
return EditingImageModel(
providerName: "",
url: switch (info.imageType ?? ImageType.primary) {
ImageType.backdrop => ref.read(imageUtilityProvider).getBackdropOrigImage(
itemId,
info.imageIndex ?? 0,
info.hashCode.toString(),
),
_ => ref.read(imageUtilityProvider).getItemsOrigImageUrl(
itemId,
type: info.imageType ?? ImageType.primary,
),
},
index: info.imageIndex,
height: info.height ?? 0,
width: info.width ?? 0,
type: info.imageType ?? ImageType.primary,
);
}
EditingImageModel copyWith({
String? providerName,
ValueGetter<String?>? url,
ValueGetter<Uint8List?>? imageData,
ValueGetter<int?>? index,
int? height,
int? width,
double? communityRating,
int? voteCount,
String? language,
jelly.ImageType? type,
jelly.RatingType? ratingType,
}) {
return EditingImageModel(
providerName: providerName ?? this.providerName,
url: url != null ? url() : this.url,
imageData: imageData != null ? imageData() : this.imageData,
index: index != null ? index() : this.index,
height: height ?? this.height,
width: width ?? this.width,
communityRating: communityRating ?? this.communityRating,
voteCount: voteCount ?? this.voteCount,
language: language ?? this.language,
type: type ?? this.type,
ratingType: ratingType ?? this.ratingType,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is EditingImageModel &&
other.providerName == providerName &&
other.url == url &&
other.imageData == imageData &&
other.height == height &&
other.width == width &&
other.communityRating == communityRating &&
other.voteCount == voteCount &&
other.language == language &&
other.type == type &&
other.ratingType == ratingType;
}
@override
int get hashCode {
return providerName.hashCode ^
url.hashCode ^
imageData.hashCode ^
height.hashCode ^
width.hashCode ^
communityRating.hashCode ^
voteCount.hashCode ^
language.hashCode ^
type.hashCode ^
ratingType.hashCode;
}
}

View file

@ -0,0 +1,90 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart';
import 'package:fladder/main.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
import 'package:fladder/providers/image_provider.dart';
class Chapter {
final String name;
final String imageUrl;
final Uint8List? imageData;
final Duration startPosition;
Chapter({
required this.name,
required this.imageUrl,
this.imageData,
required this.startPosition,
});
ImageProvider get imageProvider {
if (imageData != null) {
return Image.memory(imageData!).image;
}
if (imageUrl.startsWith("http")) {
return CachedNetworkImageProvider(
cacheKey: name + imageUrl,
cacheManager: CustomCacheManager.instance,
imageUrl,
);
} else {
return Image.file(
key: Key(name + imageUrl),
File(imageUrl),
).image;
}
}
static List<Chapter> chaptersFromInfo(String itemId, List<dto.ChapterInfo> chapters, Ref ref) {
return chapters
.mapIndexed((index, element) => Chapter(
name: element.name ?? "",
imageUrl: ref.read(imageUtilityProvider).getChapterUrl(itemId, index),
startPosition: Duration(milliseconds: (element.startPositionTicks ?? 0) ~/ 10000)))
.toList();
}
Chapter copyWith({
String? name,
String? imageUrl,
Duration? startPosition,
}) {
return Chapter(
name: name ?? this.name,
imageUrl: imageUrl ?? this.imageUrl,
startPosition: startPosition ?? this.startPosition,
);
}
Map<String, dynamic> toMap() {
return {
'name': name,
'imageUrl': imageUrl,
'startPosition': startPosition.inMilliseconds,
};
}
factory Chapter.fromMap(Map<String, dynamic> map) {
return Chapter(
name: map['name'] ?? '',
imageUrl: map['imageUrl'] ?? '',
startPosition: Duration(milliseconds: map['startPosition'] as int),
);
}
String toJson() => json.encode(toMap());
factory Chapter.fromJson(String source) => Chapter.fromMap(json.decode(source));
}
extension ChapterExtension on List<Chapter> {
Chapter? getChapterFromDuration(Duration duration) {
return lastWhereOrNull((element) => element.startPosition < duration);
}
}

View file

@ -0,0 +1,201 @@
import 'package:collection/collection.dart';
import 'package:fladder/jellyfin/enum_models.dart';
import 'package:fladder/models/items/series_model.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/string_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/item_stream_model.dart';
import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/items/overview_model.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'episode_model.mapper.dart';
enum EpisodeStatus { available, unaired, missing }
@MappableClass()
class EpisodeModel extends ItemStreamModel with EpisodeModelMappable {
final String? seriesName;
final int season;
final int episode;
final List<Chapter> chapters;
final ItemLocation? location;
final DateTime? dateAired;
const EpisodeModel({
required this.seriesName,
required this.season,
required this.episode,
this.chapters = const [],
this.location,
this.dateAired,
required super.name,
required super.id,
required super.overview,
required super.parentId,
required super.playlistId,
required super.images,
required super.childCount,
required super.primaryRatio,
required super.userData,
required super.parentImages,
required super.mediaStreams,
super.canDelete,
super.canDownload,
super.jellyType,
});
EpisodeStatus get status {
return switch (location) {
ItemLocation.filesystem => EpisodeStatus.available,
ItemLocation.virtual =>
(dateAired?.isBefore(DateTime.now()) == true) ? EpisodeStatus.missing : EpisodeStatus.unaired,
_ => EpisodeStatus.missing
};
}
@override
String? detailedName(BuildContext context) => "${subTextShort(context)} - $name";
@override
SeriesModel get parentBaseModel => SeriesModel(
originalTitle: '',
sortName: '',
status: "",
name: seriesName ?? "",
id: parentId ?? "",
playlistId: playlistId,
overview: overview,
parentId: parentId,
images: images,
childCount: childCount,
primaryRatio: primaryRatio,
userData: UserData(),
);
@override
String get streamId => parentId ?? "";
@override
String get title => seriesName ?? name;
@override
MediaStreamsModel? get streamModel => mediaStreams;
@override
ImagesData? get getPosters => parentImages;
@override
String? get subText => name.isEmpty ? "TBA" : name;
@override
String? subTextShort(BuildContext context) => seasonEpisodeLabel(context);
@override
String? label(BuildContext context) => "${subTextShort(context)} - $name";
@override
bool get playAble => switch (status) {
EpisodeStatus.available => true,
_ => false,
};
@override
String playButtonLabel(BuildContext context) {
final string = seasonEpisodeLabel(context).maxLength();
return progress != 0 ? context.localized.resume(string) : context.localized.play(string);
}
String seasonAnnotation(BuildContext context) => context.localized.season(1)[0];
String episodeAnnotation(BuildContext context) => context.localized.episode(1)[0];
String seasonEpisodeLabel(BuildContext context) {
return "${seasonAnnotation(context)}$season - ${episodeAnnotation(context)}$episode";
}
String seasonEpisodeLabelFull(BuildContext context) {
return "${context.localized.season(1)} $season - ${context.localized.episode(1)} $episode";
}
String episodeLabel(BuildContext context) {
return "${seasonEpisodeLabel(context)} - $subText";
}
String get fullName {
return "$episode. $subText";
}
@override
bool get syncAble => playAble;
@override
factory EpisodeModel.fromBaseDto(dto.BaseItemDto item, Ref ref) => EpisodeModel(
seriesName: item.seriesName,
name: item.name ?? "",
id: item.id ?? "",
childCount: item.childCount,
overview: OverviewModel.fromBaseItemDto(item, ref),
userData: UserData.fromDto(item.userData),
parentId: item.seriesId,
playlistId: item.playlistItemId,
dateAired: item.premiereDate,
chapters: Chapter.chaptersFromInfo(item.id ?? "", item.chapters ?? [], ref),
images: ImagesData.fromBaseItem(item, ref, getOriginalSize: true),
primaryRatio: item.primaryImageAspectRatio,
season: item.parentIndexNumber ?? 0,
episode: item.indexNumber ?? 0,
location: ItemLocation.fromDto(item.locationType),
parentImages: ImagesData.fromBaseItemParent(item, ref),
canDelete: item.canDelete,
canDownload: item.canDownload,
mediaStreams:
MediaStreamsModel.fromMediaStreamsList(item.mediaSources?.firstOrNull, item.mediaStreams ?? [], ref),
jellyType: item.type,
);
static List<EpisodeModel> episodesFromDto(List<dto.BaseItemDto>? dto, Ref ref) {
return dto?.map((e) => EpisodeModel.fromBaseDto(e, ref)).toList() ?? [];
}
}
extension EpisodeListExtensions on List<EpisodeModel> {
Map<int, List<EpisodeModel>> get episodesBySeason {
Map<int, List<EpisodeModel>> groupedItems = {};
for (int i = 0; i < length; i++) {
int seasonIndex = this[i].season;
if (!groupedItems.containsKey(seasonIndex)) {
groupedItems[seasonIndex] = [this[i]];
} else {
groupedItems[seasonIndex]?.add(this[i]);
}
}
return groupedItems;
}
EpisodeModel? get nextUp {
final lastProgress =
lastIndexWhere((element) => element.userData.progress != 0 && element.status == EpisodeStatus.available);
final lastPlayed =
lastIndexWhere((element) => element.userData.played && element.status == EpisodeStatus.available);
if (lastProgress == -1 && lastPlayed == -1) {
return firstWhereOrNull((element) => element.status == EpisodeStatus.available);
} else {
return getRange(lastProgress > lastPlayed ? lastProgress : lastPlayed + 1, length)
.firstWhereOrNull((element) => element.status == EpisodeStatus.available);
}
}
bool get allPlayed {
for (var element in this) {
if (!element.userData.played) {
return false;
}
}
return true;
}
}

View file

@ -0,0 +1,301 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
part of 'episode_model.dart';
class EpisodeModelMapper extends SubClassMapperBase<EpisodeModel> {
EpisodeModelMapper._();
static EpisodeModelMapper? _instance;
static EpisodeModelMapper ensureInitialized() {
if (_instance == null) {
MapperContainer.globals.use(_instance = EpisodeModelMapper._());
ItemStreamModelMapper.ensureInitialized().addSubMapper(_instance!);
OverviewModelMapper.ensureInitialized();
UserDataMapper.ensureInitialized();
}
return _instance!;
}
@override
final String id = 'EpisodeModel';
static String? _$seriesName(EpisodeModel v) => v.seriesName;
static const Field<EpisodeModel, String> _f$seriesName =
Field('seriesName', _$seriesName);
static int _$season(EpisodeModel v) => v.season;
static const Field<EpisodeModel, int> _f$season = Field('season', _$season);
static int _$episode(EpisodeModel v) => v.episode;
static const Field<EpisodeModel, int> _f$episode =
Field('episode', _$episode);
static List<Chapter> _$chapters(EpisodeModel v) => v.chapters;
static const Field<EpisodeModel, List<Chapter>> _f$chapters =
Field('chapters', _$chapters, opt: true, def: const []);
static ItemLocation? _$location(EpisodeModel v) => v.location;
static const Field<EpisodeModel, ItemLocation> _f$location =
Field('location', _$location, opt: true);
static DateTime? _$dateAired(EpisodeModel v) => v.dateAired;
static const Field<EpisodeModel, DateTime> _f$dateAired =
Field('dateAired', _$dateAired, opt: true);
static String _$name(EpisodeModel v) => v.name;
static const Field<EpisodeModel, String> _f$name = Field('name', _$name);
static String _$id(EpisodeModel v) => v.id;
static const Field<EpisodeModel, String> _f$id = Field('id', _$id);
static OverviewModel _$overview(EpisodeModel v) => v.overview;
static const Field<EpisodeModel, OverviewModel> _f$overview =
Field('overview', _$overview);
static String? _$parentId(EpisodeModel v) => v.parentId;
static const Field<EpisodeModel, String> _f$parentId =
Field('parentId', _$parentId);
static String? _$playlistId(EpisodeModel v) => v.playlistId;
static const Field<EpisodeModel, String> _f$playlistId =
Field('playlistId', _$playlistId);
static ImagesData? _$images(EpisodeModel v) => v.images;
static const Field<EpisodeModel, ImagesData> _f$images =
Field('images', _$images);
static int? _$childCount(EpisodeModel v) => v.childCount;
static const Field<EpisodeModel, int> _f$childCount =
Field('childCount', _$childCount);
static double? _$primaryRatio(EpisodeModel v) => v.primaryRatio;
static const Field<EpisodeModel, double> _f$primaryRatio =
Field('primaryRatio', _$primaryRatio);
static UserData _$userData(EpisodeModel v) => v.userData;
static const Field<EpisodeModel, UserData> _f$userData =
Field('userData', _$userData);
static ImagesData? _$parentImages(EpisodeModel v) => v.parentImages;
static const Field<EpisodeModel, ImagesData> _f$parentImages =
Field('parentImages', _$parentImages);
static MediaStreamsModel _$mediaStreams(EpisodeModel v) => v.mediaStreams;
static const Field<EpisodeModel, MediaStreamsModel> _f$mediaStreams =
Field('mediaStreams', _$mediaStreams);
static bool? _$canDelete(EpisodeModel v) => v.canDelete;
static const Field<EpisodeModel, bool> _f$canDelete =
Field('canDelete', _$canDelete, opt: true);
static bool? _$canDownload(EpisodeModel v) => v.canDownload;
static const Field<EpisodeModel, bool> _f$canDownload =
Field('canDownload', _$canDownload, opt: true);
static dto.BaseItemKind? _$jellyType(EpisodeModel v) => v.jellyType;
static const Field<EpisodeModel, dto.BaseItemKind> _f$jellyType =
Field('jellyType', _$jellyType, opt: true);
@override
final MappableFields<EpisodeModel> fields = const {
#seriesName: _f$seriesName,
#season: _f$season,
#episode: _f$episode,
#chapters: _f$chapters,
#location: _f$location,
#dateAired: _f$dateAired,
#name: _f$name,
#id: _f$id,
#overview: _f$overview,
#parentId: _f$parentId,
#playlistId: _f$playlistId,
#images: _f$images,
#childCount: _f$childCount,
#primaryRatio: _f$primaryRatio,
#userData: _f$userData,
#parentImages: _f$parentImages,
#mediaStreams: _f$mediaStreams,
#canDelete: _f$canDelete,
#canDownload: _f$canDownload,
#jellyType: _f$jellyType,
};
@override
final bool ignoreNull = true;
@override
final String discriminatorKey = 'type';
@override
final dynamic discriminatorValue = 'EpisodeModel';
@override
late final ClassMapperBase superMapper =
ItemStreamModelMapper.ensureInitialized();
static EpisodeModel _instantiate(DecodingData data) {
return EpisodeModel(
seriesName: data.dec(_f$seriesName),
season: data.dec(_f$season),
episode: data.dec(_f$episode),
chapters: data.dec(_f$chapters),
location: data.dec(_f$location),
dateAired: data.dec(_f$dateAired),
name: data.dec(_f$name),
id: data.dec(_f$id),
overview: data.dec(_f$overview),
parentId: data.dec(_f$parentId),
playlistId: data.dec(_f$playlistId),
images: data.dec(_f$images),
childCount: data.dec(_f$childCount),
primaryRatio: data.dec(_f$primaryRatio),
userData: data.dec(_f$userData),
parentImages: data.dec(_f$parentImages),
mediaStreams: data.dec(_f$mediaStreams),
canDelete: data.dec(_f$canDelete),
canDownload: data.dec(_f$canDownload),
jellyType: data.dec(_f$jellyType));
}
@override
final Function instantiate = _instantiate;
static EpisodeModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<EpisodeModel>(map);
}
static EpisodeModel fromJson(String json) {
return ensureInitialized().decodeJson<EpisodeModel>(json);
}
}
mixin EpisodeModelMappable {
String toJson() {
return EpisodeModelMapper.ensureInitialized()
.encodeJson<EpisodeModel>(this as EpisodeModel);
}
Map<String, dynamic> toMap() {
return EpisodeModelMapper.ensureInitialized()
.encodeMap<EpisodeModel>(this as EpisodeModel);
}
EpisodeModelCopyWith<EpisodeModel, EpisodeModel, EpisodeModel> get copyWith =>
_EpisodeModelCopyWithImpl(this as EpisodeModel, $identity, $identity);
@override
String toString() {
return EpisodeModelMapper.ensureInitialized()
.stringifyValue(this as EpisodeModel);
}
}
extension EpisodeModelValueCopy<$R, $Out>
on ObjectCopyWith<$R, EpisodeModel, $Out> {
EpisodeModelCopyWith<$R, EpisodeModel, $Out> get $asEpisodeModel =>
$base.as((v, t, t2) => _EpisodeModelCopyWithImpl(v, t, t2));
}
abstract class EpisodeModelCopyWith<$R, $In extends EpisodeModel, $Out>
implements ItemStreamModelCopyWith<$R, $In, $Out> {
ListCopyWith<$R, Chapter, ObjectCopyWith<$R, Chapter, Chapter>> get chapters;
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview;
@override
UserDataCopyWith<$R, UserData, UserData> get userData;
@override
$R call(
{String? seriesName,
int? season,
int? episode,
List<Chapter>? chapters,
ItemLocation? location,
DateTime? dateAired,
String? name,
String? id,
OverviewModel? overview,
String? parentId,
String? playlistId,
ImagesData? images,
int? childCount,
double? primaryRatio,
UserData? userData,
ImagesData? parentImages,
MediaStreamsModel? mediaStreams,
bool? canDelete,
bool? canDownload,
dto.BaseItemKind? jellyType});
EpisodeModelCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t);
}
class _EpisodeModelCopyWithImpl<$R, $Out>
extends ClassCopyWithBase<$R, EpisodeModel, $Out>
implements EpisodeModelCopyWith<$R, EpisodeModel, $Out> {
_EpisodeModelCopyWithImpl(super.value, super.then, super.then2);
@override
late final ClassMapperBase<EpisodeModel> $mapper =
EpisodeModelMapper.ensureInitialized();
@override
ListCopyWith<$R, Chapter, ObjectCopyWith<$R, Chapter, Chapter>>
get chapters => ListCopyWith($value.chapters,
(v, t) => ObjectCopyWith(v, $identity, t), (v) => call(chapters: v));
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview =>
$value.overview.copyWith.$chain((v) => call(overview: v));
@override
UserDataCopyWith<$R, UserData, UserData> get userData =>
$value.userData.copyWith.$chain((v) => call(userData: v));
@override
$R call(
{Object? seriesName = $none,
int? season,
int? episode,
List<Chapter>? chapters,
Object? location = $none,
Object? dateAired = $none,
String? name,
String? id,
OverviewModel? overview,
Object? parentId = $none,
Object? playlistId = $none,
Object? images = $none,
Object? childCount = $none,
Object? primaryRatio = $none,
UserData? userData,
Object? parentImages = $none,
MediaStreamsModel? mediaStreams,
Object? canDelete = $none,
Object? canDownload = $none,
Object? jellyType = $none}) =>
$apply(FieldCopyWithData({
if (seriesName != $none) #seriesName: seriesName,
if (season != null) #season: season,
if (episode != null) #episode: episode,
if (chapters != null) #chapters: chapters,
if (location != $none) #location: location,
if (dateAired != $none) #dateAired: dateAired,
if (name != null) #name: name,
if (id != null) #id: id,
if (overview != null) #overview: overview,
if (parentId != $none) #parentId: parentId,
if (playlistId != $none) #playlistId: playlistId,
if (images != $none) #images: images,
if (childCount != $none) #childCount: childCount,
if (primaryRatio != $none) #primaryRatio: primaryRatio,
if (userData != null) #userData: userData,
if (parentImages != $none) #parentImages: parentImages,
if (mediaStreams != null) #mediaStreams: mediaStreams,
if (canDelete != $none) #canDelete: canDelete,
if (canDownload != $none) #canDownload: canDownload,
if (jellyType != $none) #jellyType: jellyType
}));
@override
EpisodeModel $make(CopyWithData data) => EpisodeModel(
seriesName: data.get(#seriesName, or: $value.seriesName),
season: data.get(#season, or: $value.season),
episode: data.get(#episode, or: $value.episode),
chapters: data.get(#chapters, or: $value.chapters),
location: data.get(#location, or: $value.location),
dateAired: data.get(#dateAired, or: $value.dateAired),
name: data.get(#name, or: $value.name),
id: data.get(#id, or: $value.id),
overview: data.get(#overview, or: $value.overview),
parentId: data.get(#parentId, or: $value.parentId),
playlistId: data.get(#playlistId, or: $value.playlistId),
images: data.get(#images, or: $value.images),
childCount: data.get(#childCount, or: $value.childCount),
primaryRatio: data.get(#primaryRatio, or: $value.primaryRatio),
userData: data.get(#userData, or: $value.userData),
parentImages: data.get(#parentImages, or: $value.parentImages),
mediaStreams: data.get(#mediaStreams, or: $value.mediaStreams),
canDelete: data.get(#canDelete, or: $value.canDelete),
canDownload: data.get(#canDownload, or: $value.canDownload),
jellyType: data.get(#jellyType, or: $value.jellyType));
@override
EpisodeModelCopyWith<$R2, EpisodeModel, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t) =>
_EpisodeModelCopyWithImpl($value, $cast, t);
}

View file

@ -0,0 +1,50 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/overview_model.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'folder_model.mapper.dart';
@MappableClass()
class FolderModel extends ItemBaseModel with FolderModelMappable {
final List<ItemBaseModel> items;
const FolderModel({
required this.items,
required super.overview,
required super.parentId,
required super.playlistId,
required super.images,
required super.childCount,
required super.primaryRatio,
required super.userData,
required super.name,
required super.id,
super.canDownload,
super.canDelete,
super.jellyType,
});
factory FolderModel.fromBaseDto(BaseItemDto item, Ref ref) {
return FolderModel(
name: item.name ?? "",
id: item.id ?? "",
childCount: item.childCount,
overview: OverviewModel.fromBaseItemDto(item, ref),
userData: UserData.fromDto(item.userData),
parentId: item.parentId,
playlistId: item.playlistItemId,
images: ImagesData.fromBaseItem(item, ref),
primaryRatio: item.primaryImageAspectRatio,
items: [],
canDelete: item.canDelete,
canDownload: item.canDownload,
);
}
}

View file

@ -0,0 +1,242 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
part of 'folder_model.dart';
class FolderModelMapper extends SubClassMapperBase<FolderModel> {
FolderModelMapper._();
static FolderModelMapper? _instance;
static FolderModelMapper ensureInitialized() {
if (_instance == null) {
MapperContainer.globals.use(_instance = FolderModelMapper._());
ItemBaseModelMapper.ensureInitialized().addSubMapper(_instance!);
ItemBaseModelMapper.ensureInitialized();
OverviewModelMapper.ensureInitialized();
UserDataMapper.ensureInitialized();
}
return _instance!;
}
@override
final String id = 'FolderModel';
static List<ItemBaseModel> _$items(FolderModel v) => v.items;
static const Field<FolderModel, List<ItemBaseModel>> _f$items =
Field('items', _$items);
static OverviewModel _$overview(FolderModel v) => v.overview;
static const Field<FolderModel, OverviewModel> _f$overview =
Field('overview', _$overview);
static String? _$parentId(FolderModel v) => v.parentId;
static const Field<FolderModel, String> _f$parentId =
Field('parentId', _$parentId);
static String? _$playlistId(FolderModel v) => v.playlistId;
static const Field<FolderModel, String> _f$playlistId =
Field('playlistId', _$playlistId);
static ImagesData? _$images(FolderModel v) => v.images;
static const Field<FolderModel, ImagesData> _f$images =
Field('images', _$images);
static int? _$childCount(FolderModel v) => v.childCount;
static const Field<FolderModel, int> _f$childCount =
Field('childCount', _$childCount);
static double? _$primaryRatio(FolderModel v) => v.primaryRatio;
static const Field<FolderModel, double> _f$primaryRatio =
Field('primaryRatio', _$primaryRatio);
static UserData _$userData(FolderModel v) => v.userData;
static const Field<FolderModel, UserData> _f$userData =
Field('userData', _$userData);
static String _$name(FolderModel v) => v.name;
static const Field<FolderModel, String> _f$name = Field('name', _$name);
static String _$id(FolderModel v) => v.id;
static const Field<FolderModel, String> _f$id = Field('id', _$id);
static bool? _$canDownload(FolderModel v) => v.canDownload;
static const Field<FolderModel, bool> _f$canDownload =
Field('canDownload', _$canDownload, opt: true);
static bool? _$canDelete(FolderModel v) => v.canDelete;
static const Field<FolderModel, bool> _f$canDelete =
Field('canDelete', _$canDelete, opt: true);
static BaseItemKind? _$jellyType(FolderModel v) => v.jellyType;
static const Field<FolderModel, BaseItemKind> _f$jellyType =
Field('jellyType', _$jellyType, opt: true);
@override
final MappableFields<FolderModel> fields = const {
#items: _f$items,
#overview: _f$overview,
#parentId: _f$parentId,
#playlistId: _f$playlistId,
#images: _f$images,
#childCount: _f$childCount,
#primaryRatio: _f$primaryRatio,
#userData: _f$userData,
#name: _f$name,
#id: _f$id,
#canDownload: _f$canDownload,
#canDelete: _f$canDelete,
#jellyType: _f$jellyType,
};
@override
final bool ignoreNull = true;
@override
final String discriminatorKey = 'type';
@override
final dynamic discriminatorValue = 'FolderModel';
@override
late final ClassMapperBase superMapper =
ItemBaseModelMapper.ensureInitialized();
static FolderModel _instantiate(DecodingData data) {
return FolderModel(
items: data.dec(_f$items),
overview: data.dec(_f$overview),
parentId: data.dec(_f$parentId),
playlistId: data.dec(_f$playlistId),
images: data.dec(_f$images),
childCount: data.dec(_f$childCount),
primaryRatio: data.dec(_f$primaryRatio),
userData: data.dec(_f$userData),
name: data.dec(_f$name),
id: data.dec(_f$id),
canDownload: data.dec(_f$canDownload),
canDelete: data.dec(_f$canDelete),
jellyType: data.dec(_f$jellyType));
}
@override
final Function instantiate = _instantiate;
static FolderModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<FolderModel>(map);
}
static FolderModel fromJson(String json) {
return ensureInitialized().decodeJson<FolderModel>(json);
}
}
mixin FolderModelMappable {
String toJson() {
return FolderModelMapper.ensureInitialized()
.encodeJson<FolderModel>(this as FolderModel);
}
Map<String, dynamic> toMap() {
return FolderModelMapper.ensureInitialized()
.encodeMap<FolderModel>(this as FolderModel);
}
FolderModelCopyWith<FolderModel, FolderModel, FolderModel> get copyWith =>
_FolderModelCopyWithImpl(this as FolderModel, $identity, $identity);
@override
String toString() {
return FolderModelMapper.ensureInitialized()
.stringifyValue(this as FolderModel);
}
}
extension FolderModelValueCopy<$R, $Out>
on ObjectCopyWith<$R, FolderModel, $Out> {
FolderModelCopyWith<$R, FolderModel, $Out> get $asFolderModel =>
$base.as((v, t, t2) => _FolderModelCopyWithImpl(v, t, t2));
}
abstract class FolderModelCopyWith<$R, $In extends FolderModel, $Out>
implements ItemBaseModelCopyWith<$R, $In, $Out> {
ListCopyWith<$R, ItemBaseModel,
ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>> get items;
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview;
@override
UserDataCopyWith<$R, UserData, UserData> get userData;
@override
$R call(
{List<ItemBaseModel>? items,
OverviewModel? overview,
String? parentId,
String? playlistId,
ImagesData? images,
int? childCount,
double? primaryRatio,
UserData? userData,
String? name,
String? id,
bool? canDownload,
bool? canDelete,
BaseItemKind? jellyType});
FolderModelCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t);
}
class _FolderModelCopyWithImpl<$R, $Out>
extends ClassCopyWithBase<$R, FolderModel, $Out>
implements FolderModelCopyWith<$R, FolderModel, $Out> {
_FolderModelCopyWithImpl(super.value, super.then, super.then2);
@override
late final ClassMapperBase<FolderModel> $mapper =
FolderModelMapper.ensureInitialized();
@override
ListCopyWith<$R, ItemBaseModel,
ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>>
get items => ListCopyWith(
$value.items, (v, t) => v.copyWith.$chain(t), (v) => call(items: v));
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview =>
$value.overview.copyWith.$chain((v) => call(overview: v));
@override
UserDataCopyWith<$R, UserData, UserData> get userData =>
$value.userData.copyWith.$chain((v) => call(userData: v));
@override
$R call(
{List<ItemBaseModel>? items,
OverviewModel? overview,
Object? parentId = $none,
Object? playlistId = $none,
Object? images = $none,
Object? childCount = $none,
Object? primaryRatio = $none,
UserData? userData,
String? name,
String? id,
Object? canDownload = $none,
Object? canDelete = $none,
Object? jellyType = $none}) =>
$apply(FieldCopyWithData({
if (items != null) #items: items,
if (overview != null) #overview: overview,
if (parentId != $none) #parentId: parentId,
if (playlistId != $none) #playlistId: playlistId,
if (images != $none) #images: images,
if (childCount != $none) #childCount: childCount,
if (primaryRatio != $none) #primaryRatio: primaryRatio,
if (userData != null) #userData: userData,
if (name != null) #name: name,
if (id != null) #id: id,
if (canDownload != $none) #canDownload: canDownload,
if (canDelete != $none) #canDelete: canDelete,
if (jellyType != $none) #jellyType: jellyType
}));
@override
FolderModel $make(CopyWithData data) => FolderModel(
items: data.get(#items, or: $value.items),
overview: data.get(#overview, or: $value.overview),
parentId: data.get(#parentId, or: $value.parentId),
playlistId: data.get(#playlistId, or: $value.playlistId),
images: data.get(#images, or: $value.images),
childCount: data.get(#childCount, or: $value.childCount),
primaryRatio: data.get(#primaryRatio, or: $value.primaryRatio),
userData: data.get(#userData, or: $value.userData),
name: data.get(#name, or: $value.name),
id: data.get(#id, or: $value.id),
canDownload: data.get(#canDownload, or: $value.canDownload),
canDelete: data.get(#canDelete, or: $value.canDelete),
jellyType: data.get(#jellyType, or: $value.jellyType));
@override
FolderModelCopyWith<$R2, FolderModel, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t) =>
_FolderModelCopyWithImpl($value, $cast, t);
}

View file

@ -0,0 +1,286 @@
import 'dart:convert';
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart';
import 'package:fladder/main.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart' as enums;
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
import 'package:fladder/providers/image_provider.dart';
import 'package:fladder/util/jelly_id.dart';
class ImagesData {
final ImageData? primary;
final List<ImageData>? backDrop;
final ImageData? logo;
ImagesData({
this.primary,
this.backDrop,
this.logo,
});
bool get isEmpty {
if (primary == null && backDrop == null) return true;
return false;
}
ImageData? get firstOrNull {
return primary ?? backDrop?[0];
}
ImageData? get randomBackDrop => (backDrop?..shuffle())?.firstOrNull ?? primary;
factory ImagesData.fromBaseItem(
dto.BaseItemDto item,
Ref ref, {
Size backDrop = const Size(2000, 2000),
Size logo = const Size(1000, 1000),
Size primary = const Size(600, 600),
bool getOriginalSize = false,
int quality = 90,
}) {
final newImgesData = ImagesData(
primary: item.imageTags?['Primary'] != null
? ImageData(
path: getOriginalSize
? ref.read(imageUtilityProvider).getItemsOrigImageUrl(
item.id!,
type: enums.ImageType.primary,
)
: ref.read(imageUtilityProvider).getItemsImageUrl(
(item.id!),
type: enums.ImageType.primary,
maxHeight: primary.height.toInt(),
maxWidth: primary.width.toInt(),
quality: quality,
),
key: item.imageTags?['Primary'],
hash: item.imageBlurHashes?.primary?[item.imageTags?['Primary']] as String? ?? "",
)
: null,
logo: item.imageTags?['Logo'] != null
? ImageData(
path: getOriginalSize
? ref.read(imageUtilityProvider).getItemsOrigImageUrl(
item.id!,
type: enums.ImageType.logo,
)
: ref.read(imageUtilityProvider).getItemsImageUrl(
(item.id!),
type: enums.ImageType.logo,
maxHeight: logo.height.toInt(),
maxWidth: logo.width.toInt(),
quality: quality,
),
key: item.imageTags?['Logo'],
hash: item.imageBlurHashes?.logo?[item.imageTags?['Logo']] as String? ?? "")
: null,
backDrop: (item.backdropImageTags ?? []).mapIndexed(
(index, backdrop) {
final image = ImageData(
path: getOriginalSize
? ref.read(imageUtilityProvider).getBackdropOrigImage(
item.id!,
index,
backdrop,
)
: ref.read(imageUtilityProvider).getBackdropImage(
(item.id!),
index,
backdrop,
maxHeight: backDrop.height.toInt(),
maxWidth: backDrop.width.toInt(),
quality: quality,
),
key: backdrop,
hash: item.imageBlurHashes?.backdrop?[backdrop] ?? jellyId,
);
return image;
},
).toList(),
);
return newImgesData;
}
static ImagesData? fromBaseItemParent(
dto.BaseItemDto item,
Ref ref, {
Size backDrop = const Size(2000, 2000),
Size logo = const Size(1000, 1000),
Size primary = const Size(600, 600),
int quality = 90,
}) {
if (item.seriesId == null && item.parentId == null) return null;
final newImgesData = ImagesData(
primary: (item.seriesPrimaryImageTag != null)
? ImageData(
path: ref.read(imageUtilityProvider).getItemsImageUrl(
(item.seriesId!),
type: enums.ImageType.primary,
maxHeight: primary.height.toInt(),
maxWidth: primary.width.toInt(),
quality: quality,
),
key: item.seriesPrimaryImageTag ?? "",
hash: item.imageBlurHashes?.primary?[item.seriesPrimaryImageTag] as String? ?? "")
: null,
logo: (item.parentLogoImageTag != null)
? ImageData(
path: ref.read(imageUtilityProvider).getItemsImageUrl(
(item.seriesId!),
type: enums.ImageType.logo,
maxHeight: logo.height.toInt(),
maxWidth: logo.width.toInt(),
quality: quality,
),
key: item.parentLogoImageTag ?? "",
hash: item.imageBlurHashes?.logo?[item.parentLogoImageTag] as String? ?? "")
: null,
backDrop: (item.backdropImageTags ?? []).mapIndexed(
(index, backdrop) {
final image = ImageData(
path: ref.read(imageUtilityProvider).getBackdropImage(
((item.seriesId ?? item.parentId)!),
index,
backdrop,
maxHeight: backDrop.height.toInt(),
maxWidth: backDrop.width.toInt(),
quality: quality,
),
key: backdrop,
hash: item.imageBlurHashes?.backdrop?[backdrop],
);
return image;
},
).toList(),
);
return newImgesData;
}
static ImagesData? fromPersonDto(
dto.BaseItemPerson item,
Ref ref, {
Size backDrop = const Size(2000, 2000),
Size logo = const Size(1000, 1000),
Size primary = const Size(2000, 2000),
int quality = 90,
}) {
return ImagesData(
primary: (item.primaryImageTag != null && item.imageBlurHashes != null)
? ImageData(
path: ref.read(imageUtilityProvider).getItemsImageUrl(
item.id ?? "",
type: enums.ImageType.primary,
maxHeight: primary.height.toInt(),
maxWidth: primary.width.toInt(),
quality: quality,
),
key: item.primaryImageTag ?? "",
hash: item.imageBlurHashes?.primary?[item.primaryImageTag] as String? ?? jellyId)
: null,
logo: null,
backDrop: null,
);
}
@override
String toString() => 'ImagesData(primary: $primary, backDrop: $backDrop, logo: $logo)';
ImagesData copyWith({
ValueGetter<ImageData?>? primary,
ValueGetter<List<ImageData>?>? backDrop,
ValueGetter<ImageData?>? logo,
}) {
return ImagesData(
primary: primary != null ? primary() : this.primary,
backDrop: backDrop != null ? backDrop() : this.backDrop,
logo: logo != null ? logo() : this.logo,
);
}
Map<String, dynamic> toMap() {
return {
'primary': primary?.toMap(),
'backDrop': backDrop?.map((x) => x.toMap()).toList(),
'logo': logo?.toMap(),
};
}
factory ImagesData.fromMap(Map<String, dynamic> map) {
return ImagesData(
primary: map['primary'] != null ? ImageData.fromMap(map['primary']) : null,
backDrop:
map['backDrop'] != null ? List<ImageData>.from(map['backDrop']?.map((x) => ImageData.fromMap(x))) : null,
logo: map['logo'] != null ? ImageData.fromMap(map['logo']) : null,
);
}
String toJson() => json.encode(toMap());
factory ImagesData.fromJson(String source) => ImagesData.fromMap(json.decode(source));
}
class ImageData {
final String path;
final String hash;
final String key;
ImageData({
this.path = '',
this.hash = '',
this.key = '',
});
ImageProvider get imageProvider {
if (path.startsWith("http")) {
return CachedNetworkImageProvider(
cacheKey: key,
cacheManager: CustomCacheManager.instance,
path,
);
} else {
return Image.file(
key: Key(key),
File(path),
).image;
}
}
@override
String toString() => 'ImageData(path: $path, hash: $hash, key: $key)';
ImageData copyWith({
String? path,
String? hash,
String? key,
}) {
return ImageData(
path: path ?? this.path,
hash: hash ?? this.hash,
key: key ?? this.key,
);
}
Map<String, dynamic> toMap() {
return {
'path': path,
'hash': hash,
'key': key,
};
}
factory ImageData.fromMap(Map<String, dynamic> map) {
return ImageData(
path: map['path'] ?? '',
hash: map['hash'] ?? '',
key: map['key'] ?? '',
);
}
String toJson() => json.encode(toMap());
factory ImageData.fromJson(String source) => ImageData.fromMap(json.decode(source));
}

View file

@ -0,0 +1,47 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first, invalid_annotation_target
import 'package:freezed_annotation/freezed_annotation.dart';
part 'intro_skip_model.freezed.dart';
part 'intro_skip_model.g.dart';
@freezed
class IntroOutSkipModel with _$IntroOutSkipModel {
const IntroOutSkipModel._();
factory IntroOutSkipModel({
IntroSkipModel? intro,
IntroSkipModel? credits,
}) = _IntroOutSkipModel;
factory IntroOutSkipModel.fromJson(Map<String, dynamic> json) => _$IntroOutSkipModelFromJson(json);
bool introInRange(Duration position) {
if (intro == null) return false;
return (position.compareTo(intro!.showTime) >= 0 && position.compareTo(intro!.hideTime) <= 0);
}
bool creditsInRange(Duration position) {
if (credits == null) return false;
return (position.compareTo(credits!.showTime) >= 0 && position.compareTo(credits!.hideTime) <= 0);
}
}
@freezed
class IntroSkipModel with _$IntroSkipModel {
factory IntroSkipModel({
@JsonKey(name: "EpisodeId") required String id,
@JsonKey(name: "Valid") required bool valid,
@JsonKey(name: "IntroStart", fromJson: _durationFromMilliseconds, toJson: _durationToMilliseconds)
required Duration start,
@JsonKey(name: "IntroEnd", fromJson: _durationFromMilliseconds, toJson: _durationToMilliseconds)
required Duration end,
@JsonKey(name: "ShowSkipPromptAt", fromJson: _durationFromMilliseconds, toJson: _durationToMilliseconds)
required Duration showTime,
@JsonKey(name: "HideSkipPromptAt", fromJson: _durationFromMilliseconds, toJson: _durationToMilliseconds)
required Duration hideTime,
}) = _IntroSkipModel;
factory IntroSkipModel.fromJson(Map<String, dynamic> json) => _$IntroSkipModelFromJson(json);
}
Duration _durationFromMilliseconds(num milliseconds) => Duration(milliseconds: (milliseconds * 1000).toInt());
num _durationToMilliseconds(Duration duration) => duration.inMilliseconds.toDouble() / 1000.0;

View file

@ -0,0 +1,565 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'intro_skip_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
IntroOutSkipModel _$IntroOutSkipModelFromJson(Map<String, dynamic> json) {
return _IntroOutSkipModel.fromJson(json);
}
/// @nodoc
mixin _$IntroOutSkipModel {
IntroSkipModel? get intro => throw _privateConstructorUsedError;
IntroSkipModel? get credits => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$IntroOutSkipModelCopyWith<IntroOutSkipModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $IntroOutSkipModelCopyWith<$Res> {
factory $IntroOutSkipModelCopyWith(
IntroOutSkipModel value, $Res Function(IntroOutSkipModel) then) =
_$IntroOutSkipModelCopyWithImpl<$Res, IntroOutSkipModel>;
@useResult
$Res call({IntroSkipModel? intro, IntroSkipModel? credits});
$IntroSkipModelCopyWith<$Res>? get intro;
$IntroSkipModelCopyWith<$Res>? get credits;
}
/// @nodoc
class _$IntroOutSkipModelCopyWithImpl<$Res, $Val extends IntroOutSkipModel>
implements $IntroOutSkipModelCopyWith<$Res> {
_$IntroOutSkipModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? intro = freezed,
Object? credits = freezed,
}) {
return _then(_value.copyWith(
intro: freezed == intro
? _value.intro
: intro // ignore: cast_nullable_to_non_nullable
as IntroSkipModel?,
credits: freezed == credits
? _value.credits
: credits // ignore: cast_nullable_to_non_nullable
as IntroSkipModel?,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$IntroSkipModelCopyWith<$Res>? get intro {
if (_value.intro == null) {
return null;
}
return $IntroSkipModelCopyWith<$Res>(_value.intro!, (value) {
return _then(_value.copyWith(intro: value) as $Val);
});
}
@override
@pragma('vm:prefer-inline')
$IntroSkipModelCopyWith<$Res>? get credits {
if (_value.credits == null) {
return null;
}
return $IntroSkipModelCopyWith<$Res>(_value.credits!, (value) {
return _then(_value.copyWith(credits: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$IntroOutSkipModelImplCopyWith<$Res>
implements $IntroOutSkipModelCopyWith<$Res> {
factory _$$IntroOutSkipModelImplCopyWith(_$IntroOutSkipModelImpl value,
$Res Function(_$IntroOutSkipModelImpl) then) =
__$$IntroOutSkipModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({IntroSkipModel? intro, IntroSkipModel? credits});
@override
$IntroSkipModelCopyWith<$Res>? get intro;
@override
$IntroSkipModelCopyWith<$Res>? get credits;
}
/// @nodoc
class __$$IntroOutSkipModelImplCopyWithImpl<$Res>
extends _$IntroOutSkipModelCopyWithImpl<$Res, _$IntroOutSkipModelImpl>
implements _$$IntroOutSkipModelImplCopyWith<$Res> {
__$$IntroOutSkipModelImplCopyWithImpl(_$IntroOutSkipModelImpl _value,
$Res Function(_$IntroOutSkipModelImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? intro = freezed,
Object? credits = freezed,
}) {
return _then(_$IntroOutSkipModelImpl(
intro: freezed == intro
? _value.intro
: intro // ignore: cast_nullable_to_non_nullable
as IntroSkipModel?,
credits: freezed == credits
? _value.credits
: credits // ignore: cast_nullable_to_non_nullable
as IntroSkipModel?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$IntroOutSkipModelImpl extends _IntroOutSkipModel {
_$IntroOutSkipModelImpl({this.intro, this.credits}) : super._();
factory _$IntroOutSkipModelImpl.fromJson(Map<String, dynamic> json) =>
_$$IntroOutSkipModelImplFromJson(json);
@override
final IntroSkipModel? intro;
@override
final IntroSkipModel? credits;
@override
String toString() {
return 'IntroOutSkipModel(intro: $intro, credits: $credits)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$IntroOutSkipModelImpl &&
(identical(other.intro, intro) || other.intro == intro) &&
(identical(other.credits, credits) || other.credits == credits));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, intro, credits);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$IntroOutSkipModelImplCopyWith<_$IntroOutSkipModelImpl> get copyWith =>
__$$IntroOutSkipModelImplCopyWithImpl<_$IntroOutSkipModelImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$IntroOutSkipModelImplToJson(
this,
);
}
}
abstract class _IntroOutSkipModel extends IntroOutSkipModel {
factory _IntroOutSkipModel(
{final IntroSkipModel? intro,
final IntroSkipModel? credits}) = _$IntroOutSkipModelImpl;
_IntroOutSkipModel._() : super._();
factory _IntroOutSkipModel.fromJson(Map<String, dynamic> json) =
_$IntroOutSkipModelImpl.fromJson;
@override
IntroSkipModel? get intro;
@override
IntroSkipModel? get credits;
@override
@JsonKey(ignore: true)
_$$IntroOutSkipModelImplCopyWith<_$IntroOutSkipModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}
IntroSkipModel _$IntroSkipModelFromJson(Map<String, dynamic> json) {
return _IntroSkipModel.fromJson(json);
}
/// @nodoc
mixin _$IntroSkipModel {
@JsonKey(name: "EpisodeId")
String get id => throw _privateConstructorUsedError;
@JsonKey(name: "Valid")
bool get valid => throw _privateConstructorUsedError;
@JsonKey(
name: "IntroStart",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
Duration get start => throw _privateConstructorUsedError;
@JsonKey(
name: "IntroEnd",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
Duration get end => throw _privateConstructorUsedError;
@JsonKey(
name: "ShowSkipPromptAt",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
Duration get showTime => throw _privateConstructorUsedError;
@JsonKey(
name: "HideSkipPromptAt",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
Duration get hideTime => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$IntroSkipModelCopyWith<IntroSkipModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $IntroSkipModelCopyWith<$Res> {
factory $IntroSkipModelCopyWith(
IntroSkipModel value, $Res Function(IntroSkipModel) then) =
_$IntroSkipModelCopyWithImpl<$Res, IntroSkipModel>;
@useResult
$Res call(
{@JsonKey(name: "EpisodeId") String id,
@JsonKey(name: "Valid") bool valid,
@JsonKey(
name: "IntroStart",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
Duration start,
@JsonKey(
name: "IntroEnd",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
Duration end,
@JsonKey(
name: "ShowSkipPromptAt",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
Duration showTime,
@JsonKey(
name: "HideSkipPromptAt",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
Duration hideTime});
}
/// @nodoc
class _$IntroSkipModelCopyWithImpl<$Res, $Val extends IntroSkipModel>
implements $IntroSkipModelCopyWith<$Res> {
_$IntroSkipModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? valid = null,
Object? start = null,
Object? end = null,
Object? showTime = null,
Object? hideTime = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
valid: null == valid
? _value.valid
: valid // ignore: cast_nullable_to_non_nullable
as bool,
start: null == start
? _value.start
: start // ignore: cast_nullable_to_non_nullable
as Duration,
end: null == end
? _value.end
: end // ignore: cast_nullable_to_non_nullable
as Duration,
showTime: null == showTime
? _value.showTime
: showTime // ignore: cast_nullable_to_non_nullable
as Duration,
hideTime: null == hideTime
? _value.hideTime
: hideTime // ignore: cast_nullable_to_non_nullable
as Duration,
) as $Val);
}
}
/// @nodoc
abstract class _$$IntroSkipModelImplCopyWith<$Res>
implements $IntroSkipModelCopyWith<$Res> {
factory _$$IntroSkipModelImplCopyWith(_$IntroSkipModelImpl value,
$Res Function(_$IntroSkipModelImpl) then) =
__$$IntroSkipModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{@JsonKey(name: "EpisodeId") String id,
@JsonKey(name: "Valid") bool valid,
@JsonKey(
name: "IntroStart",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
Duration start,
@JsonKey(
name: "IntroEnd",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
Duration end,
@JsonKey(
name: "ShowSkipPromptAt",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
Duration showTime,
@JsonKey(
name: "HideSkipPromptAt",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
Duration hideTime});
}
/// @nodoc
class __$$IntroSkipModelImplCopyWithImpl<$Res>
extends _$IntroSkipModelCopyWithImpl<$Res, _$IntroSkipModelImpl>
implements _$$IntroSkipModelImplCopyWith<$Res> {
__$$IntroSkipModelImplCopyWithImpl(
_$IntroSkipModelImpl _value, $Res Function(_$IntroSkipModelImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? valid = null,
Object? start = null,
Object? end = null,
Object? showTime = null,
Object? hideTime = null,
}) {
return _then(_$IntroSkipModelImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
valid: null == valid
? _value.valid
: valid // ignore: cast_nullable_to_non_nullable
as bool,
start: null == start
? _value.start
: start // ignore: cast_nullable_to_non_nullable
as Duration,
end: null == end
? _value.end
: end // ignore: cast_nullable_to_non_nullable
as Duration,
showTime: null == showTime
? _value.showTime
: showTime // ignore: cast_nullable_to_non_nullable
as Duration,
hideTime: null == hideTime
? _value.hideTime
: hideTime // ignore: cast_nullable_to_non_nullable
as Duration,
));
}
}
/// @nodoc
@JsonSerializable()
class _$IntroSkipModelImpl implements _IntroSkipModel {
_$IntroSkipModelImpl(
{@JsonKey(name: "EpisodeId") required this.id,
@JsonKey(name: "Valid") required this.valid,
@JsonKey(
name: "IntroStart",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
required this.start,
@JsonKey(
name: "IntroEnd",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
required this.end,
@JsonKey(
name: "ShowSkipPromptAt",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
required this.showTime,
@JsonKey(
name: "HideSkipPromptAt",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
required this.hideTime});
factory _$IntroSkipModelImpl.fromJson(Map<String, dynamic> json) =>
_$$IntroSkipModelImplFromJson(json);
@override
@JsonKey(name: "EpisodeId")
final String id;
@override
@JsonKey(name: "Valid")
final bool valid;
@override
@JsonKey(
name: "IntroStart",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
final Duration start;
@override
@JsonKey(
name: "IntroEnd",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
final Duration end;
@override
@JsonKey(
name: "ShowSkipPromptAt",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
final Duration showTime;
@override
@JsonKey(
name: "HideSkipPromptAt",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
final Duration hideTime;
@override
String toString() {
return 'IntroSkipModel(id: $id, valid: $valid, start: $start, end: $end, showTime: $showTime, hideTime: $hideTime)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$IntroSkipModelImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.valid, valid) || other.valid == valid) &&
(identical(other.start, start) || other.start == start) &&
(identical(other.end, end) || other.end == end) &&
(identical(other.showTime, showTime) ||
other.showTime == showTime) &&
(identical(other.hideTime, hideTime) ||
other.hideTime == hideTime));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, id, valid, start, end, showTime, hideTime);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$IntroSkipModelImplCopyWith<_$IntroSkipModelImpl> get copyWith =>
__$$IntroSkipModelImplCopyWithImpl<_$IntroSkipModelImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$IntroSkipModelImplToJson(
this,
);
}
}
abstract class _IntroSkipModel implements IntroSkipModel {
factory _IntroSkipModel(
{@JsonKey(name: "EpisodeId") required final String id,
@JsonKey(name: "Valid") required final bool valid,
@JsonKey(
name: "IntroStart",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
required final Duration start,
@JsonKey(
name: "IntroEnd",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
required final Duration end,
@JsonKey(
name: "ShowSkipPromptAt",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
required final Duration showTime,
@JsonKey(
name: "HideSkipPromptAt",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
required final Duration hideTime}) = _$IntroSkipModelImpl;
factory _IntroSkipModel.fromJson(Map<String, dynamic> json) =
_$IntroSkipModelImpl.fromJson;
@override
@JsonKey(name: "EpisodeId")
String get id;
@override
@JsonKey(name: "Valid")
bool get valid;
@override
@JsonKey(
name: "IntroStart",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
Duration get start;
@override
@JsonKey(
name: "IntroEnd",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
Duration get end;
@override
@JsonKey(
name: "ShowSkipPromptAt",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
Duration get showTime;
@override
@JsonKey(
name: "HideSkipPromptAt",
fromJson: _durationFromMilliseconds,
toJson: _durationToMilliseconds)
Duration get hideTime;
@override
@JsonKey(ignore: true)
_$$IntroSkipModelImplCopyWith<_$IntroSkipModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,46 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'intro_skip_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$IntroOutSkipModelImpl _$$IntroOutSkipModelImplFromJson(
Map<String, dynamic> json) =>
_$IntroOutSkipModelImpl(
intro: json['intro'] == null
? null
: IntroSkipModel.fromJson(json['intro'] as Map<String, dynamic>),
credits: json['credits'] == null
? null
: IntroSkipModel.fromJson(json['credits'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$IntroOutSkipModelImplToJson(
_$IntroOutSkipModelImpl instance) =>
<String, dynamic>{
'intro': instance.intro,
'credits': instance.credits,
};
_$IntroSkipModelImpl _$$IntroSkipModelImplFromJson(Map<String, dynamic> json) =>
_$IntroSkipModelImpl(
id: json['EpisodeId'] as String,
valid: json['Valid'] as bool,
start: _durationFromMilliseconds(json['IntroStart'] as num),
end: _durationFromMilliseconds(json['IntroEnd'] as num),
showTime: _durationFromMilliseconds(json['ShowSkipPromptAt'] as num),
hideTime: _durationFromMilliseconds(json['HideSkipPromptAt'] as num),
);
Map<String, dynamic> _$$IntroSkipModelImplToJson(
_$IntroSkipModelImpl instance) =>
<String, dynamic>{
'EpisodeId': instance.id,
'Valid': instance.valid,
'IntroStart': _durationToMilliseconds(instance.start),
'IntroEnd': _durationToMilliseconds(instance.end),
'ShowSkipPromptAt': _durationToMilliseconds(instance.showTime),
'HideSkipPromptAt': _durationToMilliseconds(instance.hideTime),
};

View file

@ -0,0 +1,21 @@
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
import 'package:freezed_annotation/freezed_annotation.dart';
part 'item_properties_model.freezed.dart';
@Freezed(fromJson: false, toJson: false)
class ItemPropertiesModel with _$ItemPropertiesModel {
const ItemPropertiesModel._();
factory ItemPropertiesModel._internal({
required bool canDelete,
required bool canDownload,
}) = _ItemPropertiesModel;
factory ItemPropertiesModel.fromBaseDto(dto.BaseItemDto dtoItem) {
return ItemPropertiesModel._internal(
canDelete: dtoItem.canDelete ?? false,
canDownload: dtoItem.canDownload ?? false,
);
}
}

View file

@ -0,0 +1,156 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'item_properties_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$ItemPropertiesModel {
bool get canDelete => throw _privateConstructorUsedError;
bool get canDownload => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ItemPropertiesModelCopyWith<ItemPropertiesModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ItemPropertiesModelCopyWith<$Res> {
factory $ItemPropertiesModelCopyWith(
ItemPropertiesModel value, $Res Function(ItemPropertiesModel) then) =
_$ItemPropertiesModelCopyWithImpl<$Res, ItemPropertiesModel>;
@useResult
$Res call({bool canDelete, bool canDownload});
}
/// @nodoc
class _$ItemPropertiesModelCopyWithImpl<$Res, $Val extends ItemPropertiesModel>
implements $ItemPropertiesModelCopyWith<$Res> {
_$ItemPropertiesModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? canDelete = null,
Object? canDownload = null,
}) {
return _then(_value.copyWith(
canDelete: null == canDelete
? _value.canDelete
: canDelete // ignore: cast_nullable_to_non_nullable
as bool,
canDownload: null == canDownload
? _value.canDownload
: canDownload // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$ItemPropertiesModelImplCopyWith<$Res>
implements $ItemPropertiesModelCopyWith<$Res> {
factory _$$ItemPropertiesModelImplCopyWith(_$ItemPropertiesModelImpl value,
$Res Function(_$ItemPropertiesModelImpl) then) =
__$$ItemPropertiesModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({bool canDelete, bool canDownload});
}
/// @nodoc
class __$$ItemPropertiesModelImplCopyWithImpl<$Res>
extends _$ItemPropertiesModelCopyWithImpl<$Res, _$ItemPropertiesModelImpl>
implements _$$ItemPropertiesModelImplCopyWith<$Res> {
__$$ItemPropertiesModelImplCopyWithImpl(_$ItemPropertiesModelImpl _value,
$Res Function(_$ItemPropertiesModelImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? canDelete = null,
Object? canDownload = null,
}) {
return _then(_$ItemPropertiesModelImpl(
canDelete: null == canDelete
? _value.canDelete
: canDelete // ignore: cast_nullable_to_non_nullable
as bool,
canDownload: null == canDownload
? _value.canDownload
: canDownload // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
class _$ItemPropertiesModelImpl extends _ItemPropertiesModel {
_$ItemPropertiesModelImpl(
{required this.canDelete, required this.canDownload})
: super._();
@override
final bool canDelete;
@override
final bool canDownload;
@override
String toString() {
return 'ItemPropertiesModel._internal(canDelete: $canDelete, canDownload: $canDownload)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ItemPropertiesModelImpl &&
(identical(other.canDelete, canDelete) ||
other.canDelete == canDelete) &&
(identical(other.canDownload, canDownload) ||
other.canDownload == canDownload));
}
@override
int get hashCode => Object.hash(runtimeType, canDelete, canDownload);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ItemPropertiesModelImplCopyWith<_$ItemPropertiesModelImpl> get copyWith =>
__$$ItemPropertiesModelImplCopyWithImpl<_$ItemPropertiesModelImpl>(
this, _$identity);
}
abstract class _ItemPropertiesModel extends ItemPropertiesModel {
factory _ItemPropertiesModel(
{required final bool canDelete,
required final bool canDownload}) = _$ItemPropertiesModelImpl;
_ItemPropertiesModel._() : super._();
@override
bool get canDelete;
@override
bool get canDownload;
@override
@JsonKey(ignore: true)
_$$ItemPropertiesModelImplCopyWith<_$ItemPropertiesModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,380 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
import 'package:fladder/models/items/images_models.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'item_shared_models.mapper.dart';
@MappableClass()
class UserData with UserDataMappable {
final bool isFavourite;
final int playCount;
final int? unPlayedItemCount;
final int playbackPositionTicks;
final double progress;
final bool played;
final DateTime? lastPlayed;
const UserData({
this.isFavourite = false,
this.playCount = 0,
this.unPlayedItemCount,
this.playbackPositionTicks = 0,
this.progress = 0,
this.lastPlayed,
this.played = false,
});
factory UserData.fromDto(dto.UserItemDataDto? dto) {
if (dto == null) {
return UserData();
}
return UserData(
isFavourite: dto.isFavorite ?? false,
playCount: dto.playCount ?? 0,
playbackPositionTicks: dto.playbackPositionTicks ?? 0,
played: dto.played ?? false,
unPlayedItemCount: dto.unplayedItemCount ?? 0,
lastPlayed: dto.lastPlayedDate,
progress: dto.playedPercentage ?? 0,
);
}
Duration get playBackPosition => Duration(milliseconds: playbackPositionTicks ~/ 10000);
factory UserData.fromMap(Map<String, dynamic> map) => UserDataMapper.fromMap(map);
factory UserData.fromJson(String json) => UserDataMapper.fromJson(json);
}
class UserDataJsonSerializer extends JsonConverter<UserData, String> {
const UserDataJsonSerializer();
@override
UserData fromJson(String json) {
return UserData.fromJson(json);
}
@override
String toJson(UserData object) {
return object.toJson();
}
}
enum EditorLockedFields {
name("Name"),
overView("Overview"),
genres("Genres"),
officialRating("OfficialRating"),
cast("Cast"),
productionLocations("ProductionLocations"),
runTime("Runtime"),
studios("Studios"),
tags("Tags"),
;
const EditorLockedFields(this.value);
static Map<EditorLockedFields, bool> enabled(List<String> fromStrings) => Map.fromEntries(
EditorLockedFields.values.map(
(e) => MapEntry(e, fromStrings.contains(e.value)),
),
);
final String value;
}
enum DisplayOrder {
empty(""),
aired("aired"),
originalAirDate("originalAirDate"),
absolute("absolute"),
dvd("dvd"),
digital("digital"),
storyArc("storyArc"),
production("production"),
tv("tv"),
;
const DisplayOrder(this.value);
static DisplayOrder? fromMap(String? value) {
return DisplayOrder.values.firstWhereOrNull((element) => element.value == value) ?? DisplayOrder.empty;
}
final String value;
}
enum ShowStatus {
empty(""),
ended("Ended"),
continuing("Continuing");
const ShowStatus(this.value);
static ShowStatus? fromMap(String? value) {
return ShowStatus.values.firstWhereOrNull((element) => element.value == value) ?? ShowStatus.empty;
}
final String value;
}
class ExternalUrls {
final String name;
final String url;
ExternalUrls({
required this.name,
required this.url,
});
static List<ExternalUrls> fromDto(List<dto.ExternalUrl> dto) {
return dto.map((e) => ExternalUrls(name: e.name ?? "", url: e.url ?? "")).toList();
}
Map<String, dynamic> toMap() {
return {
'Name': name,
'Url': url,
};
}
factory ExternalUrls.fromMap(Map<String, dynamic> map) {
return ExternalUrls(
name: map['Name'] ?? '',
url: map['Url'] ?? '',
);
}
String toJson() => json.encode(toMap());
factory ExternalUrls.fromJson(String source) => ExternalUrls.fromMap(json.decode(source));
}
class GenreItems {
final String id;
final String name;
GenreItems({
required this.id,
required this.name,
});
@override
String toString() => 'GenreItems(id: $id, name: $name)';
}
class Person {
final String id;
final String name;
final ImageData? image;
final String role;
final PersonKind? type;
Person({
required this.id,
this.name = "",
this.image,
this.role = "",
this.type,
});
static Person fromBaseDto(dto.BaseItemDto item, Ref ref) {
return Person(
id: item.id ?? "",
name: item.name ?? "",
image: ImagesData.fromBaseItem(item, ref).primary,
);
}
static Person fromBasePerson(dto.BaseItemPerson person, Ref ref) {
return Person(
id: person.id ?? "",
name: person.name ?? "",
image: ImagesData.fromPersonDto(person, ref)?.primary,
role: person.role ?? "",
type: person.type);
}
dto.BaseItemPerson toPerson() {
return dto.BaseItemPerson(
id: id,
name: name,
type: type,
role: role,
);
}
static List<Person> peopleFromDto(List<dto.BaseItemPerson>? people, Ref ref) {
return people
?.mapIndexed(
(index, person) => fromBasePerson(person, ref),
)
.toList() ??
[];
}
@override
String toString() {
return 'People(id: $id, name: $name, imageUrl: $image, role: $role, type: $type)';
}
}
class Studio {
final String id;
final String name;
Studio({
required this.id,
required this.name,
});
Studio copyWith({
String? id,
String? name,
ValueGetter<String?>? image,
}) {
return Studio(
id: id ?? this.id,
name: name ?? this.name,
);
}
@override
String toString() => 'Studio(name: $name, id: $id)';
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
};
}
factory Studio.fromMap(Map<String, dynamic> map) {
return Studio(
id: map['id'] ?? map['Id'] ?? '',
name: map['name'] ?? map['Name'] ?? '',
);
}
String toJson() => json.encode(toMap());
factory Studio.fromJson(String source) => Studio.fromMap(json.decode(source));
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Studio && other.id == id && other.name == name;
}
@override
int get hashCode => id.hashCode ^ name.hashCode;
}
// class UserData {
// final bool isFavourite;
// final int playCount;
// final int? unPlayedItemCount;
// final int playbackPositionTicks;
// final double progress;
// final bool played;
// UserData({
// this.isFavourite = false,
// this.playCount = 0,
// this.unPlayedItemCount,
// this.playbackPositionTicks = 0,
// this.progress = 0,
// this.played = false,
// });
// factory UserData.fromDto(dto.UserItemDataDto? dto) {
// if (dto == null) {
// return UserData();
// }
// return UserData(
// isFavourite: dto.isFavorite ?? false,
// playCount: dto.playCount ?? 0,
// playbackPositionTicks: dto.playbackPositionTicks ?? 0,
// played: dto.played ?? false,
// unPlayedItemCount: dto.unplayedItemCount ?? 0,
// progress: dto.playedPercentage ?? 0,
// );
// }
// Duration get playBackPosition => Duration(milliseconds: playbackPositionTicks ~/ 10000);
// @override
// String toString() {
// return 'UserData(isFavourite: $isFavourite, playCount: $playCount, unPlayedItemCount: $unPlayedItemCount, playbackPositionTicks: $playbackPositionTicks, progress: $progress, played: $played)';
// }
// UserData copyWith({
// bool? isFavourite,
// int? playCount,
// int? unPlayedItemCount,
// int? playbackPositionTicks,
// double? progress,
// bool? played,
// }) {
// return UserData(
// isFavourite: isFavourite ?? this.isFavourite,
// playCount: playCount ?? this.playCount,
// unPlayedItemCount: unPlayedItemCount ?? this.unPlayedItemCount,
// playbackPositionTicks: playbackPositionTicks ?? this.playbackPositionTicks,
// progress: progress ?? this.progress,
// played: played ?? this.played,
// );
// }
// Map<String, dynamic> toMap() {
// return <String, dynamic>{
// 'isFavourite': isFavourite,
// 'playCount': playCount,
// 'unPlayedItemCount': unPlayedItemCount,
// 'playbackPositionTicks': playbackPositionTicks,
// 'progress': progress,
// 'played': played,
// };
// }
// factory UserData.fromMap(Map<String, dynamic> map) {
// return UserData(
// isFavourite: (map['isFavourite'] ?? false) as bool,
// playCount: (map['playCount'] ?? 0) as int,
// unPlayedItemCount: (map['unPlayedItemCount'] ?? 0) as int,
// playbackPositionTicks: (map['playbackPositionTicks'] ?? 0) as int,
// progress: (map['progress'] ?? 0.0) as double,
// played: (map['played'] ?? false) as bool,
// );
// }
// String toJson() => json.encode(toMap());
// factory UserData.fromJson(String source) => UserData.fromMap(json.decode(source) as Map<String, dynamic>);
// @override
// bool operator ==(covariant UserData other) {
// if (identical(this, other)) return true;
// return other.isFavourite == isFavourite &&
// other.playCount == playCount &&
// other.unPlayedItemCount == unPlayedItemCount &&
// other.playbackPositionTicks == playbackPositionTicks &&
// other.progress == progress &&
// other.played == played;
// }
// @override
// int get hashCode {
// return isFavourite.hashCode ^
// playCount.hashCode ^
// unPlayedItemCount.hashCode ^
// playbackPositionTicks.hashCode ^
// progress.hashCode ^
// played.hashCode;
// }
// }

View file

@ -0,0 +1,162 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
part of 'item_shared_models.dart';
class UserDataMapper extends ClassMapperBase<UserData> {
UserDataMapper._();
static UserDataMapper? _instance;
static UserDataMapper ensureInitialized() {
if (_instance == null) {
MapperContainer.globals.use(_instance = UserDataMapper._());
}
return _instance!;
}
@override
final String id = 'UserData';
static bool _$isFavourite(UserData v) => v.isFavourite;
static const Field<UserData, bool> _f$isFavourite =
Field('isFavourite', _$isFavourite, opt: true, def: false);
static int _$playCount(UserData v) => v.playCount;
static const Field<UserData, int> _f$playCount =
Field('playCount', _$playCount, opt: true, def: 0);
static int? _$unPlayedItemCount(UserData v) => v.unPlayedItemCount;
static const Field<UserData, int> _f$unPlayedItemCount =
Field('unPlayedItemCount', _$unPlayedItemCount, opt: true);
static int _$playbackPositionTicks(UserData v) => v.playbackPositionTicks;
static const Field<UserData, int> _f$playbackPositionTicks = Field(
'playbackPositionTicks', _$playbackPositionTicks,
opt: true, def: 0);
static double _$progress(UserData v) => v.progress;
static const Field<UserData, double> _f$progress =
Field('progress', _$progress, opt: true, def: 0);
static DateTime? _$lastPlayed(UserData v) => v.lastPlayed;
static const Field<UserData, DateTime> _f$lastPlayed =
Field('lastPlayed', _$lastPlayed, opt: true);
static bool _$played(UserData v) => v.played;
static const Field<UserData, bool> _f$played =
Field('played', _$played, opt: true, def: false);
@override
final MappableFields<UserData> fields = const {
#isFavourite: _f$isFavourite,
#playCount: _f$playCount,
#unPlayedItemCount: _f$unPlayedItemCount,
#playbackPositionTicks: _f$playbackPositionTicks,
#progress: _f$progress,
#lastPlayed: _f$lastPlayed,
#played: _f$played,
};
@override
final bool ignoreNull = true;
static UserData _instantiate(DecodingData data) {
return UserData(
isFavourite: data.dec(_f$isFavourite),
playCount: data.dec(_f$playCount),
unPlayedItemCount: data.dec(_f$unPlayedItemCount),
playbackPositionTicks: data.dec(_f$playbackPositionTicks),
progress: data.dec(_f$progress),
lastPlayed: data.dec(_f$lastPlayed),
played: data.dec(_f$played));
}
@override
final Function instantiate = _instantiate;
static UserData fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<UserData>(map);
}
static UserData fromJson(String json) {
return ensureInitialized().decodeJson<UserData>(json);
}
}
mixin UserDataMappable {
String toJson() {
return UserDataMapper.ensureInitialized()
.encodeJson<UserData>(this as UserData);
}
Map<String, dynamic> toMap() {
return UserDataMapper.ensureInitialized()
.encodeMap<UserData>(this as UserData);
}
UserDataCopyWith<UserData, UserData, UserData> get copyWith =>
_UserDataCopyWithImpl(this as UserData, $identity, $identity);
@override
String toString() {
return UserDataMapper.ensureInitialized().stringifyValue(this as UserData);
}
}
extension UserDataValueCopy<$R, $Out> on ObjectCopyWith<$R, UserData, $Out> {
UserDataCopyWith<$R, UserData, $Out> get $asUserData =>
$base.as((v, t, t2) => _UserDataCopyWithImpl(v, t, t2));
}
abstract class UserDataCopyWith<$R, $In extends UserData, $Out>
implements ClassCopyWith<$R, $In, $Out> {
$R call(
{bool? isFavourite,
int? playCount,
int? unPlayedItemCount,
int? playbackPositionTicks,
double? progress,
DateTime? lastPlayed,
bool? played});
UserDataCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t);
}
class _UserDataCopyWithImpl<$R, $Out>
extends ClassCopyWithBase<$R, UserData, $Out>
implements UserDataCopyWith<$R, UserData, $Out> {
_UserDataCopyWithImpl(super.value, super.then, super.then2);
@override
late final ClassMapperBase<UserData> $mapper =
UserDataMapper.ensureInitialized();
@override
$R call(
{bool? isFavourite,
int? playCount,
Object? unPlayedItemCount = $none,
int? playbackPositionTicks,
double? progress,
Object? lastPlayed = $none,
bool? played}) =>
$apply(FieldCopyWithData({
if (isFavourite != null) #isFavourite: isFavourite,
if (playCount != null) #playCount: playCount,
if (unPlayedItemCount != $none) #unPlayedItemCount: unPlayedItemCount,
if (playbackPositionTicks != null)
#playbackPositionTicks: playbackPositionTicks,
if (progress != null) #progress: progress,
if (lastPlayed != $none) #lastPlayed: lastPlayed,
if (played != null) #played: played
}));
@override
UserData $make(CopyWithData data) => UserData(
isFavourite: data.get(#isFavourite, or: $value.isFavourite),
playCount: data.get(#playCount, or: $value.playCount),
unPlayedItemCount:
data.get(#unPlayedItemCount, or: $value.unPlayedItemCount),
playbackPositionTicks:
data.get(#playbackPositionTicks, or: $value.playbackPositionTicks),
progress: data.get(#progress, or: $value.progress),
lastPlayed: data.get(#lastPlayed, or: $value.lastPlayed),
played: data.get(#played, or: $value.played));
@override
UserDataCopyWith<$R2, UserData, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t) =>
_UserDataCopyWithImpl($value, $cast, t);
}

View file

@ -0,0 +1,67 @@
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/items/movie_model.dart';
import 'package:fladder/models/items/overview_model.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'item_stream_model.mapper.dart';
@MappableClass()
class ItemStreamModel extends ItemBaseModel with ItemStreamModelMappable {
final ImagesData? parentImages;
final MediaStreamsModel mediaStreams;
const ItemStreamModel({
required this.parentImages,
required this.mediaStreams,
required super.name,
required super.id,
required super.overview,
required super.parentId,
required super.playlistId,
required super.images,
required super.childCount,
required super.primaryRatio,
required super.userData,
required super.canDelete,
required super.canDownload,
super.jellyType,
});
factory ItemStreamModel.fromBaseDto(dto.BaseItemDto item, Ref ref) {
return switch (item.type) {
BaseItemKind.episode => EpisodeModel.fromBaseDto(item, ref),
BaseItemKind.movie => MovieModel.fromBaseDto(item, ref),
_ => ItemStreamModel._fromBaseDto(item, ref)
};
}
factory ItemStreamModel._fromBaseDto(dto.BaseItemDto item, Ref ref) {
return ItemStreamModel(
name: item.name ?? "",
id: item.id ?? "",
childCount: item.childCount,
overview: OverviewModel.fromBaseItemDto(item, ref),
userData: UserData.fromDto(item.userData),
parentId: item.parentId,
playlistId: item.playlistItemId,
images: ImagesData.fromBaseItem(item, ref),
primaryRatio: item.primaryImageAspectRatio,
parentImages: ImagesData.fromBaseItemParent(item, ref),
canDelete: item.canDelete,
canDownload: item.canDownload,
mediaStreams:
MediaStreamsModel.fromMediaStreamsList(item.mediaSources?.firstOrNull, item.mediaStreams ?? [], ref),
);
}
String? get videoPropertiesLabel {
if (mediaStreams.displayProfile == null && mediaStreams.resolution == null) return null;
return "${mediaStreams.displayProfile?.value}";
}
}

View file

@ -0,0 +1,245 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
part of 'item_stream_model.dart';
class ItemStreamModelMapper extends SubClassMapperBase<ItemStreamModel> {
ItemStreamModelMapper._();
static ItemStreamModelMapper? _instance;
static ItemStreamModelMapper ensureInitialized() {
if (_instance == null) {
MapperContainer.globals.use(_instance = ItemStreamModelMapper._());
ItemBaseModelMapper.ensureInitialized().addSubMapper(_instance!);
OverviewModelMapper.ensureInitialized();
UserDataMapper.ensureInitialized();
}
return _instance!;
}
@override
final String id = 'ItemStreamModel';
static ImagesData? _$parentImages(ItemStreamModel v) => v.parentImages;
static const Field<ItemStreamModel, ImagesData> _f$parentImages =
Field('parentImages', _$parentImages);
static MediaStreamsModel _$mediaStreams(ItemStreamModel v) => v.mediaStreams;
static const Field<ItemStreamModel, MediaStreamsModel> _f$mediaStreams =
Field('mediaStreams', _$mediaStreams);
static String _$name(ItemStreamModel v) => v.name;
static const Field<ItemStreamModel, String> _f$name = Field('name', _$name);
static String _$id(ItemStreamModel v) => v.id;
static const Field<ItemStreamModel, String> _f$id = Field('id', _$id);
static OverviewModel _$overview(ItemStreamModel v) => v.overview;
static const Field<ItemStreamModel, OverviewModel> _f$overview =
Field('overview', _$overview);
static String? _$parentId(ItemStreamModel v) => v.parentId;
static const Field<ItemStreamModel, String> _f$parentId =
Field('parentId', _$parentId);
static String? _$playlistId(ItemStreamModel v) => v.playlistId;
static const Field<ItemStreamModel, String> _f$playlistId =
Field('playlistId', _$playlistId);
static ImagesData? _$images(ItemStreamModel v) => v.images;
static const Field<ItemStreamModel, ImagesData> _f$images =
Field('images', _$images);
static int? _$childCount(ItemStreamModel v) => v.childCount;
static const Field<ItemStreamModel, int> _f$childCount =
Field('childCount', _$childCount);
static double? _$primaryRatio(ItemStreamModel v) => v.primaryRatio;
static const Field<ItemStreamModel, double> _f$primaryRatio =
Field('primaryRatio', _$primaryRatio);
static UserData _$userData(ItemStreamModel v) => v.userData;
static const Field<ItemStreamModel, UserData> _f$userData =
Field('userData', _$userData);
static bool? _$canDelete(ItemStreamModel v) => v.canDelete;
static const Field<ItemStreamModel, bool> _f$canDelete =
Field('canDelete', _$canDelete);
static bool? _$canDownload(ItemStreamModel v) => v.canDownload;
static const Field<ItemStreamModel, bool> _f$canDownload =
Field('canDownload', _$canDownload);
static dto.BaseItemKind? _$jellyType(ItemStreamModel v) => v.jellyType;
static const Field<ItemStreamModel, dto.BaseItemKind> _f$jellyType =
Field('jellyType', _$jellyType, opt: true);
@override
final MappableFields<ItemStreamModel> fields = const {
#parentImages: _f$parentImages,
#mediaStreams: _f$mediaStreams,
#name: _f$name,
#id: _f$id,
#overview: _f$overview,
#parentId: _f$parentId,
#playlistId: _f$playlistId,
#images: _f$images,
#childCount: _f$childCount,
#primaryRatio: _f$primaryRatio,
#userData: _f$userData,
#canDelete: _f$canDelete,
#canDownload: _f$canDownload,
#jellyType: _f$jellyType,
};
@override
final bool ignoreNull = true;
@override
final String discriminatorKey = 'type';
@override
final dynamic discriminatorValue = 'ItemStreamModel';
@override
late final ClassMapperBase superMapper =
ItemBaseModelMapper.ensureInitialized();
static ItemStreamModel _instantiate(DecodingData data) {
return ItemStreamModel(
parentImages: data.dec(_f$parentImages),
mediaStreams: data.dec(_f$mediaStreams),
name: data.dec(_f$name),
id: data.dec(_f$id),
overview: data.dec(_f$overview),
parentId: data.dec(_f$parentId),
playlistId: data.dec(_f$playlistId),
images: data.dec(_f$images),
childCount: data.dec(_f$childCount),
primaryRatio: data.dec(_f$primaryRatio),
userData: data.dec(_f$userData),
canDelete: data.dec(_f$canDelete),
canDownload: data.dec(_f$canDownload),
jellyType: data.dec(_f$jellyType));
}
@override
final Function instantiate = _instantiate;
static ItemStreamModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<ItemStreamModel>(map);
}
static ItemStreamModel fromJson(String json) {
return ensureInitialized().decodeJson<ItemStreamModel>(json);
}
}
mixin ItemStreamModelMappable {
String toJson() {
return ItemStreamModelMapper.ensureInitialized()
.encodeJson<ItemStreamModel>(this as ItemStreamModel);
}
Map<String, dynamic> toMap() {
return ItemStreamModelMapper.ensureInitialized()
.encodeMap<ItemStreamModel>(this as ItemStreamModel);
}
ItemStreamModelCopyWith<ItemStreamModel, ItemStreamModel, ItemStreamModel>
get copyWith => _ItemStreamModelCopyWithImpl(
this as ItemStreamModel, $identity, $identity);
@override
String toString() {
return ItemStreamModelMapper.ensureInitialized()
.stringifyValue(this as ItemStreamModel);
}
}
extension ItemStreamModelValueCopy<$R, $Out>
on ObjectCopyWith<$R, ItemStreamModel, $Out> {
ItemStreamModelCopyWith<$R, ItemStreamModel, $Out> get $asItemStreamModel =>
$base.as((v, t, t2) => _ItemStreamModelCopyWithImpl(v, t, t2));
}
abstract class ItemStreamModelCopyWith<$R, $In extends ItemStreamModel, $Out>
implements ItemBaseModelCopyWith<$R, $In, $Out> {
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview;
@override
UserDataCopyWith<$R, UserData, UserData> get userData;
@override
$R call(
{ImagesData? parentImages,
MediaStreamsModel? mediaStreams,
String? name,
String? id,
OverviewModel? overview,
String? parentId,
String? playlistId,
ImagesData? images,
int? childCount,
double? primaryRatio,
UserData? userData,
bool? canDelete,
bool? canDownload,
dto.BaseItemKind? jellyType});
ItemStreamModelCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t);
}
class _ItemStreamModelCopyWithImpl<$R, $Out>
extends ClassCopyWithBase<$R, ItemStreamModel, $Out>
implements ItemStreamModelCopyWith<$R, ItemStreamModel, $Out> {
_ItemStreamModelCopyWithImpl(super.value, super.then, super.then2);
@override
late final ClassMapperBase<ItemStreamModel> $mapper =
ItemStreamModelMapper.ensureInitialized();
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview =>
$value.overview.copyWith.$chain((v) => call(overview: v));
@override
UserDataCopyWith<$R, UserData, UserData> get userData =>
$value.userData.copyWith.$chain((v) => call(userData: v));
@override
$R call(
{Object? parentImages = $none,
MediaStreamsModel? mediaStreams,
String? name,
String? id,
OverviewModel? overview,
Object? parentId = $none,
Object? playlistId = $none,
Object? images = $none,
Object? childCount = $none,
Object? primaryRatio = $none,
UserData? userData,
Object? canDelete = $none,
Object? canDownload = $none,
Object? jellyType = $none}) =>
$apply(FieldCopyWithData({
if (parentImages != $none) #parentImages: parentImages,
if (mediaStreams != null) #mediaStreams: mediaStreams,
if (name != null) #name: name,
if (id != null) #id: id,
if (overview != null) #overview: overview,
if (parentId != $none) #parentId: parentId,
if (playlistId != $none) #playlistId: playlistId,
if (images != $none) #images: images,
if (childCount != $none) #childCount: childCount,
if (primaryRatio != $none) #primaryRatio: primaryRatio,
if (userData != null) #userData: userData,
if (canDelete != $none) #canDelete: canDelete,
if (canDownload != $none) #canDownload: canDownload,
if (jellyType != $none) #jellyType: jellyType
}));
@override
ItemStreamModel $make(CopyWithData data) => ItemStreamModel(
parentImages: data.get(#parentImages, or: $value.parentImages),
mediaStreams: data.get(#mediaStreams, or: $value.mediaStreams),
name: data.get(#name, or: $value.name),
id: data.get(#id, or: $value.id),
overview: data.get(#overview, or: $value.overview),
parentId: data.get(#parentId, or: $value.parentId),
playlistId: data.get(#playlistId, or: $value.playlistId),
images: data.get(#images, or: $value.images),
childCount: data.get(#childCount, or: $value.childCount),
primaryRatio: data.get(#primaryRatio, or: $value.primaryRatio),
userData: data.get(#userData, or: $value.userData),
canDelete: data.get(#canDelete, or: $value.canDelete),
canDownload: data.get(#canDownload, or: $value.canDownload),
jellyType: data.get(#jellyType, or: $value.jellyType));
@override
ItemStreamModelCopyWith<$R2, ItemStreamModel, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t) =>
_ItemStreamModelCopyWithImpl($value, $cast, t);
}

View file

@ -0,0 +1,372 @@
import 'dart:convert';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:flutter/material.dart';
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/util/video_properties.dart';
class MediaStreamsModel {
final int? defaultAudioStreamIndex;
final int? defaultSubStreamIndex;
final List<VideoStreamModel> videoStreams;
final List<AudioStreamModel> audioStreams;
final List<SubStreamModel> subStreams;
MediaStreamsModel({
this.defaultAudioStreamIndex,
this.defaultSubStreamIndex,
required this.videoStreams,
required this.audioStreams,
required this.subStreams,
});
bool get isNull {
return defaultAudioStreamIndex == null ||
defaultSubStreamIndex == null ||
audioStreams.isEmpty ||
subStreams.isEmpty;
}
bool get isNotEmpty {
return audioStreams.isNotEmpty && subStreams.isNotEmpty;
}
AudioStreamModel? get currentAudioStream {
if (defaultAudioStreamIndex == -1) {
return AudioStreamModel.no();
}
return audioStreams.firstWhereOrNull((element) => element.index == defaultAudioStreamIndex) ??
audioStreams.firstOrNull;
}
SubStreamModel? get currentSubStream {
if (defaultSubStreamIndex == -1) {
return SubStreamModel.no();
}
return subStreams.firstWhereOrNull((element) => element.index == defaultSubStreamIndex) ?? subStreams.firstOrNull;
}
DisplayProfile? get displayProfile {
return DisplayProfile.fromVideoStreams(videoStreams);
}
Resolution? get resolution {
return Resolution.fromVideoStream(videoStreams.firstOrNull);
}
String? get resolutionText {
final stream = videoStreams.firstOrNull;
if (stream == null) return null;
return "${stream.width}x${stream.height}";
}
Widget? audioIcon(
BuildContext context,
Function()? onTap,
) {
final audioStream = audioStreams.firstWhereOrNull((element) => element.isDefault) ?? audioStreams.firstOrNull;
if (audioStream == null) return null;
return DefaultVideoInformationBox(
onTap: onTap,
child: Text(
audioStream.title,
),
);
}
Widget subtitleIcon(
BuildContext context,
Function()? onTap,
) {
return DefaultVideoInformationBox(
onTap: onTap,
child: Icon(
subStreams.isNotEmpty ? Icons.subtitles_rounded : Icons.subtitles_off_outlined,
),
);
}
static MediaStreamsModel fromMediaStreamsList(
dto.MediaSourceInfo? mediaSource, List<dto.MediaStream> streams, Ref ref) {
return MediaStreamsModel(
defaultAudioStreamIndex: mediaSource?.defaultAudioStreamIndex,
defaultSubStreamIndex: mediaSource?.defaultSubtitleStreamIndex,
videoStreams: streams
.where((element) => element.type == dto.MediaStreamType.video)
.map(
(e) => VideoStreamModel.fromMediaStream(e),
)
.sortByExternal(),
audioStreams: streams
.where((element) => element.type == dto.MediaStreamType.audio)
.map(
(e) => AudioStreamModel.fromMediaStream(e),
)
.sortByExternal(),
subStreams: streams
.where((element) => element.type == dto.MediaStreamType.subtitle)
.map(
(sub) => SubStreamModel.fromMediaStream(sub, ref),
)
.sortByExternal(),
);
}
MediaStreamsModel copyWith({
int? defaultAudioStreamIndex,
int? defaultSubStreamIndex,
List<VideoStreamModel>? videoStreams,
List<AudioStreamModel>? audioStreams,
List<SubStreamModel>? subStreams,
}) {
return MediaStreamsModel(
defaultAudioStreamIndex: defaultAudioStreamIndex ?? this.defaultAudioStreamIndex,
defaultSubStreamIndex: defaultSubStreamIndex ?? this.defaultSubStreamIndex,
videoStreams: videoStreams ?? this.videoStreams,
audioStreams: audioStreams ?? this.audioStreams,
subStreams: subStreams ?? this.subStreams,
);
}
@override
String toString() {
return 'MediaStreamsModel(defaultAudioStreamIndex: $defaultAudioStreamIndex, defaultSubStreamIndex: $defaultSubStreamIndex, videoStreams: $videoStreams, audioStreams: $audioStreams, subStreams: $subStreams)';
}
}
class StreamModel {
final String name;
final String codec;
final bool isDefault;
final bool isExternal;
final int index;
StreamModel({
required this.name,
required this.codec,
required this.isDefault,
required this.isExternal,
required this.index,
});
}
class VideoStreamModel extends StreamModel {
final int width;
final int height;
final double frameRate;
final String? videoDoViTitle;
final VideoRangeType? videoRangeType;
VideoStreamModel({
required super.name,
required super.codec,
required super.isDefault,
required super.isExternal,
required super.index,
required this.videoDoViTitle,
required this.videoRangeType,
required this.width,
required this.height,
required this.frameRate,
});
factory VideoStreamModel.fromMediaStream(dto.MediaStream stream) {
return VideoStreamModel(
name: stream.title ?? "",
isDefault: stream.isDefault ?? false,
codec: stream.codec ?? "",
videoDoViTitle: stream.videoDoViTitle,
videoRangeType: stream.videoRangeType,
width: stream.width ?? 0,
height: stream.height ?? 0,
frameRate: stream.realFrameRate ?? 24,
isExternal: stream.isExternal ?? false,
index: stream.index ?? -1,
);
}
String get prettyName {
return "${Resolution.fromVideoStream(this)?.value} - ${DisplayProfile.fromVideoStream(this).value} - (${codec.toUpperCase()})";
}
@override
String toString() {
return 'VideoStreamModel(width: $width, height: $height, frameRate: $frameRate, videoDoViTitle: $videoDoViTitle, videoRangeType: $videoRangeType)';
}
}
//Instead of using sortBy(a.isExternal etc..) this one seems to be more consistent for some reason
extension SortByExternalExtension<T extends StreamModel> on Iterable<T> {
List<T> sortByExternal() {
return [...where((element) => !element.isExternal), ...where((element) => element.isExternal)];
}
}
class AudioStreamModel extends StreamModel {
final String displayTitle;
final String language;
final String channelLayout;
AudioStreamModel({
required this.displayTitle,
required super.name,
required super.codec,
required super.isDefault,
required super.isExternal,
required super.index,
required this.language,
required this.channelLayout,
});
factory AudioStreamModel.fromMediaStream(dto.MediaStream stream) {
return AudioStreamModel(
displayTitle: stream.displayTitle ?? "",
name: stream.title ?? "",
isDefault: stream.isDefault ?? false,
codec: stream.codec ?? "",
language: stream.language ?? "Unknown",
channelLayout: stream.channelLayout ?? "",
isExternal: stream.isExternal ?? false,
index: stream.index ?? -1,
);
}
String get title =>
[name, language, codec, channelLayout].whereNotNull().where((element) => element.isNotEmpty).join(' - ');
AudioStreamModel.no({
super.name = 'Off',
this.displayTitle = 'Off',
this.language = '',
super.codec = '',
this.channelLayout = '',
super.isDefault = false,
super.isExternal = false,
super.index = -1,
});
}
class SubStreamModel extends StreamModel {
String id;
String title;
String displayTitle;
String language;
String? url;
bool supportsExternalStream;
SubStreamModel({
required super.name,
required this.id,
required this.title,
required this.displayTitle,
required this.language,
this.url,
required super.codec,
required super.isDefault,
required super.isExternal,
required super.index,
this.supportsExternalStream = false,
});
SubStreamModel.no({
super.name = 'Off',
this.id = 'Off',
this.title = 'Off',
this.displayTitle = 'Off',
this.language = '',
this.url = '',
super.codec = '',
super.isDefault = false,
super.isExternal = false,
super.index = -1,
this.supportsExternalStream = false,
});
factory SubStreamModel.fromMediaStream(dto.MediaStream stream, Ref ref) {
return SubStreamModel(
name: stream.title ?? "",
title: stream.title ?? "",
displayTitle: stream.displayTitle ?? "",
language: stream.language ?? "Unknown",
isDefault: stream.isDefault ?? false,
codec: stream.codec ?? "",
id: stream.hashCode.toString(),
supportsExternalStream: stream.supportsExternalStream ?? false,
url: stream.deliveryUrl != null
? "${ref.read(userProvider)?.server ?? ""}${stream.deliveryUrl}}".replaceAll(".vtt", ".srt")
: null,
isExternal: stream.isExternal ?? false,
index: stream.index ?? -1,
);
}
SubStreamModel copyWith({
String? name,
String? id,
String? title,
String? displayTitle,
String? language,
ValueGetter<String?>? url,
String? codec,
bool? isDefault,
bool? isExternal,
int? index,
bool? supportsExternalStream,
}) {
return SubStreamModel(
name: name ?? this.name,
id: id ?? this.id,
title: title ?? this.title,
displayTitle: displayTitle ?? this.displayTitle,
language: language ?? this.language,
url: url != null ? url() : this.url,
supportsExternalStream: supportsExternalStream ?? this.supportsExternalStream,
codec: codec ?? this.codec,
isDefault: isDefault ?? this.isDefault,
isExternal: isExternal ?? this.isExternal,
index: index ?? this.index,
);
}
Map<String, dynamic> toMap() {
return {
'name': name,
'id': id,
'title': title,
'displayTitle': displayTitle,
'language': language,
'url': url,
'supportsExternalStream': supportsExternalStream,
'codec': codec,
'isExternal': isExternal,
'isDefault': isDefault,
'index': index,
};
}
factory SubStreamModel.fromMap(Map<String, dynamic> map) {
return SubStreamModel(
name: map['name'] ?? '',
id: map['id'] ?? '',
title: map['title'] ?? '',
displayTitle: map['displayTitle'] ?? '',
language: map['language'] ?? '',
url: map['url'],
supportsExternalStream: map['supportsExternalStream'] ?? false,
codec: map['codec'] ?? '',
isDefault: map['isDefault'] ?? false,
isExternal: map['isExternal'] ?? false,
index: map['index'] ?? -1,
);
}
String toJson() => json.encode(toMap());
factory SubStreamModel.fromJson(String source) => SubStreamModel.fromMap(json.decode(source));
@override
String toString() {
return 'SubFile(title: $title, displayTitle: $displayTitle, language: $language, url: $url, isExternal: $isExternal)';
}
}

View file

@ -0,0 +1,107 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:fladder/util/humanize_duration.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/item_stream_model.dart';
import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/items/overview_model.dart';
import 'package:fladder/screens/details_screens/movie_detail_screen.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'movie_model.mapper.dart';
@MappableClass()
class MovieModel extends ItemStreamModel with MovieModelMappable {
final String originalTitle;
final String? path;
final DateTime premiereDate;
final String sortName;
final String status;
final List<ItemBaseModel> related;
final List<Chapter> chapters;
const MovieModel({
required this.originalTitle,
this.path,
this.chapters = const [],
required this.premiereDate,
required this.sortName,
required this.status,
this.related = const [],
required super.name,
required super.id,
required super.overview,
required super.parentId,
required super.playlistId,
required super.images,
required super.childCount,
required super.primaryRatio,
required super.userData,
required super.parentImages,
required super.mediaStreams,
required super.canDownload,
required super.canDelete,
super.jellyType,
});
@override
String? detailedName(BuildContext context) => "$name${overview.yearAired != null ? " (${overview.yearAired})" : ""}";
@override
Widget get detailScreenWidget => MovieDetailScreen(item: this);
@override
ItemBaseModel get parentBaseModel => this;
@override
String? get subText => overview.yearAired?.toString() ?? overview.runTime.humanize;
@override
bool get playAble => true;
@override
bool get identifiable => true;
@override
String? label(BuildContext context) =>
overview.yearAired == null ? overview.runTime.humanize : "$name (${overview.yearAired})";
@override
ImageData? get bannerImage => images?.backDrop?.firstOrNull ?? images?.primary ?? getPosters?.primary;
@override
MediaStreamsModel? get streamModel => mediaStreams;
@override
bool get syncAble => true;
factory MovieModel.fromBaseDto(dto.BaseItemDto item, Ref ref) {
return MovieModel(
name: item.name ?? "",
id: item.id ?? "",
childCount: item.childCount,
overview: OverviewModel.fromBaseItemDto(item, ref),
userData: UserData.fromDto(item.userData),
parentId: item.parentId,
playlistId: item.playlistItemId,
sortName: item.sortName ?? "",
status: item.status ?? "",
originalTitle: item.originalTitle ?? "",
images: ImagesData.fromBaseItem(item, ref),
primaryRatio: item.primaryImageAspectRatio,
chapters: Chapter.chaptersFromInfo(item.id ?? "", item.chapters ?? [], ref),
premiereDate: item.premiereDate ?? DateTime.now(),
parentImages: ImagesData.fromBaseItemParent(item, ref),
canDelete: item.canDelete,
canDownload: item.canDownload,
mediaStreams:
MediaStreamsModel.fromMediaStreamsList(item.mediaSources?.firstOrNull, item.mediaStreams ?? [], ref),
);
}
}

View file

@ -0,0 +1,318 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
part of 'movie_model.dart';
class MovieModelMapper extends SubClassMapperBase<MovieModel> {
MovieModelMapper._();
static MovieModelMapper? _instance;
static MovieModelMapper ensureInitialized() {
if (_instance == null) {
MapperContainer.globals.use(_instance = MovieModelMapper._());
ItemStreamModelMapper.ensureInitialized().addSubMapper(_instance!);
ItemBaseModelMapper.ensureInitialized();
OverviewModelMapper.ensureInitialized();
UserDataMapper.ensureInitialized();
}
return _instance!;
}
@override
final String id = 'MovieModel';
static String _$originalTitle(MovieModel v) => v.originalTitle;
static const Field<MovieModel, String> _f$originalTitle =
Field('originalTitle', _$originalTitle);
static String? _$path(MovieModel v) => v.path;
static const Field<MovieModel, String> _f$path =
Field('path', _$path, opt: true);
static List<Chapter> _$chapters(MovieModel v) => v.chapters;
static const Field<MovieModel, List<Chapter>> _f$chapters =
Field('chapters', _$chapters, opt: true, def: const []);
static DateTime _$premiereDate(MovieModel v) => v.premiereDate;
static const Field<MovieModel, DateTime> _f$premiereDate =
Field('premiereDate', _$premiereDate);
static String _$sortName(MovieModel v) => v.sortName;
static const Field<MovieModel, String> _f$sortName =
Field('sortName', _$sortName);
static String _$status(MovieModel v) => v.status;
static const Field<MovieModel, String> _f$status = Field('status', _$status);
static List<ItemBaseModel> _$related(MovieModel v) => v.related;
static const Field<MovieModel, List<ItemBaseModel>> _f$related =
Field('related', _$related, opt: true, def: const []);
static String _$name(MovieModel v) => v.name;
static const Field<MovieModel, String> _f$name = Field('name', _$name);
static String _$id(MovieModel v) => v.id;
static const Field<MovieModel, String> _f$id = Field('id', _$id);
static OverviewModel _$overview(MovieModel v) => v.overview;
static const Field<MovieModel, OverviewModel> _f$overview =
Field('overview', _$overview);
static String? _$parentId(MovieModel v) => v.parentId;
static const Field<MovieModel, String> _f$parentId =
Field('parentId', _$parentId);
static String? _$playlistId(MovieModel v) => v.playlistId;
static const Field<MovieModel, String> _f$playlistId =
Field('playlistId', _$playlistId);
static ImagesData? _$images(MovieModel v) => v.images;
static const Field<MovieModel, ImagesData> _f$images =
Field('images', _$images);
static int? _$childCount(MovieModel v) => v.childCount;
static const Field<MovieModel, int> _f$childCount =
Field('childCount', _$childCount);
static double? _$primaryRatio(MovieModel v) => v.primaryRatio;
static const Field<MovieModel, double> _f$primaryRatio =
Field('primaryRatio', _$primaryRatio);
static UserData _$userData(MovieModel v) => v.userData;
static const Field<MovieModel, UserData> _f$userData =
Field('userData', _$userData);
static ImagesData? _$parentImages(MovieModel v) => v.parentImages;
static const Field<MovieModel, ImagesData> _f$parentImages =
Field('parentImages', _$parentImages);
static MediaStreamsModel _$mediaStreams(MovieModel v) => v.mediaStreams;
static const Field<MovieModel, MediaStreamsModel> _f$mediaStreams =
Field('mediaStreams', _$mediaStreams);
static bool? _$canDownload(MovieModel v) => v.canDownload;
static const Field<MovieModel, bool> _f$canDownload =
Field('canDownload', _$canDownload);
static bool? _$canDelete(MovieModel v) => v.canDelete;
static const Field<MovieModel, bool> _f$canDelete =
Field('canDelete', _$canDelete);
static dto.BaseItemKind? _$jellyType(MovieModel v) => v.jellyType;
static const Field<MovieModel, dto.BaseItemKind> _f$jellyType =
Field('jellyType', _$jellyType, opt: true);
@override
final MappableFields<MovieModel> fields = const {
#originalTitle: _f$originalTitle,
#path: _f$path,
#chapters: _f$chapters,
#premiereDate: _f$premiereDate,
#sortName: _f$sortName,
#status: _f$status,
#related: _f$related,
#name: _f$name,
#id: _f$id,
#overview: _f$overview,
#parentId: _f$parentId,
#playlistId: _f$playlistId,
#images: _f$images,
#childCount: _f$childCount,
#primaryRatio: _f$primaryRatio,
#userData: _f$userData,
#parentImages: _f$parentImages,
#mediaStreams: _f$mediaStreams,
#canDownload: _f$canDownload,
#canDelete: _f$canDelete,
#jellyType: _f$jellyType,
};
@override
final bool ignoreNull = true;
@override
final String discriminatorKey = 'type';
@override
final dynamic discriminatorValue = 'MovieModel';
@override
late final ClassMapperBase superMapper =
ItemStreamModelMapper.ensureInitialized();
static MovieModel _instantiate(DecodingData data) {
return MovieModel(
originalTitle: data.dec(_f$originalTitle),
path: data.dec(_f$path),
chapters: data.dec(_f$chapters),
premiereDate: data.dec(_f$premiereDate),
sortName: data.dec(_f$sortName),
status: data.dec(_f$status),
related: data.dec(_f$related),
name: data.dec(_f$name),
id: data.dec(_f$id),
overview: data.dec(_f$overview),
parentId: data.dec(_f$parentId),
playlistId: data.dec(_f$playlistId),
images: data.dec(_f$images),
childCount: data.dec(_f$childCount),
primaryRatio: data.dec(_f$primaryRatio),
userData: data.dec(_f$userData),
parentImages: data.dec(_f$parentImages),
mediaStreams: data.dec(_f$mediaStreams),
canDownload: data.dec(_f$canDownload),
canDelete: data.dec(_f$canDelete),
jellyType: data.dec(_f$jellyType));
}
@override
final Function instantiate = _instantiate;
static MovieModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<MovieModel>(map);
}
static MovieModel fromJson(String json) {
return ensureInitialized().decodeJson<MovieModel>(json);
}
}
mixin MovieModelMappable {
String toJson() {
return MovieModelMapper.ensureInitialized()
.encodeJson<MovieModel>(this as MovieModel);
}
Map<String, dynamic> toMap() {
return MovieModelMapper.ensureInitialized()
.encodeMap<MovieModel>(this as MovieModel);
}
MovieModelCopyWith<MovieModel, MovieModel, MovieModel> get copyWith =>
_MovieModelCopyWithImpl(this as MovieModel, $identity, $identity);
@override
String toString() {
return MovieModelMapper.ensureInitialized()
.stringifyValue(this as MovieModel);
}
}
extension MovieModelValueCopy<$R, $Out>
on ObjectCopyWith<$R, MovieModel, $Out> {
MovieModelCopyWith<$R, MovieModel, $Out> get $asMovieModel =>
$base.as((v, t, t2) => _MovieModelCopyWithImpl(v, t, t2));
}
abstract class MovieModelCopyWith<$R, $In extends MovieModel, $Out>
implements ItemStreamModelCopyWith<$R, $In, $Out> {
ListCopyWith<$R, Chapter, ObjectCopyWith<$R, Chapter, Chapter>> get chapters;
ListCopyWith<$R, ItemBaseModel,
ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>> get related;
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview;
@override
UserDataCopyWith<$R, UserData, UserData> get userData;
@override
$R call(
{String? originalTitle,
String? path,
List<Chapter>? chapters,
DateTime? premiereDate,
String? sortName,
String? status,
List<ItemBaseModel>? related,
String? name,
String? id,
OverviewModel? overview,
String? parentId,
String? playlistId,
ImagesData? images,
int? childCount,
double? primaryRatio,
UserData? userData,
ImagesData? parentImages,
MediaStreamsModel? mediaStreams,
bool? canDownload,
bool? canDelete,
dto.BaseItemKind? jellyType});
MovieModelCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t);
}
class _MovieModelCopyWithImpl<$R, $Out>
extends ClassCopyWithBase<$R, MovieModel, $Out>
implements MovieModelCopyWith<$R, MovieModel, $Out> {
_MovieModelCopyWithImpl(super.value, super.then, super.then2);
@override
late final ClassMapperBase<MovieModel> $mapper =
MovieModelMapper.ensureInitialized();
@override
ListCopyWith<$R, Chapter, ObjectCopyWith<$R, Chapter, Chapter>>
get chapters => ListCopyWith($value.chapters,
(v, t) => ObjectCopyWith(v, $identity, t), (v) => call(chapters: v));
@override
ListCopyWith<$R, ItemBaseModel,
ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>>
get related => ListCopyWith($value.related,
(v, t) => v.copyWith.$chain(t), (v) => call(related: v));
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview =>
$value.overview.copyWith.$chain((v) => call(overview: v));
@override
UserDataCopyWith<$R, UserData, UserData> get userData =>
$value.userData.copyWith.$chain((v) => call(userData: v));
@override
$R call(
{String? originalTitle,
Object? path = $none,
List<Chapter>? chapters,
DateTime? premiereDate,
String? sortName,
String? status,
List<ItemBaseModel>? related,
String? name,
String? id,
OverviewModel? overview,
Object? parentId = $none,
Object? playlistId = $none,
Object? images = $none,
Object? childCount = $none,
Object? primaryRatio = $none,
UserData? userData,
Object? parentImages = $none,
MediaStreamsModel? mediaStreams,
Object? canDownload = $none,
Object? canDelete = $none,
Object? jellyType = $none}) =>
$apply(FieldCopyWithData({
if (originalTitle != null) #originalTitle: originalTitle,
if (path != $none) #path: path,
if (chapters != null) #chapters: chapters,
if (premiereDate != null) #premiereDate: premiereDate,
if (sortName != null) #sortName: sortName,
if (status != null) #status: status,
if (related != null) #related: related,
if (name != null) #name: name,
if (id != null) #id: id,
if (overview != null) #overview: overview,
if (parentId != $none) #parentId: parentId,
if (playlistId != $none) #playlistId: playlistId,
if (images != $none) #images: images,
if (childCount != $none) #childCount: childCount,
if (primaryRatio != $none) #primaryRatio: primaryRatio,
if (userData != null) #userData: userData,
if (parentImages != $none) #parentImages: parentImages,
if (mediaStreams != null) #mediaStreams: mediaStreams,
if (canDownload != $none) #canDownload: canDownload,
if (canDelete != $none) #canDelete: canDelete,
if (jellyType != $none) #jellyType: jellyType
}));
@override
MovieModel $make(CopyWithData data) => MovieModel(
originalTitle: data.get(#originalTitle, or: $value.originalTitle),
path: data.get(#path, or: $value.path),
chapters: data.get(#chapters, or: $value.chapters),
premiereDate: data.get(#premiereDate, or: $value.premiereDate),
sortName: data.get(#sortName, or: $value.sortName),
status: data.get(#status, or: $value.status),
related: data.get(#related, or: $value.related),
name: data.get(#name, or: $value.name),
id: data.get(#id, or: $value.id),
overview: data.get(#overview, or: $value.overview),
parentId: data.get(#parentId, or: $value.parentId),
playlistId: data.get(#playlistId, or: $value.playlistId),
images: data.get(#images, or: $value.images),
childCount: data.get(#childCount, or: $value.childCount),
primaryRatio: data.get(#primaryRatio, or: $value.primaryRatio),
userData: data.get(#userData, or: $value.userData),
parentImages: data.get(#parentImages, or: $value.parentImages),
mediaStreams: data.get(#mediaStreams, or: $value.mediaStreams),
canDownload: data.get(#canDownload, or: $value.canDownload),
canDelete: data.get(#canDelete, or: $value.canDelete),
jellyType: data.get(#jellyType, or: $value.jellyType));
@override
MovieModelCopyWith<$R2, MovieModel, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t) =>
_MovieModelCopyWithImpl($value, $cast, t);
}

View file

@ -0,0 +1,82 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/trick_play_model.dart';
import 'package:fladder/util/duration_extensions.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'overview_model.mapper.dart';
@MappableClass()
class OverviewModel with OverviewModelMappable {
final Duration? runTime;
final String summary;
final int? yearAired;
final DateTime? dateAdded;
final String? parentalRating;
final int? productionYear;
final double? criticRating;
final double? communityRating;
final Map<String, TrickPlayModel>? trickPlayInfo;
final List<Chapter>? chapters;
final List<ExternalUrls>? externalUrls;
final List<Studio> studios;
final List<String> genres;
final List<GenreItems> genreItems;
final List<String> tags;
final List<Person> people;
const OverviewModel({
this.runTime,
this.summary = "",
this.yearAired,
this.dateAdded,
this.parentalRating,
this.productionYear,
this.criticRating,
this.communityRating,
this.trickPlayInfo,
this.chapters,
this.externalUrls,
this.studios = const [],
this.genres = const [],
this.genreItems = const [],
this.tags = const [],
this.people = const [],
});
List<Person> get directors {
return people.where((element) => element.type == PersonKind.director).toList();
}
List<Person> get writers {
return people.where((element) => element.type == PersonKind.writer).toList();
}
factory OverviewModel.fromBaseItemDto(BaseItemDto item, Ref ref) {
final trickPlayItem = item.trickplay;
return OverviewModel(
runTime: item.runTimeDuration,
yearAired: item.productionYear,
parentalRating: item.officialRating,
summary: item.overview ?? "",
genres: item.genres ?? [],
criticRating: item.criticRating,
communityRating: item.communityRating,
tags: item.tags ?? [],
dateAdded: item.dateCreated,
trickPlayInfo:
trickPlayItem != null && trickPlayItem.isNotEmpty ? TrickPlayModel.toTrickPlayMap(trickPlayItem) : null,
chapters: item.id != null ? Chapter.chaptersFromInfo(item.id ?? "", item.chapters ?? [], ref) : null,
studios: item.studios?.map((e) => Studio(id: e.id ?? "", name: e.name ?? "")).toList() ?? [],
genreItems: item.genreItems?.map((e) => GenreItems(id: e.id ?? "", name: e.name ?? "")).toList() ?? [],
externalUrls: ExternalUrls.fromDto(item.externalUrls ?? []),
people: Person.peopleFromDto(item.people ?? [], ref),
);
}
factory OverviewModel.fromMap(Map<String, dynamic> map) => OverviewModelMapper.fromMap(map);
factory OverviewModel.fromJson(String json) => OverviewModelMapper.fromJson(json);
}

View file

@ -0,0 +1,302 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
part of 'overview_model.dart';
class OverviewModelMapper extends ClassMapperBase<OverviewModel> {
OverviewModelMapper._();
static OverviewModelMapper? _instance;
static OverviewModelMapper ensureInitialized() {
if (_instance == null) {
MapperContainer.globals.use(_instance = OverviewModelMapper._());
}
return _instance!;
}
@override
final String id = 'OverviewModel';
static Duration? _$runTime(OverviewModel v) => v.runTime;
static const Field<OverviewModel, Duration> _f$runTime =
Field('runTime', _$runTime, opt: true);
static String _$summary(OverviewModel v) => v.summary;
static const Field<OverviewModel, String> _f$summary =
Field('summary', _$summary, opt: true, def: "");
static int? _$yearAired(OverviewModel v) => v.yearAired;
static const Field<OverviewModel, int> _f$yearAired =
Field('yearAired', _$yearAired, opt: true);
static DateTime? _$dateAdded(OverviewModel v) => v.dateAdded;
static const Field<OverviewModel, DateTime> _f$dateAdded =
Field('dateAdded', _$dateAdded, opt: true);
static String? _$parentalRating(OverviewModel v) => v.parentalRating;
static const Field<OverviewModel, String> _f$parentalRating =
Field('parentalRating', _$parentalRating, opt: true);
static int? _$productionYear(OverviewModel v) => v.productionYear;
static const Field<OverviewModel, int> _f$productionYear =
Field('productionYear', _$productionYear, opt: true);
static double? _$criticRating(OverviewModel v) => v.criticRating;
static const Field<OverviewModel, double> _f$criticRating =
Field('criticRating', _$criticRating, opt: true);
static double? _$communityRating(OverviewModel v) => v.communityRating;
static const Field<OverviewModel, double> _f$communityRating =
Field('communityRating', _$communityRating, opt: true);
static Map<String, TrickPlayModel>? _$trickPlayInfo(OverviewModel v) =>
v.trickPlayInfo;
static const Field<OverviewModel, Map<String, TrickPlayModel>>
_f$trickPlayInfo = Field('trickPlayInfo', _$trickPlayInfo, opt: true);
static List<Chapter>? _$chapters(OverviewModel v) => v.chapters;
static const Field<OverviewModel, List<Chapter>> _f$chapters =
Field('chapters', _$chapters, opt: true);
static List<ExternalUrls>? _$externalUrls(OverviewModel v) => v.externalUrls;
static const Field<OverviewModel, List<ExternalUrls>> _f$externalUrls =
Field('externalUrls', _$externalUrls, opt: true);
static List<Studio> _$studios(OverviewModel v) => v.studios;
static const Field<OverviewModel, List<Studio>> _f$studios =
Field('studios', _$studios, opt: true, def: const []);
static List<String> _$genres(OverviewModel v) => v.genres;
static const Field<OverviewModel, List<String>> _f$genres =
Field('genres', _$genres, opt: true, def: const []);
static List<GenreItems> _$genreItems(OverviewModel v) => v.genreItems;
static const Field<OverviewModel, List<GenreItems>> _f$genreItems =
Field('genreItems', _$genreItems, opt: true, def: const []);
static List<String> _$tags(OverviewModel v) => v.tags;
static const Field<OverviewModel, List<String>> _f$tags =
Field('tags', _$tags, opt: true, def: const []);
static List<Person> _$people(OverviewModel v) => v.people;
static const Field<OverviewModel, List<Person>> _f$people =
Field('people', _$people, opt: true, def: const []);
@override
final MappableFields<OverviewModel> fields = const {
#runTime: _f$runTime,
#summary: _f$summary,
#yearAired: _f$yearAired,
#dateAdded: _f$dateAdded,
#parentalRating: _f$parentalRating,
#productionYear: _f$productionYear,
#criticRating: _f$criticRating,
#communityRating: _f$communityRating,
#trickPlayInfo: _f$trickPlayInfo,
#chapters: _f$chapters,
#externalUrls: _f$externalUrls,
#studios: _f$studios,
#genres: _f$genres,
#genreItems: _f$genreItems,
#tags: _f$tags,
#people: _f$people,
};
@override
final bool ignoreNull = true;
static OverviewModel _instantiate(DecodingData data) {
return OverviewModel(
runTime: data.dec(_f$runTime),
summary: data.dec(_f$summary),
yearAired: data.dec(_f$yearAired),
dateAdded: data.dec(_f$dateAdded),
parentalRating: data.dec(_f$parentalRating),
productionYear: data.dec(_f$productionYear),
criticRating: data.dec(_f$criticRating),
communityRating: data.dec(_f$communityRating),
trickPlayInfo: data.dec(_f$trickPlayInfo),
chapters: data.dec(_f$chapters),
externalUrls: data.dec(_f$externalUrls),
studios: data.dec(_f$studios),
genres: data.dec(_f$genres),
genreItems: data.dec(_f$genreItems),
tags: data.dec(_f$tags),
people: data.dec(_f$people));
}
@override
final Function instantiate = _instantiate;
static OverviewModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<OverviewModel>(map);
}
static OverviewModel fromJson(String json) {
return ensureInitialized().decodeJson<OverviewModel>(json);
}
}
mixin OverviewModelMappable {
String toJson() {
return OverviewModelMapper.ensureInitialized()
.encodeJson<OverviewModel>(this as OverviewModel);
}
Map<String, dynamic> toMap() {
return OverviewModelMapper.ensureInitialized()
.encodeMap<OverviewModel>(this as OverviewModel);
}
OverviewModelCopyWith<OverviewModel, OverviewModel, OverviewModel>
get copyWith => _OverviewModelCopyWithImpl(
this as OverviewModel, $identity, $identity);
@override
String toString() {
return OverviewModelMapper.ensureInitialized()
.stringifyValue(this as OverviewModel);
}
}
extension OverviewModelValueCopy<$R, $Out>
on ObjectCopyWith<$R, OverviewModel, $Out> {
OverviewModelCopyWith<$R, OverviewModel, $Out> get $asOverviewModel =>
$base.as((v, t, t2) => _OverviewModelCopyWithImpl(v, t, t2));
}
abstract class OverviewModelCopyWith<$R, $In extends OverviewModel, $Out>
implements ClassCopyWith<$R, $In, $Out> {
MapCopyWith<$R, String, TrickPlayModel,
ObjectCopyWith<$R, TrickPlayModel, TrickPlayModel>>? get trickPlayInfo;
ListCopyWith<$R, Chapter, ObjectCopyWith<$R, Chapter, Chapter>>? get chapters;
ListCopyWith<$R, ExternalUrls,
ObjectCopyWith<$R, ExternalUrls, ExternalUrls>>? get externalUrls;
ListCopyWith<$R, Studio, ObjectCopyWith<$R, Studio, Studio>> get studios;
ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>> get genres;
ListCopyWith<$R, GenreItems, ObjectCopyWith<$R, GenreItems, GenreItems>>
get genreItems;
ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>> get tags;
ListCopyWith<$R, Person, ObjectCopyWith<$R, Person, Person>> get people;
$R call(
{Duration? runTime,
String? summary,
int? yearAired,
DateTime? dateAdded,
String? parentalRating,
int? productionYear,
double? criticRating,
double? communityRating,
Map<String, TrickPlayModel>? trickPlayInfo,
List<Chapter>? chapters,
List<ExternalUrls>? externalUrls,
List<Studio>? studios,
List<String>? genres,
List<GenreItems>? genreItems,
List<String>? tags,
List<Person>? people});
OverviewModelCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t);
}
class _OverviewModelCopyWithImpl<$R, $Out>
extends ClassCopyWithBase<$R, OverviewModel, $Out>
implements OverviewModelCopyWith<$R, OverviewModel, $Out> {
_OverviewModelCopyWithImpl(super.value, super.then, super.then2);
@override
late final ClassMapperBase<OverviewModel> $mapper =
OverviewModelMapper.ensureInitialized();
@override
MapCopyWith<$R, String, TrickPlayModel,
ObjectCopyWith<$R, TrickPlayModel, TrickPlayModel>>?
get trickPlayInfo => $value.trickPlayInfo != null
? MapCopyWith(
$value.trickPlayInfo!,
(v, t) => ObjectCopyWith(v, $identity, t),
(v) => call(trickPlayInfo: v))
: null;
@override
ListCopyWith<$R, Chapter, ObjectCopyWith<$R, Chapter, Chapter>>?
get chapters => $value.chapters != null
? ListCopyWith(
$value.chapters!,
(v, t) => ObjectCopyWith(v, $identity, t),
(v) => call(chapters: v))
: null;
@override
ListCopyWith<$R, ExternalUrls,
ObjectCopyWith<$R, ExternalUrls, ExternalUrls>>?
get externalUrls => $value.externalUrls != null
? ListCopyWith(
$value.externalUrls!,
(v, t) => ObjectCopyWith(v, $identity, t),
(v) => call(externalUrls: v))
: null;
@override
ListCopyWith<$R, Studio, ObjectCopyWith<$R, Studio, Studio>> get studios =>
ListCopyWith($value.studios, (v, t) => ObjectCopyWith(v, $identity, t),
(v) => call(studios: v));
@override
ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>> get genres =>
ListCopyWith($value.genres, (v, t) => ObjectCopyWith(v, $identity, t),
(v) => call(genres: v));
@override
ListCopyWith<$R, GenreItems, ObjectCopyWith<$R, GenreItems, GenreItems>>
get genreItems => ListCopyWith(
$value.genreItems,
(v, t) => ObjectCopyWith(v, $identity, t),
(v) => call(genreItems: v));
@override
ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>> get tags =>
ListCopyWith($value.tags, (v, t) => ObjectCopyWith(v, $identity, t),
(v) => call(tags: v));
@override
ListCopyWith<$R, Person, ObjectCopyWith<$R, Person, Person>> get people =>
ListCopyWith($value.people, (v, t) => ObjectCopyWith(v, $identity, t),
(v) => call(people: v));
@override
$R call(
{Object? runTime = $none,
String? summary,
Object? yearAired = $none,
Object? dateAdded = $none,
Object? parentalRating = $none,
Object? productionYear = $none,
Object? criticRating = $none,
Object? communityRating = $none,
Object? trickPlayInfo = $none,
Object? chapters = $none,
Object? externalUrls = $none,
List<Studio>? studios,
List<String>? genres,
List<GenreItems>? genreItems,
List<String>? tags,
List<Person>? people}) =>
$apply(FieldCopyWithData({
if (runTime != $none) #runTime: runTime,
if (summary != null) #summary: summary,
if (yearAired != $none) #yearAired: yearAired,
if (dateAdded != $none) #dateAdded: dateAdded,
if (parentalRating != $none) #parentalRating: parentalRating,
if (productionYear != $none) #productionYear: productionYear,
if (criticRating != $none) #criticRating: criticRating,
if (communityRating != $none) #communityRating: communityRating,
if (trickPlayInfo != $none) #trickPlayInfo: trickPlayInfo,
if (chapters != $none) #chapters: chapters,
if (externalUrls != $none) #externalUrls: externalUrls,
if (studios != null) #studios: studios,
if (genres != null) #genres: genres,
if (genreItems != null) #genreItems: genreItems,
if (tags != null) #tags: tags,
if (people != null) #people: people
}));
@override
OverviewModel $make(CopyWithData data) => OverviewModel(
runTime: data.get(#runTime, or: $value.runTime),
summary: data.get(#summary, or: $value.summary),
yearAired: data.get(#yearAired, or: $value.yearAired),
dateAdded: data.get(#dateAdded, or: $value.dateAdded),
parentalRating: data.get(#parentalRating, or: $value.parentalRating),
productionYear: data.get(#productionYear, or: $value.productionYear),
criticRating: data.get(#criticRating, or: $value.criticRating),
communityRating: data.get(#communityRating, or: $value.communityRating),
trickPlayInfo: data.get(#trickPlayInfo, or: $value.trickPlayInfo),
chapters: data.get(#chapters, or: $value.chapters),
externalUrls: data.get(#externalUrls, or: $value.externalUrls),
studios: data.get(#studios, or: $value.studios),
genres: data.get(#genres, or: $value.genres),
genreItems: data.get(#genreItems, or: $value.genreItems),
tags: data.get(#tags, or: $value.tags),
people: data.get(#people, or: $value.people));
@override
OverviewModelCopyWith<$R2, OverviewModel, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t) =>
_OverviewModelCopyWithImpl($value, $cast, t);
}

View file

@ -0,0 +1,68 @@
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/overview_model.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/movie_model.dart';
import 'package:fladder/models/items/series_model.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
part 'person_model.mapper.dart';
@MappableClass()
class PersonModel extends ItemBaseModel with PersonModelMappable {
final DateTime? dateOfBirth;
final List<String> birthPlace;
final List<MovieModel> movies;
final List<SeriesModel> series;
const PersonModel({
this.dateOfBirth,
required this.birthPlace,
required this.movies,
required this.series,
required super.name,
required super.id,
required super.overview,
required super.parentId,
required super.playlistId,
required super.images,
required super.childCount,
required super.primaryRatio,
required super.userData,
super.canDownload,
super.canDelete,
super.jellyType,
});
static PersonModel fromBaseDto(BaseItemDto item, Ref ref) {
return PersonModel(
name: item.name ?? "",
id: item.id ?? "",
childCount: item.childCount,
overview: OverviewModel.fromBaseItemDto(item, ref),
userData: UserData.fromDto(item.userData),
parentId: item.parentId,
playlistId: item.playlistItemId,
images: ImagesData.fromBaseItem(item, ref, getOriginalSize: true),
primaryRatio: item.primaryImageAspectRatio,
dateOfBirth: item.premiereDate,
birthPlace: item.productionLocations ?? [],
movies: [],
series: [],
);
}
int? get age {
if (dateOfBirth == null) return null;
final today = DateTime.now();
final months = today.month - dateOfBirth!.month;
if (months < 0) {
return (dateOfBirth!.year - (DateTime.now().year - 1)).abs();
} else {
return (dateOfBirth!.year - DateTime.now().year).abs();
}
}
}

View file

@ -0,0 +1,281 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
part of 'person_model.dart';
class PersonModelMapper extends SubClassMapperBase<PersonModel> {
PersonModelMapper._();
static PersonModelMapper? _instance;
static PersonModelMapper ensureInitialized() {
if (_instance == null) {
MapperContainer.globals.use(_instance = PersonModelMapper._());
ItemBaseModelMapper.ensureInitialized().addSubMapper(_instance!);
MovieModelMapper.ensureInitialized();
SeriesModelMapper.ensureInitialized();
OverviewModelMapper.ensureInitialized();
UserDataMapper.ensureInitialized();
}
return _instance!;
}
@override
final String id = 'PersonModel';
static DateTime? _$dateOfBirth(PersonModel v) => v.dateOfBirth;
static const Field<PersonModel, DateTime> _f$dateOfBirth =
Field('dateOfBirth', _$dateOfBirth, opt: true);
static List<String> _$birthPlace(PersonModel v) => v.birthPlace;
static const Field<PersonModel, List<String>> _f$birthPlace =
Field('birthPlace', _$birthPlace);
static List<MovieModel> _$movies(PersonModel v) => v.movies;
static const Field<PersonModel, List<MovieModel>> _f$movies =
Field('movies', _$movies);
static List<SeriesModel> _$series(PersonModel v) => v.series;
static const Field<PersonModel, List<SeriesModel>> _f$series =
Field('series', _$series);
static String _$name(PersonModel v) => v.name;
static const Field<PersonModel, String> _f$name = Field('name', _$name);
static String _$id(PersonModel v) => v.id;
static const Field<PersonModel, String> _f$id = Field('id', _$id);
static OverviewModel _$overview(PersonModel v) => v.overview;
static const Field<PersonModel, OverviewModel> _f$overview =
Field('overview', _$overview);
static String? _$parentId(PersonModel v) => v.parentId;
static const Field<PersonModel, String> _f$parentId =
Field('parentId', _$parentId);
static String? _$playlistId(PersonModel v) => v.playlistId;
static const Field<PersonModel, String> _f$playlistId =
Field('playlistId', _$playlistId);
static ImagesData? _$images(PersonModel v) => v.images;
static const Field<PersonModel, ImagesData> _f$images =
Field('images', _$images);
static int? _$childCount(PersonModel v) => v.childCount;
static const Field<PersonModel, int> _f$childCount =
Field('childCount', _$childCount);
static double? _$primaryRatio(PersonModel v) => v.primaryRatio;
static const Field<PersonModel, double> _f$primaryRatio =
Field('primaryRatio', _$primaryRatio);
static UserData _$userData(PersonModel v) => v.userData;
static const Field<PersonModel, UserData> _f$userData =
Field('userData', _$userData);
static bool? _$canDownload(PersonModel v) => v.canDownload;
static const Field<PersonModel, bool> _f$canDownload =
Field('canDownload', _$canDownload, opt: true);
static bool? _$canDelete(PersonModel v) => v.canDelete;
static const Field<PersonModel, bool> _f$canDelete =
Field('canDelete', _$canDelete, opt: true);
static BaseItemKind? _$jellyType(PersonModel v) => v.jellyType;
static const Field<PersonModel, BaseItemKind> _f$jellyType =
Field('jellyType', _$jellyType, opt: true);
@override
final MappableFields<PersonModel> fields = const {
#dateOfBirth: _f$dateOfBirth,
#birthPlace: _f$birthPlace,
#movies: _f$movies,
#series: _f$series,
#name: _f$name,
#id: _f$id,
#overview: _f$overview,
#parentId: _f$parentId,
#playlistId: _f$playlistId,
#images: _f$images,
#childCount: _f$childCount,
#primaryRatio: _f$primaryRatio,
#userData: _f$userData,
#canDownload: _f$canDownload,
#canDelete: _f$canDelete,
#jellyType: _f$jellyType,
};
@override
final bool ignoreNull = true;
@override
final String discriminatorKey = 'type';
@override
final dynamic discriminatorValue = 'PersonModel';
@override
late final ClassMapperBase superMapper =
ItemBaseModelMapper.ensureInitialized();
static PersonModel _instantiate(DecodingData data) {
return PersonModel(
dateOfBirth: data.dec(_f$dateOfBirth),
birthPlace: data.dec(_f$birthPlace),
movies: data.dec(_f$movies),
series: data.dec(_f$series),
name: data.dec(_f$name),
id: data.dec(_f$id),
overview: data.dec(_f$overview),
parentId: data.dec(_f$parentId),
playlistId: data.dec(_f$playlistId),
images: data.dec(_f$images),
childCount: data.dec(_f$childCount),
primaryRatio: data.dec(_f$primaryRatio),
userData: data.dec(_f$userData),
canDownload: data.dec(_f$canDownload),
canDelete: data.dec(_f$canDelete),
jellyType: data.dec(_f$jellyType));
}
@override
final Function instantiate = _instantiate;
static PersonModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<PersonModel>(map);
}
static PersonModel fromJson(String json) {
return ensureInitialized().decodeJson<PersonModel>(json);
}
}
mixin PersonModelMappable {
String toJson() {
return PersonModelMapper.ensureInitialized()
.encodeJson<PersonModel>(this as PersonModel);
}
Map<String, dynamic> toMap() {
return PersonModelMapper.ensureInitialized()
.encodeMap<PersonModel>(this as PersonModel);
}
PersonModelCopyWith<PersonModel, PersonModel, PersonModel> get copyWith =>
_PersonModelCopyWithImpl(this as PersonModel, $identity, $identity);
@override
String toString() {
return PersonModelMapper.ensureInitialized()
.stringifyValue(this as PersonModel);
}
}
extension PersonModelValueCopy<$R, $Out>
on ObjectCopyWith<$R, PersonModel, $Out> {
PersonModelCopyWith<$R, PersonModel, $Out> get $asPersonModel =>
$base.as((v, t, t2) => _PersonModelCopyWithImpl(v, t, t2));
}
abstract class PersonModelCopyWith<$R, $In extends PersonModel, $Out>
implements ItemBaseModelCopyWith<$R, $In, $Out> {
ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>> get birthPlace;
ListCopyWith<$R, MovieModel, MovieModelCopyWith<$R, MovieModel, MovieModel>>
get movies;
ListCopyWith<$R, SeriesModel,
SeriesModelCopyWith<$R, SeriesModel, SeriesModel>> get series;
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview;
@override
UserDataCopyWith<$R, UserData, UserData> get userData;
@override
$R call(
{DateTime? dateOfBirth,
List<String>? birthPlace,
List<MovieModel>? movies,
List<SeriesModel>? series,
String? name,
String? id,
OverviewModel? overview,
String? parentId,
String? playlistId,
ImagesData? images,
int? childCount,
double? primaryRatio,
UserData? userData,
bool? canDownload,
bool? canDelete,
BaseItemKind? jellyType});
PersonModelCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t);
}
class _PersonModelCopyWithImpl<$R, $Out>
extends ClassCopyWithBase<$R, PersonModel, $Out>
implements PersonModelCopyWith<$R, PersonModel, $Out> {
_PersonModelCopyWithImpl(super.value, super.then, super.then2);
@override
late final ClassMapperBase<PersonModel> $mapper =
PersonModelMapper.ensureInitialized();
@override
ListCopyWith<$R, String, ObjectCopyWith<$R, String, String>> get birthPlace =>
ListCopyWith($value.birthPlace, (v, t) => ObjectCopyWith(v, $identity, t),
(v) => call(birthPlace: v));
@override
ListCopyWith<$R, MovieModel, MovieModelCopyWith<$R, MovieModel, MovieModel>>
get movies => ListCopyWith($value.movies, (v, t) => v.copyWith.$chain(t),
(v) => call(movies: v));
@override
ListCopyWith<$R, SeriesModel,
SeriesModelCopyWith<$R, SeriesModel, SeriesModel>>
get series => ListCopyWith($value.series, (v, t) => v.copyWith.$chain(t),
(v) => call(series: v));
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview =>
$value.overview.copyWith.$chain((v) => call(overview: v));
@override
UserDataCopyWith<$R, UserData, UserData> get userData =>
$value.userData.copyWith.$chain((v) => call(userData: v));
@override
$R call(
{Object? dateOfBirth = $none,
List<String>? birthPlace,
List<MovieModel>? movies,
List<SeriesModel>? series,
String? name,
String? id,
OverviewModel? overview,
Object? parentId = $none,
Object? playlistId = $none,
Object? images = $none,
Object? childCount = $none,
Object? primaryRatio = $none,
UserData? userData,
Object? canDownload = $none,
Object? canDelete = $none,
Object? jellyType = $none}) =>
$apply(FieldCopyWithData({
if (dateOfBirth != $none) #dateOfBirth: dateOfBirth,
if (birthPlace != null) #birthPlace: birthPlace,
if (movies != null) #movies: movies,
if (series != null) #series: series,
if (name != null) #name: name,
if (id != null) #id: id,
if (overview != null) #overview: overview,
if (parentId != $none) #parentId: parentId,
if (playlistId != $none) #playlistId: playlistId,
if (images != $none) #images: images,
if (childCount != $none) #childCount: childCount,
if (primaryRatio != $none) #primaryRatio: primaryRatio,
if (userData != null) #userData: userData,
if (canDownload != $none) #canDownload: canDownload,
if (canDelete != $none) #canDelete: canDelete,
if (jellyType != $none) #jellyType: jellyType
}));
@override
PersonModel $make(CopyWithData data) => PersonModel(
dateOfBirth: data.get(#dateOfBirth, or: $value.dateOfBirth),
birthPlace: data.get(#birthPlace, or: $value.birthPlace),
movies: data.get(#movies, or: $value.movies),
series: data.get(#series, or: $value.series),
name: data.get(#name, or: $value.name),
id: data.get(#id, or: $value.id),
overview: data.get(#overview, or: $value.overview),
parentId: data.get(#parentId, or: $value.parentId),
playlistId: data.get(#playlistId, or: $value.playlistId),
images: data.get(#images, or: $value.images),
childCount: data.get(#childCount, or: $value.childCount),
primaryRatio: data.get(#primaryRatio, or: $value.primaryRatio),
userData: data.get(#userData, or: $value.userData),
canDownload: data.get(#canDownload, or: $value.canDownload),
canDelete: data.get(#canDelete, or: $value.canDelete),
jellyType: data.get(#jellyType, or: $value.jellyType));
@override
PersonModelCopyWith<$R2, PersonModel, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t) =>
_PersonModelCopyWithImpl($value, $cast, t);
}

View file

@ -0,0 +1,154 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/overview_model.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/util/refresh_state.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'photos_model.mapper.dart';
@MappableClass()
class PhotoAlbumModel extends ItemBaseModel with PhotoAlbumModelMappable {
final List<ItemBaseModel> photos;
const PhotoAlbumModel({
required this.photos,
required super.name,
required super.id,
required super.overview,
required super.parentId,
required super.playlistId,
required super.images,
required super.childCount,
required super.primaryRatio,
required super.userData,
super.canDelete,
super.canDownload,
super.jellyType,
});
@override
bool get unWatched => userData.unPlayedItemCount != 0;
@override
bool get playAble => true;
@override
ItemBaseModel get parentBaseModel => copyWith(id: parentId);
factory PhotoAlbumModel.fromBaseDto(dto.BaseItemDto item, Ref ref) {
return PhotoAlbumModel(
name: item.name ?? "",
id: item.id ?? "",
childCount: item.childCount,
overview: OverviewModel.fromBaseItemDto(item, ref),
userData: UserData.fromDto(item.userData),
parentId: item.parentId,
playlistId: item.playlistItemId,
images: ImagesData.fromBaseItem(item, ref),
primaryRatio: item.primaryImageAspectRatio,
canDelete: item.canDelete,
canDownload: item.canDownload,
photos: [],
);
}
}
@MappableClass()
class PhotoModel extends ItemBaseModel with PhotoModelMappable {
final String? albumId;
final DateTime? dateTaken;
final ImagesData? thumbnail;
final FladderItemType internalType;
const PhotoModel({
required this.albumId,
required this.dateTaken,
required this.thumbnail,
required this.internalType,
required super.name,
required super.id,
required super.overview,
required super.parentId,
required super.playlistId,
required super.images,
required super.childCount,
required super.primaryRatio,
required super.userData,
required super.canDownload,
required super.canDelete,
super.jellyType,
});
@override
PhotoAlbumModel get parentBaseModel => PhotoAlbumModel(
photos: [],
name: "",
id: parentId ?? "",
overview: overview,
parentId: parentId,
playlistId: playlistId,
images: images,
childCount: childCount,
primaryRatio: primaryRatio,
userData: userData,
);
@override
ImagesData? get getPosters => thumbnail;
@override
bool get galleryItem => switch (internalType) {
FladderItemType.photo => albumId?.isNotEmpty == true,
FladderItemType.video => parentId?.isNotEmpty == true,
_ => false,
};
@override
bool get unWatched => false;
factory PhotoModel.fromBaseDto(dto.BaseItemDto item, Ref ref) {
return PhotoModel(
name: item.name ?? "",
id: item.id ?? "",
childCount: item.childCount,
overview: OverviewModel.fromBaseItemDto(item, ref),
userData: UserData.fromDto(item.userData),
parentId: item.parentId,
playlistId: item.playlistItemId,
primaryRatio: double.tryParse(item.aspectRatio ?? "") ?? item.primaryImageAspectRatio ?? 1.0,
dateTaken: item.dateCreated,
albumId: item.albumId,
thumbnail: ImagesData.fromBaseItem(item, ref),
images: ImagesData.fromBaseItem(item, ref, getOriginalSize: true),
canDelete: item.canDelete,
canDownload: item.canDownload,
internalType: switch (item.type) {
BaseItemKind.video => FladderItemType.video,
_ => FladderItemType.photo,
},
);
}
String downloadPath(WidgetRef ref) {
return "${ref.read(userProvider)?.server}/Items/$id/Download?api_key=${ref.read(userProvider)?.credentials.token}";
}
Future<void> navigateToAlbum(BuildContext context) async {
if ((albumId ?? parentId) == null) {
fladderSnackbar(context, title: context.localized.notPartOfAlbum);
return;
}
await parentBaseModel.navigateTo(context);
if (context.mounted) context.refreshData();
}
}

View file

@ -0,0 +1,498 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
part of 'photos_model.dart';
class PhotoAlbumModelMapper extends SubClassMapperBase<PhotoAlbumModel> {
PhotoAlbumModelMapper._();
static PhotoAlbumModelMapper? _instance;
static PhotoAlbumModelMapper ensureInitialized() {
if (_instance == null) {
MapperContainer.globals.use(_instance = PhotoAlbumModelMapper._());
ItemBaseModelMapper.ensureInitialized().addSubMapper(_instance!);
ItemBaseModelMapper.ensureInitialized();
OverviewModelMapper.ensureInitialized();
UserDataMapper.ensureInitialized();
}
return _instance!;
}
@override
final String id = 'PhotoAlbumModel';
static List<ItemBaseModel> _$photos(PhotoAlbumModel v) => v.photos;
static const Field<PhotoAlbumModel, List<ItemBaseModel>> _f$photos =
Field('photos', _$photos);
static String _$name(PhotoAlbumModel v) => v.name;
static const Field<PhotoAlbumModel, String> _f$name = Field('name', _$name);
static String _$id(PhotoAlbumModel v) => v.id;
static const Field<PhotoAlbumModel, String> _f$id = Field('id', _$id);
static OverviewModel _$overview(PhotoAlbumModel v) => v.overview;
static const Field<PhotoAlbumModel, OverviewModel> _f$overview =
Field('overview', _$overview);
static String? _$parentId(PhotoAlbumModel v) => v.parentId;
static const Field<PhotoAlbumModel, String> _f$parentId =
Field('parentId', _$parentId);
static String? _$playlistId(PhotoAlbumModel v) => v.playlistId;
static const Field<PhotoAlbumModel, String> _f$playlistId =
Field('playlistId', _$playlistId);
static ImagesData? _$images(PhotoAlbumModel v) => v.images;
static const Field<PhotoAlbumModel, ImagesData> _f$images =
Field('images', _$images);
static int? _$childCount(PhotoAlbumModel v) => v.childCount;
static const Field<PhotoAlbumModel, int> _f$childCount =
Field('childCount', _$childCount);
static double? _$primaryRatio(PhotoAlbumModel v) => v.primaryRatio;
static const Field<PhotoAlbumModel, double> _f$primaryRatio =
Field('primaryRatio', _$primaryRatio);
static UserData _$userData(PhotoAlbumModel v) => v.userData;
static const Field<PhotoAlbumModel, UserData> _f$userData =
Field('userData', _$userData);
static bool? _$canDelete(PhotoAlbumModel v) => v.canDelete;
static const Field<PhotoAlbumModel, bool> _f$canDelete =
Field('canDelete', _$canDelete, opt: true);
static bool? _$canDownload(PhotoAlbumModel v) => v.canDownload;
static const Field<PhotoAlbumModel, bool> _f$canDownload =
Field('canDownload', _$canDownload, opt: true);
static dto.BaseItemKind? _$jellyType(PhotoAlbumModel v) => v.jellyType;
static const Field<PhotoAlbumModel, dto.BaseItemKind> _f$jellyType =
Field('jellyType', _$jellyType, opt: true);
@override
final MappableFields<PhotoAlbumModel> fields = const {
#photos: _f$photos,
#name: _f$name,
#id: _f$id,
#overview: _f$overview,
#parentId: _f$parentId,
#playlistId: _f$playlistId,
#images: _f$images,
#childCount: _f$childCount,
#primaryRatio: _f$primaryRatio,
#userData: _f$userData,
#canDelete: _f$canDelete,
#canDownload: _f$canDownload,
#jellyType: _f$jellyType,
};
@override
final bool ignoreNull = true;
@override
final String discriminatorKey = 'type';
@override
final dynamic discriminatorValue = 'PhotoAlbumModel';
@override
late final ClassMapperBase superMapper =
ItemBaseModelMapper.ensureInitialized();
static PhotoAlbumModel _instantiate(DecodingData data) {
return PhotoAlbumModel(
photos: data.dec(_f$photos),
name: data.dec(_f$name),
id: data.dec(_f$id),
overview: data.dec(_f$overview),
parentId: data.dec(_f$parentId),
playlistId: data.dec(_f$playlistId),
images: data.dec(_f$images),
childCount: data.dec(_f$childCount),
primaryRatio: data.dec(_f$primaryRatio),
userData: data.dec(_f$userData),
canDelete: data.dec(_f$canDelete),
canDownload: data.dec(_f$canDownload),
jellyType: data.dec(_f$jellyType));
}
@override
final Function instantiate = _instantiate;
static PhotoAlbumModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<PhotoAlbumModel>(map);
}
static PhotoAlbumModel fromJson(String json) {
return ensureInitialized().decodeJson<PhotoAlbumModel>(json);
}
}
mixin PhotoAlbumModelMappable {
String toJson() {
return PhotoAlbumModelMapper.ensureInitialized()
.encodeJson<PhotoAlbumModel>(this as PhotoAlbumModel);
}
Map<String, dynamic> toMap() {
return PhotoAlbumModelMapper.ensureInitialized()
.encodeMap<PhotoAlbumModel>(this as PhotoAlbumModel);
}
PhotoAlbumModelCopyWith<PhotoAlbumModel, PhotoAlbumModel, PhotoAlbumModel>
get copyWith => _PhotoAlbumModelCopyWithImpl(
this as PhotoAlbumModel, $identity, $identity);
@override
String toString() {
return PhotoAlbumModelMapper.ensureInitialized()
.stringifyValue(this as PhotoAlbumModel);
}
}
extension PhotoAlbumModelValueCopy<$R, $Out>
on ObjectCopyWith<$R, PhotoAlbumModel, $Out> {
PhotoAlbumModelCopyWith<$R, PhotoAlbumModel, $Out> get $asPhotoAlbumModel =>
$base.as((v, t, t2) => _PhotoAlbumModelCopyWithImpl(v, t, t2));
}
abstract class PhotoAlbumModelCopyWith<$R, $In extends PhotoAlbumModel, $Out>
implements ItemBaseModelCopyWith<$R, $In, $Out> {
ListCopyWith<$R, ItemBaseModel,
ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>> get photos;
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview;
@override
UserDataCopyWith<$R, UserData, UserData> get userData;
@override
$R call(
{List<ItemBaseModel>? photos,
String? name,
String? id,
OverviewModel? overview,
String? parentId,
String? playlistId,
ImagesData? images,
int? childCount,
double? primaryRatio,
UserData? userData,
bool? canDelete,
bool? canDownload,
dto.BaseItemKind? jellyType});
PhotoAlbumModelCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t);
}
class _PhotoAlbumModelCopyWithImpl<$R, $Out>
extends ClassCopyWithBase<$R, PhotoAlbumModel, $Out>
implements PhotoAlbumModelCopyWith<$R, PhotoAlbumModel, $Out> {
_PhotoAlbumModelCopyWithImpl(super.value, super.then, super.then2);
@override
late final ClassMapperBase<PhotoAlbumModel> $mapper =
PhotoAlbumModelMapper.ensureInitialized();
@override
ListCopyWith<$R, ItemBaseModel,
ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>>
get photos => ListCopyWith($value.photos, (v, t) => v.copyWith.$chain(t),
(v) => call(photos: v));
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview =>
$value.overview.copyWith.$chain((v) => call(overview: v));
@override
UserDataCopyWith<$R, UserData, UserData> get userData =>
$value.userData.copyWith.$chain((v) => call(userData: v));
@override
$R call(
{List<ItemBaseModel>? photos,
String? name,
String? id,
OverviewModel? overview,
Object? parentId = $none,
Object? playlistId = $none,
Object? images = $none,
Object? childCount = $none,
Object? primaryRatio = $none,
UserData? userData,
Object? canDelete = $none,
Object? canDownload = $none,
Object? jellyType = $none}) =>
$apply(FieldCopyWithData({
if (photos != null) #photos: photos,
if (name != null) #name: name,
if (id != null) #id: id,
if (overview != null) #overview: overview,
if (parentId != $none) #parentId: parentId,
if (playlistId != $none) #playlistId: playlistId,
if (images != $none) #images: images,
if (childCount != $none) #childCount: childCount,
if (primaryRatio != $none) #primaryRatio: primaryRatio,
if (userData != null) #userData: userData,
if (canDelete != $none) #canDelete: canDelete,
if (canDownload != $none) #canDownload: canDownload,
if (jellyType != $none) #jellyType: jellyType
}));
@override
PhotoAlbumModel $make(CopyWithData data) => PhotoAlbumModel(
photos: data.get(#photos, or: $value.photos),
name: data.get(#name, or: $value.name),
id: data.get(#id, or: $value.id),
overview: data.get(#overview, or: $value.overview),
parentId: data.get(#parentId, or: $value.parentId),
playlistId: data.get(#playlistId, or: $value.playlistId),
images: data.get(#images, or: $value.images),
childCount: data.get(#childCount, or: $value.childCount),
primaryRatio: data.get(#primaryRatio, or: $value.primaryRatio),
userData: data.get(#userData, or: $value.userData),
canDelete: data.get(#canDelete, or: $value.canDelete),
canDownload: data.get(#canDownload, or: $value.canDownload),
jellyType: data.get(#jellyType, or: $value.jellyType));
@override
PhotoAlbumModelCopyWith<$R2, PhotoAlbumModel, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t) =>
_PhotoAlbumModelCopyWithImpl($value, $cast, t);
}
class PhotoModelMapper extends SubClassMapperBase<PhotoModel> {
PhotoModelMapper._();
static PhotoModelMapper? _instance;
static PhotoModelMapper ensureInitialized() {
if (_instance == null) {
MapperContainer.globals.use(_instance = PhotoModelMapper._());
ItemBaseModelMapper.ensureInitialized().addSubMapper(_instance!);
OverviewModelMapper.ensureInitialized();
UserDataMapper.ensureInitialized();
}
return _instance!;
}
@override
final String id = 'PhotoModel';
static String? _$albumId(PhotoModel v) => v.albumId;
static const Field<PhotoModel, String> _f$albumId =
Field('albumId', _$albumId);
static DateTime? _$dateTaken(PhotoModel v) => v.dateTaken;
static const Field<PhotoModel, DateTime> _f$dateTaken =
Field('dateTaken', _$dateTaken);
static ImagesData? _$thumbnail(PhotoModel v) => v.thumbnail;
static const Field<PhotoModel, ImagesData> _f$thumbnail =
Field('thumbnail', _$thumbnail);
static FladderItemType _$internalType(PhotoModel v) => v.internalType;
static const Field<PhotoModel, FladderItemType> _f$internalType =
Field('internalType', _$internalType);
static String _$name(PhotoModel v) => v.name;
static const Field<PhotoModel, String> _f$name = Field('name', _$name);
static String _$id(PhotoModel v) => v.id;
static const Field<PhotoModel, String> _f$id = Field('id', _$id);
static OverviewModel _$overview(PhotoModel v) => v.overview;
static const Field<PhotoModel, OverviewModel> _f$overview =
Field('overview', _$overview);
static String? _$parentId(PhotoModel v) => v.parentId;
static const Field<PhotoModel, String> _f$parentId =
Field('parentId', _$parentId);
static String? _$playlistId(PhotoModel v) => v.playlistId;
static const Field<PhotoModel, String> _f$playlistId =
Field('playlistId', _$playlistId);
static ImagesData? _$images(PhotoModel v) => v.images;
static const Field<PhotoModel, ImagesData> _f$images =
Field('images', _$images);
static int? _$childCount(PhotoModel v) => v.childCount;
static const Field<PhotoModel, int> _f$childCount =
Field('childCount', _$childCount);
static double? _$primaryRatio(PhotoModel v) => v.primaryRatio;
static const Field<PhotoModel, double> _f$primaryRatio =
Field('primaryRatio', _$primaryRatio);
static UserData _$userData(PhotoModel v) => v.userData;
static const Field<PhotoModel, UserData> _f$userData =
Field('userData', _$userData);
static bool? _$canDownload(PhotoModel v) => v.canDownload;
static const Field<PhotoModel, bool> _f$canDownload =
Field('canDownload', _$canDownload);
static bool? _$canDelete(PhotoModel v) => v.canDelete;
static const Field<PhotoModel, bool> _f$canDelete =
Field('canDelete', _$canDelete);
static dto.BaseItemKind? _$jellyType(PhotoModel v) => v.jellyType;
static const Field<PhotoModel, dto.BaseItemKind> _f$jellyType =
Field('jellyType', _$jellyType, opt: true);
@override
final MappableFields<PhotoModel> fields = const {
#albumId: _f$albumId,
#dateTaken: _f$dateTaken,
#thumbnail: _f$thumbnail,
#internalType: _f$internalType,
#name: _f$name,
#id: _f$id,
#overview: _f$overview,
#parentId: _f$parentId,
#playlistId: _f$playlistId,
#images: _f$images,
#childCount: _f$childCount,
#primaryRatio: _f$primaryRatio,
#userData: _f$userData,
#canDownload: _f$canDownload,
#canDelete: _f$canDelete,
#jellyType: _f$jellyType,
};
@override
final bool ignoreNull = true;
@override
final String discriminatorKey = 'type';
@override
final dynamic discriminatorValue = 'PhotoModel';
@override
late final ClassMapperBase superMapper =
ItemBaseModelMapper.ensureInitialized();
static PhotoModel _instantiate(DecodingData data) {
return PhotoModel(
albumId: data.dec(_f$albumId),
dateTaken: data.dec(_f$dateTaken),
thumbnail: data.dec(_f$thumbnail),
internalType: data.dec(_f$internalType),
name: data.dec(_f$name),
id: data.dec(_f$id),
overview: data.dec(_f$overview),
parentId: data.dec(_f$parentId),
playlistId: data.dec(_f$playlistId),
images: data.dec(_f$images),
childCount: data.dec(_f$childCount),
primaryRatio: data.dec(_f$primaryRatio),
userData: data.dec(_f$userData),
canDownload: data.dec(_f$canDownload),
canDelete: data.dec(_f$canDelete),
jellyType: data.dec(_f$jellyType));
}
@override
final Function instantiate = _instantiate;
static PhotoModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<PhotoModel>(map);
}
static PhotoModel fromJson(String json) {
return ensureInitialized().decodeJson<PhotoModel>(json);
}
}
mixin PhotoModelMappable {
String toJson() {
return PhotoModelMapper.ensureInitialized()
.encodeJson<PhotoModel>(this as PhotoModel);
}
Map<String, dynamic> toMap() {
return PhotoModelMapper.ensureInitialized()
.encodeMap<PhotoModel>(this as PhotoModel);
}
PhotoModelCopyWith<PhotoModel, PhotoModel, PhotoModel> get copyWith =>
_PhotoModelCopyWithImpl(this as PhotoModel, $identity, $identity);
@override
String toString() {
return PhotoModelMapper.ensureInitialized()
.stringifyValue(this as PhotoModel);
}
}
extension PhotoModelValueCopy<$R, $Out>
on ObjectCopyWith<$R, PhotoModel, $Out> {
PhotoModelCopyWith<$R, PhotoModel, $Out> get $asPhotoModel =>
$base.as((v, t, t2) => _PhotoModelCopyWithImpl(v, t, t2));
}
abstract class PhotoModelCopyWith<$R, $In extends PhotoModel, $Out>
implements ItemBaseModelCopyWith<$R, $In, $Out> {
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview;
@override
UserDataCopyWith<$R, UserData, UserData> get userData;
@override
$R call(
{String? albumId,
DateTime? dateTaken,
ImagesData? thumbnail,
FladderItemType? internalType,
String? name,
String? id,
OverviewModel? overview,
String? parentId,
String? playlistId,
ImagesData? images,
int? childCount,
double? primaryRatio,
UserData? userData,
bool? canDownload,
bool? canDelete,
dto.BaseItemKind? jellyType});
PhotoModelCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t);
}
class _PhotoModelCopyWithImpl<$R, $Out>
extends ClassCopyWithBase<$R, PhotoModel, $Out>
implements PhotoModelCopyWith<$R, PhotoModel, $Out> {
_PhotoModelCopyWithImpl(super.value, super.then, super.then2);
@override
late final ClassMapperBase<PhotoModel> $mapper =
PhotoModelMapper.ensureInitialized();
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview =>
$value.overview.copyWith.$chain((v) => call(overview: v));
@override
UserDataCopyWith<$R, UserData, UserData> get userData =>
$value.userData.copyWith.$chain((v) => call(userData: v));
@override
$R call(
{Object? albumId = $none,
Object? dateTaken = $none,
Object? thumbnail = $none,
FladderItemType? internalType,
String? name,
String? id,
OverviewModel? overview,
Object? parentId = $none,
Object? playlistId = $none,
Object? images = $none,
Object? childCount = $none,
Object? primaryRatio = $none,
UserData? userData,
Object? canDownload = $none,
Object? canDelete = $none,
Object? jellyType = $none}) =>
$apply(FieldCopyWithData({
if (albumId != $none) #albumId: albumId,
if (dateTaken != $none) #dateTaken: dateTaken,
if (thumbnail != $none) #thumbnail: thumbnail,
if (internalType != null) #internalType: internalType,
if (name != null) #name: name,
if (id != null) #id: id,
if (overview != null) #overview: overview,
if (parentId != $none) #parentId: parentId,
if (playlistId != $none) #playlistId: playlistId,
if (images != $none) #images: images,
if (childCount != $none) #childCount: childCount,
if (primaryRatio != $none) #primaryRatio: primaryRatio,
if (userData != null) #userData: userData,
if (canDownload != $none) #canDownload: canDownload,
if (canDelete != $none) #canDelete: canDelete,
if (jellyType != $none) #jellyType: jellyType
}));
@override
PhotoModel $make(CopyWithData data) => PhotoModel(
albumId: data.get(#albumId, or: $value.albumId),
dateTaken: data.get(#dateTaken, or: $value.dateTaken),
thumbnail: data.get(#thumbnail, or: $value.thumbnail),
internalType: data.get(#internalType, or: $value.internalType),
name: data.get(#name, or: $value.name),
id: data.get(#id, or: $value.id),
overview: data.get(#overview, or: $value.overview),
parentId: data.get(#parentId, or: $value.parentId),
playlistId: data.get(#playlistId, or: $value.playlistId),
images: data.get(#images, or: $value.images),
childCount: data.get(#childCount, or: $value.childCount),
primaryRatio: data.get(#primaryRatio, or: $value.primaryRatio),
userData: data.get(#userData, or: $value.userData),
canDownload: data.get(#canDownload, or: $value.canDownload),
canDelete: data.get(#canDelete, or: $value.canDelete),
jellyType: data.get(#jellyType, or: $value.jellyType));
@override
PhotoModelCopyWith<$R2, PhotoModel, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t) =>
_PhotoModelCopyWithImpl($value, $cast, t);
}

View file

@ -0,0 +1,98 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:collection/collection.dart';
import 'package:fladder/models/items/overview_model.dart';
import 'package:fladder/models/items/series_model.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'season_model.mapper.dart';
@MappableClass()
class SeasonModel extends ItemBaseModel with SeasonModelMappable {
final ImagesData? parentImages;
final String seasonName;
final List<EpisodeModel> episodes;
final int episodeCount;
final String seriesId;
final String seriesName;
const SeasonModel({
required this.parentImages,
required this.seasonName,
this.episodes = const [],
required this.episodeCount,
required this.seriesId,
required this.seriesName,
required super.name,
required super.id,
required super.overview,
required super.parentId,
required super.playlistId,
required super.images,
required super.childCount,
required super.primaryRatio,
required super.userData,
required super.canDelete,
required super.canDownload,
super.jellyType,
});
factory SeasonModel.fromBaseDto(dto.BaseItemDto item, Ref ref) {
return SeasonModel(
name: item.name ?? "",
id: item.id ?? "",
childCount: item.childCount,
overview: OverviewModel.fromBaseItemDto(item, ref),
userData: UserData.fromDto(item.userData),
parentId: item.seasonId ?? item.parentId,
playlistId: item.playlistItemId,
images: ImagesData.fromBaseItem(item, ref),
primaryRatio: item.primaryImageAspectRatio,
seasonName: item.seasonName ?? "",
episodeCount: item.episodeCount ?? 0,
parentImages: ImagesData.fromBaseItemParent(item, ref, primary: const Size(2000, 2000)),
seriesId: item.seriesId ?? item.parentId ?? item.id ?? "",
canDelete: item.canDelete,
canDownload: item.canDownload,
seriesName: item.seriesName ?? "",
);
}
EpisodeModel? get nextUp {
return episodes.lastWhereOrNull((element) => element.userData.progress > 0) ??
episodes.firstWhereOrNull((element) => element.userData.played == false);
}
@override
ImagesData? get getPosters => images ?? parentImages;
String localizedName(BuildContext context) => name.replaceFirst("Season", context.localized.season(1));
@override
SeriesModel get parentBaseModel => SeriesModel(
originalTitle: '',
sortName: '',
status: "",
name: seriesName,
id: parentId ?? "",
playlistId: playlistId,
overview: overview,
parentId: parentId,
images: images,
childCount: childCount,
primaryRatio: primaryRatio,
userData: UserData(),
);
static List<SeasonModel> seasonsFromDto(List<dto.BaseItemDto>? dto, Ref ref) {
return dto?.map((e) => SeasonModel.fromBaseDto(e, ref)).toList() ?? [];
}
}

View file

@ -0,0 +1,287 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
part of 'season_model.dart';
class SeasonModelMapper extends SubClassMapperBase<SeasonModel> {
SeasonModelMapper._();
static SeasonModelMapper? _instance;
static SeasonModelMapper ensureInitialized() {
if (_instance == null) {
MapperContainer.globals.use(_instance = SeasonModelMapper._());
ItemBaseModelMapper.ensureInitialized().addSubMapper(_instance!);
EpisodeModelMapper.ensureInitialized();
OverviewModelMapper.ensureInitialized();
UserDataMapper.ensureInitialized();
}
return _instance!;
}
@override
final String id = 'SeasonModel';
static ImagesData? _$parentImages(SeasonModel v) => v.parentImages;
static const Field<SeasonModel, ImagesData> _f$parentImages =
Field('parentImages', _$parentImages);
static String _$seasonName(SeasonModel v) => v.seasonName;
static const Field<SeasonModel, String> _f$seasonName =
Field('seasonName', _$seasonName);
static List<EpisodeModel> _$episodes(SeasonModel v) => v.episodes;
static const Field<SeasonModel, List<EpisodeModel>> _f$episodes =
Field('episodes', _$episodes, opt: true, def: const []);
static int _$episodeCount(SeasonModel v) => v.episodeCount;
static const Field<SeasonModel, int> _f$episodeCount =
Field('episodeCount', _$episodeCount);
static String _$seriesId(SeasonModel v) => v.seriesId;
static const Field<SeasonModel, String> _f$seriesId =
Field('seriesId', _$seriesId);
static String _$seriesName(SeasonModel v) => v.seriesName;
static const Field<SeasonModel, String> _f$seriesName =
Field('seriesName', _$seriesName);
static String _$name(SeasonModel v) => v.name;
static const Field<SeasonModel, String> _f$name = Field('name', _$name);
static String _$id(SeasonModel v) => v.id;
static const Field<SeasonModel, String> _f$id = Field('id', _$id);
static OverviewModel _$overview(SeasonModel v) => v.overview;
static const Field<SeasonModel, OverviewModel> _f$overview =
Field('overview', _$overview);
static String? _$parentId(SeasonModel v) => v.parentId;
static const Field<SeasonModel, String> _f$parentId =
Field('parentId', _$parentId);
static String? _$playlistId(SeasonModel v) => v.playlistId;
static const Field<SeasonModel, String> _f$playlistId =
Field('playlistId', _$playlistId);
static ImagesData? _$images(SeasonModel v) => v.images;
static const Field<SeasonModel, ImagesData> _f$images =
Field('images', _$images);
static int? _$childCount(SeasonModel v) => v.childCount;
static const Field<SeasonModel, int> _f$childCount =
Field('childCount', _$childCount);
static double? _$primaryRatio(SeasonModel v) => v.primaryRatio;
static const Field<SeasonModel, double> _f$primaryRatio =
Field('primaryRatio', _$primaryRatio);
static UserData _$userData(SeasonModel v) => v.userData;
static const Field<SeasonModel, UserData> _f$userData =
Field('userData', _$userData);
static bool? _$canDelete(SeasonModel v) => v.canDelete;
static const Field<SeasonModel, bool> _f$canDelete =
Field('canDelete', _$canDelete);
static bool? _$canDownload(SeasonModel v) => v.canDownload;
static const Field<SeasonModel, bool> _f$canDownload =
Field('canDownload', _$canDownload);
static dto.BaseItemKind? _$jellyType(SeasonModel v) => v.jellyType;
static const Field<SeasonModel, dto.BaseItemKind> _f$jellyType =
Field('jellyType', _$jellyType, opt: true);
@override
final MappableFields<SeasonModel> fields = const {
#parentImages: _f$parentImages,
#seasonName: _f$seasonName,
#episodes: _f$episodes,
#episodeCount: _f$episodeCount,
#seriesId: _f$seriesId,
#seriesName: _f$seriesName,
#name: _f$name,
#id: _f$id,
#overview: _f$overview,
#parentId: _f$parentId,
#playlistId: _f$playlistId,
#images: _f$images,
#childCount: _f$childCount,
#primaryRatio: _f$primaryRatio,
#userData: _f$userData,
#canDelete: _f$canDelete,
#canDownload: _f$canDownload,
#jellyType: _f$jellyType,
};
@override
final bool ignoreNull = true;
@override
final String discriminatorKey = 'type';
@override
final dynamic discriminatorValue = 'SeasonModel';
@override
late final ClassMapperBase superMapper =
ItemBaseModelMapper.ensureInitialized();
static SeasonModel _instantiate(DecodingData data) {
return SeasonModel(
parentImages: data.dec(_f$parentImages),
seasonName: data.dec(_f$seasonName),
episodes: data.dec(_f$episodes),
episodeCount: data.dec(_f$episodeCount),
seriesId: data.dec(_f$seriesId),
seriesName: data.dec(_f$seriesName),
name: data.dec(_f$name),
id: data.dec(_f$id),
overview: data.dec(_f$overview),
parentId: data.dec(_f$parentId),
playlistId: data.dec(_f$playlistId),
images: data.dec(_f$images),
childCount: data.dec(_f$childCount),
primaryRatio: data.dec(_f$primaryRatio),
userData: data.dec(_f$userData),
canDelete: data.dec(_f$canDelete),
canDownload: data.dec(_f$canDownload),
jellyType: data.dec(_f$jellyType));
}
@override
final Function instantiate = _instantiate;
static SeasonModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<SeasonModel>(map);
}
static SeasonModel fromJson(String json) {
return ensureInitialized().decodeJson<SeasonModel>(json);
}
}
mixin SeasonModelMappable {
String toJson() {
return SeasonModelMapper.ensureInitialized()
.encodeJson<SeasonModel>(this as SeasonModel);
}
Map<String, dynamic> toMap() {
return SeasonModelMapper.ensureInitialized()
.encodeMap<SeasonModel>(this as SeasonModel);
}
SeasonModelCopyWith<SeasonModel, SeasonModel, SeasonModel> get copyWith =>
_SeasonModelCopyWithImpl(this as SeasonModel, $identity, $identity);
@override
String toString() {
return SeasonModelMapper.ensureInitialized()
.stringifyValue(this as SeasonModel);
}
}
extension SeasonModelValueCopy<$R, $Out>
on ObjectCopyWith<$R, SeasonModel, $Out> {
SeasonModelCopyWith<$R, SeasonModel, $Out> get $asSeasonModel =>
$base.as((v, t, t2) => _SeasonModelCopyWithImpl(v, t, t2));
}
abstract class SeasonModelCopyWith<$R, $In extends SeasonModel, $Out>
implements ItemBaseModelCopyWith<$R, $In, $Out> {
ListCopyWith<$R, EpisodeModel,
EpisodeModelCopyWith<$R, EpisodeModel, EpisodeModel>> get episodes;
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview;
@override
UserDataCopyWith<$R, UserData, UserData> get userData;
@override
$R call(
{ImagesData? parentImages,
String? seasonName,
List<EpisodeModel>? episodes,
int? episodeCount,
String? seriesId,
String? seriesName,
String? name,
String? id,
OverviewModel? overview,
String? parentId,
String? playlistId,
ImagesData? images,
int? childCount,
double? primaryRatio,
UserData? userData,
bool? canDelete,
bool? canDownload,
dto.BaseItemKind? jellyType});
SeasonModelCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t);
}
class _SeasonModelCopyWithImpl<$R, $Out>
extends ClassCopyWithBase<$R, SeasonModel, $Out>
implements SeasonModelCopyWith<$R, SeasonModel, $Out> {
_SeasonModelCopyWithImpl(super.value, super.then, super.then2);
@override
late final ClassMapperBase<SeasonModel> $mapper =
SeasonModelMapper.ensureInitialized();
@override
ListCopyWith<$R, EpisodeModel,
EpisodeModelCopyWith<$R, EpisodeModel, EpisodeModel>>
get episodes => ListCopyWith($value.episodes,
(v, t) => v.copyWith.$chain(t), (v) => call(episodes: v));
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview =>
$value.overview.copyWith.$chain((v) => call(overview: v));
@override
UserDataCopyWith<$R, UserData, UserData> get userData =>
$value.userData.copyWith.$chain((v) => call(userData: v));
@override
$R call(
{Object? parentImages = $none,
String? seasonName,
List<EpisodeModel>? episodes,
int? episodeCount,
String? seriesId,
String? seriesName,
String? name,
String? id,
OverviewModel? overview,
Object? parentId = $none,
Object? playlistId = $none,
Object? images = $none,
Object? childCount = $none,
Object? primaryRatio = $none,
UserData? userData,
Object? canDelete = $none,
Object? canDownload = $none,
Object? jellyType = $none}) =>
$apply(FieldCopyWithData({
if (parentImages != $none) #parentImages: parentImages,
if (seasonName != null) #seasonName: seasonName,
if (episodes != null) #episodes: episodes,
if (episodeCount != null) #episodeCount: episodeCount,
if (seriesId != null) #seriesId: seriesId,
if (seriesName != null) #seriesName: seriesName,
if (name != null) #name: name,
if (id != null) #id: id,
if (overview != null) #overview: overview,
if (parentId != $none) #parentId: parentId,
if (playlistId != $none) #playlistId: playlistId,
if (images != $none) #images: images,
if (childCount != $none) #childCount: childCount,
if (primaryRatio != $none) #primaryRatio: primaryRatio,
if (userData != null) #userData: userData,
if (canDelete != $none) #canDelete: canDelete,
if (canDownload != $none) #canDownload: canDownload,
if (jellyType != $none) #jellyType: jellyType
}));
@override
SeasonModel $make(CopyWithData data) => SeasonModel(
parentImages: data.get(#parentImages, or: $value.parentImages),
seasonName: data.get(#seasonName, or: $value.seasonName),
episodes: data.get(#episodes, or: $value.episodes),
episodeCount: data.get(#episodeCount, or: $value.episodeCount),
seriesId: data.get(#seriesId, or: $value.seriesId),
seriesName: data.get(#seriesName, or: $value.seriesName),
name: data.get(#name, or: $value.name),
id: data.get(#id, or: $value.id),
overview: data.get(#overview, or: $value.overview),
parentId: data.get(#parentId, or: $value.parentId),
playlistId: data.get(#playlistId, or: $value.playlistId),
images: data.get(#images, or: $value.images),
childCount: data.get(#childCount, or: $value.childCount),
primaryRatio: data.get(#primaryRatio, or: $value.primaryRatio),
userData: data.get(#userData, or: $value.userData),
canDelete: data.get(#canDelete, or: $value.canDelete),
canDownload: data.get(#canDownload, or: $value.canDownload),
jellyType: data.get(#jellyType, or: $value.jellyType));
@override
SeasonModelCopyWith<$R2, SeasonModel, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t) =>
_SeasonModelCopyWithImpl($value, $cast, t);
}

View file

@ -0,0 +1,96 @@
import 'package:fladder/screens/details_screens/series_detail_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/overview_model.dart';
import 'package:fladder/models/items/season_model.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'series_model.mapper.dart';
@MappableClass()
class SeriesModel extends ItemBaseModel with SeriesModelMappable {
final List<EpisodeModel>? availableEpisodes;
final List<SeasonModel>? seasons;
final String originalTitle;
final String sortName;
final String status;
final List<ItemBaseModel> related;
const SeriesModel({
this.availableEpisodes,
this.seasons,
required this.originalTitle,
required this.sortName,
required this.status,
this.related = const [],
required super.name,
required super.id,
required super.overview,
required super.parentId,
required super.playlistId,
required super.images,
required super.childCount,
required super.primaryRatio,
required super.userData,
super.canDownload,
super.canDelete,
super.jellyType,
});
EpisodeModel? get nextUp => availableEpisodes?.nextUp ?? availableEpisodes?.firstOrNull;
@override
String detailedName(BuildContext context) => name;
@override
ItemBaseModel get parentBaseModel => this;
@override
Widget get detailScreenWidget => SeriesDetailScreen(item: this);
@override
bool get emptyShow => childCount == 0;
@override
bool get playAble => userData.unPlayedItemCount != 0;
@override
bool get identifiable => true;
@override
bool get unWatched =>
!userData.played && userData.progress <= 0 && userData.unPlayedItemCount == 0 && childCount != 0;
@override
String get subText => overview.yearAired?.toString() ?? "";
List<ItemBaseModel> fetchAllShows() {
return availableEpisodes?.map((e) => e).toList() ?? [];
}
@override
bool get syncAble => true;
factory SeriesModel.fromBaseDto(dto.BaseItemDto item, Ref ref) => SeriesModel(
name: item.name ?? "",
id: item.id ?? "",
childCount: item.childCount,
overview: OverviewModel.fromBaseItemDto(item, ref),
userData: UserData.fromDto(item.userData),
parentId: item.parentId,
playlistId: item.playlistItemId,
images: ImagesData.fromBaseItem(item, ref, getOriginalSize: true),
primaryRatio: item.primaryImageAspectRatio,
originalTitle: item.originalTitle ?? "",
sortName: item.sortName ?? "",
canDelete: item.canDelete,
canDownload: item.canDownload,
status: item.status ?? "Continuing",
);
}

View file

@ -0,0 +1,309 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
part of 'series_model.dart';
class SeriesModelMapper extends SubClassMapperBase<SeriesModel> {
SeriesModelMapper._();
static SeriesModelMapper? _instance;
static SeriesModelMapper ensureInitialized() {
if (_instance == null) {
MapperContainer.globals.use(_instance = SeriesModelMapper._());
ItemBaseModelMapper.ensureInitialized().addSubMapper(_instance!);
EpisodeModelMapper.ensureInitialized();
SeasonModelMapper.ensureInitialized();
ItemBaseModelMapper.ensureInitialized();
OverviewModelMapper.ensureInitialized();
UserDataMapper.ensureInitialized();
}
return _instance!;
}
@override
final String id = 'SeriesModel';
static List<EpisodeModel>? _$availableEpisodes(SeriesModel v) =>
v.availableEpisodes;
static const Field<SeriesModel, List<EpisodeModel>> _f$availableEpisodes =
Field('availableEpisodes', _$availableEpisodes, opt: true);
static List<SeasonModel>? _$seasons(SeriesModel v) => v.seasons;
static const Field<SeriesModel, List<SeasonModel>> _f$seasons =
Field('seasons', _$seasons, opt: true);
static String _$originalTitle(SeriesModel v) => v.originalTitle;
static const Field<SeriesModel, String> _f$originalTitle =
Field('originalTitle', _$originalTitle);
static String _$sortName(SeriesModel v) => v.sortName;
static const Field<SeriesModel, String> _f$sortName =
Field('sortName', _$sortName);
static String _$status(SeriesModel v) => v.status;
static const Field<SeriesModel, String> _f$status = Field('status', _$status);
static List<ItemBaseModel> _$related(SeriesModel v) => v.related;
static const Field<SeriesModel, List<ItemBaseModel>> _f$related =
Field('related', _$related, opt: true, def: const []);
static String _$name(SeriesModel v) => v.name;
static const Field<SeriesModel, String> _f$name = Field('name', _$name);
static String _$id(SeriesModel v) => v.id;
static const Field<SeriesModel, String> _f$id = Field('id', _$id);
static OverviewModel _$overview(SeriesModel v) => v.overview;
static const Field<SeriesModel, OverviewModel> _f$overview =
Field('overview', _$overview);
static String? _$parentId(SeriesModel v) => v.parentId;
static const Field<SeriesModel, String> _f$parentId =
Field('parentId', _$parentId);
static String? _$playlistId(SeriesModel v) => v.playlistId;
static const Field<SeriesModel, String> _f$playlistId =
Field('playlistId', _$playlistId);
static ImagesData? _$images(SeriesModel v) => v.images;
static const Field<SeriesModel, ImagesData> _f$images =
Field('images', _$images);
static int? _$childCount(SeriesModel v) => v.childCount;
static const Field<SeriesModel, int> _f$childCount =
Field('childCount', _$childCount);
static double? _$primaryRatio(SeriesModel v) => v.primaryRatio;
static const Field<SeriesModel, double> _f$primaryRatio =
Field('primaryRatio', _$primaryRatio);
static UserData _$userData(SeriesModel v) => v.userData;
static const Field<SeriesModel, UserData> _f$userData =
Field('userData', _$userData);
static bool? _$canDownload(SeriesModel v) => v.canDownload;
static const Field<SeriesModel, bool> _f$canDownload =
Field('canDownload', _$canDownload, opt: true);
static bool? _$canDelete(SeriesModel v) => v.canDelete;
static const Field<SeriesModel, bool> _f$canDelete =
Field('canDelete', _$canDelete, opt: true);
static dto.BaseItemKind? _$jellyType(SeriesModel v) => v.jellyType;
static const Field<SeriesModel, dto.BaseItemKind> _f$jellyType =
Field('jellyType', _$jellyType, opt: true);
@override
final MappableFields<SeriesModel> fields = const {
#availableEpisodes: _f$availableEpisodes,
#seasons: _f$seasons,
#originalTitle: _f$originalTitle,
#sortName: _f$sortName,
#status: _f$status,
#related: _f$related,
#name: _f$name,
#id: _f$id,
#overview: _f$overview,
#parentId: _f$parentId,
#playlistId: _f$playlistId,
#images: _f$images,
#childCount: _f$childCount,
#primaryRatio: _f$primaryRatio,
#userData: _f$userData,
#canDownload: _f$canDownload,
#canDelete: _f$canDelete,
#jellyType: _f$jellyType,
};
@override
final bool ignoreNull = true;
@override
final String discriminatorKey = 'type';
@override
final dynamic discriminatorValue = 'SeriesModel';
@override
late final ClassMapperBase superMapper =
ItemBaseModelMapper.ensureInitialized();
static SeriesModel _instantiate(DecodingData data) {
return SeriesModel(
availableEpisodes: data.dec(_f$availableEpisodes),
seasons: data.dec(_f$seasons),
originalTitle: data.dec(_f$originalTitle),
sortName: data.dec(_f$sortName),
status: data.dec(_f$status),
related: data.dec(_f$related),
name: data.dec(_f$name),
id: data.dec(_f$id),
overview: data.dec(_f$overview),
parentId: data.dec(_f$parentId),
playlistId: data.dec(_f$playlistId),
images: data.dec(_f$images),
childCount: data.dec(_f$childCount),
primaryRatio: data.dec(_f$primaryRatio),
userData: data.dec(_f$userData),
canDownload: data.dec(_f$canDownload),
canDelete: data.dec(_f$canDelete),
jellyType: data.dec(_f$jellyType));
}
@override
final Function instantiate = _instantiate;
static SeriesModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<SeriesModel>(map);
}
static SeriesModel fromJson(String json) {
return ensureInitialized().decodeJson<SeriesModel>(json);
}
}
mixin SeriesModelMappable {
String toJson() {
return SeriesModelMapper.ensureInitialized()
.encodeJson<SeriesModel>(this as SeriesModel);
}
Map<String, dynamic> toMap() {
return SeriesModelMapper.ensureInitialized()
.encodeMap<SeriesModel>(this as SeriesModel);
}
SeriesModelCopyWith<SeriesModel, SeriesModel, SeriesModel> get copyWith =>
_SeriesModelCopyWithImpl(this as SeriesModel, $identity, $identity);
@override
String toString() {
return SeriesModelMapper.ensureInitialized()
.stringifyValue(this as SeriesModel);
}
}
extension SeriesModelValueCopy<$R, $Out>
on ObjectCopyWith<$R, SeriesModel, $Out> {
SeriesModelCopyWith<$R, SeriesModel, $Out> get $asSeriesModel =>
$base.as((v, t, t2) => _SeriesModelCopyWithImpl(v, t, t2));
}
abstract class SeriesModelCopyWith<$R, $In extends SeriesModel, $Out>
implements ItemBaseModelCopyWith<$R, $In, $Out> {
ListCopyWith<$R, EpisodeModel,
EpisodeModelCopyWith<$R, EpisodeModel, EpisodeModel>>?
get availableEpisodes;
ListCopyWith<$R, SeasonModel,
SeasonModelCopyWith<$R, SeasonModel, SeasonModel>>? get seasons;
ListCopyWith<$R, ItemBaseModel,
ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>> get related;
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview;
@override
UserDataCopyWith<$R, UserData, UserData> get userData;
@override
$R call(
{List<EpisodeModel>? availableEpisodes,
List<SeasonModel>? seasons,
String? originalTitle,
String? sortName,
String? status,
List<ItemBaseModel>? related,
String? name,
String? id,
OverviewModel? overview,
String? parentId,
String? playlistId,
ImagesData? images,
int? childCount,
double? primaryRatio,
UserData? userData,
bool? canDownload,
bool? canDelete,
dto.BaseItemKind? jellyType});
SeriesModelCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t);
}
class _SeriesModelCopyWithImpl<$R, $Out>
extends ClassCopyWithBase<$R, SeriesModel, $Out>
implements SeriesModelCopyWith<$R, SeriesModel, $Out> {
_SeriesModelCopyWithImpl(super.value, super.then, super.then2);
@override
late final ClassMapperBase<SeriesModel> $mapper =
SeriesModelMapper.ensureInitialized();
@override
ListCopyWith<$R, EpisodeModel,
EpisodeModelCopyWith<$R, EpisodeModel, EpisodeModel>>?
get availableEpisodes => $value.availableEpisodes != null
? ListCopyWith($value.availableEpisodes!,
(v, t) => v.copyWith.$chain(t), (v) => call(availableEpisodes: v))
: null;
@override
ListCopyWith<$R, SeasonModel,
SeasonModelCopyWith<$R, SeasonModel, SeasonModel>>?
get seasons => $value.seasons != null
? ListCopyWith($value.seasons!, (v, t) => v.copyWith.$chain(t),
(v) => call(seasons: v))
: null;
@override
ListCopyWith<$R, ItemBaseModel,
ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>>
get related => ListCopyWith($value.related,
(v, t) => v.copyWith.$chain(t), (v) => call(related: v));
@override
OverviewModelCopyWith<$R, OverviewModel, OverviewModel> get overview =>
$value.overview.copyWith.$chain((v) => call(overview: v));
@override
UserDataCopyWith<$R, UserData, UserData> get userData =>
$value.userData.copyWith.$chain((v) => call(userData: v));
@override
$R call(
{Object? availableEpisodes = $none,
Object? seasons = $none,
String? originalTitle,
String? sortName,
String? status,
List<ItemBaseModel>? related,
String? name,
String? id,
OverviewModel? overview,
Object? parentId = $none,
Object? playlistId = $none,
Object? images = $none,
Object? childCount = $none,
Object? primaryRatio = $none,
UserData? userData,
Object? canDownload = $none,
Object? canDelete = $none,
Object? jellyType = $none}) =>
$apply(FieldCopyWithData({
if (availableEpisodes != $none) #availableEpisodes: availableEpisodes,
if (seasons != $none) #seasons: seasons,
if (originalTitle != null) #originalTitle: originalTitle,
if (sortName != null) #sortName: sortName,
if (status != null) #status: status,
if (related != null) #related: related,
if (name != null) #name: name,
if (id != null) #id: id,
if (overview != null) #overview: overview,
if (parentId != $none) #parentId: parentId,
if (playlistId != $none) #playlistId: playlistId,
if (images != $none) #images: images,
if (childCount != $none) #childCount: childCount,
if (primaryRatio != $none) #primaryRatio: primaryRatio,
if (userData != null) #userData: userData,
if (canDownload != $none) #canDownload: canDownload,
if (canDelete != $none) #canDelete: canDelete,
if (jellyType != $none) #jellyType: jellyType
}));
@override
SeriesModel $make(CopyWithData data) => SeriesModel(
availableEpisodes:
data.get(#availableEpisodes, or: $value.availableEpisodes),
seasons: data.get(#seasons, or: $value.seasons),
originalTitle: data.get(#originalTitle, or: $value.originalTitle),
sortName: data.get(#sortName, or: $value.sortName),
status: data.get(#status, or: $value.status),
related: data.get(#related, or: $value.related),
name: data.get(#name, or: $value.name),
id: data.get(#id, or: $value.id),
overview: data.get(#overview, or: $value.overview),
parentId: data.get(#parentId, or: $value.parentId),
playlistId: data.get(#playlistId, or: $value.playlistId),
images: data.get(#images, or: $value.images),
childCount: data.get(#childCount, or: $value.childCount),
primaryRatio: data.get(#primaryRatio, or: $value.primaryRatio),
userData: data.get(#userData, or: $value.userData),
canDownload: data.get(#canDownload, or: $value.canDownload),
canDelete: data.get(#canDelete, or: $value.canDelete),
jellyType: data.get(#jellyType, or: $value.jellyType));
@override
SeriesModelCopyWith<$R2, SeriesModel, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t) =>
_SeriesModelCopyWithImpl($value, $cast, t);
}

View file

@ -0,0 +1,61 @@
import 'dart:ui';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'trick_play_model.freezed.dart';
part 'trick_play_model.g.dart';
@freezed
class TrickPlayModel with _$TrickPlayModel {
factory TrickPlayModel({
required int width,
required int height,
required int tileWidth,
required int tileHeight,
required int thumbnailCount,
required Duration interval,
@Default([]) List<String> images,
}) = _TrickPlayModel;
const TrickPlayModel._();
int get imagesPerTile => tileWidth * tileHeight;
String? getTile(Duration position) {
final int currentIndex = (position.inMilliseconds ~/ interval.inMilliseconds).clamp(0, thumbnailCount);
final int indexOfTile = (currentIndex ~/ imagesPerTile).clamp(0, images.length);
return images.elementAtOrNull(indexOfTile);
}
Offset offset(Duration position) {
final int currentIndex = (position.inMilliseconds ~/ interval.inMilliseconds).clamp(0, thumbnailCount - 1);
final int tileIndex = currentIndex % imagesPerTile;
final int column = tileIndex % tileWidth;
final int row = tileIndex ~/ tileWidth;
return Offset((width * column).toDouble(), (height * row).toDouble());
}
static Map<String, TrickPlayModel> toTrickPlayMap(Map<String, dynamic> map) {
Map<String, TrickPlayModel> newMap = {};
final firstMap = (((map.entries.first as MapEntry).value as Map<String, dynamic>));
newMap.addEntries(firstMap.entries.map(
(e) {
final map = e.value as Map<String, dynamic>;
return MapEntry(
e.key,
TrickPlayModel(
width: map['Width'] as int? ?? 0,
height: map['Height'] as int? ?? 0,
tileWidth: map['TileWidth'] as int? ?? 0,
tileHeight: map['TileHeight'] as int? ?? 0,
thumbnailCount: map['ThumbnailCount'] as int? ?? 0,
interval: Duration(milliseconds: map['Interval'] as int? ?? 0),
),
);
},
));
return newMap;
}
factory TrickPlayModel.fromJson(Map<String, dynamic> json) => _$TrickPlayModelFromJson(json);
}

View file

@ -0,0 +1,297 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'trick_play_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
TrickPlayModel _$TrickPlayModelFromJson(Map<String, dynamic> json) {
return _TrickPlayModel.fromJson(json);
}
/// @nodoc
mixin _$TrickPlayModel {
int get width => throw _privateConstructorUsedError;
int get height => throw _privateConstructorUsedError;
int get tileWidth => throw _privateConstructorUsedError;
int get tileHeight => throw _privateConstructorUsedError;
int get thumbnailCount => throw _privateConstructorUsedError;
Duration get interval => throw _privateConstructorUsedError;
List<String> get images => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$TrickPlayModelCopyWith<TrickPlayModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $TrickPlayModelCopyWith<$Res> {
factory $TrickPlayModelCopyWith(
TrickPlayModel value, $Res Function(TrickPlayModel) then) =
_$TrickPlayModelCopyWithImpl<$Res, TrickPlayModel>;
@useResult
$Res call(
{int width,
int height,
int tileWidth,
int tileHeight,
int thumbnailCount,
Duration interval,
List<String> images});
}
/// @nodoc
class _$TrickPlayModelCopyWithImpl<$Res, $Val extends TrickPlayModel>
implements $TrickPlayModelCopyWith<$Res> {
_$TrickPlayModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? width = null,
Object? height = null,
Object? tileWidth = null,
Object? tileHeight = null,
Object? thumbnailCount = null,
Object? interval = null,
Object? images = null,
}) {
return _then(_value.copyWith(
width: null == width
? _value.width
: width // ignore: cast_nullable_to_non_nullable
as int,
height: null == height
? _value.height
: height // ignore: cast_nullable_to_non_nullable
as int,
tileWidth: null == tileWidth
? _value.tileWidth
: tileWidth // ignore: cast_nullable_to_non_nullable
as int,
tileHeight: null == tileHeight
? _value.tileHeight
: tileHeight // ignore: cast_nullable_to_non_nullable
as int,
thumbnailCount: null == thumbnailCount
? _value.thumbnailCount
: thumbnailCount // ignore: cast_nullable_to_non_nullable
as int,
interval: null == interval
? _value.interval
: interval // ignore: cast_nullable_to_non_nullable
as Duration,
images: null == images
? _value.images
: images // ignore: cast_nullable_to_non_nullable
as List<String>,
) as $Val);
}
}
/// @nodoc
abstract class _$$TrickPlayModelImplCopyWith<$Res>
implements $TrickPlayModelCopyWith<$Res> {
factory _$$TrickPlayModelImplCopyWith(_$TrickPlayModelImpl value,
$Res Function(_$TrickPlayModelImpl) then) =
__$$TrickPlayModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int width,
int height,
int tileWidth,
int tileHeight,
int thumbnailCount,
Duration interval,
List<String> images});
}
/// @nodoc
class __$$TrickPlayModelImplCopyWithImpl<$Res>
extends _$TrickPlayModelCopyWithImpl<$Res, _$TrickPlayModelImpl>
implements _$$TrickPlayModelImplCopyWith<$Res> {
__$$TrickPlayModelImplCopyWithImpl(
_$TrickPlayModelImpl _value, $Res Function(_$TrickPlayModelImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? width = null,
Object? height = null,
Object? tileWidth = null,
Object? tileHeight = null,
Object? thumbnailCount = null,
Object? interval = null,
Object? images = null,
}) {
return _then(_$TrickPlayModelImpl(
width: null == width
? _value.width
: width // ignore: cast_nullable_to_non_nullable
as int,
height: null == height
? _value.height
: height // ignore: cast_nullable_to_non_nullable
as int,
tileWidth: null == tileWidth
? _value.tileWidth
: tileWidth // ignore: cast_nullable_to_non_nullable
as int,
tileHeight: null == tileHeight
? _value.tileHeight
: tileHeight // ignore: cast_nullable_to_non_nullable
as int,
thumbnailCount: null == thumbnailCount
? _value.thumbnailCount
: thumbnailCount // ignore: cast_nullable_to_non_nullable
as int,
interval: null == interval
? _value.interval
: interval // ignore: cast_nullable_to_non_nullable
as Duration,
images: null == images
? _value._images
: images // ignore: cast_nullable_to_non_nullable
as List<String>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$TrickPlayModelImpl extends _TrickPlayModel {
_$TrickPlayModelImpl(
{required this.width,
required this.height,
required this.tileWidth,
required this.tileHeight,
required this.thumbnailCount,
required this.interval,
final List<String> images = const []})
: _images = images,
super._();
factory _$TrickPlayModelImpl.fromJson(Map<String, dynamic> json) =>
_$$TrickPlayModelImplFromJson(json);
@override
final int width;
@override
final int height;
@override
final int tileWidth;
@override
final int tileHeight;
@override
final int thumbnailCount;
@override
final Duration interval;
final List<String> _images;
@override
@JsonKey()
List<String> get images {
if (_images is EqualUnmodifiableListView) return _images;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_images);
}
@override
String toString() {
return 'TrickPlayModel(width: $width, height: $height, tileWidth: $tileWidth, tileHeight: $tileHeight, thumbnailCount: $thumbnailCount, interval: $interval, images: $images)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$TrickPlayModelImpl &&
(identical(other.width, width) || other.width == width) &&
(identical(other.height, height) || other.height == height) &&
(identical(other.tileWidth, tileWidth) ||
other.tileWidth == tileWidth) &&
(identical(other.tileHeight, tileHeight) ||
other.tileHeight == tileHeight) &&
(identical(other.thumbnailCount, thumbnailCount) ||
other.thumbnailCount == thumbnailCount) &&
(identical(other.interval, interval) ||
other.interval == interval) &&
const DeepCollectionEquality().equals(other._images, _images));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
width,
height,
tileWidth,
tileHeight,
thumbnailCount,
interval,
const DeepCollectionEquality().hash(_images));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$TrickPlayModelImplCopyWith<_$TrickPlayModelImpl> get copyWith =>
__$$TrickPlayModelImplCopyWithImpl<_$TrickPlayModelImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$TrickPlayModelImplToJson(
this,
);
}
}
abstract class _TrickPlayModel extends TrickPlayModel {
factory _TrickPlayModel(
{required final int width,
required final int height,
required final int tileWidth,
required final int tileHeight,
required final int thumbnailCount,
required final Duration interval,
final List<String> images}) = _$TrickPlayModelImpl;
_TrickPlayModel._() : super._();
factory _TrickPlayModel.fromJson(Map<String, dynamic> json) =
_$TrickPlayModelImpl.fromJson;
@override
int get width;
@override
int get height;
@override
int get tileWidth;
@override
int get tileHeight;
@override
int get thumbnailCount;
@override
Duration get interval;
@override
List<String> get images;
@override
@JsonKey(ignore: true)
_$$TrickPlayModelImplCopyWith<_$TrickPlayModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,33 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'trick_play_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$TrickPlayModelImpl _$$TrickPlayModelImplFromJson(Map<String, dynamic> json) =>
_$TrickPlayModelImpl(
width: (json['width'] as num).toInt(),
height: (json['height'] as num).toInt(),
tileWidth: (json['tileWidth'] as num).toInt(),
tileHeight: (json['tileHeight'] as num).toInt(),
thumbnailCount: (json['thumbnailCount'] as num).toInt(),
interval: Duration(microseconds: (json['interval'] as num).toInt()),
images: (json['images'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
);
Map<String, dynamic> _$$TrickPlayModelImplToJson(
_$TrickPlayModelImpl instance) =>
<String, dynamic>{
'width': instance.width,
'height': instance.height,
'tileWidth': instance.tileWidth,
'tileHeight': instance.tileHeight,
'thumbnailCount': instance.thumbnailCount,
'interval': instance.interval.inMicroseconds,
'images': instance.images,
};

View file

@ -0,0 +1,61 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart' as enums;
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/photos_model.dart';
import 'package:fladder/models/recommended_model.dart';
class LibraryModel {
final bool loading;
final String id;
final String name;
final List<PhotoModel> timelinePhotos;
final List<ItemBaseModel> posters;
final List<RecommendedModel> recommendations;
final List<String> genres;
final List<ItemBaseModel> latest;
final List<ItemBaseModel> nextUp;
final List<ItemBaseModel> favourites;
final enums.BaseItemKind type;
LibraryModel({
this.loading = false,
required this.id,
required this.name,
this.timelinePhotos = const [],
this.posters = const [],
this.recommendations = const [],
this.genres = const [],
this.latest = const [],
this.nextUp = const [],
this.favourites = const [],
required this.type,
});
LibraryModel copyWith({
bool? loading,
String? id,
String? name,
List<PhotoModel>? timelinePhotos,
List<ItemBaseModel>? posters,
List<RecommendedModel>? recommendations,
List<String>? genres,
List<ItemBaseModel>? latest,
List<ItemBaseModel>? nextUp,
List<ItemBaseModel>? favourites,
enums.BaseItemKind? type,
}) {
return LibraryModel(
loading: loading ?? this.loading,
id: id ?? this.id,
name: name ?? this.name,
timelinePhotos: timelinePhotos ?? this.timelinePhotos,
posters: posters ?? this.posters,
recommendations: recommendations ?? this.recommendations,
genres: genres ?? this.genres,
latest: latest ?? this.latest,
nextUp: nextUp ?? this.nextUp,
favourites: favourites ?? this.favourites,
type: type ?? this.type,
);
}
}

View file

@ -0,0 +1,223 @@
import 'package:collection/collection.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:fladder/util/list_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/library_search/library_search_options.dart';
import 'package:fladder/models/view_model.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/map_bool_helper.dart';
part 'library_search_model.mapper.dart';
@MappableClass()
class LibrarySearchModel with LibrarySearchModelMappable {
final bool loading;
final bool selecteMode;
final String searchQuery;
final List<ItemBaseModel> folderOverwrite;
final Map<ViewModel, bool> views;
final List<ItemBaseModel> posters;
final List<ItemBaseModel> selectedPosters;
final Map<ItemFilter, bool> filters;
final Map<String, bool> genres;
final Map<Studio, bool> studios;
final Map<String, bool> tags;
final Map<int, bool> years;
final Map<String, bool> officialRatings;
final Map<FladderItemType, bool> types;
final SortingOptions sortingOption;
final SortingOrder sortOrder;
final bool favourites;
final bool hideEmtpyShows;
final bool recursive;
final GroupBy groupBy;
final Map<String, int> lastIndices;
final Map<String, int> libraryItemCounts;
final bool fetchingItems;
const LibrarySearchModel({
this.loading = false,
this.selecteMode = false,
this.folderOverwrite = const [],
this.searchQuery = "",
this.views = const {},
this.posters = const [],
this.selectedPosters = const [],
this.filters = const {
ItemFilter.isplayed: false,
ItemFilter.isunplayed: false,
ItemFilter.isresumable: false,
},
this.genres = const {},
this.studios = const {},
this.tags = const {},
this.years = const {},
this.officialRatings = const {},
this.types = const {
FladderItemType.audio: false,
FladderItemType.boxset: false,
FladderItemType.book: false,
FladderItemType.collectionFolder: false,
FladderItemType.episode: false,
FladderItemType.folder: false,
FladderItemType.movie: true,
FladderItemType.musicAlbum: false,
FladderItemType.musicVideo: false,
FladderItemType.photo: false,
FladderItemType.person: false,
FladderItemType.photoalbum: false,
FladderItemType.series: true,
FladderItemType.video: true,
},
this.favourites = false,
this.sortingOption = SortingOptions.name,
this.sortOrder = SortingOrder.ascending,
this.hideEmtpyShows = true,
this.recursive = false,
this.groupBy = GroupBy.none,
this.lastIndices = const {},
this.libraryItemCounts = const {},
this.fetchingItems = false,
});
bool get hasActiveFilters {
return genres.hasEnabled ||
studios.hasEnabled ||
tags.hasEnabled ||
years.hasEnabled ||
officialRatings.hasEnabled ||
hideEmtpyShows ||
filters.hasEnabled ||
favourites ||
searchQuery.isNotEmpty;
}
int get totalItemCount {
if (libraryItemCounts.isEmpty) return posters.length;
int totalCount = 0;
for (var item in libraryItemCounts.values) {
totalCount += item;
}
return totalCount;
}
bool get allDoneFetching {
if (libraryItemCounts.isEmpty) return false;
if (libraryItemCounts.length != lastIndices.length) {
return false;
} else {
for (var item in libraryItemCounts.entries) {
if (lastIndices[item.key] != item.value) {
return false;
}
}
}
return true;
}
String searchBarTitle(BuildContext context) {
if (folderOverwrite.isNotEmpty) {
return "${context.localized.search} ${folderOverwrite.last.name}...";
}
return views.included.length == 1
? "${context.localized.search} ${views.included.first.name}..."
: "${context.localized.search} ${context.localized.library(2)}...";
}
ItemBaseModel? get nestedCurrentItem => folderOverwrite.lastOrNull;
List<ItemBaseModel> get activePosters => selectedPosters.isNotEmpty ? selectedPosters : posters;
bool get showPlayButtons {
if (totalItemCount == 0) return false;
return types.included.isEmpty ||
types.included.containsAny(
{...FladderItemType.playable, FladderItemType.folder},
);
}
bool get showGalleryButtons {
if (totalItemCount == 0) return false;
return types.included.isEmpty ||
types.included.containsAny(
{...FladderItemType.galleryItem, FladderItemType.photoalbum, FladderItemType.folder},
);
}
LibrarySearchModel resetLazyLoad() {
return copyWith(
selectedPosters: [],
lastIndices: const {},
libraryItemCounts: const {},
);
}
LibrarySearchModel fullReset() {
return copyWith(
posters: [],
selectedPosters: [],
lastIndices: const {},
libraryItemCounts: const {},
);
}
LibrarySearchModel setFiltersToDefault() {
return copyWith(
genres: const {},
tags: const {},
officialRatings: const {},
years: const {},
searchQuery: '',
favourites: false,
recursive: false,
studios: const {},
hideEmtpyShows: true,
);
}
@override
bool operator ==(covariant LibrarySearchModel other) {
if (identical(this, other)) return true;
return other.searchQuery == searchQuery &&
listEquals(other.folderOverwrite, folderOverwrite) &&
mapEquals(other.views, views) &&
mapEquals(other.filters, filters) &&
mapEquals(other.genres, genres) &&
mapEquals(other.studios, studios) &&
mapEquals(other.tags, tags) &&
mapEquals(other.years, years) &&
mapEquals(other.officialRatings, officialRatings) &&
mapEquals(other.types, types) &&
other.sortingOption == sortingOption &&
other.sortOrder == sortOrder &&
other.favourites == favourites &&
other.recursive == recursive;
}
@override
int get hashCode {
return searchQuery.hashCode ^
folderOverwrite.hashCode ^
views.hashCode ^
posters.hashCode ^
selectedPosters.hashCode ^
filters.hashCode ^
genres.hashCode ^
studios.hashCode ^
tags.hashCode ^
years.hashCode ^
officialRatings.hashCode ^
types.hashCode ^
sortingOption.hashCode ^
sortOrder.hashCode ^
favourites.hashCode ^
recursive.hashCode;
}
}

View file

@ -0,0 +1,418 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, unnecessary_cast, override_on_non_overriding_member
// ignore_for_file: strict_raw_type, inference_failure_on_untyped_parameter
part of 'library_search_model.dart';
class LibrarySearchModelMapper extends ClassMapperBase<LibrarySearchModel> {
LibrarySearchModelMapper._();
static LibrarySearchModelMapper? _instance;
static LibrarySearchModelMapper ensureInitialized() {
if (_instance == null) {
MapperContainer.globals.use(_instance = LibrarySearchModelMapper._());
ItemBaseModelMapper.ensureInitialized();
}
return _instance!;
}
@override
final String id = 'LibrarySearchModel';
static bool _$loading(LibrarySearchModel v) => v.loading;
static const Field<LibrarySearchModel, bool> _f$loading =
Field('loading', _$loading, opt: true, def: false);
static bool _$selecteMode(LibrarySearchModel v) => v.selecteMode;
static const Field<LibrarySearchModel, bool> _f$selecteMode =
Field('selecteMode', _$selecteMode, opt: true, def: false);
static List<ItemBaseModel> _$folderOverwrite(LibrarySearchModel v) =>
v.folderOverwrite;
static const Field<LibrarySearchModel, List<ItemBaseModel>>
_f$folderOverwrite =
Field('folderOverwrite', _$folderOverwrite, opt: true, def: const []);
static String _$searchQuery(LibrarySearchModel v) => v.searchQuery;
static const Field<LibrarySearchModel, String> _f$searchQuery =
Field('searchQuery', _$searchQuery, opt: true, def: "");
static Map<ViewModel, bool> _$views(LibrarySearchModel v) => v.views;
static const Field<LibrarySearchModel, Map<ViewModel, bool>> _f$views =
Field('views', _$views, opt: true, def: const {});
static List<ItemBaseModel> _$posters(LibrarySearchModel v) => v.posters;
static const Field<LibrarySearchModel, List<ItemBaseModel>> _f$posters =
Field('posters', _$posters, opt: true, def: const []);
static List<ItemBaseModel> _$selectedPosters(LibrarySearchModel v) =>
v.selectedPosters;
static const Field<LibrarySearchModel, List<ItemBaseModel>>
_f$selectedPosters =
Field('selectedPosters', _$selectedPosters, opt: true, def: const []);
static Map<ItemFilter, bool> _$filters(LibrarySearchModel v) => v.filters;
static const Field<LibrarySearchModel, Map<ItemFilter, bool>> _f$filters =
Field('filters', _$filters, opt: true, def: const {
ItemFilter.isplayed: false,
ItemFilter.isunplayed: false,
ItemFilter.isresumable: false
});
static Map<String, bool> _$genres(LibrarySearchModel v) => v.genres;
static const Field<LibrarySearchModel, Map<String, bool>> _f$genres =
Field('genres', _$genres, opt: true, def: const {});
static Map<Studio, bool> _$studios(LibrarySearchModel v) => v.studios;
static const Field<LibrarySearchModel, Map<Studio, bool>> _f$studios =
Field('studios', _$studios, opt: true, def: const {});
static Map<String, bool> _$tags(LibrarySearchModel v) => v.tags;
static const Field<LibrarySearchModel, Map<String, bool>> _f$tags =
Field('tags', _$tags, opt: true, def: const {});
static Map<int, bool> _$years(LibrarySearchModel v) => v.years;
static const Field<LibrarySearchModel, Map<int, bool>> _f$years =
Field('years', _$years, opt: true, def: const {});
static Map<String, bool> _$officialRatings(LibrarySearchModel v) =>
v.officialRatings;
static const Field<LibrarySearchModel, Map<String, bool>> _f$officialRatings =
Field('officialRatings', _$officialRatings, opt: true, def: const {});
static Map<FladderItemType, bool> _$types(LibrarySearchModel v) => v.types;
static const Field<LibrarySearchModel, Map<FladderItemType, bool>> _f$types =
Field('types', _$types, opt: true, def: const {
FladderItemType.audio: false,
FladderItemType.boxset: false,
FladderItemType.book: false,
FladderItemType.collectionFolder: false,
FladderItemType.episode: false,
FladderItemType.folder: false,
FladderItemType.movie: true,
FladderItemType.musicAlbum: false,
FladderItemType.musicVideo: false,
FladderItemType.photo: false,
FladderItemType.person: false,
FladderItemType.photoalbum: false,
FladderItemType.series: true,
FladderItemType.video: true
});
static bool _$favourites(LibrarySearchModel v) => v.favourites;
static const Field<LibrarySearchModel, bool> _f$favourites =
Field('favourites', _$favourites, opt: true, def: false);
static SortingOptions _$sortingOption(LibrarySearchModel v) =>
v.sortingOption;
static const Field<LibrarySearchModel, SortingOptions> _f$sortingOption =
Field('sortingOption', _$sortingOption,
opt: true, def: SortingOptions.name);
static SortingOrder _$sortOrder(LibrarySearchModel v) => v.sortOrder;
static const Field<LibrarySearchModel, SortingOrder> _f$sortOrder =
Field('sortOrder', _$sortOrder, opt: true, def: SortingOrder.ascending);
static bool _$hideEmtpyShows(LibrarySearchModel v) => v.hideEmtpyShows;
static const Field<LibrarySearchModel, bool> _f$hideEmtpyShows =
Field('hideEmtpyShows', _$hideEmtpyShows, opt: true, def: true);
static bool _$recursive(LibrarySearchModel v) => v.recursive;
static const Field<LibrarySearchModel, bool> _f$recursive =
Field('recursive', _$recursive, opt: true, def: false);
static GroupBy _$groupBy(LibrarySearchModel v) => v.groupBy;
static const Field<LibrarySearchModel, GroupBy> _f$groupBy =
Field('groupBy', _$groupBy, opt: true, def: GroupBy.none);
static Map<String, int> _$lastIndices(LibrarySearchModel v) => v.lastIndices;
static const Field<LibrarySearchModel, Map<String, int>> _f$lastIndices =
Field('lastIndices', _$lastIndices, opt: true, def: const {});
static Map<String, int> _$libraryItemCounts(LibrarySearchModel v) =>
v.libraryItemCounts;
static const Field<LibrarySearchModel, Map<String, int>>
_f$libraryItemCounts =
Field('libraryItemCounts', _$libraryItemCounts, opt: true, def: const {});
static bool _$fetchingItems(LibrarySearchModel v) => v.fetchingItems;
static const Field<LibrarySearchModel, bool> _f$fetchingItems =
Field('fetchingItems', _$fetchingItems, opt: true, def: false);
@override
final MappableFields<LibrarySearchModel> fields = const {
#loading: _f$loading,
#selecteMode: _f$selecteMode,
#folderOverwrite: _f$folderOverwrite,
#searchQuery: _f$searchQuery,
#views: _f$views,
#posters: _f$posters,
#selectedPosters: _f$selectedPosters,
#filters: _f$filters,
#genres: _f$genres,
#studios: _f$studios,
#tags: _f$tags,
#years: _f$years,
#officialRatings: _f$officialRatings,
#types: _f$types,
#favourites: _f$favourites,
#sortingOption: _f$sortingOption,
#sortOrder: _f$sortOrder,
#hideEmtpyShows: _f$hideEmtpyShows,
#recursive: _f$recursive,
#groupBy: _f$groupBy,
#lastIndices: _f$lastIndices,
#libraryItemCounts: _f$libraryItemCounts,
#fetchingItems: _f$fetchingItems,
};
@override
final bool ignoreNull = true;
static LibrarySearchModel _instantiate(DecodingData data) {
return LibrarySearchModel(
loading: data.dec(_f$loading),
selecteMode: data.dec(_f$selecteMode),
folderOverwrite: data.dec(_f$folderOverwrite),
searchQuery: data.dec(_f$searchQuery),
views: data.dec(_f$views),
posters: data.dec(_f$posters),
selectedPosters: data.dec(_f$selectedPosters),
filters: data.dec(_f$filters),
genres: data.dec(_f$genres),
studios: data.dec(_f$studios),
tags: data.dec(_f$tags),
years: data.dec(_f$years),
officialRatings: data.dec(_f$officialRatings),
types: data.dec(_f$types),
favourites: data.dec(_f$favourites),
sortingOption: data.dec(_f$sortingOption),
sortOrder: data.dec(_f$sortOrder),
hideEmtpyShows: data.dec(_f$hideEmtpyShows),
recursive: data.dec(_f$recursive),
groupBy: data.dec(_f$groupBy),
lastIndices: data.dec(_f$lastIndices),
libraryItemCounts: data.dec(_f$libraryItemCounts),
fetchingItems: data.dec(_f$fetchingItems));
}
@override
final Function instantiate = _instantiate;
static LibrarySearchModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<LibrarySearchModel>(map);
}
static LibrarySearchModel fromJson(String json) {
return ensureInitialized().decodeJson<LibrarySearchModel>(json);
}
}
mixin LibrarySearchModelMappable {
String toJson() {
return LibrarySearchModelMapper.ensureInitialized()
.encodeJson<LibrarySearchModel>(this as LibrarySearchModel);
}
Map<String, dynamic> toMap() {
return LibrarySearchModelMapper.ensureInitialized()
.encodeMap<LibrarySearchModel>(this as LibrarySearchModel);
}
LibrarySearchModelCopyWith<LibrarySearchModel, LibrarySearchModel,
LibrarySearchModel>
get copyWith => _LibrarySearchModelCopyWithImpl(
this as LibrarySearchModel, $identity, $identity);
@override
String toString() {
return LibrarySearchModelMapper.ensureInitialized()
.stringifyValue(this as LibrarySearchModel);
}
}
extension LibrarySearchModelValueCopy<$R, $Out>
on ObjectCopyWith<$R, LibrarySearchModel, $Out> {
LibrarySearchModelCopyWith<$R, LibrarySearchModel, $Out>
get $asLibrarySearchModel =>
$base.as((v, t, t2) => _LibrarySearchModelCopyWithImpl(v, t, t2));
}
abstract class LibrarySearchModelCopyWith<$R, $In extends LibrarySearchModel,
$Out> implements ClassCopyWith<$R, $In, $Out> {
ListCopyWith<$R, ItemBaseModel,
ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>>
get folderOverwrite;
MapCopyWith<$R, ViewModel, bool, ObjectCopyWith<$R, bool, bool>> get views;
ListCopyWith<$R, ItemBaseModel,
ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>> get posters;
ListCopyWith<$R, ItemBaseModel,
ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>>
get selectedPosters;
MapCopyWith<$R, ItemFilter, bool, ObjectCopyWith<$R, bool, bool>> get filters;
MapCopyWith<$R, String, bool, ObjectCopyWith<$R, bool, bool>> get genres;
MapCopyWith<$R, Studio, bool, ObjectCopyWith<$R, bool, bool>> get studios;
MapCopyWith<$R, String, bool, ObjectCopyWith<$R, bool, bool>> get tags;
MapCopyWith<$R, int, bool, ObjectCopyWith<$R, bool, bool>> get years;
MapCopyWith<$R, String, bool, ObjectCopyWith<$R, bool, bool>>
get officialRatings;
MapCopyWith<$R, FladderItemType, bool, ObjectCopyWith<$R, bool, bool>>
get types;
MapCopyWith<$R, String, int, ObjectCopyWith<$R, int, int>> get lastIndices;
MapCopyWith<$R, String, int, ObjectCopyWith<$R, int, int>>
get libraryItemCounts;
$R call(
{bool? loading,
bool? selecteMode,
List<ItemBaseModel>? folderOverwrite,
String? searchQuery,
Map<ViewModel, bool>? views,
List<ItemBaseModel>? posters,
List<ItemBaseModel>? selectedPosters,
Map<ItemFilter, bool>? filters,
Map<String, bool>? genres,
Map<Studio, bool>? studios,
Map<String, bool>? tags,
Map<int, bool>? years,
Map<String, bool>? officialRatings,
Map<FladderItemType, bool>? types,
bool? favourites,
SortingOptions? sortingOption,
SortingOrder? sortOrder,
bool? hideEmtpyShows,
bool? recursive,
GroupBy? groupBy,
Map<String, int>? lastIndices,
Map<String, int>? libraryItemCounts,
bool? fetchingItems});
LibrarySearchModelCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t);
}
class _LibrarySearchModelCopyWithImpl<$R, $Out>
extends ClassCopyWithBase<$R, LibrarySearchModel, $Out>
implements LibrarySearchModelCopyWith<$R, LibrarySearchModel, $Out> {
_LibrarySearchModelCopyWithImpl(super.value, super.then, super.then2);
@override
late final ClassMapperBase<LibrarySearchModel> $mapper =
LibrarySearchModelMapper.ensureInitialized();
@override
ListCopyWith<$R, ItemBaseModel,
ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>>
get folderOverwrite => ListCopyWith($value.folderOverwrite,
(v, t) => v.copyWith.$chain(t), (v) => call(folderOverwrite: v));
@override
MapCopyWith<$R, ViewModel, bool, ObjectCopyWith<$R, bool, bool>> get views =>
MapCopyWith($value.views, (v, t) => ObjectCopyWith(v, $identity, t),
(v) => call(views: v));
@override
ListCopyWith<$R, ItemBaseModel,
ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>>
get posters => ListCopyWith($value.posters,
(v, t) => v.copyWith.$chain(t), (v) => call(posters: v));
@override
ListCopyWith<$R, ItemBaseModel,
ItemBaseModelCopyWith<$R, ItemBaseModel, ItemBaseModel>>
get selectedPosters => ListCopyWith($value.selectedPosters,
(v, t) => v.copyWith.$chain(t), (v) => call(selectedPosters: v));
@override
MapCopyWith<$R, ItemFilter, bool, ObjectCopyWith<$R, bool, bool>>
get filters => MapCopyWith($value.filters,
(v, t) => ObjectCopyWith(v, $identity, t), (v) => call(filters: v));
@override
MapCopyWith<$R, String, bool, ObjectCopyWith<$R, bool, bool>> get genres =>
MapCopyWith($value.genres, (v, t) => ObjectCopyWith(v, $identity, t),
(v) => call(genres: v));
@override
MapCopyWith<$R, Studio, bool, ObjectCopyWith<$R, bool, bool>> get studios =>
MapCopyWith($value.studios, (v, t) => ObjectCopyWith(v, $identity, t),
(v) => call(studios: v));
@override
MapCopyWith<$R, String, bool, ObjectCopyWith<$R, bool, bool>> get tags =>
MapCopyWith($value.tags, (v, t) => ObjectCopyWith(v, $identity, t),
(v) => call(tags: v));
@override
MapCopyWith<$R, int, bool, ObjectCopyWith<$R, bool, bool>> get years =>
MapCopyWith($value.years, (v, t) => ObjectCopyWith(v, $identity, t),
(v) => call(years: v));
@override
MapCopyWith<$R, String, bool, ObjectCopyWith<$R, bool, bool>>
get officialRatings => MapCopyWith(
$value.officialRatings,
(v, t) => ObjectCopyWith(v, $identity, t),
(v) => call(officialRatings: v));
@override
MapCopyWith<$R, FladderItemType, bool, ObjectCopyWith<$R, bool, bool>>
get types => MapCopyWith($value.types,
(v, t) => ObjectCopyWith(v, $identity, t), (v) => call(types: v));
@override
MapCopyWith<$R, String, int, ObjectCopyWith<$R, int, int>> get lastIndices =>
MapCopyWith($value.lastIndices, (v, t) => ObjectCopyWith(v, $identity, t),
(v) => call(lastIndices: v));
@override
MapCopyWith<$R, String, int, ObjectCopyWith<$R, int, int>>
get libraryItemCounts => MapCopyWith(
$value.libraryItemCounts,
(v, t) => ObjectCopyWith(v, $identity, t),
(v) => call(libraryItemCounts: v));
@override
$R call(
{bool? loading,
bool? selecteMode,
List<ItemBaseModel>? folderOverwrite,
String? searchQuery,
Map<ViewModel, bool>? views,
List<ItemBaseModel>? posters,
List<ItemBaseModel>? selectedPosters,
Map<ItemFilter, bool>? filters,
Map<String, bool>? genres,
Map<Studio, bool>? studios,
Map<String, bool>? tags,
Map<int, bool>? years,
Map<String, bool>? officialRatings,
Map<FladderItemType, bool>? types,
bool? favourites,
SortingOptions? sortingOption,
SortingOrder? sortOrder,
bool? hideEmtpyShows,
bool? recursive,
GroupBy? groupBy,
Map<String, int>? lastIndices,
Map<String, int>? libraryItemCounts,
bool? fetchingItems}) =>
$apply(FieldCopyWithData({
if (loading != null) #loading: loading,
if (selecteMode != null) #selecteMode: selecteMode,
if (folderOverwrite != null) #folderOverwrite: folderOverwrite,
if (searchQuery != null) #searchQuery: searchQuery,
if (views != null) #views: views,
if (posters != null) #posters: posters,
if (selectedPosters != null) #selectedPosters: selectedPosters,
if (filters != null) #filters: filters,
if (genres != null) #genres: genres,
if (studios != null) #studios: studios,
if (tags != null) #tags: tags,
if (years != null) #years: years,
if (officialRatings != null) #officialRatings: officialRatings,
if (types != null) #types: types,
if (favourites != null) #favourites: favourites,
if (sortingOption != null) #sortingOption: sortingOption,
if (sortOrder != null) #sortOrder: sortOrder,
if (hideEmtpyShows != null) #hideEmtpyShows: hideEmtpyShows,
if (recursive != null) #recursive: recursive,
if (groupBy != null) #groupBy: groupBy,
if (lastIndices != null) #lastIndices: lastIndices,
if (libraryItemCounts != null) #libraryItemCounts: libraryItemCounts,
if (fetchingItems != null) #fetchingItems: fetchingItems
}));
@override
LibrarySearchModel $make(CopyWithData data) => LibrarySearchModel(
loading: data.get(#loading, or: $value.loading),
selecteMode: data.get(#selecteMode, or: $value.selecteMode),
folderOverwrite: data.get(#folderOverwrite, or: $value.folderOverwrite),
searchQuery: data.get(#searchQuery, or: $value.searchQuery),
views: data.get(#views, or: $value.views),
posters: data.get(#posters, or: $value.posters),
selectedPosters: data.get(#selectedPosters, or: $value.selectedPosters),
filters: data.get(#filters, or: $value.filters),
genres: data.get(#genres, or: $value.genres),
studios: data.get(#studios, or: $value.studios),
tags: data.get(#tags, or: $value.tags),
years: data.get(#years, or: $value.years),
officialRatings: data.get(#officialRatings, or: $value.officialRatings),
types: data.get(#types, or: $value.types),
favourites: data.get(#favourites, or: $value.favourites),
sortingOption: data.get(#sortingOption, or: $value.sortingOption),
sortOrder: data.get(#sortOrder, or: $value.sortOrder),
hideEmtpyShows: data.get(#hideEmtpyShows, or: $value.hideEmtpyShows),
recursive: data.get(#recursive, or: $value.recursive),
groupBy: data.get(#groupBy, or: $value.groupBy),
lastIndices: data.get(#lastIndices, or: $value.lastIndices),
libraryItemCounts:
data.get(#libraryItemCounts, or: $value.libraryItemCounts),
fetchingItems: data.get(#fetchingItems, or: $value.fetchingItems));
@override
LibrarySearchModelCopyWith<$R2, LibrarySearchModel, $Out2> $chain<$R2, $Out2>(
Then<$Out2, $R2> t) =>
_LibrarySearchModelCopyWithImpl($value, $cast, t);
}

View file

@ -0,0 +1,127 @@
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:flutter/material.dart';
enum SortingOptions {
name([ItemSortBy.name]),
communityRating([ItemSortBy.communityrating]),
// criticsRating([ItemSortBy.criticrating]),
parentalRating([ItemSortBy.officialrating]),
dateAdded([ItemSortBy.datecreated]),
dateLastContentAdded([ItemSortBy.datelastcontentadded]),
favorite([ItemSortBy.isfavoriteorliked]),
datePlayed([ItemSortBy.dateplayed]),
folders([ItemSortBy.isfolder]),
playCount([ItemSortBy.playcount]),
releaseDate([ItemSortBy.productionyear, ItemSortBy.premieredate]),
runTime([ItemSortBy.runtime]),
random([ItemSortBy.random]);
const SortingOptions(this.value);
final List<ItemSortBy> value;
List<ItemSortBy> get toSortBy => [...value, ItemSortBy.name];
String label(BuildContext context) => switch (this) {
name => context.localized.name,
communityRating => context.localized.communityRating,
parentalRating => context.localized.parentalRating,
dateAdded => context.localized.dateAdded,
dateLastContentAdded => context.localized.dateLastContentAdded,
favorite => context.localized.favorite,
datePlayed => context.localized.datePlayed,
folders => context.localized.folders,
playCount => context.localized.playCount,
releaseDate => context.localized.releaseDate,
runTime => context.localized.runTime,
random => context.localized.random,
};
}
enum GroupBy {
none,
name,
genres,
dateAdded,
tags,
releaseDate,
rating,
type;
String value(BuildContext context) => switch (this) {
GroupBy.none => context.localized.none,
GroupBy.name => context.localized.name,
GroupBy.genres => context.localized.genre(1),
GroupBy.dateAdded => context.localized.dateAdded,
GroupBy.tags => context.localized.tag(1),
GroupBy.releaseDate => context.localized.releaseDate,
GroupBy.rating => context.localized.rating(1),
GroupBy.type => context.localized.type(1),
};
}
enum SortingOrder {
ascending,
descending;
SortOrder get sortOrder => switch (this) {
ascending => SortOrder.ascending,
descending => SortOrder.descending,
};
String label(BuildContext context) => switch (this) {
ascending => context.localized.ascending,
descending => context.localized.descending,
};
}
extension ItemFilterExtension on ItemFilter {
String label(BuildContext context) {
return switch (this) {
ItemFilter.isplayed => context.localized.played,
ItemFilter.isunplayed => context.localized.unPlayed,
ItemFilter.isresumable => context.localized.resumable,
_ => "",
};
}
}
int sortItems(ItemBaseModel a, ItemBaseModel b, SortingOptions sortingOption, SortingOrder sortingOrder) {
for (var sortBy in sortingOption.toSortBy) {
int comparison = 0;
switch (sortBy) {
case ItemSortBy.communityrating:
comparison = (a.overview.communityRating ?? 0).compareTo(b.overview.communityRating ?? 0);
break;
case ItemSortBy.isfavoriteorliked:
comparison = a.userData.isFavourite == b.userData.isFavourite
? 0
: a.userData.isFavourite
? 1
: -1;
break;
case ItemSortBy.dateplayed:
comparison = (a.userData.lastPlayed ?? DateTime(0)).compareTo(b.userData.lastPlayed ?? DateTime(0));
break;
case ItemSortBy.playcount:
comparison = a.userData.playCount.compareTo(b.userData.playCount);
break;
case ItemSortBy.premieredate:
case ItemSortBy.productionyear:
comparison = (a.overview.productionYear ?? 0).compareTo(b.overview.productionYear ?? 0);
break;
case ItemSortBy.runtime:
comparison = (a.overview.runTime ?? Duration.zero).compareTo(b.overview.runTime ?? Duration.zero);
break;
default:
comparison = a.name.compareTo(b.name);
}
if (comparison != 0) {
return sortingOrder == SortingOrder.ascending ? comparison : -comparison;
}
}
return 0;
}

View file

@ -0,0 +1,26 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:fladder/models/account_model.dart';
import 'package:fladder/models/credentials_model.dart';
class LoginScreenModel {
final List<AccountModel> accounts;
final CredentialsModel tempCredentials;
final bool loading;
LoginScreenModel({
required this.accounts,
required this.tempCredentials,
required this.loading,
});
LoginScreenModel copyWith({
List<AccountModel>? accounts,
CredentialsModel? tempCredentials,
bool? loading,
}) {
return LoginScreenModel(
accounts: accounts ?? this.accounts,
tempCredentials: tempCredentials ?? this.tempCredentials,
loading: loading ?? this.loading,
);
}
}

View file

@ -0,0 +1,52 @@
enum VideoPlayerState {
minimized,
fullScreen,
disposed,
}
class MediaPlaybackModel {
final VideoPlayerState state;
final bool playing;
final Duration position;
final Duration lastPosition;
final Duration duration;
final Duration buffer;
final bool completed;
final bool errorPlaying;
final bool buffering;
MediaPlaybackModel({
this.state = VideoPlayerState.disposed,
this.playing = false,
this.position = Duration.zero,
this.lastPosition = Duration.zero,
this.duration = Duration.zero,
this.buffer = Duration.zero,
this.completed = false,
this.errorPlaying = false,
this.buffering = false,
});
MediaPlaybackModel copyWith({
VideoPlayerState? state,
bool? playing,
Duration? position,
Duration? lastPosition,
Duration? duration,
Duration? buffer,
bool? completed,
bool? errorPlaying,
bool? buffering,
}) {
return MediaPlaybackModel(
state: state ?? this.state,
playing: playing ?? this.playing,
position: position ?? this.position,
lastPosition: lastPosition ?? this.lastPosition,
duration: duration ?? this.duration,
buffer: buffer ?? this.buffer,
completed: completed ?? this.completed,
errorPlaying: errorPlaying ?? this.errorPlaying,
buffering: buffering ?? this.buffering,
);
}
}

View file

@ -0,0 +1,209 @@
import 'package:collection/collection.dart';
import 'package:fladder/models/items/trick_play_model.dart';
import 'package:fladder/util/list_extensions.dart';
import 'package:fladder/wrappers/media_control_wrapper.dart'
if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:media_kit/media_kit.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/models/items/intro_skip_model.dart';
import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/playback/playback_model.dart';
import 'package:fladder/providers/api_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/util/duration_extensions.dart';
class DirectPlaybackModel implements PlaybackModel {
DirectPlaybackModel({
required this.item,
required this.media,
required this.playbackInfo,
this.mediaStreams,
this.introSkipModel,
this.chapters,
this.trickPlay,
this.queue = const [],
});
@override
final ItemBaseModel item;
@override
final Media? media;
@override
final PlaybackInfoResponse playbackInfo;
@override
final MediaStreamsModel? mediaStreams;
@override
final IntroOutSkipModel? introSkipModel;
@override
final List<Chapter>? chapters;
@override
final TrickPlayModel? trickPlay;
@override
ItemBaseModel? get nextVideo => queue.nextOrNull(item);
@override
ItemBaseModel? get previousVideo => queue.previousOrNull(item);
@override
Future<Duration>? startDuration() async => item.userData.playBackPosition;
@override
List<SubStreamModel> get subStreams => [SubStreamModel.no(), ...mediaStreams?.subStreams ?? []];
List<QueueItem> get itemsInQueue =>
queue.mapIndexed((index, element) => QueueItem(id: element.id, playlistItemId: "playlistItem$index")).toList();
@override
Future<DirectPlaybackModel> setSubtitle(SubStreamModel? model, MediaControlsWrapper player) async {
final wantedSubtitle =
model ?? subStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultSubStreamIndex);
if (wantedSubtitle == null) return this;
if (wantedSubtitle.index == SubStreamModel.no().index) {
await player.setSubtitleTrack(SubtitleTrack.no());
} else {
final subTracks = player.subTracks.getRange(2, player.subTracks.length).toList();
final index = subStreams.sublist(1).indexWhere((element) => element.id == wantedSubtitle.id);
final subTrack = subTracks.elementAtOrNull(index);
if (wantedSubtitle.isExternal && wantedSubtitle.url != null && subTrack == null) {
await player.setSubtitleTrack(SubtitleTrack.uri(wantedSubtitle.url!));
} else if (subTrack != null) {
await player.setSubtitleTrack(subTrack);
}
}
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: wantedSubtitle.index));
}
@override
List<AudioStreamModel> get audioStreams => [AudioStreamModel.no(), ...mediaStreams?.audioStreams ?? []];
@override
Future<DirectPlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) async {
final wantedAudioStream =
model ?? audioStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultAudioStreamIndex);
if (wantedAudioStream == null) return this;
if (wantedAudioStream.index == AudioStreamModel.no().index) {
await player.setAudioTrack(AudioTrack.no());
} else {
final audioTracks = player.audioTracks.getRange(2, player.audioTracks.length).toList();
final audioTrack = audioTracks.elementAtOrNull(audioStreams.indexOf(wantedAudioStream) - 1);
if (audioTrack != null) {
await player.setAudioTrack(audioTrack);
}
}
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: wantedAudioStream.index));
}
@override
Future<PlaybackModel?> playbackStarted(Duration position, Ref ref) async {
await ref.read(jellyApiProvider).sessionsPlayingPost(
body: PlaybackStartInfo(
canSeek: true,
itemId: item.id,
mediaSourceId: item.id,
playSessionId: playbackInfo.playSessionId,
subtitleStreamIndex: item.streamModel?.defaultSubStreamIndex,
audioStreamIndex: item.streamModel?.defaultAudioStreamIndex,
volumeLevel: 100,
playbackStartTimeTicks: position.toRuntimeTicks,
playMethod: PlayMethod.directplay,
isMuted: false,
isPaused: false,
repeatMode: RepeatMode.repeatall,
nowPlayingQueue: itemsInQueue,
),
);
return null;
}
@override
Future<PlaybackModel?> playbackStopped(Duration position, Duration? totalDuration, Ref ref) async {
ref.read(playBackModel.notifier).update((state) => null);
await ref.read(jellyApiProvider).sessionsPlayingStoppedPost(
body: PlaybackStopInfo(
itemId: item.id,
mediaSourceId: item.id,
playSessionId: playbackInfo.playSessionId,
positionTicks: position.toRuntimeTicks,
failed: false,
nowPlayingQueue: itemsInQueue,
),
totalDuration: totalDuration,
);
return null;
}
@override
Future<PlaybackModel?> updatePlaybackPosition(Duration position, bool isPlaying, Ref ref) async {
final api = ref.read(jellyApiProvider);
//Check for newly generated scrubImages
if (trickPlay == null) {
final trickplay = await api.getTrickPlay(item: item, ref: ref);
ref.read(playBackModel.notifier).update((state) => copyWith(trickPlay: () => trickplay?.body));
}
await api.sessionsPlayingProgressPost(
body: PlaybackProgressInfo(
canSeek: true,
itemId: item.id,
mediaSourceId: item.id,
playSessionId: playbackInfo.playSessionId,
subtitleStreamIndex: item.streamModel?.defaultSubStreamIndex,
audioStreamIndex: item.streamModel?.defaultAudioStreamIndex,
volumeLevel: 100,
playMethod: PlayMethod.directplay,
isPaused: !isPlaying,
isMuted: false,
positionTicks: position.toRuntimeTicks,
repeatMode: RepeatMode.repeatall,
nowPlayingQueue: itemsInQueue,
),
);
return null;
}
@override
String toString() => 'DirectPlaybackModel(item: $item, playbackInfo: $playbackInfo)';
@override
final List<ItemBaseModel> queue;
@override
DirectPlaybackModel copyWith({
ItemBaseModel? item,
ValueGetter<Media?>? media,
ValueGetter<Duration>? lastPosition,
PlaybackInfoResponse? playbackInfo,
ValueGetter<MediaStreamsModel?>? mediaStreams,
ValueGetter<IntroOutSkipModel?>? introSkipModel,
ValueGetter<List<Chapter>?>? chapters,
ValueGetter<TrickPlayModel?>? trickPlay,
List<ItemBaseModel>? queue,
}) {
return DirectPlaybackModel(
item: item ?? this.item,
media: media != null ? media() : this.media,
playbackInfo: playbackInfo ?? this.playbackInfo,
mediaStreams: mediaStreams != null ? mediaStreams() : this.mediaStreams,
introSkipModel: introSkipModel != null ? introSkipModel() : this.introSkipModel,
chapters: chapters != null ? chapters() : this.chapters,
trickPlay: trickPlay != null ? trickPlay() : this.trickPlay,
queue: queue ?? this.queue,
);
}
}

View file

@ -0,0 +1,175 @@
import 'package:collection/collection.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/items/trick_play_model.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/util/list_extensions.dart';
import 'package:fladder/wrappers/media_control_wrapper.dart'
if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:media_kit/media_kit.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/models/items/intro_skip_model.dart';
import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/playback/playback_model.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/util/duration_extensions.dart';
class OfflinePlaybackModel implements PlaybackModel {
OfflinePlaybackModel({
required this.item,
required this.media,
required this.syncedItem,
this.mediaStreams,
this.playbackInfo,
this.introSkipModel,
this.trickPlay,
this.queue = const [],
this.syncedQueue = const [],
});
@override
final ItemBaseModel item;
@override
final PlaybackInfoResponse? playbackInfo;
@override
final Media? media;
final SyncedItem syncedItem;
@override
final MediaStreamsModel? mediaStreams;
@override
final IntroOutSkipModel? introSkipModel;
@override
List<Chapter>? get chapters => syncedItem.chapters;
@override
final TrickPlayModel? trickPlay;
@override
Future<Duration>? startDuration() async => item.userData.playBackPosition;
@override
ItemBaseModel? get nextVideo => queue.nextOrNull(item);
@override
ItemBaseModel? get previousVideo => queue.previousOrNull(item);
@override
List<SubStreamModel> get subStreams => [SubStreamModel.no(), ...syncedItem.subtitles];
@override
Future<OfflinePlaybackModel> setSubtitle(SubStreamModel? model, MediaControlsWrapper player) async {
final wantedSubtitle =
model ?? subStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultSubStreamIndex);
if (wantedSubtitle == null) return this;
if (wantedSubtitle.index == SubStreamModel.no().index) {
await player.setSubtitleTrack(SubtitleTrack.no());
} else {
final subTracks = player.subTracks.getRange(2, player.subTracks.length).toList();
final index = subStreams.sublist(1).indexWhere((element) => element.id == wantedSubtitle.id);
final subTrack = subTracks.elementAtOrNull(index);
if (wantedSubtitle.isExternal && wantedSubtitle.url != null && subTrack == null) {
await player.setSubtitleTrack(SubtitleTrack.uri(wantedSubtitle.url!));
} else if (subTrack != null) {
await player.setSubtitleTrack(subTrack);
}
}
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: wantedSubtitle.index));
}
@override
List<AudioStreamModel> get audioStreams => [AudioStreamModel.no(), ...mediaStreams?.audioStreams ?? []];
@override
Future<OfflinePlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) async {
final wantedAudioStream =
model ?? audioStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultAudioStreamIndex);
if (wantedAudioStream == null) return this;
if (wantedAudioStream.index == AudioStreamModel.no().index) {
await player.setAudioTrack(AudioTrack.no());
} else {
final audioTracks = player.audioTracks.getRange(2, player.audioTracks.length).toList();
final audioTrack = audioTracks.elementAtOrNull(audioStreams.indexOf(wantedAudioStream) - 1);
if (audioTrack != null) {
await player.setAudioTrack(audioTrack);
}
}
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: wantedAudioStream.index));
}
@override
Future<PlaybackModel?> playbackStarted(Duration position, Ref ref) async {
return null;
}
@override
Future<PlaybackModel?> playbackStopped(Duration position, Duration? totalDuration, Ref ref) async {
return null;
}
@override
Future<PlaybackModel?> updatePlaybackPosition(Duration position, bool isPlaying, Ref ref) async {
final progress = position.inMilliseconds / (item.overview.runTime?.inMilliseconds ?? 0) * 100;
final newItem = syncedItem.copyWith(
userData: syncedItem.userData?.copyWith(
playbackPositionTicks: position.toRuntimeTicks,
progress: progress,
played: isPlayed(position, item.overview.runTime ?? Duration.zero),
),
);
await ref.read(syncProvider.notifier).updateItem(newItem);
return this;
}
bool isPlayed(Duration position, Duration totalDuration) {
Duration startBuffer = totalDuration * 0.05;
Duration endBuffer = totalDuration * 0.90;
Duration validStart = startBuffer;
Duration validEnd = endBuffer;
if (position >= validStart && position <= validEnd) {
return true;
}
return false;
}
@override
String toString() => 'OfflinePlaybackModel(item: $item, syncedItem: $syncedItem)';
@override
final List<ItemBaseModel> queue;
final List<SyncedItem> syncedQueue;
@override
OfflinePlaybackModel copyWith({
ItemBaseModel? item,
ValueGetter<Media?>? media,
SyncedItem? syncedItem,
ValueGetter<MediaStreamsModel?>? mediaStreams,
ValueGetter<IntroOutSkipModel?>? introSkipModel,
ValueGetter<TrickPlayModel?>? trickPlay,
List<ItemBaseModel>? queue,
List<SyncedItem>? syncedQueue,
}) {
return OfflinePlaybackModel(
item: item ?? this.item,
media: media != null ? media() : this.media,
syncedItem: syncedItem ?? this.syncedItem,
mediaStreams: mediaStreams != null ? mediaStreams() : this.mediaStreams,
introSkipModel: introSkipModel != null ? introSkipModel() : this.introSkipModel,
trickPlay: trickPlay != null ? trickPlay() : this.trickPlay,
queue: queue ?? this.queue,
syncedQueue: syncedQueue ?? this.syncedQueue,
);
}
}

View file

@ -0,0 +1,370 @@
import 'dart:developer';
import 'package:chopper/chopper.dart';
import 'package:collection/collection.dart';
import 'package:fladder/models/items/intro_skip_model.dart';
import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/models/items/series_model.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:media_kit/media_kit.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/items/trick_play_model.dart';
import 'package:fladder/models/playback/direct_playback_model.dart';
import 'package:fladder/models/playback/offline_playback_model.dart';
import 'package:fladder/models/playback/transcode_playback_model.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/models/video_stream_model.dart';
import 'package:fladder/profiles/default_profile.dart';
import 'package:fladder/providers/api_provider.dart';
import 'package:fladder/providers/service_provider.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/util/duration_extensions.dart';
import 'package:fladder/wrappers/media_control_wrapper.dart'
if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart';
extension PlaybackModelExtension on PlaybackModel? {
String? get label => switch (this) {
DirectPlaybackModel _ => PlaybackType.directStream.name,
TranscodePlaybackModel _ => PlaybackType.transcode.name,
OfflinePlaybackModel _ => PlaybackType.offline.name,
_ => null
};
}
abstract class PlaybackModel {
final ItemBaseModel item = throw UnimplementedError();
final Media? media = throw UnimplementedError();
final List<ItemBaseModel> queue = const [];
final IntroOutSkipModel? introSkipModel = null;
final PlaybackInfoResponse? playbackInfo = throw UnimplementedError();
List<Chapter>? get chapters;
TrickPlayModel? get trickPlay;
Future<PlaybackModel?> updatePlaybackPosition(Duration position, bool isPlaying, Ref ref) =>
throw UnimplementedError();
Future<PlaybackModel?> playbackStarted(Duration position, Ref ref) => throw UnimplementedError();
Future<PlaybackModel?> playbackStopped(Duration position, Duration? totalDuration, Ref ref) =>
throw UnimplementedError();
final MediaStreamsModel? mediaStreams = throw UnimplementedError();
List<SubStreamModel>? get subStreams;
List<AudioStreamModel>? get audioStreams;
Future<Duration>? startDuration() async => item.userData.playBackPosition;
Future<PlaybackModel>? setSubtitle(SubStreamModel? model, MediaControlsWrapper player) {
return null;
}
Future<PlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) => null;
ItemBaseModel? get nextVideo => throw UnimplementedError();
ItemBaseModel? get previousVideo => throw UnimplementedError();
PlaybackModel copyWith();
}
final playbackModelHelper = Provider<PlaybackModelHelper>((ref) {
return PlaybackModelHelper(ref: ref);
});
class PlaybackModelHelper {
const PlaybackModelHelper({required this.ref});
final Ref ref;
JellyService get api => ref.read(jellyApiProvider);
Future<PlaybackModel?> loadNewVideo(ItemBaseModel newItem) async {
ref.read(videoPlayerProvider).pause();
ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(buffering: true));
final currentModel = ref.read(playBackModel);
final newModel = (await createServerPlaybackModel(
newItem,
null,
oldModel: currentModel,
)) ??
await createOfflinePlaybackModel(
newItem,
ref.read(syncProvider.notifier).getSyncedItem(newItem),
oldModel: currentModel,
);
if (newModel == null) return null;
ref.read(videoPlayerProvider.notifier).loadPlaybackItem(newModel, startPosition: Duration.zero);
return newModel;
}
Future<OfflinePlaybackModel?> createOfflinePlaybackModel(
ItemBaseModel item,
SyncedItem? syncedItem, {
PlaybackModel? oldModel,
}) async {
final ItemBaseModel? syncedItemModel = ref.read(syncProvider.notifier).getItem(syncedItem);
if (syncedItemModel == null || syncedItem == null || !syncedItem.dataFile.existsSync()) return null;
final children = ref.read(syncChildrenProvider(syncedItem));
final syncedItems = children.where((element) => element.videoFile.existsSync()).toList();
final itemQueue = syncedItems.map((e) => e.createItemModel(ref));
return OfflinePlaybackModel(
item: syncedItemModel,
syncedItem: syncedItem,
trickPlay: syncedItem.trickPlayModel,
introSkipModel: syncedItem.introOutSkipModel,
media: Media(syncedItem.videoFile.path),
queue: itemQueue.whereNotNull().toList(),
syncedQueue: children,
mediaStreams: item.streamModel ?? syncedItemModel.streamModel,
);
}
Future<EpisodeModel?> getNextUpEpisode(String itemId) async {
final responnse = await api.showsNextUpGet(parentId: itemId, fields: [ItemFields.overview]);
final episode = responnse.body?.items?.firstOrNull;
if (episode == null) {
return null;
} else {
return EpisodeModel.fromBaseDto(episode, ref);
}
}
Future<PlaybackModel?> createServerPlaybackModel(ItemBaseModel? item, PlaybackType? type,
{PlaybackModel? oldModel, List<ItemBaseModel>? libraryQueue, Duration? startPosition}) async {
try {
if (item == null) return null;
final userId = ref.read(userProvider)?.id;
if (userId?.isEmpty == true) return null;
final queue = oldModel?.queue ?? libraryQueue ?? await collectQueue(item);
final firstItemToPlay = switch (item) {
SeriesModel _ || SeasonModel _ => (await getNextUpEpisode(item.id) ?? queue.first),
_ => item,
};
final fullItem = await api.usersUserIdItemsItemIdGet(itemId: firstItemToPlay.id);
final streamModel = firstItemToPlay.streamModel;
Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
itemId: firstItemToPlay.id,
enableDirectPlay: type != PlaybackType.transcode,
enableDirectStream: type != PlaybackType.transcode,
enableTranscoding: true,
autoOpenLiveStream: true,
startTimeTicks: startPosition?.toRuntimeTicks,
audioStreamIndex: streamModel?.defaultAudioStreamIndex,
subtitleStreamIndex: streamModel?.defaultSubStreamIndex,
mediaSourceId: firstItemToPlay.id,
body: PlaybackInfoDto(
startTimeTicks: startPosition?.toRuntimeTicks,
audioStreamIndex: streamModel?.defaultAudioStreamIndex,
subtitleStreamIndex: streamModel?.defaultSubStreamIndex,
enableTranscoding: true,
autoOpenLiveStream: true,
deviceProfile: defaultProfile,
userId: userId,
mediaSourceId: firstItemToPlay.id,
),
);
PlaybackInfoResponse? playbackInfo = response.body;
if (playbackInfo == null) return null;
final mediaSource = playbackInfo.mediaSources?.first;
final mediaStreamsWithUrls = MediaStreamsModel.fromMediaStreamsList(
playbackInfo.mediaSources?.firstOrNull, playbackInfo.mediaSources?.firstOrNull?.mediaStreams ?? [], ref)
.copyWith(
defaultAudioStreamIndex: streamModel?.defaultAudioStreamIndex,
defaultSubStreamIndex: streamModel?.defaultSubStreamIndex,
);
final intro = await api.introSkipGet(id: item.id);
final trickPlay = (await api.getTrickPlay(item: fullItem.body, ref: ref))?.body;
final chapters = fullItem.body?.overview.chapters ?? [];
if (mediaSource == null) return null;
if ((mediaSource.supportsDirectStream ?? false) || (mediaSource.supportsDirectPlay ?? false)) {
final Map<String, String?> directOptions = {
'Static': 'true',
'mediaSourceId': mediaSource.id,
'api_key': ref.read(userProvider)?.credentials.token,
};
if (mediaSource.eTag != null) {
directOptions['Tag'] = mediaSource.eTag;
}
if (mediaSource.liveStreamId != null) {
directOptions['LiveStreamId'] = mediaSource.liveStreamId;
}
final params = Uri(queryParameters: directOptions).query;
return DirectPlaybackModel(
item: fullItem.body ?? item,
queue: queue,
introSkipModel: intro?.body,
chapters: chapters,
playbackInfo: playbackInfo,
trickPlay: trickPlay,
media: Media('${ref.read(userProvider)?.server ?? ""}/Videos/${mediaSource.id}/stream?$params'),
mediaStreams: mediaStreamsWithUrls,
);
} else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) {
return TranscodePlaybackModel(
item: fullItem.body ?? item,
queue: queue,
introSkipModel: intro?.body,
chapters: chapters,
trickPlay: trickPlay,
playbackInfo: playbackInfo,
media: Media("${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"),
mediaStreams: mediaStreamsWithUrls,
);
}
return null;
} catch (e) {
log(e.toString());
return null;
}
}
Future<List<ItemBaseModel>> collectQueue(ItemBaseModel model) async {
switch (model) {
case EpisodeModel _:
case SeriesModel _:
case SeasonModel _:
List<EpisodeModel> episodeList = ((await fetchEpisodesFromSeries(model.streamId)).body ?? [])
..removeWhere((element) => element.status != EpisodeStatus.available);
return episodeList;
default:
return [];
}
}
Future<Response<List<EpisodeModel>>> fetchEpisodesFromSeries(String seriesId) async {
final response = await api.showsSeriesIdEpisodesGet(
seriesId: seriesId,
fields: [
ItemFields.overview,
ItemFields.originaltitle,
ItemFields.mediastreams,
ItemFields.mediasources,
ItemFields.mediasourcecount,
ItemFields.width,
ItemFields.height,
],
);
return Response(response.base, (response.body?.items?.map((e) => EpisodeModel.fromBaseDto(e, ref)).toList() ?? []));
}
Future<void> shouldReload(PlaybackModel playbackModel) async {
if (playbackModel is OfflinePlaybackModel) {
return;
}
final item = playbackModel.item;
final userId = ref.read(userProvider)?.id;
if (userId?.isEmpty == true) return;
final currentPosition = ref.read(mediaPlaybackProvider.select((value) => value.position));
final audioIndex = playbackModel.mediaStreams?.defaultAudioStreamIndex;
final subIndex = playbackModel.mediaStreams?.defaultSubStreamIndex;
Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
itemId: item.id,
enableDirectPlay: true,
enableDirectStream: true,
enableTranscoding: true,
autoOpenLiveStream: true,
startTimeTicks: currentPosition.toRuntimeTicks,
audioStreamIndex: audioIndex,
subtitleStreamIndex: subIndex,
mediaSourceId: item.id,
body: PlaybackInfoDto(
startTimeTicks: currentPosition.toRuntimeTicks,
audioStreamIndex: audioIndex,
subtitleStreamIndex: subIndex,
enableTranscoding: true,
autoOpenLiveStream: true,
deviceProfile: defaultProfile,
userId: userId,
mediaSourceId: item.id,
),
);
PlaybackInfoResponse playbackInfo = response.bodyOrThrow;
final mediaSource = playbackInfo.mediaSources?.first;
final mediaStreamsWithUrls = MediaStreamsModel.fromMediaStreamsList(
playbackInfo.mediaSources?.firstOrNull, playbackInfo.mediaSources?.firstOrNull?.mediaStreams ?? [], ref)
.copyWith(
defaultAudioStreamIndex: audioIndex,
defaultSubStreamIndex: subIndex,
);
if (mediaSource == null) return;
PlaybackModel? newModel;
if ((mediaSource.supportsDirectStream ?? false) || (mediaSource.supportsDirectPlay ?? false)) {
final Map<String, String?> directOptions = {
'Static': 'true',
'mediaSourceId': mediaSource.id,
'api_key': ref.read(userProvider)?.credentials.token,
};
if (mediaSource.eTag != null) {
directOptions['Tag'] = mediaSource.eTag;
}
if (mediaSource.liveStreamId != null) {
directOptions['LiveStreamId'] = mediaSource.liveStreamId;
}
final params = Uri(queryParameters: directOptions).query;
final directPlay = '${ref.read(userProvider)?.server ?? ""}/Videos/${mediaSource.id}/stream?$params';
newModel = DirectPlaybackModel(
item: playbackModel.item,
queue: playbackModel.queue,
introSkipModel: playbackModel.introSkipModel,
chapters: playbackModel.chapters,
playbackInfo: playbackInfo,
trickPlay: playbackModel.trickPlay,
media: Media(directPlay),
mediaStreams: mediaStreamsWithUrls,
);
} else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) {
newModel = TranscodePlaybackModel(
item: playbackModel.item,
queue: playbackModel.queue,
introSkipModel: playbackModel.introSkipModel,
chapters: playbackModel.chapters,
playbackInfo: playbackInfo,
trickPlay: playbackModel.trickPlay,
media: Media("${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"),
mediaStreams: mediaStreamsWithUrls,
);
}
if (newModel == null) return;
if (newModel.runtimeType != playbackModel.runtimeType || newModel is TranscodePlaybackModel) {
ref.read(videoPlayerProvider.notifier).loadPlaybackItem(newModel, startPosition: currentPosition);
}
}
}

View file

@ -0,0 +1,210 @@
import 'package:collection/collection.dart';
import 'package:fladder/models/items/trick_play_model.dart';
import 'package:fladder/util/list_extensions.dart';
import 'package:fladder/wrappers/media_control_wrapper.dart'
if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:media_kit/media_kit.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/models/items/intro_skip_model.dart';
import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/playback/playback_model.dart';
import 'package:fladder/providers/api_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/util/duration_extensions.dart';
class TranscodePlaybackModel implements PlaybackModel {
TranscodePlaybackModel({
required this.item,
required this.media,
required this.playbackInfo,
this.mediaStreams,
this.introSkipModel,
this.chapters,
this.trickPlay,
this.queue = const [],
});
@override
final ItemBaseModel item;
@override
final Media? media;
@override
final PlaybackInfoResponse playbackInfo;
@override
final MediaStreamsModel? mediaStreams;
@override
final IntroOutSkipModel? introSkipModel;
@override
final List<Chapter>? chapters;
@override
final TrickPlayModel? trickPlay;
@override
ItemBaseModel? get nextVideo => queue.nextOrNull(item);
@override
ItemBaseModel? get previousVideo => queue.previousOrNull(item);
@override
Future<Duration>? startDuration() async => item.userData.playBackPosition;
@override
List<SubStreamModel> get subStreams => [SubStreamModel.no(), ...mediaStreams?.subStreams ?? []];
List<QueueItem> get itemsInQueue =>
queue.mapIndexed((index, element) => QueueItem(id: element.id, playlistItemId: "playlistItem$index")).toList();
@override
Future<TranscodePlaybackModel> setSubtitle(SubStreamModel? model, MediaControlsWrapper player) async {
final wantedSubtitle =
model ?? subStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultSubStreamIndex);
if (wantedSubtitle == null) return this;
if (wantedSubtitle.index == SubStreamModel.no().index) {
await player.setSubtitleTrack(SubtitleTrack.no());
} else {
final subTracks = player.subTracks.getRange(2, player.subTracks.length).toList();
final index = subStreams.sublist(1).indexWhere((element) => element.id == wantedSubtitle.id);
final subTrack = subTracks.elementAtOrNull(index);
if (wantedSubtitle.isExternal && wantedSubtitle.url != null && subTrack == null) {
await player.setSubtitleTrack(SubtitleTrack.uri(wantedSubtitle.url!));
} else if (subTrack != null) {
await player.setSubtitleTrack(subTrack);
}
}
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: wantedSubtitle.index));
}
@override
List<AudioStreamModel> get audioStreams => [AudioStreamModel.no(), ...mediaStreams?.audioStreams ?? []];
@override
Future<TranscodePlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) async {
final wantedAudioStream =
model ?? audioStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultAudioStreamIndex);
if (wantedAudioStream == null) return this;
if (wantedAudioStream.index == AudioStreamModel.no().index) {
await player.setAudioTrack(AudioTrack.no());
} else {
final audioTracks = player.audioTracks.getRange(2, player.audioTracks.length).toList();
final audioTrack = audioTracks.elementAtOrNull(audioStreams.indexOf(wantedAudioStream) - 1);
if (audioTrack != null) {
await player.setAudioTrack(audioTrack);
}
}
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: wantedAudioStream.index));
}
@override
Future<PlaybackModel?> playbackStarted(Duration position, Ref ref) async {
await ref.read(jellyApiProvider).sessionsPlayingPost(
body: PlaybackStartInfo(
canSeek: true,
itemId: item.id,
mediaSourceId: item.id,
playSessionId: playbackInfo.playSessionId,
sessionId: playbackInfo.playSessionId,
subtitleStreamIndex: item.streamModel?.defaultSubStreamIndex,
audioStreamIndex: item.streamModel?.defaultAudioStreamIndex,
volumeLevel: 100,
playbackStartTimeTicks: position.toRuntimeTicks,
playMethod: PlayMethod.transcode,
isMuted: false,
isPaused: false,
repeatMode: RepeatMode.repeatall,
nowPlayingQueue: itemsInQueue,
),
);
return null;
}
@override
Future<PlaybackModel?> playbackStopped(Duration position, Duration? totalDuration, Ref ref) async {
ref.read(playBackModel.notifier).update((state) => null);
await ref.read(jellyApiProvider).sessionsPlayingStoppedPost(
body: PlaybackStopInfo(
itemId: item.id,
mediaSourceId: item.id,
playSessionId: playbackInfo.playSessionId,
positionTicks: position.toRuntimeTicks,
failed: false,
nowPlayingQueue: itemsInQueue,
),
totalDuration: totalDuration,
);
return null;
}
@override
Future<PlaybackModel?> updatePlaybackPosition(Duration position, bool isPlaying, Ref ref) async {
final api = ref.read(jellyApiProvider);
//Check for newly generated scrubImages
if (trickPlay == null) {
final trickplay = await api.getTrickPlay(item: item, ref: ref);
ref.read(playBackModel.notifier).update((state) => copyWith(trickPlay: () => trickplay?.bodyOrThrow));
}
await api.sessionsPlayingProgressPost(
body: PlaybackProgressInfo(
canSeek: true,
itemId: item.id,
mediaSourceId: item.id,
playSessionId: playbackInfo.playSessionId,
sessionId: playbackInfo.playSessionId,
subtitleStreamIndex: item.streamModel?.defaultSubStreamIndex,
audioStreamIndex: item.streamModel?.defaultAudioStreamIndex,
volumeLevel: 100,
positionTicks: position.toRuntimeTicks,
playMethod: PlayMethod.transcode,
isPaused: !isPlaying,
isMuted: false,
repeatMode: RepeatMode.repeatall,
nowPlayingQueue: itemsInQueue,
),
);
return this;
}
@override
String toString() => 'TranscodePlaybackModel(item: $item, playbackInfo: $playbackInfo)';
@override
final List<ItemBaseModel> queue;
@override
TranscodePlaybackModel copyWith({
ItemBaseModel? item,
ValueGetter<Media?>? media,
ValueGetter<Duration>? lastPosition,
PlaybackInfoResponse? playbackInfo,
ValueGetter<MediaStreamsModel?>? mediaStreams,
ValueGetter<IntroOutSkipModel?>? introSkipModel,
ValueGetter<List<Chapter>?>? chapters,
ValueGetter<TrickPlayModel?>? trickPlay,
List<ItemBaseModel>? queue,
}) {
return TranscodePlaybackModel(
item: item ?? this.item,
media: media != null ? media() : this.media,
playbackInfo: playbackInfo ?? this.playbackInfo,
mediaStreams: mediaStreams != null ? mediaStreams() : this.mediaStreams,
introSkipModel: introSkipModel != null ? introSkipModel() : this.introSkipModel,
chapters: chapters != null ? chapters() : this.chapters,
trickPlay: trickPlay != null ? trickPlay() : this.trickPlay,
queue: queue ?? this.queue,
);
}
}

View file

@ -0,0 +1,40 @@
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/overview_model.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class PlaylistModel extends ItemBaseModel {
PlaylistModel({
required super.name,
required super.id,
required super.overview,
required super.parentId,
required super.playlistId,
required super.images,
required super.childCount,
required super.primaryRatio,
required super.userData,
super.canDelete,
super.canDownload,
super.jellyType,
});
factory PlaylistModel.fromBaseDto(BaseItemDto item, Ref ref) {
return PlaylistModel(
name: item.name ?? "",
id: item.id ?? "",
childCount: item.childCount,
overview: OverviewModel.fromBaseItemDto(item, ref),
userData: UserData.fromDto(item.userData),
parentId: item.parentId,
playlistId: item.playlistItemId,
images: ImagesData.fromBaseItem(item, ref),
primaryRatio: item.primaryImageAspectRatio,
canDelete: item.canDelete,
canDownload: item.canDownload,
jellyType: item.type,
);
}
}

View file

@ -0,0 +1,25 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:fladder/models/item_base_model.dart';
class RecommendedModel {
final String name;
final List<ItemBaseModel> posters;
final String type;
RecommendedModel({
required this.name,
required this.posters,
required this.type,
});
RecommendedModel copyWith({
String? name,
List<ItemBaseModel>? posters,
String? type,
}) {
return RecommendedModel(
name: name ?? this.name,
posters: posters ?? this.posters,
type: type ?? this.type,
);
}
}

View file

@ -0,0 +1,28 @@
import 'package:fladder/models/item_base_model.dart';
class SearchModel {
final bool loading;
final String searchQuery;
final int resultCount;
final Map<FladderItemType, List<ItemBaseModel>> results;
SearchModel({
this.loading = false,
this.searchQuery = "",
this.resultCount = 0,
this.results = const {},
});
SearchModel copyWith({
bool? loading,
String? searchQuery,
int? resultCount,
Map<FladderItemType, List<ItemBaseModel>>? results,
}) {
return SearchModel(
loading: loading ?? this.loading,
searchQuery: searchQuery ?? this.searchQuery,
resultCount: resultCount ?? this.resultCount,
results: results ?? this.results,
);
}
}

View file

@ -0,0 +1,117 @@
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fladder/util/custom_color_themes.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'client_settings_model.freezed.dart';
part 'client_settings_model.g.dart';
@freezed
class ClientSettingsModel with _$ClientSettingsModel {
factory ClientSettingsModel({
String? syncPath,
@Default(Vector2(x: 0, y: 0)) Vector2 position,
@Default(Vector2(x: 1280, y: 720)) Vector2 size,
@Default(Duration(seconds: 30)) Duration? timeOut,
Duration? nextUpDateCutoff,
@Default(ThemeMode.system) ThemeMode themeMode,
ColorThemes? themeColor,
@Default(false) bool amoledBlack,
@Default(false) bool blurPlaceHolders,
@Default(false) bool blurUpcomingEpisodes,
@LocaleConvert() Locale? selectedLocale,
@Default(true) bool enableMediaKeys,
@Default(1.0) double posterSize,
@Default(false) bool pinchPosterZoom,
@Default(false) bool mouseDragSupport,
int? libraryPageSize,
}) = _ClientSettingsModel;
factory ClientSettingsModel.fromJson(Map<String, dynamic> json) => _$ClientSettingsModelFromJson(json);
}
class LocaleConvert implements JsonConverter<Locale?, String?> {
const LocaleConvert();
@override
Locale? fromJson(String? json) {
if (json == null) return null;
final parts = json.split('_');
if (parts.length == 1) {
return Locale(parts[0]);
} else if (parts.length == 2) {
return Locale(parts[0], parts[1]);
} else {
log("Invalid Locale format");
return null;
}
}
@override
String? toJson(Locale? object) {
if (object == null) return null;
if (object.countryCode == null || object.countryCode?.isEmpty == true) {
return object.languageCode;
}
return '${object.languageCode}_${object.countryCode}';
}
}
class Vector2 {
final double x;
final double y;
const Vector2({
required this.x,
required this.y,
});
Vector2 copyWith({
double? x,
double? y,
}) {
return Vector2(
x: x ?? this.x,
y: y ?? this.y,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'x': x,
'y': y,
};
}
factory Vector2.fromMap(Map<String, dynamic> map) {
return Vector2(
x: map['x'] as double,
y: map['y'] as double,
);
}
String toJson() => json.encode(toMap());
factory Vector2.fromJson(String source) => Vector2.fromMap(json.decode(source) as Map<String, dynamic>);
factory Vector2.fromSize(Size size) => Vector2(x: size.width, y: size.height);
@override
String toString() => 'Vector2(x: $x, y: $y)';
@override
bool operator ==(covariant Vector2 other) {
if (identical(this, other)) return true;
return other.x == x && other.y == y;
}
@override
int get hashCode => x.hashCode ^ y.hashCode;
static fromPosition(Offset windowPosition) => Vector2(x: windowPosition.dx, y: windowPosition.dy);
}

View file

@ -0,0 +1,526 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'client_settings_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
ClientSettingsModel _$ClientSettingsModelFromJson(Map<String, dynamic> json) {
return _ClientSettingsModel.fromJson(json);
}
/// @nodoc
mixin _$ClientSettingsModel {
String? get syncPath => throw _privateConstructorUsedError;
Vector2 get position => throw _privateConstructorUsedError;
Vector2 get size => throw _privateConstructorUsedError;
Duration? get timeOut => throw _privateConstructorUsedError;
Duration? get nextUpDateCutoff => throw _privateConstructorUsedError;
ThemeMode get themeMode => throw _privateConstructorUsedError;
ColorThemes? get themeColor => throw _privateConstructorUsedError;
bool get amoledBlack => throw _privateConstructorUsedError;
bool get blurPlaceHolders => throw _privateConstructorUsedError;
bool get blurUpcomingEpisodes => throw _privateConstructorUsedError;
@LocaleConvert()
Locale? get selectedLocale => throw _privateConstructorUsedError;
bool get enableMediaKeys => throw _privateConstructorUsedError;
double get posterSize => throw _privateConstructorUsedError;
bool get pinchPosterZoom => throw _privateConstructorUsedError;
bool get mouseDragSupport => throw _privateConstructorUsedError;
int? get libraryPageSize => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ClientSettingsModelCopyWith<ClientSettingsModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ClientSettingsModelCopyWith<$Res> {
factory $ClientSettingsModelCopyWith(
ClientSettingsModel value, $Res Function(ClientSettingsModel) then) =
_$ClientSettingsModelCopyWithImpl<$Res, ClientSettingsModel>;
@useResult
$Res call(
{String? syncPath,
Vector2 position,
Vector2 size,
Duration? timeOut,
Duration? nextUpDateCutoff,
ThemeMode themeMode,
ColorThemes? themeColor,
bool amoledBlack,
bool blurPlaceHolders,
bool blurUpcomingEpisodes,
@LocaleConvert() Locale? selectedLocale,
bool enableMediaKeys,
double posterSize,
bool pinchPosterZoom,
bool mouseDragSupport,
int? libraryPageSize});
}
/// @nodoc
class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
implements $ClientSettingsModelCopyWith<$Res> {
_$ClientSettingsModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? syncPath = freezed,
Object? position = null,
Object? size = null,
Object? timeOut = freezed,
Object? nextUpDateCutoff = freezed,
Object? themeMode = null,
Object? themeColor = freezed,
Object? amoledBlack = null,
Object? blurPlaceHolders = null,
Object? blurUpcomingEpisodes = null,
Object? selectedLocale = freezed,
Object? enableMediaKeys = null,
Object? posterSize = null,
Object? pinchPosterZoom = null,
Object? mouseDragSupport = null,
Object? libraryPageSize = freezed,
}) {
return _then(_value.copyWith(
syncPath: freezed == syncPath
? _value.syncPath
: syncPath // ignore: cast_nullable_to_non_nullable
as String?,
position: null == position
? _value.position
: position // ignore: cast_nullable_to_non_nullable
as Vector2,
size: null == size
? _value.size
: size // ignore: cast_nullable_to_non_nullable
as Vector2,
timeOut: freezed == timeOut
? _value.timeOut
: timeOut // ignore: cast_nullable_to_non_nullable
as Duration?,
nextUpDateCutoff: freezed == nextUpDateCutoff
? _value.nextUpDateCutoff
: nextUpDateCutoff // ignore: cast_nullable_to_non_nullable
as Duration?,
themeMode: null == themeMode
? _value.themeMode
: themeMode // ignore: cast_nullable_to_non_nullable
as ThemeMode,
themeColor: freezed == themeColor
? _value.themeColor
: themeColor // ignore: cast_nullable_to_non_nullable
as ColorThemes?,
amoledBlack: null == amoledBlack
? _value.amoledBlack
: amoledBlack // ignore: cast_nullable_to_non_nullable
as bool,
blurPlaceHolders: null == blurPlaceHolders
? _value.blurPlaceHolders
: blurPlaceHolders // ignore: cast_nullable_to_non_nullable
as bool,
blurUpcomingEpisodes: null == blurUpcomingEpisodes
? _value.blurUpcomingEpisodes
: blurUpcomingEpisodes // ignore: cast_nullable_to_non_nullable
as bool,
selectedLocale: freezed == selectedLocale
? _value.selectedLocale
: selectedLocale // ignore: cast_nullable_to_non_nullable
as Locale?,
enableMediaKeys: null == enableMediaKeys
? _value.enableMediaKeys
: enableMediaKeys // ignore: cast_nullable_to_non_nullable
as bool,
posterSize: null == posterSize
? _value.posterSize
: posterSize // ignore: cast_nullable_to_non_nullable
as double,
pinchPosterZoom: null == pinchPosterZoom
? _value.pinchPosterZoom
: pinchPosterZoom // ignore: cast_nullable_to_non_nullable
as bool,
mouseDragSupport: null == mouseDragSupport
? _value.mouseDragSupport
: mouseDragSupport // ignore: cast_nullable_to_non_nullable
as bool,
libraryPageSize: freezed == libraryPageSize
? _value.libraryPageSize
: libraryPageSize // ignore: cast_nullable_to_non_nullable
as int?,
) as $Val);
}
}
/// @nodoc
abstract class _$$ClientSettingsModelImplCopyWith<$Res>
implements $ClientSettingsModelCopyWith<$Res> {
factory _$$ClientSettingsModelImplCopyWith(_$ClientSettingsModelImpl value,
$Res Function(_$ClientSettingsModelImpl) then) =
__$$ClientSettingsModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String? syncPath,
Vector2 position,
Vector2 size,
Duration? timeOut,
Duration? nextUpDateCutoff,
ThemeMode themeMode,
ColorThemes? themeColor,
bool amoledBlack,
bool blurPlaceHolders,
bool blurUpcomingEpisodes,
@LocaleConvert() Locale? selectedLocale,
bool enableMediaKeys,
double posterSize,
bool pinchPosterZoom,
bool mouseDragSupport,
int? libraryPageSize});
}
/// @nodoc
class __$$ClientSettingsModelImplCopyWithImpl<$Res>
extends _$ClientSettingsModelCopyWithImpl<$Res, _$ClientSettingsModelImpl>
implements _$$ClientSettingsModelImplCopyWith<$Res> {
__$$ClientSettingsModelImplCopyWithImpl(_$ClientSettingsModelImpl _value,
$Res Function(_$ClientSettingsModelImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? syncPath = freezed,
Object? position = null,
Object? size = null,
Object? timeOut = freezed,
Object? nextUpDateCutoff = freezed,
Object? themeMode = null,
Object? themeColor = freezed,
Object? amoledBlack = null,
Object? blurPlaceHolders = null,
Object? blurUpcomingEpisodes = null,
Object? selectedLocale = freezed,
Object? enableMediaKeys = null,
Object? posterSize = null,
Object? pinchPosterZoom = null,
Object? mouseDragSupport = null,
Object? libraryPageSize = freezed,
}) {
return _then(_$ClientSettingsModelImpl(
syncPath: freezed == syncPath
? _value.syncPath
: syncPath // ignore: cast_nullable_to_non_nullable
as String?,
position: null == position
? _value.position
: position // ignore: cast_nullable_to_non_nullable
as Vector2,
size: null == size
? _value.size
: size // ignore: cast_nullable_to_non_nullable
as Vector2,
timeOut: freezed == timeOut
? _value.timeOut
: timeOut // ignore: cast_nullable_to_non_nullable
as Duration?,
nextUpDateCutoff: freezed == nextUpDateCutoff
? _value.nextUpDateCutoff
: nextUpDateCutoff // ignore: cast_nullable_to_non_nullable
as Duration?,
themeMode: null == themeMode
? _value.themeMode
: themeMode // ignore: cast_nullable_to_non_nullable
as ThemeMode,
themeColor: freezed == themeColor
? _value.themeColor
: themeColor // ignore: cast_nullable_to_non_nullable
as ColorThemes?,
amoledBlack: null == amoledBlack
? _value.amoledBlack
: amoledBlack // ignore: cast_nullable_to_non_nullable
as bool,
blurPlaceHolders: null == blurPlaceHolders
? _value.blurPlaceHolders
: blurPlaceHolders // ignore: cast_nullable_to_non_nullable
as bool,
blurUpcomingEpisodes: null == blurUpcomingEpisodes
? _value.blurUpcomingEpisodes
: blurUpcomingEpisodes // ignore: cast_nullable_to_non_nullable
as bool,
selectedLocale: freezed == selectedLocale
? _value.selectedLocale
: selectedLocale // ignore: cast_nullable_to_non_nullable
as Locale?,
enableMediaKeys: null == enableMediaKeys
? _value.enableMediaKeys
: enableMediaKeys // ignore: cast_nullable_to_non_nullable
as bool,
posterSize: null == posterSize
? _value.posterSize
: posterSize // ignore: cast_nullable_to_non_nullable
as double,
pinchPosterZoom: null == pinchPosterZoom
? _value.pinchPosterZoom
: pinchPosterZoom // ignore: cast_nullable_to_non_nullable
as bool,
mouseDragSupport: null == mouseDragSupport
? _value.mouseDragSupport
: mouseDragSupport // ignore: cast_nullable_to_non_nullable
as bool,
libraryPageSize: freezed == libraryPageSize
? _value.libraryPageSize
: libraryPageSize // ignore: cast_nullable_to_non_nullable
as int?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ClientSettingsModelImpl
with DiagnosticableTreeMixin
implements _ClientSettingsModel {
_$ClientSettingsModelImpl(
{this.syncPath,
this.position = const Vector2(x: 0, y: 0),
this.size = const Vector2(x: 1280, y: 720),
this.timeOut = const Duration(seconds: 30),
this.nextUpDateCutoff,
this.themeMode = ThemeMode.system,
this.themeColor,
this.amoledBlack = false,
this.blurPlaceHolders = false,
this.blurUpcomingEpisodes = false,
@LocaleConvert() this.selectedLocale,
this.enableMediaKeys = true,
this.posterSize = 1.0,
this.pinchPosterZoom = false,
this.mouseDragSupport = false,
this.libraryPageSize});
factory _$ClientSettingsModelImpl.fromJson(Map<String, dynamic> json) =>
_$$ClientSettingsModelImplFromJson(json);
@override
final String? syncPath;
@override
@JsonKey()
final Vector2 position;
@override
@JsonKey()
final Vector2 size;
@override
@JsonKey()
final Duration? timeOut;
@override
final Duration? nextUpDateCutoff;
@override
@JsonKey()
final ThemeMode themeMode;
@override
final ColorThemes? themeColor;
@override
@JsonKey()
final bool amoledBlack;
@override
@JsonKey()
final bool blurPlaceHolders;
@override
@JsonKey()
final bool blurUpcomingEpisodes;
@override
@LocaleConvert()
final Locale? selectedLocale;
@override
@JsonKey()
final bool enableMediaKeys;
@override
@JsonKey()
final double posterSize;
@override
@JsonKey()
final bool pinchPosterZoom;
@override
@JsonKey()
final bool mouseDragSupport;
@override
final int? libraryPageSize;
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, libraryPageSize: $libraryPageSize)';
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('type', 'ClientSettingsModel'))
..add(DiagnosticsProperty('syncPath', syncPath))
..add(DiagnosticsProperty('position', position))
..add(DiagnosticsProperty('size', size))
..add(DiagnosticsProperty('timeOut', timeOut))
..add(DiagnosticsProperty('nextUpDateCutoff', nextUpDateCutoff))
..add(DiagnosticsProperty('themeMode', themeMode))
..add(DiagnosticsProperty('themeColor', themeColor))
..add(DiagnosticsProperty('amoledBlack', amoledBlack))
..add(DiagnosticsProperty('blurPlaceHolders', blurPlaceHolders))
..add(DiagnosticsProperty('blurUpcomingEpisodes', blurUpcomingEpisodes))
..add(DiagnosticsProperty('selectedLocale', selectedLocale))
..add(DiagnosticsProperty('enableMediaKeys', enableMediaKeys))
..add(DiagnosticsProperty('posterSize', posterSize))
..add(DiagnosticsProperty('pinchPosterZoom', pinchPosterZoom))
..add(DiagnosticsProperty('mouseDragSupport', mouseDragSupport))
..add(DiagnosticsProperty('libraryPageSize', libraryPageSize));
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ClientSettingsModelImpl &&
(identical(other.syncPath, syncPath) ||
other.syncPath == syncPath) &&
(identical(other.position, position) ||
other.position == position) &&
(identical(other.size, size) || other.size == size) &&
(identical(other.timeOut, timeOut) || other.timeOut == timeOut) &&
(identical(other.nextUpDateCutoff, nextUpDateCutoff) ||
other.nextUpDateCutoff == nextUpDateCutoff) &&
(identical(other.themeMode, themeMode) ||
other.themeMode == themeMode) &&
(identical(other.themeColor, themeColor) ||
other.themeColor == themeColor) &&
(identical(other.amoledBlack, amoledBlack) ||
other.amoledBlack == amoledBlack) &&
(identical(other.blurPlaceHolders, blurPlaceHolders) ||
other.blurPlaceHolders == blurPlaceHolders) &&
(identical(other.blurUpcomingEpisodes, blurUpcomingEpisodes) ||
other.blurUpcomingEpisodes == blurUpcomingEpisodes) &&
(identical(other.selectedLocale, selectedLocale) ||
other.selectedLocale == selectedLocale) &&
(identical(other.enableMediaKeys, enableMediaKeys) ||
other.enableMediaKeys == enableMediaKeys) &&
(identical(other.posterSize, posterSize) ||
other.posterSize == posterSize) &&
(identical(other.pinchPosterZoom, pinchPosterZoom) ||
other.pinchPosterZoom == pinchPosterZoom) &&
(identical(other.mouseDragSupport, mouseDragSupport) ||
other.mouseDragSupport == mouseDragSupport) &&
(identical(other.libraryPageSize, libraryPageSize) ||
other.libraryPageSize == libraryPageSize));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
syncPath,
position,
size,
timeOut,
nextUpDateCutoff,
themeMode,
themeColor,
amoledBlack,
blurPlaceHolders,
blurUpcomingEpisodes,
selectedLocale,
enableMediaKeys,
posterSize,
pinchPosterZoom,
mouseDragSupport,
libraryPageSize);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ClientSettingsModelImplCopyWith<_$ClientSettingsModelImpl> get copyWith =>
__$$ClientSettingsModelImplCopyWithImpl<_$ClientSettingsModelImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ClientSettingsModelImplToJson(
this,
);
}
}
abstract class _ClientSettingsModel implements ClientSettingsModel {
factory _ClientSettingsModel(
{final String? syncPath,
final Vector2 position,
final Vector2 size,
final Duration? timeOut,
final Duration? nextUpDateCutoff,
final ThemeMode themeMode,
final ColorThemes? themeColor,
final bool amoledBlack,
final bool blurPlaceHolders,
final bool blurUpcomingEpisodes,
@LocaleConvert() final Locale? selectedLocale,
final bool enableMediaKeys,
final double posterSize,
final bool pinchPosterZoom,
final bool mouseDragSupport,
final int? libraryPageSize}) = _$ClientSettingsModelImpl;
factory _ClientSettingsModel.fromJson(Map<String, dynamic> json) =
_$ClientSettingsModelImpl.fromJson;
@override
String? get syncPath;
@override
Vector2 get position;
@override
Vector2 get size;
@override
Duration? get timeOut;
@override
Duration? get nextUpDateCutoff;
@override
ThemeMode get themeMode;
@override
ColorThemes? get themeColor;
@override
bool get amoledBlack;
@override
bool get blurPlaceHolders;
@override
bool get blurUpcomingEpisodes;
@override
@LocaleConvert()
Locale? get selectedLocale;
@override
bool get enableMediaKeys;
@override
double get posterSize;
@override
bool get pinchPosterZoom;
@override
bool get mouseDragSupport;
@override
int? get libraryPageSize;
@override
@JsonKey(ignore: true)
_$$ClientSettingsModelImplCopyWith<_$ClientSettingsModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,83 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'client_settings_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$ClientSettingsModelImpl _$$ClientSettingsModelImplFromJson(
Map<String, dynamic> json) =>
_$ClientSettingsModelImpl(
syncPath: json['syncPath'] as String?,
position: json['position'] == null
? const Vector2(x: 0, y: 0)
: Vector2.fromJson(json['position'] as String),
size: json['size'] == null
? const Vector2(x: 1280, y: 720)
: Vector2.fromJson(json['size'] as String),
timeOut: json['timeOut'] == null
? const Duration(seconds: 30)
: Duration(microseconds: (json['timeOut'] as num).toInt()),
nextUpDateCutoff: json['nextUpDateCutoff'] == null
? null
: Duration(microseconds: (json['nextUpDateCutoff'] as num).toInt()),
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
ThemeMode.system,
themeColor: $enumDecodeNullable(_$ColorThemesEnumMap, json['themeColor']),
amoledBlack: json['amoledBlack'] as bool? ?? false,
blurPlaceHolders: json['blurPlaceHolders'] as bool? ?? false,
blurUpcomingEpisodes: json['blurUpcomingEpisodes'] as bool? ?? false,
selectedLocale:
const LocaleConvert().fromJson(json['selectedLocale'] as String?),
enableMediaKeys: json['enableMediaKeys'] as bool? ?? true,
posterSize: (json['posterSize'] as num?)?.toDouble() ?? 1.0,
pinchPosterZoom: json['pinchPosterZoom'] as bool? ?? false,
mouseDragSupport: json['mouseDragSupport'] as bool? ?? false,
libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(),
);
Map<String, dynamic> _$$ClientSettingsModelImplToJson(
_$ClientSettingsModelImpl instance) =>
<String, dynamic>{
'syncPath': instance.syncPath,
'position': instance.position,
'size': instance.size,
'timeOut': instance.timeOut?.inMicroseconds,
'nextUpDateCutoff': instance.nextUpDateCutoff?.inMicroseconds,
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'themeColor': _$ColorThemesEnumMap[instance.themeColor],
'amoledBlack': instance.amoledBlack,
'blurPlaceHolders': instance.blurPlaceHolders,
'blurUpcomingEpisodes': instance.blurUpcomingEpisodes,
'selectedLocale': const LocaleConvert().toJson(instance.selectedLocale),
'enableMediaKeys': instance.enableMediaKeys,
'posterSize': instance.posterSize,
'pinchPosterZoom': instance.pinchPosterZoom,
'mouseDragSupport': instance.mouseDragSupport,
'libraryPageSize': instance.libraryPageSize,
};
const _$ThemeModeEnumMap = {
ThemeMode.system: 'system',
ThemeMode.light: 'light',
ThemeMode.dark: 'dark',
};
const _$ColorThemesEnumMap = {
ColorThemes.fladder: 'fladder',
ColorThemes.deepOrange: 'deepOrange',
ColorThemes.amber: 'amber',
ColorThemes.green: 'green',
ColorThemes.lightGreen: 'lightGreen',
ColorThemes.lime: 'lime',
ColorThemes.cyan: 'cyan',
ColorThemes.blue: 'blue',
ColorThemes.lightBlue: 'lightBlue',
ColorThemes.indigo: 'indigo',
ColorThemes.deepBlue: 'deepBlue',
ColorThemes.brown: 'brown',
ColorThemes.purple: 'purple',
ColorThemes.deepPurple: 'deepPurple',
ColorThemes.blueGrey: 'blueGrey',
};

View file

@ -0,0 +1,108 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:flutter/material.dart';
enum HomeCarouselSettings {
off,
nextUp,
cont,
combined,
;
const HomeCarouselSettings();
String label(BuildContext context) => switch (this) {
HomeCarouselSettings.off => context.localized.hide,
HomeCarouselSettings.nextUp => context.localized.nextUp,
HomeCarouselSettings.cont => context.localized.settingsContinue,
HomeCarouselSettings.combined => context.localized.combined,
};
String toMap() {
return toString();
}
static HomeCarouselSettings fromMap(String value) {
return HomeCarouselSettings.values.firstWhereOrNull((element) => element.name == value) ??
HomeCarouselSettings.combined;
}
}
enum HomeNextUp {
off,
nextUp,
cont,
combined,
separate,
;
const HomeNextUp();
String label(BuildContext context) => switch (this) {
HomeNextUp.off => context.localized.hide,
HomeNextUp.nextUp => context.localized.nextUp,
HomeNextUp.cont => context.localized.settingsContinue,
HomeNextUp.combined => context.localized.combined,
HomeNextUp.separate => context.localized.separate,
};
String toMap() {
return toString();
}
static HomeNextUp fromMap(String value) {
return HomeNextUp.values.firstWhereOrNull((element) => element.name == value) ?? HomeNextUp.separate;
}
}
class HomeSettingsModel {
final HomeCarouselSettings carouselSettings;
final HomeNextUp nextUp;
HomeSettingsModel({
this.carouselSettings = HomeCarouselSettings.combined,
this.nextUp = HomeNextUp.separate,
});
HomeSettingsModel copyWith({
HomeCarouselSettings? carouselSettings,
HomeNextUp? nextUp,
}) {
return HomeSettingsModel(
carouselSettings: carouselSettings ?? this.carouselSettings,
nextUp: nextUp ?? this.nextUp,
);
}
Map<String, dynamic> toMap() {
return {
'carouselSettings': carouselSettings.toMap(),
'nextUp': nextUp.toMap(),
};
}
factory HomeSettingsModel.fromMap(Map<String, dynamic> map) {
return HomeSettingsModel(
carouselSettings: HomeCarouselSettings.fromMap(map['carouselSettings']),
nextUp: HomeNextUp.fromMap(map['nextUp']),
);
}
String toJson() => json.encode(toMap());
factory HomeSettingsModel.fromJson(String source) => HomeSettingsModel.fromMap(json.decode(source));
@override
String toString() => 'HomeSettingsModel(carouselSettings: $carouselSettings, nextUp: $nextUp)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is HomeSettingsModel && other.carouselSettings == carouselSettings && other.nextUp == nextUp;
}
@override
int get hashCode => carouselSettings.hashCode ^ nextUp.hashCode;
}

View file

@ -0,0 +1,254 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
import 'dart:math' as math;
import 'package:collection/collection.dart';
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
class SubtitleSettingsModel {
final double fontSize;
final FontWeight fontWeight;
final double verticalOffset;
final Color color;
final Color outlineColor;
final double outlineSize;
final Color backGroundColor;
final double shadow;
const SubtitleSettingsModel({
this.fontSize = 60,
this.fontWeight = FontWeight.normal,
this.verticalOffset = 0.10,
this.color = Colors.white,
this.outlineColor = const Color.fromRGBO(0, 0, 0, 0.85),
this.outlineSize = 4,
this.backGroundColor = const Color.fromARGB(0, 0, 0, 0),
this.shadow = 0.5,
});
SubtitleSettingsModel copyWith({
double? fontSize,
FontWeight? fontWeight,
double? verticalOffset,
Color? color,
Color? outlineColor,
double? outlineSize,
Color? backGroundColor,
double? shadow,
}) {
return SubtitleSettingsModel(
fontSize: fontSize ?? this.fontSize,
fontWeight: fontWeight ?? this.fontWeight,
verticalOffset: verticalOffset ?? this.verticalOffset,
color: color ?? this.color,
outlineColor: outlineColor ?? this.outlineColor,
outlineSize: outlineSize ?? this.outlineSize,
backGroundColor: backGroundColor ?? this.backGroundColor,
shadow: shadow ?? this.shadow,
);
}
TextStyle get backGroundStyle {
return style.copyWith(
shadows: (shadow > 0.01)
? [
Shadow(
blurRadius: 16,
color: Colors.black.withOpacity(shadow),
),
Shadow(
blurRadius: 8,
color: Colors.black.withOpacity(shadow),
),
]
: null,
foreground: Paint()
..style = PaintingStyle.stroke
..strokeWidth = outlineSize * (fontSize / 30)
..color = outlineColor
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round,
);
}
TextStyle get style {
return TextStyle(
height: 1.4,
fontSize: fontSize,
fontWeight: fontWeight,
fontFamily: GoogleFonts.openSans().fontFamily,
letterSpacing: 0.0,
wordSpacing: 0.0,
color: color,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'fontSize': fontSize,
'fontWeight': fontWeight.value,
'verticalOffset': verticalOffset,
'color': color.value,
'outlineColor': outlineColor.value,
'outlineSize': outlineSize,
'backGroundColor': backGroundColor.value,
'shadow': shadow,
};
}
String toJson() => json.encode(toMap());
factory SubtitleSettingsModel.fromJson(String source) => SubtitleSettingsModel.fromMap(json.decode(source));
factory SubtitleSettingsModel.fromMap(Map<String, dynamic> map) {
return const SubtitleSettingsModel().copyWith(
fontSize: map['fontSize'] as double?,
fontWeight: FontWeight.values.firstWhereOrNull((element) => element.index == map['fontWeight'] as int?),
verticalOffset: map['verticalOffset'] as double?,
color: map['color'] != null ? Color(map['color'] as int) : null,
outlineColor: map['outlineColor'] != null ? Color(map['outlineColor'] as int) : null,
outlineSize: map['outlineSize'] as double?,
backGroundColor: map['backGroundColor'] != null ? Color(map['backGroundColor'] as int) : null,
shadow: map['shadow'] as double?,
);
}
@override
String toString() {
return 'SubtitleSettingsModel(fontSize: $fontSize, fontWeight: $fontWeight, verticalOffset: $verticalOffset, color: $color, outlineColor: $outlineColor, outlineSize: $outlineSize, backGroundColor: $backGroundColor, shadow: $shadow)';
}
@override
bool operator ==(covariant SubtitleSettingsModel other) {
if (identical(this, other)) return true;
return other.fontSize == fontSize &&
other.fontWeight == fontWeight &&
other.verticalOffset == verticalOffset &&
other.color == color &&
other.outlineColor == outlineColor &&
other.outlineSize == outlineSize &&
other.backGroundColor == backGroundColor &&
other.shadow == shadow;
}
@override
int get hashCode {
return fontSize.hashCode ^
fontWeight.hashCode ^
verticalOffset.hashCode ^
color.hashCode ^
outlineColor.hashCode ^
outlineSize.hashCode ^
backGroundColor.hashCode ^
shadow.hashCode;
}
}
class SubtitleText extends ConsumerWidget {
final SubtitleSettingsModel subModel;
final EdgeInsets padding;
final String text;
final double offset;
const SubtitleText({
required this.subModel,
required this.padding,
required this.offset,
required this.text,
super.key,
});
// The reference width for calculating the visible text scale factor.
static const kTextScaleFactorReferenceWidth = 1920.0;
// The reference height for calculating the visible text scale factor.
static const kTextScaleFactorReferenceHeight = 1080.0;
@override
Widget build(BuildContext context, WidgetRef ref) {
final fillScreen = ref.watch(videoPlayerSettingsProvider.select((value) => value.fillScreen));
return Padding(
padding: (fillScreen ? EdgeInsets.zero : EdgeInsets.only(left: padding.left, right: padding.right))
.add(const EdgeInsets.all(16)),
child: LayoutBuilder(
builder: (context, constraints) {
final textScale = MediaQuery.textScalerOf(context)
.scale((ref.read(subtitleSettingsProvider.select((value) => value.fontSize)) *
math.sqrt(
((constraints.maxWidth * constraints.maxHeight) /
(kTextScaleFactorReferenceWidth * kTextScaleFactorReferenceHeight))
.clamp(0.0, 1.0),
)));
// Function to calculate the height of the text
double getTextHeight(BuildContext context, String text, TextStyle style) {
final TextPainter textPainter = TextPainter(
text: TextSpan(text: text, style: style),
textDirection: TextDirection.ltr,
textScaler: MediaQuery.textScalerOf(context),
)..layout(minWidth: 0, maxWidth: double.infinity);
return textPainter.height;
}
// Calculate the available height for the text alignment
double availableHeight = constraints.maxHeight;
// Calculate the desired position based on the percentage
double desiredPosition = availableHeight * offset;
// Get the height of the Text widget with the current font style
double textHeight = getTextHeight(context, text, subModel.style);
// Calculate the position to keep the text within visible bounds
double position = desiredPosition - textHeight / 2;
// Ensure the text doesn't go off-screen
position = position.clamp(0, availableHeight - textHeight);
return IgnorePointer(
child: Stack(
alignment: Alignment.bottomCenter,
children: [
Positioned(
bottom: position,
child: Container(
constraints: BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight),
decoration: BoxDecoration(
color: subModel.backGroundColor,
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
text,
style: subModel.backGroundStyle.copyWith(fontSize: textScale),
textAlign: TextAlign.center,
),
),
),
),
Positioned(
bottom: position,
child: Container(
constraints: BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
text,
style: subModel.style.copyWith(fontSize: textScale),
textAlign: TextAlign.center,
),
),
),
)
],
),
);
},
),
);
}
}

View file

@ -0,0 +1,113 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class VideoPlayerSettingsModel {
final double? screenBrightness;
final BoxFit videoFit;
final bool fillScreen;
final bool hardwareAccel;
final bool useLibass;
final double internalVolume;
final String? audioDevice;
const VideoPlayerSettingsModel({
this.screenBrightness,
this.videoFit = BoxFit.contain,
this.fillScreen = false,
this.hardwareAccel = true,
this.useLibass = false,
this.internalVolume = 100,
this.audioDevice,
});
double get volume => switch (defaultTargetPlatform) {
TargetPlatform.android || TargetPlatform.iOS => 100,
_ => internalVolume,
};
VideoPlayerSettingsModel copyWith({
ValueGetter<double?>? screenBrightness,
BoxFit? videoFit,
bool? fillScreen,
bool? hardwareAccel,
bool? useLibass,
double? internalVolume,
ValueGetter<String?>? audioDevice,
}) {
return VideoPlayerSettingsModel(
screenBrightness: screenBrightness != null ? screenBrightness() : this.screenBrightness,
videoFit: videoFit ?? this.videoFit,
fillScreen: fillScreen ?? this.fillScreen,
hardwareAccel: hardwareAccel ?? this.hardwareAccel,
useLibass: useLibass ?? this.useLibass,
internalVolume: internalVolume ?? this.internalVolume,
audioDevice: audioDevice != null ? audioDevice() : this.audioDevice,
);
}
Map<String, dynamic> toMap() {
return {
'screenBrightness': screenBrightness,
'videoFit': videoFit.name,
'fillScreen': fillScreen,
'hardwareAccel': hardwareAccel,
'useLibass': useLibass,
'internalVolume': internalVolume,
'audioDevice': audioDevice,
};
}
factory VideoPlayerSettingsModel.fromMap(Map<String, dynamic> map) {
return VideoPlayerSettingsModel(
screenBrightness: map['screenBrightness']?.toDouble(),
videoFit: BoxFit.values.firstWhereOrNull((element) => element.name == map['videoFit']) ?? BoxFit.contain,
fillScreen: map['fillScreen'] ?? false,
hardwareAccel: map['hardwareAccel'] ?? false,
useLibass: map['useLibass'] ?? false,
internalVolume: map['internalVolume']?.toDouble() ?? 0.0,
audioDevice: map['audioDevice'],
);
}
String toJson() => json.encode(toMap());
factory VideoPlayerSettingsModel.fromJson(String source) => VideoPlayerSettingsModel.fromMap(json.decode(source));
@override
String toString() {
return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, internalVolume: $internalVolume, audioDevice: $audioDevice)';
}
bool playerSame(VideoPlayerSettingsModel other) {
return other.hardwareAccel == hardwareAccel && other.useLibass == useLibass;
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is VideoPlayerSettingsModel &&
other.screenBrightness == screenBrightness &&
other.videoFit == videoFit &&
other.fillScreen == fillScreen &&
other.hardwareAccel == hardwareAccel &&
other.useLibass == useLibass &&
other.internalVolume == internalVolume &&
other.audioDevice == audioDevice;
}
@override
int get hashCode {
return screenBrightness.hashCode ^
videoFit.hashCode ^
fillScreen.hashCode ^
hardwareAccel.hashCode ^
useLibass.hashCode ^
internalVolume.hashCode ^
audioDevice.hashCode;
}
}

View file

@ -0,0 +1,41 @@
import 'package:background_downloader/background_downloader.dart' as dl;
class DownloadStream {
final String id;
final dl.DownloadTask? task;
final double progress;
final dl.TaskStatus status;
DownloadStream({
required this.id,
this.task,
required this.progress,
required this.status,
});
DownloadStream.empty()
: id = '',
task = null,
progress = -1,
status = dl.TaskStatus.notFound;
bool get hasDownload => progress != -1.0 && status != dl.TaskStatus.notFound && status != dl.TaskStatus.complete;
DownloadStream copyWith({
String? id,
dl.DownloadTask? task,
double? progress,
dl.TaskStatus? status,
}) {
return DownloadStream(
id: id ?? this.id,
task: task ?? this.task,
progress: progress ?? this.progress,
status: status ?? this.status,
);
}
@override
String toString() {
return 'DownloadStream(id: $id, task: $task, progress: $progress, status: $status)';
}
}

View file

@ -0,0 +1,75 @@
import 'dart:convert';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:isar/isar.dart';
part 'i_synced_item.g.dart';
// extension IsarExtensions on String? {
// int get fastHash {
// if (this == null) return 0;
// var hash = 0xcbf29ce484222325;
// var i = 0;
// while (i < this!.length) {
// final codeUnit = this!.codeUnitAt(i++);
// hash ^= codeUnit >> 8;
// hash *= 0x100000001b3;
// hash ^= codeUnit & 0xFF;
// hash *= 0x100000001b3;
// }
// return hash;
// }
// }
@collection
class ISyncedItem {
String? userId;
String id;
int? sortKey;
String? parentId;
String? path;
int? fileSize;
String? videoFileName;
String? trickPlayModel;
String? introOutroSkipModel;
String? images;
List<String>? chapters;
List<String>? subtitles;
String? userData;
ISyncedItem({
this.userId,
required this.id,
this.sortKey,
this.parentId,
this.path,
this.fileSize,
this.videoFileName,
this.trickPlayModel,
this.introOutroSkipModel,
this.images,
this.chapters,
this.subtitles,
this.userData,
});
factory ISyncedItem.fromSynced(SyncedItem syncedItem, String? path) {
return ISyncedItem(
id: syncedItem.id,
parentId: syncedItem.parentId,
userId: syncedItem.userId,
path: syncedItem.path?.replaceAll(path ?? "", '').substring(1),
fileSize: syncedItem.fileSize,
sortKey: syncedItem.sortKey,
videoFileName: syncedItem.videoFileName,
trickPlayModel: syncedItem.fTrickPlayModel != null ? jsonEncode(syncedItem.fTrickPlayModel?.toJson()) : null,
introOutroSkipModel:
syncedItem.introOutSkipModel != null ? jsonEncode(syncedItem.introOutSkipModel?.toJson()) : null,
images: syncedItem.fImages != null ? jsonEncode(syncedItem.fImages?.toJson()) : null,
chapters: syncedItem.fChapters.map((e) => jsonEncode(e.toJson())).toList(),
subtitles: syncedItem.subtitles.map((e) => jsonEncode(e.toJson())).toList(),
userData: syncedItem.userData != null ? jsonEncode(syncedItem.userData?.toJson()) : null,
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,197 @@
import 'dart:convert';
import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:ficonsax/ficonsax.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/intro_skip_model.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/items/trick_play_model.dart';
import 'package:fladder/models/syncing/i_synced_item.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:path/path.dart';
part 'sync_item.freezed.dart';
part 'sync_item.g.dart';
@freezed
class SyncedItem with _$SyncedItem {
const SyncedItem._();
factory SyncedItem({
required String id,
String? parentId,
required String userId,
String? path,
@Default(false) bool markedForDelete,
int? sortKey,
int? fileSize,
String? videoFileName,
IntroOutSkipModel? introOutSkipModel,
TrickPlayModel? fTrickPlayModel,
ImagesData? fImages,
@Default([]) List<Chapter> fChapters,
@Default([]) List<SubStreamModel> subtitles,
@UserDataJsonSerializer() UserData? userData,
}) = _SyncItem;
static String trickPlayPath = "TrickPlay";
List<Chapter> get chapters => fChapters.map((e) => e.copyWith(imageUrl: joinAll({"$path", e.imageUrl}))).toList();
ImagesData? get images => fImages?.copyWith(
primary: () => fImages?.primary?.copyWith(path: joinAll(["$path", "${fImages?.primary?.path}"])),
logo: () => fImages?.logo?.copyWith(path: joinAll(["$path", "${fImages?.logo?.path}"])),
backDrop: () => fImages?.backDrop?.map((e) => e.copyWith(path: joinAll(["$path", (e.path)]))).toList(),
);
TrickPlayModel? get trickPlayModel => fTrickPlayModel?.copyWith(
images: fTrickPlayModel?.images
.map(
(trickPlayPath) => joinAll(["$path", trickPlayPath]),
)
.toList() ??
[]);
File get dataFile => File(joinAll(["$path", "data.json"]));
Directory get trickPlayDirectory => Directory(joinAll(["$path", trickPlayPath]));
File get videoFile => File(joinAll(["$path", "$videoFileName"]));
Directory get directory => Directory(path ?? "");
SyncStatus get status => switch (videoFile.existsSync()) {
true => SyncStatus.complete,
_ => SyncStatus.partially,
};
String? get taskId => null;
bool get childHasTask => false;
double get totalProgress => 0.0;
bool get hasVideoFile => videoFileName?.isNotEmpty == true && (fileSize ?? 0) > 0;
TaskStatus get anyStatus {
return TaskStatus.notFound;
}
double get downloadProgress => 0.0;
TaskStatus get downloadStatus => TaskStatus.notFound;
DownloadTask? get task => null;
Future<bool> deleteDatFiles(Ref ref) async {
try {
await videoFile.delete();
await Directory(joinAll([directory.path, trickPlayPath])).delete(recursive: true);
} catch (e) {
return false;
}
return true;
}
List<SyncedItem> nestedChildren(WidgetRef ref) => ref.watch(syncChildrenProvider(this));
List<SyncedItem> getChildren(Ref ref) => ref.read(syncProvider.notifier).getChildren(this);
List<SyncedItem> getNestedChildren(Ref ref) => ref.read(syncProvider.notifier).getNestedChildren(this);
Future<int> get getDirSize async {
var files = await directory.list(recursive: true).toList();
var dirSize = files.fold(0, (int sum, file) => sum + file.statSync().size);
return dirSize;
}
ItemBaseModel? createItemModel(Ref ref) {
if (!dataFile.existsSync()) return null;
final BaseItemDto itemDto = BaseItemDto.fromJson(jsonDecode(dataFile.readAsStringSync()));
final itemModel = ItemBaseModel.fromBaseDto(itemDto, ref);
return itemModel.copyWith(
images: images,
userData: userData,
);
}
factory SyncedItem.fromJson(Map<String, dynamic> json) => _$SyncedItemFromJson(json);
factory SyncedItem.fromIsar(ISyncedItem isarSyncedItem, String savePath) {
return SyncedItem(
id: isarSyncedItem.id,
parentId: isarSyncedItem.parentId,
userId: isarSyncedItem.userId ?? "",
sortKey: isarSyncedItem.sortKey,
path: joinAll([savePath, isarSyncedItem.path ?? ""]),
fileSize: isarSyncedItem.fileSize,
videoFileName: isarSyncedItem.videoFileName,
introOutSkipModel: isarSyncedItem.introOutroSkipModel != null
? IntroOutSkipModel.fromJson(jsonDecode(isarSyncedItem.introOutroSkipModel!))
: null,
fTrickPlayModel: isarSyncedItem.trickPlayModel != null
? TrickPlayModel.fromJson(jsonDecode(isarSyncedItem.trickPlayModel!))
: null,
fImages: isarSyncedItem.images != null ? ImagesData.fromJson(jsonDecode(isarSyncedItem.images!)) : null,
fChapters: isarSyncedItem.chapters
?.map(
(e) => Chapter.fromJson(jsonDecode(e)),
)
.toList() ??
[],
subtitles: isarSyncedItem.subtitles
?.map(
(e) => SubStreamModel.fromJson(jsonDecode(e)),
)
.toList() ??
[],
userData: isarSyncedItem.userData != null ? UserData.fromJson(jsonDecode(isarSyncedItem.userData!)) : null,
);
}
}
enum SyncStatus {
complete(
"Synced",
Color.fromARGB(255, 141, 214, 58),
IconsaxOutline.tick_circle,
),
partially(
"Partially",
Color.fromARGB(255, 221, 135, 23),
IconsaxOutline.more_circle,
),
;
const SyncStatus(this.label, this.color, this.icon);
final Color color;
final String label;
final IconData icon;
}
extension StatusExtension on TaskStatus {
Color color(BuildContext context) => switch (this) {
TaskStatus.enqueued => Colors.blueAccent,
TaskStatus.running => Colors.limeAccent,
TaskStatus.complete => Colors.limeAccent,
TaskStatus.canceled || TaskStatus.notFound || TaskStatus.failed => Theme.of(context).colorScheme.error,
TaskStatus.waitingToRetry => Colors.yellowAccent,
TaskStatus.paused => Colors.orangeAccent,
};
String get name => switch (this) {
TaskStatus.enqueued => 'Enqueued',
TaskStatus.running => 'Running',
TaskStatus.complete => 'Complete',
TaskStatus.notFound => 'Not Found',
TaskStatus.failed => 'Failed',
TaskStatus.canceled => 'Canceled',
TaskStatus.waitingToRetry => 'Waiting To Retry',
TaskStatus.paused => 'Paused',
};
}

View file

@ -0,0 +1,494 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'sync_item.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
SyncedItem _$SyncedItemFromJson(Map<String, dynamic> json) {
return _SyncItem.fromJson(json);
}
/// @nodoc
mixin _$SyncedItem {
String get id => throw _privateConstructorUsedError;
String? get parentId => throw _privateConstructorUsedError;
String get userId => throw _privateConstructorUsedError;
String? get path => throw _privateConstructorUsedError;
bool get markedForDelete => throw _privateConstructorUsedError;
int? get sortKey => throw _privateConstructorUsedError;
int? get fileSize => throw _privateConstructorUsedError;
String? get videoFileName => throw _privateConstructorUsedError;
IntroOutSkipModel? get introOutSkipModel =>
throw _privateConstructorUsedError;
TrickPlayModel? get fTrickPlayModel => throw _privateConstructorUsedError;
ImagesData? get fImages => throw _privateConstructorUsedError;
List<Chapter> get fChapters => throw _privateConstructorUsedError;
List<SubStreamModel> get subtitles => throw _privateConstructorUsedError;
@UserDataJsonSerializer()
UserData? get userData => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$SyncedItemCopyWith<SyncedItem> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SyncedItemCopyWith<$Res> {
factory $SyncedItemCopyWith(
SyncedItem value, $Res Function(SyncedItem) then) =
_$SyncedItemCopyWithImpl<$Res, SyncedItem>;
@useResult
$Res call(
{String id,
String? parentId,
String userId,
String? path,
bool markedForDelete,
int? sortKey,
int? fileSize,
String? videoFileName,
IntroOutSkipModel? introOutSkipModel,
TrickPlayModel? fTrickPlayModel,
ImagesData? fImages,
List<Chapter> fChapters,
List<SubStreamModel> subtitles,
@UserDataJsonSerializer() UserData? userData});
$IntroOutSkipModelCopyWith<$Res>? get introOutSkipModel;
$TrickPlayModelCopyWith<$Res>? get fTrickPlayModel;
}
/// @nodoc
class _$SyncedItemCopyWithImpl<$Res, $Val extends SyncedItem>
implements $SyncedItemCopyWith<$Res> {
_$SyncedItemCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? parentId = freezed,
Object? userId = null,
Object? path = freezed,
Object? markedForDelete = null,
Object? sortKey = freezed,
Object? fileSize = freezed,
Object? videoFileName = freezed,
Object? introOutSkipModel = freezed,
Object? fTrickPlayModel = freezed,
Object? fImages = freezed,
Object? fChapters = null,
Object? subtitles = null,
Object? userData = freezed,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
parentId: freezed == parentId
? _value.parentId
: parentId // ignore: cast_nullable_to_non_nullable
as String?,
userId: null == userId
? _value.userId
: userId // ignore: cast_nullable_to_non_nullable
as String,
path: freezed == path
? _value.path
: path // ignore: cast_nullable_to_non_nullable
as String?,
markedForDelete: null == markedForDelete
? _value.markedForDelete
: markedForDelete // ignore: cast_nullable_to_non_nullable
as bool,
sortKey: freezed == sortKey
? _value.sortKey
: sortKey // ignore: cast_nullable_to_non_nullable
as int?,
fileSize: freezed == fileSize
? _value.fileSize
: fileSize // ignore: cast_nullable_to_non_nullable
as int?,
videoFileName: freezed == videoFileName
? _value.videoFileName
: videoFileName // ignore: cast_nullable_to_non_nullable
as String?,
introOutSkipModel: freezed == introOutSkipModel
? _value.introOutSkipModel
: introOutSkipModel // ignore: cast_nullable_to_non_nullable
as IntroOutSkipModel?,
fTrickPlayModel: freezed == fTrickPlayModel
? _value.fTrickPlayModel
: fTrickPlayModel // ignore: cast_nullable_to_non_nullable
as TrickPlayModel?,
fImages: freezed == fImages
? _value.fImages
: fImages // ignore: cast_nullable_to_non_nullable
as ImagesData?,
fChapters: null == fChapters
? _value.fChapters
: fChapters // ignore: cast_nullable_to_non_nullable
as List<Chapter>,
subtitles: null == subtitles
? _value.subtitles
: subtitles // ignore: cast_nullable_to_non_nullable
as List<SubStreamModel>,
userData: freezed == userData
? _value.userData
: userData // ignore: cast_nullable_to_non_nullable
as UserData?,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$IntroOutSkipModelCopyWith<$Res>? get introOutSkipModel {
if (_value.introOutSkipModel == null) {
return null;
}
return $IntroOutSkipModelCopyWith<$Res>(_value.introOutSkipModel!, (value) {
return _then(_value.copyWith(introOutSkipModel: value) as $Val);
});
}
@override
@pragma('vm:prefer-inline')
$TrickPlayModelCopyWith<$Res>? get fTrickPlayModel {
if (_value.fTrickPlayModel == null) {
return null;
}
return $TrickPlayModelCopyWith<$Res>(_value.fTrickPlayModel!, (value) {
return _then(_value.copyWith(fTrickPlayModel: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$SyncItemImplCopyWith<$Res>
implements $SyncedItemCopyWith<$Res> {
factory _$$SyncItemImplCopyWith(
_$SyncItemImpl value, $Res Function(_$SyncItemImpl) then) =
__$$SyncItemImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String id,
String? parentId,
String userId,
String? path,
bool markedForDelete,
int? sortKey,
int? fileSize,
String? videoFileName,
IntroOutSkipModel? introOutSkipModel,
TrickPlayModel? fTrickPlayModel,
ImagesData? fImages,
List<Chapter> fChapters,
List<SubStreamModel> subtitles,
@UserDataJsonSerializer() UserData? userData});
@override
$IntroOutSkipModelCopyWith<$Res>? get introOutSkipModel;
@override
$TrickPlayModelCopyWith<$Res>? get fTrickPlayModel;
}
/// @nodoc
class __$$SyncItemImplCopyWithImpl<$Res>
extends _$SyncedItemCopyWithImpl<$Res, _$SyncItemImpl>
implements _$$SyncItemImplCopyWith<$Res> {
__$$SyncItemImplCopyWithImpl(
_$SyncItemImpl _value, $Res Function(_$SyncItemImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? parentId = freezed,
Object? userId = null,
Object? path = freezed,
Object? markedForDelete = null,
Object? sortKey = freezed,
Object? fileSize = freezed,
Object? videoFileName = freezed,
Object? introOutSkipModel = freezed,
Object? fTrickPlayModel = freezed,
Object? fImages = freezed,
Object? fChapters = null,
Object? subtitles = null,
Object? userData = freezed,
}) {
return _then(_$SyncItemImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
parentId: freezed == parentId
? _value.parentId
: parentId // ignore: cast_nullable_to_non_nullable
as String?,
userId: null == userId
? _value.userId
: userId // ignore: cast_nullable_to_non_nullable
as String,
path: freezed == path
? _value.path
: path // ignore: cast_nullable_to_non_nullable
as String?,
markedForDelete: null == markedForDelete
? _value.markedForDelete
: markedForDelete // ignore: cast_nullable_to_non_nullable
as bool,
sortKey: freezed == sortKey
? _value.sortKey
: sortKey // ignore: cast_nullable_to_non_nullable
as int?,
fileSize: freezed == fileSize
? _value.fileSize
: fileSize // ignore: cast_nullable_to_non_nullable
as int?,
videoFileName: freezed == videoFileName
? _value.videoFileName
: videoFileName // ignore: cast_nullable_to_non_nullable
as String?,
introOutSkipModel: freezed == introOutSkipModel
? _value.introOutSkipModel
: introOutSkipModel // ignore: cast_nullable_to_non_nullable
as IntroOutSkipModel?,
fTrickPlayModel: freezed == fTrickPlayModel
? _value.fTrickPlayModel
: fTrickPlayModel // ignore: cast_nullable_to_non_nullable
as TrickPlayModel?,
fImages: freezed == fImages
? _value.fImages
: fImages // ignore: cast_nullable_to_non_nullable
as ImagesData?,
fChapters: null == fChapters
? _value._fChapters
: fChapters // ignore: cast_nullable_to_non_nullable
as List<Chapter>,
subtitles: null == subtitles
? _value._subtitles
: subtitles // ignore: cast_nullable_to_non_nullable
as List<SubStreamModel>,
userData: freezed == userData
? _value.userData
: userData // ignore: cast_nullable_to_non_nullable
as UserData?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SyncItemImpl extends _SyncItem {
_$SyncItemImpl(
{required this.id,
this.parentId,
required this.userId,
this.path,
this.markedForDelete = false,
this.sortKey,
this.fileSize,
this.videoFileName,
this.introOutSkipModel,
this.fTrickPlayModel,
this.fImages,
final List<Chapter> fChapters = const [],
final List<SubStreamModel> subtitles = const [],
@UserDataJsonSerializer() this.userData})
: _fChapters = fChapters,
_subtitles = subtitles,
super._();
factory _$SyncItemImpl.fromJson(Map<String, dynamic> json) =>
_$$SyncItemImplFromJson(json);
@override
final String id;
@override
final String? parentId;
@override
final String userId;
@override
final String? path;
@override
@JsonKey()
final bool markedForDelete;
@override
final int? sortKey;
@override
final int? fileSize;
@override
final String? videoFileName;
@override
final IntroOutSkipModel? introOutSkipModel;
@override
final TrickPlayModel? fTrickPlayModel;
@override
final ImagesData? fImages;
final List<Chapter> _fChapters;
@override
@JsonKey()
List<Chapter> get fChapters {
if (_fChapters is EqualUnmodifiableListView) return _fChapters;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_fChapters);
}
final List<SubStreamModel> _subtitles;
@override
@JsonKey()
List<SubStreamModel> get subtitles {
if (_subtitles is EqualUnmodifiableListView) return _subtitles;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_subtitles);
}
@override
@UserDataJsonSerializer()
final UserData? userData;
@override
String toString() {
return 'SyncedItem(id: $id, parentId: $parentId, userId: $userId, path: $path, markedForDelete: $markedForDelete, sortKey: $sortKey, fileSize: $fileSize, videoFileName: $videoFileName, introOutSkipModel: $introOutSkipModel, fTrickPlayModel: $fTrickPlayModel, fImages: $fImages, fChapters: $fChapters, subtitles: $subtitles, userData: $userData)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SyncItemImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.parentId, parentId) ||
other.parentId == parentId) &&
(identical(other.userId, userId) || other.userId == userId) &&
(identical(other.path, path) || other.path == path) &&
(identical(other.markedForDelete, markedForDelete) ||
other.markedForDelete == markedForDelete) &&
(identical(other.sortKey, sortKey) || other.sortKey == sortKey) &&
(identical(other.fileSize, fileSize) ||
other.fileSize == fileSize) &&
(identical(other.videoFileName, videoFileName) ||
other.videoFileName == videoFileName) &&
(identical(other.introOutSkipModel, introOutSkipModel) ||
other.introOutSkipModel == introOutSkipModel) &&
(identical(other.fTrickPlayModel, fTrickPlayModel) ||
other.fTrickPlayModel == fTrickPlayModel) &&
(identical(other.fImages, fImages) || other.fImages == fImages) &&
const DeepCollectionEquality()
.equals(other._fChapters, _fChapters) &&
const DeepCollectionEquality()
.equals(other._subtitles, _subtitles) &&
(identical(other.userData, userData) ||
other.userData == userData));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
id,
parentId,
userId,
path,
markedForDelete,
sortKey,
fileSize,
videoFileName,
introOutSkipModel,
fTrickPlayModel,
fImages,
const DeepCollectionEquality().hash(_fChapters),
const DeepCollectionEquality().hash(_subtitles),
userData);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$SyncItemImplCopyWith<_$SyncItemImpl> get copyWith =>
__$$SyncItemImplCopyWithImpl<_$SyncItemImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SyncItemImplToJson(
this,
);
}
}
abstract class _SyncItem extends SyncedItem {
factory _SyncItem(
{required final String id,
final String? parentId,
required final String userId,
final String? path,
final bool markedForDelete,
final int? sortKey,
final int? fileSize,
final String? videoFileName,
final IntroOutSkipModel? introOutSkipModel,
final TrickPlayModel? fTrickPlayModel,
final ImagesData? fImages,
final List<Chapter> fChapters,
final List<SubStreamModel> subtitles,
@UserDataJsonSerializer() final UserData? userData}) = _$SyncItemImpl;
_SyncItem._() : super._();
factory _SyncItem.fromJson(Map<String, dynamic> json) =
_$SyncItemImpl.fromJson;
@override
String get id;
@override
String? get parentId;
@override
String get userId;
@override
String? get path;
@override
bool get markedForDelete;
@override
int? get sortKey;
@override
int? get fileSize;
@override
String? get videoFileName;
@override
IntroOutSkipModel? get introOutSkipModel;
@override
TrickPlayModel? get fTrickPlayModel;
@override
ImagesData? get fImages;
@override
List<Chapter> get fChapters;
@override
List<SubStreamModel> get subtitles;
@override
@UserDataJsonSerializer()
UserData? get userData;
@override
@JsonKey(ignore: true)
_$$SyncItemImplCopyWith<_$SyncItemImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,71 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'sync_item.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$SyncItemImpl _$$SyncItemImplFromJson(Map<String, dynamic> json) =>
_$SyncItemImpl(
id: json['id'] as String,
parentId: json['parentId'] as String?,
userId: json['userId'] as String,
path: json['path'] as String?,
markedForDelete: json['markedForDelete'] as bool? ?? false,
sortKey: (json['sortKey'] as num?)?.toInt(),
fileSize: (json['fileSize'] as num?)?.toInt(),
videoFileName: json['videoFileName'] as String?,
introOutSkipModel: json['introOutSkipModel'] == null
? null
: IntroOutSkipModel.fromJson(
json['introOutSkipModel'] as Map<String, dynamic>),
fTrickPlayModel: json['fTrickPlayModel'] == null
? null
: TrickPlayModel.fromJson(
json['fTrickPlayModel'] as Map<String, dynamic>),
fImages: json['fImages'] == null
? null
: ImagesData.fromJson(json['fImages'] as String),
fChapters: (json['fChapters'] as List<dynamic>?)
?.map((e) => Chapter.fromJson(e as String))
.toList() ??
const [],
subtitles: (json['subtitles'] as List<dynamic>?)
?.map((e) => SubStreamModel.fromJson(e as String))
.toList() ??
const [],
userData: _$JsonConverterFromJson<String, UserData>(
json['userData'], const UserDataJsonSerializer().fromJson),
);
Map<String, dynamic> _$$SyncItemImplToJson(_$SyncItemImpl instance) =>
<String, dynamic>{
'id': instance.id,
'parentId': instance.parentId,
'userId': instance.userId,
'path': instance.path,
'markedForDelete': instance.markedForDelete,
'sortKey': instance.sortKey,
'fileSize': instance.fileSize,
'videoFileName': instance.videoFileName,
'introOutSkipModel': instance.introOutSkipModel,
'fTrickPlayModel': instance.fTrickPlayModel,
'fImages': instance.fImages,
'fChapters': instance.fChapters,
'subtitles': instance.subtitles,
'userData': _$JsonConverterToJson<String, UserData>(
instance.userData, const UserDataJsonSerializer().toJson),
};
Value? _$JsonConverterFromJson<Json, Value>(
Object? json,
Value? Function(Json json) fromJson,
) =>
json == null ? null : fromJson(json as Json);
Json? _$JsonConverterToJson<Json, Value>(
Value? value,
Json? Function(Value value) toJson,
) =>
value == null ? null : toJson(value);

View file

@ -0,0 +1,16 @@
// ignore_for_file: invalid_annotation_target
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:fladder/models/syncing/sync_item.dart';
part 'sync_settings_model.freezed.dart';
@Freezed(toJson: false, fromJson: false)
class SyncSettingsModel with _$SyncSettingsModel {
const SyncSettingsModel._();
factory SyncSettingsModel({
@Default([]) List<SyncedItem> items,
}) = _SyncSettignsModel;
}

View file

@ -0,0 +1,144 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'sync_settings_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$SyncSettingsModel {
List<SyncedItem> get items => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$SyncSettingsModelCopyWith<SyncSettingsModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SyncSettingsModelCopyWith<$Res> {
factory $SyncSettingsModelCopyWith(
SyncSettingsModel value, $Res Function(SyncSettingsModel) then) =
_$SyncSettingsModelCopyWithImpl<$Res, SyncSettingsModel>;
@useResult
$Res call({List<SyncedItem> items});
}
/// @nodoc
class _$SyncSettingsModelCopyWithImpl<$Res, $Val extends SyncSettingsModel>
implements $SyncSettingsModelCopyWith<$Res> {
_$SyncSettingsModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? items = null,
}) {
return _then(_value.copyWith(
items: null == items
? _value.items
: items // ignore: cast_nullable_to_non_nullable
as List<SyncedItem>,
) as $Val);
}
}
/// @nodoc
abstract class _$$SyncSettignsModelImplCopyWith<$Res>
implements $SyncSettingsModelCopyWith<$Res> {
factory _$$SyncSettignsModelImplCopyWith(_$SyncSettignsModelImpl value,
$Res Function(_$SyncSettignsModelImpl) then) =
__$$SyncSettignsModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({List<SyncedItem> items});
}
/// @nodoc
class __$$SyncSettignsModelImplCopyWithImpl<$Res>
extends _$SyncSettingsModelCopyWithImpl<$Res, _$SyncSettignsModelImpl>
implements _$$SyncSettignsModelImplCopyWith<$Res> {
__$$SyncSettignsModelImplCopyWithImpl(_$SyncSettignsModelImpl _value,
$Res Function(_$SyncSettignsModelImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? items = null,
}) {
return _then(_$SyncSettignsModelImpl(
items: null == items
? _value._items
: items // ignore: cast_nullable_to_non_nullable
as List<SyncedItem>,
));
}
}
/// @nodoc
class _$SyncSettignsModelImpl extends _SyncSettignsModel {
_$SyncSettignsModelImpl({final List<SyncedItem> items = const []})
: _items = items,
super._();
final List<SyncedItem> _items;
@override
@JsonKey()
List<SyncedItem> get items {
if (_items is EqualUnmodifiableListView) return _items;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_items);
}
@override
String toString() {
return 'SyncSettingsModel(items: $items)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SyncSettignsModelImpl &&
const DeepCollectionEquality().equals(other._items, _items));
}
@override
int get hashCode =>
Object.hash(runtimeType, const DeepCollectionEquality().hash(_items));
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$SyncSettignsModelImplCopyWith<_$SyncSettignsModelImpl> get copyWith =>
__$$SyncSettignsModelImplCopyWithImpl<_$SyncSettignsModelImpl>(
this, _$identity);
}
abstract class _SyncSettignsModel extends SyncSettingsModel {
factory _SyncSettignsModel({final List<SyncedItem> items}) =
_$SyncSettignsModelImpl;
_SyncSettignsModel._() : super._();
@override
List<SyncedItem> get items;
@override
@JsonKey(ignore: true)
_$$SyncSettignsModelImplCopyWith<_$SyncSettignsModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,177 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:fladder/models/items/chapters_model.dart';
class Bif {
final String version;
final List<int> widthResolutions;
Bif({
required this.version,
required this.widthResolutions,
});
Bif copyWith({
String? version,
List<int>? widthResolutions,
}) {
return Bif(
version: version ?? this.version,
widthResolutions: widthResolutions ?? this.widthResolutions,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'Version': version,
'WidthResolutions': widthResolutions,
};
}
factory Bif.fromMap(Map<String, dynamic> map) {
return Bif(
version: (map['Version'] ?? '') as String,
widthResolutions: List<int>.from((map['WidthResolutions'] ?? const <int>[]) as List<dynamic>),
);
}
String toJson() => json.encode(toMap());
factory Bif.fromJson(String source) => Bif.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() => 'Bif(version: $version, widthResolutions: $widthResolutions)';
@override
bool operator ==(covariant Bif other) {
if (identical(this, other)) return true;
return other.version == version && listEquals(other.widthResolutions, widthResolutions);
}
@override
int get hashCode => version.hashCode ^ widthResolutions.hashCode;
}
class BifUtil {
static List<int> bifMagicNumbers = [0x89, 0x42, 0x49, 0x46, 0x0D, 0x0A, 0x1A, 0x0A];
static const int supportedBifVersion = 0;
static Future<BifData?> trickPlayDecode(Uint8List array, int width) async {
Indexed8Array data = Indexed8Array(array);
for (int b in bifMagicNumbers) {
if (data.read() != b) {
print('Attempted to read invalid bif file.');
return null;
}
}
int bifVersion = data.readInt32();
if (bifVersion != supportedBifVersion) {
print('Client only supports BIF v$supportedBifVersion but file is v$bifVersion');
return null;
}
print('BIF version: $bifVersion');
int bifImgCount = data.readInt32();
if (bifImgCount <= 0) {
print('BIF file contains no images.');
return null;
}
print('BIF image count: $bifImgCount');
int timestampMultiplier = data.readInt32();
if (timestampMultiplier == 0) timestampMultiplier = 1000;
data.addPosition(44); // Reserved
List<BifIndexEntry> bifIndex = [];
for (int i = 0; i < bifImgCount; i++) {
bifIndex.add(BifIndexEntry(data.readInt32(), data.readInt32()));
}
List<Chapter> bifImages = [];
for (int i = 0; i < bifIndex.length; i++) {
BifIndexEntry indexEntry = bifIndex[i];
int timestamp = indexEntry.timestamp;
int offset = indexEntry.offset;
int nextOffset = i + 1 < bifIndex.length ? bifIndex[i + 1].offset : data.limit();
Uint8List imageBytes = Uint8List.sublistView(Uint8List.view(data.array.buffer, offset, nextOffset - offset));
bifImages.add(
Chapter(
name: "",
imageUrl: "",
imageData: imageBytes,
startPosition: Duration(milliseconds: timestamp * timestampMultiplier),
),
);
}
return BifData(bifVersion, bifImgCount, bifImages, width);
}
}
class Indexed8Array {
Uint8List array;
int readIndex;
Indexed8Array(Uint8List byteArray)
: array = byteArray,
readIndex = 0;
int read() {
return array[readIndex++];
}
void addPosition(int amount) {
readIndex += amount;
}
int readInt32() {
int b1 = read() & 0xFF;
int b2 = read() & 0xFF;
int b3 = read() & 0xFF;
int b4 = read() & 0xFF;
return b1 | (b2 << 8) | (b3 << 16) | (b4 << 24);
}
Uint8List getByteArray() {
return array;
}
int limit() {
return array.length;
}
Endian order() {
return Endian.big;
}
}
class BifIndexEntry {
int timestamp;
int offset;
BifIndexEntry(this.timestamp, this.offset);
}
class BifData {
int bifVersion;
int bifImgCount;
List<Chapter> chapters;
int width;
BifData(
this.bifVersion,
this.bifImgCount,
this.chapters,
this.width,
);
}

View file

@ -0,0 +1,208 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:collection/collection.dart';
import 'package:ficonsax/ficonsax.dart';
import 'package:fladder/models/items/item_stream_model.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:media_kit/media_kit.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/models/items/intro_skip_model.dart';
import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/providers/user_provider.dart';
enum PlaybackType {
directStream,
offline,
transcode;
IconData get icon => switch (this) {
PlaybackType.offline => IconsaxOutline.cloud,
PlaybackType.directStream => IconsaxOutline.arrow_right_1,
PlaybackType.transcode => IconsaxOutline.convert,
};
String get name {
switch (this) {
case PlaybackType.directStream:
return "Direct";
case PlaybackType.offline:
return "Offline";
case PlaybackType.transcode:
return "Transcoding";
}
}
}
class VideoPlayback {
final List<ItemStreamModel> queue;
final ItemStreamModel? currentItem;
final SyncedItem? currentSyncedItem;
final VideoStream? currentStream;
VideoPlayback({
required this.queue,
this.currentItem,
this.currentSyncedItem,
this.currentStream,
});
List<SubStreamModel> get subStreams => currentStream?.mediaStreamsModel?.subStreams ?? [];
List<AudioStreamModel> get audioStreams => currentStream?.mediaStreamsModel?.audioStreams ?? [];
ItemStreamModel? get previousVideo {
final int currentIndex = queue.indexWhere((element) => element.id == currentItem?.id);
if (currentIndex > 0) {
return queue[currentIndex - 1];
}
return null;
}
ItemStreamModel? get nextVideo {
final int currentIndex = queue.indexWhere((element) => element.id == currentItem?.id);
if ((currentIndex + 1) < queue.length) {
return queue[currentIndex + 1];
}
return null;
}
VideoPlayback copyWith({
List<ItemStreamModel>? queue,
ItemStreamModel? currentItem,
SyncedItem? currentSyncedItem,
VideoStream? currentStream,
Map<AudioStreamModel, AudioTrack>? audioMappings,
Map<SubStreamModel, SubtitleTrack>? subMappings,
}) {
return VideoPlayback(
queue: queue ?? this.queue,
currentItem: currentItem ?? this.currentItem,
currentSyncedItem: currentSyncedItem ?? this.currentSyncedItem,
currentStream: currentStream ?? this.currentStream,
);
}
VideoPlayback clear() {
return VideoPlayback(
queue: queue,
currentItem: null,
currentStream: null,
);
}
}
class VideoStream {
final String id;
final Duration? currentPosition;
final PlaybackType playbackType;
final String playbackUrl;
final String playSessionId;
final List<Chapter>? chapters;
final List<Chapter>? trickPlay;
final IntroSkipModel? introSkipModel;
final int? audioStreamIndex;
final int? subtitleStreamIndex;
final MediaStreamsModel? mediaStreamsModel;
AudioStreamModel? get currentAudioStream {
if (audioStreamIndex == -1) {
return null;
}
return mediaStreamsModel?.audioStreams.firstWhereOrNull(
(element) => element.index == (audioStreamIndex ?? mediaStreamsModel?.currentAudioStream ?? 0));
}
SubStreamModel? get currentSubStream {
if (subtitleStreamIndex == -1) {
return null;
}
return mediaStreamsModel?.subStreams.firstWhereOrNull(
(element) => element.index == (subtitleStreamIndex ?? mediaStreamsModel?.currentSubStream ?? 0));
}
VideoStream({
required this.id,
this.currentPosition,
required this.playbackType,
required this.playbackUrl,
required this.playSessionId,
this.chapters,
this.trickPlay,
this.introSkipModel,
this.audioStreamIndex,
this.subtitleStreamIndex,
this.mediaStreamsModel,
});
VideoStream copyWith({
String? id,
Duration? currentPosition,
PlaybackType? playbackType,
String? playbackUrl,
String? playSessionId,
List<Chapter>? chapters,
List<Chapter>? trickPlay,
IntroSkipModel? introSkipModel,
int? audioStreamIndex,
int? subtitleStreamIndex,
MediaStreamsModel? mediaStreamsModel,
}) {
return VideoStream(
id: id ?? this.id,
currentPosition: currentPosition ?? this.currentPosition,
playbackType: playbackType ?? this.playbackType,
playbackUrl: playbackUrl ?? this.playbackUrl,
playSessionId: playSessionId ?? this.playSessionId,
chapters: chapters ?? this.chapters,
trickPlay: trickPlay ?? this.trickPlay,
introSkipModel: introSkipModel ?? this.introSkipModel,
audioStreamIndex: audioStreamIndex ?? this.audioStreamIndex,
subtitleStreamIndex: subtitleStreamIndex ?? this.subtitleStreamIndex,
mediaStreamsModel: mediaStreamsModel ?? this.mediaStreamsModel,
);
}
static VideoStream? fromPlayBackInfo(PlaybackInfoResponse info, Ref ref) {
final mediaSource = info.mediaSources?.first;
var playType = PlaybackType.directStream;
String? playbackUrl;
if (mediaSource == null) return null;
if (mediaSource.supportsDirectStream ?? false) {
final Map<String, String?> directOptions = {
'Static': 'true',
'mediaSourceId': mediaSource.id,
'api_key': ref.read(userProvider)?.credentials.token,
};
if (mediaSource.eTag != null) {
directOptions['Tag'] = mediaSource.eTag;
}
if (mediaSource.liveStreamId != null) {
directOptions['LiveStreamId'] = mediaSource.liveStreamId;
}
final params = Uri(queryParameters: directOptions).query;
playbackUrl = '${ref.read(userProvider)?.server ?? ""}/Videos/${mediaSource.id}/stream?$params';
} else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) {
playbackUrl = "${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}";
playType = PlaybackType.transcode;
}
if (playbackUrl == null) return null;
return VideoStream(
id: info.mediaSources?.first.id ?? "",
playbackUrl: playbackUrl,
playbackType: playType,
playSessionId: info.playSessionId ?? "",
mediaStreamsModel: MediaStreamsModel.fromMediaStreamsList(
info.mediaSources?.firstOrNull, info.mediaSources?.firstOrNull?.mediaStreams ?? [], ref),
);
}
}

View file

@ -0,0 +1,97 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
import 'package:fladder/models/item_base_model.dart';
class ViewModel {
final String name;
final String id;
final String serverId;
final DateTime dateCreated;
final bool canDelete;
final bool canDownload;
final String parentId;
final CollectionType collectionType;
final dto.PlayAccess playAccess;
final List<ItemBaseModel> recentlyAdded;
final int childCount;
ViewModel({
required this.name,
required this.id,
required this.serverId,
required this.dateCreated,
required this.canDelete,
required this.canDownload,
required this.parentId,
required this.collectionType,
required this.playAccess,
required this.recentlyAdded,
required this.childCount,
});
ViewModel copyWith({
String? name,
String? id,
String? serverId,
DateTime? dateCreated,
bool? canDelete,
bool? canDownload,
String? parentId,
CollectionType? collectionType,
dto.PlayAccess? playAccess,
List<ItemBaseModel>? recentlyAdded,
int? childCount,
}) {
return ViewModel(
name: name ?? this.name,
id: id ?? this.id,
serverId: serverId ?? this.serverId,
dateCreated: dateCreated ?? this.dateCreated,
canDelete: canDelete ?? this.canDelete,
canDownload: canDownload ?? this.canDownload,
parentId: parentId ?? this.parentId,
collectionType: collectionType ?? this.collectionType,
playAccess: playAccess ?? this.playAccess,
recentlyAdded: recentlyAdded ?? this.recentlyAdded,
childCount: childCount ?? this.childCount,
);
}
factory ViewModel.fromBodyDto(dto.BaseItemDto item, Ref ref) {
return ViewModel(
name: item.name ?? "",
id: item.id ?? "",
serverId: item.serverId ?? "",
dateCreated: item.dateCreated ?? DateTime.now(),
canDelete: item.canDelete ?? false,
canDownload: item.canDownload ?? false,
parentId: item.parentId ?? "",
recentlyAdded: [],
collectionType: CollectionType.values
.firstWhereOrNull((element) => element.name.toLowerCase() == item.collectionType?.value?.toLowerCase()) ??
CollectionType.movies,
playAccess: item.playAccess ?? PlayAccess.none,
childCount: item.childCount ?? 0,
);
}
@override
bool operator ==(covariant ViewModel other) {
if (identical(this, other)) return true;
return other.id == id && other.serverId == serverId;
}
@override
int get hashCode {
return id.hashCode ^ serverId.hashCode;
}
@override
String toString() {
return 'ViewModel(name: $name, id: $id, serverId: $serverId, dateCreated: $dateCreated, canDelete: $canDelete, canDownload: $canDownload, parentId: $parentId, collectionType: $collectionType, playAccess: $playAccess, recentlyAdded: $recentlyAdded, childCount: $childCount)';
}
}

View file

@ -0,0 +1,24 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:fladder/models/view_model.dart';
class ViewsModel {
final bool loading;
final List<ViewModel> views;
final List<ViewModel> dashboardViews;
ViewsModel({
this.loading = false,
this.views = const [],
this.dashboardViews = const [],
});
ViewsModel copyWith({
bool? loading,
List<ViewModel>? views,
List<ViewModel>? dashboardViews,
}) {
return ViewsModel(
loading: loading ?? this.loading,
views: views ?? this.views,
dashboardViews: dashboardViews ?? this.dashboardViews);
}
}

View file

@ -0,0 +1,35 @@
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/profiles/web_profile.dart';
import 'package:flutter/foundation.dart';
const DeviceProfile defaultProfile = kIsWeb
? webProfile
: DeviceProfile(
maxStreamingBitrate: 120000000,
maxStaticBitrate: 120000000,
musicStreamingTranscodingBitrate: 384000,
directPlayProfiles: [
DirectPlayProfile(
type: DlnaProfileType.video,
),
DirectPlayProfile(
type: DlnaProfileType.audio,
)
],
transcodingProfiles: [
TranscodingProfile(
audioCodec: 'aac,mp3,mp2',
container: 'ts',
maxAudioChannels: '2',
protocol: MediaStreamProtocol.hls,
type: DlnaProfileType.video,
videoCodec: 'h264',
),
],
containerProfiles: [],
subtitleProfiles: [
SubtitleProfile(format: 'vtt', method: SubtitleDeliveryMethod.$external),
SubtitleProfile(format: 'ass', method: SubtitleDeliveryMethod.$external),
SubtitleProfile(format: 'ssa', method: SubtitleDeliveryMethod.$external),
],
);

View file

@ -0,0 +1,269 @@
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
const DeviceProfile webProfile = DeviceProfile(
maxStreamingBitrate: 120000000,
maxStaticBitrate: 100000000,
musicStreamingTranscodingBitrate: 384000,
directPlayProfiles: [
DirectPlayProfile(
container: 'mkv,webm',
type: DlnaProfileType.video,
videoCodec: 'h264,hevc,vp8,vp9,av1',
audioCodec: 'vorbis,opus,aac,eac3',
),
DirectPlayProfile(
container: 'mp4,m4v',
type: DlnaProfileType.video,
videoCodec: 'h264,hevc,vp8,vp9,av1',
audioCodec: 'aac,mp3,mp2,opus,flac,vorbis',
),
DirectPlayProfile(
container: 'mov',
type: DlnaProfileType.video,
videoCodec: 'h264',
audioCodec: 'aac,mp3,mp2,opus,flac,vorbis',
),
DirectPlayProfile(container: 'opus', type: DlnaProfileType.audio),
DirectPlayProfile(container: 'webm', audioCodec: 'opus', type: DlnaProfileType.audio),
DirectPlayProfile(container: 'mp3', type: DlnaProfileType.audio),
DirectPlayProfile(container: 'aac', type: DlnaProfileType.audio),
DirectPlayProfile(container: 'm4a', audioCodec: 'aac', type: DlnaProfileType.audio),
DirectPlayProfile(container: 'm4b', audioCodec: 'aac', type: DlnaProfileType.audio),
DirectPlayProfile(container: 'flac', type: DlnaProfileType.audio),
DirectPlayProfile(container: 'webma', type: DlnaProfileType.audio),
DirectPlayProfile(container: 'webm', audioCodec: 'webma', type: DlnaProfileType.audio),
DirectPlayProfile(container: 'wav', type: DlnaProfileType.audio),
DirectPlayProfile(container: 'ogg', type: DlnaProfileType.audio),
DirectPlayProfile(
container: 'hls',
type: DlnaProfileType.video,
videoCodec: 'h264',
audioCodec: 'aac,mp3,mp2',
),
],
transcodingProfiles: [
TranscodingProfile(
container: 'ts',
type: DlnaProfileType.audio,
audioCodec: 'aac',
context: EncodingContext.streaming,
protocol: MediaStreamProtocol.hls,
maxAudioChannels: '2',
minSegments: 1,
breakOnNonKeyFrames: true,
),
TranscodingProfile(
container: 'aac',
type: DlnaProfileType.audio,
audioCodec: 'aac',
context: EncodingContext.streaming,
protocol: MediaStreamProtocol.http,
maxAudioChannels: '2',
),
TranscodingProfile(
container: 'mp3',
type: DlnaProfileType.audio,
audioCodec: 'mp3',
context: EncodingContext.streaming,
protocol: MediaStreamProtocol.http,
maxAudioChannels: '2',
),
TranscodingProfile(
container: 'opus',
type: DlnaProfileType.audio,
audioCodec: 'opus',
context: EncodingContext.streaming,
protocol: MediaStreamProtocol.http,
maxAudioChannels: '2',
),
TranscodingProfile(
container: 'wav',
type: DlnaProfileType.audio,
audioCodec: 'wav',
context: EncodingContext.streaming,
protocol: MediaStreamProtocol.http,
maxAudioChannels: '2',
),
TranscodingProfile(
container: 'opus',
type: DlnaProfileType.audio,
audioCodec: 'opus',
context: EncodingContext.$static,
protocol: MediaStreamProtocol.http,
maxAudioChannels: '2',
),
TranscodingProfile(
container: 'mp3',
type: DlnaProfileType.audio,
audioCodec: 'mp3',
context: EncodingContext.$static,
protocol: MediaStreamProtocol.http,
maxAudioChannels: '2',
),
TranscodingProfile(
container: 'aac',
type: DlnaProfileType.audio,
audioCodec: 'aac',
context: EncodingContext.$static,
protocol: MediaStreamProtocol.http,
maxAudioChannels: '2',
),
TranscodingProfile(
container: 'wav',
type: DlnaProfileType.audio,
audioCodec: 'wav',
context: EncodingContext.$static,
protocol: MediaStreamProtocol.http,
maxAudioChannels: '2',
),
TranscodingProfile(
container: 'ts',
type: DlnaProfileType.video,
audioCodec: 'aac,mp3,mp2',
videoCodec: 'h264',
context: EncodingContext.streaming,
protocol: MediaStreamProtocol.hls,
maxAudioChannels: '2',
minSegments: 1,
breakOnNonKeyFrames: true,
),
],
containerProfiles: [],
codecProfiles: [
CodecProfile(
type: CodecType.videoaudio,
codec: 'aac',
conditions: [
ProfileCondition(
condition: ProfileConditionType.equals,
property: ProfileConditionValue.issecondaryaudio,
$Value: 'false',
isRequired: false,
),
],
),
CodecProfile(
type: CodecType.videoaudio,
conditions: [
ProfileCondition(
condition: ProfileConditionType.equals,
property: ProfileConditionValue.issecondaryaudio,
$Value: 'false',
isRequired: false,
),
],
),
CodecProfile(
type: CodecType.video,
codec: 'h264',
conditions: [
ProfileCondition(
condition: ProfileConditionType.notequals,
property: ProfileConditionValue.isanamorphic,
$Value: 'true',
isRequired: false,
),
ProfileCondition(
condition: ProfileConditionType.equalsany,
property: ProfileConditionValue.videorangetype,
$Value: 'SDR',
isRequired: false,
),
ProfileCondition(
condition: ProfileConditionType.lessthanequal,
property: ProfileConditionValue.videolevel,
$Value: '52',
isRequired: false,
),
ProfileCondition(
condition: ProfileConditionType.notequals,
property: ProfileConditionValue.isinterlaced,
$Value: 'true',
isRequired: false,
),
],
),
CodecProfile(
type: CodecType.video,
codec: 'hevc',
conditions: [
ProfileCondition(
condition: ProfileConditionType.notequals,
property: ProfileConditionValue.isanamorphic,
$Value: 'true',
isRequired: false,
),
ProfileCondition(
condition: ProfileConditionType.equalsany,
property: ProfileConditionValue.videoprofile,
$Value: 'main',
isRequired: false,
),
ProfileCondition(
condition: ProfileConditionType.equalsany,
property: ProfileConditionValue.videorangetype,
$Value: 'SDR|HDR10|HLG',
isRequired: false,
),
ProfileCondition(
condition: ProfileConditionType.lessthanequal,
property: ProfileConditionValue.videolevel,
$Value: '120',
isRequired: false,
),
ProfileCondition(
condition: ProfileConditionType.notequals,
property: ProfileConditionValue.isinterlaced,
$Value: 'true',
isRequired: false,
),
],
),
CodecProfile(
type: CodecType.video,
codec: 'vp9',
conditions: [
ProfileCondition(
condition: ProfileConditionType.equalsany,
property: ProfileConditionValue.videorangetype,
$Value: 'SDR|HDR10|HLG',
isRequired: false,
),
],
),
CodecProfile(
type: CodecType.video,
codec: 'av1',
conditions: [
ProfileCondition(
condition: ProfileConditionType.notequals,
property: ProfileConditionValue.isanamorphic,
$Value: 'true',
isRequired: false,
),
ProfileCondition(
condition: ProfileConditionType.equalsany,
property: ProfileConditionValue.videoprofile,
$Value: 'main',
isRequired: false,
),
ProfileCondition(
condition: ProfileConditionType.equalsany,
property: ProfileConditionValue.videorangetype,
$Value: 'SDR|HDR10|HLG',
isRequired: false,
),
ProfileCondition(
condition: ProfileConditionType.lessthanequal,
property: ProfileConditionValue.videolevel,
$Value: '19',
isRequired: false,
),
],
),
],
subtitleProfiles: [
SubtitleProfile(format: 'vtt', method: SubtitleDeliveryMethod.embed),
SubtitleProfile(format: 'srt', method: SubtitleDeliveryMethod.embed),
],
);

View file

@ -0,0 +1,73 @@
import 'dart:developer';
import 'package:chopper/chopper.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/providers/auth_provider.dart';
import 'package:fladder/providers/service_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'api_provider.g.dart';
@riverpod
class JellyApi extends _$JellyApi {
@override
JellyService build() {
return JellyService(
ref,
JellyfinOpenApi.create(
interceptors: [
JellyRequest(ref),
JellyResponse(ref),
HttpLoggingInterceptor(level: Level.basic),
],
));
}
}
class JellyRequest implements RequestInterceptor {
JellyRequest(this.ref);
final Ref ref;
@override
FutureOr<Request> onRequest(Request request) async {
if (request.method == HttpMethod.Post) {
chopperLogger.info('Performed a POST request');
}
final serverUrl = Uri.parse(ref.read(userProvider)?.server ?? ref.read(authProvider).tempCredentials.server);
//Use current logged in user otherwise use the authprovider
var loginModel = ref.read(userProvider)?.credentials ?? ref.read(authProvider).tempCredentials;
var headers = loginModel.header(ref);
return request.copyWith(
baseUri: serverUrl,
headers: request.headers..addAll(headers),
);
}
}
class JellyResponse implements ResponseInterceptor {
JellyResponse(this.ref);
final Ref ref;
@override
FutureOr<Response<dynamic>> onResponse(Response<dynamic> response) {
if (!response.isSuccessful) {
log('x- ${response.base.statusCode} - ${response.base.reasonPhrase} - ${response.error} - ${response.base.request?.method} ${response.base.request?.url.toString()}');
}
if (response.statusCode == 404) {
chopperLogger.severe('404 NOT FOUND');
}
if (response.statusCode == 401) {
// ref.read(sharedUtilityProvider).removeAccount(ref.read(userProvider));
}
return response;
}
}

View file

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'api_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$jellyApiHash() => r'c0cdc4127e7191523b1356e71c54c93f99020c1e';
/// See also [JellyApi].
@ProviderFor(JellyApi)
final jellyApiProvider =
AutoDisposeNotifierProvider<JellyApi, JellyService>.internal(
JellyApi.new,
name: r'jellyApiProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$jellyApiHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$JellyApi = AutoDisposeNotifier<JellyService>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -0,0 +1,123 @@
import 'dart:developer';
import 'package:chopper/chopper.dart';
import 'package:fladder/models/account_model.dart';
import 'package:fladder/models/credentials_model.dart';
import 'package:fladder/models/login_screen_model.dart';
import 'package:fladder/providers/api_provider.dart';
import 'package:fladder/providers/dashboard_provider.dart';
import 'package:fladder/providers/favourites_provider.dart';
import 'package:fladder/providers/image_provider.dart';
import 'package:fladder/providers/service_provider.dart';
import 'package:fladder/providers/shared_provider.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/providers/views_provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final authProvider = StateNotifierProvider<AuthNotifier, LoginScreenModel>((ref) {
return AuthNotifier(ref);
});
class AuthNotifier extends StateNotifier<LoginScreenModel> {
AuthNotifier(this.ref)
: super(
LoginScreenModel(
accounts: [],
tempCredentials: CredentialsModel.createNewCredentials(),
loading: false,
),
);
final Ref ref;
late final JellyService api = ref.read(jellyApiProvider);
Future<Response<List<AccountModel>>?> getPublicUsers() async {
try {
var response = await api.usersPublicGet(state.tempCredentials);
if (response.isSuccessful && response.body != null) {
var models = response.body ?? [];
return response.copyWith(body: models.toList());
}
return response.copyWith(body: []);
} catch (e) {
return null;
}
}
Future<Response<AccountModel>?> authenticateByName(String userName, String password) async {
state = state.copyWith(loading: true);
clearAllProviders();
var response = await api.usersAuthenticateByNamePost(userName: userName, password: password);
var serverResponse = await api.systemInfoPublicGet();
CredentialsModel credentials = state.tempCredentials;
if (response.isSuccessful && (response.body?.accessToken?.isNotEmpty ?? false)) {
credentials = credentials.copyWith(
token: response.body?.accessToken ?? "",
serverId: response.body?.serverId,
serverName: serverResponse.body?.serverName ?? "",
);
var imageUrl = ref.read(imageUtilityProvider).getUserImageUrl(response.body?.user?.id ?? "");
AccountModel newUser = AccountModel(
name: response.body?.user?.name ?? "",
id: response.body?.user?.id ?? "",
avatar: imageUrl,
credentials: credentials,
lastUsed: DateTime.now(),
);
ref.read(sharedUtilityProvider).addAccount(newUser);
ref.read(userProvider.notifier).userState = newUser;
state = state.copyWith(loading: false);
return Response(response.base, newUser);
}
state = state.copyWith(loading: false);
return Response(response.base, null);
}
Future<Response?> logOutUser() async {
if (ref.read(userProvider) != null) {
final response = await api.sessionsLogoutPost();
if (response.isSuccessful) {
log('Logged out');
}
state = state.copyWith(tempCredentials: CredentialsModel.createNewCredentials());
await ref.read(sharedUtilityProvider).removeAccount(ref.read(userProvider));
return response;
}
clearAllProviders();
return null;
}
Future<void> switchUser() async {
clearAllProviders();
}
void clearAllProviders() {
ref.read(dashboardProvider.notifier).clear();
ref.read(viewsProvider.notifier).clear();
ref.read(favouritesProvider.notifier).clear();
ref.read(userProvider.notifier).clear();
ref.read(syncProvider.notifier).setup();
}
void setServer(String server) {
state = state.copyWith(
tempCredentials: state.tempCredentials.copyWith(server: server),
);
}
List<AccountModel> getSavedAccounts() {
state = state.copyWith(accounts: ref.read(sharedUtilityProvider).getAccounts());
return state.accounts;
}
void reOrderUsers(int oldIndex, int newIndex) {
final accounts = state.accounts;
final original = accounts.elementAt(oldIndex);
accounts.removeAt(oldIndex);
accounts.insert(newIndex, original);
ref.read(sharedUtilityProvider).saveAccounts(accounts);
}
}

View file

@ -0,0 +1,188 @@
import 'dart:developer';
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:chopper/chopper.dart';
import 'package:fladder/providers/service_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/book_model.dart';
import 'package:fladder/providers/api_provider.dart';
import 'package:path_provider/path_provider.dart';
class BookViewerModel {
final BookModel? book;
final bool loading;
final List<String> pages;
final int currentPage;
BookViewerModel({
this.book,
this.loading = false,
this.pages = const [],
this.currentPage = 0,
});
int get clampedCurrentPage => currentPage.clamp(0, pages.length);
BookViewerModel copyWith({
ValueGetter<BookModel?>? book,
bool? loading,
List<String>? pages,
int? currentPage,
}) {
return BookViewerModel(
book: book != null ? book.call() : this.book,
loading: loading ?? this.loading,
pages: pages ?? this.pages,
currentPage: currentPage ?? this.currentPage,
);
}
}
final bookViewerProvider = StateNotifierProvider<BookViewerNotifier, BookViewerModel>((ref) {
return BookViewerNotifier(ref);
});
class BookViewerNotifier extends StateNotifier<BookViewerModel> {
BookViewerNotifier(this.ref) : super(BookViewerModel());
final Ref ref;
late Directory savedDirectory;
late final JellyService api = ref.read(jellyApiProvider);
Future<List<String>?> fetchBook(BookModel? book) async {
final oldState = state.copyWith();
state = state.copyWith(loading: true, book: () => book, currentPage: 0);
//Stop and cleanup old state
await _stopPlaybackOldState(oldState);
if (state.book == null) return null;
try {
final response = await api.itemsItemIdDownloadGet(itemId: state.book?.id);
final bookDirectory = state.book?.id;
String tempDir = (await getTemporaryDirectory()).path;
savedDirectory = Directory('$tempDir/$bookDirectory');
await savedDirectory.create();
File bookFile = File('${savedDirectory.path}/archive.book');
await bookFile.writeAsBytes(response.bodyBytes);
final inputStream = InputFileStream(bookFile.path);
final archive = ZipDecoder().decodeBuffer(inputStream);
final List<String> imagesPath = [];
for (var file in archive.files) {
//filter out files with image extension
if (file.isFile && _isImageFile(file.name)) {
final path = '${savedDirectory.path}/Pages/${file.name}';
final outputStream = OutputFileStream('${savedDirectory.path}/Pages/${file.name}');
file.writeContent(outputStream);
imagesPath.add(path);
outputStream.close();
}
}
state = state.copyWith(pages: imagesPath, loading: false);
await inputStream.close();
await bookFile.delete();
return imagesPath;
} catch (e) {
log(e.toString());
state = state.copyWith(loading: false);
}
return null;
}
//Simple file checker
bool _isImageFile(String filePath) {
final imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'tif', 'webp'];
final fileExtension = filePath.toLowerCase().split('.').last;
return imageExtensions.contains(fileExtension);
}
Future<Response?> updatePlayback(int page) async {
if (state.book == null) return null;
if (page == state.currentPage) return null;
state = state.copyWith(currentPage: page);
return await api.sessionsPlayingStoppedPost(
body: PlaybackStopInfo(
itemId: state.book?.id,
mediaSourceId: state.book?.id,
positionTicks: state.clampedCurrentPage * 10000,
),
);
}
Future<Response?> _stopPlaybackOldState(BookViewerModel oldState) async {
if (oldState.book == null) return null;
if (oldState.clampedCurrentPage < oldState.pages.length && oldState.pages.isNotEmpty) {
await ref.read(userProvider.notifier).markAsPlayed(false, oldState.book?.id ?? "");
}
final response = await api.sessionsPlayingStoppedPost(
body: PlaybackStopInfo(
itemId: oldState.book?.id,
mediaSourceId: oldState.book?.id,
positionTicks: oldState.clampedCurrentPage * 10000,
));
if (oldState.clampedCurrentPage >= oldState.pages.length && oldState.pages.isNotEmpty) {
await ref.read(userProvider.notifier).markAsPlayed(true, oldState.book?.id ?? "");
}
await _cleanUp();
return response;
}
Future<Response?> stopPlayback() async {
if (state.book == null) return null;
if (state.clampedCurrentPage < state.pages.length && state.pages.isNotEmpty) {
await ref.read(userProvider.notifier).markAsPlayed(false, state.book?.id ?? "");
}
final response = await api.sessionsPlayingStoppedPost(
body: PlaybackStopInfo(
itemId: state.book?.id,
mediaSourceId: state.book?.id,
positionTicks: state.clampedCurrentPage * 10000,
));
if (state.clampedCurrentPage >= state.pages.length && state.pages.isNotEmpty) {
await ref.read(userProvider.notifier).markAsPlayed(true, state.book?.id ?? "");
}
await _cleanUp();
return response;
}
Future<void> _cleanUp() async {
try {
for (var i = 0; i < state.pages.length; i++) {
final file = File(state.pages[i]);
if (file.existsSync()) {
await file.delete();
}
}
final directoryExists = await savedDirectory.exists();
if (directoryExists) {
await savedDirectory.delete(recursive: true);
}
} catch (e) {
log(e.toString());
}
}
void setPage(double value) => state = state.copyWith(currentPage: value.toInt());
void setBook(BookModel book) => state = state.copyWith(
book: () => book,
);
}

View file

@ -0,0 +1,100 @@
import 'package:chopper/chopper.dart';
import 'package:fladder/providers/service_provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/boxset_model.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/providers/api_provider.dart';
import 'package:fladder/util/map_bool_helper.dart';
class _CollectionSetModel {
final List<ItemBaseModel> items;
final Map<BoxSetModel, bool?> collections;
_CollectionSetModel({
required this.items,
required this.collections,
});
_CollectionSetModel copyWith({
List<ItemBaseModel>? items,
Map<BoxSetModel, bool?>? collections,
}) {
return _CollectionSetModel(
items: items ?? this.items,
collections: collections ?? this.collections,
);
}
}
final collectionsProvider = StateNotifierProvider.autoDispose<BoxSetNotifier, _CollectionSetModel>((ref) {
return BoxSetNotifier(ref);
});
class BoxSetNotifier extends StateNotifier<_CollectionSetModel> {
BoxSetNotifier(this.ref) : super(_CollectionSetModel(items: [], collections: {}));
final Ref ref;
late final JellyService api = ref.read(jellyApiProvider);
Future<void> setItems(List<ItemBaseModel> items) async {
state = state.copyWith(items: items);
return _init();
}
Future<void> _init() async {
final collections = await api.usersUserIdItemsGet(
recursive: true,
includeItemTypes: [
BaseItemKind.boxset,
],
);
final boxsets = collections.body?.items?.map((e) => BoxSetModel.fromBaseDto(e, ref)).toList();
if (state.items.length == 1 && (boxsets?.length ?? 0) < 25) {
final List<Future<bool>> itemChecks = boxsets?.map((element) async {
final itemList = await api.usersUserIdItemsGet(
parentId: element.id,
);
final List<String?> items = (itemList.body?.items ?? []).map((e) => e.id).toList();
return items.contains(state.items.firstOrNull?.id);
}).toList() ??
[];
final List<bool> results = await Future.wait(itemChecks);
final Map<BoxSetModel, bool?> boxSetContainsItemMap = Map.fromIterables(boxsets ?? [], results);
state = state.copyWith(collections: boxSetContainsItemMap);
} else {
final Map<BoxSetModel, bool?> boxSetContainsItemMap =
Map.fromIterables(boxsets ?? [], List.generate(boxsets?.length ?? 0, (index) => null));
state = state.copyWith(collections: boxSetContainsItemMap);
}
}
Future<Response> toggleCollection(
{required BoxSetModel boxSet, required bool value, required ItemBaseModel item}) async {
final Response response = value
? await api.collectionsCollectionIdItemsPost(collectionId: boxSet.id, ids: [item.id])
: await api.collectionsCollectionIdItemsDelete(collectionId: boxSet.id, ids: [item.id]);
if (response.isSuccessful) {
state = state.copyWith(collections: state.collections.setKey(boxSet, response.isSuccessful ? value : !value));
}
return response;
}
Future<Response> addToCollection({required BoxSetModel boxSet, required bool add}) async => add
? await api.collectionsCollectionIdItemsPost(collectionId: boxSet.id, ids: state.items.map((e) => e.id).toList())
: await api.collectionsCollectionIdItemsDelete(
collectionId: boxSet.id, ids: state.items.map((e) => e.id).toList());
Future<void> addToNewCollection({required String name}) async {
final result = await api.collectionsPost(name: name, ids: state.items.map((e) => e.id).toList());
if (result.isSuccessful) {
await _init();
}
}
}

View file

@ -0,0 +1,111 @@
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/models/home_model.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/providers/api_provider.dart';
import 'package:fladder/providers/service_provider.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/views_provider.dart';
import 'package:fladder/util/list_extensions.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final dashboardProvider = StateNotifierProvider<DashboardNotifier, HomeModel>((ref) {
return DashboardNotifier(ref);
});
class DashboardNotifier extends StateNotifier<HomeModel> {
DashboardNotifier(this.ref) : super(HomeModel());
final Ref ref;
late final JellyService api = ref.read(jellyApiProvider);
Future<void> fetchNextUpAndResume() async {
if (state.loading) return;
state = state.copyWith(loading: true);
final viewTypes =
ref.read(viewsProvider.select((value) => value.dashboardViews)).map((e) => e.collectionType).toSet().toList();
if (viewTypes.containsAny([CollectionType.movies, CollectionType.tvshows])) {
final resumeVideoResponse = await api.usersUserIdItemsResumeGet(
limit: 16,
fields: [
ItemFields.parentid,
ItemFields.mediastreams,
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
],
mediaTypes: [MediaType.video],
enableTotalRecordCount: false,
);
state = state.copyWith(
resumeVideo: resumeVideoResponse.body?.items?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList(),
);
}
if (viewTypes.contains(CollectionType.music)) {
final resumeAudioResponse = await api.usersUserIdItemsResumeGet(
limit: 16,
fields: [
ItemFields.parentid,
ItemFields.mediastreams,
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
],
mediaTypes: [MediaType.audio],
enableTotalRecordCount: false,
);
state = state.copyWith(
resumeAudio: resumeAudioResponse.body?.items?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList(),
);
}
if (viewTypes.contains(CollectionType.books)) {
final resumeBookResponse = await api.usersUserIdItemsResumeGet(
limit: 16,
fields: [
ItemFields.parentid,
ItemFields.mediastreams,
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
],
mediaTypes: [MediaType.book],
enableTotalRecordCount: false,
);
state = state.copyWith(
resumeBooks: resumeBookResponse.body?.items?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList(),
);
}
final nextResponse = await api.showsNextUpGet(
limit: 16,
nextUpDateCutoff: DateTime.now()
.subtract(ref.read(clientSettingsProvider.select((value) => value.nextUpDateCutoff ?? Duration(days: 28)))),
fields: [
ItemFields.parentid,
ItemFields.mediastreams,
ItemFields.mediasources,
ItemFields.candelete,
ItemFields.candownload,
],
);
final next = nextResponse.body?.items
?.map(
(e) => ItemBaseModel.fromBaseDto(e, ref),
)
.toList() ??
[];
state = state.copyWith(nextUp: next, loading: false);
}
void clear() {
state = HomeModel();
}
}

Some files were not shown because too many files have changed in this diff Show more