Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 34 additions & 5 deletions lib/bootstrap/platform/platform_app_wrapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,48 @@ import 'package:fladder/bootstrap/platform/base_app_wrapper.dart';
import 'package:fladder/bootstrap/platform/desktop_platform_wrapper.dart';
import 'package:fladder/bootstrap/platform/mobile_app_wrapper.dart';
import 'package:fladder/bootstrap/platform/web_app_wrapper.dart';
import 'package:fladder/providers/connectivity_provider.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';

class PlatformAppWrapper extends ConsumerWidget {
class PlatformAppWrapper extends ConsumerStatefulWidget {
const PlatformAppWrapper({super.key, required this.builder});

final PlatformAppBuilder builder;

@override
Widget build(BuildContext context, WidgetRef ref) {
if (kIsWeb) return WebAppWrapper(builder: builder);
ConsumerState<ConsumerStatefulWidget> createState() => _PlatformAppWrapperState();
}

class _PlatformAppWrapperState extends ConsumerState<PlatformAppWrapper> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}

@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
// Safety check to ensure connectivity status is up to date when the app is resumed
ref.read(connectivityStatusProvider.notifier).checkConnectivity();
default:
break;
}
}

@override
Widget build(BuildContext context) {
if (kIsWeb) return WebAppWrapper(builder: widget.builder);

if (AdaptiveLayout.isDesktop(context)) return DesktopAppWrapper(builder: builder);
if (AdaptiveLayout.isDesktop(context)) return DesktopAppWrapper(builder: widget.builder);

return MobileAppWrapper(builder: builder);
return MobileAppWrapper(builder: widget.builder);
}
}
66 changes: 29 additions & 37 deletions lib/models/playback/playback_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,7 @@ class PlaybackModelHelper {
oldModel: currentModel,
);
if (newModel == null) return null;
final advancedQueue =
currentModel?.playbackQueue.advanceFromCurrentTo(currentModel.item.id, newItem.id);
final advancedQueue = currentModel?.playbackQueue.advanceFromCurrentTo(currentModel.item.id, newItem.id);
final modelToLoad = advancedQueue != null ? newModel.updatePlaybackQueue(advancedQueue) : newModel;
ref.read(videoPlayerProvider.notifier).loadPlaybackItem(modelToLoad, Duration.zero);
return modelToLoad;
Expand Down Expand Up @@ -306,6 +305,24 @@ class PlaybackModelHelper {
}
}

Future<PlaybackModel?> getOfflineModel() => _createOfflinePlaybackModel(
fullItem,
item.streamModel,
syncedItem,
oldModel: oldModel,
queueSource: effectiveQueueSource,
);

Future<PlaybackModel?> getServerModel(PlaybackType type) => _createServerPlaybackModel(
fullItem,
item.streamModel,
forcedPlaybackType ?? type,
oldModel: oldModel,
libraryQueue: queue,
queueSource: effectiveQueueSource,
startPosition: actualStartPosition,
);

if (((showPlaybackOptions || firstItemIsSynced) && !isOffline) && context != null) {
final playbackType = await showPlaybackTypeSelection(
context: context,
Expand All @@ -315,42 +332,17 @@ class PlaybackModelHelper {
if (!context.mounted) return null;

return switch (playbackType) {
PlaybackType.directStream || PlaybackType.transcode || PlaybackType.tv => await _createServerPlaybackModel(
fullItem,
item.streamModel,
forcedPlaybackType ?? playbackType,
oldModel: oldModel,
libraryQueue: queue,
queueSource: effectiveQueueSource,
startPosition: actualStartPosition,
),
PlaybackType.offline => await _createOfflinePlaybackModel(
fullItem,
item.streamModel,
syncedItem,
oldModel: oldModel,
queueSource: effectiveQueueSource,
),
null => null
PlaybackType.directStream || PlaybackType.transcode || PlaybackType.tv => await getServerModel(playbackType!),
PlaybackType.offline => await getOfflineModel(),
null => null,
};
} else {
return (await _createServerPlaybackModel(
fullItem,
item.streamModel,
forcedPlaybackType ?? PlaybackType.directStream,
startPosition: actualStartPosition,
oldModel: oldModel,
libraryQueue: queue,
queueSource: effectiveQueueSource,
)) ??
await _createOfflinePlaybackModel(
fullItem,
item.streamModel,
syncedItem,
oldModel: oldModel,
queueSource: effectiveQueueSource,
);
}

if (isOffline) {
return await getOfflineModel();
}

return await getServerModel(PlaybackType.directStream) ?? await getOfflineModel();
} catch (e) {
log("Error creating playback model: ${e.toString()}");
return null;
Expand Down Expand Up @@ -392,7 +384,7 @@ class PlaybackModelHelper {
newStreamModel?.subStreams,
newStreamModel?.defaultSubStreamIndex);

//Native player does not allow for loading external subtitles with transcoding
//Native player does not allow for loading external subtitles with transcoding
final isNativePlayer =
ref.read(videoPlayerSettingsProvider.select((value) => value.wantedPlayer == PlayerOptions.nativePlayer));
final isExternalSub = newStreamModel?.currentSubStream?.isExternal == true;
Expand Down
3 changes: 2 additions & 1 deletion lib/providers/api_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class JellyRequest implements Interceptor {
@override
FutureOr<Response<BodyType>> intercept<BodyType>(Chain<BodyType> chain) async {
final connectivityNotifier = ref.read(connectivityStatusProvider.notifier);
// final serverUrl = "https://example.com"; // ref.read(serverUrlProvider); --- IGNORE ---
final serverUrl = ref.read(serverUrlProvider);

if (serverUrl == null || serverUrl.isEmpty) {
Expand All @@ -112,7 +113,7 @@ class JellyRequest implements Interceptor {
),
);

connectivityNotifier.checkConnectivity();
unawaited(connectivityNotifier.checkConnectivity());
return response;
} catch (e) {
if (!_isConnectionError(e) || attempt == _maxRetries) {
Expand Down
26 changes: 23 additions & 3 deletions lib/providers/connectivity_provider.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';

Expand Down Expand Up @@ -48,7 +49,7 @@ class ConnectivityStatus extends _$ConnectivityStatus {
}
}

void onStateChange(List<ConnectivityResult> connectivityResult) async {
Future<void> onStateChange(List<ConnectivityResult> connectivityResult) async {
if (connectivityResult.contains(ConnectivityResult.ethernet)) {
state = ConnectionState.ethernet;
} else if (connectivityResult.contains(ConnectivityResult.wifi)) {
Expand All @@ -68,9 +69,28 @@ class ConnectivityStatus extends _$ConnectivityStatus {
ref.read(localConnectionAvailableProvider.notifier).update((state) => correctServerResponse);
}

void checkConnectivity() async {
Future<void> checkConnectivity() async {
final connectivityResult = await Connectivity().checkConnectivity();
onStateChange(connectivityResult);
final serverUrl = ref.read(serverUrlProvider);
final checkServer = await probeJellyfinUrl(
serverUrl ?? "",
);
if (checkServer != null) {
onStateChange(connectivityResult);
} else {
onStateChange([ConnectivityResult.none]);
}
}

ConnectionState getConnectivityStates() {
unawaited(ref.read(jellyApiProvider).systemInfoPublicGet().then(
(value) async {
if (!value.isSuccessful) {
onStateChange([ConnectivityResult.none]);
}
},
));
return state;
}
}

Expand Down
25 changes: 25 additions & 0 deletions lib/providers/items/album_details_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/album_model.dart';
import 'package:fladder/models/items/audio_model.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/sync_provider.dart';

final albumDetailsProvider =
StateNotifierProvider.autoDispose.family<AlbumDetailsNotifier, AlbumModel?, String>((ref, id) {
Expand Down Expand Up @@ -45,6 +47,15 @@ class AlbumDetailsNotifier extends StateNotifier<AlbumModel?> {

Future<void> fetchTracks() async {
if (state == null) return;
if (ref.read(connectivityStatusProvider) == ConnectionState.offline) {
final tracks = (await ref.read(syncProvider.notifier).getChildren(state!.id))
.map((item) => item.itemModel)
.whereType<AudioModel>()
.toList();
state = state?.copyWith(tracks: tracks);
return;
}

try {
final response = await api.itemsGet(
parentId: state!.id,
Expand All @@ -70,6 +81,20 @@ class AlbumDetailsNotifier extends StateNotifier<AlbumModel?> {

Future<void> fetchArtistRelated() async {
if (state == null) return;
if (ref.read(connectivityStatusProvider) == ConnectionState.offline) {
final parentId = state!.parentId;
if (parentId == null) return;

final albums = (await ref.read(syncProvider.notifier).getChildren(parentId))
.map((item) => item.itemModel)
.whereType<AlbumModel>()
.where((album) => album.id != state!.id)
.toList();

state = state?.copyWith(relatedAlbums: albums);
return;
}

try {
final artistIds = state!.artistIds.isNotEmpty ? state!.artistIds : state!.albumArtistIds;
if (artistIds.isEmpty) return;
Expand Down
Loading
Loading