From 2461c82e1d687deedc4e94f9a0f03a02a1ae157f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Fri, 24 May 2024 00:10:11 +0200 Subject: [PATCH] fix: loading albums and songs --- bruno/Subsonic API/getMusicDirectory.bru | 4 +- lib/api/audio/audioplayer_service.dart | 52 ++++- lib/api/subsonic/album.dart | 9 +- lib/api/subsonic/album.g.dart | 4 +- lib/api/subsonic/song.dart | 10 +- lib/api/subsonic/song.g.dart | 4 +- lib/api/subsonic/subsonic.dart | 10 +- lib/main.dart | 47 +++-- lib/widgets/player.dart | 181 ++++++++++++------ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 8 + pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 16 files changed, 241 insertions(+), 100 deletions(-) diff --git a/bruno/Subsonic API/getMusicDirectory.bru b/bruno/Subsonic API/getMusicDirectory.bru index 9c763cc..210bac2 100644 --- a/bruno/Subsonic API/getMusicDirectory.bru +++ b/bruno/Subsonic API/getMusicDirectory.bru @@ -5,11 +5,11 @@ meta { } get { - url: {{baseUrl}}/rest/getMusicDirectory?id=1aee72dc6749f4cb906f7d740ee23b78 + url: {{baseUrl}}/rest/getMusicDirectory?id=615eb32e4ca36d6ab405852a2842f6b6 body: none auth: none } query { - id: 1aee72dc6749f4cb906f7d740ee23b78 + id: 615eb32e4ca36d6ab405852a2842f6b6 } diff --git a/lib/api/audio/audioplayer_service.dart b/lib/api/audio/audioplayer_service.dart index 903fc97..b51f213 100644 --- a/lib/api/audio/audioplayer_service.dart +++ b/lib/api/audio/audioplayer_service.dart @@ -1,7 +1,10 @@ +import 'package:fast_cached_network_image/fast_cached_network_image.dart'; +import 'package:flutter/material.dart'; import 'package:just_audio/just_audio.dart'; import 'package:just_audio_background/just_audio_background.dart'; import 'package:ocarina/api/subsonic/song.dart'; import 'package:ocarina/main.dart'; +import 'package:ocarina/util/util.dart'; /// Service used to control the audio player class AudioPlayerService { @@ -15,19 +18,57 @@ class AudioPlayerService { AudioPlayerService._internal(); /// The [AudioPlayer] instance - final player = AudioPlayer(); + final _player = AudioPlayer(); + + /// True if [AudioPlayer] instance is playing + bool get isPlaying => _player.playing; /// Currently playing song /// /// Null if no song is loaded - Song? song; + Song? get song => _song; + + set song(Song? s) { + _song = s; + logger.d("CHANGE song"); + songNotifier.value = s; + + _setColorScheme(); + } + + Song? _song; + + /// Pauses playback + Future pause() async { + await _player.pause(); + logger.d("Paused"); + } + + /// Resumes playback + void resume() { + _player.play(); + logger.d("Playing"); + } + + /// Sets color scheme from image + Future _setColorScheme() async { + if (AudioPlayerService().song == null) { + themeNotifier.value = ColorScheme.fromSeed(seedColor: Colors.deepPurple); + } + themeNotifier.value = await ColorScheme.fromImageProvider( + provider: FastCachedImageProvider( + AudioPlayerService().song!.coverArtUrl, + ), + ); + logger.d(AudioPlayerService().song!.coverArtUrl); + } /// Plays the passed [Song] as a file Future playFile({Song? song}) async { final doCache = sp.getBool("doCache") ?? true; if (song == null && this.song == null) return; song ??= this.song; - await player.setAudioSource( + await _player.setAudioSource( doCache ? LockCachingAudioSource( Uri.parse(song!.streamUrl), @@ -50,7 +91,8 @@ class AudioPlayerService { ), ), ); - await player.seek(Duration.zero); - await player.play(); + await _player.seek(Duration.zero); + playerKey.currentState?.update(); + resume(); } } diff --git a/lib/api/subsonic/album.dart b/lib/api/subsonic/album.dart index 5a9b82a..24e55ec 100644 --- a/lib/api/subsonic/album.dart +++ b/lib/api/subsonic/album.dart @@ -7,7 +7,7 @@ class Album { Album({ required this.id, required this.name, - required String coverArtId, + required this.coverArtId, required this.playCount, required this.artistName, required this.artistId, @@ -15,12 +15,13 @@ class Album { required this.songCount, required this.genres, required this.duration, - }) : _coverArtId = coverArtId; + }); final String id; final String name; @JsonKey(name: "coverArt") - final String _coverArtId; + final String coverArtId; + @JsonKey(defaultValue: 0) final int playCount; @JsonKey(name: "artist") final String artistName; @@ -30,7 +31,7 @@ class Album { final List genres; final int duration; - String get coverArtUrl => SubsonicApiService().getCoverArtUrl(_coverArtId); + String get coverArtUrl => SubsonicApiService().getCoverArtUrl(coverArtId); factory Album.fromJson(Map json) => _$AlbumFromJson(json); diff --git a/lib/api/subsonic/album.g.dart b/lib/api/subsonic/album.g.dart index 363d92f..9e947d1 100644 --- a/lib/api/subsonic/album.g.dart +++ b/lib/api/subsonic/album.g.dart @@ -10,7 +10,7 @@ Album _$AlbumFromJson(Map json) => Album( id: json['id'] as String, name: json['name'] as String, coverArtId: json['coverArt'] as String, - playCount: (json['playCount'] as num).toInt(), + playCount: (json['playCount'] as num?)?.toInt() ?? 0, artistName: json['artist'] as String, artistId: json['artistId'] as String, year: (json['year'] as num).toInt(), @@ -23,7 +23,7 @@ Album _$AlbumFromJson(Map json) => Album( Map _$AlbumToJson(Album instance) => { 'id': instance.id, 'name': instance.name, - 'coverArt': instance._coverArtId, + 'coverArt': instance.coverArtId, 'playCount': instance.playCount, 'artist': instance.artistName, 'artistId': instance.artistId, diff --git a/lib/api/subsonic/song.dart b/lib/api/subsonic/song.dart index 957c5fd..f963aa3 100644 --- a/lib/api/subsonic/song.dart +++ b/lib/api/subsonic/song.dart @@ -15,9 +15,9 @@ class Song { required this.bitRate, required this.contentType, required this.fileType, - required String coverArtId, + required this.coverArtId, this.trackNumber, - }) : _coverArtId = coverArtId; + }); factory Song.fromJson(Map json) => _$SongFromJson(json); @@ -25,7 +25,7 @@ class Song { final String id; @JsonKey(name: "artist") final String artistName; - final String artistId; + final String? artistId; final String albumId; final String title; @JsonKey(name: "album") @@ -38,8 +38,8 @@ class Song { @JsonKey(name: "suffix") final String fileType; @JsonKey(name: "coverArt") - final String _coverArtId; + final String coverArtId; - String get coverArtUrl => SubsonicApiService().getCoverArtUrl(_coverArtId); + String get coverArtUrl => SubsonicApiService().getCoverArtUrl(coverArtId); String get streamUrl => SubsonicApiService().getStreamUrl(id); } diff --git a/lib/api/subsonic/song.g.dart b/lib/api/subsonic/song.g.dart index 8a1f5e4..ae0d5b0 100644 --- a/lib/api/subsonic/song.g.dart +++ b/lib/api/subsonic/song.g.dart @@ -12,7 +12,7 @@ Song _$SongFromJson(Map json) => Song( title: json['title'] as String, albumName: json['album'] as String, albumId: json['albumId'] as String, - artistId: json['artistId'] as String, + artistId: json['artistId'] as String?, duration: (json['duration'] as num).toInt(), bitRate: (json['bitRate'] as num).toInt(), contentType: json['contentType'] as String, @@ -33,5 +33,5 @@ Map _$SongToJson(Song instance) => { 'bitRate': instance.bitRate, 'contentType': instance.contentType, 'suffix': instance.fileType, - 'coverArt': instance._coverArtId, + 'coverArt': instance.coverArtId, }; diff --git a/lib/api/subsonic/subsonic.dart b/lib/api/subsonic/subsonic.dart index f1171a1..2bd606a 100644 --- a/lib/api/subsonic/subsonic.dart +++ b/lib/api/subsonic/subsonic.dart @@ -105,7 +105,10 @@ class SubsonicApiService { try { albums.add(Album.fromJson(albumData)); } catch (e) { - logger.e(e); + logger + ..e("Error for $artistId") + ..e(e) + ..e(albumData); continue; } } @@ -129,7 +132,10 @@ class SubsonicApiService { try { songs.add(Song.fromJson(songData)); } catch (e) { - logger.e(e); + logger + ..e("Error for $albumId") + ..e(e) + ..e(songData); continue; } } diff --git a/lib/main.dart b/lib/main.dart index 3ba4dad..22bd0fe 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'package:fast_cached_network_image/fast_cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:just_audio_background/just_audio_background.dart'; import 'package:just_audio_media_kit/just_audio_media_kit.dart'; +import 'package:ocarina/api/subsonic/song.dart'; import 'package:ocarina/views/home_view.dart'; import 'package:ocarina/widgets/player.dart'; import 'package:path_provider/path_provider.dart'; @@ -22,6 +23,7 @@ void main() async { clearCacheAfter: const Duration(days: 31), subDir: (await getApplicationCacheDirectory()).path, ); + sp = await SharedPreferences.getInstance(); runApp(const MyApp()); } @@ -32,29 +34,42 @@ final playerKey = GlobalKey(); /// Instance of [SharedPreferences] used to get shared preferences late final SharedPreferences sp; +/// Notifier to change theme from inside the app +final ValueNotifier themeNotifier = + ValueNotifier(ColorScheme.fromSeed(seedColor: Colors.deepPurple)); + +/// Notifier to change theme from inside the app +final ValueNotifier songNotifier = ValueNotifier(null); + +/// Main app class class MyApp extends StatelessWidget { + /// Main app class const MyApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { return ResponsiveSizer( builder: (context, orientation, screenType) { - return MaterialApp( - title: 'Ocarina', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, - ), - home: const HomeView(), - builder: (context, child) { - return Stack( - children: [ - child!, - Player( - key: playerKey, - ), - ], + return ValueListenableBuilder( + valueListenable: themeNotifier, + builder: (BuildContext context, ColorScheme value, Widget? child) { + return MaterialApp( + title: 'Ocarina', + theme: ThemeData( + colorScheme: value, + useMaterial3: true, + ), + home: const HomeView(), + builder: (context, child) { + return Stack( + children: [ + child!, + Player( + key: playerKey, + ), + ], + ); + }, ); }, ); diff --git a/lib/widgets/player.dart b/lib/widgets/player.dart index d1d28ee..ab2887a 100644 --- a/lib/widgets/player.dart +++ b/lib/widgets/player.dart @@ -1,10 +1,16 @@ +import 'dart:convert'; + import 'package:auto_size_text/auto_size_text.dart'; +import 'package:crypto/crypto.dart'; import 'package:fast_cached_network_image/fast_cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:ocarina/api/audio/audioplayer_service.dart'; +import 'package:ocarina/api/subsonic/song.dart'; +import 'package:ocarina/main.dart'; import 'package:ocarina/util/util.dart'; import 'package:responsive_sizer/responsive_sizer.dart'; import 'package:shimmer/shimmer.dart'; +import 'package:text_scroll/text_scroll.dart'; /// The player widget /// @@ -22,7 +28,7 @@ class Player extends StatefulWidget { /// State of [Player] class PlayerState extends State { void update() { - logger.d(AudioPlayerService().song?.title); + logger.d(AudioPlayerService().song?.coverArtUrl); setState(() {}); } @@ -69,46 +75,19 @@ class PlayerState extends State { width: 100.w, child: Padding( padding: const EdgeInsets.all(8), - child: Row( - children: [ - SizedBox( - height: 10.h, - width: 10.h, - child: Padding( - padding: const EdgeInsets.all(8), - child: ClipRRect( - child: (AudioPlayerService().song == null) - ? ColoredBox( - color: Theme.of(context) - .colorScheme - .primaryContainer, - child: Center( - child: Icon( - Icons.music_note, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - ), - ) - : (FastCachedImage( - url: AudioPlayerService() - .song! - .coverArtUrl, - loadingBuilder: (c, d) => - Shimmer.fromColors( - baseColor: Colors.grey.shade300, - highlightColor: - Colors.grey.shade100, - child: Container( - color: Colors.grey, - ), - ), - errorBuilder: (c, _, __) { - logger - ..e(_) - ..e(__); - return ColoredBox( + child: ValueListenableBuilder( + valueListenable: songNotifier, + builder: (c, t, w) { + return Row( + children: [ + SizedBox( + height: 10.h, + width: 10.h, + child: Padding( + padding: const EdgeInsets.all(8), + child: ClipRRect( + child: (t == null) + ? ColoredBox( color: Theme.of(context) .colorScheme .primaryContainer, @@ -120,28 +99,106 @@ class PlayerState extends State { .onPrimaryContainer, ), ), - ); - }, - )), + ) + : FastCachedImage( + key: Key( + md5 + .convert( + utf8.encode( + t.coverArtUrl, + ), + ) + .toString(), + ), + url: t.coverArtUrl, + loadingBuilder: (c, d) => + Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: + Colors.grey.shade100, + child: Container( + color: Colors.grey, + ), + ), + errorBuilder: (c, _, __) { + logger + ..e(_) + ..e(__); + return ColoredBox( + color: Theme.of(context) + .colorScheme + .primaryContainer, + child: Center( + child: Icon( + Icons.music_note, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + ); + }, + ), + ), + ), ), - ), - ), - const SizedBox( - width: 5, - ), - AutoSizeText( - AudioPlayerService().song == null - ? "Nothing" - : AudioPlayerService().song!.title, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - fontSize: 14, - decoration: TextDecoration.none, - ), - ), - ], + const SizedBox( + width: 5, + ), + Expanded( + child: AutoSizeText( + AudioPlayerService().song == null + ? "Nothing" + : AudioPlayerService().song!.title, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + fontSize: 14, + decoration: TextDecoration.none, + ), + overflowReplacement: TextScroll( + AudioPlayerService().song == null + ? "Nothing" + : AudioPlayerService().song!.title, + ), + ), + ), + SizedBox( + width: 30.w, + child: Row( + children: [ + IconButton( + onPressed: () async { + if (AudioPlayerService().song == + null) { + return; + } + if (AudioPlayerService().isPlaying) { + await AudioPlayerService().pause(); + } else { + AudioPlayerService().resume(); + } + setState(() {}); + }, + icon: AnimatedCrossFade( + firstChild: + const Icon(Icons.play_arrow), + secondChild: const Icon(Icons.pause), + crossFadeState: + (AudioPlayerService().isPlaying) + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + duration: + const Duration(milliseconds: 300), + ), + ), + ], + ), + ), + ], + ); + }, ), ), ), diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 1c10bb6..36bc62d 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) dynamic_color_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); + dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 52a747c..5875823 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + dynamic_color flutter_secure_storage_linux media_kit_libs_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 39d5c75..53f8453 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import audio_service import audio_session +import dynamic_color import flutter_secure_storage_macos import just_audio import path_provider_foundation @@ -16,6 +17,7 @@ import sqflite func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 42859ee..122eacd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -233,6 +233,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.4.3+1" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d + url: "https://pub.dev" + source: hosted + version: "1.7.0" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ee140ac..0b8d466 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,7 @@ dependencies: json_serializable: ^6.8.0 json_annotation: ^4.9.0 shared_preferences: ^2.2.3 + dynamic_color: ^1.7.0 dev_dependencies: build_runner: ^2.4.9 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 67e32e3..38f4d78 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,10 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + DynamicColorPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index a71f5fa..2c7189d 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + dynamic_color flutter_secure_storage_windows media_kit_libs_windows_audio )