fix: loading albums and songs
This commit is contained in:
parent
139844596a
commit
2461c82e1d
16 changed files with 241 additions and 100 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<void> pause() async {
|
||||
await _player.pause();
|
||||
logger.d("Paused");
|
||||
}
|
||||
|
||||
/// Resumes playback
|
||||
void resume() {
|
||||
_player.play();
|
||||
logger.d("Playing");
|
||||
}
|
||||
|
||||
/// Sets color scheme from image
|
||||
Future<void> _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<void> 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String> genres;
|
||||
final int duration;
|
||||
|
||||
String get coverArtUrl => SubsonicApiService().getCoverArtUrl(_coverArtId);
|
||||
String get coverArtUrl => SubsonicApiService().getCoverArtUrl(coverArtId);
|
||||
|
||||
factory Album.fromJson(Map<String, dynamic> json) => _$AlbumFromJson(json);
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ Album _$AlbumFromJson(Map<String, dynamic> 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<String, dynamic> json) => Album(
|
|||
Map<String, dynamic> _$AlbumToJson(Album instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'coverArt': instance._coverArtId,
|
||||
'coverArt': instance.coverArtId,
|
||||
'playCount': instance.playCount,
|
||||
'artist': instance.artistName,
|
||||
'artistId': instance.artistId,
|
||||
|
|
|
@ -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<String, dynamic> 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);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ Song _$SongFromJson(Map<String, dynamic> 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<String, dynamic> _$SongToJson(Song instance) => <String, dynamic>{
|
|||
'bitRate': instance.bitRate,
|
||||
'contentType': instance.contentType,
|
||||
'suffix': instance.fileType,
|
||||
'coverArt': instance._coverArtId,
|
||||
'coverArt': instance.coverArtId,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<PlayerState>();
|
|||
/// Instance of [SharedPreferences] used to get shared preferences
|
||||
late final SharedPreferences sp;
|
||||
|
||||
/// Notifier to change theme from inside the app
|
||||
final ValueNotifier<ColorScheme> themeNotifier =
|
||||
ValueNotifier(ColorScheme.fromSeed(seedColor: Colors.deepPurple));
|
||||
|
||||
/// Notifier to change theme from inside the app
|
||||
final ValueNotifier<Song?> 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<ColorScheme>(
|
||||
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,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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<Player> {
|
||||
void update() {
|
||||
logger.d(AudioPlayerService().song?.title);
|
||||
logger.d(AudioPlayerService().song?.coverArtUrl);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
|
@ -69,46 +75,19 @@ class PlayerState extends State<Player> {
|
|||
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<Song?>(
|
||||
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<Player> {
|
|||
.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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -6,10 +6,14 @@
|
|||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <dynamic_color/dynamic_color_plugin.h>
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||
|
||||
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);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
dynamic_color
|
||||
flutter_secure_storage_linux
|
||||
media_kit_libs_linux
|
||||
)
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,10 +6,13 @@
|
|||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <dynamic_color/dynamic_color_plugin_c_api.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <media_kit_libs_windows_audio/media_kit_libs_windows_audio_plugin_c_api.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
DynamicColorPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar(
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
dynamic_color
|
||||
flutter_secure_storage_windows
|
||||
media_kit_libs_windows_audio
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue