mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-09 07:28:14 -07:00
feat: Sync offline/online playback when able (#431)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
15ac3566e2
commit
092836328f
42 changed files with 1002 additions and 497 deletions
|
|
@ -1,11 +1,13 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:chopper/chopper.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/providers/auth_provider.dart';
|
||||
import 'package:fladder/providers/connectivity_provider.dart';
|
||||
import 'package:fladder/providers/service_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
|
||||
|
|
@ -33,21 +35,27 @@ class JellyRequest implements Interceptor {
|
|||
|
||||
@override
|
||||
FutureOr<Response<BodyType>> intercept<BodyType>(Chain<BodyType> chain) async {
|
||||
final serverUrl = Uri.parse(ref.read(userProvider)?.server ?? ref.read(authProvider).tempCredentials.server);
|
||||
final connectivityNotifier = ref.read(connectivityStatusProvider.notifier);
|
||||
try {
|
||||
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);
|
||||
//Use current logged in user otherwise use the authprovider
|
||||
var loginModel = ref.read(userProvider)?.credentials ?? ref.read(authProvider).tempCredentials;
|
||||
var headers = loginModel.header(ref);
|
||||
final Response<BodyType> response = await chain.proceed(
|
||||
applyHeaders(
|
||||
chain.request.copyWith(
|
||||
baseUri: serverUrl,
|
||||
),
|
||||
headers),
|
||||
);
|
||||
|
||||
final Response<BodyType> response = await chain.proceed(
|
||||
applyHeaders(
|
||||
chain.request.copyWith(
|
||||
baseUri: serverUrl,
|
||||
),
|
||||
headers),
|
||||
);
|
||||
|
||||
return response;
|
||||
connectivityNotifier.checkConnectivity();
|
||||
return response;
|
||||
} catch (e) {
|
||||
connectivityNotifier.onStateChange([ConnectivityResult.none]);
|
||||
throw Exception('Failed to make request\n$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,10 +75,6 @@ class JellyResponse implements Interceptor {
|
|||
chopperLogger.severe('404 NOT FOUND');
|
||||
}
|
||||
|
||||
if (response.statusCode == 401) {
|
||||
// ref.read(sharedUtilityProvider).removeAccount(ref.read(userProvider));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ 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';
|
||||
|
||||
|
|
@ -92,7 +91,6 @@ class AuthNotifier extends StateNotifier<LoginScreenModel> {
|
|||
ref.read(viewsProvider.notifier).clear();
|
||||
ref.read(favouritesProvider.notifier).clear();
|
||||
ref.read(userProvider.notifier).clear();
|
||||
ref.read(syncProvider.notifier).setup();
|
||||
}
|
||||
|
||||
void setServer(String server) {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ class ConnectivityStatus extends _$ConnectivityStatus {
|
|||
@override
|
||||
ConnectionState build() {
|
||||
Connectivity().onConnectivityChanged.listen(onStateChange);
|
||||
return ConnectionState.offline;
|
||||
checkConnectivity();
|
||||
return ConnectionState.mobile;
|
||||
}
|
||||
|
||||
void onStateChange(List<ConnectivityResult> connectivityResult) {
|
||||
|
|
@ -36,4 +37,9 @@ class ConnectivityStatus extends _$ConnectivityStatus {
|
|||
state = ConnectionState.offline;
|
||||
}
|
||||
}
|
||||
|
||||
void checkConnectivity() async {
|
||||
final connectivityResult = await Connectivity().checkConnectivity();
|
||||
onStateChange(connectivityResult);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ part of 'connectivity_provider.dart';
|
|||
// **************************************************************************
|
||||
|
||||
String _$connectivityStatusHash() =>
|
||||
r'2e9b645c78146ed25456e1286df83761588b8e27';
|
||||
r'7a4ac96d163a479bd34fc6a3efcd556755f8d5e9';
|
||||
|
||||
/// See also [ConnectivityStatus].
|
||||
@ProviderFor(ConnectivityStatus)
|
||||
|
|
|
|||
|
|
@ -74,9 +74,9 @@ class EpisodeDetailsProvider extends StateNotifier<EpisodeDetailModel> {
|
|||
|
||||
Future<void> _tryToCreateOfflineState(ItemBaseModel item) async {
|
||||
final syncNotifier = ref.read(syncProvider.notifier);
|
||||
final episodeModel = (await syncNotifier.getSyncedItem(item))?.itemModel as EpisodeModel?;
|
||||
final episodeModel = (await syncNotifier.getSyncedItem(item.id))?.itemModel as EpisodeModel?;
|
||||
if (episodeModel == null) return;
|
||||
final seriesSyncedItem = await syncNotifier.getSyncedItem(episodeModel.parentBaseModel);
|
||||
final seriesSyncedItem = await syncNotifier.getSyncedItem(episodeModel.parentBaseModel.id);
|
||||
if (seriesSyncedItem == null) return;
|
||||
final seriesModel = seriesSyncedItem.itemModel as SeriesModel?;
|
||||
if (seriesModel == null) return;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ 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';
|
||||
|
||||
part 'movies_details_provider.g.dart';
|
||||
|
||||
|
|
@ -30,19 +29,10 @@ class MovieDetails extends _$MovieDetails {
|
|||
state = newState.copyWith(related: related.body);
|
||||
return null;
|
||||
} catch (e) {
|
||||
_tryToCreateOfflineState(item);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void _tryToCreateOfflineState(ItemBaseModel item) async {
|
||||
final syncNotifier = ref.read(syncProvider.notifier);
|
||||
final syncedItem = await syncNotifier.getParentItem(item.id);
|
||||
if (syncedItem == null) return;
|
||||
final movieModel = syncedItem.itemModel as MovieModel?;
|
||||
state = movieModel;
|
||||
}
|
||||
|
||||
void setSubIndex(int index) {
|
||||
state = state?.copyWith(mediaStreams: state?.mediaStreams.copyWith(defaultSubStreamIndex: index));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'movies_details_provider.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$movieDetailsHash() => r'a9d8d2eeb7fa37652f25c1820b5e346efeeb59fc';
|
||||
String _$movieDetailsHash() => r'322236f235bcd387cfecc3468c0876490d9afc39';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:chopper/chopper.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
|
|
@ -9,7 +11,6 @@ import 'package:fladder/models/items/series_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';
|
||||
|
||||
final seriesDetailsProvider =
|
||||
StateNotifierProvider.autoDispose.family<SeriesDetailViewNotifier, SeriesModel?, String>((ref, id) {
|
||||
|
|
@ -33,6 +34,14 @@ class SeriesDetailViewNotifier extends StateNotifier<SeriesModel?> {
|
|||
if (response.body == null) return null;
|
||||
newState = response.bodyOrThrow as SeriesModel;
|
||||
|
||||
final seasons = await api.showsSeriesIdSeasonsGet(seriesId: seriesModel.id, fields: [
|
||||
ItemFields.mediastreams,
|
||||
ItemFields.mediasources,
|
||||
ItemFields.overview,
|
||||
ItemFields.candownload,
|
||||
ItemFields.childcount,
|
||||
]);
|
||||
|
||||
final episodes = await api.showsSeriesIdEpisodesGet(seriesId: seriesModel.id, fields: [
|
||||
ItemFields.mediastreams,
|
||||
ItemFields.mediasources,
|
||||
|
|
@ -45,14 +54,9 @@ class SeriesDetailViewNotifier extends StateNotifier<SeriesModel?> {
|
|||
episodes.body?.items,
|
||||
ref,
|
||||
);
|
||||
|
||||
final episodesCanDownload = newEpisodes.any((episode) => episode.canDownload == true);
|
||||
final seasons = await api.showsSeriesIdSeasonsGet(seriesId: seriesModel.id, fields: [
|
||||
ItemFields.mediastreams,
|
||||
ItemFields.mediasources,
|
||||
ItemFields.overview,
|
||||
ItemFields.candownload,
|
||||
ItemFields.childcount,
|
||||
]);
|
||||
|
||||
newState = newState.copyWith(
|
||||
seasons: SeasonModel.seasonsFromDto(seasons.body?.items, ref)
|
||||
.map((element) => element.copyWith(
|
||||
|
|
@ -71,26 +75,11 @@ class SeriesDetailViewNotifier extends StateNotifier<SeriesModel?> {
|
|||
state = newState.copyWith(related: related.body);
|
||||
return response;
|
||||
} catch (e) {
|
||||
_tryToCreateOfflineState(seriesModel);
|
||||
log("Error fetching series details: $e");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _tryToCreateOfflineState(ItemBaseModel series) async {
|
||||
final syncNotifier = ref.read(syncProvider.notifier);
|
||||
final syncedItem = await syncNotifier.getSyncedItem(series);
|
||||
if (syncedItem == null) return;
|
||||
final seriesModel = syncedItem.itemModel as SeriesModel;
|
||||
final allChildren = (await syncedItem.getNestedChildren(ref)).map((e) => e.itemModel).toList();
|
||||
if (mounted) {
|
||||
state = seriesModel.copyWith(
|
||||
availableEpisodes: allChildren.whereType<EpisodeModel>().toList(),
|
||||
seasons: allChildren.whereType<SeasonModel>().toList(),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
void updateEpisodeInfo(EpisodeModel episode) {
|
||||
final index = state?.availableEpisodes?.indexOf(episode);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import 'package:chopper/chopper.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.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);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'dart:typed_data';
|
|||
import 'package:chopper/chopper.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path/path.dart';
|
||||
|
||||
import 'package:fladder/fake/fake_jellyfin_open_api.dart';
|
||||
|
|
@ -12,10 +13,13 @@ import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
|||
import 'package:fladder/models/account_model.dart';
|
||||
import 'package:fladder/models/credentials_model.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/items/episode_model.dart';
|
||||
import 'package:fladder/models/items/item_shared_models.dart';
|
||||
import 'package:fladder/models/items/media_segments_model.dart';
|
||||
import 'package:fladder/models/items/trick_play_model.dart';
|
||||
import 'package:fladder/providers/auth_provider.dart';
|
||||
import 'package:fladder/providers/image_provider.dart';
|
||||
import 'package:fladder/providers/sync_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/util/jellyfin_extension.dart';
|
||||
|
||||
|
|
@ -84,21 +88,78 @@ class JellyService {
|
|||
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));
|
||||
try {
|
||||
final response = await api.itemsItemIdGet(
|
||||
userId: account?.id,
|
||||
itemId: itemId,
|
||||
);
|
||||
return response.copyWith(body: ItemBaseModel.fromBaseDto(response.bodyOrThrow, ref));
|
||||
} catch (e) {
|
||||
final item = (await ref.read(syncProvider.notifier).getSyncedItem(itemId))?.itemModel;
|
||||
return Response<ItemBaseModel>(
|
||||
http.Response("", 202),
|
||||
item,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Response<BaseItemDto>> usersUserIdItemsItemIdGetBaseItem({
|
||||
String? itemId,
|
||||
}) async {
|
||||
final response = await api.itemsItemIdGet(
|
||||
try {
|
||||
return await api.itemsItemIdGet(
|
||||
userId: account?.id,
|
||||
itemId: itemId,
|
||||
);
|
||||
} catch (e) {
|
||||
return ref.read(syncProvider.notifier).getSyncedItem(itemId).then(
|
||||
(value) => value?.data != null
|
||||
? Response<BaseItemDto>(
|
||||
http.Response("", 202),
|
||||
value?.data,
|
||||
)
|
||||
: Response<BaseItemDto>(
|
||||
http.Response("", 404),
|
||||
null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Response<UserData>> userItemsItemIdUserDataGet({
|
||||
String? itemId,
|
||||
}) async {
|
||||
final response = await api.userItemsItemIdUserDataGet(
|
||||
userId: account?.id,
|
||||
itemId: itemId,
|
||||
);
|
||||
return response;
|
||||
return response.copyWith(
|
||||
body: UserData.fromDto(response.bodyOrThrow),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<UserData>?> userItemsItemIdUserDataPost({
|
||||
String? itemId,
|
||||
required UserData? body,
|
||||
}) async {
|
||||
if (body == null) {
|
||||
return null;
|
||||
}
|
||||
final response = await api.userItemsItemIdUserDataPost(
|
||||
userId: account?.id,
|
||||
itemId: itemId,
|
||||
body: UpdateUserItemDataDto(
|
||||
playCount: body.playCount,
|
||||
playbackPositionTicks: body.playbackPositionTicks,
|
||||
isFavorite: body.isFavourite,
|
||||
played: body.played,
|
||||
lastPlayedDate: body.lastPlayed,
|
||||
itemId: itemId,
|
||||
),
|
||||
);
|
||||
return response.copyWith(
|
||||
body: UserData.fromDto(response.bodyOrThrow),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<ServerQueryResult>> itemsGet({
|
||||
|
|
@ -491,8 +552,15 @@ class JellyService {
|
|||
|
||||
Future<Response> sessionsPlayingStoppedPost({
|
||||
required PlaybackStopInfo? body,
|
||||
}) =>
|
||||
api.sessionsPlayingStoppedPost(body: body);
|
||||
}) {
|
||||
final positionTicks = body?.positionTicks;
|
||||
if (positionTicks != null) {
|
||||
ref
|
||||
.read(syncProvider.notifier)
|
||||
.updatePlaybackPosition(itemId: body?.itemId, position: Duration(milliseconds: positionTicks ~/ 10000));
|
||||
}
|
||||
return api.sessionsPlayingStoppedPost(body: body);
|
||||
}
|
||||
|
||||
Future<Response> sessionsPlayingProgressPost({required PlaybackProgressInfo? body}) async =>
|
||||
api.sessionsPlayingProgressPost(body: body);
|
||||
|
|
@ -533,25 +601,51 @@ class JellyService {
|
|||
bool? enableUserData,
|
||||
ShowsSeriesIdEpisodesGetSortBy? sortBy,
|
||||
}) async {
|
||||
return api.showsSeriesIdEpisodesGet(
|
||||
seriesId: seriesId,
|
||||
userId: account?.id,
|
||||
fields: [
|
||||
...?fields,
|
||||
ItemFields.parentid,
|
||||
],
|
||||
isMissing: isMissing,
|
||||
limit: limit,
|
||||
sortBy: sortBy,
|
||||
enableUserData: enableUserData,
|
||||
startIndex: startIndex,
|
||||
adjacentTo: adjacentTo,
|
||||
startItemId: startItemId,
|
||||
season: season,
|
||||
seasonId: seasonId,
|
||||
enableImages: enableImages,
|
||||
enableImageTypes: enableImageTypes,
|
||||
);
|
||||
try {
|
||||
var response = await api.showsSeriesIdEpisodesGet(
|
||||
seriesId: seriesId,
|
||||
userId: account?.id,
|
||||
fields: [
|
||||
...?fields,
|
||||
ItemFields.parentid,
|
||||
],
|
||||
isMissing: isMissing,
|
||||
limit: limit,
|
||||
sortBy: sortBy,
|
||||
enableUserData: enableUserData,
|
||||
startIndex: startIndex,
|
||||
adjacentTo: adjacentTo,
|
||||
startItemId: startItemId,
|
||||
season: season,
|
||||
seasonId: seasonId,
|
||||
enableImages: enableImages,
|
||||
enableImageTypes: enableImageTypes,
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
final seriesItem = await ref.read(syncProvider.notifier).getSyncedItem(seriesId);
|
||||
if (seriesItem != null) {
|
||||
final episodes = await ref.read(syncProvider.notifier).getNestedChildren(seriesItem)
|
||||
..where((e) => e.itemModel is EpisodeModel);
|
||||
return Response<BaseItemDtoQueryResult>(
|
||||
http.Response("", 200),
|
||||
BaseItemDtoQueryResult(
|
||||
items: episodes.map((e) => e.data).nonNulls.toList(),
|
||||
totalRecordCount: episodes.length,
|
||||
startIndex: 0,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Response<BaseItemDtoQueryResult>(
|
||||
http.Response("", 400),
|
||||
const BaseItemDtoQueryResult(
|
||||
items: [],
|
||||
totalRecordCount: 0,
|
||||
startIndex: 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<ItemBaseModel>> fetchEpisodeFromShow({
|
||||
|
|
@ -566,11 +660,22 @@ class JellyService {
|
|||
String? itemId,
|
||||
int? limit,
|
||||
}) async {
|
||||
return api.itemsItemIdSimilarGet(
|
||||
userId: account?.id,
|
||||
itemId: itemId,
|
||||
limit: limit,
|
||||
);
|
||||
try {
|
||||
return await api.itemsItemIdSimilarGet(userId: account?.id, itemId: itemId, limit: limit, fields: [
|
||||
ItemFields.parentid,
|
||||
ItemFields.candelete,
|
||||
ItemFields.candownload,
|
||||
]);
|
||||
} catch (e) {
|
||||
return Response<BaseItemDtoQueryResult>(
|
||||
http.Response("", 400),
|
||||
const BaseItemDtoQueryResult(
|
||||
items: [],
|
||||
totalRecordCount: 0,
|
||||
startIndex: 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Response<BaseItemDtoQueryResult>> usersUserIdItemsGet({
|
||||
|
|
@ -692,8 +797,9 @@ class JellyService {
|
|||
bool? enableUserData,
|
||||
bool? isMissing,
|
||||
List<ItemFields>? fields,
|
||||
}) =>
|
||||
api.showsSeriesIdSeasonsGet(
|
||||
}) async {
|
||||
try {
|
||||
final response = await api.showsSeriesIdSeasonsGet(
|
||||
seriesId: seriesId,
|
||||
isMissing: isMissing,
|
||||
enableUserData: enableUserData,
|
||||
|
|
@ -702,6 +808,31 @@ class JellyService {
|
|||
ItemFields.parentid,
|
||||
],
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
final seriesItem = await ref.read(syncProvider.notifier).getSyncedItem(seriesId);
|
||||
if (seriesItem != null) {
|
||||
final seasons = await ref.read(syncProvider.notifier).getChildren(seriesItem.id);
|
||||
return Response<BaseItemDtoQueryResult>(
|
||||
http.Response("", 200),
|
||||
BaseItemDtoQueryResult(
|
||||
items: seasons.map((e) => e.data).nonNulls.toList(),
|
||||
totalRecordCount: seasons.length,
|
||||
startIndex: 0,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Response<BaseItemDtoQueryResult>(
|
||||
http.Response("", 400),
|
||||
const BaseItemDtoQueryResult(
|
||||
items: [],
|
||||
totalRecordCount: 0,
|
||||
startIndex: 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<Response<QueryFilters>> itemsFilters2Get({
|
||||
String? parentId,
|
||||
|
|
@ -817,33 +948,75 @@ class JellyService {
|
|||
|
||||
Future<Response<UserItemDataDto>> usersUserIdFavoriteItemsItemIdPost({
|
||||
required String? itemId,
|
||||
}) =>
|
||||
api.userFavoriteItemsItemIdPost(
|
||||
}) async {
|
||||
Response<UserItemDataDto>? response;
|
||||
try {
|
||||
response = await api.userFavoriteItemsItemIdPost(
|
||||
itemId: itemId,
|
||||
userId: account?.id,
|
||||
);
|
||||
} finally {
|
||||
await ref
|
||||
.read(syncProvider.notifier)
|
||||
.updateFavoriteItem(itemId, isFavorite: true, responseSuccessful: response?.isSuccessful ?? false);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<Response<UserItemDataDto>> usersUserIdFavoriteItemsItemIdDelete({
|
||||
required String? itemId,
|
||||
}) =>
|
||||
api.userFavoriteItemsItemIdDelete(
|
||||
}) async {
|
||||
Response<UserItemDataDto>? response;
|
||||
try {
|
||||
response = await api.userFavoriteItemsItemIdDelete(
|
||||
itemId: itemId,
|
||||
userId: account?.id,
|
||||
);
|
||||
} finally {
|
||||
await ref
|
||||
.read(syncProvider.notifier)
|
||||
.updateFavoriteItem(itemId, isFavorite: false, responseSuccessful: response?.isSuccessful ?? false);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<Response<UserItemDataDto>> usersUserIdPlayedItemsItemIdPost({
|
||||
required String? itemId,
|
||||
DateTime? datePlayed,
|
||||
}) =>
|
||||
api.userPlayedItemsItemIdPost(itemId: itemId, userId: account?.id, datePlayed: datePlayed);
|
||||
}) async {
|
||||
Response<UserItemDataDto>? response;
|
||||
try {
|
||||
response = await api.userPlayedItemsItemIdPost(itemId: itemId, userId: account?.id, datePlayed: datePlayed);
|
||||
} finally {
|
||||
await ref.read(syncProvider.notifier).updatePlayedItem(
|
||||
itemId,
|
||||
datePlayed: datePlayed,
|
||||
played: true,
|
||||
responseSuccessful: response?.isSuccessful ?? false,
|
||||
);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<Response<UserItemDataDto>> usersUserIdPlayedItemsItemIdDelete({
|
||||
required String? itemId,
|
||||
}) =>
|
||||
api.userPlayedItemsItemIdDelete(
|
||||
}) async {
|
||||
Response<UserItemDataDto>? response;
|
||||
try {
|
||||
response = await api.userPlayedItemsItemIdDelete(
|
||||
itemId: itemId,
|
||||
userId: account?.id,
|
||||
);
|
||||
} finally {
|
||||
await ref.read(syncProvider.notifier).updatePlayedItem(
|
||||
itemId,
|
||||
played: false,
|
||||
responseSuccessful: response?.isSuccessful ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Future<Response<MediaSegmentsModel>?> mediaSegmentsGet({
|
||||
required String id,
|
||||
|
|
|
|||
|
|
@ -63,10 +63,6 @@ final photoViewSettingsProvider = StateNotifierProvider<PhotoViewSettingsNotifie
|
|||
return PhotoViewSettingsNotifier(ref);
|
||||
});
|
||||
|
||||
final testProviderProvider = StateProvider<int>((ref) {
|
||||
return 0;
|
||||
});
|
||||
|
||||
class PhotoViewSettingsNotifier extends StateNotifier<PhotoViewSettingsModel> {
|
||||
PhotoViewSettingsNotifier(this.ref) : super(PhotoViewSettingsModel());
|
||||
|
||||
|
|
|
|||
|
|
@ -16,13 +16,13 @@ Stream<SyncedItem?> syncedItem(Ref ref, ItemBaseModel? item) {
|
|||
return Stream.value(null);
|
||||
}
|
||||
|
||||
return ref.watch(syncProvider.notifier).db.getItem(id).watchSingleOrNull();
|
||||
return ref.watch(syncProvider.notifier).watchItem(id);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class SyncedChildren extends _$SyncedChildren {
|
||||
@override
|
||||
FutureOr<List<SyncedItem>> build(SyncedItem item) => ref.read(syncProvider.notifier).getChildren(item);
|
||||
FutureOr<List<SyncedItem>> build(SyncedItem item) => ref.read(syncProvider.notifier).getChildren(item.id);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import 'dart:developer';
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide ConnectionState;
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
|
@ -33,11 +33,13 @@ import 'package:fladder/models/syncing/sync_settings_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/connectivity_provider.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/providers/user_provider.dart';
|
||||
import 'package:fladder/screens/shared/fladder_snackbar.dart';
|
||||
import 'package:fladder/util/duration_extensions.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/migration/isar_drift_migration.dart';
|
||||
|
||||
|
|
@ -51,15 +53,49 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
|||
}
|
||||
|
||||
final Ref ref;
|
||||
late final db = AppDatabase(ref);
|
||||
late AppDatabase _db = AppDatabase(ref);
|
||||
final Directory mobileDirectory;
|
||||
final String subPath = "Synced";
|
||||
|
||||
bool updatingSyncStatus = false;
|
||||
|
||||
StreamSubscription<List<SyncedItem>>? _subscription;
|
||||
|
||||
@override
|
||||
set state(SyncSettingsModel value) {
|
||||
super.state = value;
|
||||
updateSyncStates();
|
||||
}
|
||||
|
||||
void migrateFromIsar() async {
|
||||
await isarMigration(ref, db, mainDirectory.path);
|
||||
await isarMigration(ref, _db, mainDirectory.path);
|
||||
_initializeQueryStream();
|
||||
}
|
||||
|
||||
Future<void> updateSyncStates() async {
|
||||
final lastState =
|
||||
(await _db.getAllItems.get()).where((item) => item.unSyncedData && item.userData != null).toList();
|
||||
if (updatingSyncStatus || lastState.isEmpty) return;
|
||||
updatingSyncStatus = true;
|
||||
try {
|
||||
for (final item in lastState) {
|
||||
if (item.userData == null) continue;
|
||||
final updatedItem =
|
||||
await ref.read(jellyApiProvider).userItemsItemIdUserDataPost(itemId: item.id, body: item.userData);
|
||||
if (updatedItem?.isSuccessful == true) {
|
||||
final syncedItem = item.copyWith(unSyncedData: false);
|
||||
await _db.insertItem(syncedItem);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// log('Error updating sync states: $e');
|
||||
} finally {
|
||||
updatingSyncStatus = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _init() {
|
||||
cleanupTemporaryFiles();
|
||||
ref.listen(
|
||||
|
|
@ -67,25 +103,29 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
|||
(previous, next) {
|
||||
if (previous?.id != next?.id) {
|
||||
if (next?.id != null) {
|
||||
_initializeQueryStream();
|
||||
_initializeQueryStream(id: next!.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
_initializeQueryStream();
|
||||
|
||||
ref.listen(connectivityStatusProvider, (_, next) {
|
||||
if (next != ConnectionState.offline) {
|
||||
updateSyncStates();
|
||||
}
|
||||
});
|
||||
migrateFromIsar();
|
||||
}
|
||||
|
||||
void _initializeQueryStream() async {
|
||||
final userId = ref.read(userProvider)?.id;
|
||||
if (userId == null) return;
|
||||
void _initializeQueryStream({String? id}) async {
|
||||
final userId = id ?? ref.read(userProvider)?.id;
|
||||
_subscription?.cancel();
|
||||
state = state.copyWith(items: []);
|
||||
|
||||
final queryStream = db.getParentItems.watch();
|
||||
if (userId == null) return;
|
||||
|
||||
final initItems = await db.getParentItems.get();
|
||||
final queryStream = _db.getParentItems.watch().distinct();
|
||||
final initItems = await _db.getParentItems.get();
|
||||
|
||||
state = state.copyWith(items: initItems);
|
||||
|
||||
|
|
@ -123,8 +163,6 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
|||
}
|
||||
}
|
||||
|
||||
StreamSubscription<List<SyncedItem>>? _subscription;
|
||||
|
||||
late final JellyService api = ref.read(jellyApiProvider);
|
||||
|
||||
String? get _savePath => !kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)
|
||||
|
|
@ -163,21 +201,25 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = state.copyWith(items: (await db.getParentItems.get()));
|
||||
Future<void> refresh() async => state = state.copyWith(items: (await _db.getParentItems.get()));
|
||||
|
||||
Future<List<SyncedItem>> getNestedChildren(SyncedItem item) async => _db.getNestedChildren(item);
|
||||
|
||||
Future<List<SyncedItem>> getChildren(String parentId) async => await _db.getChildren(parentId).get();
|
||||
|
||||
Future<List<SyncedItem>> getSiblings(SyncedItem syncedItem) async {
|
||||
if (syncedItem.parentId == null) return [];
|
||||
return getChildren(syncedItem.parentId!);
|
||||
}
|
||||
|
||||
Future<List<SyncedItem>> getNestedChildren(SyncedItem item) async => db.getNestedChildren(item);
|
||||
|
||||
Future<List<SyncedItem>> getChildren(SyncedItem root) async => await db.getChildren(root.id).get();
|
||||
|
||||
Future<SyncedItem?> getSyncedItem(ItemBaseModel? item) async {
|
||||
final id = item?.id;
|
||||
Future<SyncedItem?> getSyncedItem(String? id) async {
|
||||
if (id == null) return null;
|
||||
return await db.getItem(id).getSingleOrNull();
|
||||
return await _db.getItem(id).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<SyncedItem?> getParentItem(String id) async => await db.getParent(id).getSingleOrNull();
|
||||
Stream<SyncedItem?> watchItem(String id) => _db.getItem(id).watchSingleOrNull();
|
||||
|
||||
Future<SyncedItem?> getParentItem(String id) async => await _db.getParent(id).getSingleOrNull();
|
||||
|
||||
Future<SyncedItem> refreshSyncItem(SyncedItem item) async {
|
||||
List<SyncedItem> itemsToSync = await getNestedChildren(item);
|
||||
|
|
@ -196,7 +238,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
|||
|
||||
final itemModel = ItemBaseModel.fromBaseDto(itemResponse.bodyOrThrow, ref);
|
||||
|
||||
final syncedParent = await db.getItem(itemToSync.parentId ?? "").getSingleOrNull();
|
||||
final syncedParent = await _db.getItem(itemToSync.parentId ?? "").getSingleOrNull();
|
||||
|
||||
SyncedItem newSyncedItem = await _syncItemData(syncedParent, itemModel, itemResponse.bodyOrThrow);
|
||||
|
||||
|
|
@ -217,7 +259,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
|||
}
|
||||
}
|
||||
|
||||
await db.insertMultipleEntries(newItems);
|
||||
await _db.insertMultipleEntries(newItems);
|
||||
|
||||
return parentItem;
|
||||
}
|
||||
|
|
@ -235,7 +277,9 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
|||
ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory);
|
||||
}
|
||||
|
||||
fladderSnackbar(context, title: context.localized.syncAddItemForSyncing(item.detailedName(context) ?? "Unknown"));
|
||||
if (context.mounted) {
|
||||
fladderSnackbar(context, title: context.localized.syncAddItemForSyncing(item.detailedName(context) ?? "Unknown"));
|
||||
}
|
||||
final newSync = switch (item) {
|
||||
EpisodeModel episode => await syncSeries(item.parentBaseModel, episode: episode),
|
||||
SeasonModel season => await syncSeries(item.parentBaseModel, season: season),
|
||||
|
|
@ -254,7 +298,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
|||
}
|
||||
|
||||
void viewDatabase(BuildContext context) =>
|
||||
Navigator.of(context, rootNavigator: true).push(MaterialPageRoute(builder: (context) => DriftDbViewer(db)));
|
||||
Navigator.of(context, rootNavigator: true).push(MaterialPageRoute(builder: (context) => DriftDbViewer(_db)));
|
||||
|
||||
Future<bool> removeSync(BuildContext context, SyncedItem? item) async {
|
||||
try {
|
||||
|
|
@ -273,7 +317,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
|||
await ref.read(backgroundDownloaderProvider).cancelTaskWithId(item.taskId!);
|
||||
}
|
||||
|
||||
await db.deleteAllItems([...nestedChildren, item]);
|
||||
await _db.deleteAllItems([...nestedChildren, item]);
|
||||
|
||||
for (var i = 0; i < nestedChildren.length; i++) {
|
||||
final element = nestedChildren[i];
|
||||
|
|
@ -291,8 +335,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
|||
|
||||
return true;
|
||||
} catch (e) {
|
||||
log('Error deleting synced item');
|
||||
log(e.toString());
|
||||
log('Error deleting synced item ${e.toString()}');
|
||||
state = state.copyWith(items: state.items.map((e) => e.copyWith(markedForDelete: false)).toList());
|
||||
fladderSnackbar(context, title: context.localized.syncRemoveUnableToDeleteItem);
|
||||
return false;
|
||||
|
|
@ -395,7 +438,16 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
|||
return data?.copyWith(path: fileName);
|
||||
}
|
||||
|
||||
Future<void> updateItem(SyncedItem syncedItem) async => db.insertItem(syncedItem);
|
||||
Future<int> updateItem(SyncedItem item) async {
|
||||
SyncedItem syncedItem = item;
|
||||
try {
|
||||
await ref.read(jellyApiProvider).userItemsItemIdUserDataPost(itemId: syncedItem.id, body: syncedItem.userData);
|
||||
} catch (e) {
|
||||
log('Error updating item: ${syncedItem.id}');
|
||||
syncedItem = syncedItem.copyWith(unSyncedData: true);
|
||||
}
|
||||
return _db.insertItem(syncedItem);
|
||||
}
|
||||
|
||||
Future<SyncedItem> deleteFullSyncFiles(SyncedItem syncedItem, DownloadTask? task) async {
|
||||
await syncedItem.deleteDatFiles(ref);
|
||||
|
|
@ -504,15 +556,81 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
|||
return null;
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
await mainDirectory.delete(recursive: true);
|
||||
await db.clearDatabase();
|
||||
Future<void> removeAllSyncedData() async {
|
||||
if (await mainDirectory.exists()) {
|
||||
await mainDirectory.delete(recursive: true);
|
||||
}
|
||||
await _db.close();
|
||||
await _db.clearDatabase();
|
||||
_db = AppDatabase(ref);
|
||||
state = state.copyWith(items: []);
|
||||
}
|
||||
|
||||
Future<void> setup() async {
|
||||
state = state.copyWith(items: []);
|
||||
_init();
|
||||
Future<void> updatePlaybackPosition({String? itemId, required Duration position}) async {
|
||||
if (itemId == null) return;
|
||||
|
||||
final syncedItem = await _db.getItem(itemId).getSingleOrNull();
|
||||
if (syncedItem == null) return;
|
||||
|
||||
final item = syncedItem.itemModel;
|
||||
if (item == null) return;
|
||||
|
||||
final progress = position.inMilliseconds / (item.overview.runTime?.inMilliseconds ?? 0) * 100;
|
||||
|
||||
final updatedItem = syncedItem.copyWith(
|
||||
userData: syncedItem.userData?.copyWith(
|
||||
playbackPositionTicks: position.toRuntimeTicks,
|
||||
progress: progress,
|
||||
played: UserData.isPlayed(position, item.overview.runTime ?? Duration.zero),
|
||||
),
|
||||
);
|
||||
await _db.insertItem(updatedItem);
|
||||
}
|
||||
|
||||
Future<void> updatePlayedItem(String? itemId,
|
||||
{DateTime? datePlayed, required bool played, bool responseSuccessful = false}) async {
|
||||
if (itemId == null) return;
|
||||
|
||||
final syncedItem = _db.getItem(itemId).getSingleOrNull();
|
||||
syncedItem.then((item) async {
|
||||
if (item == null) return;
|
||||
final updatedUserData = item.userData?.copyWith(
|
||||
played: played,
|
||||
playbackPositionTicks: 0,
|
||||
progress: 0.0,
|
||||
lastPlayed: datePlayed ?? DateTime.now().toUtc(),
|
||||
);
|
||||
SyncedItem updatedItem = item.copyWith(userData: updatedUserData, unSyncedData: !responseSuccessful);
|
||||
|
||||
List<SyncedItem> children = [];
|
||||
final shouldUpdateChildren = {FladderItemType.series, FladderItemType.season}.contains(item.itemModel?.type);
|
||||
if (shouldUpdateChildren) {
|
||||
// Update child items with the same played status, jellyfin server does this was well
|
||||
// when marking a series or season as played
|
||||
children = (await getNestedChildren(item))
|
||||
.map((e) => e.copyWith(
|
||||
userData: e.userData?.copyWith(
|
||||
played: played,
|
||||
playbackPositionTicks: 0,
|
||||
progress: 0.0,
|
||||
),
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
await _db.insertMultipleEntries([updatedItem, ...children]);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateFavoriteItem(String? itemId, {required bool isFavorite, bool responseSuccessful = false}) async {
|
||||
if (itemId == null) return;
|
||||
|
||||
final syncedItem = _db.getItem(itemId).getSingleOrNull();
|
||||
syncedItem.then((item) async {
|
||||
if (item == null) return;
|
||||
final updatedUserData = item.userData?.copyWith(isFavourite: isFavorite);
|
||||
final updatedItem = item.copyWith(userData: updatedUserData, unSyncedData: !responseSuccessful);
|
||||
await _db.insertItem(updatedItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -520,14 +638,14 @@ extension SyncNotifierHelpers on SyncNotifier {
|
|||
Future<SyncedItem> createSyncItem(BaseItemDto response, {SyncedItem? parent}) async {
|
||||
final ItemBaseModel item = ItemBaseModel.fromBaseDto(response, ref);
|
||||
|
||||
final existingSyncedItem = await getSyncedItem(item);
|
||||
final existingSyncedItem = await getSyncedItem(item.id);
|
||||
|
||||
if (existingSyncedItem != null) return existingSyncedItem;
|
||||
|
||||
SyncedItem syncItem = await _syncItemData(parent, item, response);
|
||||
|
||||
if (parent == null) {
|
||||
await db.insertItem(syncItem);
|
||||
await _db.insertItem(syncItem);
|
||||
}
|
||||
|
||||
return syncItem.copyWith(
|
||||
|
|
@ -573,7 +691,7 @@ extension SyncNotifierHelpers on SyncNotifier {
|
|||
|
||||
if (!syncItem.directory.existsSync()) return null;
|
||||
|
||||
await db.insertItem(syncItem);
|
||||
await _db.insertItem(syncItem);
|
||||
|
||||
await syncFile(syncItem, skipDownload);
|
||||
|
||||
|
|
@ -665,7 +783,7 @@ extension SyncNotifierHelpers on SyncNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
await db.insertMultipleEntries(newItems);
|
||||
await _db.insertMultipleEntries(newItems);
|
||||
|
||||
for (var i = 0; i < itemsToDownload.length; i++) {
|
||||
final item = itemsToDownload[i];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue