diff --git a/lib/main.dart b/lib/main.dart index ec405c1..be6bca7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,7 @@ import 'package:rhythm_box/providers/audio_player_stream.dart'; import 'package:rhythm_box/providers/auth.dart'; import 'package:rhythm_box/providers/database.dart'; import 'package:rhythm_box/providers/endless_playback.dart'; +import 'package:rhythm_box/providers/error_notifier.dart'; import 'package:rhythm_box/providers/history.dart'; import 'package:rhythm_box/providers/palette.dart'; import 'package:rhythm_box/providers/recent_played.dart'; @@ -84,8 +85,8 @@ class RhythmApp extends StatelessWidget { translations: AppTranslations(), onInit: () => _initializeProviders(context), builder: (context, child) { - return SystemShell( - child: ScaffoldMessenger( + return ScaffoldMessenger( + child: SystemShell( child: child ?? const SizedBox(), ), ); @@ -97,6 +98,8 @@ class RhythmApp extends StatelessWidget { Get.lazyPut(() => SpotifyProvider()); Get.lazyPut(() => SyncedLyricsProvider()); + Get.put(ErrorNotifier()); + Get.put(DatabaseProvider()); Get.put(AuthenticationProvider()); diff --git a/lib/providers/audio_player_stream.dart b/lib/providers/audio_player_stream.dart index f6812fc..f85bc05 100644 --- a/lib/providers/audio_player_stream.dart +++ b/lib/providers/audio_player_stream.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:get/get.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:rhythm_box/providers/audio_player.dart'; +import 'package:rhythm_box/providers/error_notifier.dart'; import 'package:rhythm_box/providers/history.dart'; import 'package:rhythm_box/providers/palette.dart'; import 'package:rhythm_box/providers/scrobbler.dart'; @@ -126,7 +127,8 @@ class AudioPlayerStreamProvider extends GetxController { .addTrack(playback.state.value.activeTrack!); lastScrobbled = uid; } catch (e, stack) { - log('[Scrobbler] Error: $e; Trace:\n$stack'); + Get.find() + .logError('[Scrobbler] Error: $e', trace: stack); } }); } diff --git a/lib/providers/endless_playback.dart b/lib/providers/endless_playback.dart index b6e9392..a2d858a 100644 --- a/lib/providers/endless_playback.dart +++ b/lib/providers/endless_playback.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:developer'; import 'package:get/get.dart'; import 'package:rhythm_box/providers/audio_player.dart'; @@ -9,6 +8,8 @@ import 'package:rhythm_box/providers/user_preferences.dart'; import 'package:rhythm_box/services/audio_player/audio_player.dart'; import 'package:spotify/spotify.dart'; +import 'error_notifier.dart'; + class EndlessPlaybackProvider extends GetxController { late final _auth = Get.find(); late final _playback = Get.find(); @@ -88,7 +89,8 @@ class EndlessPlaybackProvider extends GetxController { }), ); } catch (e, stack) { - log('[EndlessPlayback] Error: $e; Trace:\n$stack'); + Get.find() + .logError('[EndlessPlayback] Error: $e', trace: stack); } } diff --git a/lib/providers/error_notifier.dart b/lib/providers/error_notifier.dart new file mode 100644 index 0000000..4939054 --- /dev/null +++ b/lib/providers/error_notifier.dart @@ -0,0 +1,37 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class ErrorNotifier extends GetxController { + Rx showing = Rx(null); + + void logError(String msg, {StackTrace? trace}) { + log('$msg${trace != null ? '\nTrace:\ntrace' : ''}'); + showError(msg); + } + + void showError(String msg) { + showing.value = MaterialBanner( + leading: const Icon(Icons.error), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Something went wrong...', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text(msg), + ], + ), + actions: [ + TextButton( + onPressed: () { + showing.value = null; + }, + child: const Text('Dismiss'), + ), + ], + ); + } +} diff --git a/lib/providers/scrobbler.dart b/lib/providers/scrobbler.dart index 1cad7d7..50b0d3a 100644 --- a/lib/providers/scrobbler.dart +++ b/lib/providers/scrobbler.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import 'dart:developer'; import 'package:drift/drift.dart'; import 'package:get/get.dart' hide Value; import 'package:rhythm_box/providers/database.dart'; +import 'package:rhythm_box/providers/error_notifier.dart'; import 'package:rhythm_box/services/artist.dart'; import 'package:rhythm_box/services/database/database.dart'; import 'package:scrobblenaut/scrobblenaut.dart'; @@ -44,7 +44,8 @@ class ScrobblerProvider extends GetxController { ), ); } catch (e, stack) { - log('[Scrobble] Error: $e; Trace:\n$stack'); + Get.find() + .logError('[Scrobbler] Error: $e', trace: stack); scrobbler.value = null; } } else { @@ -63,8 +64,9 @@ class ScrobblerProvider extends GetxController { timestamp: DateTime.now().toUtc(), trackNumber: track.trackNumber, ); - } catch (e, stackTrace) { - log('[Scrobble] Error: $e; Trace:\n$stackTrace'); + } catch (e, stack) { + Get.find() + .logError('[Scrobbler] Error: $e', trace: stack); } }); diff --git a/lib/providers/skip_segments.dart b/lib/providers/skip_segments.dart index 6d3a0b5..6b7774e 100644 --- a/lib/providers/skip_segments.dart +++ b/lib/providers/skip_segments.dart @@ -1,8 +1,7 @@ -import 'dart:developer'; - import 'package:dio/dio.dart'; import 'package:get/get.dart'; import 'package:rhythm_box/providers/database.dart'; +import 'package:rhythm_box/providers/error_notifier.dart'; import 'package:rhythm_box/providers/user_preferences.dart'; import 'package:rhythm_box/services/database/database.dart'; import 'package:rhythm_box/services/server/active_sourced_track.dart'; @@ -72,7 +71,7 @@ Future> getAndCacheSkipSegments(String id) async { ..where((s) => s.trackId.equals(id))) .get(); } catch (e, stack) { - log('[SkipSegment] Error: $e; Trace:\n$stack'); + Get.find().logError('[SkipSegment] Error: $e', trace: stack); return List.castFrom([]); } } diff --git a/lib/screens/player/view.dart b/lib/screens/player/view.dart index dd71bae..16fdf9b 100644 --- a/lib/screens/player/view.dart +++ b/lib/screens/player/view.dart @@ -102,11 +102,11 @@ class _PlayerScreenState extends State { maxWidth: maxAlbumSize, child: Hero( tag: const Key('current-active-track-album-art'), - child: ClipRRect( - borderRadius: - const BorderRadius.all(Radius.circular(16)), - child: AspectRatio( - aspectRatio: 1, + child: AspectRatio( + aspectRatio: 1, + child: ClipRRect( + borderRadius: + const BorderRadius.all(Radius.circular(16)), child: _albumArt != null ? AutoCacheImage( _albumArt!, @@ -230,21 +230,23 @@ class _PlayerScreenState extends State { stream: audioPlayer.shuffledStream, builder: (context, snapshot) { final shuffled = snapshot.data ?? false; - return IconButton( - icon: Icon( - shuffled - ? Icons.shuffle_on_outlined - : Icons.shuffle, + return Obx( + () => IconButton( + icon: Icon( + shuffled + ? Icons.shuffle_on_outlined + : Icons.shuffle, + ), + onPressed: _isFetchingActiveTrack + ? null + : () { + if (shuffled) { + audioPlayer.setShuffle(false); + } else { + audioPlayer.setShuffle(true); + } + }, ), - onPressed: _isFetchingActiveTrack - ? null - : () { - if (shuffled) { - audioPlayer.setShuffle(false); - } else { - audioPlayer.setShuffle(true); - } - }, ); }, ), diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 7bb4ae4..e8db415 100755 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -1,10 +1,11 @@ -import 'dart:developer'; import 'dart:io'; import 'package:get/get.dart'; import 'package:media_kit/media_kit.dart' hide Track; import 'package:flutter/foundation.dart'; import 'package:rhythm_box/platform.dart'; +import 'package:rhythm_box/providers/audio_player.dart'; +import 'package:rhythm_box/providers/error_notifier.dart'; import 'package:rhythm_box/services/local_track.dart'; import 'package:rhythm_box/services/server/server.dart'; import 'package:rhythm_box/widgets/tracks/querying_track_info.dart'; @@ -93,7 +94,7 @@ abstract class AudioPlayerInterface { ), ) { _mkPlayer.stream.error.listen((event) { - log('[Playback] Error: $event'); + Get.find().logError('[Playback] Error: $event'); }); } diff --git a/lib/services/audio_player/audio_player_impl.dart b/lib/services/audio_player/audio_player_impl.dart index e102aba..a997e27 100755 --- a/lib/services/audio_player/audio_player_impl.dart +++ b/lib/services/audio_player/audio_player_impl.dart @@ -90,16 +90,28 @@ class RhythmAudioPlayer extends AudioPlayerInterface Future skipToNext() async { Get.find().isQueryingTrackInfo.value = true; + Get.find().durationBuffered.value = + const Duration(seconds: 0); + Get.find().durationCurrent.value = + const Duration(seconds: 0); await _mkPlayer.next(); } Future skipToPrevious() async { Get.find().isQueryingTrackInfo.value = true; + Get.find().durationBuffered.value = + const Duration(seconds: 0); + Get.find().durationCurrent.value = + const Duration(seconds: 0); await _mkPlayer.previous(); } Future jumpTo(int index) async { Get.find().isQueryingTrackInfo.value = true; + Get.find().durationBuffered.value = + const Duration(seconds: 0); + Get.find().durationCurrent.value = + const Duration(seconds: 0); await _mkPlayer.jump(index); } diff --git a/lib/services/audio_player/custom_player.dart b/lib/services/audio_player/custom_player.dart index 3f122ee..c7bf4c1 100755 --- a/lib/services/audio_player/custom_player.dart +++ b/lib/services/audio_player/custom_player.dart @@ -1,10 +1,11 @@ import 'dart:async'; -import 'dart:developer'; +import 'package:get/get.dart'; import 'package:media_kit/media_kit.dart'; import 'package:flutter_broadcasts/flutter_broadcasts.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:audio_session/audio_session.dart'; import 'package:rhythm_box/platform.dart'; +import 'package:rhythm_box/providers/error_notifier.dart'; // ignore: implementation_imports import 'package:rhythm_box/services/audio_player/playback_state.dart'; @@ -49,7 +50,7 @@ class CustomPlayer extends Player { } }), stream.error.listen((event) { - log('[MediaKitError] $event'); + Get.find().logError('[Playback] Error: $event'); }), ]; PackageInfo.fromPlatform().then((packageInfo) { diff --git a/lib/services/lyrics/provider.dart b/lib/services/lyrics/provider.dart index 0f0b23c..53be6fd 100644 --- a/lib/services/lyrics/provider.dart +++ b/lib/services/lyrics/provider.dart @@ -1,11 +1,10 @@ -import 'dart:developer'; - import 'package:dio/dio.dart'; import 'package:drift/drift.dart'; import 'package:get/get.dart'; import 'package:lrc/lrc.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:rhythm_box/providers/database.dart'; +import 'package:rhythm_box/providers/error_notifier.dart'; import 'package:rhythm_box/providers/spotify.dart'; import 'package:rhythm_box/services/database/database.dart'; import 'package:rhythm_box/services/lyrics/model.dart'; @@ -164,8 +163,8 @@ class SyncedLyricsProvider extends GetxController { } return lyrics; - } catch (e, stackTrace) { - log('[Lyrics] Error: $e; Trace:\n$stackTrace'); + } catch (e, stack) { + Get.find().logError('[Lyrics] Error: $e', trace: stack); return SubtitleSimple( uri: Uri.parse('https://example.com/not-found'), name: 'Lyrics Not Found', diff --git a/lib/services/rhythm_media.dart b/lib/services/rhythm_media.dart index a9d8c5a..6622287 100644 --- a/lib/services/rhythm_media.dart +++ b/lib/services/rhythm_media.dart @@ -1,9 +1,10 @@ -import 'dart:developer'; import 'dart:io'; +import 'package:get/get.dart'; import 'package:media_kit/media_kit.dart' hide Track; import 'package:flutter/foundation.dart'; import 'package:rhythm_box/platform.dart'; +import 'package:rhythm_box/providers/error_notifier.dart'; import 'package:rhythm_box/services/audio_player/custom_player.dart'; import 'package:rhythm_box/services/local_track.dart'; import 'package:rhythm_box/services/sourced_track/sourced_track.dart'; @@ -85,7 +86,7 @@ abstract class AudioPlayerInterface { ), ) { _mkPlayer.stream.error.listen((event) { - log('[Playback] Error: $event'); + Get.find().logError('[Playback] Error: $event'); }); } diff --git a/lib/services/server/active_sourced_track.dart b/lib/services/server/active_sourced_track.dart index b07ff4f..f8ac8f0 100755 --- a/lib/services/server/active_sourced_track.dart +++ b/lib/services/server/active_sourced_track.dart @@ -1,7 +1,6 @@ -import 'dart:developer'; - import 'package:get/get.dart'; import 'package:rhythm_box/providers/audio_player.dart'; +import 'package:rhythm_box/providers/error_notifier.dart'; import 'package:rhythm_box/services/audio_player/audio_player.dart'; import 'package:rhythm_box/services/sourced_track/models/source_info.dart'; import 'package:rhythm_box/services/sourced_track/sourced_track.dart'; @@ -41,7 +40,9 @@ class ActiveSourcedTrackProvider extends GetxController { await audioPlayer.removeTrack(oldActiveIndex); await playback.jumpToTrack(newTrack); } catch (e, stack) { - log('[Playback] Failed to swap with siblings. Error: $e; Trace:\n$stack'); + Get.find().logError( + '[Playback] Failed to swap with siblings. Error: $e', + trace: stack); } finally { query.isQueryingTrackInfo.value = false; await audioPlayer.resume(); diff --git a/lib/services/server/routes/playback.dart b/lib/services/server/routes/playback.dart index e2fb5bf..fcc282c 100755 --- a/lib/services/server/routes/playback.dart +++ b/lib/services/server/routes/playback.dart @@ -1,9 +1,8 @@ -import 'dart:developer'; - import 'package:dio/dio.dart' hide Response; import 'package:flutter/foundation.dart'; import 'package:get/get.dart' hide Response; import 'package:rhythm_box/providers/audio_player.dart'; +import 'package:rhythm_box/providers/error_notifier.dart'; import 'package:rhythm_box/services/audio_player/audio_player.dart'; import 'package:rhythm_box/services/server/active_sourced_track.dart'; import 'package:rhythm_box/services/server/sourced_track.dart'; @@ -57,8 +56,9 @@ class ServerPlaybackRoutesProvider { }, headers: res.headers.map, ); - } catch (e, stackTrace) { - log('[PlaybackSever] Error: $e; Trace:\n $stackTrace'); + } catch (e, stack) { + Get.find() + .logError('[PlaybackSever] Error: $e', trace: stack); return Response.internalServerError(); } } diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index dc615b6..3138296 100755 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -1,10 +1,9 @@ -import 'dart:developer'; - import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:get/get.dart' hide Value; import 'package:http/http.dart'; import 'package:rhythm_box/providers/database.dart'; +import 'package:rhythm_box/providers/error_notifier.dart'; import 'package:rhythm_box/services/database/database.dart'; import 'package:rhythm_box/services/utils.dart'; import 'package:spotify/spotify.dart'; @@ -242,7 +241,8 @@ class YoutubeSourcedTrack extends SourcedTrack { ]; } on VideoUnplayableException catch (e) { // Ignore this error and continue with the search - log('[Source][YoutubeMusic] Unable to search data: $e'); + Get.find().logError( + '[Source][YoutubeMusic] Unable to play stream on youtube: $e'); } } @@ -250,7 +250,7 @@ class YoutubeSourcedTrack extends SourcedTrack { final searchResults = await youtubeClient.search.search( query, - filter: const SearchFilter('CAMSAhAB'), + filter: TypeFilters.video, ); if (ServiceUtils.onlyContainsEnglish(query)) { diff --git a/lib/shells/system_shell.dart b/lib/shells/system_shell.dart index 6f3ca9f..16bb1ce 100644 --- a/lib/shells/system_shell.dart +++ b/lib/shells/system_shell.dart @@ -1,12 +1,43 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:rhythm_box/platform.dart'; +import 'package:rhythm_box/providers/error_notifier.dart'; import 'package:window_manager/window_manager.dart'; -class SystemShell extends StatelessWidget { +class SystemShell extends StatefulWidget { final Widget child; const SystemShell({super.key, required this.child}); + @override + State createState() => _SystemShellState(); +} + +class _SystemShellState extends State { + late final ErrorNotifier _errorNotifier = Get.find(); + + StreamSubscription? _subscription; + + @override + void initState() { + super.initState(); + _subscription = _errorNotifier.showing.listen((value) { + if (value == null) { + ScaffoldMessenger.of(context).clearMaterialBanners(); + } else { + ScaffoldMessenger.of(context).showMaterialBanner(value); + } + }); + } + + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { if (PlatformInfo.isMacOS) { @@ -21,12 +52,12 @@ class SystemShell extends StatelessWidget { thickness: 0.3, height: 0.3, ), - Expanded(child: child), + Expanded(child: widget.child), ], ), ); } - return child; + return widget.child; } } diff --git a/lib/widgets/lyrics/synced_lyrics.dart b/lib/widgets/lyrics/synced_lyrics.dart index 6d349c9..cab83e2 100644 --- a/lib/widgets/lyrics/synced_lyrics.dart +++ b/lib/widgets/lyrics/synced_lyrics.dart @@ -50,25 +50,27 @@ class _SyncedLyricsState extends State { Theme.of(context).colorScheme.onSurface.withOpacity(0.5); void _syncLyricsProgress() { - for (var idx = 0; idx < _lyric!.lyrics.length; idx++) { - final lyricSlice = _lyric!.lyrics[idx]; - final lyricNextSlice = - idx + 1 < _lyric!.lyrics.length ? _lyric!.lyrics[idx + 1] : null; - final isActive = _playback.durationCurrent.value.inSeconds >= - lyricSlice.time.inSeconds && - (lyricNextSlice == null || - lyricNextSlice.time.inSeconds > - _playback.durationCurrent.value.inSeconds); - if (isActive) { - _autoScrollController.scrollToIndex( - idx, - preferPosition: AutoScrollPosition.middle, - ); - return; + if (_isLyricSynced) { + for (var idx = 0; idx < _lyric!.lyrics.length; idx++) { + final lyricSlice = _lyric!.lyrics[idx]; + final lyricNextSlice = + idx + 1 < _lyric!.lyrics.length ? _lyric!.lyrics[idx + 1] : null; + final isActive = _playback.durationCurrent.value.inSeconds >= + lyricSlice.time.inSeconds && + (lyricNextSlice == null || + lyricNextSlice.time.inSeconds > + _playback.durationCurrent.value.inSeconds); + if (isActive) { + _autoScrollController.scrollToIndex( + idx, + preferPosition: AutoScrollPosition.middle, + ); + return; + } } } - if (_lyric!.lyrics.isNotEmpty) { + if (_lyric!.lyrics.isNotEmpty || !_isLyricSynced) { _autoScrollController.scrollToIndex( 0, preferPosition: AutoScrollPosition.begin, @@ -120,6 +122,18 @@ class _SyncedLyricsState extends State { child: CircularProgressIndicator(), ), ), + if (_lyric != null && _lyric!.lyrics.isNotEmpty && !_isLyricSynced) + SliverToBoxAdapter( + child: Text( + 'Lyrics isn\'t synced', + textAlign: MediaQuery.of(context).size.width >= 720 + ? TextAlign.center + : TextAlign.left, + ).paddingSymmetric( + horizontal: 24, + vertical: 8, + ), + ), if (_lyric != null && _lyric!.lyrics.isNotEmpty) SliverList.builder( itemCount: _lyric!.lyrics.length, @@ -132,7 +146,8 @@ class _SyncedLyricsState extends State { lyricSlice.time.inSeconds && (lyricNextSlice == null || lyricNextSlice.time.inSeconds > - _playback.durationCurrent.value.inSeconds); + _playback.durationCurrent.value.inSeconds) && + _isLyricSynced; if (_playback.durationCurrent.value.inSeconds == lyricSlice.time.inSeconds && @@ -142,6 +157,7 @@ class _SyncedLyricsState extends State { preferPosition: AutoScrollPosition.middle, ); } + return AutoScrollTag( key: ValueKey(idx), index: idx, @@ -215,6 +231,7 @@ class _SyncedLyricsState extends State { children: [ Text( 'Lyrics Not Found', + textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge, ), const Text( diff --git a/pubspec.yaml b/pubspec.yaml index 9908ca9..6ef2c17 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+6 +version: 1.0.0+7 environment: sdk: ^3.5.0