mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-08 23:18:16 -07:00
Init repo
This commit is contained in:
commit
764b6034e3
566 changed files with 212335 additions and 0 deletions
73
lib/providers/api_provider.dart
Normal file
73
lib/providers/api_provider.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
25
lib/providers/api_provider.g.dart
Normal file
25
lib/providers/api_provider.g.dart
Normal 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
|
||||
123
lib/providers/auth_provider.dart
Normal file
123
lib/providers/auth_provider.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
188
lib/providers/book_viewer_provider.dart
Normal file
188
lib/providers/book_viewer_provider.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
100
lib/providers/collections_provider.dart
Normal file
100
lib/providers/collections_provider.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
111
lib/providers/dashboard_provider.dart
Normal file
111
lib/providers/dashboard_provider.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
114
lib/providers/discovery_provider.dart
Normal file
114
lib/providers/discovery_provider.dart
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dart_mappable/dart_mappable.dart';
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/providers/service_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'discovery_provider.g.dart';
|
||||
part 'discovery_provider.mapper.dart';
|
||||
|
||||
@riverpod
|
||||
class ServerDiscovery extends _$ServerDiscovery {
|
||||
final String discoveryMessage = 'Who is JellyfinServer?';
|
||||
final int discoveryPort = 7359;
|
||||
final int maxServerCount = 25;
|
||||
final Duration timeOut = const Duration(seconds: 5);
|
||||
late final JellyService api = JellyService(ref, JellyfinOpenApi.create());
|
||||
|
||||
@override
|
||||
Stream<List<DiscoveryInfo>> build() async* {
|
||||
final List<DiscoveryInfo> discoveredServers = [];
|
||||
final StreamController<List<DiscoveryInfo>> controller = StreamController<List<DiscoveryInfo>>();
|
||||
|
||||
// Bind the socket and start listening
|
||||
final RawDatagramSocket socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
|
||||
socket.broadcastEnabled = true;
|
||||
|
||||
// Send the broadcast message
|
||||
socket.send(
|
||||
utf8.encode(discoveryMessage),
|
||||
InternetAddress('255.255.255.255'), // Broadcast address
|
||||
discoveryPort,
|
||||
);
|
||||
|
||||
// log('Discovery message sent. Waiting for response...');
|
||||
|
||||
// Set a timer to close the socket after the timeout
|
||||
Timer timer = Timer(timeOut, () {
|
||||
// log('Timeout reached, closing socket.');
|
||||
if (discoveredServers.isEmpty) {
|
||||
controller.add([]);
|
||||
}
|
||||
socket.close();
|
||||
controller.close(); // Close the stream controller when done
|
||||
});
|
||||
|
||||
socket.listen((RawSocketEvent event) {
|
||||
if (event == RawSocketEvent.read) {
|
||||
Datagram? dg = socket.receive();
|
||||
if (dg != null) {
|
||||
// Decode the response
|
||||
String response = utf8.decode(dg.data);
|
||||
Map<String, dynamic> jsonResponse = jsonDecode(response);
|
||||
|
||||
final discovery = DiscoveryInfo.fromMap(jsonResponse);
|
||||
|
||||
discoveredServers.add(discovery);
|
||||
controller.add(List<DiscoveryInfo>.from(discoveredServers)); // Emit the updated list
|
||||
|
||||
if (discoveredServers.length >= maxServerCount) {
|
||||
log('Max servers found, closing socket.');
|
||||
timer.cancel();
|
||||
socket.close();
|
||||
controller.close(); // Close the stream controller
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
yield* controller.stream;
|
||||
|
||||
// Handle disposal when the provider is no longer needed
|
||||
ref.onDispose(() {
|
||||
timer.cancel();
|
||||
socket.close();
|
||||
controller.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@MappableClass()
|
||||
class DiscoveryInfo with DiscoveryInfoMappable {
|
||||
@MappableField(key: 'Id')
|
||||
final String id;
|
||||
@MappableField(key: 'Name')
|
||||
final String name;
|
||||
@MappableField(key: 'Address')
|
||||
final String address;
|
||||
@MappableField(key: "EndpointAddress")
|
||||
final String? endPointAddress;
|
||||
|
||||
const DiscoveryInfo({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.address,
|
||||
required this.endPointAddress,
|
||||
});
|
||||
|
||||
factory DiscoveryInfo.fromMap(Map<String, dynamic> map) => DiscoveryInfoMapper.fromMap(map);
|
||||
factory DiscoveryInfo.fromJson(String json) => DiscoveryInfoMapper.fromJson(json);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is DiscoveryInfo && other.id == id && other.address == address;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ address.hashCode;
|
||||
}
|
||||
26
lib/providers/discovery_provider.g.dart
Normal file
26
lib/providers/discovery_provider.g.dart
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'discovery_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$serverDiscoveryHash() => r'f299dab33f48950f0bd91afab1f831fd6e351923';
|
||||
|
||||
/// See also [ServerDiscovery].
|
||||
@ProviderFor(ServerDiscovery)
|
||||
final serverDiscoveryProvider = AutoDisposeStreamNotifierProvider<
|
||||
ServerDiscovery, List<DiscoveryInfo>>.internal(
|
||||
ServerDiscovery.new,
|
||||
name: r'serverDiscoveryProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$serverDiscoveryHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$ServerDiscovery = AutoDisposeStreamNotifier<List<DiscoveryInfo>>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
130
lib/providers/discovery_provider.mapper.dart
Normal file
130
lib/providers/discovery_provider.mapper.dart
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
// 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 'discovery_provider.dart';
|
||||
|
||||
class DiscoveryInfoMapper extends ClassMapperBase<DiscoveryInfo> {
|
||||
DiscoveryInfoMapper._();
|
||||
|
||||
static DiscoveryInfoMapper? _instance;
|
||||
static DiscoveryInfoMapper ensureInitialized() {
|
||||
if (_instance == null) {
|
||||
MapperContainer.globals.use(_instance = DiscoveryInfoMapper._());
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
@override
|
||||
final String id = 'DiscoveryInfo';
|
||||
|
||||
static String _$id(DiscoveryInfo v) => v.id;
|
||||
static const Field<DiscoveryInfo, String> _f$id =
|
||||
Field('id', _$id, key: 'Id');
|
||||
static String _$name(DiscoveryInfo v) => v.name;
|
||||
static const Field<DiscoveryInfo, String> _f$name =
|
||||
Field('name', _$name, key: 'Name');
|
||||
static String _$address(DiscoveryInfo v) => v.address;
|
||||
static const Field<DiscoveryInfo, String> _f$address =
|
||||
Field('address', _$address, key: 'Address');
|
||||
static String? _$endPointAddress(DiscoveryInfo v) => v.endPointAddress;
|
||||
static const Field<DiscoveryInfo, String> _f$endPointAddress =
|
||||
Field('endPointAddress', _$endPointAddress, key: 'EndpointAddress');
|
||||
|
||||
@override
|
||||
final MappableFields<DiscoveryInfo> fields = const {
|
||||
#id: _f$id,
|
||||
#name: _f$name,
|
||||
#address: _f$address,
|
||||
#endPointAddress: _f$endPointAddress,
|
||||
};
|
||||
@override
|
||||
final bool ignoreNull = true;
|
||||
|
||||
static DiscoveryInfo _instantiate(DecodingData data) {
|
||||
return DiscoveryInfo(
|
||||
id: data.dec(_f$id),
|
||||
name: data.dec(_f$name),
|
||||
address: data.dec(_f$address),
|
||||
endPointAddress: data.dec(_f$endPointAddress));
|
||||
}
|
||||
|
||||
@override
|
||||
final Function instantiate = _instantiate;
|
||||
|
||||
static DiscoveryInfo fromMap(Map<String, dynamic> map) {
|
||||
return ensureInitialized().decodeMap<DiscoveryInfo>(map);
|
||||
}
|
||||
|
||||
static DiscoveryInfo fromJson(String json) {
|
||||
return ensureInitialized().decodeJson<DiscoveryInfo>(json);
|
||||
}
|
||||
}
|
||||
|
||||
mixin DiscoveryInfoMappable {
|
||||
String toJson() {
|
||||
return DiscoveryInfoMapper.ensureInitialized()
|
||||
.encodeJson<DiscoveryInfo>(this as DiscoveryInfo);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return DiscoveryInfoMapper.ensureInitialized()
|
||||
.encodeMap<DiscoveryInfo>(this as DiscoveryInfo);
|
||||
}
|
||||
|
||||
DiscoveryInfoCopyWith<DiscoveryInfo, DiscoveryInfo, DiscoveryInfo>
|
||||
get copyWith => _DiscoveryInfoCopyWithImpl(
|
||||
this as DiscoveryInfo, $identity, $identity);
|
||||
@override
|
||||
String toString() {
|
||||
return DiscoveryInfoMapper.ensureInitialized()
|
||||
.stringifyValue(this as DiscoveryInfo);
|
||||
}
|
||||
}
|
||||
|
||||
extension DiscoveryInfoValueCopy<$R, $Out>
|
||||
on ObjectCopyWith<$R, DiscoveryInfo, $Out> {
|
||||
DiscoveryInfoCopyWith<$R, DiscoveryInfo, $Out> get $asDiscoveryInfo =>
|
||||
$base.as((v, t, t2) => _DiscoveryInfoCopyWithImpl(v, t, t2));
|
||||
}
|
||||
|
||||
abstract class DiscoveryInfoCopyWith<$R, $In extends DiscoveryInfo, $Out>
|
||||
implements ClassCopyWith<$R, $In, $Out> {
|
||||
$R call({String? id, String? name, String? address, String? endPointAddress});
|
||||
DiscoveryInfoCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t);
|
||||
}
|
||||
|
||||
class _DiscoveryInfoCopyWithImpl<$R, $Out>
|
||||
extends ClassCopyWithBase<$R, DiscoveryInfo, $Out>
|
||||
implements DiscoveryInfoCopyWith<$R, DiscoveryInfo, $Out> {
|
||||
_DiscoveryInfoCopyWithImpl(super.value, super.then, super.then2);
|
||||
|
||||
@override
|
||||
late final ClassMapperBase<DiscoveryInfo> $mapper =
|
||||
DiscoveryInfoMapper.ensureInitialized();
|
||||
@override
|
||||
$R call(
|
||||
{String? id,
|
||||
String? name,
|
||||
String? address,
|
||||
Object? endPointAddress = $none}) =>
|
||||
$apply(FieldCopyWithData({
|
||||
if (id != null) #id: id,
|
||||
if (name != null) #name: name,
|
||||
if (address != null) #address: address,
|
||||
if (endPointAddress != $none) #endPointAddress: endPointAddress
|
||||
}));
|
||||
@override
|
||||
DiscoveryInfo $make(CopyWithData data) => DiscoveryInfo(
|
||||
id: data.get(#id, or: $value.id),
|
||||
name: data.get(#name, or: $value.name),
|
||||
address: data.get(#address, or: $value.address),
|
||||
endPointAddress: data.get(#endPointAddress, or: $value.endPointAddress));
|
||||
|
||||
@override
|
||||
DiscoveryInfoCopyWith<$R2, DiscoveryInfo, $Out2> $chain<$R2, $Out2>(
|
||||
Then<$Out2, $R2> t) =>
|
||||
_DiscoveryInfoCopyWithImpl($value, $cast, t);
|
||||
}
|
||||
234
lib/providers/edit_item_provider.dart
Normal file
234
lib/providers/edit_item_provider.dart
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
import 'dart:convert';
|
||||
import 'package:chopper/chopper.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/item_editing_model.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
|
||||
final editItemProvider =
|
||||
StateNotifierProvider.autoDispose<EditItemNotifier, ItemEditingModel>((ref) => EditItemNotifier(ref));
|
||||
|
||||
class EditItemNotifier extends StateNotifier<ItemEditingModel> {
|
||||
EditItemNotifier(this.ref) : super(ItemEditingModel());
|
||||
|
||||
final Ref ref;
|
||||
|
||||
late final api = ref.read(jellyApiProvider);
|
||||
|
||||
Map<String, dynamic>? get getFields => state.editAbleFields();
|
||||
Map<String, dynamic>? get advancedFields => state.editAdvancedAbleFields(ref);
|
||||
|
||||
Future<void> fetchInformation(String id) async {
|
||||
state = ItemEditingModel();
|
||||
final itemResponse = await api.usersUserIdItemsItemIdGet(
|
||||
itemId: id,
|
||||
);
|
||||
final itemModel = itemResponse.body;
|
||||
if (itemModel == null) return;
|
||||
final images = await api.itemsItemIdImagesGet(itemId: itemModel.id);
|
||||
|
||||
state = state.copyWith(
|
||||
item: () => itemModel,
|
||||
json: () => jsonDecode(itemResponse.bodyString),
|
||||
editedJson: () => jsonDecode(itemResponse.bodyString),
|
||||
primary: state.primary.copyWith(
|
||||
serverImages: images.body
|
||||
?.where((element) => element.imageType == ImageType.primary)
|
||||
.map((e) => EditingImageModel.fromImage(e, itemModel.id, ref))
|
||||
.toList(),
|
||||
),
|
||||
logo: state.logo.copyWith(
|
||||
serverImages: images.body
|
||||
?.where((element) => element.imageType == ImageType.logo)
|
||||
.map((e) => EditingImageModel.fromImage(e, itemModel.id, ref))
|
||||
.toList(),
|
||||
),
|
||||
backdrop: state.backdrop.copyWith(
|
||||
serverImages: images.body
|
||||
?.where((element) => element.imageType == ImageType.backdrop)
|
||||
.map((e) => EditingImageModel.fromImage(e, itemModel.id, ref))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
final response = await api.itemsItemIdMetadataEditorGet(itemId: id);
|
||||
state = state.copyWith(
|
||||
editorInfo: () => response.bodyOrThrow,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<dynamic>?> fetchRemoteImages({ImageType type = ImageType.primary}) async {
|
||||
final currentItem = state.item;
|
||||
if (currentItem == null) return null;
|
||||
final response = await api.itemsItemIdRemoteImagesGet(
|
||||
itemId: currentItem.id,
|
||||
type: type,
|
||||
includeAllLanguages: state.includeAllImages,
|
||||
);
|
||||
final newImages = (response.body?.images ?? []).map((e) => EditingImageModel.fromDto(e)).toList();
|
||||
switch (type) {
|
||||
case ImageType.backdrop:
|
||||
state = state.copyWith(backdrop: state.backdrop.copyWith(images: newImages));
|
||||
case ImageType.logo:
|
||||
state = state.copyWith(logo: state.logo.copyWith(images: newImages));
|
||||
case ImageType.primary:
|
||||
default:
|
||||
state = state.copyWith(primary: state.primary.copyWith(images: newImages));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<void> updateField(MapEntry<String, dynamic> field) async {
|
||||
final editedJson = state.editedJson;
|
||||
editedJson?.update(
|
||||
field.key,
|
||||
(value) => field.value,
|
||||
ifAbsent: () => editedJson.addEntries({field}),
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
editedJson: () => editedJson,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> resetChanged() async {
|
||||
state = state.copyWith(
|
||||
editedJson: () => state.json,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<ItemBaseModel>?> saveInformation() async {
|
||||
final currentItem = state.item;
|
||||
if (currentItem == null) return null;
|
||||
final jsonBody = state.editedJson;
|
||||
if (jsonBody == null) return null;
|
||||
state = state.copyWith(saving: true);
|
||||
final response = await api.itemsItemIdPost(
|
||||
itemId: currentItem.id,
|
||||
body: BaseItemDto.fromJson(jsonBody),
|
||||
);
|
||||
await state.primary.setImage(
|
||||
ImageType.primary,
|
||||
uploadData: uploadImage,
|
||||
uploadUrl: _setImage,
|
||||
);
|
||||
await state.logo.setImage(
|
||||
ImageType.logo,
|
||||
uploadData: uploadImage,
|
||||
uploadUrl: _setImage,
|
||||
);
|
||||
|
||||
await state.backdrop.setImage(
|
||||
ImageType.backdrop,
|
||||
uploadData: uploadImage,
|
||||
uploadUrl: _setImage,
|
||||
);
|
||||
|
||||
final newItem = await api.usersUserIdItemsItemIdGet(itemId: currentItem.id);
|
||||
|
||||
state = state.copyWith(saving: false);
|
||||
return response.copyWith(body: newItem.body);
|
||||
}
|
||||
|
||||
Future<Response<dynamic>?> uploadImage(EditingImageModel? imageModel) async {
|
||||
final currentItem = state.item;
|
||||
if (currentItem == null || imageModel == null) return null;
|
||||
final response = await api.itemIdImagesImageTypePost(
|
||||
imageModel.type,
|
||||
currentItem.id,
|
||||
imageModel.imageData!,
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<Response<dynamic>?> _setImage(EditingImageModel? imageModel) async {
|
||||
final currentItem = state.item;
|
||||
if (currentItem == null) return null;
|
||||
if (imageModel == null) return null;
|
||||
return await api.itemsItemIdRemoteImagesDownloadPost(
|
||||
itemId: state.item?.id,
|
||||
type: imageModel.type,
|
||||
imageUrl: imageModel.url,
|
||||
);
|
||||
}
|
||||
|
||||
void selectImage(ImageType type, EditingImageModel? image) {
|
||||
switch (type) {
|
||||
case ImageType.primary:
|
||||
state = state.copyWith(
|
||||
primary: state.primary.copyWith(selected: () => state.primary.selected == image ? null : image));
|
||||
case ImageType.logo:
|
||||
state = state.copyWith(logo: state.logo.copyWith(selected: () => state.logo.selected == image ? null : image));
|
||||
default:
|
||||
if (image == null) return;
|
||||
state = state.copyWith(
|
||||
backdrop: state.backdrop.copyWith(
|
||||
selection: state.backdrop.selection.contains(image)
|
||||
? (List.of(state.backdrop.selection)..remove(image))
|
||||
: [...state.backdrop.selection, image],
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void setIncludeImages(bool value) => state = state.copyWith(includeAllImages: value);
|
||||
|
||||
void addCustomImages(ImageType type, Iterable<EditingImageModel> list) {
|
||||
switch (type) {
|
||||
case ImageType.primary:
|
||||
state = state.copyWith(
|
||||
primary: state.primary.copyWith(
|
||||
customImages: [...state.primary.customImages, ...list],
|
||||
selected: () => list.firstOrNull,
|
||||
),
|
||||
);
|
||||
return;
|
||||
case ImageType.logo:
|
||||
state = state.copyWith(
|
||||
logo: state.logo.copyWith(
|
||||
customImages: [...state.logo.customImages, ...list],
|
||||
selected: () => list.firstOrNull,
|
||||
),
|
||||
);
|
||||
return;
|
||||
case ImageType.backdrop:
|
||||
state = state.copyWith(
|
||||
backdrop: state.backdrop.copyWith(
|
||||
customImages: [...state.backdrop.customImages, ...list],
|
||||
selection: [...state.backdrop.selection, ...list]),
|
||||
);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Response<dynamic>?> deleteImage(ImageType type, EditingImageModel image) async {
|
||||
final currentItem = state.item;
|
||||
if (currentItem == null) return null;
|
||||
final response = await api.itemsItemIdImagesImageTypeDelete(
|
||||
itemId: state.item?.id,
|
||||
imageType: type,
|
||||
imageIndex: image.index,
|
||||
);
|
||||
switch (type) {
|
||||
case ImageType.primary:
|
||||
state = state.copyWith(
|
||||
primary: state.primary
|
||||
.copyWith(serverImages: state.primary.serverImages..removeWhere((element) => element == image)),
|
||||
);
|
||||
case ImageType.logo:
|
||||
state = state.copyWith(
|
||||
logo: state.logo.copyWith(serverImages: state.logo.serverImages..removeWhere((element) => element == image)),
|
||||
);
|
||||
case ImageType.backdrop:
|
||||
state = state.copyWith(
|
||||
backdrop: state.backdrop
|
||||
.copyWith(serverImages: state.backdrop.serverImages..removeWhere((element) => element == image)),
|
||||
);
|
||||
default:
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
81
lib/providers/favourites_provider.dart
Normal file
81
lib/providers/favourites_provider.dart
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import 'package:chopper/chopper.dart';
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/favourites_model.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/view_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/views_provider.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final favouritesProvider = StateNotifierProvider<FavouritesNotifier, FavouritesModel>((ref) {
|
||||
return FavouritesNotifier(ref);
|
||||
});
|
||||
|
||||
class FavouritesNotifier extends StateNotifier<FavouritesModel> {
|
||||
FavouritesNotifier(this.ref) : super(FavouritesModel());
|
||||
|
||||
final Ref ref;
|
||||
|
||||
late final api = ref.read(jellyApiProvider);
|
||||
|
||||
Future<void> fetchFavourites() async {
|
||||
if (state.loading) return;
|
||||
|
||||
state = state.copyWith(loading: true);
|
||||
await _fetchMoviesAndSeries();
|
||||
await _fetchPeople();
|
||||
state = state.copyWith(loading: false);
|
||||
}
|
||||
|
||||
Future<void> _fetchMoviesAndSeries() async {
|
||||
final views = ref.read(viewsProvider);
|
||||
|
||||
final mappedList = await Future.wait(views.dashboardViews.map((viewModel) => _loadLibrary(viewModel: viewModel)));
|
||||
|
||||
state = state.copyWith(
|
||||
favourites: (mappedList
|
||||
.expand((innerList) => innerList ?? [])
|
||||
.where((item) => item != null)
|
||||
.cast<ItemBaseModel>()
|
||||
.toList())
|
||||
.groupedItems);
|
||||
}
|
||||
|
||||
Future<List<ItemBaseModel>?> _loadLibrary({ViewModel? viewModel}) async {
|
||||
final response = await api.itemsGet(
|
||||
parentId: viewModel?.id,
|
||||
isFavorite: true,
|
||||
limit: 10,
|
||||
sortOrder: [SortOrder.ascending],
|
||||
sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname],
|
||||
);
|
||||
final response2 = await api.itemsGet(
|
||||
parentId: viewModel?.id,
|
||||
isFavorite: true,
|
||||
recursive: true,
|
||||
limit: 10,
|
||||
includeItemTypes: [BaseItemKind.photo, BaseItemKind.episode, BaseItemKind.video, BaseItemKind.collectionfolder],
|
||||
sortOrder: [SortOrder.ascending],
|
||||
sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname],
|
||||
);
|
||||
return [...?response.body?.items, ...?response2.body?.items];
|
||||
}
|
||||
|
||||
Future<Response<List<ItemBaseModel>>?> _fetchPeople() async {
|
||||
final response = await api.personsGet(
|
||||
limit: 20,
|
||||
isFavorite: true,
|
||||
);
|
||||
state = state.copyWith(people: response.body ?? []);
|
||||
return response;
|
||||
}
|
||||
|
||||
void setSearch(String value) {
|
||||
state = state.copyWith(searchQuery: value);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
state = FavouritesModel();
|
||||
}
|
||||
}
|
||||
90
lib/providers/image_provider.dart
Normal file
90
lib/providers/image_provider.dart
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import 'package:fladder/providers/auth_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
|
||||
const _defaultHeight = 576;
|
||||
const _defaultWidth = 384;
|
||||
const _defaultQuality = 96;
|
||||
|
||||
final imageUtilityProvider = Provider<ImageNotifier>((ref) {
|
||||
return ImageNotifier(ref: ref);
|
||||
});
|
||||
|
||||
class ImageNotifier {
|
||||
final Ref ref;
|
||||
ImageNotifier({
|
||||
required this.ref,
|
||||
});
|
||||
|
||||
String get currentServerUrl {
|
||||
return ref.read(userProvider)?.server ?? ref.read(authProvider).tempCredentials.server;
|
||||
}
|
||||
|
||||
String getUserImageUrl(String id) {
|
||||
return Uri.decodeFull("$currentServerUrl/Users/$id/Images/${ImageType.primary.value}");
|
||||
}
|
||||
|
||||
String getItemsImageUrl(String itemId,
|
||||
{ImageType type = ImageType.primary,
|
||||
int maxHeight = _defaultHeight,
|
||||
int maxWidth = _defaultWidth,
|
||||
int quality = _defaultQuality}) {
|
||||
try {
|
||||
return Uri.decodeFull(
|
||||
"$currentServerUrl/Items/$itemId/Images/${type.value}?fillHeight=$maxHeight&fillWidth=$maxWidth&quality=$quality");
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
String getItemsOrigImageUrl(String itemId, {ImageType type = ImageType.primary}) {
|
||||
try {
|
||||
return Uri.decodeFull("$currentServerUrl/Items/$itemId/Images/${type.value}");
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
String getBackdropOrigImage(
|
||||
String itemId,
|
||||
int index,
|
||||
String hash,
|
||||
) {
|
||||
try {
|
||||
return Uri.decodeFull("$currentServerUrl/Items/$itemId/Images/Backdrop/$index?tag=$hash");
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
String getBackdropImage(
|
||||
String itemId,
|
||||
int index,
|
||||
String hash, {
|
||||
int maxHeight = _defaultHeight,
|
||||
int maxWidth = _defaultWidth,
|
||||
int quality = _defaultQuality,
|
||||
}) {
|
||||
try {
|
||||
return Uri.decodeFull(
|
||||
"$currentServerUrl/Items/$itemId/Images/Backdrop/$index?tag=$hash&fillHeight=$maxHeight&fillWidth=$maxWidth&quality=$quality");
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
String getChapterUrl(String itemId, int index,
|
||||
{ImageType type = ImageType.primary,
|
||||
int maxHeight = _defaultHeight,
|
||||
int maxWidth = _defaultWidth,
|
||||
int quality = _defaultQuality}) {
|
||||
try {
|
||||
return Uri.decodeFull(
|
||||
"$currentServerUrl/Items/$itemId/Images/Chapter/$index?fillHeight=$maxHeight&fillWidth=$maxWidth&quality=$quality");
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
149
lib/providers/items/book_details_provider.dart
Normal file
149
lib/providers/items/book_details_provider.dart
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:chopper/chopper.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fladder/models/items/images_models.dart';
|
||||
import 'package:fladder/models/library_search/library_search_options.dart';
|
||||
import 'package:fladder/providers/service_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/models/item_base_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
|
||||
class BookProviderModel {
|
||||
final List<BookModel> chapters;
|
||||
final ItemBaseModel? parentModel;
|
||||
BookProviderModel({
|
||||
this.chapters = const [],
|
||||
this.parentModel,
|
||||
});
|
||||
|
||||
BookModel? get book => chapters.firstOrNull;
|
||||
|
||||
ImagesData? get cover => parentModel?.getPosters ?? book?.getPosters;
|
||||
|
||||
List<BookModel> get allBooks {
|
||||
if (chapters.isEmpty) return [book].whereNotNull().toList();
|
||||
return chapters;
|
||||
}
|
||||
|
||||
bool get collectionPlayed {
|
||||
if (chapters.isEmpty) return book?.userData.played ?? false;
|
||||
for (var i = 0; i < chapters.length; i++) {
|
||||
if (!chapters[i].userData.played) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
BookModel? get nextUp {
|
||||
if (chapters.isEmpty) return book;
|
||||
return chapters.lastWhereOrNull((element) => element.currentPage != 0) ??
|
||||
chapters.firstWhereOrNull((element) => !element.userData.played) ??
|
||||
chapters.first;
|
||||
}
|
||||
|
||||
BookModel? nextChapter(BookModel? currentBook) {
|
||||
if (currentBook != null && chapters.isEmpty) return null;
|
||||
|
||||
final currentChapter = chapters.indexOf(currentBook!);
|
||||
|
||||
// Check if the current chapter is the last one
|
||||
if (currentChapter == chapters.length - 1) return null;
|
||||
|
||||
// Return the next chapter
|
||||
return chapters[currentChapter + 1];
|
||||
}
|
||||
|
||||
BookModel? previousChapter(BookModel? currentBook) {
|
||||
if (currentBook != null && chapters.isEmpty) return null;
|
||||
|
||||
final currentChapter = chapters.indexOf(currentBook!);
|
||||
|
||||
// Check if the current chapter is the first one
|
||||
if (currentChapter == 0) return null;
|
||||
|
||||
// Return the previous chapter
|
||||
return chapters[currentChapter - 1];
|
||||
}
|
||||
|
||||
BookProviderModel copyWith({
|
||||
List<BookModel>? chapters,
|
||||
ValueGetter<ItemBaseModel?>? parentModel,
|
||||
}) {
|
||||
return BookProviderModel(
|
||||
chapters: chapters ?? this.chapters,
|
||||
parentModel: parentModel != null ? parentModel.call() : this.parentModel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final bookDetailsProvider =
|
||||
StateNotifierProvider.autoDispose.family<BookDetailsProviderNotifier, BookProviderModel, String>((ref, id) {
|
||||
return BookDetailsProviderNotifier(ref);
|
||||
});
|
||||
|
||||
class BookDetailsProviderNotifier extends StateNotifier<BookProviderModel> {
|
||||
BookDetailsProviderNotifier(this.ref) : super(BookProviderModel());
|
||||
|
||||
final Ref ref;
|
||||
|
||||
late Directory savedDirectory;
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
Future<Response?> fetchDetails(BookModel book) async {
|
||||
state = state.copyWith(
|
||||
parentModel: () => book,
|
||||
);
|
||||
String bookId = state.book?.id ?? book.id;
|
||||
|
||||
final response = await api.usersUserIdItemsItemIdGet(itemId: bookId);
|
||||
final parentResponse = await api.usersUserIdItemsItemIdGet(itemId: response.body?.parentId);
|
||||
|
||||
final parentModel = parentResponse.bodyOrThrow;
|
||||
final getViews = await api.usersUserIdViewsGet();
|
||||
|
||||
//Hacky solution more false positives so good enough for now.
|
||||
final parentIsView =
|
||||
getViews.body?.items?.firstWhereOrNull((element) => element.name == parentResponse.body?.name) != null;
|
||||
|
||||
Response<ServerQueryResult>? siblingsResponse;
|
||||
if (!parentIsView) {
|
||||
siblingsResponse = await api.itemsGet(
|
||||
parentId: parentModel.id,
|
||||
recursive: true,
|
||||
sortBy: SortingOptions.name.toSortBy,
|
||||
fields: [
|
||||
ItemFields.genres,
|
||||
ItemFields.parentid,
|
||||
ItemFields.tags,
|
||||
ItemFields.datecreated,
|
||||
ItemFields.datelastmediaadded,
|
||||
ItemFields.parentid,
|
||||
ItemFields.overview,
|
||||
ItemFields.originaltitle,
|
||||
ItemFields.primaryimageaspectratio,
|
||||
],
|
||||
includeItemTypes: [
|
||||
BaseItemKind.book,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
siblingsResponse = null;
|
||||
}
|
||||
|
||||
final openedBook = response.bodyOrThrow;
|
||||
|
||||
state = state.copyWith(
|
||||
parentModel: !parentIsView ? () => parentResponse.bodyOrThrow : null,
|
||||
chapters: (siblingsResponse?.body?.items ?? [openedBook]).whereType<BookModel>().whereNotNull().toList(),
|
||||
);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
106
lib/providers/items/episode_details_provider.dart
Normal file
106
lib/providers/items/episode_details_provider.dart
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import 'package:chopper/chopper.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fladder/providers/service_provider.dart';
|
||||
import 'package:fladder/providers/sync_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/items/episode_model.dart';
|
||||
import 'package:fladder/models/items/series_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
|
||||
class EpisodeDetailModel {
|
||||
final SeriesModel? series;
|
||||
final List<EpisodeModel> episodes;
|
||||
final EpisodeModel? episode;
|
||||
EpisodeDetailModel({
|
||||
this.series,
|
||||
this.episodes = const [],
|
||||
this.episode,
|
||||
});
|
||||
|
||||
EpisodeDetailModel copyWith({
|
||||
SeriesModel? series,
|
||||
List<EpisodeModel>? episodes,
|
||||
EpisodeModel? episode,
|
||||
}) {
|
||||
return EpisodeDetailModel(
|
||||
series: series ?? this.series,
|
||||
episodes: episodes ?? this.episodes,
|
||||
episode: episode ?? this.episode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final episodeDetailsProvider =
|
||||
StateNotifierProvider.autoDispose.family<EpisodeDetailsProvider, EpisodeDetailModel, String>((ref, id) {
|
||||
return EpisodeDetailsProvider(ref);
|
||||
});
|
||||
|
||||
class EpisodeDetailsProvider extends StateNotifier<EpisodeDetailModel> {
|
||||
EpisodeDetailsProvider(this.ref) : super(EpisodeDetailModel());
|
||||
|
||||
final Ref ref;
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
Future<Response?> fetchDetails(ItemBaseModel item) async {
|
||||
try {
|
||||
final seriesResponse = await api.usersUserIdItemsItemIdGet(itemId: item.parentBaseModel.id);
|
||||
if (seriesResponse.body == null) return null;
|
||||
final episodes = await api.showsSeriesIdEpisodesGet(seriesId: item.parentBaseModel.id);
|
||||
|
||||
if (episodes.body == null) return null;
|
||||
|
||||
final episode = (await api.usersUserIdItemsItemIdGet(itemId: item.id)).bodyOrThrow as EpisodeModel;
|
||||
|
||||
state = state.copyWith(
|
||||
series: seriesResponse.bodyOrThrow as SeriesModel,
|
||||
episodes: EpisodeModel.episodesFromDto(episodes.bodyOrThrow.items, ref),
|
||||
episode: episode,
|
||||
);
|
||||
|
||||
return seriesResponse;
|
||||
} catch (e) {
|
||||
_tryToCreateOfflineState(item);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _tryToCreateOfflineState(ItemBaseModel item) {
|
||||
final syncNotifier = ref.read(syncProvider.notifier);
|
||||
final syncedItem = syncNotifier.getParentItem(item.id);
|
||||
if (syncedItem == null) return;
|
||||
final seriesModel = syncedItem.createItemModel(ref) as SeriesModel;
|
||||
final episodes = ref
|
||||
.read(syncProvider.notifier)
|
||||
.getChildren(syncedItem)
|
||||
.map(
|
||||
(e) => e.createItemModel(ref) as EpisodeModel,
|
||||
)
|
||||
.whereNotNull()
|
||||
.toList();
|
||||
state = state.copyWith(
|
||||
series: seriesModel,
|
||||
episode: episodes.firstWhereOrNull((element) => element.id == item.id),
|
||||
episodes: episodes,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
void setSubIndex(int index) {
|
||||
state = state.copyWith(
|
||||
episode: state.episode?.copyWith(
|
||||
mediaStreams: state.episode?.mediaStreams.copyWith(
|
||||
defaultSubStreamIndex: index,
|
||||
)));
|
||||
}
|
||||
|
||||
void setAudioIndex(int index) {
|
||||
state = state.copyWith(
|
||||
episode: state.episode?.copyWith(
|
||||
mediaStreams: state.episode?.mediaStreams.copyWith(
|
||||
defaultAudioStreamIndex: index,
|
||||
)));
|
||||
}
|
||||
}
|
||||
42
lib/providers/items/folder_details_provider.dart
Normal file
42
lib/providers/items/folder_details_provider.dart
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import 'package:chopper/chopper.dart';
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/items/folder_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/service_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final folderDetailsProvider =
|
||||
StateNotifierProvider.autoDispose.family<FolderDetailsNotifier, FolderModel?, String>((ref, id) {
|
||||
return FolderDetailsNotifier(ref);
|
||||
});
|
||||
|
||||
class FolderDetailsNotifier extends StateNotifier<FolderModel?> {
|
||||
FolderDetailsNotifier(this.ref) : super(null);
|
||||
|
||||
final Ref ref;
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
Future<Response?> fetchDetails(String id) async {
|
||||
if (state == null) {
|
||||
final folderItem = await api.usersUserIdItemsItemIdGet(itemId: id);
|
||||
|
||||
if (folderItem.body != null) {
|
||||
state = folderItem.bodyOrThrow as FolderModel;
|
||||
}
|
||||
}
|
||||
|
||||
final response = await api.itemsGet(
|
||||
parentId: id,
|
||||
sortBy: [ItemSortBy.sortname, ItemSortBy.name],
|
||||
sortOrder: [SortOrder.ascending],
|
||||
fields: [
|
||||
ItemFields.primaryimageaspectratio,
|
||||
ItemFields.childcount,
|
||||
],
|
||||
);
|
||||
|
||||
state = state?.copyWith(items: response.body?.items.where((element) => element.childCount != 0).toList());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
132
lib/providers/items/identify_provider.dart
Normal file
132
lib/providers/items/identify_provider.dart
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import 'package:chopper/chopper.dart';
|
||||
import 'package:fladder/models/items/movie_model.dart';
|
||||
import 'package:fladder/models/items/series_model.dart';
|
||||
import 'package:fladder/providers/service_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/item_base_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
|
||||
class IdentifyModel {
|
||||
final ItemBaseModel? item;
|
||||
final String searchString;
|
||||
final List<ExternalIdInfo> externalIds;
|
||||
final Map<String, String> keys;
|
||||
final List<RemoteSearchResult> results;
|
||||
final int? year;
|
||||
final bool replaceAllImages;
|
||||
final bool processing;
|
||||
IdentifyModel({
|
||||
this.item,
|
||||
this.searchString = "",
|
||||
this.externalIds = const [],
|
||||
this.keys = const {},
|
||||
this.results = const [],
|
||||
this.year,
|
||||
this.replaceAllImages = true,
|
||||
this.processing = false,
|
||||
});
|
||||
|
||||
Map<String, dynamic> get body => {
|
||||
"SearchInfo": {
|
||||
"ProviderIds": keys,
|
||||
"Name": searchString,
|
||||
"Year": year,
|
||||
},
|
||||
"ItemId": item?.id,
|
||||
}..removeWhere((key, value) => value == null);
|
||||
|
||||
IdentifyModel copyWith({
|
||||
ValueGetter<ItemBaseModel?>? item,
|
||||
String? searchString,
|
||||
List<ExternalIdInfo>? externalIds,
|
||||
Map<String, String>? keys,
|
||||
List<RemoteSearchResult>? results,
|
||||
ValueGetter<int?>? year,
|
||||
bool? replaceAllImages,
|
||||
bool? processing,
|
||||
}) {
|
||||
return IdentifyModel(
|
||||
item: item != null ? item() : this.item,
|
||||
searchString: searchString ?? this.searchString,
|
||||
externalIds: externalIds ?? this.externalIds,
|
||||
keys: keys ?? this.keys,
|
||||
results: results ?? this.results,
|
||||
year: year != null ? year() : this.year,
|
||||
replaceAllImages: replaceAllImages ?? this.replaceAllImages,
|
||||
processing: processing ?? this.processing,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final simpleProviderProvider = StateProvider<String>((ref) {
|
||||
return "";
|
||||
});
|
||||
|
||||
final identifyProvider = StateNotifierProvider.autoDispose.family<IdentifyNotifier, IdentifyModel, String>((ref, id) {
|
||||
return IdentifyNotifier(ref, id);
|
||||
});
|
||||
|
||||
class IdentifyNotifier extends StateNotifier<IdentifyModel> {
|
||||
IdentifyNotifier(this.ref, this.id) : super(IdentifyModel());
|
||||
|
||||
final String id;
|
||||
final Ref ref;
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
Future<void> fetchInformation() async {
|
||||
state = state.copyWith(processing: true);
|
||||
final item = await api.usersUserIdItemsItemIdGet(itemId: id);
|
||||
final itemModel = item.bodyOrThrow;
|
||||
final response = await api.itemsItemIdExternalIdInfosGet(itemId: id);
|
||||
state = state.copyWith(
|
||||
item: () => itemModel,
|
||||
externalIds: response.body,
|
||||
searchString: itemModel.name,
|
||||
year: () => itemModel.overview.yearAired,
|
||||
keys: {for (var element in response.body ?? []) (element as ExternalIdInfo).key ?? "": ""},
|
||||
);
|
||||
state = state.copyWith(processing: false);
|
||||
}
|
||||
|
||||
IdentifyModel update(IdentifyModel Function(IdentifyModel state) cb) => state = cb(state);
|
||||
|
||||
void clearFields() {
|
||||
state = state.copyWith(
|
||||
searchString: "",
|
||||
year: () => null,
|
||||
keys: state.keys..updateAll((key, value) => ""),
|
||||
);
|
||||
}
|
||||
|
||||
void updateKey(MapEntry<String, String> map) {
|
||||
state = state.copyWith(keys: state.keys..update(map.key, (value) => map.value));
|
||||
}
|
||||
|
||||
Future<Response<List<RemoteSearchResult>>?> remoteSearch() async {
|
||||
if (state.item == null) return null;
|
||||
state = state.copyWith(processing: true);
|
||||
late Response<List<RemoteSearchResult>> response;
|
||||
switch (state.item) {
|
||||
case SeriesModel _:
|
||||
response = await api.itemsRemoteSearchSeriesPost(body: SeriesInfoRemoteSearchQuery.fromJson(state.body));
|
||||
case MovieModel _:
|
||||
default:
|
||||
response = await api.itemsRemoteSearchMoviePost(body: MovieInfoRemoteSearchQuery.fromJson(state.body));
|
||||
}
|
||||
state = state.copyWith(results: response.body, processing: false);
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<Response<dynamic>?> setIdentity(RemoteSearchResult result) async {
|
||||
if (state.item == null) return null;
|
||||
state = state.copyWith(processing: true);
|
||||
final response = await api.itemsRemoteSearchApplyItemIdPost(
|
||||
itemId: state.item?.id ?? "", body: RemoteSearchResult.fromJson(result.toJson()));
|
||||
state = state.copyWith(processing: false);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
47
lib/providers/items/information_provider.dart
Normal file
47
lib/providers/items/information_provider.dart
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import 'package:chopper/chopper.dart';
|
||||
import 'package:fladder/providers/service_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/information_model.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
|
||||
class InformationProviderModel {
|
||||
final InformationModel? model;
|
||||
final bool loading;
|
||||
InformationProviderModel({
|
||||
this.model,
|
||||
this.loading = false,
|
||||
});
|
||||
|
||||
InformationProviderModel copyWith({
|
||||
InformationModel? model,
|
||||
bool? loading,
|
||||
}) {
|
||||
return InformationProviderModel(
|
||||
model: model ?? this.model,
|
||||
loading: loading ?? this.loading,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final informationProvider =
|
||||
StateNotifierProvider.autoDispose.family<InformationNotifier, InformationProviderModel, String>((ref, id) {
|
||||
return InformationNotifier(ref);
|
||||
});
|
||||
|
||||
class InformationNotifier extends StateNotifier<InformationProviderModel> {
|
||||
InformationNotifier(this.ref) : super(InformationProviderModel());
|
||||
|
||||
final Ref ref;
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
Future<Response> getItemInformation(ItemBaseModel item) async {
|
||||
state = state.copyWith(loading: true);
|
||||
final response = await api.usersUserIdItemsItemIdGetBaseItem(itemId: item.id);
|
||||
await Future.delayed(const Duration(milliseconds: 250));
|
||||
state = state.copyWith(loading: false, model: InformationModel.fromResponse(response.body));
|
||||
return response;
|
||||
}
|
||||
}
|
||||
22
lib/providers/items/item_details_provider.dart
Normal file
22
lib/providers/items/item_details_provider.dart
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/service_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final itemDetailsProvider = StateNotifierProvider.autoDispose<ItemDetailsNotifier, ItemBaseModel?>((ref) {
|
||||
return ItemDetailsNotifier(ref);
|
||||
});
|
||||
|
||||
class ItemDetailsNotifier extends StateNotifier<ItemBaseModel?> {
|
||||
ItemDetailsNotifier(this.ref) : super(null);
|
||||
|
||||
final Ref ref;
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
Future<ItemBaseModel?> fetchDetails(String itemId) async {
|
||||
final response = await api.usersUserIdItemsItemIdGet(itemId: itemId);
|
||||
if (response.body == null) return null;
|
||||
return response.bodyOrThrow;
|
||||
}
|
||||
}
|
||||
51
lib/providers/items/movies_details_provider.dart
Normal file
51
lib/providers/items/movies_details_provider.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import 'package:chopper/chopper.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/items/movie_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/related_provider.dart';
|
||||
import 'package:fladder/providers/service_provider.dart';
|
||||
import 'package:fladder/providers/sync_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'movies_details_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
class MovieDetails extends _$MovieDetails {
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
@override
|
||||
MovieModel? build(String arg) => null;
|
||||
|
||||
Future<Response?> fetchDetails(ItemBaseModel item) async {
|
||||
try {
|
||||
if (item is MovieModel && state == null) {
|
||||
state = item;
|
||||
}
|
||||
final response = await api.usersUserIdItemsItemIdGet(itemId: item.id);
|
||||
if (response.body == null) return null;
|
||||
state = response.bodyOrThrow as MovieModel;
|
||||
final related = await ref.read(relatedUtilityProvider).relatedContent(item.id);
|
||||
state = state?.copyWith(related: related.body);
|
||||
return null;
|
||||
} catch (e) {
|
||||
_tryToCreateOfflineState(item);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _tryToCreateOfflineState(ItemBaseModel item) {
|
||||
final syncNotifier = ref.read(syncProvider.notifier);
|
||||
final syncedItem = syncNotifier.getParentItem(item.id);
|
||||
if (syncedItem == null) return;
|
||||
final movieModel = syncedItem.createItemModel(ref) as MovieModel;
|
||||
state = movieModel;
|
||||
}
|
||||
|
||||
void setSubIndex(int index) {
|
||||
state = state?.copyWith(mediaStreams: state?.mediaStreams.copyWith(defaultSubStreamIndex: index));
|
||||
}
|
||||
|
||||
void setAudioIndex(int index) {
|
||||
state = state?.copyWith(mediaStreams: state?.mediaStreams.copyWith(defaultAudioStreamIndex: index));
|
||||
}
|
||||
}
|
||||
174
lib/providers/items/movies_details_provider.g.dart
Normal file
174
lib/providers/items/movies_details_provider.g.dart
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'movies_details_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$movieDetailsHash() => r'e5ab0af7fab9eb7a8ea50a873e8875bb572bd240';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$MovieDetails
|
||||
extends BuildlessAutoDisposeNotifier<MovieModel?> {
|
||||
late final String arg;
|
||||
|
||||
MovieModel? build(
|
||||
String arg,
|
||||
);
|
||||
}
|
||||
|
||||
/// See also [MovieDetails].
|
||||
@ProviderFor(MovieDetails)
|
||||
const movieDetailsProvider = MovieDetailsFamily();
|
||||
|
||||
/// See also [MovieDetails].
|
||||
class MovieDetailsFamily extends Family<MovieModel?> {
|
||||
/// See also [MovieDetails].
|
||||
const MovieDetailsFamily();
|
||||
|
||||
/// See also [MovieDetails].
|
||||
MovieDetailsProvider call(
|
||||
String arg,
|
||||
) {
|
||||
return MovieDetailsProvider(
|
||||
arg,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
MovieDetailsProvider getProviderOverride(
|
||||
covariant MovieDetailsProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.arg,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'movieDetailsProvider';
|
||||
}
|
||||
|
||||
/// See also [MovieDetails].
|
||||
class MovieDetailsProvider
|
||||
extends AutoDisposeNotifierProviderImpl<MovieDetails, MovieModel?> {
|
||||
/// See also [MovieDetails].
|
||||
MovieDetailsProvider(
|
||||
String arg,
|
||||
) : this._internal(
|
||||
() => MovieDetails()..arg = arg,
|
||||
from: movieDetailsProvider,
|
||||
name: r'movieDetailsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$movieDetailsHash,
|
||||
dependencies: MovieDetailsFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
MovieDetailsFamily._allTransitiveDependencies,
|
||||
arg: arg,
|
||||
);
|
||||
|
||||
MovieDetailsProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.arg,
|
||||
}) : super.internal();
|
||||
|
||||
final String arg;
|
||||
|
||||
@override
|
||||
MovieModel? runNotifierBuild(
|
||||
covariant MovieDetails notifier,
|
||||
) {
|
||||
return notifier.build(
|
||||
arg,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(MovieDetails Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: MovieDetailsProvider._internal(
|
||||
() => create()..arg = arg,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
arg: arg,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeNotifierProviderElement<MovieDetails, MovieModel?>
|
||||
createElement() {
|
||||
return _MovieDetailsProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is MovieDetailsProvider && other.arg == arg;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, arg.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin MovieDetailsRef on AutoDisposeNotifierProviderRef<MovieModel?> {
|
||||
/// The parameter `arg` of this provider.
|
||||
String get arg;
|
||||
}
|
||||
|
||||
class _MovieDetailsProviderElement
|
||||
extends AutoDisposeNotifierProviderElement<MovieDetails, MovieModel?>
|
||||
with MovieDetailsRef {
|
||||
_MovieDetailsProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get arg => (origin as MovieDetailsProvider).arg;
|
||||
}
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
69
lib/providers/items/person_details_provider.dart
Normal file
69
lib/providers/items/person_details_provider.dart
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import 'package:chopper/chopper.dart';
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/items/item_shared_models.dart';
|
||||
import 'package:fladder/models/items/movie_model.dart';
|
||||
import 'package:fladder/models/items/person_model.dart';
|
||||
import 'package:fladder/models/items/series_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/service_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final personDetailsProvider =
|
||||
StateNotifierProvider.autoDispose.family<PersonDetailsNotifier, PersonModel?, String>((ref, id) {
|
||||
return PersonDetailsNotifier(ref);
|
||||
});
|
||||
|
||||
class PersonDetailsNotifier extends StateNotifier<PersonModel?> {
|
||||
PersonDetailsNotifier(this.ref) : super(null);
|
||||
|
||||
final Ref ref;
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
Future<Response?> fetchPerson(Person person) async {
|
||||
final response = await api.usersUserIdItemsItemIdGet(itemId: person.id);
|
||||
|
||||
if (response.isSuccessful && response.body != null) {
|
||||
state = response.bodyOrThrow as PersonModel;
|
||||
fetchMovies();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<Response?> fetchMovies() async {
|
||||
final movies = await api.itemsGet(
|
||||
personIds: [state?.id ?? ""],
|
||||
limit: 25,
|
||||
sortBy: [ItemSortBy.premieredate, ItemSortBy.communityrating, ItemSortBy.sortname, ItemSortBy.productionyear],
|
||||
sortOrder: [SortOrder.descending],
|
||||
recursive: true,
|
||||
fields: [
|
||||
ItemFields.primaryimageaspectratio,
|
||||
],
|
||||
includeItemTypes: [
|
||||
BaseItemKind.movie,
|
||||
],
|
||||
);
|
||||
|
||||
final series = await api.itemsGet(
|
||||
personIds: [state?.id ?? ""],
|
||||
limit: 25,
|
||||
sortBy: [ItemSortBy.premieredate, ItemSortBy.communityrating, ItemSortBy.sortname, ItemSortBy.productionyear],
|
||||
sortOrder: [SortOrder.descending],
|
||||
recursive: true,
|
||||
fields: [
|
||||
ItemFields.primaryimageaspectratio,
|
||||
],
|
||||
includeItemTypes: [
|
||||
BaseItemKind.series,
|
||||
],
|
||||
);
|
||||
|
||||
state = state?.copyWith(
|
||||
movies: movies.body?.items.whereType<MovieModel>().toList(),
|
||||
series: series.body?.items.whereType<SeriesModel>().toList(),
|
||||
);
|
||||
return movies;
|
||||
}
|
||||
}
|
||||
49
lib/providers/items/photo_details_provider.dart
Normal file
49
lib/providers/items/photo_details_provider.dart
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import 'package:chopper/chopper.dart';
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/items/photos_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/service_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final photoDetailsProvider =
|
||||
StateNotifierProvider.autoDispose.family<PhotoDetailsNotifier, PhotoAlbumModel?, String>((ref, id) {
|
||||
return PhotoDetailsNotifier(ref);
|
||||
});
|
||||
|
||||
class PhotoDetailsNotifier extends StateNotifier<PhotoAlbumModel?> {
|
||||
PhotoDetailsNotifier(this.ref) : super(null);
|
||||
|
||||
final Ref ref;
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
Future<Response?> fetchDetails(ItemBaseModel item) async {
|
||||
String? albumId;
|
||||
if (item is PhotoModel) {
|
||||
albumId = item.albumId;
|
||||
} else if (item is PhotoAlbumModel) {
|
||||
albumId = item.id;
|
||||
}
|
||||
|
||||
final albumData = await api.usersUserIdItemsItemIdGet(itemId: albumId);
|
||||
if (albumData.body == null) return albumData;
|
||||
PhotoAlbumModel newState = albumData.bodyOrThrow as PhotoAlbumModel;
|
||||
final response = await api.itemsGet(
|
||||
parentId: albumId,
|
||||
fields: [ItemFields.primaryimageaspectratio],
|
||||
sortBy: [ItemSortBy.sortname],
|
||||
includeItemTypes: [
|
||||
BaseItemKind.folder,
|
||||
BaseItemKind.photoalbum,
|
||||
BaseItemKind.photo,
|
||||
BaseItemKind.video,
|
||||
],
|
||||
sortOrder: [SortOrder.ascending],
|
||||
);
|
||||
if (response.body == null) return null;
|
||||
newState = newState.copyWith(photos: response.body?.items.toList());
|
||||
state = newState;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
33
lib/providers/items/season_details_provider.dart
Normal file
33
lib/providers/items/season_details_provider.dart
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import 'package:chopper/chopper.dart';
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/items/episode_model.dart';
|
||||
import 'package:fladder/models/items/season_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/service_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final seasonDetailsProvider =
|
||||
StateNotifierProvider.autoDispose.family<SeasonDetailsNotifier, SeasonModel?, String>((ref, id) {
|
||||
return SeasonDetailsNotifier(ref);
|
||||
});
|
||||
|
||||
class SeasonDetailsNotifier extends StateNotifier<SeasonModel?> {
|
||||
SeasonDetailsNotifier(this.ref) : super(null);
|
||||
|
||||
final Ref ref;
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
Future<Response?> fetchDetails(String seasonId) async {
|
||||
SeasonModel? newState;
|
||||
|
||||
final season = await api.usersUserIdItemsItemIdGet(itemId: seasonId);
|
||||
if (season.body != null) newState = season.bodyOrThrow as SeasonModel;
|
||||
|
||||
final episodes = await api.showsSeriesIdEpisodesGet(
|
||||
seriesId: newState?.seriesId ?? "", seasonId: seasonId, fields: [ItemFields.overview]);
|
||||
newState = newState?.copyWith(episodes: EpisodeModel.episodesFromDto(episodes.body?.items, ref));
|
||||
state = newState;
|
||||
return season;
|
||||
}
|
||||
}
|
||||
95
lib/providers/items/series_details_provider.dart
Normal file
95
lib/providers/items/series_details_provider.dart
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import 'package:chopper/chopper.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/providers/service_provider.dart';
|
||||
import 'package:fladder/providers/sync_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/items/episode_model.dart';
|
||||
import 'package:fladder/models/items/season_model.dart';
|
||||
import 'package:fladder/models/items/series_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/related_provider.dart';
|
||||
|
||||
final seriesDetailsProvider =
|
||||
StateNotifierProvider.autoDispose.family<SeriesDetailViewNotifier, SeriesModel?, String>((ref, id) {
|
||||
return SeriesDetailViewNotifier(ref);
|
||||
});
|
||||
|
||||
class SeriesDetailViewNotifier extends StateNotifier<SeriesModel?> {
|
||||
SeriesDetailViewNotifier(this.ref) : super(null);
|
||||
|
||||
final Ref ref;
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
Future<Response?> fetchDetails(ItemBaseModel seriesModel) async {
|
||||
try {
|
||||
if (seriesModel is SeriesModel) {
|
||||
state = seriesModel;
|
||||
}
|
||||
SeriesModel? newState;
|
||||
final response = await api.usersUserIdItemsItemIdGet(itemId: seriesModel.id);
|
||||
if (response.body == null) {
|
||||
state = seriesModel as SeriesModel;
|
||||
return null;
|
||||
}
|
||||
newState = response.bodyOrThrow as SeriesModel;
|
||||
|
||||
final seasons = await api.showsSeriesIdSeasonsGet(seriesId: seriesModel.id);
|
||||
newState = newState.copyWith(seasons: SeasonModel.seasonsFromDto(seasons.body?.items, ref));
|
||||
|
||||
final episodes = await api.showsSeriesIdEpisodesGet(seriesId: seriesModel.id, fields: [
|
||||
ItemFields.mediastreams,
|
||||
ItemFields.mediasources,
|
||||
ItemFields.overview,
|
||||
]);
|
||||
|
||||
newState = newState.copyWith(
|
||||
availableEpisodes: EpisodeModel.episodesFromDto(
|
||||
episodes.body?.items,
|
||||
ref,
|
||||
),
|
||||
);
|
||||
|
||||
final related = await ref.read(relatedUtilityProvider).relatedContent(seriesModel.id);
|
||||
state = newState.copyWith(related: related.body);
|
||||
return response;
|
||||
} catch (e) {
|
||||
_tryToCreateOfflineState(seriesModel);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _tryToCreateOfflineState(ItemBaseModel series) async {
|
||||
final syncNotifier = ref.read(syncProvider.notifier);
|
||||
final syncedItem = syncNotifier.getSyncedItem(series);
|
||||
if (syncedItem == null) return;
|
||||
final seriesModel = syncedItem.createItemModel(ref) as SeriesModel;
|
||||
final allChildren = syncedItem
|
||||
.getNestedChildren(ref)
|
||||
.map(
|
||||
(e) => e.createItemModel(ref),
|
||||
)
|
||||
.whereNotNull()
|
||||
.toList();
|
||||
state = seriesModel.copyWith(
|
||||
availableEpisodes: allChildren.whereType<EpisodeModel>().toList(),
|
||||
seasons: allChildren.whereType<SeasonModel>().toList(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
void updateEpisodeInfo(EpisodeModel episode) {
|
||||
final index = state?.availableEpisodes?.indexOf(episode);
|
||||
|
||||
if (index != null) {
|
||||
state = state?.copyWith(
|
||||
availableEpisodes: state?.availableEpisodes
|
||||
?..remove(episode)
|
||||
..insert(index, episode),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
174
lib/providers/library_provider.dart
Normal file
174
lib/providers/library_provider.dart
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import 'package:chopper/chopper.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/items/photos_model.dart';
|
||||
import 'package:fladder/models/library_model.dart';
|
||||
import 'package:fladder/models/recommended_model.dart';
|
||||
import 'package:fladder/models/view_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/service_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
|
||||
bool _useFolders(ViewModel model) {
|
||||
switch (model.collectionType) {
|
||||
case CollectionType.boxsets:
|
||||
case CollectionType.homevideos:
|
||||
case CollectionType.folders:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final libraryProvider = StateNotifierProvider.autoDispose.family<LibraryNotifier, LibraryModel?, String>((ref, id) {
|
||||
return LibraryNotifier(ref);
|
||||
});
|
||||
|
||||
class LibraryNotifier extends StateNotifier<LibraryModel?> {
|
||||
LibraryNotifier(this.ref) : super(null);
|
||||
|
||||
final Ref ref;
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
set loading(bool value) {
|
||||
state = state?.copyWith(loading: value);
|
||||
}
|
||||
|
||||
bool get loading => state?.loading ?? true;
|
||||
|
||||
Future<void> setupLibrary(ViewModel viewModel) async {
|
||||
state ??= LibraryModel(id: viewModel.id, name: viewModel.name, loading: true, type: BaseItemKind.movie);
|
||||
}
|
||||
|
||||
Future<Response?> loadLibrary(ViewModel viewModel) async {
|
||||
final response = await api.itemsGet(
|
||||
parentId: viewModel.id,
|
||||
sortBy: [ItemSortBy.sortname, ItemSortBy.productionyear],
|
||||
isMissing: false,
|
||||
excludeItemTypes: !_useFolders(viewModel) ? [BaseItemKind.folder] : [],
|
||||
fields: [ItemFields.genres, ItemFields.childcount, ItemFields.parentid],
|
||||
);
|
||||
state = state?.copyWith(posters: response.body?.items);
|
||||
loading = false;
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<void> loadRecommendations(ViewModel viewModel) async {
|
||||
loading = true;
|
||||
//Clear recommendations because of all the copying
|
||||
state = state?.copyWith(recommendations: []);
|
||||
final latest = await api.usersUserIdItemsLatestGet(
|
||||
parentId: viewModel.id,
|
||||
limit: 14,
|
||||
isPlayed: false,
|
||||
imageTypeLimit: 1,
|
||||
includeItemTypes: viewModel.collectionType == CollectionType.tvshows ? [BaseItemKind.episode] : null,
|
||||
);
|
||||
state = state?.copyWith(
|
||||
recommendations: [
|
||||
...?state?.recommendations,
|
||||
RecommendedModel(
|
||||
name: "Latest",
|
||||
posters: latest.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [],
|
||||
type: "Latest",
|
||||
),
|
||||
],
|
||||
);
|
||||
if (viewModel.collectionType == CollectionType.movies) {
|
||||
final response = await api.moviesRecommendationsGet(
|
||||
parentId: viewModel.id,
|
||||
categoryLimit: 6,
|
||||
itemLimit: 8,
|
||||
fields: [ItemFields.mediasourcecount],
|
||||
);
|
||||
state = state?.copyWith(recommendations: [
|
||||
...?state?.recommendations,
|
||||
...response.body?.map(
|
||||
(e) => RecommendedModel(
|
||||
name: e.baselineItemName ?? "",
|
||||
posters: e.items?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [],
|
||||
type: e.recommendationType.toString(),
|
||||
),
|
||||
) ??
|
||||
[],
|
||||
]);
|
||||
loading = false;
|
||||
} else {
|
||||
final nextUp = await api.showsNextUpGet(
|
||||
parentId: viewModel.id,
|
||||
limit: 14,
|
||||
imageTypeLimit: 1,
|
||||
fields: [ItemFields.mediasourcecount, ItemFields.primaryimageaspectratio],
|
||||
);
|
||||
state = state?.copyWith(recommendations: [
|
||||
...?state?.recommendations,
|
||||
...[
|
||||
RecommendedModel(
|
||||
name: "Next up",
|
||||
posters: nextUp.body?.items
|
||||
?.map(
|
||||
(e) => ItemBaseModel.fromBaseDto(
|
||||
e,
|
||||
ref,
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
[],
|
||||
type: "Latest series")
|
||||
],
|
||||
]);
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Response?> loadFavourites(ViewModel viewModel) async {
|
||||
loading = true;
|
||||
final response = await api.itemsGet(
|
||||
parentId: viewModel.id,
|
||||
isFavorite: true,
|
||||
recursive: true,
|
||||
);
|
||||
|
||||
state = state?.copyWith(favourites: response.body?.items);
|
||||
loading = false;
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<Response?> loadTimeline(ViewModel viewModel) async {
|
||||
loading = true;
|
||||
final response = await api.itemsGet(
|
||||
parentId: viewModel.id,
|
||||
recursive: true,
|
||||
fields: [ItemFields.primaryimageaspectratio, ItemFields.datecreated],
|
||||
sortBy: [ItemSortBy.datecreated],
|
||||
sortOrder: [SortOrder.descending],
|
||||
includeItemTypes: [
|
||||
BaseItemKind.photo,
|
||||
BaseItemKind.video,
|
||||
],
|
||||
);
|
||||
state = state?.copyWith(
|
||||
timelinePhotos: response.body?.items.map((e) => e as PhotoModel).toList(),
|
||||
);
|
||||
loading = false;
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<Response?> loadGenres(ViewModel viewModel) async {
|
||||
final genres = await api.genresGet(
|
||||
sortBy: [ItemSortBy.sortname],
|
||||
sortOrder: [SortOrder.ascending],
|
||||
includeItemTypes: viewModel.collectionType == CollectionType.movies
|
||||
? [BaseItemKind.movie]
|
||||
: [
|
||||
BaseItemKind.series,
|
||||
],
|
||||
parentId: viewModel.id,
|
||||
);
|
||||
state = state?.copyWith(
|
||||
genres: genres.body?.items?.where((element) => element.name?.isNotEmpty ?? false).map((e) => e.name!).toList());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
670
lib/providers/library_search_provider.dart
Normal file
670
lib/providers/library_search_provider.dart
Normal file
|
|
@ -0,0 +1,670 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:chopper/chopper.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fladder/models/collection_types.dart';
|
||||
import 'package:fladder/models/items/folder_model.dart';
|
||||
import 'package:fladder/models/items/item_shared_models.dart';
|
||||
import 'package:fladder/models/items/photos_model.dart';
|
||||
import 'package:fladder/models/library_search/library_search_options.dart';
|
||||
import 'package:fladder/models/playlist_model.dart';
|
||||
import 'package:fladder/providers/service_provider.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart';
|
||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/util/item_base_model/play_item_helpers.dart';
|
||||
import 'package:fladder/util/list_extensions.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';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/library_search/library_search_model.dart';
|
||||
import 'package:fladder/models/view_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/util/map_bool_helper.dart';
|
||||
import 'package:page_transition/page_transition.dart';
|
||||
|
||||
final librarySearchProvider =
|
||||
StateNotifierProvider.family.autoDispose<LibrarySearchNotifier, LibrarySearchModel, Key>((ref, id) {
|
||||
return LibrarySearchNotifier(ref);
|
||||
});
|
||||
|
||||
class LibrarySearchNotifier extends StateNotifier<LibrarySearchModel> {
|
||||
LibrarySearchNotifier(this.ref) : super(LibrarySearchModel());
|
||||
|
||||
final Ref ref;
|
||||
|
||||
int get pageSize => ref.read(clientSettingsProvider).libraryPageSize ?? 500;
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
set loading(bool loading) => state = state.copyWith(loading: loading);
|
||||
|
||||
bool loadedFilters = false;
|
||||
|
||||
bool get loading => state.loading;
|
||||
|
||||
Future<void> initRefresh(
|
||||
List<String>? folderId,
|
||||
String? viewModelId,
|
||||
bool? favourites,
|
||||
) async {
|
||||
loading = true;
|
||||
state = state.resetLazyLoad();
|
||||
if (state.views.isEmpty && state.folderOverwrite.isEmpty) {
|
||||
if (folderId != null) {
|
||||
await loadFolders(folderId: folderId);
|
||||
} else {
|
||||
await loadViews(viewModelId, favourites);
|
||||
}
|
||||
}
|
||||
|
||||
await loadFilters();
|
||||
await loadMore(init: true);
|
||||
loading = false;
|
||||
}
|
||||
|
||||
Future<void> loadMore({bool? init}) async {
|
||||
if ((loading && init != true) || state.allDoneFetching) return;
|
||||
loading = true;
|
||||
|
||||
final newLastIndices = Map<String, int>.from(state.lastIndices);
|
||||
final newLibraryItemCounts = Map<String, int>.from(state.libraryItemCounts);
|
||||
final isEmpty = newLastIndices.isEmpty;
|
||||
|
||||
Future<void> handleItemLoading(String itemId, ItemBaseModel currentModel) async {
|
||||
final lastIndices = newLastIndices[itemId];
|
||||
final libraryTotalCount = newLibraryItemCounts[itemId];
|
||||
if (libraryTotalCount != null && lastIndices != null && libraryTotalCount <= lastIndices) return;
|
||||
|
||||
final result = currentModel is PlaylistModel
|
||||
? await _loadPlaylistItems(id: itemId, startIndex: lastIndices, limit: pageSize)
|
||||
: await _loadLibrary(id: itemId, startIndex: lastIndices, limit: pageSize);
|
||||
|
||||
if (result != null) {
|
||||
newLibraryItemCounts[itemId] = result.totalRecordCount ?? 0;
|
||||
newLastIndices[itemId] = (lastIndices ?? 0) + result.items.length;
|
||||
state = state.copyWith(
|
||||
posters: isEmpty ? result.items : [...state.posters, ...result.items],
|
||||
lastIndices: newLastIndices,
|
||||
libraryItemCounts: newLibraryItemCounts,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleViewLoading() async {
|
||||
final results = await Future.wait(
|
||||
state.views.included.map((viewModel) async {
|
||||
final lastIndices = newLastIndices[viewModel.id];
|
||||
final libraryTotalCount = newLibraryItemCounts[viewModel.id];
|
||||
if (libraryTotalCount != null && lastIndices != null && libraryTotalCount <= lastIndices) return null;
|
||||
|
||||
final libraryItems = await _loadLibrary(
|
||||
viewModel: viewModel,
|
||||
startIndex: lastIndices,
|
||||
limit: pageSize ~/ state.views.included.length,
|
||||
);
|
||||
|
||||
if (libraryItems != null) {
|
||||
newLibraryItemCounts[viewModel.id] = libraryItems.totalRecordCount ?? 0;
|
||||
newLastIndices[viewModel.id] = (lastIndices ?? 0) + libraryItems.items.length;
|
||||
}
|
||||
return libraryItems;
|
||||
}).whereNotNull(),
|
||||
);
|
||||
|
||||
List<ItemBaseModel> newPosters = results.whereNotNull().expand((element) => element.items).toList();
|
||||
if (state.views.included.length > 1) {
|
||||
if (state.sortingOption == SortingOptions.random) {
|
||||
newPosters = newPosters.random();
|
||||
} else {
|
||||
newPosters = newPosters.sorted(
|
||||
(a, b) => sortItems(a, b, state.sortingOption, state.sortOrder),
|
||||
);
|
||||
}
|
||||
}
|
||||
state = state.copyWith(
|
||||
posters: isEmpty ? newPosters : [...state.posters, ...newPosters],
|
||||
lastIndices: newLastIndices,
|
||||
libraryItemCounts: newLibraryItemCounts,
|
||||
);
|
||||
}
|
||||
|
||||
if (state.folderOverwrite.isNotEmpty) {
|
||||
await handleItemLoading(state.folderOverwrite.last.id, state.folderOverwrite.last);
|
||||
} else if (state.views.hasEnabled) {
|
||||
await handleViewLoading();
|
||||
} else {
|
||||
if (state.searchQuery.isEmpty && !state.favourites) {
|
||||
state = state.copyWith(posters: []);
|
||||
} else {
|
||||
final response = await _loadLibrary(recursive: true);
|
||||
state = state.copyWith(posters: response?.items ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
|
||||
//Pas viewmodel otherwise select first
|
||||
Future<void> loadViews(String? viewModelId, bool? favourites) async {
|
||||
final response = await api.usersUserIdViewsGet(includeHidden: false);
|
||||
final createdViews = response.body?.items?.map((e) => ViewModel.fromBodyDto(e, ref));
|
||||
Map<ViewModel, bool> mappedModels =
|
||||
createdViews?.isNotEmpty ?? false ? {for (var element in createdViews!) element: false} : {};
|
||||
|
||||
final selectedModel = mappedModels.keys.firstWhereOrNull((element) => element.id == viewModelId);
|
||||
|
||||
state = state.copyWith(
|
||||
views: selectedModel != null
|
||||
? mappedModels.setKey(mappedModels.keys.firstWhere((element) => element.id == viewModelId), true)
|
||||
: mappedModels,
|
||||
favourites: favourites,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> loadFolders({List<String>? folderId}) async {
|
||||
final response = await api.itemsGet(
|
||||
ids: folderId ?? state.folderOverwrite.map((e) => e.id).toList(),
|
||||
sortBy: state.sortingOption.toSortBy,
|
||||
sortOrder: [state.sortOrder.sortOrder],
|
||||
fields: [
|
||||
ItemFields.parentid,
|
||||
ItemFields.primaryimageaspectratio,
|
||||
],
|
||||
);
|
||||
|
||||
state = state.copyWith(folderOverwrite: response.body?.items.toList());
|
||||
}
|
||||
|
||||
Future<void> loadFilters() async {
|
||||
if (loadedFilters == true) return;
|
||||
loadedFilters = true;
|
||||
final enabledCollections = state.views.included.map((e) => e.collectionType.itemKinds).expand((element) => element);
|
||||
final mappedList = await Future.wait(state.views.included.map((viewModel) => _loadFilters(viewModel)));
|
||||
final studios = (await Future.wait(state.views.included.map((viewModel) => _loadStudios(viewModel))))
|
||||
.expand((element) => element)
|
||||
.toSet()
|
||||
.toList();
|
||||
var tempState = state.copyWith();
|
||||
final genres = mappedList
|
||||
.expand((element) => element?.genres ?? <NameGuidPair>[])
|
||||
.whereNotNull()
|
||||
.sorted((a, b) => a.name!.toLowerCase().compareTo(b.name!.toLowerCase()));
|
||||
final tags = mappedList
|
||||
.expand((element) => element?.tags ?? <String>[])
|
||||
.sorted((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
|
||||
tempState = tempState.copyWith(
|
||||
types: state.types.setAll(false).setKeys(enabledCollections, true),
|
||||
genres: {for (var element in genres) element.name!: false}.replaceMap(tempState.genres),
|
||||
studios: {for (var element in studios) element: false}.replaceMap(tempState.studios),
|
||||
tags: {for (var element in tags) element: false}.replaceMap(tempState.tags),
|
||||
);
|
||||
state = tempState;
|
||||
}
|
||||
|
||||
Future<QueryFilters?> _loadFilters(ViewModel viewModel) async {
|
||||
final response = await api.itemsFilters2Get(parentId: viewModel.id);
|
||||
return response.body;
|
||||
}
|
||||
|
||||
Future<List<Studio>> _loadStudios(ViewModel viewModel) async {
|
||||
final response = await api.studiosGet(parentId: viewModel.id);
|
||||
return response.body?.items?.map((e) => Studio(id: e.id ?? "", name: e.name ?? "")).toList() ?? [];
|
||||
}
|
||||
|
||||
Future<ServerQueryResult?> _loadLibrary(
|
||||
{ViewModel? viewModel,
|
||||
bool? recursive,
|
||||
bool? shuffle,
|
||||
String? id,
|
||||
int? limit,
|
||||
int? startIndex,
|
||||
String? searchTerm}) async {
|
||||
final searchString = searchTerm ?? (state.searchQuery.isNotEmpty ? state.searchQuery : null);
|
||||
final response = await api.itemsGet(
|
||||
parentId: viewModel?.id ?? id,
|
||||
searchTerm: searchString,
|
||||
genres: state.genres.included,
|
||||
tags: state.tags.included,
|
||||
recursive: searchString?.isNotEmpty == true ? true : recursive ?? state.recursive,
|
||||
officialRatings: state.officialRatings.included,
|
||||
years: state.years.included,
|
||||
isMissing: false,
|
||||
limit: (limit ?? 0) > 0 ? limit : null,
|
||||
startIndex: (limit ?? 0) > 0 ? startIndex : null,
|
||||
collapseBoxSetItems: false,
|
||||
studioIds: state.studios.included.map((e) => e.id).toList(),
|
||||
sortBy: shuffle == true ? [ItemSortBy.random] : state.sortingOption.toSortBy,
|
||||
sortOrder: [state.sortOrder.sortOrder],
|
||||
fields: {
|
||||
ItemFields.genres,
|
||||
ItemFields.parentid,
|
||||
ItemFields.tags,
|
||||
ItemFields.datecreated,
|
||||
ItemFields.datelastmediaadded,
|
||||
ItemFields.overview,
|
||||
ItemFields.originaltitle,
|
||||
ItemFields.customrating,
|
||||
ItemFields.primaryimageaspectratio,
|
||||
if (viewModel?.collectionType == CollectionType.tvshows) ItemFields.childcount,
|
||||
}.toList(),
|
||||
filters: [
|
||||
...state.filters.included,
|
||||
if (state.favourites) ItemFilter.isfavorite,
|
||||
],
|
||||
includeItemTypes: state.types.included.map((e) => e.dtoKind).toList(),
|
||||
);
|
||||
return response.body;
|
||||
}
|
||||
|
||||
Future<ServerQueryResult?> _loadPlaylistItems({ViewModel? viewModel, String? id, int? startIndex, int? limit}) async {
|
||||
final response = await api.playlistsPlaylistIdItemsGet(
|
||||
playlistId: viewModel?.id ?? id,
|
||||
limit: (limit ?? 0) > 0 ? limit : null,
|
||||
startIndex: (limit ?? 0) > 0 ? startIndex : null,
|
||||
fields: {
|
||||
ItemFields.genres,
|
||||
ItemFields.parentid,
|
||||
ItemFields.tags,
|
||||
ItemFields.datecreated,
|
||||
ItemFields.datelastmediaadded,
|
||||
ItemFields.overview,
|
||||
ItemFields.originaltitle,
|
||||
ItemFields.customrating,
|
||||
ItemFields.primaryimageaspectratio,
|
||||
if (viewModel?.collectionType == CollectionType.tvshows) ItemFields.childcount,
|
||||
}.toList(),
|
||||
);
|
||||
return response.body;
|
||||
}
|
||||
|
||||
Future<List<ItemBaseModel>> fetchSuggestions(String searchTerm) async {
|
||||
if (state.folderOverwrite.isNotEmpty) {
|
||||
final response = await _loadLibrary(id: state.nestedCurrentItem?.id ?? "", searchTerm: searchTerm, limit: 25);
|
||||
return response?.items ?? [];
|
||||
} else {
|
||||
if (state.views.hasEnabled) {
|
||||
final mappedList = await Future.wait(state.views.included
|
||||
.map((viewModel) => _loadLibrary(viewModel: viewModel, limit: 25, searchTerm: searchTerm)));
|
||||
return mappedList
|
||||
.expand((innerList) => innerList?.items ?? [])
|
||||
.where((item) => item != null)
|
||||
.cast<ItemBaseModel>()
|
||||
.toList();
|
||||
} else {
|
||||
if (searchTerm.isEmpty) {
|
||||
return [];
|
||||
} else {
|
||||
final response = await _loadLibrary(limit: 25, recursive: true, searchTerm: searchTerm);
|
||||
return response?.items ?? [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setSearch(String query) {
|
||||
state = state.copyWith(searchQuery: query);
|
||||
ref.read(userProvider.notifier).addSearchQuery(query);
|
||||
}
|
||||
|
||||
void toggleFavourite() => state = state.copyWith(favourites: !state.favourites);
|
||||
void toggleRecursive() => state = state.copyWith(recursive: !state.recursive);
|
||||
void toggleType(FladderItemType type) => state = state.copyWith(types: state.types.toggleKey(type));
|
||||
void toggleView(ViewModel view) => state = state.copyWith(views: state.views.toggleKey(view));
|
||||
void toggleGenre(String genre) => state = state.copyWith(genres: state.genres.toggleKey(genre));
|
||||
void toggleStudio(Studio studio) => state = state.copyWith(studios: state.studios.toggleKey(studio));
|
||||
void toggleTag(String tag) => state = state.copyWith(tags: state.tags.toggleKey(tag));
|
||||
void toggleRatings(String officialRatings) =>
|
||||
state = state.copyWith(officialRatings: state.officialRatings.toggleKey(officialRatings));
|
||||
void toggleYears(int year) => state = state.copyWith(years: state.years.toggleKey(year));
|
||||
void toggleFilters(ItemFilter filter) => state = state.copyWith(filters: state.filters.toggleKey(filter));
|
||||
|
||||
void setViews(Map<ViewModel, bool> views) {
|
||||
loadedFilters = false;
|
||||
state = state.copyWith(views: views).setFiltersToDefault();
|
||||
}
|
||||
|
||||
void setGenres(Map<String, bool> genres) => state = state.copyWith(genres: genres);
|
||||
void setStudios(Map<Studio, bool> studios) => state = state.copyWith(studios: studios);
|
||||
void setTags(Map<String, bool> tags) => state = state.copyWith(tags: tags);
|
||||
void setTypes(Map<FladderItemType, bool> types) => state = state.copyWith(types: types);
|
||||
void setRatings(Map<String, bool> officialRatings) => state = state.copyWith(officialRatings: officialRatings);
|
||||
void setYears(Map<int, bool> years) => state = state.copyWith(years: years);
|
||||
void setFilters(Map<ItemFilter, bool> filters) => state = state.copyWith(filters: filters);
|
||||
|
||||
void clearAllFilters() {
|
||||
state = state.copyWith(
|
||||
genres: state.genres.setAll(false),
|
||||
tags: state.tags.setAll(false),
|
||||
officialRatings: state.officialRatings.setAll(false),
|
||||
years: state.years.setAll(false),
|
||||
searchQuery: '',
|
||||
favourites: false,
|
||||
recursive: false,
|
||||
studios: state.studios.setAll(false),
|
||||
filters: state.filters.setAll(false),
|
||||
hideEmtpyShows: false,
|
||||
);
|
||||
}
|
||||
|
||||
void setSortBy(SortingOptions e) => state = state.copyWith(sortingOption: e);
|
||||
|
||||
void setSortOrder(SortingOrder e) => state = state.copyWith(sortOrder: e);
|
||||
|
||||
void setHideEmpty(bool value) => state = state.copyWith(hideEmtpyShows: value);
|
||||
void setGroupBy(GroupBy groupBy) => state = state.copyWith(groupBy: groupBy);
|
||||
|
||||
void setFolderId(ItemBaseModel item) {
|
||||
if (state.folderOverwrite.contains(item)) return;
|
||||
state = state.copyWith(folderOverwrite: [...state.folderOverwrite, item]);
|
||||
}
|
||||
|
||||
void backToFolder(ItemBaseModel item) => state = state.copyWith(
|
||||
folderOverwrite: state.folderOverwrite.getRange(0, state.folderOverwrite.indexOf(item) + 1).toList());
|
||||
|
||||
void clearFolderOverWrite() => state = state.copyWith(folderOverwrite: []);
|
||||
|
||||
void toggleSelectMode() =>
|
||||
state = state.copyWith(selecteMode: !state.selecteMode, selectedPosters: !state.selecteMode == false ? [] : null);
|
||||
|
||||
void toggleSelection(ItemBaseModel item) {
|
||||
if (state.selectedPosters.contains(item)) {
|
||||
state = state.copyWith(selectedPosters: state.selectedPosters.where((element) => element != item).toList());
|
||||
} else {
|
||||
state = state.copyWith(selectedPosters: [...state.selectedPosters, item]);
|
||||
}
|
||||
}
|
||||
|
||||
selectAll(bool select) => state = state.copyWith(selectedPosters: select ? state.posters : []);
|
||||
|
||||
Future<void> setSelectedAsFavorite(bool bool) async {
|
||||
final Map<String, UserData> updateInfo = {};
|
||||
for (var i = 0; i < state.selectedPosters.length; i++) {
|
||||
final response = await ref.read(userProvider.notifier).setAsFavorite(bool, state.selectedPosters[i].id);
|
||||
final userData = response?.bodyOrThrow;
|
||||
if (userData != null) {
|
||||
updateInfo.putIfAbsent(state.selectedPosters[i].id, () => userData);
|
||||
}
|
||||
}
|
||||
updateMultiUserData(updateInfo);
|
||||
}
|
||||
|
||||
Future<void> setSelectedAsWatched(bool bool) async {
|
||||
final Map<String, UserData> updateInfo = {};
|
||||
for (var i = 0; i < state.selectedPosters.length; i++) {
|
||||
final response = await ref.read(userProvider.notifier).markAsPlayed(bool, state.selectedPosters[i].id);
|
||||
final userData = response?.bodyOrThrow;
|
||||
if (userData != null) {
|
||||
updateInfo.putIfAbsent(state.selectedPosters[i].id, () => userData);
|
||||
}
|
||||
}
|
||||
updateMultiUserData(updateInfo);
|
||||
}
|
||||
|
||||
Future<Response> removeSelectedFromCollection() async {
|
||||
final response = await api.collectionsCollectionIdItemsDelete(
|
||||
collectionId: state.nestedCurrentItem?.id, ids: state.selectedPosters.map((e) => e.id).toList());
|
||||
if (response.isSuccessful) {
|
||||
removeFromPosters([state.nestedCurrentItem?.id].whereNotNull().toList());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<Response> removeSelectedFromPlaylist() async {
|
||||
final response = await api.playlistsPlaylistIdItemsDelete(
|
||||
playlistId: state.nestedCurrentItem?.id,
|
||||
entryIds: state.selectedPosters.map((e) => e.playlistId).whereNotNull().toList());
|
||||
if (response.isSuccessful) {
|
||||
removeFromPosters([state.nestedCurrentItem?.id].whereNotNull().toList());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<Response> removeFromCollection({required List<ItemBaseModel> items}) async {
|
||||
final response = await api.collectionsCollectionIdItemsDelete(
|
||||
collectionId: state.nestedCurrentItem?.id, ids: items.map((e) => e.id).toList());
|
||||
if (response.isSuccessful) {
|
||||
removeFromPosters(items.map((e) => e.id).toList());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<Response> removeFromPlaylist({required List<ItemBaseModel> items}) async {
|
||||
final response = await api.playlistsPlaylistIdItemsDelete(
|
||||
playlistId: state.nestedCurrentItem?.id, entryIds: items.map((e) => e.playlistId).whereNotNull().toList());
|
||||
if (response.isSuccessful) {
|
||||
removeFromPosters(items.map((e) => e.id).toList());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<void> updateMultiUserData(Map<String, UserData?> newData) async {
|
||||
for (var element in newData.entries) {
|
||||
updateUserData(element.key, element.value);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateUserData(String id, UserData? newData) async {
|
||||
final currentItems = state.posters.toList();
|
||||
final item = currentItems.firstWhereOrNull((element) => element.id == id);
|
||||
if (item == null) return;
|
||||
final indexOf = currentItems.indexOf(item);
|
||||
if (indexOf == -1) return;
|
||||
currentItems.removeAt(indexOf);
|
||||
currentItems.insert(indexOf, item.copyWith(userData: newData));
|
||||
state = state.copyWith(posters: currentItems);
|
||||
}
|
||||
|
||||
void setDefaultOptions(SortingOrder? sortOrder, SortingOptions? sortingOptions) {
|
||||
state = state.copyWith(
|
||||
sortOrder: sortOrder,
|
||||
sortingOption: sortingOptions,
|
||||
);
|
||||
}
|
||||
|
||||
void updateUserDataMain(UserData? userData) {
|
||||
state = state.copyWith(
|
||||
folderOverwrite: [state.folderOverwrite.lastOrNull?.copyWith(userData: userData)].whereNotNull().toList(),
|
||||
);
|
||||
}
|
||||
|
||||
void updateParentItem(ItemBaseModel item) {
|
||||
state = state.copyWith(
|
||||
folderOverwrite: [item],
|
||||
);
|
||||
}
|
||||
|
||||
void removeFromPosters(List<String> ids) {
|
||||
final newPosters = state.posters;
|
||||
state = state.copyWith(posters: newPosters..removeWhere((element) => ids.contains(element.id)));
|
||||
}
|
||||
|
||||
void updateItems(List<ItemBaseModel> items) {}
|
||||
|
||||
void updateItem(ItemBaseModel item) {
|
||||
state = state.copyWith(posters: state.posters.replace(item));
|
||||
}
|
||||
|
||||
Future<List<ItemBaseModel>> _loadAllItems({bool shuffle = false, int? limit}) async {
|
||||
List<ItemBaseModel> itemsToPlay = [];
|
||||
|
||||
Future<void> handleItemLoading(String itemId, ItemBaseModel currentModel) async {
|
||||
final result =
|
||||
currentModel is PlaylistModel ? await _loadPlaylistItems(id: itemId) : await _loadLibrary(id: itemId);
|
||||
|
||||
itemsToPlay = result?.items ?? [];
|
||||
}
|
||||
|
||||
Future<void> handleViewLoading() async {
|
||||
final results = await Future.wait(
|
||||
state.views.included.map((viewModel) async {
|
||||
final libraryItems = await _loadLibrary(
|
||||
shuffle: shuffle,
|
||||
viewModel: viewModel,
|
||||
limit: limit,
|
||||
);
|
||||
return libraryItems;
|
||||
}).whereNotNull(),
|
||||
);
|
||||
|
||||
List<ItemBaseModel> newPosters = results.whereNotNull().expand((element) => element.items).toList();
|
||||
if (state.views.included.length > 1) {
|
||||
if (shuffle || state.sortingOption == SortingOptions.random) {
|
||||
newPosters = newPosters.random();
|
||||
} else {
|
||||
newPosters = newPosters.sorted(
|
||||
(a, b) => sortItems(a, b, state.sortingOption, state.sortOrder),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
itemsToPlay = newPosters;
|
||||
}
|
||||
|
||||
if (state.folderOverwrite.isNotEmpty) {
|
||||
await handleItemLoading(state.folderOverwrite.last.id, state.folderOverwrite.last);
|
||||
} else if (state.views.hasEnabled) {
|
||||
await handleViewLoading();
|
||||
} else {
|
||||
if (state.searchQuery.isEmpty && !state.favourites) {
|
||||
itemsToPlay = [];
|
||||
} else {
|
||||
final response = await _loadLibrary(recursive: true, shuffle: shuffle);
|
||||
itemsToPlay = response?.items ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
return itemsToPlay;
|
||||
}
|
||||
|
||||
Future<void> playLibraryItems(BuildContext context, WidgetRef ref, {bool shuffle = false}) async {
|
||||
state = state.copyWith(fetchingItems: true);
|
||||
List<ItemBaseModel> itemsToPlay = [];
|
||||
|
||||
if (state.selectedPosters.isNotEmpty) {
|
||||
itemsToPlay = shuffle ? state.selectedPosters.random() : state.selectedPosters;
|
||||
} else {
|
||||
itemsToPlay = await _loadAllItems(shuffle: shuffle);
|
||||
}
|
||||
|
||||
state = state.copyWith(fetchingItems: false);
|
||||
|
||||
if (itemsToPlay.isNotEmpty) {
|
||||
await itemsToPlay.playLibraryItems(context, ref);
|
||||
} else {
|
||||
fladderSnackbar(context, title: context.localized.libraryFetchNoItemsFound);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<PhotoModel>> fetchGallery({bool shuffle = false}) async {
|
||||
try {
|
||||
List<ItemBaseModel> itemsToPlay = [];
|
||||
|
||||
if (state.selectedPosters.isNotEmpty) {
|
||||
itemsToPlay = shuffle ? state.selectedPosters.random() : state.selectedPosters;
|
||||
} else {
|
||||
itemsToPlay = await _loadAllItems(shuffle: shuffle);
|
||||
}
|
||||
|
||||
List<PhotoModel> albumItems = [];
|
||||
|
||||
if (!state.types.included.containsAny([FladderItemType.video, FladderItemType.photo]) && state.recursive) {
|
||||
for (var album in itemsToPlay.where(
|
||||
(element) => element is PhotoAlbumModel || element is FolderModel,
|
||||
)) {
|
||||
try {
|
||||
final fetchedAlbumContent = await api.itemsGet(
|
||||
parentId: album.id,
|
||||
includeItemTypes: [BaseItemKind.video, BaseItemKind.photo],
|
||||
recursive: true,
|
||||
fields: {
|
||||
ItemFields.genres,
|
||||
ItemFields.parentid,
|
||||
ItemFields.tags,
|
||||
ItemFields.datecreated,
|
||||
ItemFields.datelastmediaadded,
|
||||
ItemFields.overview,
|
||||
ItemFields.originaltitle,
|
||||
ItemFields.customrating,
|
||||
ItemFields.primaryimageaspectratio,
|
||||
}.toList(),
|
||||
filters: [
|
||||
...state.filters.included,
|
||||
if (state.favourites) ItemFilter.isfavorite,
|
||||
],
|
||||
sortBy: shuffle ? [ItemSortBy.random] : null,
|
||||
);
|
||||
albumItems.addAll(fetchedAlbumContent.body?.items.whereType<PhotoModel>() ?? []);
|
||||
} catch (e) {
|
||||
log("Error fetching ${e.toString()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final galleryItems = itemsToPlay.whereType<PhotoModel>().toList();
|
||||
|
||||
if (shuffle) {
|
||||
albumItems = albumItems.random();
|
||||
}
|
||||
|
||||
final allItems = {...albumItems.whereType<PhotoModel>(), ...galleryItems}.toList();
|
||||
|
||||
return allItems;
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
} finally {}
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<void> viewGallery(BuildContext context, {PhotoModel? selected, bool shuffle = false}) async {
|
||||
state = state.copyWith(fetchingItems: true);
|
||||
final allItems = await fetchGallery(shuffle: shuffle);
|
||||
|
||||
if (allItems.isNotEmpty) {
|
||||
if (state.fetchingItems == true) {
|
||||
state = state.copyWith(fetchingItems: false);
|
||||
await Navigator.of(context, rootNavigator: true).push(
|
||||
PageTransition(
|
||||
child: PhotoViewerScreen(
|
||||
items: allItems,
|
||||
indexOfSelected: selected != null ? allItems.indexOf(selected) : 0,
|
||||
),
|
||||
type: PageTransitionType.fade),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
fladderSnackbar(context, title: context.localized.libraryFetchNoItemsFound);
|
||||
}
|
||||
state = state.copyWith(fetchingItems: false);
|
||||
}
|
||||
|
||||
void cancelFetch() {
|
||||
state = state.copyWith(fetchingItems: false);
|
||||
}
|
||||
|
||||
Future<void> openRandom(BuildContext context) async {
|
||||
final items = await _loadAllItems(shuffle: true, limit: 1);
|
||||
if (items.isNotEmpty) {
|
||||
items.firstOrNull?.navigateTo(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SimpleSorter on List<ItemBaseModel> {
|
||||
List<ItemBaseModel> hideEmptyChildren(bool hide) {
|
||||
if (hide) {
|
||||
return where((element) {
|
||||
if (element.childCount == null) {
|
||||
return true;
|
||||
}
|
||||
return (element.childCount ?? 0) > 0;
|
||||
}).toList();
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
83
lib/providers/playlist_provider.dart
Normal file
83
lib/providers/playlist_provider.dart
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import 'package:chopper/chopper.dart';
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/playlist_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/service_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class _PlaylistProviderModel {
|
||||
final List<ItemBaseModel> items;
|
||||
final Map<PlaylistModel, bool?> collections;
|
||||
_PlaylistProviderModel({
|
||||
required this.items,
|
||||
required this.collections,
|
||||
});
|
||||
|
||||
_PlaylistProviderModel copyWith({
|
||||
List<ItemBaseModel>? items,
|
||||
Map<PlaylistModel, bool?>? collections,
|
||||
}) {
|
||||
return _PlaylistProviderModel(
|
||||
items: items ?? this.items,
|
||||
collections: collections ?? this.collections,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final playlistProvider = StateNotifierProvider.autoDispose<BoxSetNotifier, _PlaylistProviderModel>((ref) {
|
||||
return BoxSetNotifier(ref);
|
||||
});
|
||||
|
||||
class BoxSetNotifier extends StateNotifier<_PlaylistProviderModel> {
|
||||
BoxSetNotifier(this.ref) : super(_PlaylistProviderModel(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 serverPlaylists = await api.usersUserIdItemsGet(
|
||||
recursive: true,
|
||||
includeItemTypes: [
|
||||
BaseItemKind.playlist,
|
||||
],
|
||||
);
|
||||
|
||||
final playlists = serverPlaylists.body?.items?.map((e) => PlaylistModel.fromBaseDto(e, ref)).toList();
|
||||
|
||||
if (state.items.length == 1 && (playlists?.length ?? 0) < 25) {
|
||||
final List<Future<bool>> itemChecks = playlists?.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<PlaylistModel, bool?> boxSetContainsItemMap = Map.fromIterables(playlists ?? [], results);
|
||||
|
||||
state = state.copyWith(collections: boxSetContainsItemMap);
|
||||
} else {
|
||||
final Map<PlaylistModel, bool?> playlistContainsMap =
|
||||
Map.fromIterables(playlists ?? [], List.generate(playlists?.length ?? 0, (index) => null));
|
||||
state = state.copyWith(collections: playlistContainsMap);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Response> addToPlaylist({required PlaylistModel playlist}) async =>
|
||||
await api.playlistsPlaylistIdItemsPost(playlistId: playlist.id, ids: state.items.map((e) => e.id).toList());
|
||||
|
||||
Future<Response> addToNewPlaylist({required String name}) async {
|
||||
final result = await api.playlistsPost(name: name, ids: state.items.map((e) => e.id).toList(), body: null);
|
||||
if (result.isSuccessful) {
|
||||
await _init();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
27
lib/providers/related_provider.dart
Normal file
27
lib/providers/related_provider.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import 'package:chopper/chopper.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/user_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final relatedUtilityProvider = Provider<RelatedNotifier>((ref) {
|
||||
return RelatedNotifier(ref: ref);
|
||||
});
|
||||
|
||||
class RelatedNotifier {
|
||||
final Ref ref;
|
||||
RelatedNotifier({
|
||||
required this.ref,
|
||||
});
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
late final String currentServerUrl = ref.read(userProvider)?.server ?? "";
|
||||
|
||||
Future<Response<List<ItemBaseModel>>> relatedContent(String itemId) async {
|
||||
final related = await api.itemsItemIdSimilarGet(itemId: itemId, limit: 50);
|
||||
List<ItemBaseModel> posters = related.body?.items?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [];
|
||||
return Response(related.base, posters);
|
||||
}
|
||||
}
|
||||
42
lib/providers/search_provider.dart
Normal file
42
lib/providers/search_provider.dart
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import 'package:chopper/chopper.dart';
|
||||
import 'package:fladder/models/search_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/service_provider.dart';
|
||||
import 'package:fladder/util/item_base_model/item_base_model_extensions.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final searchProvider = StateNotifierProvider<SearchNotifier, SearchModel>((ref) {
|
||||
return SearchNotifier(ref);
|
||||
});
|
||||
|
||||
class SearchNotifier extends StateNotifier<SearchModel> {
|
||||
SearchNotifier(this.ref) : super(SearchModel());
|
||||
|
||||
final Ref ref;
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
Future<Response?> searchQuery() async {
|
||||
if (state.searchQuery.isEmpty) return null;
|
||||
state = state.copyWith(loading: true);
|
||||
final response = await api.itemsGet(
|
||||
recursive: true,
|
||||
searchTerm: state.searchQuery,
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
resultCount: response.body?.totalRecordCount ?? 0,
|
||||
results: (response.body?.items)?.groupedItems,
|
||||
);
|
||||
state = state.copyWith(loading: false);
|
||||
return response;
|
||||
}
|
||||
|
||||
void setQuery(String searchQuery) {
|
||||
state = state.copyWith(searchQuery: searchQuery);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
state = SearchModel();
|
||||
}
|
||||
}
|
||||
938
lib/providers/service_provider.dart
Normal file
938
lib/providers/service_provider.dart
Normal file
|
|
@ -0,0 +1,938 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:chopper/chopper.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fladder/jellyfin/enum_models.dart';
|
||||
import 'package:fladder/models/credentials_model.dart';
|
||||
import 'package:fladder/models/items/intro_skip_model.dart';
|
||||
import 'package:fladder/models/items/trick_play_model.dart';
|
||||
import 'package:fladder/providers/image_provider.dart';
|
||||
import 'package:fladder/util/duration_extensions.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/account_model.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/util/jellyfin_extension.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
final jellyServiceProvider = StateProvider<JellyService>(
|
||||
(ref) => JellyService(
|
||||
ref,
|
||||
JellyfinOpenApi.create(
|
||||
interceptors: [
|
||||
JellyRequest(ref),
|
||||
JellyResponse(ref),
|
||||
HttpLoggingInterceptor(level: Level.basic),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
class ServerQueryResult {
|
||||
final List<BaseItemDto> original;
|
||||
final List<ItemBaseModel> items;
|
||||
final int? totalRecordCount;
|
||||
final int? startIndex;
|
||||
ServerQueryResult({
|
||||
required this.original,
|
||||
required this.items,
|
||||
this.totalRecordCount,
|
||||
this.startIndex,
|
||||
});
|
||||
|
||||
factory ServerQueryResult.fromBaseQuery(
|
||||
BaseItemDtoQueryResult baseQuery,
|
||||
Ref ref,
|
||||
) {
|
||||
return ServerQueryResult(
|
||||
original: baseQuery.items ?? [],
|
||||
items: baseQuery.items
|
||||
?.map(
|
||||
(e) => ItemBaseModel.fromBaseDto(e, ref),
|
||||
)
|
||||
.toList() ??
|
||||
[],
|
||||
totalRecordCount: baseQuery.totalRecordCount,
|
||||
startIndex: baseQuery.startIndex,
|
||||
);
|
||||
}
|
||||
|
||||
ServerQueryResult copyWith({
|
||||
List<BaseItemDto>? original,
|
||||
List<ItemBaseModel>? items,
|
||||
int? totalRecordCount,
|
||||
int? startIndex,
|
||||
}) {
|
||||
return ServerQueryResult(
|
||||
original: original ?? this.original,
|
||||
items: items ?? this.items,
|
||||
totalRecordCount: totalRecordCount ?? this.totalRecordCount,
|
||||
startIndex: startIndex ?? this.startIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class JellyService {
|
||||
JellyService(this.ref, this.api);
|
||||
|
||||
final JellyfinOpenApi api;
|
||||
final Ref ref;
|
||||
AccountModel? get account => ref.read(userProvider);
|
||||
|
||||
Future<Response<ItemBaseModel>> usersUserIdItemsItemIdGet({
|
||||
String? itemId,
|
||||
}) async {
|
||||
final response = await api.itemsItemIdGet(
|
||||
userId: account?.id,
|
||||
itemId: itemId,
|
||||
);
|
||||
return response.copyWith(body: ItemBaseModel.fromBaseDto(response.bodyOrThrow, ref));
|
||||
}
|
||||
|
||||
Future<Response<BaseItemDto>> usersUserIdItemsItemIdGetBaseItem({
|
||||
String? itemId,
|
||||
}) async {
|
||||
final response = await api.itemsItemIdGet(
|
||||
userId: account?.id,
|
||||
itemId: itemId,
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<Response<ServerQueryResult>> itemsGet({
|
||||
String? maxOfficialRating,
|
||||
bool? hasThemeSong,
|
||||
bool? hasThemeVideo,
|
||||
bool? hasSubtitles,
|
||||
bool? hasSpecialFeature,
|
||||
bool? hasTrailer,
|
||||
String? adjacentTo,
|
||||
int? parentIndexNumber,
|
||||
bool? hasParentalRating,
|
||||
bool? isHd,
|
||||
bool? is4K,
|
||||
List<LocationType>? locationTypes,
|
||||
List<LocationType>? excludeLocationTypes,
|
||||
bool? isMissing,
|
||||
bool? isUnaired,
|
||||
num? minCommunityRating,
|
||||
num? minCriticRating,
|
||||
DateTime? minPremiereDate,
|
||||
DateTime? minDateLastSaved,
|
||||
DateTime? minDateLastSavedForUser,
|
||||
DateTime? maxPremiereDate,
|
||||
bool? hasOverview,
|
||||
bool? hasImdbId,
|
||||
bool? hasTmdbId,
|
||||
bool? hasTvdbId,
|
||||
bool? isMovie,
|
||||
bool? isSeries,
|
||||
bool? isNews,
|
||||
bool? isKids,
|
||||
bool? isSports,
|
||||
List<String>? excludeItemIds,
|
||||
int? startIndex,
|
||||
int? limit,
|
||||
bool? recursive,
|
||||
String? searchTerm,
|
||||
List<SortOrder>? sortOrder,
|
||||
String? parentId,
|
||||
List<ItemFields>? fields,
|
||||
List<BaseItemKind>? excludeItemTypes,
|
||||
List<BaseItemKind>? includeItemTypes,
|
||||
List<ItemFilter>? filters,
|
||||
bool? isFavorite,
|
||||
List<MediaType>? mediaTypes,
|
||||
List<ImageType>? imageTypes,
|
||||
List<ItemSortBy>? sortBy,
|
||||
bool? isPlayed,
|
||||
List<String>? genres,
|
||||
List<String>? officialRatings,
|
||||
List<String>? tags,
|
||||
List<int>? years,
|
||||
bool? enableUserData,
|
||||
int? imageTypeLimit,
|
||||
List<ImageType>? enableImageTypes,
|
||||
String? person,
|
||||
List<String>? personIds,
|
||||
List<String>? personTypes,
|
||||
List<String>? studios,
|
||||
List<String>? artists,
|
||||
List<String>? excludeArtistIds,
|
||||
List<String>? artistIds,
|
||||
List<String>? albumArtistIds,
|
||||
List<String>? contributingArtistIds,
|
||||
List<String>? albums,
|
||||
List<String>? albumIds,
|
||||
List<String>? ids,
|
||||
List<VideoType>? videoTypes,
|
||||
String? minOfficialRating,
|
||||
bool? isLocked,
|
||||
bool? isPlaceHolder,
|
||||
bool? hasOfficialRating,
|
||||
bool? collapseBoxSetItems,
|
||||
int? minWidth,
|
||||
int? minHeight,
|
||||
int? maxWidth,
|
||||
int? maxHeight,
|
||||
bool? is3D,
|
||||
List<SeriesStatus>? seriesStatus,
|
||||
String? nameStartsWithOrGreater,
|
||||
String? nameStartsWith,
|
||||
String? nameLessThan,
|
||||
List<String>? studioIds,
|
||||
List<String>? genreIds,
|
||||
bool? enableTotalRecordCount,
|
||||
bool? enableImages,
|
||||
}) async {
|
||||
final response = await api.itemsGet(
|
||||
userId: account?.id,
|
||||
maxOfficialRating: maxOfficialRating,
|
||||
hasThemeSong: hasThemeSong,
|
||||
hasThemeVideo: hasThemeVideo,
|
||||
hasSubtitles: hasSubtitles,
|
||||
hasSpecialFeature: hasSpecialFeature,
|
||||
hasTrailer: hasTrailer,
|
||||
adjacentTo: adjacentTo,
|
||||
parentIndexNumber: parentIndexNumber,
|
||||
hasParentalRating: hasParentalRating,
|
||||
isHd: isHd,
|
||||
is4K: is4K,
|
||||
locationTypes: locationTypes,
|
||||
excludeLocationTypes: excludeLocationTypes,
|
||||
isMissing: isMissing,
|
||||
isUnaired: isUnaired,
|
||||
minCommunityRating: minCommunityRating,
|
||||
minCriticRating: minCriticRating,
|
||||
minPremiereDate: minPremiereDate,
|
||||
minDateLastSaved: minDateLastSaved,
|
||||
minDateLastSavedForUser: minDateLastSavedForUser,
|
||||
maxPremiereDate: maxPremiereDate,
|
||||
hasOverview: hasOverview,
|
||||
hasImdbId: hasImdbId,
|
||||
hasTmdbId: hasTmdbId,
|
||||
hasTvdbId: hasTvdbId,
|
||||
isMovie: isMovie,
|
||||
isSeries: isSeries,
|
||||
isNews: isNews,
|
||||
isKids: isKids,
|
||||
isSports: isSports,
|
||||
excludeItemIds: excludeItemIds,
|
||||
startIndex: startIndex,
|
||||
limit: limit,
|
||||
recursive: recursive,
|
||||
searchTerm: searchTerm,
|
||||
sortOrder: sortOrder,
|
||||
sortBy: sortBy,
|
||||
parentId: parentId,
|
||||
fields: {...?fields, ItemFields.candelete, ItemFields.candownload}.toList(),
|
||||
excludeItemTypes: excludeItemTypes,
|
||||
includeItemTypes: includeItemTypes,
|
||||
filters: filters,
|
||||
isFavorite: isFavorite,
|
||||
mediaTypes: mediaTypes,
|
||||
imageTypes: imageTypes,
|
||||
isPlayed: isPlayed,
|
||||
genres: genres,
|
||||
officialRatings: officialRatings,
|
||||
tags: tags,
|
||||
years: years,
|
||||
enableUserData: enableUserData,
|
||||
imageTypeLimit: imageTypeLimit,
|
||||
enableImageTypes: enableImageTypes,
|
||||
person: person,
|
||||
personIds: personIds,
|
||||
personTypes: personTypes,
|
||||
studios: studios,
|
||||
artists: artists,
|
||||
excludeArtistIds: excludeArtistIds,
|
||||
artistIds: artistIds,
|
||||
albumArtistIds: albumArtistIds,
|
||||
contributingArtistIds: contributingArtistIds,
|
||||
albums: albums,
|
||||
albumIds: albumIds,
|
||||
ids: ids,
|
||||
videoTypes: videoTypes,
|
||||
minOfficialRating: minOfficialRating,
|
||||
isLocked: isLocked,
|
||||
isPlaceHolder: isPlaceHolder,
|
||||
hasOfficialRating: hasOfficialRating,
|
||||
collapseBoxSetItems: collapseBoxSetItems,
|
||||
minWidth: minWidth,
|
||||
minHeight: minHeight,
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
is3D: is3D,
|
||||
seriesStatus: seriesStatus,
|
||||
nameStartsWithOrGreater: nameStartsWithOrGreater,
|
||||
nameStartsWith: nameStartsWith,
|
||||
nameLessThan: nameLessThan,
|
||||
studioIds: studioIds,
|
||||
genreIds: genreIds,
|
||||
enableTotalRecordCount: enableTotalRecordCount,
|
||||
enableImages: enableImages,
|
||||
);
|
||||
|
||||
return response.copyWith(
|
||||
body: ServerQueryResult.fromBaseQuery(response.bodyOrThrow, ref),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<List<ItemBaseModel>>> personsGet({
|
||||
String? searchTerm,
|
||||
int? limit,
|
||||
bool? isFavorite,
|
||||
}) async {
|
||||
final response = await api.personsGet(
|
||||
userId: account?.id,
|
||||
limit: limit,
|
||||
isFavorite: isFavorite,
|
||||
);
|
||||
return response.copyWith(
|
||||
body: response.body?.items
|
||||
?.map(
|
||||
(e) => ItemBaseModel.fromBaseDto(e, ref),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<List<ImageInfo>>> itemsItemIdImagesGet({
|
||||
String? itemId,
|
||||
bool? isFavorite,
|
||||
}) async {
|
||||
final response = await api.itemsItemIdImagesGet(itemId: itemId);
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<Response<MetadataEditorInfo>> itemsItemIdMetadataEditorGet({
|
||||
String? itemId,
|
||||
}) async {
|
||||
return api.itemsItemIdMetadataEditorGet(itemId: itemId);
|
||||
}
|
||||
|
||||
Future<Response<RemoteImageResult>> itemsItemIdRemoteImagesGet({
|
||||
String? itemId,
|
||||
ImageType? type,
|
||||
bool? includeAllLanguages,
|
||||
}) async {
|
||||
return api.itemsItemIdRemoteImagesGet(
|
||||
itemId: itemId,
|
||||
type: ItemsItemIdRemoteImagesGetType.values.firstWhereOrNull(
|
||||
(element) => element.value == type?.value,
|
||||
),
|
||||
includeAllLanguages: includeAllLanguages,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> itemsItemIdPost({
|
||||
String? itemId,
|
||||
required BaseItemDto? body,
|
||||
}) async {
|
||||
return api.itemsItemIdPost(
|
||||
itemId: itemId,
|
||||
body: body,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<dynamic>?> itemIdImagesImageTypePost(
|
||||
ImageType type,
|
||||
String itemId,
|
||||
Uint8List data,
|
||||
) async {
|
||||
return api.itemIdImagesImageTypePost(
|
||||
type,
|
||||
itemId,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> itemsItemIdRemoteImagesDownloadPost({
|
||||
required String? itemId,
|
||||
required ImageType? type,
|
||||
String? imageUrl,
|
||||
}) async {
|
||||
return api.itemsItemIdRemoteImagesDownloadPost(
|
||||
itemId: itemId,
|
||||
type: ItemsItemIdRemoteImagesDownloadPostType.values.firstWhereOrNull(
|
||||
(element) => element.value == type?.value,
|
||||
),
|
||||
imageUrl: imageUrl,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> itemsItemIdImagesImageTypeDelete({
|
||||
required String? itemId,
|
||||
required ImageType? imageType,
|
||||
int? imageIndex,
|
||||
}) async {
|
||||
return api.itemsItemIdImagesImageTypeDelete(
|
||||
itemId: itemId,
|
||||
imageType: ItemsItemIdImagesImageTypeDeleteImageType.values.firstWhereOrNull(
|
||||
(element) => element.value == imageType?.value,
|
||||
),
|
||||
imageIndex: imageIndex,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<BaseItemDtoQueryResult>> usersUserIdItemsResumeGet({
|
||||
int? startIndex,
|
||||
int? limit,
|
||||
String? searchTerm,
|
||||
String? parentId,
|
||||
List<ItemFields>? fields,
|
||||
List<MediaType>? mediaTypes,
|
||||
bool? enableUserData,
|
||||
bool? enableTotalRecordCount,
|
||||
List<ImageType>? enableImageTypes,
|
||||
List<BaseItemKind>? excludeItemTypes,
|
||||
List<BaseItemKind>? includeItemTypes,
|
||||
}) async {
|
||||
return api.userItemsResumeGet(
|
||||
userId: account?.id,
|
||||
searchTerm: searchTerm,
|
||||
parentId: parentId,
|
||||
limit: limit,
|
||||
fields: fields,
|
||||
mediaTypes: mediaTypes,
|
||||
enableTotalRecordCount: enableTotalRecordCount,
|
||||
enableImageTypes: enableImageTypes,
|
||||
enableUserData: enableUserData,
|
||||
includeItemTypes: includeItemTypes,
|
||||
excludeItemTypes: excludeItemTypes,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<List<BaseItemDto>>> usersUserIdItemsLatestGet({
|
||||
String? parentId,
|
||||
List<ItemFields>? fields,
|
||||
List<BaseItemKind>? includeItemTypes,
|
||||
bool? isPlayed,
|
||||
bool? enableImages,
|
||||
int? imageTypeLimit,
|
||||
List<ImageType>? enableImageTypes,
|
||||
bool? enableUserData,
|
||||
int? limit,
|
||||
bool? groupItems,
|
||||
}) async {
|
||||
return api.itemsLatestGet(
|
||||
parentId: parentId,
|
||||
userId: account?.id,
|
||||
fields: fields,
|
||||
includeItemTypes: includeItemTypes,
|
||||
isPlayed: isPlayed,
|
||||
enableImages: enableImages,
|
||||
imageTypeLimit: imageTypeLimit,
|
||||
enableImageTypes: enableImageTypes,
|
||||
enableUserData: enableUserData,
|
||||
limit: limit,
|
||||
groupItems: groupItems,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<List<RecommendationDto>>> moviesRecommendationsGet({
|
||||
String? parentId,
|
||||
List<ItemFields>? fields,
|
||||
int? categoryLimit,
|
||||
int? itemLimit,
|
||||
}) async {
|
||||
return api.moviesRecommendationsGet(
|
||||
userId: account?.id,
|
||||
parentId: parentId,
|
||||
fields: fields,
|
||||
categoryLimit: categoryLimit,
|
||||
itemLimit: itemLimit,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<BaseItemDtoQueryResult>> showsNextUpGet({
|
||||
int? startIndex,
|
||||
int? limit,
|
||||
String? parentId,
|
||||
DateTime? nextUpDateCutoff,
|
||||
List<ItemFields>? fields,
|
||||
bool? enableUserData,
|
||||
List<ImageType>? enableImageTypes,
|
||||
int? imageTypeLimit,
|
||||
}) async {
|
||||
return api.showsNextUpGet(
|
||||
userId: account?.id,
|
||||
parentId: parentId,
|
||||
limit: limit,
|
||||
fields: fields,
|
||||
enableResumable: false,
|
||||
enableRewatching: false,
|
||||
disableFirstEpisode: false,
|
||||
nextUpDateCutoff: nextUpDateCutoff,
|
||||
enableImageTypes: enableImageTypes,
|
||||
enableUserData: enableUserData,
|
||||
imageTypeLimit: imageTypeLimit,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<BaseItemDtoQueryResult>> genresGet({
|
||||
String? parentId,
|
||||
List<ItemSortBy>? sortBy,
|
||||
List<SortOrder>? sortOrder,
|
||||
List<BaseItemKind>? includeItemTypes,
|
||||
}) async {
|
||||
return api.genresGet(
|
||||
parentId: parentId,
|
||||
userId: account?.id,
|
||||
sortBy: sortBy,
|
||||
sortOrder: sortOrder,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> sessionsPlayingPost({required PlaybackStartInfo? body}) async => api.sessionsPlayingPost(body: body);
|
||||
|
||||
Future<Response> sessionsPlayingStoppedPost({
|
||||
required PlaybackStopInfo? body,
|
||||
Duration? totalDuration,
|
||||
}) async {
|
||||
final position = body?.positionTicks;
|
||||
final totalTime = totalDuration?.toRuntimeTicks;
|
||||
final maxTime = ref.read(userProvider.select((value) => value?.serverConfiguration?.maxResumePct ?? 90));
|
||||
|
||||
final response = await api.sessionsPlayingStoppedPost(
|
||||
body: body?.copyWith(
|
||||
failed: false,
|
||||
),
|
||||
);
|
||||
|
||||
//This is a temporary fix
|
||||
if (totalTime != null && position != null && position > (totalTime * (maxTime / 100))) {
|
||||
await usersUserIdPlayedItemsItemIdPost(itemId: body?.itemId, datePlayed: DateTime.now());
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<Response> sessionsPlayingProgressPost({required PlaybackProgressInfo? body}) async =>
|
||||
api.sessionsPlayingProgressPost(body: body);
|
||||
|
||||
Future<Response<PlaybackInfoResponse>> itemsItemIdPlaybackInfoPost({
|
||||
required String? itemId,
|
||||
int? maxStreamingBitrate,
|
||||
int? startTimeTicks,
|
||||
int? audioStreamIndex,
|
||||
int? subtitleStreamIndex,
|
||||
int? maxAudioChannels,
|
||||
String? mediaSourceId,
|
||||
String? liveStreamId,
|
||||
bool? autoOpenLiveStream,
|
||||
bool? enableDirectPlay,
|
||||
bool? enableDirectStream,
|
||||
bool? enableTranscoding,
|
||||
bool? allowVideoStreamCopy,
|
||||
bool? allowAudioStreamCopy,
|
||||
required PlaybackInfoDto? body,
|
||||
}) async =>
|
||||
api.itemsItemIdPlaybackInfoPost(
|
||||
itemId: itemId,
|
||||
userId: account?.id,
|
||||
enableDirectPlay: enableDirectPlay,
|
||||
enableDirectStream: enableDirectStream,
|
||||
enableTranscoding: enableTranscoding,
|
||||
autoOpenLiveStream: autoOpenLiveStream,
|
||||
maxStreamingBitrate: maxStreamingBitrate,
|
||||
liveStreamId: liveStreamId,
|
||||
startTimeTicks: startTimeTicks,
|
||||
mediaSourceId: mediaSourceId,
|
||||
audioStreamIndex: audioStreamIndex,
|
||||
subtitleStreamIndex: subtitleStreamIndex,
|
||||
body: body,
|
||||
);
|
||||
|
||||
Future<Response<BaseItemDtoQueryResult>> showsSeriesIdEpisodesGet({
|
||||
required String? seriesId,
|
||||
List<ItemFields>? fields,
|
||||
int? season,
|
||||
String? seasonId,
|
||||
bool? isMissing,
|
||||
String? adjacentTo,
|
||||
String? startItemId,
|
||||
int? startIndex,
|
||||
int? limit,
|
||||
bool? enableImages,
|
||||
int? imageTypeLimit,
|
||||
List<ImageType>? enableImageTypes,
|
||||
bool? enableUserData,
|
||||
ShowsSeriesIdEpisodesGetSortBy? sortBy,
|
||||
}) async {
|
||||
return api.showsSeriesIdEpisodesGet(
|
||||
seriesId: seriesId,
|
||||
userId: account?.id,
|
||||
fields: fields,
|
||||
isMissing: isMissing,
|
||||
limit: limit,
|
||||
sortBy: sortBy,
|
||||
enableUserData: enableUserData,
|
||||
startIndex: startIndex,
|
||||
adjacentTo: adjacentTo,
|
||||
startItemId: startItemId,
|
||||
season: season,
|
||||
seasonId: seasonId,
|
||||
enableImages: enableImages,
|
||||
enableImageTypes: enableImageTypes,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ItemBaseModel>> fetchEpisodeFromShow({
|
||||
required String? seriesId,
|
||||
String? seasonId,
|
||||
}) async {
|
||||
final response = await showsSeriesIdEpisodesGet(seriesId: seriesId, seasonId: seasonId);
|
||||
return response.body?.items?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [];
|
||||
}
|
||||
|
||||
Future<Response<BaseItemDtoQueryResult>> itemsItemIdSimilarGet({
|
||||
String? itemId,
|
||||
int? limit,
|
||||
}) async {
|
||||
return api.itemsItemIdSimilarGet(
|
||||
userId: account?.id,
|
||||
itemId: itemId,
|
||||
limit: limit,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<BaseItemDtoQueryResult>> usersUserIdItemsGet({
|
||||
String? parentId,
|
||||
bool? recursive,
|
||||
List<BaseItemKind>? includeItemTypes,
|
||||
}) async {
|
||||
return api.itemsGet(
|
||||
parentId: parentId,
|
||||
userId: account?.id,
|
||||
recursive: recursive,
|
||||
includeItemTypes: includeItemTypes,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<dynamic>> playlistsPlaylistIdItemsPost({
|
||||
String? playlistId,
|
||||
List<String>? ids,
|
||||
}) async {
|
||||
return api.playlistsPlaylistIdItemsPost(
|
||||
playlistId: playlistId,
|
||||
ids: ids,
|
||||
userId: account?.id,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<dynamic>> playlistsPost({
|
||||
String? name,
|
||||
List<String>? ids,
|
||||
required CreatePlaylistDto? body,
|
||||
}) async {
|
||||
return api.playlistsPost(
|
||||
name: name,
|
||||
ids: ids,
|
||||
userId: account?.id,
|
||||
body: body,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<List<AccountModel>>> usersPublicGet(
|
||||
CredentialsModel credentials,
|
||||
) async {
|
||||
final response = await api.usersPublicGet();
|
||||
return response.copyWith(
|
||||
body: response.body?.map(
|
||||
(e) {
|
||||
var imageUrl = ref.read(imageUtilityProvider).getUserImageUrl(e.id ?? "");
|
||||
return AccountModel(
|
||||
name: e.name ?? "",
|
||||
credentials: credentials,
|
||||
id: e.id ?? "",
|
||||
avatar: imageUrl,
|
||||
lastUsed: DateTime.now(),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<AuthenticationResult>> usersAuthenticateByNamePost({
|
||||
required String userName,
|
||||
required String password,
|
||||
}) async {
|
||||
return api.usersAuthenticateByNamePost(body: AuthenticateUserByName(username: userName, pw: password));
|
||||
}
|
||||
|
||||
Future<Response<ServerConfiguration>> systemConfigurationGet() => api.systemConfigurationGet();
|
||||
Future<Response<PublicSystemInfo>> systemInfoPublicGet() => api.systemInfoPublicGet();
|
||||
|
||||
Future<Response> sessionsLogoutPost() => api.sessionsLogoutPost();
|
||||
|
||||
Future<Response<String>> itemsItemIdDownloadGet({
|
||||
String? itemId,
|
||||
}) =>
|
||||
api.itemsItemIdDownloadGet(itemId: itemId);
|
||||
|
||||
Future<Response> collectionsCollectionIdItemsPost({required String? collectionId, required List<String>? ids}) =>
|
||||
api.collectionsCollectionIdItemsPost(collectionId: collectionId, ids: ids);
|
||||
Future<Response> collectionsCollectionIdItemsDelete({required String? collectionId, required List<String>? ids}) =>
|
||||
api.collectionsCollectionIdItemsDelete(collectionId: collectionId, ids: ids);
|
||||
|
||||
Future<Response> collectionsPost({String? name, List<String>? ids, String? parentId, bool? isLocked}) =>
|
||||
api.collectionsPost(name: name, ids: ids, parentId: parentId, isLocked: isLocked);
|
||||
|
||||
Future<Response<BaseItemDtoQueryResult>> usersUserIdViewsGet({
|
||||
bool? includeExternalContent,
|
||||
List<CollectionType>? presetViews,
|
||||
bool? includeHidden,
|
||||
}) =>
|
||||
api.userViewsGet(
|
||||
userId: account?.id,
|
||||
includeExternalContent: includeExternalContent,
|
||||
presetViews: presetViews,
|
||||
includeHidden: includeHidden);
|
||||
|
||||
Future<Response<List<ExternalIdInfo>>> itemsItemIdExternalIdInfosGet({required String? itemId}) =>
|
||||
api.itemsItemIdExternalIdInfosGet(itemId: itemId);
|
||||
|
||||
Future<Response<List<RemoteSearchResult>>> itemsRemoteSearchSeriesPost(
|
||||
{required SeriesInfoRemoteSearchQuery? body}) =>
|
||||
api.itemsRemoteSearchSeriesPost(body: body);
|
||||
|
||||
Future<Response<List<RemoteSearchResult>>> itemsRemoteSearchMoviePost({required MovieInfoRemoteSearchQuery? body}) =>
|
||||
api.itemsRemoteSearchMoviePost(body: body);
|
||||
|
||||
Future<Response<dynamic>> itemsRemoteSearchApplyItemIdPost({
|
||||
required String? itemId,
|
||||
bool? replaceAllImages,
|
||||
required RemoteSearchResult? body,
|
||||
}) =>
|
||||
api.itemsRemoteSearchApplyItemIdPost(
|
||||
itemId: itemId,
|
||||
replaceAllImages: replaceAllImages,
|
||||
body: body,
|
||||
);
|
||||
|
||||
Future<Response<BaseItemDtoQueryResult>> showsSeriesIdSeasonsGet({
|
||||
required String? seriesId,
|
||||
bool? enableUserData,
|
||||
bool? isMissing,
|
||||
List<ItemFields>? fields,
|
||||
}) =>
|
||||
api.showsSeriesIdSeasonsGet(
|
||||
seriesId: seriesId,
|
||||
isMissing: isMissing,
|
||||
enableUserData: enableUserData,
|
||||
fields: fields,
|
||||
);
|
||||
|
||||
Future<Response<QueryFilters>> itemsFilters2Get({
|
||||
String? parentId,
|
||||
List<BaseItemKind>? includeItemTypes,
|
||||
bool? isAiring,
|
||||
bool? isMovie,
|
||||
bool? isSports,
|
||||
bool? isKids,
|
||||
bool? isNews,
|
||||
bool? isSeries,
|
||||
bool? recursive,
|
||||
}) =>
|
||||
api.itemsFilters2Get(
|
||||
parentId: parentId,
|
||||
includeItemTypes: includeItemTypes,
|
||||
isAiring: isAiring,
|
||||
isMovie: isMovie,
|
||||
isSports: isSports,
|
||||
isKids: isKids,
|
||||
isNews: isNews,
|
||||
isSeries: isSeries,
|
||||
recursive: recursive,
|
||||
);
|
||||
|
||||
Future<Response<BaseItemDtoQueryResult>> studiosGet({
|
||||
int? startIndex,
|
||||
int? limit,
|
||||
String? searchTerm,
|
||||
String? parentId,
|
||||
List<ItemFields>? fields,
|
||||
List<BaseItemKind>? excludeItemTypes,
|
||||
List<BaseItemKind>? includeItemTypes,
|
||||
bool? isFavorite,
|
||||
bool? enableUserData,
|
||||
int? imageTypeLimit,
|
||||
List<ImageType>? enableImageTypes,
|
||||
String? userId,
|
||||
String? nameStartsWithOrGreater,
|
||||
String? nameStartsWith,
|
||||
String? nameLessThan,
|
||||
bool? enableImages,
|
||||
bool? enableTotalRecordCount,
|
||||
}) =>
|
||||
api.studiosGet(
|
||||
startIndex: startIndex,
|
||||
limit: limit,
|
||||
searchTerm: searchTerm,
|
||||
parentId: parentId,
|
||||
fields: fields,
|
||||
excludeItemTypes: excludeItemTypes,
|
||||
includeItemTypes: includeItemTypes,
|
||||
isFavorite: isFavorite,
|
||||
enableUserData: enableUserData,
|
||||
imageTypeLimit: imageTypeLimit,
|
||||
enableImageTypes: enableImageTypes,
|
||||
nameStartsWithOrGreater: nameStartsWithOrGreater,
|
||||
nameStartsWith: nameStartsWith,
|
||||
nameLessThan: nameLessThan,
|
||||
enableImages: enableImages,
|
||||
enableTotalRecordCount: enableTotalRecordCount,
|
||||
);
|
||||
|
||||
Future<Response<ServerQueryResult>> playlistsPlaylistIdItemsGet({
|
||||
required String? playlistId,
|
||||
int? startIndex,
|
||||
int? limit,
|
||||
List<ItemFields>? fields,
|
||||
bool? enableImages,
|
||||
bool? enableUserData,
|
||||
int? imageTypeLimit,
|
||||
List<ImageType>? enableImageTypes,
|
||||
}) async {
|
||||
final response = await api.playlistsPlaylistIdItemsGet(
|
||||
playlistId: playlistId,
|
||||
userId: account?.id,
|
||||
startIndex: startIndex,
|
||||
limit: limit,
|
||||
fields: fields,
|
||||
enableImages: enableImages,
|
||||
enableUserData: enableUserData,
|
||||
imageTypeLimit: imageTypeLimit,
|
||||
enableImageTypes: enableImageTypes,
|
||||
);
|
||||
return response.copyWith(
|
||||
body: ServerQueryResult.fromBaseQuery(response.bodyOrThrow, ref),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> playlistsPlaylistIdItemsDelete({required String? playlistId, List<String>? entryIds}) =>
|
||||
api.playlistsPlaylistIdItemsDelete(
|
||||
playlistId: playlistId,
|
||||
entryIds: entryIds,
|
||||
);
|
||||
|
||||
Future<Response<UserDto>> usersMeGet() => api.usersMeGet();
|
||||
|
||||
Future<Response> configuration() => api.systemConfigurationGet();
|
||||
|
||||
Future<Response> itemsItemIdRefreshPost({
|
||||
required String? itemId,
|
||||
MetadataRefresh? metadataRefreshMode,
|
||||
MetadataRefresh? imageRefreshMode,
|
||||
bool? replaceAllMetadata,
|
||||
bool? replaceAllImages,
|
||||
}) =>
|
||||
api.itemsItemIdRefreshPost(
|
||||
itemId: itemId,
|
||||
metadataRefreshMode: metadataRefreshMode?.metadataRefreshMode,
|
||||
imageRefreshMode: imageRefreshMode?.imageRefreshMode,
|
||||
replaceAllMetadata: replaceAllMetadata,
|
||||
replaceAllImages: replaceAllImages,
|
||||
);
|
||||
|
||||
Future<Response<UserItemDataDto>> usersUserIdFavoriteItemsItemIdPost({
|
||||
required String? itemId,
|
||||
}) =>
|
||||
api.userFavoriteItemsItemIdPost(
|
||||
itemId: itemId,
|
||||
userId: account?.id,
|
||||
);
|
||||
|
||||
Future<Response<UserItemDataDto>> usersUserIdFavoriteItemsItemIdDelete({
|
||||
required String? itemId,
|
||||
}) =>
|
||||
api.userFavoriteItemsItemIdDelete(
|
||||
itemId: itemId,
|
||||
userId: account?.id,
|
||||
);
|
||||
|
||||
Future<Response<UserItemDataDto>> usersUserIdPlayedItemsItemIdPost({
|
||||
required String? itemId,
|
||||
DateTime? datePlayed,
|
||||
}) =>
|
||||
api.userPlayedItemsItemIdPost(itemId: itemId, userId: account?.id, datePlayed: datePlayed);
|
||||
|
||||
Future<Response<UserItemDataDto>> usersUserIdPlayedItemsItemIdDelete({
|
||||
required String? itemId,
|
||||
}) =>
|
||||
api.userPlayedItemsItemIdDelete(
|
||||
itemId: itemId,
|
||||
userId: account?.id,
|
||||
);
|
||||
|
||||
Future<Response<IntroOutSkipModel>?> introSkipGet({
|
||||
required String id,
|
||||
}) async {
|
||||
try {
|
||||
final response = await api.episodeIdIntroTimestampsGet(id: id);
|
||||
final outro = await api.episodeIdIntroSkipperSegmentsGet(id: id);
|
||||
final map = jsonDecode(outro.bodyString) as Map<String, dynamic>;
|
||||
final newModel = IntroOutSkipModel(
|
||||
intro:
|
||||
map["Introduction"] != null ? IntroSkipModel.fromJson(map["Introduction"] as Map<String, dynamic>) : null,
|
||||
credits: map["Credits"] != null ? IntroSkipModel.fromJson(map["Credits"] as Map<String, dynamic>) : null,
|
||||
);
|
||||
return response.copyWith(
|
||||
body: newModel,
|
||||
);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Response<TrickPlayModel>?> getTrickPlay({
|
||||
required ItemBaseModel? item,
|
||||
int? width,
|
||||
required Ref ref,
|
||||
}) async {
|
||||
try {
|
||||
if (item == null) return null;
|
||||
if (item.overview.trickPlayInfo?.isEmpty == true) {
|
||||
return null;
|
||||
}
|
||||
final trickPlayModel = item.overview.trickPlayInfo?.values.lastOrNull;
|
||||
if (trickPlayModel == null) return null;
|
||||
final response = await api.videosItemIdTrickplayWidthTilesM3u8Get(
|
||||
itemId: item.id,
|
||||
width: trickPlayModel.width,
|
||||
);
|
||||
|
||||
final server = ref.read(userProvider)?.server;
|
||||
|
||||
if (server == null) return null;
|
||||
|
||||
final lines = response.bodyString.split('\n')
|
||||
..removeWhere((element) => element.startsWith('#') || !element.contains('.jpg'));
|
||||
return response.copyWith(
|
||||
body: trickPlayModel.copyWith(
|
||||
images: lines
|
||||
.map(
|
||||
(e) => joinAll([server, 'Videos/${item.id}/Trickplay/${trickPlayModel.width}', e]),
|
||||
)
|
||||
.toList()));
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Response<List<SessionInfo>>> sessionsInfo(String deviceId) async => api.sessionsGet(deviceId: deviceId);
|
||||
|
||||
Future<Response<bool>> quickConnect(String code) async => api.quickConnectAuthorizePost(code: code);
|
||||
|
||||
Future<Response<bool>> quickConnectEnabled() async => api.quickConnectEnabledGet();
|
||||
|
||||
Future<Response<dynamic>> deleteItem(String itemId) => api.itemsItemIdDelete(itemId: itemId);
|
||||
}
|
||||
64
lib/providers/session_info_provider.dart
Normal file
64
lib/providers/session_info_provider.dart
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'session_info_provider.freezed.dart';
|
||||
part 'session_info_provider.g.dart';
|
||||
|
||||
@Riverpod()
|
||||
class SessionInfo extends _$SessionInfo {
|
||||
late final api = ref.read(jellyApiProvider);
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
SessionInfoModel build() {
|
||||
ref.onDispose(() => _timer?.cancel());
|
||||
_startTimer();
|
||||
return SessionInfoModel();
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
_fetchData();
|
||||
_timer = Timer.periodic(Duration(seconds: 2), (timer) async {
|
||||
await _fetchData();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _fetchData() async {
|
||||
final deviceId = ref.read(userProvider)?.credentials.deviceId;
|
||||
if (deviceId == null) {
|
||||
state = SessionInfoModel();
|
||||
return;
|
||||
}
|
||||
|
||||
final response = await api.sessionsInfo(deviceId);
|
||||
|
||||
final session = response.body?.firstOrNull;
|
||||
|
||||
if (session == null) {
|
||||
state = SessionInfoModel();
|
||||
return;
|
||||
}
|
||||
|
||||
state = SessionInfoModel(
|
||||
playbackModel: session.playState?.playMethod?.name,
|
||||
transCodeInfo: session.transcodingInfo,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SessionInfoModel with _$SessionInfoModel {
|
||||
const SessionInfoModel._();
|
||||
|
||||
factory SessionInfoModel({
|
||||
String? playbackModel,
|
||||
TranscodingInfo? transCodeInfo,
|
||||
}) = _SessionInfoModel;
|
||||
|
||||
factory SessionInfoModel.fromJson(Map<String, dynamic> json) => _$SessionInfoModelFromJson(json);
|
||||
}
|
||||
173
lib/providers/session_info_provider.freezed.dart
Normal file
173
lib/providers/session_info_provider.freezed.dart
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
// 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 'session_info_provider.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');
|
||||
|
||||
SessionInfoModel _$SessionInfoModelFromJson(Map<String, dynamic> json) {
|
||||
return _SessionInfoModel.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SessionInfoModel {
|
||||
String? get playbackModel => throw _privateConstructorUsedError;
|
||||
TranscodingInfo? get transCodeInfo => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$SessionInfoModelCopyWith<SessionInfoModel> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SessionInfoModelCopyWith<$Res> {
|
||||
factory $SessionInfoModelCopyWith(
|
||||
SessionInfoModel value, $Res Function(SessionInfoModel) then) =
|
||||
_$SessionInfoModelCopyWithImpl<$Res, SessionInfoModel>;
|
||||
@useResult
|
||||
$Res call({String? playbackModel, TranscodingInfo? transCodeInfo});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SessionInfoModelCopyWithImpl<$Res, $Val extends SessionInfoModel>
|
||||
implements $SessionInfoModelCopyWith<$Res> {
|
||||
_$SessionInfoModelCopyWithImpl(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? playbackModel = freezed,
|
||||
Object? transCodeInfo = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
playbackModel: freezed == playbackModel
|
||||
? _value.playbackModel
|
||||
: playbackModel // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
transCodeInfo: freezed == transCodeInfo
|
||||
? _value.transCodeInfo
|
||||
: transCodeInfo // ignore: cast_nullable_to_non_nullable
|
||||
as TranscodingInfo?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SessionInfoModelImplCopyWith<$Res>
|
||||
implements $SessionInfoModelCopyWith<$Res> {
|
||||
factory _$$SessionInfoModelImplCopyWith(_$SessionInfoModelImpl value,
|
||||
$Res Function(_$SessionInfoModelImpl) then) =
|
||||
__$$SessionInfoModelImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({String? playbackModel, TranscodingInfo? transCodeInfo});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SessionInfoModelImplCopyWithImpl<$Res>
|
||||
extends _$SessionInfoModelCopyWithImpl<$Res, _$SessionInfoModelImpl>
|
||||
implements _$$SessionInfoModelImplCopyWith<$Res> {
|
||||
__$$SessionInfoModelImplCopyWithImpl(_$SessionInfoModelImpl _value,
|
||||
$Res Function(_$SessionInfoModelImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? playbackModel = freezed,
|
||||
Object? transCodeInfo = freezed,
|
||||
}) {
|
||||
return _then(_$SessionInfoModelImpl(
|
||||
playbackModel: freezed == playbackModel
|
||||
? _value.playbackModel
|
||||
: playbackModel // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
transCodeInfo: freezed == transCodeInfo
|
||||
? _value.transCodeInfo
|
||||
: transCodeInfo // ignore: cast_nullable_to_non_nullable
|
||||
as TranscodingInfo?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SessionInfoModelImpl extends _SessionInfoModel {
|
||||
_$SessionInfoModelImpl({this.playbackModel, this.transCodeInfo}) : super._();
|
||||
|
||||
factory _$SessionInfoModelImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SessionInfoModelImplFromJson(json);
|
||||
|
||||
@override
|
||||
final String? playbackModel;
|
||||
@override
|
||||
final TranscodingInfo? transCodeInfo;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SessionInfoModel(playbackModel: $playbackModel, transCodeInfo: $transCodeInfo)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SessionInfoModelImpl &&
|
||||
(identical(other.playbackModel, playbackModel) ||
|
||||
other.playbackModel == playbackModel) &&
|
||||
(identical(other.transCodeInfo, transCodeInfo) ||
|
||||
other.transCodeInfo == transCodeInfo));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, playbackModel, transCodeInfo);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SessionInfoModelImplCopyWith<_$SessionInfoModelImpl> get copyWith =>
|
||||
__$$SessionInfoModelImplCopyWithImpl<_$SessionInfoModelImpl>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SessionInfoModelImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SessionInfoModel extends SessionInfoModel {
|
||||
factory _SessionInfoModel(
|
||||
{final String? playbackModel,
|
||||
final TranscodingInfo? transCodeInfo}) = _$SessionInfoModelImpl;
|
||||
_SessionInfoModel._() : super._();
|
||||
|
||||
factory _SessionInfoModel.fromJson(Map<String, dynamic> json) =
|
||||
_$SessionInfoModelImpl.fromJson;
|
||||
|
||||
@override
|
||||
String? get playbackModel;
|
||||
@override
|
||||
TranscodingInfo? get transCodeInfo;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$SessionInfoModelImplCopyWith<_$SessionInfoModelImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
46
lib/providers/session_info_provider.g.dart
Normal file
46
lib/providers/session_info_provider.g.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'session_info_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$SessionInfoModelImpl _$$SessionInfoModelImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$SessionInfoModelImpl(
|
||||
playbackModel: json['playbackModel'] as String?,
|
||||
transCodeInfo: json['transCodeInfo'] == null
|
||||
? null
|
||||
: TranscodingInfo.fromJson(
|
||||
json['transCodeInfo'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SessionInfoModelImplToJson(
|
||||
_$SessionInfoModelImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'playbackModel': instance.playbackModel,
|
||||
'transCodeInfo': instance.transCodeInfo,
|
||||
};
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$sessionInfoHash() => r'ab5afcada1c9677cadda954c9abf7eb361dc057d';
|
||||
|
||||
/// See also [SessionInfo].
|
||||
@ProviderFor(SessionInfo)
|
||||
final sessionInfoProvider =
|
||||
AutoDisposeNotifierProvider<SessionInfo, SessionInfoModel>.internal(
|
||||
SessionInfo.new,
|
||||
name: r'sessionInfoProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$sessionInfoHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$SessionInfo = AutoDisposeNotifier<SessionInfoModel>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
140
lib/providers/settings/book_viewer_settings_provider.dart
Normal file
140
lib/providers/settings/book_viewer_settings_provider.dart
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
|
||||
import 'package:fladder/providers/shared_provider.dart';
|
||||
import 'package:fladder/util/debouncer.dart';
|
||||
|
||||
enum ReadDirection {
|
||||
leftToRight,
|
||||
rightToLeft;
|
||||
|
||||
String toMap() => name;
|
||||
static ReadDirection fromMap(String? map) {
|
||||
return ReadDirection.values.firstWhereOrNull((element) => element.name == map) ?? ReadDirection.leftToRight;
|
||||
}
|
||||
}
|
||||
|
||||
enum InitZoomState {
|
||||
contained,
|
||||
covered;
|
||||
|
||||
String toMap() => name;
|
||||
|
||||
static InitZoomState fromMap(String? map) {
|
||||
return InitZoomState.values.firstWhereOrNull((element) => element.name == map) ?? InitZoomState.contained;
|
||||
}
|
||||
}
|
||||
|
||||
enum GestureCache {
|
||||
contained,
|
||||
covered;
|
||||
|
||||
String toMap() => name;
|
||||
|
||||
static InitZoomState fromMap(String? map) {
|
||||
return InitZoomState.values.firstWhereOrNull((element) => element.name == map) ?? InitZoomState.contained;
|
||||
}
|
||||
}
|
||||
|
||||
class BookViewerSettingsModel {
|
||||
final double? screenBrightness;
|
||||
final ReadDirection readDirection;
|
||||
final InitZoomState initZoomState;
|
||||
final bool cachePageZoom;
|
||||
final bool keepPageZoom;
|
||||
final bool disableScrollOnZoom;
|
||||
|
||||
BookViewerSettingsModel({
|
||||
this.screenBrightness,
|
||||
this.readDirection = ReadDirection.leftToRight,
|
||||
this.initZoomState = InitZoomState.contained,
|
||||
this.cachePageZoom = false,
|
||||
this.keepPageZoom = true,
|
||||
this.disableScrollOnZoom = false,
|
||||
});
|
||||
|
||||
BookViewerSettingsModel copyWith({
|
||||
ValueGetter<double?>? screenBrightness,
|
||||
ReadDirection? readDirection,
|
||||
InitZoomState? initZoomState,
|
||||
bool? cachePageZoom,
|
||||
bool? keepPageZoom,
|
||||
bool? disableScrollOnZoom,
|
||||
}) {
|
||||
return BookViewerSettingsModel(
|
||||
screenBrightness: screenBrightness != null ? screenBrightness.call() : this.screenBrightness,
|
||||
readDirection: readDirection ?? this.readDirection,
|
||||
initZoomState: initZoomState ?? this.initZoomState,
|
||||
cachePageZoom: cachePageZoom ?? this.cachePageZoom,
|
||||
keepPageZoom: keepPageZoom ?? this.keepPageZoom,
|
||||
disableScrollOnZoom: disableScrollOnZoom ?? this.disableScrollOnZoom,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'screenBrightness': screenBrightness,
|
||||
'readDirection': readDirection.toMap(),
|
||||
'initZoomState': initZoomState.toMap(),
|
||||
'cachePageZoom': cachePageZoom,
|
||||
'keepPageZoom': keepPageZoom,
|
||||
'disableScrollOnZoom': disableScrollOnZoom,
|
||||
};
|
||||
}
|
||||
|
||||
factory BookViewerSettingsModel.fromMap(Map<String, dynamic> map) {
|
||||
return BookViewerSettingsModel(
|
||||
screenBrightness: map['screenBrightness']?.toDouble(),
|
||||
readDirection: ReadDirection.fromMap(map['readDirection']),
|
||||
initZoomState: InitZoomState.fromMap(map['initZoomState']),
|
||||
cachePageZoom: map['cachePageZoom'] ?? false,
|
||||
keepPageZoom: map['keepPageZoom'] ?? true,
|
||||
disableScrollOnZoom: map['disableScrollOnZoom'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory BookViewerSettingsModel.fromJson(String source) => BookViewerSettingsModel.fromMap(json.decode(source));
|
||||
}
|
||||
|
||||
final bookViewerSettingsProvider = StateNotifierProvider<BookViewerSettingsNotifier, BookViewerSettingsModel>((ref) {
|
||||
return BookViewerSettingsNotifier(ref);
|
||||
});
|
||||
|
||||
class BookViewerSettingsNotifier extends StateNotifier<BookViewerSettingsModel> {
|
||||
BookViewerSettingsNotifier(this.ref) : super(BookViewerSettingsModel());
|
||||
|
||||
final Ref ref;
|
||||
|
||||
final Debouncer _debouncer = Debouncer(const Duration(seconds: 1));
|
||||
|
||||
@override
|
||||
set state(BookViewerSettingsModel value) {
|
||||
_debouncer.run(() => ref.read(sharedUtilityProvider).bookViewSettings = value);
|
||||
super.state = value;
|
||||
}
|
||||
|
||||
void setScreenBrightness(double? value) async {
|
||||
state = state.copyWith(
|
||||
screenBrightness: () => value,
|
||||
);
|
||||
if (state.screenBrightness != null) {
|
||||
ScreenBrightness().setScreenBrightness(state.screenBrightness!);
|
||||
} else {
|
||||
ScreenBrightness().resetScreenBrightness();
|
||||
}
|
||||
}
|
||||
|
||||
setSavedBrightness() {
|
||||
if (state.screenBrightness != null) {
|
||||
ScreenBrightness().setScreenBrightness(state.screenBrightness!);
|
||||
}
|
||||
}
|
||||
|
||||
void update(BookViewerSettingsModel Function(BookViewerSettingsModel state) outgoing) => state = outgoing(state);
|
||||
}
|
||||
54
lib/providers/settings/client_settings_provider.dart
Normal file
54
lib/providers/settings/client_settings_provider.dart
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import 'package:fladder/models/settings/client_settings_model.dart';
|
||||
import 'package:fladder/providers/shared_provider.dart';
|
||||
import 'package:fladder/util/custom_color_themes.dart';
|
||||
import 'package:fladder/util/debouncer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final clientSettingsProvider = StateNotifierProvider<ClientSettingsNotifier, ClientSettingsModel>((ref) {
|
||||
return ClientSettingsNotifier(ref);
|
||||
});
|
||||
|
||||
class ClientSettingsNotifier extends StateNotifier<ClientSettingsModel> {
|
||||
ClientSettingsNotifier(this.ref) : super(ClientSettingsModel());
|
||||
|
||||
final Ref ref;
|
||||
|
||||
final Debouncer _debouncer = Debouncer(const Duration(seconds: 1));
|
||||
|
||||
@override
|
||||
set state(ClientSettingsModel value) {
|
||||
super.state = value;
|
||||
_debouncer.run(() => ref.read(sharedUtilityProvider).clientSettings = state);
|
||||
}
|
||||
|
||||
void setWindowPosition(Offset windowPosition) =>
|
||||
state = state.copyWith(position: Vector2.fromPosition(windowPosition));
|
||||
|
||||
void setWindowSize(Size windowSize) => state = state.copyWith(size: Vector2.fromSize(windowSize));
|
||||
|
||||
void setThemeMode(ThemeMode? themeMode) {
|
||||
if (themeMode == null) return;
|
||||
state = state.copyWith(themeMode: themeMode);
|
||||
}
|
||||
|
||||
void setThemeColor(ColorThemes? themeColor) => state = state.copyWith(themeColor: themeColor);
|
||||
|
||||
void setAmoledBlack(bool? value) => state = state.copyWith(amoledBlack: value ?? false);
|
||||
|
||||
void setBlurPlaceholders(bool value) => state = state.copyWith(blurPlaceHolders: value);
|
||||
|
||||
void setTimeOut(Duration? duration) => state = state.copyWith(timeOut: duration);
|
||||
|
||||
void setBlurEpisodes(bool value) => state = state.copyWith(blurUpcomingEpisodes: value);
|
||||
|
||||
void setMediaKeys(bool value) => state = state.copyWith(enableMediaKeys: value);
|
||||
|
||||
void setPosterSize(double value) => state = state.copyWith(posterSize: value.clamp(0.5, 1.5));
|
||||
|
||||
void addPosterSize(double value) => state = state.copyWith(posterSize: (state.posterSize + value).clamp(0.5, 1.5));
|
||||
|
||||
void setSyncPath(String? path) => state = state.copyWith(syncPath: path);
|
||||
|
||||
void update(Function(ClientSettingsModel current) value) => state = value(state);
|
||||
}
|
||||
21
lib/providers/settings/home_settings_provider.dart
Normal file
21
lib/providers/settings/home_settings_provider.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/shared_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final homeSettingsProvider = StateNotifierProvider<HomeSettingsNotifier, HomeSettingsModel>((ref) {
|
||||
return HomeSettingsNotifier(ref);
|
||||
});
|
||||
|
||||
class HomeSettingsNotifier extends StateNotifier<HomeSettingsModel> {
|
||||
HomeSettingsNotifier(this.ref) : super(HomeSettingsModel());
|
||||
|
||||
final Ref ref;
|
||||
|
||||
@override
|
||||
set state(HomeSettingsModel value) {
|
||||
super.state = value;
|
||||
ref.read(sharedUtilityProvider).homeSettings = value;
|
||||
}
|
||||
|
||||
update(HomeSettingsModel Function(HomeSettingsModel currentState) value) => state = value(state);
|
||||
}
|
||||
82
lib/providers/settings/photo_view_settings_provider.dart
Normal file
82
lib/providers/settings/photo_view_settings_provider.dart
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/providers/shared_provider.dart';
|
||||
|
||||
class PhotoViewSettingsModel {
|
||||
final bool repeat;
|
||||
final bool mute;
|
||||
final bool autoPlay;
|
||||
final bool theaterMode;
|
||||
final Duration timer;
|
||||
PhotoViewSettingsModel({
|
||||
this.repeat = true,
|
||||
this.mute = false,
|
||||
this.autoPlay = false,
|
||||
this.theaterMode = false,
|
||||
this.timer = const Duration(seconds: 15),
|
||||
});
|
||||
|
||||
PhotoViewSettingsModel copyWith({
|
||||
bool? repeat,
|
||||
bool? mute,
|
||||
bool? autoPlay,
|
||||
bool? theaterMode,
|
||||
Duration? timer,
|
||||
}) {
|
||||
return PhotoViewSettingsModel(
|
||||
repeat: repeat ?? this.repeat,
|
||||
mute: mute ?? this.mute,
|
||||
autoPlay: autoPlay ?? this.autoPlay,
|
||||
theaterMode: theaterMode ?? this.theaterMode,
|
||||
timer: timer ?? this.timer,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'repeat': repeat,
|
||||
'mute': mute,
|
||||
'autoPlay': autoPlay,
|
||||
'theaterMode': theaterMode,
|
||||
'timer': timer.inMilliseconds,
|
||||
};
|
||||
}
|
||||
|
||||
factory PhotoViewSettingsModel.fromMap(Map<String, dynamic> map) {
|
||||
return PhotoViewSettingsModel(
|
||||
repeat: map['repeat'] ?? false,
|
||||
mute: map['mute'] ?? false,
|
||||
autoPlay: map['autoPlay'] ?? false,
|
||||
theaterMode: map['theaterMode'] ?? false,
|
||||
timer: map['timer'] != null ? Duration(milliseconds: map['timer'] as int) : const Duration(seconds: 15),
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory PhotoViewSettingsModel.fromJson(String source) => PhotoViewSettingsModel.fromMap(json.decode(source));
|
||||
}
|
||||
|
||||
final photoViewSettingsProvider = StateNotifierProvider<PhotoViewSettingsNotifier, PhotoViewSettingsModel>((ref) {
|
||||
return PhotoViewSettingsNotifier(ref);
|
||||
});
|
||||
|
||||
final testProviderProvider = StateProvider<int>((ref) {
|
||||
return 0;
|
||||
});
|
||||
|
||||
class PhotoViewSettingsNotifier extends StateNotifier<PhotoViewSettingsModel> {
|
||||
PhotoViewSettingsNotifier(this.ref) : super(PhotoViewSettingsModel());
|
||||
|
||||
final Ref ref;
|
||||
|
||||
@override
|
||||
set state(PhotoViewSettingsModel value) {
|
||||
super.state = value;
|
||||
ref.read(sharedUtilityProvider).photoViewSettings = value;
|
||||
}
|
||||
|
||||
PhotoViewSettingsModel update(PhotoViewSettingsModel Function(PhotoViewSettingsModel state) cb) => state = cb(state);
|
||||
}
|
||||
40
lib/providers/settings/subtitle_settings_provider.dart
Normal file
40
lib/providers/settings/subtitle_settings_provider.dart
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:fladder/models/settings/subtitle_settings_model.dart';
|
||||
import 'package:fladder/providers/shared_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final subtitleSettingsProvider = StateNotifierProvider<SubtitleSettingsNotifier, SubtitleSettingsModel>((ref) {
|
||||
return SubtitleSettingsNotifier(ref);
|
||||
});
|
||||
|
||||
class SubtitleSettingsNotifier extends StateNotifier<SubtitleSettingsModel> {
|
||||
SubtitleSettingsNotifier(this.ref) : super(const SubtitleSettingsModel());
|
||||
|
||||
final Ref ref;
|
||||
|
||||
@override
|
||||
set state(SubtitleSettingsModel value) {
|
||||
super.state = value;
|
||||
ref.read(sharedUtilityProvider).subtitleSettings = value;
|
||||
}
|
||||
|
||||
void setFontSize(double value) => state = state.copyWith(fontSize: value);
|
||||
|
||||
void setVerticalOffset(double value) => state = state.copyWith(verticalOffset: value);
|
||||
|
||||
void setSubColor(Color color) => state = state.copyWith(color: color);
|
||||
|
||||
void setOutlineColor(Color e) => state = state.copyWith(outlineColor: e);
|
||||
|
||||
setOutlineThickness(double value) => state = state.copyWith(outlineSize: value);
|
||||
|
||||
void resetSettings({SubtitleSettingsModel? value}) => state = value ?? const SubtitleSettingsModel();
|
||||
|
||||
void setFontWeight(FontWeight? value) => state = state.copyWith(fontWeight: value);
|
||||
|
||||
setBackGroundOpacity(double value) =>
|
||||
state = state.copyWith(backGroundColor: state.backGroundColor.withOpacity(value));
|
||||
|
||||
setShadowIntensity(double value) => state = state.copyWith(shadow: value);
|
||||
}
|
||||
57
lib/providers/settings/video_player_settings_provider.dart
Normal file
57
lib/providers/settings/video_player_settings_provider.dart
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import 'package:fladder/models/settings/video_player_settings.dart';
|
||||
import 'package:fladder/providers/shared_provider.dart';
|
||||
import 'package:fladder/providers/video_player_provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
|
||||
final videoPlayerSettingsProvider =
|
||||
StateNotifierProvider<VideoPlayerSettingsProviderNotifier, VideoPlayerSettingsModel>((ref) {
|
||||
return VideoPlayerSettingsProviderNotifier(ref);
|
||||
});
|
||||
|
||||
class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSettingsModel> {
|
||||
VideoPlayerSettingsProviderNotifier(this.ref) : super(const VideoPlayerSettingsModel());
|
||||
|
||||
final Ref ref;
|
||||
|
||||
@override
|
||||
set state(VideoPlayerSettingsModel value) {
|
||||
final oldState = super.state;
|
||||
super.state = value;
|
||||
ref.read(sharedUtilityProvider).videoPlayerSettings = value;
|
||||
if (!oldState.playerSame(value)) {
|
||||
ref.read(videoPlayerProvider.notifier).init();
|
||||
}
|
||||
}
|
||||
|
||||
void setScreenBrightness(double? value) async {
|
||||
state = state.copyWith(
|
||||
screenBrightness: () => value,
|
||||
);
|
||||
if (state.screenBrightness != null) {
|
||||
ScreenBrightness().setScreenBrightness(state.screenBrightness!);
|
||||
} else {
|
||||
ScreenBrightness().resetScreenBrightness();
|
||||
}
|
||||
}
|
||||
|
||||
setSavedBrightness() {
|
||||
if (state.screenBrightness != null) {
|
||||
ScreenBrightness().setScreenBrightness(state.screenBrightness!);
|
||||
}
|
||||
}
|
||||
|
||||
void setFillScreen(bool? value, {BuildContext? context}) {
|
||||
state = state.copyWith(fillScreen: value);
|
||||
}
|
||||
|
||||
void setHardwareAccel(bool? value) => state = state.copyWith(hardwareAccel: value);
|
||||
void setUseLibass(bool? value) => state = state.copyWith(useLibass: value);
|
||||
|
||||
void setFitType(BoxFit? value) => state = state.copyWith(videoFit: value);
|
||||
|
||||
void setVolume(double value) => state = state.copyWith(internalVolume: value);
|
||||
|
||||
void steppedVolume(int i) => state = state.copyWith(internalVolume: (state.volume + i).clamp(0, 100));
|
||||
}
|
||||
202
lib/providers/shared_provider.dart
Normal file
202
lib/providers/shared_provider.dart
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fladder/models/account_model.dart';
|
||||
import 'package:fladder/models/settings/client_settings_model.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/models/settings/subtitle_settings_model.dart';
|
||||
import 'package:fladder/models/settings/video_player_settings.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/service_provider.dart';
|
||||
import 'package:fladder/providers/settings/book_viewer_settings_provider.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/providers/settings/home_settings_provider.dart';
|
||||
import 'package:fladder/providers/settings/photo_view_settings_provider.dart';
|
||||
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
|
||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
|
||||
throw UnimplementedError();
|
||||
});
|
||||
|
||||
final sharedUtilityProvider = Provider<SharedUtility>((ref) {
|
||||
final sharedPrefs = ref.watch(sharedPreferencesProvider);
|
||||
return SharedUtility(ref: ref, sharedPreferences: sharedPrefs);
|
||||
});
|
||||
|
||||
class SharedUtility {
|
||||
SharedUtility({
|
||||
required this.ref,
|
||||
required this.sharedPreferences,
|
||||
});
|
||||
|
||||
final Ref ref;
|
||||
|
||||
final SharedPreferences sharedPreferences;
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
Future<bool?> loadSettings() async {
|
||||
try {
|
||||
ref.read(clientSettingsProvider.notifier).state = clientSettings;
|
||||
ref.read(homeSettingsProvider.notifier).state = homeSettings;
|
||||
ref.read(videoPlayerSettingsProvider.notifier).state = videoPlayerSettings;
|
||||
ref.read(subtitleSettingsProvider.notifier).state = subtitleSettings;
|
||||
ref.read(bookViewerSettingsProvider.notifier).state = bookViewSettings;
|
||||
ref.read(photoViewSettingsProvider.notifier).state = photoViewSettings;
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool?> addAccount(AccountModel account) async {
|
||||
return await saveAccounts(getAccounts()
|
||||
..add(account.copyWith(
|
||||
lastUsed: DateTime.now(),
|
||||
)));
|
||||
}
|
||||
|
||||
Future<bool?> removeAccount(AccountModel? account) async {
|
||||
if (account == null) return null;
|
||||
|
||||
//Try to logout user
|
||||
await ref.read(userProvider.notifier).forceLogoutUser(account);
|
||||
|
||||
//Remove from local database
|
||||
final savedAccounts = getAccounts();
|
||||
savedAccounts.removeWhere((element) {
|
||||
return element.sameIdentity(account);
|
||||
});
|
||||
return (await saveAccounts(savedAccounts));
|
||||
}
|
||||
|
||||
List<AccountModel> getAccounts() {
|
||||
final savedAccounts = sharedPreferences.getStringList(_loginCredentialsKey);
|
||||
try {
|
||||
return savedAccounts != null ? savedAccounts.map((e) => AccountModel.fromJson(jsonDecode(e))).toList() : [];
|
||||
} catch (_, stacktrace) {
|
||||
log(stacktrace.toString());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
AccountModel? getActiveAccount() {
|
||||
try {
|
||||
final accounts = getAccounts();
|
||||
AccountModel recentUsedAccount = accounts.reduce((lastLoggedIn, element) {
|
||||
return (element.lastUsed.compareTo(lastLoggedIn.lastUsed)) > 0 ? element : lastLoggedIn;
|
||||
});
|
||||
|
||||
if (recentUsedAccount.authMethod == Authentication.autoLogin) return recentUsedAccount;
|
||||
return null;
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool?> saveAccounts(List<AccountModel> accounts) async =>
|
||||
sharedPreferences.setStringList(_loginCredentialsKey, accounts.map((e) => jsonEncode(e)).toList());
|
||||
|
||||
ClientSettingsModel get clientSettings {
|
||||
try {
|
||||
return ClientSettingsModel.fromJson(jsonDecode(sharedPreferences.getString(_clientSettingsKey) ?? ""));
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
return ClientSettingsModel();
|
||||
}
|
||||
}
|
||||
|
||||
set clientSettings(ClientSettingsModel settings) =>
|
||||
sharedPreferences.setString(_clientSettingsKey, jsonEncode(settings.toJson()));
|
||||
|
||||
HomeSettingsModel get homeSettings {
|
||||
try {
|
||||
return HomeSettingsModel.fromJson(sharedPreferences.getString(_homeSettingsKey) ?? "");
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
return HomeSettingsModel();
|
||||
}
|
||||
}
|
||||
|
||||
set homeSettings(HomeSettingsModel settings) => sharedPreferences.setString(_homeSettingsKey, settings.toJson());
|
||||
|
||||
BookViewerSettingsModel get bookViewSettings {
|
||||
try {
|
||||
return BookViewerSettingsModel.fromJson(sharedPreferences.getString(_bookViewSettingsKey) ?? "");
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
return BookViewerSettingsModel();
|
||||
}
|
||||
}
|
||||
|
||||
set bookViewSettings(BookViewerSettingsModel settings) {
|
||||
sharedPreferences.setString(_bookViewSettingsKey, settings.toJson());
|
||||
}
|
||||
|
||||
Future<void> updateAccountInfo(AccountModel account) async {
|
||||
final accounts = getAccounts();
|
||||
await Future.microtask(() async {
|
||||
await saveAccounts(accounts.map((e) {
|
||||
if (e.sameIdentity(account)) {
|
||||
return account.copyWith(
|
||||
lastUsed: DateTime.now(),
|
||||
);
|
||||
} else {
|
||||
return e;
|
||||
}
|
||||
}).toList());
|
||||
});
|
||||
}
|
||||
|
||||
SubtitleSettingsModel get subtitleSettings {
|
||||
try {
|
||||
return SubtitleSettingsModel.fromJson(sharedPreferences.getString(_subtitleSettingsKey) ?? "");
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
return const SubtitleSettingsModel();
|
||||
}
|
||||
}
|
||||
|
||||
set subtitleSettings(SubtitleSettingsModel settings) {
|
||||
sharedPreferences.setString(_subtitleSettingsKey, settings.toJson());
|
||||
}
|
||||
|
||||
VideoPlayerSettingsModel get videoPlayerSettings {
|
||||
try {
|
||||
return VideoPlayerSettingsModel.fromJson(sharedPreferences.getString(_videoPlayerSettingsKey) ?? "");
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
return const VideoPlayerSettingsModel();
|
||||
}
|
||||
}
|
||||
|
||||
set videoPlayerSettings(VideoPlayerSettingsModel settings) {
|
||||
sharedPreferences.setString(_videoPlayerSettingsKey, settings.toJson());
|
||||
}
|
||||
|
||||
PhotoViewSettingsModel get photoViewSettings {
|
||||
try {
|
||||
return PhotoViewSettingsModel.fromJson(sharedPreferences.getString(_photoViewSettingsKey) ?? "");
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
return PhotoViewSettingsModel();
|
||||
}
|
||||
}
|
||||
|
||||
set photoViewSettings(PhotoViewSettingsModel settings) {
|
||||
sharedPreferences.setString(_photoViewSettingsKey, settings.toJson());
|
||||
}
|
||||
}
|
||||
|
||||
const String _loginCredentialsKey = 'loginCredentialsKey';
|
||||
const String _clientSettingsKey = 'clientSettings';
|
||||
const String _homeSettingsKey = 'homeSettings';
|
||||
const String _videoPlayerSettingsKey = 'videoPlayerSettings';
|
||||
const String _subtitleSettingsKey = 'subtitleSettings';
|
||||
const String _bookViewSettingsKey = 'bookViewSettings';
|
||||
const String _photoViewSettingsKey = 'photoViewSettings';
|
||||
16
lib/providers/sync/background_download_provider.dart
Normal file
16
lib/providers/sync/background_download_provider.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'background_download_provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
FileDownloader backgroundDownloader(BackgroundDownloaderRef ref) {
|
||||
return FileDownloader()
|
||||
..trackTasks()
|
||||
..configureNotification(
|
||||
running: TaskNotification('Downloading', 'file: {filename}'),
|
||||
complete: TaskNotification('Download finished', 'file: {filename}'),
|
||||
paused: TaskNotification('Download paused', 'file: {filename}'),
|
||||
progressBar: true,
|
||||
);
|
||||
}
|
||||
26
lib/providers/sync/background_download_provider.g.dart
Normal file
26
lib/providers/sync/background_download_provider.g.dart
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'background_download_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$backgroundDownloaderHash() =>
|
||||
r'2bc7a06682cdcfa9a754dce9b7f7ea48f873682e';
|
||||
|
||||
/// See also [backgroundDownloader].
|
||||
@ProviderFor(backgroundDownloader)
|
||||
final backgroundDownloaderProvider = Provider<FileDownloader>.internal(
|
||||
backgroundDownloader,
|
||||
name: r'backgroundDownloaderProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$backgroundDownloaderHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef BackgroundDownloaderRef = ProviderRef<FileDownloader>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
104
lib/providers/sync/sync_provider_helpers.dart
Normal file
104
lib/providers/sync/sync_provider_helpers.dart
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import 'package:fladder/models/syncing/i_synced_item.dart';
|
||||
import 'package:fladder/providers/sync_provider.dart';
|
||||
|
||||
import 'package:fladder/models/syncing/download_stream.dart';
|
||||
import 'package:fladder/models/syncing/sync_item.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'sync_provider_helpers.g.dart';
|
||||
|
||||
@riverpod
|
||||
class SyncChildren extends _$SyncChildren {
|
||||
@override
|
||||
List<SyncedItem> build(SyncedItem arg) {
|
||||
final syncedItemIsar = ref.watch(syncProvider.notifier).isar;
|
||||
final allChildren = <SyncedItem>[];
|
||||
List<SyncedItem> toProcess = [arg];
|
||||
while (toProcess.isNotEmpty) {
|
||||
final currentLevel = toProcess.map(
|
||||
(parent) {
|
||||
final children = syncedItemIsar?.iSyncedItems.where().parentIdEqualTo(parent.id).sortBySortKey().findAll();
|
||||
return children?.map((e) => SyncedItem.fromIsar(e, ref.read(syncProvider.notifier).syncPath ?? "")) ??
|
||||
<SyncedItem>[];
|
||||
},
|
||||
);
|
||||
allChildren.addAll(currentLevel.expand((list) => list));
|
||||
toProcess = currentLevel.expand((list) => list).toList();
|
||||
}
|
||||
return allChildren;
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class SyncDownloadStatus extends _$SyncDownloadStatus {
|
||||
@override
|
||||
DownloadStream? build(SyncedItem arg) {
|
||||
final nestedChildren = ref.watch(syncChildrenProvider(arg));
|
||||
|
||||
ref.watch(downloadTasksProvider(arg.id));
|
||||
for (var element in nestedChildren) {
|
||||
ref.watch(downloadTasksProvider(element.id));
|
||||
}
|
||||
|
||||
DownloadStream mainStream = ref.read(downloadTasksProvider(arg.id));
|
||||
int downloadCount = 0;
|
||||
double fullProgress = mainStream.hasDownload ? mainStream.progress : 0.0;
|
||||
|
||||
for (var i = 0; i < nestedChildren.length; i++) {
|
||||
final childItem = nestedChildren[i];
|
||||
final downloadStream = ref.read(downloadTasksProvider(childItem.id));
|
||||
if (downloadStream.hasDownload) {
|
||||
downloadCount++;
|
||||
fullProgress += downloadStream.progress;
|
||||
mainStream = mainStream.copyWith(status: downloadStream.status);
|
||||
}
|
||||
}
|
||||
|
||||
return mainStream.copyWith(
|
||||
progress: fullProgress / downloadCount.clamp(1, double.infinity).toInt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class SyncStatuses extends _$SyncStatuses {
|
||||
@override
|
||||
FutureOr<SyncStatus> build(SyncedItem arg) async {
|
||||
final nestedChildren = ref.watch(syncChildrenProvider(arg));
|
||||
|
||||
ref.watch(downloadTasksProvider(arg.id));
|
||||
for (var element in nestedChildren) {
|
||||
ref.watch(downloadTasksProvider(element.id));
|
||||
}
|
||||
|
||||
for (var i = 0; i < nestedChildren.length; i++) {
|
||||
final item = nestedChildren[i];
|
||||
if (item.hasVideoFile && !await item.videoFile.exists()) {
|
||||
return SyncStatus.partially;
|
||||
}
|
||||
}
|
||||
if (arg.hasVideoFile && !await arg.videoFile.exists()) {
|
||||
return SyncStatus.partially;
|
||||
}
|
||||
return SyncStatus.complete;
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class SyncSize extends _$SyncSize {
|
||||
@override
|
||||
int? build(SyncedItem arg) {
|
||||
final nestedChildren = ref.watch(syncChildrenProvider(arg));
|
||||
|
||||
ref.watch(downloadTasksProvider(arg.id));
|
||||
for (var element in nestedChildren) {
|
||||
ref.watch(downloadTasksProvider(element.id));
|
||||
}
|
||||
int size = arg.fileSize ?? 0;
|
||||
for (var element in nestedChildren) {
|
||||
size += element.fileSize ?? 0;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
}
|
||||
603
lib/providers/sync/sync_provider_helpers.g.dart
Normal file
603
lib/providers/sync/sync_provider_helpers.g.dart
Normal file
|
|
@ -0,0 +1,603 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'sync_provider_helpers.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$syncChildrenHash() => r'798a9998103adae18a238a69219559f3ccbf3def';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$SyncChildren
|
||||
extends BuildlessAutoDisposeNotifier<List<SyncedItem>> {
|
||||
late final SyncedItem arg;
|
||||
|
||||
List<SyncedItem> build(
|
||||
SyncedItem arg,
|
||||
);
|
||||
}
|
||||
|
||||
/// See also [SyncChildren].
|
||||
@ProviderFor(SyncChildren)
|
||||
const syncChildrenProvider = SyncChildrenFamily();
|
||||
|
||||
/// See also [SyncChildren].
|
||||
class SyncChildrenFamily extends Family<List<SyncedItem>> {
|
||||
/// See also [SyncChildren].
|
||||
const SyncChildrenFamily();
|
||||
|
||||
/// See also [SyncChildren].
|
||||
SyncChildrenProvider call(
|
||||
SyncedItem arg,
|
||||
) {
|
||||
return SyncChildrenProvider(
|
||||
arg,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
SyncChildrenProvider getProviderOverride(
|
||||
covariant SyncChildrenProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.arg,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'syncChildrenProvider';
|
||||
}
|
||||
|
||||
/// See also [SyncChildren].
|
||||
class SyncChildrenProvider
|
||||
extends AutoDisposeNotifierProviderImpl<SyncChildren, List<SyncedItem>> {
|
||||
/// See also [SyncChildren].
|
||||
SyncChildrenProvider(
|
||||
SyncedItem arg,
|
||||
) : this._internal(
|
||||
() => SyncChildren()..arg = arg,
|
||||
from: syncChildrenProvider,
|
||||
name: r'syncChildrenProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$syncChildrenHash,
|
||||
dependencies: SyncChildrenFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
SyncChildrenFamily._allTransitiveDependencies,
|
||||
arg: arg,
|
||||
);
|
||||
|
||||
SyncChildrenProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.arg,
|
||||
}) : super.internal();
|
||||
|
||||
final SyncedItem arg;
|
||||
|
||||
@override
|
||||
List<SyncedItem> runNotifierBuild(
|
||||
covariant SyncChildren notifier,
|
||||
) {
|
||||
return notifier.build(
|
||||
arg,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(SyncChildren Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: SyncChildrenProvider._internal(
|
||||
() => create()..arg = arg,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
arg: arg,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeNotifierProviderElement<SyncChildren, List<SyncedItem>>
|
||||
createElement() {
|
||||
return _SyncChildrenProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is SyncChildrenProvider && other.arg == arg;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, arg.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin SyncChildrenRef on AutoDisposeNotifierProviderRef<List<SyncedItem>> {
|
||||
/// The parameter `arg` of this provider.
|
||||
SyncedItem get arg;
|
||||
}
|
||||
|
||||
class _SyncChildrenProviderElement
|
||||
extends AutoDisposeNotifierProviderElement<SyncChildren, List<SyncedItem>>
|
||||
with SyncChildrenRef {
|
||||
_SyncChildrenProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
SyncedItem get arg => (origin as SyncChildrenProvider).arg;
|
||||
}
|
||||
|
||||
String _$syncDownloadStatusHash() =>
|
||||
r'5a0f8537a977c52e6083bd84265631ea5d160637';
|
||||
|
||||
abstract class _$SyncDownloadStatus
|
||||
extends BuildlessAutoDisposeNotifier<DownloadStream?> {
|
||||
late final SyncedItem arg;
|
||||
|
||||
DownloadStream? build(
|
||||
SyncedItem arg,
|
||||
);
|
||||
}
|
||||
|
||||
/// See also [SyncDownloadStatus].
|
||||
@ProviderFor(SyncDownloadStatus)
|
||||
const syncDownloadStatusProvider = SyncDownloadStatusFamily();
|
||||
|
||||
/// See also [SyncDownloadStatus].
|
||||
class SyncDownloadStatusFamily extends Family<DownloadStream?> {
|
||||
/// See also [SyncDownloadStatus].
|
||||
const SyncDownloadStatusFamily();
|
||||
|
||||
/// See also [SyncDownloadStatus].
|
||||
SyncDownloadStatusProvider call(
|
||||
SyncedItem arg,
|
||||
) {
|
||||
return SyncDownloadStatusProvider(
|
||||
arg,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
SyncDownloadStatusProvider getProviderOverride(
|
||||
covariant SyncDownloadStatusProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.arg,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'syncDownloadStatusProvider';
|
||||
}
|
||||
|
||||
/// See also [SyncDownloadStatus].
|
||||
class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl<
|
||||
SyncDownloadStatus, DownloadStream?> {
|
||||
/// See also [SyncDownloadStatus].
|
||||
SyncDownloadStatusProvider(
|
||||
SyncedItem arg,
|
||||
) : this._internal(
|
||||
() => SyncDownloadStatus()..arg = arg,
|
||||
from: syncDownloadStatusProvider,
|
||||
name: r'syncDownloadStatusProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$syncDownloadStatusHash,
|
||||
dependencies: SyncDownloadStatusFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
SyncDownloadStatusFamily._allTransitiveDependencies,
|
||||
arg: arg,
|
||||
);
|
||||
|
||||
SyncDownloadStatusProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.arg,
|
||||
}) : super.internal();
|
||||
|
||||
final SyncedItem arg;
|
||||
|
||||
@override
|
||||
DownloadStream? runNotifierBuild(
|
||||
covariant SyncDownloadStatus notifier,
|
||||
) {
|
||||
return notifier.build(
|
||||
arg,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(SyncDownloadStatus Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: SyncDownloadStatusProvider._internal(
|
||||
() => create()..arg = arg,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
arg: arg,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeNotifierProviderElement<SyncDownloadStatus, DownloadStream?>
|
||||
createElement() {
|
||||
return _SyncDownloadStatusProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is SyncDownloadStatusProvider && other.arg == arg;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, arg.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin SyncDownloadStatusRef on AutoDisposeNotifierProviderRef<DownloadStream?> {
|
||||
/// The parameter `arg` of this provider.
|
||||
SyncedItem get arg;
|
||||
}
|
||||
|
||||
class _SyncDownloadStatusProviderElement
|
||||
extends AutoDisposeNotifierProviderElement<SyncDownloadStatus,
|
||||
DownloadStream?> with SyncDownloadStatusRef {
|
||||
_SyncDownloadStatusProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
SyncedItem get arg => (origin as SyncDownloadStatusProvider).arg;
|
||||
}
|
||||
|
||||
String _$syncStatusesHash() => r'f05ee53368d1de130714bba09132e08aba15bc44';
|
||||
|
||||
abstract class _$SyncStatuses
|
||||
extends BuildlessAutoDisposeAsyncNotifier<SyncStatus> {
|
||||
late final SyncedItem arg;
|
||||
|
||||
FutureOr<SyncStatus> build(
|
||||
SyncedItem arg,
|
||||
);
|
||||
}
|
||||
|
||||
/// See also [SyncStatuses].
|
||||
@ProviderFor(SyncStatuses)
|
||||
const syncStatusesProvider = SyncStatusesFamily();
|
||||
|
||||
/// See also [SyncStatuses].
|
||||
class SyncStatusesFamily extends Family<AsyncValue<SyncStatus>> {
|
||||
/// See also [SyncStatuses].
|
||||
const SyncStatusesFamily();
|
||||
|
||||
/// See also [SyncStatuses].
|
||||
SyncStatusesProvider call(
|
||||
SyncedItem arg,
|
||||
) {
|
||||
return SyncStatusesProvider(
|
||||
arg,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
SyncStatusesProvider getProviderOverride(
|
||||
covariant SyncStatusesProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.arg,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'syncStatusesProvider';
|
||||
}
|
||||
|
||||
/// See also [SyncStatuses].
|
||||
class SyncStatusesProvider
|
||||
extends AutoDisposeAsyncNotifierProviderImpl<SyncStatuses, SyncStatus> {
|
||||
/// See also [SyncStatuses].
|
||||
SyncStatusesProvider(
|
||||
SyncedItem arg,
|
||||
) : this._internal(
|
||||
() => SyncStatuses()..arg = arg,
|
||||
from: syncStatusesProvider,
|
||||
name: r'syncStatusesProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$syncStatusesHash,
|
||||
dependencies: SyncStatusesFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
SyncStatusesFamily._allTransitiveDependencies,
|
||||
arg: arg,
|
||||
);
|
||||
|
||||
SyncStatusesProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.arg,
|
||||
}) : super.internal();
|
||||
|
||||
final SyncedItem arg;
|
||||
|
||||
@override
|
||||
FutureOr<SyncStatus> runNotifierBuild(
|
||||
covariant SyncStatuses notifier,
|
||||
) {
|
||||
return notifier.build(
|
||||
arg,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(SyncStatuses Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: SyncStatusesProvider._internal(
|
||||
() => create()..arg = arg,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
arg: arg,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeAsyncNotifierProviderElement<SyncStatuses, SyncStatus>
|
||||
createElement() {
|
||||
return _SyncStatusesProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is SyncStatusesProvider && other.arg == arg;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, arg.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin SyncStatusesRef on AutoDisposeAsyncNotifierProviderRef<SyncStatus> {
|
||||
/// The parameter `arg` of this provider.
|
||||
SyncedItem get arg;
|
||||
}
|
||||
|
||||
class _SyncStatusesProviderElement
|
||||
extends AutoDisposeAsyncNotifierProviderElement<SyncStatuses, SyncStatus>
|
||||
with SyncStatusesRef {
|
||||
_SyncStatusesProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
SyncedItem get arg => (origin as SyncStatusesProvider).arg;
|
||||
}
|
||||
|
||||
String _$syncSizeHash() => r'138702f2dd69ab28d142bab67ab4a497bb24f252';
|
||||
|
||||
abstract class _$SyncSize extends BuildlessAutoDisposeNotifier<int?> {
|
||||
late final SyncedItem arg;
|
||||
|
||||
int? build(
|
||||
SyncedItem arg,
|
||||
);
|
||||
}
|
||||
|
||||
/// See also [SyncSize].
|
||||
@ProviderFor(SyncSize)
|
||||
const syncSizeProvider = SyncSizeFamily();
|
||||
|
||||
/// See also [SyncSize].
|
||||
class SyncSizeFamily extends Family<int?> {
|
||||
/// See also [SyncSize].
|
||||
const SyncSizeFamily();
|
||||
|
||||
/// See also [SyncSize].
|
||||
SyncSizeProvider call(
|
||||
SyncedItem arg,
|
||||
) {
|
||||
return SyncSizeProvider(
|
||||
arg,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
SyncSizeProvider getProviderOverride(
|
||||
covariant SyncSizeProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.arg,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'syncSizeProvider';
|
||||
}
|
||||
|
||||
/// See also [SyncSize].
|
||||
class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
|
||||
/// See also [SyncSize].
|
||||
SyncSizeProvider(
|
||||
SyncedItem arg,
|
||||
) : this._internal(
|
||||
() => SyncSize()..arg = arg,
|
||||
from: syncSizeProvider,
|
||||
name: r'syncSizeProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$syncSizeHash,
|
||||
dependencies: SyncSizeFamily._dependencies,
|
||||
allTransitiveDependencies: SyncSizeFamily._allTransitiveDependencies,
|
||||
arg: arg,
|
||||
);
|
||||
|
||||
SyncSizeProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.arg,
|
||||
}) : super.internal();
|
||||
|
||||
final SyncedItem arg;
|
||||
|
||||
@override
|
||||
int? runNotifierBuild(
|
||||
covariant SyncSize notifier,
|
||||
) {
|
||||
return notifier.build(
|
||||
arg,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(SyncSize Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: SyncSizeProvider._internal(
|
||||
() => create()..arg = arg,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
arg: arg,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeNotifierProviderElement<SyncSize, int?> createElement() {
|
||||
return _SyncSizeProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is SyncSizeProvider && other.arg == arg;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, arg.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin SyncSizeRef on AutoDisposeNotifierProviderRef<int?> {
|
||||
/// The parameter `arg` of this provider.
|
||||
SyncedItem get arg;
|
||||
}
|
||||
|
||||
class _SyncSizeProviderElement
|
||||
extends AutoDisposeNotifierProviderElement<SyncSize, int?>
|
||||
with SyncSizeRef {
|
||||
_SyncSizeProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
SyncedItem get arg => (origin as SyncSizeProvider).arg;
|
||||
}
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
627
lib/providers/sync_provider.dart
Normal file
627
lib/providers/sync_provider.dart
Normal file
|
|
@ -0,0 +1,627 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fladder/models/items/chapters_model.dart';
|
||||
import 'package:fladder/models/items/movie_model.dart';
|
||||
import 'package:fladder/models/items/trick_play_model.dart';
|
||||
import 'package:fladder/models/syncing/download_stream.dart';
|
||||
import 'package:fladder/models/syncing/i_synced_item.dart';
|
||||
import 'package:fladder/models/syncing/sync_item.dart';
|
||||
import 'package:fladder/models/syncing/sync_settings_model.dart';
|
||||
import 'package:fladder/providers/service_provider.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/providers/sync/background_download_provider.dart';
|
||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
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/media_streams_model.dart';
|
||||
import 'package:fladder/models/items/series_model.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/user_provider.dart';
|
||||
|
||||
final syncProvider = StateNotifierProvider<SyncNotifier, SyncSettingsModel>((ref) => throw UnimplementedError());
|
||||
|
||||
final downloadTasksProvider = StateProvider.family<DownloadStream, String>((ref, id) => DownloadStream.empty());
|
||||
|
||||
class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
||||
SyncNotifier(this.ref, this.isar, this.mobileDirectory) : super(SyncSettingsModel()) {
|
||||
_init();
|
||||
}
|
||||
|
||||
void _init() {
|
||||
ref.listen(
|
||||
userProvider,
|
||||
(previous, next) {
|
||||
if (previous?.id != next?.id) {
|
||||
if (next?.id != null) {
|
||||
_initializeQueryStream(next?.id ?? "");
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
final userId = ref.read(userProvider)?.id;
|
||||
if (userId != null) {
|
||||
_initializeQueryStream(userId);
|
||||
}
|
||||
}
|
||||
|
||||
void _initializeQueryStream(String userId) {
|
||||
_subscription?.cancel();
|
||||
|
||||
final queryStream = getParentSyncItems
|
||||
?.userIdEqualTo(userId)
|
||||
.watch()
|
||||
.asyncMap((event) => event.map((e) => SyncedItem.fromIsar(e, syncPath ?? "")).toList());
|
||||
|
||||
final initItems = getParentSyncItems
|
||||
?.userIdEqualTo(userId)
|
||||
.findAll()
|
||||
.mapIndexed((index, element) => SyncedItem.fromIsar(element, syncPath ?? ""))
|
||||
.toList();
|
||||
|
||||
state = state.copyWith(items: initItems ?? []);
|
||||
|
||||
_subscription = queryStream?.listen((items) {
|
||||
state = state.copyWith(items: items);
|
||||
});
|
||||
}
|
||||
|
||||
final Ref ref;
|
||||
final Isar? isar;
|
||||
final Directory mobileDirectory;
|
||||
final String subPath = "Synced";
|
||||
|
||||
StreamSubscription<List<SyncedItem>>? _subscription;
|
||||
|
||||
IsarCollection<String, ISyncedItem>? get syncedItems => isar?.iSyncedItems;
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
String? get _savePath => !kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)
|
||||
? ref.read(clientSettingsProvider.select((value) => value.syncPath))
|
||||
: mobileDirectory.path;
|
||||
|
||||
String? get savePath => _savePath;
|
||||
|
||||
Directory get mainDirectory => Directory(path.joinAll([_savePath ?? "", subPath]));
|
||||
|
||||
Directory? get saveDirectory {
|
||||
if (kIsWeb) return null;
|
||||
final directory = _savePath != null
|
||||
? Directory(path.joinAll([_savePath ?? "", subPath, ref.read(userProvider)?.id ?? "UnknownUser"]))
|
||||
: null;
|
||||
directory?.createSync(recursive: true);
|
||||
if (directory?.existsSync() == true) {
|
||||
final noMedia = File(path.joinAll([directory?.path ?? "", ".nomedia"]));
|
||||
noMedia.writeAsString('');
|
||||
}
|
||||
return directory;
|
||||
}
|
||||
|
||||
String? get syncPath => saveDirectory?.path;
|
||||
|
||||
QueryBuilder<ISyncedItem, ISyncedItem, QAfterFilterCondition>? get getParentSyncItems =>
|
||||
syncedItems?.where().parentIdIsNull();
|
||||
|
||||
Future<int> get directorySize async {
|
||||
if (saveDirectory == null) return 0;
|
||||
var files = await saveDirectory!.list(recursive: true).toList();
|
||||
var dirSize = files.fold(0, (int sum, file) => sum + file.statSync().size);
|
||||
return dirSize;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = state.copyWith(
|
||||
items: (await getParentSyncItems?.userIdEqualTo(ref.read(userProvider)?.id).findAllAsync())
|
||||
?.map((e) => SyncedItem.fromIsar(e, syncPath ?? ""))
|
||||
.toList() ??
|
||||
[]);
|
||||
}
|
||||
|
||||
List<SyncedItem> getNestedChildren(SyncedItem item) {
|
||||
final allChildren = <SyncedItem>[];
|
||||
List<SyncedItem> toProcess = [item];
|
||||
while (toProcess.isNotEmpty) {
|
||||
final currentLevel = toProcess.map(
|
||||
(parent) {
|
||||
final children = syncedItems?.where().parentIdEqualTo(parent.id).sortBySortKey().findAll();
|
||||
return children?.map((e) => SyncedItem.fromIsar(e, ref.read(syncProvider.notifier).syncPath ?? "")) ??
|
||||
<SyncedItem>[];
|
||||
},
|
||||
);
|
||||
allChildren.addAll(currentLevel.expand((list) => list));
|
||||
toProcess = currentLevel.expand((list) => list).toList();
|
||||
}
|
||||
return allChildren;
|
||||
}
|
||||
|
||||
List<SyncedItem> getChildren(SyncedItem item) {
|
||||
return (syncedItems?.where().parentIdEqualTo(item.id).sortBySortKey().findAll())
|
||||
?.map(
|
||||
(e) => SyncedItem.fromIsar(e, syncPath ?? ""),
|
||||
)
|
||||
.toList() ??
|
||||
[];
|
||||
}
|
||||
|
||||
SyncedItem? getSyncedItem(ItemBaseModel? item) {
|
||||
final id = item?.id;
|
||||
if (id == null) return null;
|
||||
final newItem = syncedItems?.get(id);
|
||||
if (newItem == null) return null;
|
||||
return SyncedItem.fromIsar(newItem, syncPath ?? "");
|
||||
}
|
||||
|
||||
SyncedItem? getParentItem(String id) {
|
||||
ISyncedItem? newItem = syncedItems?.get(id);
|
||||
while (newItem?.parentId != null) {
|
||||
newItem = syncedItems?.get(newItem!.parentId!);
|
||||
}
|
||||
if (newItem == null) return null;
|
||||
return SyncedItem.fromIsar(newItem, syncPath ?? "");
|
||||
}
|
||||
|
||||
ItemBaseModel? getItem(SyncedItem? syncedItem) {
|
||||
if (syncedItem == null) return null;
|
||||
return syncedItem.createItemModel(ref);
|
||||
}
|
||||
|
||||
Future<SyncedItem?> addSyncItem(BuildContext? context, ItemBaseModel item) async {
|
||||
if (context == null) return null;
|
||||
|
||||
if (saveDirectory == null) {
|
||||
String? selectedDirectory = await FilePicker.platform.getDirectoryPath(dialogTitle: 'Select downloads folder');
|
||||
if (selectedDirectory?.isEmpty == true) {
|
||||
fladderSnackbar(context, title: "No sync folder setup");
|
||||
return null;
|
||||
}
|
||||
ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory);
|
||||
}
|
||||
|
||||
fladderSnackbar(context, title: "Added ${item.detailedName(context)} for syncing");
|
||||
final newSync = switch (item) {
|
||||
EpisodeModel episode => await syncSeries(item.parentBaseModel, episode: episode),
|
||||
SeriesModel series => await syncSeries(series),
|
||||
MovieModel movie => await syncMovie(movie),
|
||||
_ => null
|
||||
};
|
||||
fladderSnackbar(context,
|
||||
title: newSync != null
|
||||
? "Started syncing ${item.detailedName(context)}"
|
||||
: "Unable to sync ${item.detailedName(context)}, type not supported?");
|
||||
return newSync;
|
||||
}
|
||||
|
||||
Future<bool> removeSync(SyncedItem? item) async {
|
||||
try {
|
||||
if (item == null) return false;
|
||||
|
||||
final nestedChildren = getNestedChildren(item);
|
||||
|
||||
state = state.copyWith(
|
||||
items: state.items
|
||||
.map(
|
||||
(e) => e.copyWith(markedForDelete: e.id == item.id ? true : false),
|
||||
)
|
||||
.toList());
|
||||
|
||||
if (item.taskId != null) {
|
||||
await ref.read(backgroundDownloaderProvider).cancelTaskWithId(item.taskId!);
|
||||
}
|
||||
|
||||
final deleteFromDatabase = isar?.write((isar) => syncedItems?.deleteAll([...nestedChildren, item]
|
||||
.map(
|
||||
(e) => e.id,
|
||||
)
|
||||
.toList()));
|
||||
|
||||
if (deleteFromDatabase == 0) return false;
|
||||
|
||||
for (var i = 0; i < nestedChildren.length; i++) {
|
||||
final element = nestedChildren[i];
|
||||
if (element.taskId != null) {
|
||||
await ref.read(backgroundDownloaderProvider).cancelTaskWithId(element.taskId!);
|
||||
}
|
||||
if (await element.directory.exists()) {
|
||||
await element.directory.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
if (await item.directory.exists()) {
|
||||
await item.directory.delete(recursive: true);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
log('Error deleting synced item');
|
||||
log(e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//Utility functions
|
||||
Future<List<SubStreamModel>> saveExternalSubtitles(List<SubStreamModel>? subtitles, SyncedItem item) async {
|
||||
if (subtitles == null) return [];
|
||||
|
||||
final directory = item.directory;
|
||||
|
||||
await directory.create(recursive: true);
|
||||
|
||||
return Stream.fromIterable(subtitles).asyncMap((element) async {
|
||||
if (element.isExternal) {
|
||||
final response = await http.get(Uri.parse(element.url!));
|
||||
|
||||
final file = File(path.joinAll([directory.path, "${element.displayTitle}.${element.language}.srt"]));
|
||||
file.writeAsBytesSync(response.bodyBytes);
|
||||
return element.copyWith(
|
||||
url: () => file.path,
|
||||
);
|
||||
}
|
||||
return element;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Future<TrickPlayModel?> saveTrickPlayData(ItemBaseModel? item, Directory saveDirectory) async {
|
||||
if (item == null) return null;
|
||||
final trickPlayDirectory = Directory(path.joinAll([saveDirectory.path, SyncedItem.trickPlayPath]))
|
||||
..createSync(recursive: true);
|
||||
final trickPlayData = await api.getTrickPlay(item: item, ref: ref);
|
||||
final List<String> newStringList = [];
|
||||
|
||||
for (var index = 0; index < (trickPlayData?.body?.images.length ?? 0); index++) {
|
||||
final image = trickPlayData?.body?.images[index];
|
||||
if (image != null) {
|
||||
final http.Response response = await http.get(Uri.parse(image));
|
||||
File? newFile;
|
||||
final fileName = "tile_$index.jpg";
|
||||
if (response.statusCode == 200) {
|
||||
final Uint8List bytes = response.bodyBytes;
|
||||
newFile = File(path.joinAll([trickPlayDirectory.path, fileName]));
|
||||
await newFile.writeAsBytes(bytes);
|
||||
}
|
||||
if (newFile != null && await newFile.exists()) {
|
||||
newStringList.add(path.joinAll(['TrickPlay', fileName]));
|
||||
}
|
||||
}
|
||||
}
|
||||
return trickPlayData?.body?.copyWith(images: newStringList.toList());
|
||||
}
|
||||
|
||||
Future<ImagesData?> saveImageData(ImagesData? data, Directory saveDirectory) async {
|
||||
if (data == null) return data;
|
||||
if (!saveDirectory.existsSync()) return data;
|
||||
|
||||
final primary = await urlDataToFileData(data.primary, saveDirectory, "primary.jpg");
|
||||
final logo = await urlDataToFileData(data.logo, saveDirectory, "logo.jpg");
|
||||
final backdrops = await Stream.fromIterable(data.backDrop ?? <ImageData>[])
|
||||
.asyncMap((element) async => await urlDataToFileData(element, saveDirectory, "backdrop-${element.key}.jpg"))
|
||||
.toList();
|
||||
|
||||
return data.copyWith(
|
||||
primary: () => primary,
|
||||
logo: () => logo,
|
||||
backDrop: () => backdrops.whereNotNull().toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Chapter>?> saveChapterImages(List<Chapter>? data, Directory itemPath) async {
|
||||
if (data == null) return data;
|
||||
if (!itemPath.existsSync()) return data;
|
||||
if (data.isEmpty) return data;
|
||||
final saveDirectory = Directory(path.joinAll([itemPath.path, "Chapters"]));
|
||||
|
||||
await saveDirectory.create(recursive: true);
|
||||
|
||||
final saveChapters = await Stream.fromIterable(data).asyncMap((event) async {
|
||||
final fileName = "${event.name}.jpg";
|
||||
final response = await http.get(Uri.parse(event.imageUrl));
|
||||
final file = File(path.joinAll([saveDirectory.path, fileName]));
|
||||
if (response.bodyBytes.isEmpty) return null;
|
||||
file.writeAsBytesSync(response.bodyBytes);
|
||||
return event.copyWith(
|
||||
imageUrl: path.joinAll(["Chapters", fileName]),
|
||||
);
|
||||
}).toList();
|
||||
return saveChapters.whereNotNull().toList();
|
||||
}
|
||||
|
||||
Future<ImageData?> urlDataToFileData(ImageData? data, Directory directory, String fileName) async {
|
||||
if (data?.path == null) return null;
|
||||
final response = await http.get(Uri.parse(data?.path ?? ""));
|
||||
|
||||
final file = File(path.joinAll([directory.path, fileName]));
|
||||
file.writeAsBytesSync(response.bodyBytes);
|
||||
|
||||
return data?.copyWith(path: fileName);
|
||||
}
|
||||
|
||||
void updateItemSync(SyncedItem syncedItem) =>
|
||||
isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncedItem, syncPath ?? "")));
|
||||
|
||||
Future<void> updateItem(SyncedItem syncedItem) async {
|
||||
isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncedItem, syncPath ?? "")));
|
||||
}
|
||||
|
||||
Future<SyncedItem> deleteFullSyncFiles(SyncedItem syncedItem) async {
|
||||
await syncedItem.deleteDatFiles(ref);
|
||||
ref.read(downloadTasksProvider(syncedItem.id).notifier).update((state) => DownloadStream.empty());
|
||||
refresh();
|
||||
return syncedItem;
|
||||
}
|
||||
|
||||
Future<DownloadStream?> syncVideoFile(SyncedItem syncItem, bool skipDownload) async {
|
||||
final playbackResponse = await api.itemsItemIdPlaybackInfoPost(
|
||||
itemId: syncItem.id,
|
||||
body: PlaybackInfoDto(
|
||||
enableDirectPlay: true,
|
||||
enableDirectStream: true,
|
||||
enableTranscoding: false,
|
||||
deviceProfile: defaultProfile,
|
||||
),
|
||||
);
|
||||
|
||||
final item = syncItem.createItemModel(ref);
|
||||
|
||||
final directory = await Directory(syncItem.directory.path).create(recursive: true);
|
||||
|
||||
final newState = VideoStream.fromPlayBackInfo(playbackResponse.bodyOrThrow, ref)?.copyWith();
|
||||
final subtitles = await saveExternalSubtitles(newState?.mediaStreamsModel?.subStreams, syncItem);
|
||||
|
||||
final trickPlayFile = await saveTrickPlayData(item, directory);
|
||||
final introOutroSkip = (await api.introSkipGet(id: syncItem.id))?.body;
|
||||
|
||||
syncItem = syncItem.copyWith(
|
||||
subtitles: subtitles,
|
||||
fTrickPlayModel: trickPlayFile,
|
||||
introOutSkipModel: introOutroSkip,
|
||||
);
|
||||
|
||||
await updateItem(syncItem);
|
||||
|
||||
final currentTask = ref.read(downloadTasksProvider(syncItem.id));
|
||||
|
||||
final downloadString = path.joinAll([
|
||||
"${ref.read(userProvider)?.server}",
|
||||
"Items",
|
||||
"${syncItem.id}/Download?api_key=${ref.read(userProvider)?.credentials.token}"
|
||||
]);
|
||||
|
||||
try {
|
||||
if (!skipDownload && currentTask.task == null) {
|
||||
final downloadTask = DownloadTask(
|
||||
url: downloadString,
|
||||
directory: syncItem.directory.path,
|
||||
filename: syncItem.videoFileName,
|
||||
updates: Updates.statusAndProgress,
|
||||
baseDirectory: BaseDirectory.root,
|
||||
requiresWiFi: true,
|
||||
retries: 5,
|
||||
allowPause: true,
|
||||
);
|
||||
|
||||
final defaultDownloadStream =
|
||||
DownloadStream(id: syncItem.id, task: downloadTask, progress: 0.0, status: TaskStatus.enqueued);
|
||||
|
||||
ref.read(downloadTasksProvider(syncItem.id).notifier).update((state) => defaultDownloadStream);
|
||||
|
||||
ref.read(backgroundDownloaderProvider).download(
|
||||
downloadTask,
|
||||
onProgress: (progress) {
|
||||
if (progress > 0 && progress < 1) {
|
||||
ref.read(downloadTasksProvider(syncItem.id).notifier).update(
|
||||
(state) => state.copyWith(progress: progress),
|
||||
);
|
||||
} else {
|
||||
ref.read(downloadTasksProvider(syncItem.id).notifier).update(
|
||||
(state) => state.copyWith(progress: null),
|
||||
);
|
||||
}
|
||||
},
|
||||
onStatus: (status) {
|
||||
ref.read(downloadTasksProvider(syncItem.id).notifier).update(
|
||||
(state) => state.copyWith(status: status),
|
||||
);
|
||||
|
||||
if (status == TaskStatus.complete) {
|
||||
ref.read(downloadTasksProvider(syncItem.id).notifier).update((state) => DownloadStream.empty());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return defaultDownloadStream;
|
||||
}
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
await mainDirectory.delete(recursive: true);
|
||||
isar?.write((isar) => syncedItems?.clear());
|
||||
state = state.copyWith(items: []);
|
||||
}
|
||||
|
||||
Future<void> setup() async {
|
||||
state = state.copyWith(items: []);
|
||||
_init();
|
||||
}
|
||||
}
|
||||
|
||||
extension SyncNotifierHelpers on SyncNotifier {
|
||||
Future<SyncedItem> createSyncItem(BaseItemDto response, {SyncedItem? parent}) async {
|
||||
final ItemBaseModel item = ItemBaseModel.fromBaseDto(response, ref);
|
||||
|
||||
final Directory? parentDirectory = parent?.directory;
|
||||
|
||||
SyncedItem syncItem = SyncedItem(id: item.id, userId: ref.read(userProvider)?.id ?? "");
|
||||
final directory = Directory(path.joinAll([(parentDirectory ?? saveDirectory)?.path ?? "", item.id]));
|
||||
|
||||
await directory.create(recursive: true);
|
||||
|
||||
File dataFile = File(path.joinAll([directory.path, 'data.json']));
|
||||
await dataFile.writeAsString(jsonEncode(response.toJson()));
|
||||
|
||||
final imageData = await saveImageData(item.images, directory);
|
||||
final origChapters = Chapter.chaptersFromInfo(item.id, response.chapters ?? [], ref);
|
||||
|
||||
return syncItem.copyWith(
|
||||
id: item.id,
|
||||
parentId: parent?.id,
|
||||
sortKey: (response.parentIndexNumber ?? 0) * (response.indexNumber ?? 0),
|
||||
path: directory.path,
|
||||
fChapters: await saveChapterImages(origChapters, directory) ?? [],
|
||||
fileSize: response.mediaSources?.firstOrNull?.size ?? 0,
|
||||
fImages: imageData,
|
||||
videoFileName: response.path?.split('/').lastOrNull ?? "",
|
||||
userData: item.userData,
|
||||
);
|
||||
}
|
||||
|
||||
// Need to move the file after downloading on Android
|
||||
Future<void> moveFile(DownloadTask downloadTask, SyncedItem syncItem) async {
|
||||
final currentLocation = File(await downloadTask.filePath());
|
||||
final wantedLocation = syncItem.videoFile;
|
||||
if (currentLocation.path != wantedLocation.path) {
|
||||
await currentLocation.copy(wantedLocation.path);
|
||||
await currentLocation.delete();
|
||||
}
|
||||
}
|
||||
|
||||
Future<SyncedItem?> syncMovie(ItemBaseModel item, {bool skipDownload = false}) async {
|
||||
final response = await api.usersUserIdItemsItemIdGetBaseItem(
|
||||
itemId: item.id,
|
||||
);
|
||||
|
||||
final itemBaseModel = response.body;
|
||||
if (itemBaseModel == null) return null;
|
||||
|
||||
SyncedItem syncItem = await createSyncItem(itemBaseModel);
|
||||
|
||||
if (!syncItem.directory.existsSync()) return null;
|
||||
|
||||
await syncVideoFile(syncItem, skipDownload);
|
||||
|
||||
await isar?.writeAsync((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncItem, syncPath)));
|
||||
|
||||
return syncItem;
|
||||
}
|
||||
|
||||
Future<SyncedItem?> syncSeries(SeriesModel item, {EpisodeModel? episode}) async {
|
||||
final response = await api.usersUserIdItemsItemIdGetBaseItem(
|
||||
itemId: item.id,
|
||||
);
|
||||
|
||||
List<SyncedItem> newItems = [];
|
||||
|
||||
SyncedItem? itemToDownload;
|
||||
|
||||
SyncedItem seriesItem = await createSyncItem(response.bodyOrThrow);
|
||||
newItems.add(seriesItem);
|
||||
if (!seriesItem.directory.existsSync()) return null;
|
||||
|
||||
final seasonsResponse = await api.showsSeriesIdSeasonsGet(
|
||||
isMissing: false,
|
||||
enableUserData: true,
|
||||
fields: [
|
||||
ItemFields.mediastreams,
|
||||
ItemFields.mediasources,
|
||||
ItemFields.overview,
|
||||
ItemFields.mediasourcecount,
|
||||
ItemFields.airtime,
|
||||
ItemFields.datecreated,
|
||||
ItemFields.datelastmediaadded,
|
||||
ItemFields.datelastrefreshed,
|
||||
ItemFields.sortname,
|
||||
ItemFields.seasonuserdata,
|
||||
ItemFields.externalurls,
|
||||
ItemFields.genres,
|
||||
ItemFields.parentid,
|
||||
ItemFields.path,
|
||||
ItemFields.chapters,
|
||||
ItemFields.trickplay,
|
||||
],
|
||||
seriesId: item.id,
|
||||
);
|
||||
|
||||
final seasons = seasonsResponse.body?.items ?? [];
|
||||
|
||||
for (var i = 0; i < seasons.length; i++) {
|
||||
final season = seasons[i];
|
||||
final syncedSeason = await createSyncItem(season, parent: seriesItem);
|
||||
newItems.add(syncedSeason);
|
||||
final episodesResponse = await api.showsSeriesIdEpisodesGet(
|
||||
isMissing: false,
|
||||
enableUserData: true,
|
||||
fields: [
|
||||
ItemFields.mediastreams,
|
||||
ItemFields.mediasources,
|
||||
ItemFields.overview,
|
||||
ItemFields.mediasourcecount,
|
||||
ItemFields.airtime,
|
||||
ItemFields.datecreated,
|
||||
ItemFields.datelastmediaadded,
|
||||
ItemFields.datelastrefreshed,
|
||||
ItemFields.sortname,
|
||||
ItemFields.seasonuserdata,
|
||||
ItemFields.externalurls,
|
||||
ItemFields.genres,
|
||||
ItemFields.parentid,
|
||||
ItemFields.path,
|
||||
ItemFields.chapters,
|
||||
ItemFields.trickplay,
|
||||
],
|
||||
seasonId: season.id,
|
||||
seriesId: seriesItem.id,
|
||||
);
|
||||
final episodes = episodesResponse.body?.items ?? [];
|
||||
for (var i = 0; i < episodes.length; i++) {
|
||||
final item = episodes[i];
|
||||
final newEpisode = await createSyncItem(item, parent: syncedSeason);
|
||||
newItems.add(newEpisode);
|
||||
if (episode?.id == item.id) {
|
||||
itemToDownload = newEpisode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isar?.write(
|
||||
(isar) => syncedItems?.putAll(newItems
|
||||
.map(
|
||||
(e) => ISyncedItem.fromSynced(e, syncPath ?? ""),
|
||||
)
|
||||
.toList()),
|
||||
);
|
||||
|
||||
if (itemToDownload != null) {
|
||||
await syncVideoFile(itemToDownload, false);
|
||||
}
|
||||
|
||||
return seriesItem;
|
||||
}
|
||||
}
|
||||
13
lib/providers/sync_provider_web.dart
Normal file
13
lib/providers/sync_provider_web.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
class AbstractSyncNotifier {
|
||||
final Ref ref;
|
||||
final Isar? isar;
|
||||
final Directory mobileDirectory;
|
||||
final String subPath = "Synced";
|
||||
|
||||
AbstractSyncNotifier(this.ref, this.isar, this.mobileDirectory);
|
||||
}
|
||||
145
lib/providers/user_provider.dart
Normal file
145
lib/providers/user_provider.dart
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import 'package:chopper/chopper.dart';
|
||||
import 'package:fladder/jellyfin/enum_models.dart';
|
||||
import 'package:fladder/models/account_model.dart';
|
||||
import 'package:fladder/models/items/item_shared_models.dart';
|
||||
import 'package:fladder/providers/api_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:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'user_provider.g.dart';
|
||||
|
||||
@riverpod
|
||||
bool showSyncButtonProvider(ShowSyncButtonProviderRef ref) {
|
||||
final userCanSync = ref.watch(userProvider.select((value) => value?.canDownload ?? false));
|
||||
final hasSyncedItems = ref.watch(syncProvider.select((value) => value.items.isNotEmpty));
|
||||
return userCanSync || hasSyncedItems;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class User extends _$User {
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
set userState(AccountModel? account) {
|
||||
state = account?.copyWith(lastUsed: DateTime.now());
|
||||
if (account != null) {
|
||||
ref.read(sharedUtilityProvider).updateAccountInfo(account);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Response<bool>> quickConnect(String pin) async => api.quickConnect(pin);
|
||||
|
||||
Future<Response<AccountModel>?> updateInformation() async {
|
||||
if (state == null) return null;
|
||||
var response = await api.usersMeGet();
|
||||
var quickConnectStatus = await api.quickConnectEnabled();
|
||||
var systemConfiguration = await api.systemConfigurationGet();
|
||||
if (response.isSuccessful && response.body != null) {
|
||||
userState = state?.copyWith(
|
||||
name: response.body?.name ?? state?.name ?? "",
|
||||
policy: response.body?.policy,
|
||||
serverConfiguration: systemConfiguration.body,
|
||||
quickConnectState: quickConnectStatus.body ?? false,
|
||||
latestItemsExcludes: response.body?.configuration?.latestItemsExcludes ?? [],
|
||||
);
|
||||
return response.copyWith(body: state);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<Response> refreshMetaData(
|
||||
String itemId, {
|
||||
MetadataRefresh? metadataRefreshMode,
|
||||
bool? replaceAllMetadata,
|
||||
}) async {
|
||||
return api.itemsItemIdRefreshPost(
|
||||
itemId: itemId,
|
||||
metadataRefreshMode: metadataRefreshMode,
|
||||
imageRefreshMode: metadataRefreshMode,
|
||||
replaceAllMetadata: switch (metadataRefreshMode) {
|
||||
MetadataRefresh.fullRefresh => false,
|
||||
MetadataRefresh.validation => true,
|
||||
_ => false,
|
||||
},
|
||||
replaceAllImages: switch (metadataRefreshMode) {
|
||||
MetadataRefresh.fullRefresh => true,
|
||||
MetadataRefresh.validation => true,
|
||||
_ => false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<UserData>?> setAsFavorite(bool favorite, String itemId) async {
|
||||
final response = await (favorite
|
||||
? api.usersUserIdFavoriteItemsItemIdPost(itemId: itemId)
|
||||
: api.usersUserIdFavoriteItemsItemIdDelete(itemId: itemId));
|
||||
return Response(response.base, UserData.fromDto(response.body));
|
||||
}
|
||||
|
||||
Future<Response<UserData>?> markAsPlayed(bool enable, String itemId) async {
|
||||
final response = await (enable
|
||||
? api.usersUserIdPlayedItemsItemIdPost(
|
||||
itemId: itemId,
|
||||
datePlayed: DateTime.now(),
|
||||
)
|
||||
: api.usersUserIdPlayedItemsItemIdDelete(
|
||||
itemId: itemId,
|
||||
));
|
||||
return Response(response.base, UserData.fromDto(response.body));
|
||||
}
|
||||
|
||||
void clear() {
|
||||
userState = null;
|
||||
}
|
||||
|
||||
void updateUser(AccountModel? user) {
|
||||
userState = user;
|
||||
}
|
||||
|
||||
void loginUser(AccountModel? user) {
|
||||
state = user;
|
||||
}
|
||||
|
||||
void setAuthMethod(Authentication method) {
|
||||
userState = state?.copyWith(authMethod: method);
|
||||
}
|
||||
|
||||
void addSearchQuery(String value) {
|
||||
if (value.isEmpty) return;
|
||||
final newList = state?.searchQueryHistory.toList() ?? [];
|
||||
if (newList.contains(value)) {
|
||||
newList.remove(value);
|
||||
}
|
||||
newList.add(value);
|
||||
userState = state?.copyWith(searchQueryHistory: newList);
|
||||
}
|
||||
|
||||
void removeSearchQuery(String value) {
|
||||
userState = state?.copyWith(
|
||||
searchQueryHistory: state?.searchQueryHistory ?? []
|
||||
..remove(value)
|
||||
..take(50),
|
||||
);
|
||||
}
|
||||
|
||||
void clearSearchQuery() {
|
||||
userState = state?.copyWith(searchQueryHistory: []);
|
||||
}
|
||||
|
||||
Future<void> logoutUser() async {
|
||||
if (state == null) return;
|
||||
userState = null;
|
||||
}
|
||||
|
||||
Future<void> forceLogoutUser(AccountModel account) async {
|
||||
userState = account;
|
||||
await api.sessionsLogoutPost();
|
||||
userState = null;
|
||||
}
|
||||
|
||||
@override
|
||||
AccountModel? build() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
40
lib/providers/user_provider.g.dart
Normal file
40
lib/providers/user_provider.g.dart
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'user_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$showSyncButtonProviderHash() =>
|
||||
r'3468d7309f3859f7b60b1bd317e306e1f5f00555';
|
||||
|
||||
/// See also [showSyncButtonProvider].
|
||||
@ProviderFor(showSyncButtonProvider)
|
||||
final showSyncButtonProviderProvider = AutoDisposeProvider<bool>.internal(
|
||||
showSyncButtonProvider,
|
||||
name: r'showSyncButtonProviderProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$showSyncButtonProviderHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef ShowSyncButtonProviderRef = AutoDisposeProviderRef<bool>;
|
||||
String _$userHash() => r'4a4302c819d26fc7c28d04b9274d0dfd0dc8e201';
|
||||
|
||||
/// See also [User].
|
||||
@ProviderFor(User)
|
||||
final userProvider = NotifierProvider<User, AccountModel?>.internal(
|
||||
User.new,
|
||||
name: r'userProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$userHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$User = Notifier<AccountModel?>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
125
lib/providers/video_player_provider.dart
Normal file
125
lib/providers/video_player_provider.dart
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:fladder/models/media_playback_model.dart';
|
||||
import 'package:fladder/models/playback/playback_model.dart';
|
||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||
import 'package:fladder/wrappers/media_control_wrapper.dart'
|
||||
if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
final mediaPlaybackProvider = StateProvider<MediaPlaybackModel>((ref) => MediaPlaybackModel());
|
||||
|
||||
final playBackModel = StateProvider<PlaybackModel?>((ref) => null);
|
||||
|
||||
final videoPlayerProvider = StateNotifierProvider<VideoPlayerNotifier, MediaControlsWrapper>((ref) {
|
||||
final videoPlayer = VideoPlayerNotifier(ref);
|
||||
videoPlayer.init();
|
||||
return videoPlayer;
|
||||
});
|
||||
|
||||
class VideoPlayerNotifier extends StateNotifier<MediaControlsWrapper> {
|
||||
VideoPlayerNotifier(this.ref) : super(MediaControlsWrapper(ref: ref));
|
||||
|
||||
final Ref ref;
|
||||
|
||||
List<StreamSubscription> subscriptions = [];
|
||||
|
||||
late final mediaState = ref.read(mediaPlaybackProvider.notifier);
|
||||
|
||||
bool initMediaControls = false;
|
||||
|
||||
void init() async {
|
||||
state.player?.dispose();
|
||||
if (!initMediaControls && !kDebugMode) {
|
||||
state.init();
|
||||
initMediaControls = true;
|
||||
}
|
||||
for (final s in subscriptions) {
|
||||
s.cancel();
|
||||
}
|
||||
final player = state.setup();
|
||||
if (player.platform is NativePlayer) {
|
||||
await (player.platform as dynamic).setProperty(
|
||||
'force-seekable',
|
||||
'yes',
|
||||
);
|
||||
}
|
||||
subscriptions.addAll(
|
||||
[
|
||||
player.stream.buffering.listen((event) => mediaState.update((state) => state.copyWith(buffering: event))),
|
||||
player.stream.buffer.listen((event) => mediaState.update((state) => state.copyWith(buffer: event))),
|
||||
player.stream.playing.listen((event) => updatePlaying(event)),
|
||||
player.stream.position.listen((event) => updatePosition(event)),
|
||||
player.stream.duration.listen((event) => mediaState.update((state) => state.copyWith(duration: event))),
|
||||
].whereNotNull(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updatePlaying(bool event) async {
|
||||
final player = state.player;
|
||||
if (player == null) return;
|
||||
mediaState.update((state) => state.copyWith(playing: event));
|
||||
}
|
||||
|
||||
Future<void> updatePosition(Duration event) async {
|
||||
final player = state.player;
|
||||
if (player == null) return;
|
||||
if (!player.state.playing) return;
|
||||
|
||||
final position = event;
|
||||
final lastPosition = ref.read(mediaPlaybackProvider.select((value) => value.lastPosition));
|
||||
final diff = (position.inMilliseconds - lastPosition.inMilliseconds).abs();
|
||||
|
||||
if (diff > Duration(seconds: 1, milliseconds: 500).inMilliseconds) {
|
||||
mediaState.update((value) => value.copyWith(
|
||||
position: event,
|
||||
playing: player.state.playing,
|
||||
duration: player.state.duration,
|
||||
lastPosition: position,
|
||||
));
|
||||
ref.read(playBackModel)?.updatePlaybackPosition(position, player.state.playing, ref);
|
||||
} else {
|
||||
mediaState.update((value) => value.copyWith(
|
||||
position: event,
|
||||
playing: player.state.playing,
|
||||
duration: player.state.duration,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> loadPlaybackItem(PlaybackModel model, {Duration? startPosition}) async {
|
||||
await state.stop();
|
||||
mediaState
|
||||
.update((state) => state.copyWith(state: VideoPlayerState.fullScreen, buffering: true, errorPlaying: false));
|
||||
|
||||
final media = model.media;
|
||||
PlaybackModel? newPlaybackModel = model;
|
||||
|
||||
if (media != null) {
|
||||
await state.setVolume(ref.read(videoPlayerSettingsProvider).volume);
|
||||
await state.open(media, play: false);
|
||||
state.player?.stream.buffering.takeWhile((event) => event == true).listen(
|
||||
null,
|
||||
onDone: () async {
|
||||
final start = startPosition ?? await model.startDuration();
|
||||
if (start != null) {
|
||||
await state.seek(start);
|
||||
}
|
||||
newPlaybackModel = await newPlaybackModel?.setAudio(null, state);
|
||||
newPlaybackModel = await newPlaybackModel?.setSubtitle(null, state);
|
||||
ref.read(playBackModel.notifier).update((state) => newPlaybackModel);
|
||||
state.play();
|
||||
},
|
||||
);
|
||||
ref.read(playBackModel.notifier).update((state) => model);
|
||||
return true;
|
||||
}
|
||||
|
||||
mediaState.update((state) => state.copyWith(errorPlaying: true));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
64
lib/providers/views_provider.dart
Normal file
64
lib/providers/views_provider.dart
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/view_model.dart';
|
||||
import 'package:fladder/models/views_model.dart';
|
||||
import 'package:fladder/providers/api_provider.dart';
|
||||
import 'package:fladder/providers/service_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final viewsProvider = StateNotifierProvider<ViewsNotifier, ViewsModel>((ref) {
|
||||
return ViewsNotifier(ref);
|
||||
});
|
||||
|
||||
class ViewsNotifier extends StateNotifier<ViewsModel> {
|
||||
ViewsNotifier(this.ref) : super(ViewsModel());
|
||||
|
||||
final Ref ref;
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
Future<void> fetchViews() async {
|
||||
if (state.loading) return;
|
||||
final response = await api.usersUserIdViewsGet();
|
||||
final createdViews = response.body?.items?.map((e) => ViewModel.fromBodyDto(e, ref));
|
||||
List<ViewModel> newList = [];
|
||||
|
||||
if (createdViews != null) {
|
||||
newList = await Future.wait(createdViews.map((e) async {
|
||||
if (ref.read(userProvider)?.latestItemsExcludes.contains(e.id) == true) return e;
|
||||
if ([CollectionType.boxsets, CollectionType.folders].contains(e.collectionType)) return e;
|
||||
final recents = await api.usersUserIdItemsLatestGet(
|
||||
parentId: e.id,
|
||||
imageTypeLimit: 1,
|
||||
limit: 16,
|
||||
enableImageTypes: [
|
||||
ImageType.primary,
|
||||
ImageType.backdrop,
|
||||
ImageType.thumb,
|
||||
],
|
||||
fields: [
|
||||
ItemFields.parentid,
|
||||
ItemFields.mediastreams,
|
||||
ItemFields.mediasources,
|
||||
ItemFields.candelete,
|
||||
ItemFields.candownload,
|
||||
],
|
||||
);
|
||||
return e.copyWith(recentlyAdded: recents.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList());
|
||||
}));
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
views: newList,
|
||||
dashboardViews: newList
|
||||
.where((element) => !(ref.read(userProvider)?.latestItemsExcludes.contains(element.id) ?? true))
|
||||
.where((element) => ![CollectionType.boxsets, CollectionType.folders].contains(element.collectionType))
|
||||
.toList(),
|
||||
loading: false);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
state = ViewsModel();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue