better connectivity checks + error handling for linux
desktop UI add setting for navigation rail changes to DeezerAudioSource: get quality when needed and cache url to avoid re generating and resending too many HEAD requests
This commit is contained in:
parent
6aa596177f
commit
6f1fb73ed8
|
|
@ -59,8 +59,7 @@ android {
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Put back signingConfig.release
|
signingConfig signingConfigs.release
|
||||||
signingConfig signingConfigs.debug
|
|
||||||
shrinkResources false
|
shrinkResources false
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ typedef _IsolateMessage = (
|
||||||
class DeezerAudioSource extends StreamAudioSource {
|
class DeezerAudioSource extends StreamAudioSource {
|
||||||
final _logger = Logger("DeezerAudioSource");
|
final _logger = Logger("DeezerAudioSource");
|
||||||
|
|
||||||
late AudioQuality? _quality;
|
late AudioQuality Function() _getQuality;
|
||||||
late AudioQuality? _initialQuality;
|
late AudioQuality? _initialQuality;
|
||||||
late String _trackId;
|
late String _trackId;
|
||||||
late String _md5origin;
|
late String _md5origin;
|
||||||
|
|
@ -35,24 +35,26 @@ class DeezerAudioSource extends StreamAudioSource {
|
||||||
final StreamInfoCallback? onStreamObtained;
|
final StreamInfoCallback? onStreamObtained;
|
||||||
|
|
||||||
// some cache
|
// some cache
|
||||||
|
AudioQuality? _currentQuality;
|
||||||
int? _cachedSourceLength;
|
int? _cachedSourceLength;
|
||||||
String? _cachedContentType;
|
String? _cachedContentType;
|
||||||
|
Uri? _downloadUrl;
|
||||||
|
|
||||||
DeezerAudioSource({
|
DeezerAudioSource({
|
||||||
required AudioQuality quality,
|
required AudioQuality Function() getQuality,
|
||||||
required String trackId,
|
required String trackId,
|
||||||
required String md5origin,
|
required String md5origin,
|
||||||
required String mediaVersion,
|
required String mediaVersion,
|
||||||
this.onStreamObtained,
|
this.onStreamObtained,
|
||||||
}) {
|
}) {
|
||||||
_quality = quality;
|
_getQuality = getQuality;
|
||||||
_initialQuality = quality;
|
_initialQuality = quality;
|
||||||
_trackId = trackId;
|
_trackId = trackId;
|
||||||
_md5origin = md5origin;
|
_md5origin = md5origin;
|
||||||
_mediaVersion = mediaVersion;
|
_mediaVersion = mediaVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioQuality? get quality => _quality;
|
AudioQuality? get quality => _currentQuality;
|
||||||
String get trackId => _trackId;
|
String get trackId => _trackId;
|
||||||
String get md5origin => _md5origin;
|
String get md5origin => _md5origin;
|
||||||
String get mediaVersion => _mediaVersion;
|
String get mediaVersion => _mediaVersion;
|
||||||
|
|
@ -71,7 +73,7 @@ class DeezerAudioSource extends StreamAudioSource {
|
||||||
return await _qualityFallback();
|
return await _qualityFallback();
|
||||||
} on QualityException {
|
} on QualityException {
|
||||||
_logger.warning("quality fallback failed! trying trackId fallback");
|
_logger.warning("quality fallback failed! trying trackId fallback");
|
||||||
_quality = _initialQuality;
|
_currentQuality = _initialQuality;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map? privateJson;
|
Map? privateJson;
|
||||||
|
|
@ -128,16 +130,16 @@ class DeezerAudioSource extends StreamAudioSource {
|
||||||
if (rc > 400) {
|
if (rc > 400) {
|
||||||
_logger.warning(
|
_logger.warning(
|
||||||
"quality fallback, response code: $rc, current quality: $quality");
|
"quality fallback, response code: $rc, current quality: $quality");
|
||||||
switch (_quality) {
|
switch (_currentQuality) {
|
||||||
case AudioQuality.FLAC:
|
case AudioQuality.FLAC:
|
||||||
_quality = AudioQuality.MP3_320;
|
_currentQuality = AudioQuality.MP3_320;
|
||||||
break;
|
break;
|
||||||
case AudioQuality.MP3_320:
|
case AudioQuality.MP3_320:
|
||||||
_quality = AudioQuality.MP3_128;
|
_currentQuality = AudioQuality.MP3_128;
|
||||||
break;
|
break;
|
||||||
case AudioQuality.MP3_128:
|
case AudioQuality.MP3_128:
|
||||||
default:
|
default:
|
||||||
_quality = null;
|
_currentQuality = null;
|
||||||
throw QualityException("No quality to fallback to!");
|
throw QualityException("No quality to fallback to!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,7 +222,7 @@ class DeezerAudioSource extends StreamAudioSource {
|
||||||
final deezerStart = start - dropBytes;
|
final deezerStart = start - dropBytes;
|
||||||
int counter = deezerStart ~/ chunkSize;
|
int counter = deezerStart ~/ chunkSize;
|
||||||
final buffer = List<int>.empty(growable: true);
|
final buffer = List<int>.empty(growable: true);
|
||||||
final key = await flutter.compute(getKey, trackId);
|
final key = getKey(trackId);
|
||||||
|
|
||||||
await for (var bytes in source) {
|
await for (var bytes in source) {
|
||||||
if (dropBytes > 0) {
|
if (dropBytes > 0) {
|
||||||
|
|
@ -277,12 +279,19 @@ class DeezerAudioSource extends StreamAudioSource {
|
||||||
throw Exception("Authorization failed!");
|
throw Exception("Authorization failed!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// determine quality to use
|
||||||
|
_currentQuality = _getQuality!.call();
|
||||||
|
|
||||||
final Uri uri;
|
final Uri uri;
|
||||||
|
if (_downloadUrl != null) {
|
||||||
|
uri = _downloadUrl!;
|
||||||
|
} else {
|
||||||
try {
|
try {
|
||||||
uri = await _fallbackUrl();
|
_downloadUrl = uri = await _fallbackUrl();
|
||||||
} on QualityException {
|
} on QualityException {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
_logger.fine("Downloading track from ${uri.toString()}");
|
_logger.fine("Downloading track from ${uri.toString()}");
|
||||||
final int deezerStart = start - (start % 2048);
|
final int deezerStart = start - (start % 2048);
|
||||||
final req = http.Request('GET', uri)
|
final req = http.Request('GET', uri)
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,11 @@ class Track extends DeezerMediaItem {
|
||||||
String get artistString => artists == null
|
String get artistString => artists == null
|
||||||
? ""
|
? ""
|
||||||
: artists!.map<String?>((art) => art.name).join(', ');
|
: artists!.map<String?>((art) => art.name).join(', ');
|
||||||
String get durationString =>
|
String get durationString => durationAsString(duration!);
|
||||||
"${duration!.inMinutes}:${duration!.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
|
||||||
|
static String durationAsString(Duration duration) {
|
||||||
|
return "${duration.inMinutes}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||||
|
}
|
||||||
|
|
||||||
//MediaItem
|
//MediaItem
|
||||||
Future<MediaItem> toMediaItem() async {
|
Future<MediaItem> toMediaItem() async {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:sqflite/sqflite.dart';
|
||||||
import 'package:disk_space_plus/disk_space_plus.dart';
|
import 'package:disk_space_plus/disk_space_plus.dart';
|
||||||
import 'package:filesize/filesize.dart';
|
import 'package:filesize/filesize.dart';
|
||||||
|
|
@ -22,7 +23,6 @@ class DownloadManager {
|
||||||
// DownloadManager currently only supports android
|
// DownloadManager currently only supports android
|
||||||
static bool get isSupported => Platform.isAndroid;
|
static bool get isSupported => Platform.isAndroid;
|
||||||
|
|
||||||
|
|
||||||
//Platform channels
|
//Platform channels
|
||||||
static MethodChannel platform = const MethodChannel('f.f.freezer/native');
|
static MethodChannel platform = const MethodChannel('f.f.freezer/native');
|
||||||
static EventChannel eventChannel =
|
static EventChannel eventChannel =
|
||||||
|
|
@ -37,7 +37,7 @@ class DownloadManager {
|
||||||
|
|
||||||
//Start/Resume downloads
|
//Start/Resume downloads
|
||||||
Future start() async {
|
Future start() async {
|
||||||
if (!Platform.isAndroid) return;
|
if (!isSupported) return;
|
||||||
|
|
||||||
//Returns whether service is bound or not, the delay is really shitty/hacky way, until i find a real solution
|
//Returns whether service is bound or not, the delay is really shitty/hacky way, until i find a real solution
|
||||||
await updateServiceSettings();
|
await updateServiceSettings();
|
||||||
|
|
@ -46,13 +46,13 @@ class DownloadManager {
|
||||||
|
|
||||||
//Stop/Pause downloads
|
//Stop/Pause downloads
|
||||||
Future stop() async {
|
Future stop() async {
|
||||||
if (!Platform.isAndroid) return;
|
if (!isSupported) return;
|
||||||
|
|
||||||
await platform.invokeMethod('stop');
|
await platform.invokeMethod('stop');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future init() async {
|
Future init() async {
|
||||||
if (!Platform.isAndroid) return;
|
if (!isSupported) return;
|
||||||
//Remove old DB
|
//Remove old DB
|
||||||
File oldDbFile = File(p.join((await getDatabasesPath()), 'offline.db'));
|
File oldDbFile = File(p.join((await getDatabasesPath()), 'offline.db'));
|
||||||
if (await oldDbFile.exists()) {
|
if (await oldDbFile.exists()) {
|
||||||
|
|
@ -100,7 +100,7 @@ class DownloadManager {
|
||||||
|
|
||||||
//Get all downloads from db
|
//Get all downloads from db
|
||||||
Future<List<Download>> getDownloads() async {
|
Future<List<Download>> getDownloads() async {
|
||||||
if (!Platform.isAndroid) return [];
|
if (!isSupported) return [];
|
||||||
|
|
||||||
List raw = await platform.invokeMethod('getDownloads');
|
List raw = await platform.invokeMethod('getDownloads');
|
||||||
return raw.map((d) => Download.fromJson(d)).toList();
|
return raw.map((d) => Download.fromJson(d)).toList();
|
||||||
|
|
@ -158,7 +158,7 @@ class DownloadManager {
|
||||||
|
|
||||||
Future<bool> addOfflineTrack(Track track,
|
Future<bool> addOfflineTrack(Track track,
|
||||||
{private = true, BuildContext? context, isSingleton = false}) async {
|
{private = true, BuildContext? context, isSingleton = false}) async {
|
||||||
if (!Platform.isAndroid) return false;
|
if (!isSupported) return false;
|
||||||
//Permission
|
//Permission
|
||||||
if (!private && !(await checkPermission())) return false;
|
if (!private && !(await checkPermission())) return false;
|
||||||
|
|
||||||
|
|
@ -241,7 +241,7 @@ class DownloadManager {
|
||||||
|
|
||||||
Future addOfflinePlaylist(Playlist? playlist,
|
Future addOfflinePlaylist(Playlist? playlist,
|
||||||
{private = true, BuildContext? context, AudioQuality? quality}) async {
|
{private = true, BuildContext? context, AudioQuality? quality}) async {
|
||||||
if (!Platform.isAndroid) return false;
|
if (!isSupported) return false;
|
||||||
|
|
||||||
//Permission
|
//Permission
|
||||||
if (!private && !(await checkPermission())) return;
|
if (!private && !(await checkPermission())) return;
|
||||||
|
|
@ -338,7 +338,7 @@ class DownloadManager {
|
||||||
|
|
||||||
//Get all offline available tracks
|
//Get all offline available tracks
|
||||||
Future<List<Track?>> allOfflineTracks() async {
|
Future<List<Track?>> allOfflineTracks() async {
|
||||||
if (!Platform.isAndroid) return [];
|
if (!isSupported) return [];
|
||||||
|
|
||||||
List rawTracks =
|
List rawTracks =
|
||||||
await db.query('Tracks', where: 'offline == 1', columns: ['id']);
|
await db.query('Tracks', where: 'offline == 1', columns: ['id']);
|
||||||
|
|
@ -352,6 +352,8 @@ class DownloadManager {
|
||||||
|
|
||||||
//Get all offline albums
|
//Get all offline albums
|
||||||
Future<List<Album>> getOfflineAlbums() async {
|
Future<List<Album>> getOfflineAlbums() async {
|
||||||
|
if (!isSupported) return [];
|
||||||
|
|
||||||
List rawAlbums =
|
List rawAlbums =
|
||||||
await db.query('Albums', where: 'offline == 1', columns: ['id']);
|
await db.query('Albums', where: 'offline == 1', columns: ['id']);
|
||||||
List<Album> out = [];
|
List<Album> out = [];
|
||||||
|
|
@ -396,6 +398,7 @@ class DownloadManager {
|
||||||
|
|
||||||
//Get all offline playlists
|
//Get all offline playlists
|
||||||
Future<List<Playlist>> getOfflinePlaylists() async {
|
Future<List<Playlist>> getOfflinePlaylists() async {
|
||||||
|
if (!isSupported) return [];
|
||||||
final rawPlaylists = await db.query('Playlists', columns: ['id']);
|
final rawPlaylists = await db.query('Playlists', columns: ['id']);
|
||||||
final out = <Playlist>[];
|
final out = <Playlist>[];
|
||||||
for (final rawPlaylist in rawPlaylists) {
|
for (final rawPlaylist in rawPlaylists) {
|
||||||
|
|
@ -470,6 +473,7 @@ class DownloadManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future removeOfflinePlaylist(String? id) async {
|
Future removeOfflinePlaylist(String? id) async {
|
||||||
|
if (!isSupported) return;
|
||||||
//Fetch playlist
|
//Fetch playlist
|
||||||
List rawPlaylists =
|
List rawPlaylists =
|
||||||
await db.query('Playlists', where: 'id == ?', whereArgs: [id]);
|
await db.query('Playlists', where: 'id == ?', whereArgs: [id]);
|
||||||
|
|
@ -481,9 +485,11 @@ class DownloadManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Check if album, track or playlist is offline
|
//Check if album, track or playlist is offline
|
||||||
Future<bool> checkOffline(
|
Future<bool> _checkOffline(
|
||||||
{Album? album, Track? track, Playlist? playlist}) async {
|
(Album? album, Track? track, Playlist? playlist) message) async {
|
||||||
if (!Platform.isAndroid) return false;
|
if (!isSupported) return false;
|
||||||
|
|
||||||
|
final (album, track, playlist) = message;
|
||||||
|
|
||||||
//Track
|
//Track
|
||||||
if (track != null) {
|
if (track != null) {
|
||||||
|
|
@ -509,6 +515,10 @@ class DownloadManager {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> checkOffline({Album? album, Track? track, Playlist? playlist}) {
|
||||||
|
return compute(_checkOffline, (album, track, playlist));
|
||||||
|
}
|
||||||
|
|
||||||
//Offline search
|
//Offline search
|
||||||
Future<SearchResults> search(String? query) async {
|
Future<SearchResults> search(String? query) async {
|
||||||
SearchResults results =
|
SearchResults results =
|
||||||
|
|
@ -619,7 +629,7 @@ class DownloadManager {
|
||||||
|
|
||||||
//Send settings to download service
|
//Send settings to download service
|
||||||
Future updateServiceSettings() async {
|
Future updateServiceSettings() async {
|
||||||
if (!Platform.isAndroid) return;
|
if (!isSupported) return;
|
||||||
await platform.invokeMethod(
|
await platform.invokeMethod(
|
||||||
'updateSettings', settings.getServiceSettings());
|
'updateSettings', settings.getServiceSettings());
|
||||||
}
|
}
|
||||||
|
|
@ -639,21 +649,21 @@ class DownloadManager {
|
||||||
|
|
||||||
//Remove download from queue/finished
|
//Remove download from queue/finished
|
||||||
Future removeDownload(int? id) async {
|
Future removeDownload(int? id) async {
|
||||||
if (!Platform.isAndroid) return;
|
if (!isSupported) return;
|
||||||
|
|
||||||
await platform.invokeMethod('removeDownload', {'id': id});
|
await platform.invokeMethod('removeDownload', {'id': id});
|
||||||
}
|
}
|
||||||
|
|
||||||
//Restart failed downloads
|
//Restart failed downloads
|
||||||
Future retryDownloads() async {
|
Future retryDownloads() async {
|
||||||
if (!Platform.isAndroid) return;
|
if (!isSupported) return;
|
||||||
|
|
||||||
await platform.invokeMethod('retryDownloads');
|
await platform.invokeMethod('retryDownloads');
|
||||||
}
|
}
|
||||||
|
|
||||||
//Delete downloads by state
|
//Delete downloads by state
|
||||||
Future removeDownloads(DownloadState state) async {
|
Future removeDownloads(DownloadState state) async {
|
||||||
if (!Platform.isAndroid) return;
|
if (!isSupported) return;
|
||||||
|
|
||||||
await platform.invokeMethod(
|
await platform.invokeMethod(
|
||||||
'removeDownloads', {'state': DownloadState.values.indexOf(state)});
|
'removeDownloads', {'state': DownloadState.values.indexOf(state)});
|
||||||
|
|
|
||||||
|
|
@ -354,6 +354,8 @@ class AudioPlayerTaskInitArguments {
|
||||||
}
|
}
|
||||||
|
|
||||||
class AudioPlayerTask extends BaseAudioHandler {
|
class AudioPlayerTask extends BaseAudioHandler {
|
||||||
|
final _logger = Logger('AudioPlayerTask');
|
||||||
|
|
||||||
late AudioPlayer _player;
|
late AudioPlayer _player;
|
||||||
late ConcatenatingAudioSource _audioSource;
|
late ConcatenatingAudioSource _audioSource;
|
||||||
late DeezerAPI _deezerAPI;
|
late DeezerAPI _deezerAPI;
|
||||||
|
|
@ -376,6 +378,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
StreamSubscription? _bufferPositionSubscription;
|
StreamSubscription? _bufferPositionSubscription;
|
||||||
StreamSubscription? _audioSessionSubscription;
|
StreamSubscription? _audioSessionSubscription;
|
||||||
StreamSubscription? _visualizerSubscription;
|
StreamSubscription? _visualizerSubscription;
|
||||||
|
StreamSubscription? _connectivitySubscription;
|
||||||
|
|
||||||
/// Android Auto helper class for navigation
|
/// Android Auto helper class for navigation
|
||||||
late final AndroidAuto _androidAuto;
|
late final AndroidAuto _androidAuto;
|
||||||
|
|
@ -384,6 +387,8 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
AudioQuality mobileQuality = AudioQuality.MP3_128;
|
AudioQuality mobileQuality = AudioQuality.MP3_128;
|
||||||
AudioQuality wifiQuality = AudioQuality.MP3_128;
|
AudioQuality wifiQuality = AudioQuality.MP3_128;
|
||||||
|
|
||||||
|
AudioQuality _currentQuality = AudioQuality.MP3_128;
|
||||||
|
|
||||||
/// Current queueSource (=> where playback has begun from)
|
/// Current queueSource (=> where playback has begun from)
|
||||||
QueueSource? queueSource;
|
QueueSource? queueSource;
|
||||||
|
|
||||||
|
|
@ -466,7 +471,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
//Update
|
//Update
|
||||||
_broadcastState();
|
_broadcastState();
|
||||||
}, onError: (Object e, StackTrace st) {
|
}, onError: (Object e, StackTrace st) {
|
||||||
print('A stream error occurred: $e');
|
_logger.severe('A stream error occurred: $e');
|
||||||
});
|
});
|
||||||
_player.processingStateStream.listen((state) {
|
_player.processingStateStream.listen((state) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
|
|
@ -496,6 +501,20 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
//Load queue
|
//Load queue
|
||||||
// queue.add(_queue);
|
// queue.add(_queue);
|
||||||
|
|
||||||
|
// Determine audio quality to use
|
||||||
|
try {
|
||||||
|
await Connectivity().checkConnectivity().then(_determineAudioQuality);
|
||||||
|
|
||||||
|
// listen for connectivity changes
|
||||||
|
_connectivitySubscription =
|
||||||
|
Connectivity().onConnectivityChanged.listen(_determineAudioQuality);
|
||||||
|
} catch (e) {
|
||||||
|
_logger.warning(
|
||||||
|
'Couldn\'t determine connection! Falling back to other (which may use wifi quality)');
|
||||||
|
// on error, return dummy value -- error can happen on linux if not using NetworkManager, for example
|
||||||
|
_determineAudioQuality(ConnectivityResult.other);
|
||||||
|
}
|
||||||
|
|
||||||
await _loadQueueFile();
|
await _loadQueueFile();
|
||||||
|
|
||||||
if (initArgs.lastFMUsername != null && initArgs.lastFMPassword != null) {
|
if (initArgs.lastFMUsername != null && initArgs.lastFMPassword != null) {
|
||||||
|
|
@ -506,6 +525,21 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
customEvent.add({'action': 'onLoad'});
|
customEvent.add({'action': 'onLoad'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _determineAudioQuality(ConnectivityResult result) {
|
||||||
|
switch (result) {
|
||||||
|
case ConnectivityResult.mobile:
|
||||||
|
case ConnectivityResult.bluetooth:
|
||||||
|
_currentQuality = mobileQuality;
|
||||||
|
case ConnectivityResult.other:
|
||||||
|
_currentQuality =
|
||||||
|
Platform.isLinux || Platform.isLinux || Platform.isMacOS
|
||||||
|
? wifiQuality
|
||||||
|
: mobileQuality;
|
||||||
|
default:
|
||||||
|
_currentQuality = wifiQuality;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future skipToQueueItem(int index) async {
|
Future skipToQueueItem(int index) async {
|
||||||
_lastPosition = null;
|
_lastPosition = null;
|
||||||
|
|
@ -772,7 +806,9 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
if (!queue.hasValue || queue.value.isEmpty) {
|
if (!queue.hasValue || queue.value.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final sources = await Future.wait(queue.value.map(_mediaItemToAudioSource));
|
|
||||||
|
final sources =
|
||||||
|
await Future.wait(queue.value.map((e) => _mediaItemToAudioSource(e)));
|
||||||
|
|
||||||
_audioSource = ConcatenatingAudioSource(
|
_audioSource = ConcatenatingAudioSource(
|
||||||
children: sources,
|
children: sources,
|
||||||
|
|
@ -827,10 +863,6 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
//Due to current limitations of just_audio, quality fallback moved to DeezerDataSource in ExoPlayer
|
//Due to current limitations of just_audio, quality fallback moved to DeezerDataSource in ExoPlayer
|
||||||
//This just returns fake url that contains metadata
|
//This just returns fake url that contains metadata
|
||||||
List? playbackDetails = jsonDecode(mediaItem.extras!['playbackDetails']);
|
List? playbackDetails = jsonDecode(mediaItem.extras!['playbackDetails']);
|
||||||
//Quality
|
|
||||||
ConnectivityResult conn = await Connectivity().checkConnectivity();
|
|
||||||
AudioQuality quality = mobileQuality;
|
|
||||||
if (conn == ConnectivityResult.wifi) quality = wifiQuality;
|
|
||||||
|
|
||||||
if ((playbackDetails ?? []).length < 2) {
|
if ((playbackDetails ?? []).length < 2) {
|
||||||
throw Exception('not enough playback details');
|
throw Exception('not enough playback details');
|
||||||
|
|
@ -848,7 +880,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
||||||
|
|
||||||
// DON'T use the java backend anymore, useless.
|
// DON'T use the java backend anymore, useless.
|
||||||
return DeezerAudioSource(
|
return DeezerAudioSource(
|
||||||
quality: quality,
|
getQuality: () => _currentQuality,
|
||||||
trackId: mediaItem.id,
|
trackId: mediaItem.id,
|
||||||
md5origin: playbackDetails![0],
|
md5origin: playbackDetails![0],
|
||||||
mediaVersion: playbackDetails[1],
|
mediaVersion: playbackDetails[1],
|
||||||
|
|
|
||||||
|
|
@ -150,9 +150,7 @@ class _FreezerAppState extends State<FreezerApp> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateTheme() {
|
void _updateTheme() {
|
||||||
setState(() {
|
setState(() {});
|
||||||
settings.themeData;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Locale? _locale() {
|
Locale? _locale() {
|
||||||
|
|
@ -317,9 +315,8 @@ class MainScreenState extends State<MainScreen>
|
||||||
final playerScreenFocusNode = FocusScopeNode();
|
final playerScreenFocusNode = FocusScopeNode();
|
||||||
final playerBarFocusNode = FocusNode();
|
final playerBarFocusNode = FocusNode();
|
||||||
final _fancyScaffoldKey = GlobalKey<FancyScaffoldState>();
|
final _fancyScaffoldKey = GlobalKey<FancyScaffoldState>();
|
||||||
final routeObserver = RouteObserver();
|
|
||||||
|
|
||||||
late bool _isDesktop;
|
late bool isDesktop;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -647,11 +644,11 @@ class MainScreenState extends State<MainScreen>
|
||||||
child: LayoutBuilder(builder: (context, constraints) {
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
// check if we're running on a desktop platform
|
// check if we're running on a desktop platform
|
||||||
final isLandscape = constraints.maxWidth > constraints.maxHeight;
|
final isLandscape = constraints.maxWidth > constraints.maxHeight;
|
||||||
_isDesktop = isLandscape && constraints.maxWidth > 1024;
|
isDesktop = isLandscape && constraints.maxWidth > 1024;
|
||||||
return FancyScaffold(
|
return FancyScaffold(
|
||||||
key: _fancyScaffoldKey,
|
key: _fancyScaffoldKey,
|
||||||
bodyDrawer: _buildNavigationRail(_isDesktop),
|
bodyDrawer: _buildNavigationRail(isDesktop),
|
||||||
bottomNavigationBar: buildBottomBar(_isDesktop),
|
bottomNavigationBar: buildBottomBar(isDesktop),
|
||||||
bottomPanel: PlayerBar(
|
bottomPanel: PlayerBar(
|
||||||
focusNode: playerBarFocusNode,
|
focusNode: playerBarFocusNode,
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
|
|
@ -678,7 +675,6 @@ class MainScreenState extends State<MainScreen>
|
||||||
skipTraversal: true,
|
skipTraversal: true,
|
||||||
canRequestFocus: false,
|
canRequestFocus: false,
|
||||||
child: _MainRouteNavigator(
|
child: _MainRouteNavigator(
|
||||||
observers: [routeObserver],
|
|
||||||
navigatorKey: navigatorKey,
|
navigatorKey: navigatorKey,
|
||||||
routes: {
|
routes: {
|
||||||
Navigator.defaultRouteName: (context) =>
|
Navigator.defaultRouteName: (context) =>
|
||||||
|
|
@ -805,16 +801,24 @@ class _ExtensibleNavigationRailState extends State<ExtensibleNavigationRail> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MouseRegion(
|
final child = NavigationRail(
|
||||||
onEnter: (_) => setState(() => _extended = true),
|
|
||||||
onExit: (_) => setState(() => _extended = false),
|
|
||||||
child: NavigationRail(
|
|
||||||
extended: _extended,
|
extended: _extended,
|
||||||
destinations: widget.destinations,
|
destinations: widget.destinations,
|
||||||
selectedIndex: widget.selectedIndex,
|
selectedIndex: widget.selectedIndex,
|
||||||
onDestinationSelected: widget.onDestinationSelected,
|
onDestinationSelected: widget.onDestinationSelected,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (settings.navigationRailAppearance !=
|
||||||
|
NavigationRailAppearance.expand_on_hover) {
|
||||||
|
_extended = settings.navigationRailAppearance ==
|
||||||
|
NavigationRailAppearance.always_expanded;
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: (_) => setState(() => _extended = true),
|
||||||
|
onExit: (_) => setState(() => _extended = false),
|
||||||
|
child: child);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,11 @@ import 'package:freezer/page_routes/basic_page_route.dart';
|
||||||
import 'package:freezer/ui/animated_blur.dart';
|
import 'package:freezer/ui/animated_blur.dart';
|
||||||
|
|
||||||
class FadePageRoute<T> extends BasicPageRoute<T> {
|
class FadePageRoute<T> extends BasicPageRoute<T> {
|
||||||
|
@override
|
||||||
|
final bool barrierDismissible;
|
||||||
|
@override
|
||||||
|
final Color? barrierColor;
|
||||||
|
|
||||||
final WidgetBuilder builder;
|
final WidgetBuilder builder;
|
||||||
final bool blur;
|
final bool blur;
|
||||||
FadePageRoute({
|
FadePageRoute({
|
||||||
|
|
@ -11,6 +16,8 @@ class FadePageRoute<T> extends BasicPageRoute<T> {
|
||||||
super.transitionDuration,
|
super.transitionDuration,
|
||||||
super.maintainState,
|
super.maintainState,
|
||||||
super.settings,
|
super.settings,
|
||||||
|
this.barrierColor,
|
||||||
|
this.barrierDismissible = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,10 @@ class Settings {
|
||||||
@HiveField(47, defaultValue: false)
|
@HiveField(47, defaultValue: false)
|
||||||
bool seekAsSkip = false;
|
bool seekAsSkip = false;
|
||||||
|
|
||||||
|
@HiveField(48, defaultValue: NavigationRailAppearance.expand_on_hover)
|
||||||
|
NavigationRailAppearance navigationRailAppearance =
|
||||||
|
NavigationRailAppearance.expand_on_hover;
|
||||||
|
|
||||||
static LazyBox<Settings>? __box;
|
static LazyBox<Settings>? __box;
|
||||||
static Future<LazyBox<Settings>> get _box async =>
|
static Future<LazyBox<Settings>> get _box async =>
|
||||||
__box ??= await Hive.openLazyBox<Settings>('settings');
|
__box ??= await Hive.openLazyBox<Settings>('settings');
|
||||||
|
|
@ -415,3 +419,13 @@ class SpotifyCredentialsSave {
|
||||||
_$SpotifyCredentialsSaveFromJson(json);
|
_$SpotifyCredentialsSaveFromJson(json);
|
||||||
Map<String, dynamic> toJson() => _$SpotifyCredentialsSaveToJson(this);
|
Map<String, dynamic> toJson() => _$SpotifyCredentialsSaveToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HiveType(typeId: 34)
|
||||||
|
enum NavigationRailAppearance {
|
||||||
|
@HiveField(0)
|
||||||
|
expand_on_hover,
|
||||||
|
@HiveField(1)
|
||||||
|
always_expanded,
|
||||||
|
@HiveField(2)
|
||||||
|
icons_only,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:freezer/page_routes/fade.dart';
|
||||||
import 'package:palette_generator/palette_generator.dart';
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
|
@ -103,14 +105,14 @@ class _CachedImageState extends State<CachedImage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ZoomableImage extends StatefulWidget {
|
class ZoomableImage extends StatelessWidget {
|
||||||
final String url;
|
final String url;
|
||||||
final bool rounded;
|
final bool rounded;
|
||||||
final double? width;
|
final double? width;
|
||||||
final bool enableHero;
|
final bool enableHero;
|
||||||
final Object? heroTag;
|
final Object? heroTag;
|
||||||
|
|
||||||
const ZoomableImage({
|
ZoomableImage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.url,
|
required this.url,
|
||||||
this.rounded = false,
|
this.rounded = false,
|
||||||
|
|
@ -119,38 +121,15 @@ class ZoomableImage extends StatefulWidget {
|
||||||
this.heroTag,
|
this.heroTag,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
late final Object? _key = enableHero ? (heroTag ?? UniqueKey()) : null;
|
||||||
State<ZoomableImage> createState() => _ZoomableImageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ZoomableImageState extends State<ZoomableImage> {
|
|
||||||
PhotoViewController? controller;
|
|
||||||
bool photoViewOpened = false;
|
|
||||||
late final Object? _key =
|
|
||||||
widget.enableHero ? (widget.heroTag ?? UniqueKey()) : null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
controller = PhotoViewController()..outputStateStream.listen(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listener of PhotoView scale changes. Used for closing PhotoView by pinch-in
|
|
||||||
void listener(PhotoViewControllerValue value) {
|
|
||||||
if (value.scale! < 0.16 && photoViewOpened) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
photoViewOpened =
|
|
||||||
false; // to avoid multiple pop() when picture are being scaled out too slowly
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
print('key: $_key');
|
print('key: $_key');
|
||||||
final image = CachedImage(
|
final image = CachedImage(
|
||||||
url: widget.url,
|
url: url,
|
||||||
rounded: widget.rounded,
|
rounded: rounded,
|
||||||
width: widget.width,
|
width: width,
|
||||||
fullThumb: true,
|
fullThumb: true,
|
||||||
);
|
);
|
||||||
final child = _key != null
|
final child = _key != null
|
||||||
|
|
@ -165,25 +144,72 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).push(PageRouteBuilder(
|
Navigator.of(context).push(FadePageRoute(
|
||||||
opaque: false, // transparent background
|
builder: (context) =>
|
||||||
pageBuilder: (context, animation, __) {
|
ZoomableImageRoute(imageUrl: url, heroKey: _key),
|
||||||
photoViewOpened = true;
|
barrierDismissible: true));
|
||||||
return FadeTransition(
|
|
||||||
opacity: animation,
|
|
||||||
child: PhotoView(
|
|
||||||
imageProvider: CachedNetworkImageProvider(widget.url),
|
|
||||||
maxScale: 8.0,
|
|
||||||
minScale: 0.2,
|
|
||||||
controller: controller,
|
|
||||||
heroAttributes: _key == null
|
|
||||||
? null
|
|
||||||
: PhotoViewHeroAttributes(tag: _key!),
|
|
||||||
backgroundDecoration: const BoxDecoration(
|
|
||||||
color: Color.fromARGB(0x90, 0, 0, 0))),
|
|
||||||
);
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ZoomableImageRoute extends StatefulWidget {
|
||||||
|
final Object? heroKey;
|
||||||
|
final String imageUrl;
|
||||||
|
const ZoomableImageRoute({required this.imageUrl, super.key, this.heroKey});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ZoomableImageRoute> createState() => _ZoomableImageRouteState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ZoomableImageRouteState extends State<ZoomableImageRoute> {
|
||||||
|
bool photoViewOpened = false;
|
||||||
|
final controller = PhotoViewController();
|
||||||
|
final _focusNode = FocusNode();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
controller.outputStateStream.listen(listener);
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
controller.dispose();
|
||||||
|
_focusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void listener(PhotoViewControllerValue value) {
|
||||||
|
if (value.scale! < 0.16 && photoViewOpened) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
photoViewOpened =
|
||||||
|
false; // to avoid multiple pop() when picture are being scaled out too slowly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RawKeyboardListener(
|
||||||
|
focusNode: _focusNode,
|
||||||
|
onKey: (event) {
|
||||||
|
if (event is! KeyUpEvent) return;
|
||||||
|
|
||||||
|
if (event.isKeyPressed(LogicalKeyboardKey.escape)) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: PhotoView(
|
||||||
|
imageProvider: CachedNetworkImageProvider(widget.imageUrl),
|
||||||
|
maxScale: 8.0,
|
||||||
|
minScale: 0.2,
|
||||||
|
controller: controller,
|
||||||
|
heroAttributes: widget.heroKey == null
|
||||||
|
? null
|
||||||
|
: PhotoViewHeroAttributes(tag: widget.heroKey!),
|
||||||
|
backgroundDecoration:
|
||||||
|
const BoxDecoration(color: Color.fromARGB(0x90, 0, 0, 0))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:fluttericon/font_awesome5_icons.dart';
|
import 'package:fluttericon/font_awesome5_icons.dart';
|
||||||
import 'package:freezer/api/cache.dart';
|
import 'package:freezer/api/cache.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
|
|
@ -77,11 +79,14 @@ class _AlbumDetailsState extends State<AlbumDetails> {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const SizedBox(height: 8.0),
|
const SizedBox(height: 8.0),
|
||||||
ZoomableImage(
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints.loose(
|
||||||
|
MediaQuery.of(context).size / 3),
|
||||||
|
child: ZoomableImage(
|
||||||
url: album!.art!.full,
|
url: album!.art!.full,
|
||||||
width: MediaQuery.of(context).size.width / 2,
|
|
||||||
rounded: true,
|
rounded: true,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 8.0),
|
const SizedBox(height: 8.0),
|
||||||
Text(
|
Text(
|
||||||
album!.title!,
|
album!.title!,
|
||||||
|
|
@ -228,12 +233,14 @@ class _AlbumDetailsState extends State<AlbumDetails> {
|
||||||
),
|
),
|
||||||
...List.generate(
|
...List.generate(
|
||||||
tracks.length,
|
tracks.length,
|
||||||
(i) => TrackTile(tracks[i]!, onTap: () {
|
(i) =>
|
||||||
|
TrackTile.fromTrack(tracks[i]!, onTap: () {
|
||||||
playerHelper.playFromAlbum(
|
playerHelper.playFromAlbum(
|
||||||
album!, tracks[i]!.id);
|
album!, tracks[i]!.id);
|
||||||
}, onHold: () {
|
}, onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultTrackMenu(tracks[i]!);
|
m.defaultTrackMenu(tracks[i]!,
|
||||||
|
details: details);
|
||||||
}))
|
}))
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -299,18 +306,25 @@ class _MakeAlbumOfflineState extends State<MakeAlbumOffline> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ArtistDetails extends StatelessWidget {
|
class ArtistDetails extends StatefulWidget {
|
||||||
late final Artist artist;
|
final Artist artist;
|
||||||
late final Future? _future;
|
|
||||||
|
|
||||||
ArtistDetails(Artist artist, {Key? key}) : super(key: key) {
|
const ArtistDetails(this.artist, {super.key});
|
||||||
FutureOr<Artist> future = _loadArtist(artist);
|
|
||||||
|
@override
|
||||||
|
State<ArtistDetails> createState() => _ArtistDetailsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ArtistDetailsState extends State<ArtistDetails> {
|
||||||
|
late final Future<Artist> _future;
|
||||||
|
void initState() {
|
||||||
|
FutureOr<Artist> future = _loadArtist(widget.artist);
|
||||||
if (future is Artist) {
|
if (future is Artist) {
|
||||||
this.artist = future;
|
_future = Future.value(widget.artist);
|
||||||
_future = null;
|
|
||||||
} else {
|
} else {
|
||||||
_future = future.then((value) => this.artist = value);
|
_future = future;
|
||||||
}
|
}
|
||||||
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureOr<Artist> _loadArtist(Artist artist) {
|
FutureOr<Artist> _loadArtist(Artist artist) {
|
||||||
|
|
@ -318,44 +332,53 @@ class ArtistDetails extends StatelessWidget {
|
||||||
if ((artist.albums ?? []).isEmpty) {
|
if ((artist.albums ?? []).isEmpty) {
|
||||||
return deezerAPI.artist(artist.id);
|
return deezerAPI.artist(artist.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return artist;
|
return artist;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: FutureBuilder(
|
body: FutureBuilder<Artist>(
|
||||||
future: _future ?? Future.value(),
|
future: _future,
|
||||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
builder: (BuildContext context, snapshot) {
|
||||||
//Error / not done
|
//Error / not done
|
||||||
if (snapshot.hasError) return const ErrorScreen();
|
if (snapshot.hasError) return const ErrorScreen();
|
||||||
if (snapshot.connectionState != ConnectionState.done) {
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final artist = snapshot.data!;
|
||||||
|
|
||||||
return ListView(
|
return ListView(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const SizedBox(height: 4.0),
|
const SizedBox(height: 4.0),
|
||||||
Row(
|
Padding(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.height / 3,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ZoomableImage(
|
Flexible(
|
||||||
url: artist.picture!.full,
|
child: ZoomableImage(
|
||||||
width: MediaQuery.of(context).size.width / 2 - 8,
|
url: widget.artist.picture!.full,
|
||||||
rounded: true,
|
rounded: true,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: MediaQuery.of(context).size.width / 2 - 24,
|
width: min(
|
||||||
|
MediaQuery.of(context).size.width / 16, 60.0)),
|
||||||
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.max,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
artist.name!,
|
artist.name!,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 24.0, fontWeight: FontWeight.bold),
|
fontSize: 24.0, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
|
|
@ -386,7 +409,7 @@ class ArtistDetails extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8.0),
|
const SizedBox(width: 8.0),
|
||||||
Text(
|
Text(
|
||||||
artist.albumCount.toString(),
|
widget.artist.albumCount.toString(),
|
||||||
style: const TextStyle(fontSize: 16),
|
style: const TextStyle(fontSize: 16),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
@ -396,7 +419,8 @@ class ArtistDetails extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4.0),
|
),
|
||||||
|
),
|
||||||
const FreezerDivider(),
|
const FreezerDivider(),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
|
@ -411,7 +435,7 @@ class ArtistDetails extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await deezerAPI.addFavoriteArtist(artist.id);
|
await deezerAPI.addFavoriteArtist(widget.artist.id);
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
.snack('Added to library'.i18n);
|
.snack('Added to library'.i18n);
|
||||||
},
|
},
|
||||||
|
|
@ -485,15 +509,15 @@ class ArtistDetails extends StatelessWidget {
|
||||||
return const SizedBox(height: 0.0, width: 0.0);
|
return const SizedBox(height: 0.0, width: 0.0);
|
||||||
}
|
}
|
||||||
Track t = artist.topTracks![i];
|
Track t = artist.topTracks![i];
|
||||||
return TrackTile(
|
return TrackTile.fromTrack(
|
||||||
t,
|
t,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
playerHelper.playFromTopTracks(
|
playerHelper.playFromTopTracks(
|
||||||
artist.topTracks!, t.id, artist);
|
artist.topTracks!, t.id, artist);
|
||||||
},
|
},
|
||||||
onHold: () {
|
onSecondary: (details) {
|
||||||
MenuSheet mi = MenuSheet(context);
|
MenuSheet mi = MenuSheet(context);
|
||||||
mi.defaultTrackMenu(t);
|
mi.defaultTrackMenu(t, details: details);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
@ -542,9 +566,9 @@ class ArtistDetails extends StatelessWidget {
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
.pushRoute(builder: (context) => AlbumDetails(a));
|
.pushRoute(builder: (context) => AlbumDetails(a));
|
||||||
},
|
},
|
||||||
onHold: () {
|
onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultAlbumMenu(a);
|
m.defaultAlbumMenu(a, details: details);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
@ -603,9 +627,9 @@ class _DiscographyScreenState extends State<DiscographyScreen> {
|
||||||
a,
|
a,
|
||||||
onTap: () => Navigator.of(context)
|
onTap: () => Navigator.of(context)
|
||||||
.push(MaterialPageRoute(builder: (context) => AlbumDetails(a))),
|
.push(MaterialPageRoute(builder: (context) => AlbumDetails(a))),
|
||||||
onHold: () {
|
onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultAlbumMenu(a);
|
m.defaultAlbumMenu(a, details: details);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -857,57 +881,59 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
||||||
child: ListView(
|
child: ListView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(
|
const SizedBox(height: 4.0),
|
||||||
height: 4.0,
|
ConstrainedBox(
|
||||||
),
|
constraints: BoxConstraints.tight(
|
||||||
Padding(
|
Size.fromHeight(MediaQuery.of(context).size.height / 3)),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
child: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
CachedImage(
|
Flexible(
|
||||||
|
child: CachedImage(
|
||||||
url: playlist!.image!.full,
|
url: playlist!.image!.full,
|
||||||
height: MediaQuery.of(context).size.width / 2 - 8,
|
|
||||||
rounded: true,
|
rounded: true,
|
||||||
fullThumb: true,
|
fullThumb: true,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: MediaQuery.of(context).size.width / 2 - 8,
|
width: min(MediaQuery.of(context).size.width / 16, 60.0)),
|
||||||
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
playlist!.title!,
|
playlist!.title!,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.start,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 20.0, fontWeight: FontWeight.bold),
|
fontSize: 20.0, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
Container(height: 4.0),
|
|
||||||
Text(
|
Text(
|
||||||
playlist!.user!.name ?? '',
|
playlist!.user!.name ?? '',
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.start,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontSize: 17.0),
|
fontSize: 17.0),
|
||||||
),
|
),
|
||||||
Container(height: 10.0),
|
const SizedBox(height: 16.0),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Icon(
|
Icon(
|
||||||
Icons.audiotrack,
|
Icons.audiotrack,
|
||||||
size: 32.0,
|
size: 20.0,
|
||||||
semanticLabel: "Tracks".i18n,
|
semanticLabel: "Tracks".i18n,
|
||||||
),
|
),
|
||||||
Container(
|
const SizedBox(width: 8.0),
|
||||||
width: 8.0,
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
(playlist!.trackCount ?? playlist!.tracks!.length)
|
(playlist!.trackCount ?? playlist!.tracks!.length)
|
||||||
.toString(),
|
.toString(),
|
||||||
|
|
@ -915,6 +941,7 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 6.0),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
|
@ -923,9 +950,7 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
||||||
size: 32.0,
|
size: 32.0,
|
||||||
semanticLabel: "Duration".i18n,
|
semanticLabel: "Duration".i18n,
|
||||||
),
|
),
|
||||||
Container(
|
const SizedBox(width: 8.0),
|
||||||
width: 8.0,
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
playlist!.durationString,
|
playlist!.durationString,
|
||||||
style: const TextStyle(fontSize: 16),
|
style: const TextStyle(fontSize: 16),
|
||||||
|
|
@ -934,10 +959,11 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
if (playlist!.description != null &&
|
if (playlist!.description != null &&
|
||||||
playlist!.description!.isNotEmpty)
|
playlist!.description!.isNotEmpty)
|
||||||
const FreezerDivider(),
|
const FreezerDivider(),
|
||||||
|
|
@ -1057,13 +1083,13 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
||||||
const FreezerDivider(),
|
const FreezerDivider(),
|
||||||
...List.generate(playlist!.tracks!.length, (i) {
|
...List.generate(playlist!.tracks!.length, (i) {
|
||||||
Track t = sorted[i];
|
Track t = sorted[i];
|
||||||
return TrackTile(t, onTap: () {
|
return TrackTile.fromTrack(t, onTap: () {
|
||||||
Playlist p = Playlist(
|
Playlist p = Playlist(
|
||||||
title: playlist!.title, id: playlist!.id, tracks: sorted);
|
title: playlist!.title, id: playlist!.id, tracks: sorted);
|
||||||
playerHelper.playFromPlaylist(p, t.id);
|
playerHelper.playFromPlaylist(p, t.id);
|
||||||
}, onHold: () {
|
}, onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultTrackMenu(t, options: [
|
m.defaultTrackMenu(t, details: details, options: [
|
||||||
(playlist!.user!.id == deezerAPI.userId)
|
(playlist!.user!.id == deezerAPI.userId)
|
||||||
? m.removeFromPlaylist(t, playlist)
|
? m.removeFromPlaylist(t, playlist)
|
||||||
: const SizedBox(
|
: const SizedBox(
|
||||||
|
|
@ -1138,9 +1164,7 @@ class _MakePlaylistOfflineState extends State<MakePlaylistOffline> {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Container(
|
const SizedBox(width: 4.0),
|
||||||
width: 4.0,
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
'Offline'.i18n,
|
'Offline'.i18n,
|
||||||
style: const TextStyle(fontSize: 16),
|
style: const TextStyle(fontSize: 16),
|
||||||
|
|
|
||||||
|
|
@ -328,9 +328,9 @@ class HomePageItemWidget extends StatelessWidget {
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
.pushRoute(builder: (context) => ArtistDetails(item.value));
|
.pushRoute(builder: (context) => ArtistDetails(item.value));
|
||||||
},
|
},
|
||||||
onHold: () {
|
onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultArtistMenu(item.value);
|
m.defaultArtistMenu(item.value, details: details);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
case HomePageItemType.PLAYLIST:
|
case HomePageItemType.PLAYLIST:
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,8 @@ class LibraryScreen extends StatelessWidget {
|
||||||
if (DownloadManager.isSupported)
|
if (DownloadManager.isSupported)
|
||||||
ExpansionTile(
|
ExpansionTile(
|
||||||
title: Text('Statistics'.i18n),
|
title: Text('Statistics'.i18n),
|
||||||
leading: const LeadingIcon(Icons.insert_chart, color: Colors.grey),
|
leading:
|
||||||
|
const LeadingIcon(Icons.insert_chart, color: Colors.grey),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
FutureBuilder(
|
FutureBuilder(
|
||||||
future: downloadManager.getStats(),
|
future: downloadManager.getStats(),
|
||||||
|
|
@ -501,7 +502,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
||||||
Track? t = (tracks.length == (trackCount ?? 0))
|
Track? t = (tracks.length == (trackCount ?? 0))
|
||||||
? _sorted[i]
|
? _sorted[i]
|
||||||
: tracks[i];
|
: tracks[i];
|
||||||
return TrackTile(
|
return TrackTile.fromTrack(
|
||||||
t,
|
t,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
playerHelper.playFromTrackList(
|
playerHelper.playFromTrackList(
|
||||||
|
|
@ -514,9 +515,9 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
||||||
text: 'Favorites'.i18n,
|
text: 'Favorites'.i18n,
|
||||||
source: 'playlist'));
|
source: 'playlist'));
|
||||||
},
|
},
|
||||||
onHold: () {
|
onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultTrackMenu(t, onRemove: () {
|
m.defaultTrackMenu(t, details: details, onRemove: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
tracks.removeWhere((track) => t.id == track.id);
|
tracks.removeWhere((track) => t.id == track.id);
|
||||||
});
|
});
|
||||||
|
|
@ -533,7 +534,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8.0),
|
const SizedBox(height: 8.0),
|
||||||
for (final track in allTracks)
|
for (final track in allTracks)
|
||||||
TrackTile(track!, onTap: () {
|
TrackTile.fromTrack(track!, onTap: () {
|
||||||
playerHelper.playFromTrackList(
|
playerHelper.playFromTrackList(
|
||||||
allTracks,
|
allTracks,
|
||||||
track.id,
|
track.id,
|
||||||
|
|
@ -541,8 +542,9 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
||||||
id: 'allTracks',
|
id: 'allTracks',
|
||||||
text: 'All offline tracks'.i18n,
|
text: 'All offline tracks'.i18n,
|
||||||
source: 'offline'));
|
source: 'offline'));
|
||||||
}, onHold: () {
|
}, onSecondary: (details) {
|
||||||
MenuSheet(context).defaultTrackMenu(track);
|
MenuSheet(context)
|
||||||
|
.defaultTrackMenu(track, details: details);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
)));
|
)));
|
||||||
|
|
@ -689,11 +691,15 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
|
||||||
Navigator.of(context).pushRoute(
|
Navigator.of(context).pushRoute(
|
||||||
builder: (context) => AlbumDetails(a));
|
builder: (context) => AlbumDetails(a));
|
||||||
},
|
},
|
||||||
onHold: () async {
|
onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultAlbumMenu(a, onRemove: () {
|
m.defaultAlbumMenu(
|
||||||
|
a,
|
||||||
|
details: details,
|
||||||
|
onRemove: () {
|
||||||
setState(() => _albums!.remove(a));
|
setState(() => _albums!.remove(a));
|
||||||
});
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
@ -727,9 +733,10 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
|
||||||
Navigator.of(context).pushRoute(
|
Navigator.of(context).pushRoute(
|
||||||
builder: (context) => AlbumDetails(a));
|
builder: (context) => AlbumDetails(a));
|
||||||
},
|
},
|
||||||
onHold: () async {
|
onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultAlbumMenu(a, onRemove: () {
|
m.defaultAlbumMenu(a, details: details,
|
||||||
|
onRemove: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
albums.remove(a);
|
albums.remove(a);
|
||||||
_albums!.remove(a);
|
_albums!.remove(a);
|
||||||
|
|
@ -1090,10 +1097,10 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
||||||
Navigator.of(context).pushRoute(
|
Navigator.of(context).pushRoute(
|
||||||
builder: (context) => PlaylistDetails(favoritesPlaylist));
|
builder: (context) => PlaylistDetails(favoritesPlaylist));
|
||||||
},
|
},
|
||||||
onHold: () {
|
onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
favoritesPlaylist.library = true;
|
favoritesPlaylist.library = true;
|
||||||
m.defaultPlaylistMenu(favoritesPlaylist);
|
m.defaultPlaylistMenu(favoritesPlaylist, details: details);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -1104,9 +1111,9 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
||||||
p,
|
p,
|
||||||
onTap: () => Navigator.of(context)
|
onTap: () => Navigator.of(context)
|
||||||
.pushRoute(builder: (context) => PlaylistDetails(p)),
|
.pushRoute(builder: (context) => PlaylistDetails(p)),
|
||||||
onHold: () {
|
onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultPlaylistMenu(p, onRemove: () {
|
m.defaultPlaylistMenu(p, details: details, onRemove: () {
|
||||||
setState(() => _playlists!.remove(p));
|
setState(() => _playlists!.remove(p));
|
||||||
}, onUpdate: () {
|
}, onUpdate: () {
|
||||||
_load();
|
_load();
|
||||||
|
|
@ -1140,9 +1147,10 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
||||||
p,
|
p,
|
||||||
onTap: () => Navigator.of(context).pushRoute(
|
onTap: () => Navigator.of(context).pushRoute(
|
||||||
builder: (context) => PlaylistDetails(p)),
|
builder: (context) => PlaylistDetails(p)),
|
||||||
onHold: () {
|
onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultPlaylistMenu(p, onRemove: () {
|
m.defaultPlaylistMenu(p, details: details,
|
||||||
|
onRemove: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
playlists.remove(p);
|
playlists.remove(p);
|
||||||
_playlists!.remove(p);
|
_playlists!.remove(p);
|
||||||
|
|
@ -1196,7 +1204,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||||
itemCount: cache.history.length,
|
itemCount: cache.history.length,
|
||||||
itemBuilder: (BuildContext context, int i) {
|
itemBuilder: (BuildContext context, int i) {
|
||||||
Track t = cache.history[cache.history.length - i - 1];
|
Track t = cache.history[cache.history.length - i - 1];
|
||||||
return TrackTile(
|
return TrackTile.fromTrack(
|
||||||
t,
|
t,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
playerHelper.playFromTrackList(
|
playerHelper.playFromTrackList(
|
||||||
|
|
@ -1205,9 +1213,9 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
||||||
QueueSource(
|
QueueSource(
|
||||||
id: null, text: 'History'.i18n, source: 'history'));
|
id: null, text: 'History'.i18n, source: 'history'));
|
||||||
},
|
},
|
||||||
onHold: () {
|
onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultTrackMenu(t);
|
m.defaultTrackMenu(t, details: details);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -13,20 +13,45 @@ import 'package:freezer/ui/error.dart';
|
||||||
import 'package:freezer/ui/player_bar.dart';
|
import 'package:freezer/ui/player_bar.dart';
|
||||||
import 'package:freezer/ui/player_screen.dart';
|
import 'package:freezer/ui/player_screen.dart';
|
||||||
|
|
||||||
class LyricsScreen extends StatefulWidget {
|
class LyricsScreen extends StatelessWidget {
|
||||||
const LyricsScreen({Key? key}) : super(key: key);
|
const LyricsScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<LyricsScreen> createState() => _LyricsScreenState();
|
Widget build(BuildContext context) {
|
||||||
|
return PlayerScreenBackground(
|
||||||
|
enabled: settings.playerBackgroundOnLyrics,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Lyrics'.i18n),
|
||||||
|
systemOverlayStyle: PlayerScreenBackground.getSystemUiOverlayStyle(
|
||||||
|
context,
|
||||||
|
enabled: settings.playerBackgroundOnLyrics),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
child: const Column(
|
||||||
|
children: [
|
||||||
|
LyricsWidget(),
|
||||||
|
Divider(height: 1.0, thickness: 1.0),
|
||||||
|
PlayerBar(backgroundColor: Colors.transparent),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LyricsScreenState extends State<LyricsScreen> {
|
class LyricsWidget extends StatefulWidget {
|
||||||
|
const LyricsWidget({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LyricsWidget> createState() => _LyricsWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LyricsWidgetState extends State<LyricsWidget> {
|
||||||
late StreamSubscription _mediaItemSub;
|
late StreamSubscription _mediaItemSub;
|
||||||
late StreamSubscription _playbackStateSub;
|
late StreamSubscription _playbackStateSub;
|
||||||
int? _currentIndex = -1;
|
int? _currentIndex = -1;
|
||||||
int? _prevIndex = -1;
|
int? _prevIndex = -1;
|
||||||
final ScrollController _controller = ScrollController();
|
final ScrollController _controller = ScrollController();
|
||||||
final double height = 90;
|
final double height = 90;
|
||||||
|
BoxConstraints? _widgetConstraints;
|
||||||
Lyrics? _lyrics;
|
Lyrics? _lyrics;
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
CancelableOperation<Lyrics>? _lyricsCancelable;
|
CancelableOperation<Lyrics>? _lyricsCancelable;
|
||||||
|
|
@ -60,7 +85,9 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
||||||
_loading = false;
|
_loading = false;
|
||||||
_lyrics = lyrics;
|
_lyrics = lyrics;
|
||||||
});
|
});
|
||||||
_scrollToLyric();
|
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback(
|
||||||
|
(_) => _updatePosition(audioHandler.playbackState.value.position));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -72,8 +99,15 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
||||||
Future<void> _scrollToLyric() async {
|
Future<void> _scrollToLyric() async {
|
||||||
if (!_controller.hasClients) return;
|
if (!_controller.hasClients) return;
|
||||||
//Lyric height, screen height, appbar height
|
//Lyric height, screen height, appbar height
|
||||||
double scrollTo = (height * _currentIndex!) -
|
double scrollTo;
|
||||||
|
if (_widgetConstraints == null) {
|
||||||
|
scrollTo = (height * _currentIndex!) -
|
||||||
(MediaQuery.of(context).size.height / 4 + height / 2);
|
(MediaQuery.of(context).size.height / 4 + height / 2);
|
||||||
|
} else {
|
||||||
|
final widgetHeight = _widgetConstraints!.maxHeight;
|
||||||
|
final minScroll = height * _currentIndex!;
|
||||||
|
scrollTo = minScroll - widgetHeight / 2 + height / 2;
|
||||||
|
}
|
||||||
|
|
||||||
print(
|
print(
|
||||||
'${height * _currentIndex!}, ${MediaQuery.of(context).size.height / 2}');
|
'${height * _currentIndex!}, ${MediaQuery.of(context).size.height / 2}');
|
||||||
|
|
@ -87,12 +121,7 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
||||||
_animatedScroll = false;
|
_animatedScroll = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _updatePosition(Duration position) {
|
||||||
void initState() {
|
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
|
||||||
//Enable visualizer
|
|
||||||
// if (settings.lyricsVisualizer) playerHelper.startVisualizer();
|
|
||||||
_playbackStateSub = AudioService.position.listen((position) {
|
|
||||||
if (_loading) return;
|
if (_loading) return;
|
||||||
if (!_syncedLyrics) return;
|
if (!_syncedLyrics) return;
|
||||||
_currentIndex =
|
_currentIndex =
|
||||||
|
|
@ -105,7 +134,14 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
||||||
_prevIndex = _currentIndex;
|
_prevIndex = _currentIndex;
|
||||||
if (_freeScroll) return;
|
if (_freeScroll) return;
|
||||||
_scrollToLyric();
|
_scrollToLyric();
|
||||||
});
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
|
//Enable visualizer
|
||||||
|
// if (settings.lyricsVisualizer) playerHelper.startVisualizer();
|
||||||
|
_playbackStateSub = AudioService.position.listen(_updatePosition);
|
||||||
});
|
});
|
||||||
if (audioHandler.mediaItem.value != null) {
|
if (audioHandler.mediaItem.value != null) {
|
||||||
_loadForId(audioHandler.mediaItem.value!.id);
|
_loadForId(audioHandler.mediaItem.value!.id);
|
||||||
|
|
@ -130,19 +166,17 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScrollBehavior get _scrollBehavior {
|
||||||
|
if (_freeScroll) {
|
||||||
|
return ScrollConfiguration.of(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ScrollConfiguration.of(context).copyWith(scrollbars: false);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PlayerScreenBackground(
|
return Column(children: [
|
||||||
enabled: settings.playerBackgroundOnLyrics,
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text('Lyrics'.i18n),
|
|
||||||
systemOverlayStyle: PlayerScreenBackground.getSystemUiOverlayStyle(
|
|
||||||
context,
|
|
||||||
enabled: settings.playerBackgroundOnLyrics),
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
if (_freeScroll && !_loading)
|
if (_freeScroll && !_loading)
|
||||||
Center(
|
Center(
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
|
|
@ -160,9 +194,7 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Stack(children: [
|
child: _error != null
|
||||||
//Lyrics
|
|
||||||
_error != null
|
|
||||||
?
|
?
|
||||||
//Shouldn't really happen, empty lyrics have own text
|
//Shouldn't really happen, empty lyrics have own text
|
||||||
ErrorScreen(message: _error.toString())
|
ErrorScreen(message: _error.toString())
|
||||||
|
|
@ -170,9 +202,10 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
||||||
// Loading lyrics
|
// Loading lyrics
|
||||||
_loading
|
_loading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: NotificationListener<ScrollStartNotification>(
|
: LayoutBuilder(builder: (context, constraints) {
|
||||||
onNotification:
|
_widgetConstraints = constraints;
|
||||||
(ScrollStartNotification notification) {
|
return NotificationListener<ScrollStartNotification>(
|
||||||
|
onNotification: (ScrollStartNotification notification) {
|
||||||
if (!_syncedLyrics) return false;
|
if (!_syncedLyrics) return false;
|
||||||
final extentDiff =
|
final extentDiff =
|
||||||
(notification.metrics.extentBefore -
|
(notification.metrics.extentBefore -
|
||||||
|
|
@ -188,6 +221,8 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
child: ScrollConfiguration(
|
||||||
|
behavior: _scrollBehavior,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
itemCount: _lyrics!.lyrics!.length,
|
itemCount: _lyrics!.lyrics!.length,
|
||||||
|
|
@ -216,7 +251,8 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: _currentIndex == i
|
padding: _currentIndex == i
|
||||||
? EdgeInsets.zero
|
? EdgeInsets.zero
|
||||||
: const EdgeInsets.symmetric(
|
: const EdgeInsets
|
||||||
|
.symmetric(
|
||||||
horizontal: 1.0),
|
horizontal: 1.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
_lyrics!.lyrics![i].text!,
|
_lyrics!.lyrics![i].text!,
|
||||||
|
|
@ -236,39 +272,9 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
||||||
),
|
),
|
||||||
))));
|
))));
|
||||||
},
|
},
|
||||||
)),
|
)));
|
||||||
|
}),
|
||||||
//Visualizer
|
|
||||||
//if (settings.lyricsVisualizer)
|
|
||||||
// Positioned(
|
|
||||||
// bottom: 0,
|
|
||||||
// left: 0,
|
|
||||||
// right: 0,
|
|
||||||
// child: StreamBuilder(
|
|
||||||
// stream: playerHelper.visualizerStream,
|
|
||||||
// builder: (BuildContext context, AsyncSnapshot snapshot) {
|
|
||||||
// List<double> data = snapshot.data ?? [];
|
|
||||||
// double width = MediaQuery.of(context).size.width /
|
|
||||||
// data.length; //- 0.25;
|
|
||||||
// return Row(
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
// children: List.generate(
|
|
||||||
// data.length,
|
|
||||||
// (i) => AnimatedContainer(
|
|
||||||
// duration: Duration(milliseconds: 130),
|
|
||||||
// color: settings.primaryColor,
|
|
||||||
// height: data[i] * 100,
|
|
||||||
// width: width,
|
|
||||||
// )),
|
|
||||||
// );
|
|
||||||
// }),
|
|
||||||
// ),
|
|
||||||
]),
|
|
||||||
),
|
),
|
||||||
const Divider(height: 1.0, thickness: 1.0),
|
]);
|
||||||
const PlayerBar(backgroundColor: Colors.transparent),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:ffi';
|
||||||
|
|
||||||
import 'package:freezer/main.dart';
|
import 'package:freezer/main.dart';
|
||||||
import 'package:freezer/ui/player_bar.dart';
|
import 'package:freezer/ui/player_bar.dart';
|
||||||
|
|
@ -132,8 +133,9 @@ class MenuSheet {
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
enableDrag: true,
|
enableDrag: false,
|
||||||
showDragHandle: false,
|
showDragHandle: false,
|
||||||
|
elevation: 0.0,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return DraggableScrollableSheet(
|
return DraggableScrollableSheet(
|
||||||
initialChildSize: 0.5,
|
initialChildSize: 0.5,
|
||||||
|
|
@ -160,8 +162,12 @@ class MenuSheet {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Default track options
|
//Default track options
|
||||||
void defaultTrackMenu(Track track,
|
void defaultTrackMenu(
|
||||||
{List<Widget> options = const [], Function? onRemove}) {
|
Track track, {
|
||||||
|
List<Widget> options = const [],
|
||||||
|
Function? onRemove,
|
||||||
|
TapDownDetails? details,
|
||||||
|
}) {
|
||||||
showWithTrack(track, [
|
showWithTrack(track, [
|
||||||
addToQueueNext(track),
|
addToQueueNext(track),
|
||||||
addToQueue(track),
|
addToQueue(track),
|
||||||
|
|
@ -359,7 +365,9 @@ class MenuSheet {
|
||||||
|
|
||||||
//Default album options
|
//Default album options
|
||||||
void defaultAlbumMenu(Album album,
|
void defaultAlbumMenu(Album album,
|
||||||
{List<Widget> options = const [], Function? onRemove}) {
|
{List<Widget> options = const [],
|
||||||
|
Function? onRemove,
|
||||||
|
TapDownDetails? details}) {
|
||||||
show([
|
show([
|
||||||
album.library!
|
album.library!
|
||||||
? removeAlbum(album, onRemove: onRemove)
|
? removeAlbum(album, onRemove: onRemove)
|
||||||
|
|
@ -424,7 +432,9 @@ class MenuSheet {
|
||||||
//===================
|
//===================
|
||||||
|
|
||||||
void defaultArtistMenu(Artist artist,
|
void defaultArtistMenu(Artist artist,
|
||||||
{List<Widget> options = const [], Function? onRemove}) {
|
{List<Widget> options = const [],
|
||||||
|
Function? onRemove,
|
||||||
|
TapDownDetails? details}) {
|
||||||
show([
|
show([
|
||||||
artist.library!
|
artist.library!
|
||||||
? removeArtist(artist, onRemove: onRemove)
|
? removeArtist(artist, onRemove: onRemove)
|
||||||
|
|
@ -467,8 +477,10 @@ class MenuSheet {
|
||||||
void defaultPlaylistMenu(Playlist playlist,
|
void defaultPlaylistMenu(Playlist playlist,
|
||||||
{List<Widget> options = const [],
|
{List<Widget> options = const [],
|
||||||
Function? onRemove,
|
Function? onRemove,
|
||||||
Function? onUpdate}) {
|
Function? onUpdate,
|
||||||
|
TapDownDetails? details}) {
|
||||||
show([
|
show([
|
||||||
|
if (playlist.library != null)
|
||||||
playlist.library!
|
playlist.library!
|
||||||
? removePlaylistLibrary(playlist, onRemove: onRemove)
|
? removePlaylistLibrary(playlist, onRemove: onRemove)
|
||||||
: addPlaylistLibrary(playlist),
|
: addPlaylistLibrary(playlist),
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,7 @@ class FancyScaffoldState extends State<FancyScaffold>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlayerBar extends StatefulWidget {
|
class PlayerBar extends StatelessWidget {
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
final bool shouldHaveHero;
|
final bool shouldHaveHero;
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
|
|
@ -211,14 +211,7 @@ class PlayerBar extends StatefulWidget {
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
|
||||||
State<PlayerBar> createState() => _PlayerBarState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PlayerBarState extends State<PlayerBar> {
|
|
||||||
final double iconSize = 28;
|
final double iconSize = 28;
|
||||||
late StreamSubscription mediaItemSub;
|
|
||||||
late bool _isNothingPlaying = audioHandler.mediaItem.value == null;
|
|
||||||
|
|
||||||
double parsePosition(Duration position) {
|
double parsePosition(Duration position) {
|
||||||
if (audioHandler.mediaItem.value == null) return 0.0;
|
if (audioHandler.mediaItem.value == null) return 0.0;
|
||||||
|
|
@ -229,59 +222,26 @@ class _PlayerBarState extends State<PlayerBar> {
|
||||||
audioHandler.mediaItem.value!.duration!.inSeconds;
|
audioHandler.mediaItem.value!.duration!.inSeconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Color? get _backgroundColor => backgroundColor;
|
||||||
void initState() {
|
|
||||||
mediaItemSub = audioHandler.mediaItem.listen((playingItem) {
|
|
||||||
if ((playingItem == null && !_isNothingPlaying) ||
|
|
||||||
(playingItem != null && _isNothingPlaying)) {
|
|
||||||
setState(() => _isNothingPlaying = playingItem == null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
Color get backgroundColor =>
|
|
||||||
widget.backgroundColor ??
|
|
||||||
Theme.of(context).navigationBarTheme.backgroundColor ??
|
|
||||||
Theme.of(context).colorScheme.surface;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
mediaItemSub.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _gestureRegistered = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 68.0,
|
height: 68.0,
|
||||||
child: _isNothingPlaying
|
|
||||||
? null
|
|
||||||
: GestureDetector(
|
|
||||||
onHorizontalDragUpdate: (details) async {
|
|
||||||
if (_gestureRegistered) return;
|
|
||||||
const double sensitivity = 12.69;
|
|
||||||
//Right swipe
|
|
||||||
_gestureRegistered = true;
|
|
||||||
if (details.delta.dx > sensitivity) {
|
|
||||||
await audioHandler.skipToPrevious();
|
|
||||||
}
|
|
||||||
//Left
|
|
||||||
if (details.delta.dx < -sensitivity) {
|
|
||||||
await audioHandler.skipToNext();
|
|
||||||
}
|
|
||||||
_gestureRegistered = false;
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: StreamBuilder<MediaItem?>(
|
child: StreamBuilder<MediaItem?>(
|
||||||
stream: audioHandler.mediaItem,
|
stream: audioHandler.mediaItem,
|
||||||
initialData: audioHandler.mediaItem.valueOrNull,
|
initialData: audioHandler.mediaItem.valueOrNull,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData) return const SizedBox();
|
if (snapshot.data == null) {
|
||||||
|
return Material(
|
||||||
|
child: ListTile(
|
||||||
|
leading: Image.asset('assets/cover_thumb.jpg'),
|
||||||
|
title: Text('Nothing is currently playing'.i18n),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
final currentMediaItem = snapshot.data!;
|
final currentMediaItem = snapshot.data!;
|
||||||
final image = CachedImage(
|
final image = CachedImage(
|
||||||
width: 50,
|
width: 50,
|
||||||
|
|
@ -289,16 +249,16 @@ class _PlayerBarState extends State<PlayerBar> {
|
||||||
url: currentMediaItem.extras!['thumb'] ??
|
url: currentMediaItem.extras!['thumb'] ??
|
||||||
currentMediaItem.artUri.toString(),
|
currentMediaItem.artUri.toString(),
|
||||||
);
|
);
|
||||||
final leadingWidget = widget.shouldHaveHero
|
final leadingWidget = shouldHaveHero
|
||||||
? Hero(tag: currentMediaItem.id, child: image)
|
? Hero(tag: currentMediaItem.id, child: image)
|
||||||
: image;
|
: image;
|
||||||
return Material(
|
return Material(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
dense: true,
|
tileColor: _backgroundColor,
|
||||||
focusNode: widget.focusNode,
|
focusNode: focusNode,
|
||||||
contentPadding:
|
contentPadding:
|
||||||
const EdgeInsets.symmetric(horizontal: 8.0),
|
const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
onTap: widget.onTap,
|
onTap: onTap,
|
||||||
leading: AnimatedSwitcher(
|
leading: AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 250),
|
duration: const Duration(milliseconds: 250),
|
||||||
child: leadingWidget),
|
child: leadingWidget),
|
||||||
|
|
@ -342,7 +302,6 @@ class _PlayerBarState extends State<PlayerBar> {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
@ -9,6 +10,7 @@ import 'package:freezer/api/cache.dart';
|
||||||
import 'package:freezer/api/deezer.dart';
|
import 'package:freezer/api/deezer.dart';
|
||||||
import 'package:freezer/api/definitions.dart';
|
import 'package:freezer/api/definitions.dart';
|
||||||
import 'package:freezer/api/player.dart';
|
import 'package:freezer/api/player.dart';
|
||||||
|
import 'package:freezer/main.dart';
|
||||||
import 'package:freezer/page_routes/fade.dart';
|
import 'package:freezer/page_routes/fade.dart';
|
||||||
import 'package:freezer/settings.dart';
|
import 'package:freezer/settings.dart';
|
||||||
import 'package:freezer/translations.i18n.dart';
|
import 'package:freezer/translations.i18n.dart';
|
||||||
|
|
@ -91,7 +93,9 @@ class PlayerScreen extends StatelessWidget {
|
||||||
return ChangeNotifierProvider(
|
return ChangeNotifierProvider(
|
||||||
create: (context) => BackgroundProvider(),
|
create: (context) => BackgroundProvider(),
|
||||||
child: PlayerScreenBackground(
|
child: PlayerScreenBackground(
|
||||||
child: OrientationBuilder(
|
child: MainScreen.of(context).isDesktop
|
||||||
|
? const PlayerScreenDesktop()
|
||||||
|
: OrientationBuilder(
|
||||||
builder: (context, orientation) =>
|
builder: (context, orientation) =>
|
||||||
orientation == Orientation.landscape
|
orientation == Orientation.landscape
|
||||||
? const PlayerScreenHorizontal()
|
? const PlayerScreenHorizontal()
|
||||||
|
|
@ -287,8 +291,11 @@ class PlayerScreenVertical extends StatelessWidget {
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||||
child: PlayerTextSubtext(textSize: 64.sp),
|
child: PlayerTextSubtext(textSize: 64.sp),
|
||||||
),
|
),
|
||||||
SeekBar(textSize: 48.sp),
|
SeekBar(textSize: 38.sp),
|
||||||
PlaybackControls(86.sp),
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: PlaybackControls(86.sp),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(vertical: 0, horizontal: 16.0),
|
const EdgeInsets.symmetric(vertical: 0, horizontal: 16.0),
|
||||||
|
|
@ -299,6 +306,98 @@ class PlayerScreenVertical extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PlayerScreenDesktop extends StatelessWidget {
|
||||||
|
const PlayerScreenDesktop({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(children: [
|
||||||
|
Flexible(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
|
child: PlayerScreenTopRow(
|
||||||
|
textSize: 10.sp,
|
||||||
|
iconSize: 17.sp,
|
||||||
|
showQueueButton: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints.loose(const Size.square(500)),
|
||||||
|
child: const BigAlbumArt()),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||||
|
child: PlayerTextSubtext(textSize: 18.sp),
|
||||||
|
),
|
||||||
|
SeekBar(textSize: 12.sp),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: PlaybackControls(24.sp),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 0, horizontal: 16.0),
|
||||||
|
child: BottomBarControls(
|
||||||
|
size: 16.sp,
|
||||||
|
showLyricsButton: false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(left: 16.0, right: 16.0, top: 24.0),
|
||||||
|
child: _DesktopTabView(),
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DesktopTabView extends StatelessWidget {
|
||||||
|
const _DesktopTabView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DefaultTabController(
|
||||||
|
length: 2,
|
||||||
|
child: Column(children: [
|
||||||
|
TabBar(
|
||||||
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
text: 'Queue'.i18n,
|
||||||
|
height: 48.0,
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
text: 'Lyrics'.i18n,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
labelStyle: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.labelLarge!
|
||||||
|
.copyWith(fontSize: 18.0)),
|
||||||
|
const Expanded(
|
||||||
|
child: SizedBox.expand(
|
||||||
|
child: Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: TabBarView(children: [
|
||||||
|
!kDebugMode
|
||||||
|
? Text('Queue view is disabled in Debug mode')
|
||||||
|
: QueueListWidget(),
|
||||||
|
LyricsWidget(),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class FitOrScrollText extends StatefulWidget {
|
class FitOrScrollText extends StatefulWidget {
|
||||||
final String text;
|
final String text;
|
||||||
final TextStyle style;
|
final TextStyle style;
|
||||||
|
|
@ -741,23 +840,32 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
|
||||||
// },
|
// },
|
||||||
onTap: () => Navigator.push(
|
onTap: () => Navigator.push(
|
||||||
context,
|
context,
|
||||||
PageRouteBuilder(
|
FadePageRoute(
|
||||||
opaque: false, // transparent background
|
|
||||||
barrierDismissible: true,
|
barrierDismissible: true,
|
||||||
pageBuilder: (context, animation, __) {
|
builder: (context) {
|
||||||
return FadeTransition(
|
final mediaItem = audioHandler.mediaItem.value!;
|
||||||
opacity: animation,
|
return ZoomableImageRoute(
|
||||||
child: PhotoView(
|
imageUrl: mediaItem.artUri.toString(), heroKey: mediaItem.id);
|
||||||
imageProvider: CachedNetworkImageProvider(
|
},
|
||||||
audioHandler.mediaItem.value!.artUri.toString()),
|
)
|
||||||
maxScale: 8.0,
|
// PageRouteBuilder(
|
||||||
minScale: 0.2,
|
// opaque: false, // transparent background
|
||||||
heroAttributes: PhotoViewHeroAttributes(
|
// barrierDismissible: true,
|
||||||
tag: audioHandler.mediaItem.value!.id),
|
// pageBuilder: (context, animation, __) {
|
||||||
backgroundDecoration: const BoxDecoration(
|
// return FadeTransition(
|
||||||
color: Color.fromARGB(0x90, 0, 0, 0))),
|
// opacity: animation,
|
||||||
);
|
// child: PhotoView(
|
||||||
})),
|
// imageProvider: CachedNetworkImageProvider(
|
||||||
|
// audioHandler.mediaItem.value!.artUri.toString()),
|
||||||
|
// maxScale: 8.0,
|
||||||
|
// minScale: 0.2,
|
||||||
|
// heroAttributes: PhotoViewHeroAttributes(
|
||||||
|
// tag: audioHandler.mediaItem.value!.id),
|
||||||
|
// backgroundDecoration: const BoxDecoration(
|
||||||
|
// color: Color.fromARGB(0x90, 0, 0, 0))),
|
||||||
|
// );
|
||||||
|
// }),
|
||||||
|
),
|
||||||
onHorizontalDragDown: (_) => _userScroll = true,
|
onHorizontalDragDown: (_) => _userScroll = true,
|
||||||
// delayed a bit, so to make sure that the page view updated.
|
// delayed a bit, so to make sure that the page view updated.
|
||||||
onHorizontalDragEnd: (_) => Future.delayed(
|
onHorizontalDragEnd: (_) => Future.delayed(
|
||||||
|
|
@ -826,12 +934,14 @@ class PlayerScreenTopRow extends StatelessWidget {
|
||||||
final double? iconSize;
|
final double? iconSize;
|
||||||
final double? textWidth;
|
final double? textWidth;
|
||||||
final bool short;
|
final bool short;
|
||||||
|
final bool showQueueButton; // not needed on desktop
|
||||||
const PlayerScreenTopRow(
|
const PlayerScreenTopRow(
|
||||||
{super.key,
|
{super.key,
|
||||||
this.textSize,
|
this.textSize,
|
||||||
this.iconSize,
|
this.iconSize,
|
||||||
this.textWidth,
|
this.textWidth,
|
||||||
this.short = false});
|
this.short = false,
|
||||||
|
this.showQueueButton = true});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -867,7 +977,8 @@ class PlayerScreenTopRow extends StatelessWidget {
|
||||||
TextSpan(text: playerHelper.queueSource!.text ?? '')
|
TextSpan(text: playerHelper.queueSource!.text ?? '')
|
||||||
], style: TextStyle(fontSize: textSize ?? 38.sp))),
|
], style: TextStyle(fontSize: textSize ?? 38.sp))),
|
||||||
),
|
),
|
||||||
IconButton(
|
showQueueButton
|
||||||
|
? IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.menu,
|
Icons.menu,
|
||||||
semanticLabel: "Queue".i18n,
|
semanticLabel: "Queue".i18n,
|
||||||
|
|
@ -876,7 +987,8 @@ class PlayerScreenTopRow extends StatelessWidget {
|
||||||
splashRadius: size * 1.5,
|
splashRadius: size * 1.5,
|
||||||
onPressed: () => Navigator.of(context)
|
onPressed: () => Navigator.of(context)
|
||||||
.pushRoute(builder: (context) => const QueueScreen()),
|
.pushRoute(builder: (context) => const QueueScreen()),
|
||||||
),
|
)
|
||||||
|
: SizedBox.square(dimension: size + 16.0),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -997,7 +1109,13 @@ class _SeekBarState extends State<SeekBar> {
|
||||||
|
|
||||||
class BottomBarControls extends StatelessWidget {
|
class BottomBarControls extends StatelessWidget {
|
||||||
final double size;
|
final double size;
|
||||||
const BottomBarControls({Key? key, required this.size}) : super(key: key);
|
final bool
|
||||||
|
showLyricsButton; // removed in desktop mode, because there's a tabbed view which includes it
|
||||||
|
const BottomBarControls({
|
||||||
|
super.key,
|
||||||
|
required this.size,
|
||||||
|
this.showLyricsButton = true,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -1018,6 +1136,8 @@ class BottomBarControls extends StatelessWidget {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
QualityInfoWidget(textSize: size * 0.75),
|
||||||
|
if (showLyricsButton)
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: size,
|
iconSize: size,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
|
|
@ -1055,7 +1175,6 @@ class BottomBarControls extends StatelessWidget {
|
||||||
// toastLength: Toast.LENGTH_SHORT);
|
// toastLength: Toast.LENGTH_SHORT);
|
||||||
// },
|
// },
|
||||||
// ),
|
// ),
|
||||||
QualityInfoWidget(textSize: size * 0.75),
|
|
||||||
FavoriteButton(size: size * 0.85),
|
FavoriteButton(size: size * 0.85),
|
||||||
PlayerMenuButton(size: size)
|
PlayerMenuButton(size: size)
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
@ -8,15 +9,50 @@ import 'package:freezer/translations.i18n.dart';
|
||||||
import 'package:freezer/ui/menu.dart';
|
import 'package:freezer/ui/menu.dart';
|
||||||
import 'package:freezer/ui/tiles.dart';
|
import 'package:freezer/ui/tiles.dart';
|
||||||
|
|
||||||
class QueueScreen extends StatefulWidget {
|
class QueueScreen extends StatelessWidget {
|
||||||
const QueueScreen({super.key});
|
const QueueScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<QueueScreen> createState() => _QueueScreenState();
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Queue'.i18n),
|
||||||
|
systemOverlayStyle: SystemUiOverlayStyle(
|
||||||
|
statusBarColor: Colors.transparent,
|
||||||
|
statusBarIconBrightness: Brightness.light,
|
||||||
|
statusBarBrightness: Brightness.light,
|
||||||
|
systemNavigationBarColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
systemNavigationBarDividerColor: Color(
|
||||||
|
Theme.of(context).scaffoldBackgroundColor.value - 0x00111111),
|
||||||
|
systemNavigationBarIconBrightness: Brightness.light,
|
||||||
|
),
|
||||||
|
// actions: <Widget>[
|
||||||
|
// IconButton(
|
||||||
|
// icon: Icon(
|
||||||
|
// Icons.shuffle,
|
||||||
|
// semanticLabel: "Shuffle".i18n,
|
||||||
|
// ),
|
||||||
|
// onPressed: () async {
|
||||||
|
// await playerHelper.toggleShuffle();
|
||||||
|
// setState(() {});
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
// ],
|
||||||
|
),
|
||||||
|
body: const SafeArea(child: QueueListWidget(shouldPopOnTap: true)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _QueueScreenState extends State<QueueScreen> {
|
class QueueListWidget extends StatefulWidget {
|
||||||
static const itemExtent = 72.0; // height of each TrackTile
|
final bool shouldPopOnTap;
|
||||||
|
const QueueListWidget({super.key, this.shouldPopOnTap = false});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<QueueListWidget> createState() => _QueueListWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QueueListWidgetState extends State<QueueListWidget> {
|
||||||
|
static const itemExtent = 68.0; // height of each TrackTile
|
||||||
final _scrollController = ScrollController();
|
final _scrollController = ScrollController();
|
||||||
late StreamSubscription _queueSub;
|
late StreamSubscription _queueSub;
|
||||||
static const _dismissibleBackground = DecoratedBox(
|
static const _dismissibleBackground = DecoratedBox(
|
||||||
|
|
@ -55,7 +91,9 @@ class _QueueScreenState extends State<QueueScreen> {
|
||||||
});
|
});
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
// calculate position of current item
|
// calculate position of current item
|
||||||
final position = playerHelper.queueIndex * itemExtent;
|
double position = min(playerHelper.queueIndex * itemExtent,
|
||||||
|
_scrollController.position.maxScrollExtent);
|
||||||
|
|
||||||
_scrollController.jumpTo(position);
|
_scrollController.jumpTo(position);
|
||||||
});
|
});
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -69,33 +107,7 @@ class _QueueScreenState extends State<QueueScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return ReorderableListView.builder(
|
||||||
appBar: AppBar(
|
|
||||||
title: Text('Queue'.i18n),
|
|
||||||
systemOverlayStyle: SystemUiOverlayStyle(
|
|
||||||
statusBarColor: Colors.transparent,
|
|
||||||
statusBarIconBrightness: Brightness.light,
|
|
||||||
statusBarBrightness: Brightness.light,
|
|
||||||
systemNavigationBarColor: Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
systemNavigationBarDividerColor: Color(
|
|
||||||
Theme.of(context).scaffoldBackgroundColor.value - 0x00111111),
|
|
||||||
systemNavigationBarIconBrightness: Brightness.light,
|
|
||||||
),
|
|
||||||
// actions: <Widget>[
|
|
||||||
// IconButton(
|
|
||||||
// icon: Icon(
|
|
||||||
// Icons.shuffle,
|
|
||||||
// semanticLabel: "Shuffle".i18n,
|
|
||||||
// ),
|
|
||||||
// onPressed: () async {
|
|
||||||
// await playerHelper.toggleShuffle();
|
|
||||||
// setState(() {});
|
|
||||||
// },
|
|
||||||
// )
|
|
||||||
// ],
|
|
||||||
),
|
|
||||||
body: SafeArea(
|
|
||||||
child: ReorderableListView.builder(
|
|
||||||
buildDefaultDragHandles: false,
|
buildDefaultDragHandles: false,
|
||||||
scrollController: _scrollController,
|
scrollController: _scrollController,
|
||||||
// specify the itemExtent normally and remove it when reordering because of an issue with [SliverFixedExtentList]
|
// specify the itemExtent normally and remove it when reordering because of an issue with [SliverFixedExtentList]
|
||||||
|
|
@ -112,10 +124,10 @@ class _QueueScreenState extends State<QueueScreen> {
|
||||||
},
|
},
|
||||||
itemCount: _queueCache.length,
|
itemCount: _queueCache.length,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
Track track = Track.fromMediaItem(_queueCache[index]);
|
final mediaItem = _queueCache[index];
|
||||||
final int itemId = _queueCache[index].extras!['id'] ?? 0;
|
final int itemId = mediaItem.extras!['id'] ?? 0;
|
||||||
return Dismissible(
|
return Dismissible(
|
||||||
key: ValueKey(track.id.hashCode ^ itemId),
|
key: ValueKey(mediaItem.id.hashCode | itemId),
|
||||||
background: _dismissibleBackground,
|
background: _dismissibleBackground,
|
||||||
secondaryBackground: _dismissibleSecondaryBackground,
|
secondaryBackground: _dismissibleSecondaryBackground,
|
||||||
onDismissed: (_) {
|
onDismissed: (_) {
|
||||||
|
|
@ -145,22 +157,24 @@ class _QueueScreenState extends State<QueueScreen> {
|
||||||
},
|
},
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: itemExtent,
|
height: itemExtent,
|
||||||
child: TrackTile(
|
child: TrackTile.fromMediaItem(
|
||||||
track,
|
mediaItem,
|
||||||
trailing: ReorderableDragStartListener(
|
trailing: ReorderableDragStartListener(
|
||||||
index: index, child: const Icon(Icons.drag_handle)),
|
index: index, child: const Icon(Icons.drag_handle)),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
audioHandler.skipToQueueItem(index).then((value) {
|
audioHandler.skipToQueueItem(index).then((value) {
|
||||||
|
if (widget.shouldPopOnTap) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onHold: () => MenuSheet(context).defaultTrackMenu(track),
|
onSecondary: (_) => MenuSheet(context)
|
||||||
|
.defaultTrackMenu(Track.fromMediaItem(mediaItem)),
|
||||||
|
checkTrackOffline: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -277,7 +277,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
final data = cache.searchHistory[i].data;
|
final data = cache.searchHistory[i].data;
|
||||||
switch (cache.searchHistory[i].type) {
|
switch (cache.searchHistory[i].type) {
|
||||||
case SearchHistoryItemType.track:
|
case SearchHistoryItemType.track:
|
||||||
return TrackTile(
|
return TrackTile.fromTrack(
|
||||||
data,
|
data,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
List<Track?> queue = cache.searchHistory
|
List<Track?> queue = cache.searchHistory
|
||||||
|
|
@ -293,8 +293,8 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
source: 'searchhistory',
|
source: 'searchhistory',
|
||||||
id: 'searchhistory'));
|
id: 'searchhistory'));
|
||||||
},
|
},
|
||||||
onHold: () =>
|
onSecondary: (details) => MenuSheet(context)
|
||||||
MenuSheet(context).defaultTrackMenu(data),
|
.defaultTrackMenu(data, details: details),
|
||||||
trailing: _removeHistoryItemWidget(i),
|
trailing: _removeHistoryItemWidget(i),
|
||||||
);
|
);
|
||||||
case SearchHistoryItemType.album:
|
case SearchHistoryItemType.album:
|
||||||
|
|
@ -304,8 +304,8 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
Navigator.of(context).pushRoute(
|
Navigator.of(context).pushRoute(
|
||||||
builder: (context) => AlbumDetails(data));
|
builder: (context) => AlbumDetails(data));
|
||||||
},
|
},
|
||||||
onHold: () =>
|
onSecondary: (details) => MenuSheet(context)
|
||||||
MenuSheet(context).defaultAlbumMenu(data),
|
.defaultAlbumMenu(data, details: details),
|
||||||
trailing: _removeHistoryItemWidget(i),
|
trailing: _removeHistoryItemWidget(i),
|
||||||
);
|
);
|
||||||
case SearchHistoryItemType.artist:
|
case SearchHistoryItemType.artist:
|
||||||
|
|
@ -328,8 +328,9 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
PlaylistDetails(data));
|
PlaylistDetails(data));
|
||||||
},
|
},
|
||||||
onHold: () => MenuSheet(context)
|
onSecondary: (details) => MenuSheet(context)
|
||||||
.defaultPlaylistMenu(data),
|
.defaultPlaylistMenu(data,
|
||||||
|
details: details),
|
||||||
trailing: _removeHistoryItemWidget(i),
|
trailing: _removeHistoryItemWidget(i),
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
|
@ -477,7 +478,7 @@ class SearchResultsScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
for (final track in results.tracks!
|
for (final track in results.tracks!
|
||||||
.getRange(0, min(results.tracks!.length, 3)))
|
.getRange(0, min(results.tracks!.length, 3)))
|
||||||
TrackTile(track, onTap: () {
|
TrackTile.fromTrack(track, onTap: () {
|
||||||
cache.addToSearchHistory(track);
|
cache.addToSearchHistory(track);
|
||||||
playerHelper.playFromTrackList(
|
playerHelper.playFromTrackList(
|
||||||
results.tracks!,
|
results.tracks!,
|
||||||
|
|
@ -486,9 +487,9 @@ class SearchResultsScreen extends StatelessWidget {
|
||||||
text: 'Search'.i18n,
|
text: 'Search'.i18n,
|
||||||
id: query,
|
id: query,
|
||||||
source: 'search'));
|
source: 'search'));
|
||||||
}, onHold: () {
|
}, onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultTrackMenu(track);
|
m.defaultTrackMenu(track, details: details);
|
||||||
}),
|
}),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text('Show all tracks'.i18n),
|
title: Text('Show all tracks'.i18n),
|
||||||
|
|
@ -518,9 +519,9 @@ class SearchResultsScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
for (final album in results.albums!
|
for (final album in results.albums!
|
||||||
.getRange(0, min(results.albums!.length, 3)))
|
.getRange(0, min(results.albums!.length, 3)))
|
||||||
AlbumTile(album, onHold: () {
|
AlbumTile(album, onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultAlbumMenu(album);
|
m.defaultAlbumMenu(album, details: details);
|
||||||
}, onTap: () {
|
}, onTap: () {
|
||||||
cache.addToSearchHistory(album);
|
cache.addToSearchHistory(album);
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
|
|
@ -560,9 +561,9 @@ class SearchResultsScreen extends StatelessWidget {
|
||||||
Navigator.of(context).pushRoute(
|
Navigator.of(context).pushRoute(
|
||||||
builder: (context) => ArtistDetails(artist));
|
builder: (context) => ArtistDetails(artist));
|
||||||
},
|
},
|
||||||
onHold: () {
|
onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultArtistMenu(artist);
|
m.defaultArtistMenu(artist, details: details);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
])),
|
])),
|
||||||
|
|
@ -590,9 +591,9 @@ class SearchResultsScreen extends StatelessWidget {
|
||||||
Navigator.of(context).pushRoute(
|
Navigator.of(context).pushRoute(
|
||||||
builder: (context) => PlaylistDetails(playlist));
|
builder: (context) => PlaylistDetails(playlist));
|
||||||
},
|
},
|
||||||
onHold: () {
|
onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultPlaylistMenu(playlist);
|
m.defaultPlaylistMenu(playlist, details: details);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|
@ -702,14 +703,14 @@ class TrackListScreen extends StatelessWidget {
|
||||||
itemCount: tracks!.length,
|
itemCount: tracks!.length,
|
||||||
itemBuilder: (BuildContext context, int i) {
|
itemBuilder: (BuildContext context, int i) {
|
||||||
Track t = tracks![i]!;
|
Track t = tracks![i]!;
|
||||||
return TrackTile(
|
return TrackTile.fromTrack(
|
||||||
t,
|
t,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
playerHelper.playFromTrackList(tracks!, t.id, queueSource);
|
playerHelper.playFromTrackList(tracks!, t.id, queueSource);
|
||||||
},
|
},
|
||||||
onHold: () {
|
onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultTrackMenu(t);
|
m.defaultTrackMenu(t, details: details);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -737,9 +738,9 @@ class AlbumListScreen extends StatelessWidget {
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
.pushRoute(builder: (context) => AlbumDetails(a));
|
.pushRoute(builder: (context) => AlbumDetails(a));
|
||||||
},
|
},
|
||||||
onHold: () {
|
onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultAlbumMenu(a!);
|
m.defaultAlbumMenu(a!, details: details);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -766,9 +767,9 @@ class SearchResultPlaylists extends StatelessWidget {
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
.pushRoute(builder: (context) => PlaylistDetails(p));
|
.pushRoute(builder: (context) => PlaylistDetails(p));
|
||||||
},
|
},
|
||||||
onHold: () {
|
onSecondary: (details) {
|
||||||
MenuSheet m = MenuSheet(context);
|
MenuSheet m = MenuSheet(context);
|
||||||
m.defaultPlaylistMenu(p!);
|
m.defaultPlaylistMenu(p!, details: details);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:country_pickers/country.dart';
|
import 'package:country_pickers/country.dart';
|
||||||
import 'package:country_pickers/country_picker_dialog.dart';
|
import 'package:country_pickers/country_picker_dialog.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||||
|
|
@ -144,6 +147,18 @@ class AppearanceSettings extends StatefulWidget {
|
||||||
class _AppearanceSettingsState extends State<AppearanceSettings> {
|
class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||||
ColorSwatch<dynamic> _swatch(int c) => ColorSwatch(c, {500: Color(c)});
|
ColorSwatch<dynamic> _swatch(int c) => ColorSwatch(c, {500: Color(c)});
|
||||||
|
|
||||||
|
String _navigationRailAppearanceToString(
|
||||||
|
NavigationRailAppearance navigationRailAppearance) {
|
||||||
|
switch (navigationRailAppearance) {
|
||||||
|
case NavigationRailAppearance.always_expanded:
|
||||||
|
return 'Always expanded'.i18n;
|
||||||
|
case NavigationRailAppearance.expand_on_hover:
|
||||||
|
return 'Expand on hover'.i18n;
|
||||||
|
case NavigationRailAppearance.icons_only:
|
||||||
|
return 'Icons only'.i18n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -411,7 +426,30 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||||
value: settings.useArtColor,
|
value: settings.useArtColor,
|
||||||
onChanged: (v) => setState(() => settings.updateUseArtColor(v)),
|
onChanged: (v) => setState(() => settings.updateUseArtColor(v)),
|
||||||
),
|
),
|
||||||
//Display mode
|
if (MainScreen.of(context).isDesktop)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.view_sidebar),
|
||||||
|
title: Text('Navigation rail appearance'.i18n),
|
||||||
|
subtitle: Text(
|
||||||
|
'${'Currently'.i18n}: ${_navigationRailAppearanceToString(settings.navigationRailAppearance)}'),
|
||||||
|
onTap: () => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => SimpleDialog(
|
||||||
|
title: Text('Navigation rail appearance'.i18n),
|
||||||
|
children: NavigationRailAppearance.values
|
||||||
|
.map((value) => SimpleDialogOption(
|
||||||
|
child: Text(
|
||||||
|
_navigationRailAppearanceToString(value)),
|
||||||
|
onPressed: () {
|
||||||
|
settings.navigationRailAppearance = value;
|
||||||
|
Navigator.pop(context);
|
||||||
|
settings.save().then((_) => updateTheme());
|
||||||
|
}))
|
||||||
|
.toList(growable: false),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
//Display mode (Android only!)
|
||||||
|
if (defaultTargetPlatform == TargetPlatform.android)
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.screen_lock_portrait),
|
leading: const Icon(Icons.screen_lock_portrait),
|
||||||
title: Text('Change display mode'.i18n),
|
title: Text('Change display mode'.i18n),
|
||||||
|
|
|
||||||
|
|
@ -12,31 +12,107 @@ import 'cached_image.dart';
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
class TrackTile extends StatelessWidget {
|
typedef SecondaryTapCallback = void Function(TapDownDetails?);
|
||||||
final Track track;
|
|
||||||
final void Function()? onTap;
|
|
||||||
final void Function()? onHold;
|
|
||||||
final Widget? trailing;
|
|
||||||
|
|
||||||
const TrackTile(this.track,
|
class WrapSecondaryAction extends StatelessWidget {
|
||||||
{this.onTap, this.onHold, this.trailing, Key? key})
|
final SecondaryTapCallback? onSecondaryTapDown;
|
||||||
: super(key: key);
|
final Widget child;
|
||||||
|
const WrapSecondaryAction(
|
||||||
|
{super.key, this.onSecondaryTapDown, required this.child});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return GestureDetector(
|
||||||
|
onSecondaryTapDown: onSecondaryTapDown,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VoidCallback? normalizeSecondary(SecondaryTapCallback? callback) {
|
||||||
|
if (callback == null) return null;
|
||||||
|
|
||||||
|
return () => callback.call(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrackTile extends StatelessWidget {
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
/// Hold or Right Click
|
||||||
|
final SecondaryTapCallback? onSecondary;
|
||||||
|
final Widget? trailing;
|
||||||
|
final String trackId;
|
||||||
|
final String title;
|
||||||
|
final String artist;
|
||||||
|
final String artUri;
|
||||||
|
final bool explicit;
|
||||||
|
final String durationString;
|
||||||
|
|
||||||
|
/// Disable if not needed, makes app lag, and uses lots of resources
|
||||||
|
final bool checkTrackOffline;
|
||||||
|
|
||||||
|
const TrackTile({
|
||||||
|
required this.trackId,
|
||||||
|
required this.title,
|
||||||
|
required this.artist,
|
||||||
|
required this.artUri,
|
||||||
|
required this.explicit,
|
||||||
|
required this.durationString,
|
||||||
|
this.onTap,
|
||||||
|
this.onSecondary,
|
||||||
|
this.trailing,
|
||||||
|
this.checkTrackOffline = true,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
factory TrackTile.fromTrack(Track track,
|
||||||
|
{VoidCallback? onTap,
|
||||||
|
SecondaryTapCallback? onSecondary,
|
||||||
|
Widget? trailing,
|
||||||
|
bool checkTrackOffline = true}) =>
|
||||||
|
TrackTile(
|
||||||
|
trackId: track.id,
|
||||||
|
title: track.title!,
|
||||||
|
artist: track.artistString,
|
||||||
|
artUri: track.albumArt!.thumb,
|
||||||
|
explicit: track.explicit!,
|
||||||
|
durationString: track.durationString,
|
||||||
|
onSecondary: onSecondary,
|
||||||
|
onTap: onTap,
|
||||||
|
trailing: trailing,
|
||||||
|
checkTrackOffline: checkTrackOffline,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory TrackTile.fromMediaItem(MediaItem mediaItem,
|
||||||
|
{VoidCallback? onTap,
|
||||||
|
SecondaryTapCallback? onSecondary,
|
||||||
|
Widget? trailing,
|
||||||
|
bool checkTrackOffline = true}) =>
|
||||||
|
TrackTile(
|
||||||
|
trackId: mediaItem.id,
|
||||||
|
title: mediaItem.title,
|
||||||
|
artist: mediaItem.artist!,
|
||||||
|
artUri: mediaItem.extras!['thumb'],
|
||||||
|
explicit: false,
|
||||||
|
durationString: Track.durationAsString(mediaItem.duration!),
|
||||||
|
onSecondary: onSecondary,
|
||||||
|
onTap: onTap,
|
||||||
|
trailing: trailing,
|
||||||
|
checkTrackOffline: checkTrackOffline,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return WrapSecondaryAction(
|
||||||
|
onSecondaryTapDown: onSecondary,
|
||||||
|
child: ListTile(
|
||||||
title: StreamBuilder<MediaItem?>(
|
title: StreamBuilder<MediaItem?>(
|
||||||
stream: audioHandler.mediaItem,
|
stream: audioHandler.mediaItem,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final bool isHighlighted;
|
|
||||||
final mediaItem = snapshot.data;
|
final mediaItem = snapshot.data;
|
||||||
if (!snapshot.hasData || snapshot.data == null) {
|
final bool isHighlighted = mediaItem?.id == trackId;
|
||||||
isHighlighted = false;
|
|
||||||
} else {
|
|
||||||
isHighlighted = mediaItem!.id == track.id;
|
|
||||||
}
|
|
||||||
return Text(
|
return Text(
|
||||||
track.title!,
|
title,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.clip,
|
overflow: TextOverflow.clip,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
|
@ -46,21 +122,23 @@ class TrackTile extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
track.artistString,
|
artist,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
leading: CachedImage(
|
leading: CachedImage(
|
||||||
url: track.albumArt!.thumb,
|
url: artUri,
|
||||||
width: 48.0,
|
width: 48.0,
|
||||||
height: 48.0,
|
height: 48.0,
|
||||||
),
|
),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onHold,
|
onLongPress: normalizeSecondary(onSecondary),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
if (checkTrackOffline)
|
||||||
FutureBuilder<bool>(
|
FutureBuilder<bool>(
|
||||||
future: downloadManager.checkOffline(track: track),
|
future:
|
||||||
|
downloadManager.checkOffline(track: Track(id: trackId)),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.data == true) {
|
if (snapshot.data == true) {
|
||||||
return const Padding(
|
return const Padding(
|
||||||
|
|
@ -74,7 +152,7 @@ class TrackTile extends StatelessWidget {
|
||||||
}
|
}
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}),
|
}),
|
||||||
if (track.explicit ?? false)
|
if (explicit)
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 2.0),
|
padding: EdgeInsets.symmetric(horizontal: 2.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -85,13 +163,14 @@ class TrackTile extends StatelessWidget {
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 42.0,
|
width: 42.0,
|
||||||
child: Text(
|
child: Text(
|
||||||
track.durationString,
|
durationString,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (trailing != null) trailing!
|
if (trailing != null) trailing!
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,15 +178,19 @@ class TrackTile extends StatelessWidget {
|
||||||
class AlbumTile extends StatelessWidget {
|
class AlbumTile extends StatelessWidget {
|
||||||
final Album? album;
|
final Album? album;
|
||||||
final void Function()? onTap;
|
final void Function()? onTap;
|
||||||
final void Function()? onHold;
|
|
||||||
|
/// Hold or Right click
|
||||||
|
final SecondaryTapCallback? onSecondary;
|
||||||
final Widget? trailing;
|
final Widget? trailing;
|
||||||
|
|
||||||
const AlbumTile(this.album,
|
const AlbumTile(this.album,
|
||||||
{super.key, this.onTap, this.onHold, this.trailing});
|
{super.key, this.onTap, this.onSecondary, this.trailing});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return WrapSecondaryAction(
|
||||||
|
onSecondaryTapDown: onSecondary,
|
||||||
|
child: ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
album!.title!,
|
album!.title!,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
|
|
@ -121,8 +204,9 @@ class AlbumTile extends StatelessWidget {
|
||||||
width: 48,
|
width: 48,
|
||||||
),
|
),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onHold,
|
onLongPress: normalizeSecondary(onSecondary),
|
||||||
trailing: trailing,
|
trailing: trailing,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -130,16 +214,19 @@ class AlbumTile extends StatelessWidget {
|
||||||
class ArtistTile extends StatelessWidget {
|
class ArtistTile extends StatelessWidget {
|
||||||
final Artist? artist;
|
final Artist? artist;
|
||||||
final void Function()? onTap;
|
final void Function()? onTap;
|
||||||
final void Function()? onHold;
|
|
||||||
|
|
||||||
const ArtistTile(this.artist, {super.key, this.onTap, this.onHold});
|
/// Hold or Right click
|
||||||
|
final SecondaryTapCallback? onSecondary;
|
||||||
|
|
||||||
|
const ArtistTile(this.artist, {super.key, this.onTap, this.onSecondary});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
|
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onHold,
|
onLongPress: normalizeSecondary(onSecondary),
|
||||||
|
onSecondaryTapDown: onSecondary,
|
||||||
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
CachedImage(
|
CachedImage(
|
||||||
|
|
@ -163,11 +250,11 @@ class ArtistTile extends StatelessWidget {
|
||||||
class PlaylistTile extends StatelessWidget {
|
class PlaylistTile extends StatelessWidget {
|
||||||
final Playlist? playlist;
|
final Playlist? playlist;
|
||||||
final void Function()? onTap;
|
final void Function()? onTap;
|
||||||
final void Function()? onHold;
|
final SecondaryTapCallback? onSecondary;
|
||||||
final Widget? trailing;
|
final Widget? trailing;
|
||||||
|
|
||||||
const PlaylistTile(this.playlist,
|
const PlaylistTile(this.playlist,
|
||||||
{super.key, this.onHold, this.onTap, this.trailing});
|
{super.key, this.onSecondary, this.onTap, this.trailing});
|
||||||
|
|
||||||
String? get subtitle {
|
String? get subtitle {
|
||||||
if (playlist!.user == null ||
|
if (playlist!.user == null ||
|
||||||
|
|
@ -182,7 +269,9 @@ class PlaylistTile extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return WrapSecondaryAction(
|
||||||
|
onSecondaryTapDown: onSecondary,
|
||||||
|
child: ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
playlist!.title!,
|
playlist!.title!,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
|
|
@ -196,8 +285,9 @@ class PlaylistTile extends StatelessWidget {
|
||||||
width: 48,
|
width: 48,
|
||||||
),
|
),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onHold,
|
onLongPress: normalizeSecondary(onSecondary),
|
||||||
trailing: trailing,
|
trailing: trailing,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.18.12"
|
version: "0.18.12"
|
||||||
|
audio_service_mpris:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: audio_service_mpris
|
||||||
|
sha256: "31be5de2db0c71b217157afce1974ac6d0ad329bd91deb1f19ad094d29340d8e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.0"
|
||||||
audio_service_platform_interface:
|
audio_service_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ dependencies:
|
||||||
isar: ^3.1.0+1
|
isar: ^3.1.0+1
|
||||||
isar_flutter_libs: ^3.1.0+1
|
isar_flutter_libs: ^3.1.0+1
|
||||||
flutter_background_service: ^5.0.1
|
flutter_background_service: ^5.0.1
|
||||||
|
audio_service_mpris: ^0.1.0
|
||||||
#deezcryptor:
|
#deezcryptor:
|
||||||
#path: deezcryptor/
|
#path: deezcryptor/
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue