pre-new design

This commit is contained in:
pato05 2021-11-01 17:41:25 +01:00
parent 1ce60e70de
commit c792daea19
No known key found for this signature in database
GPG key ID: C7C6C9EDFCBB6681
28 changed files with 1772 additions and 1220 deletions

View file

@ -19,7 +19,7 @@ class DeezerAPI {
String? favoritesPlaylistId; String? favoritesPlaylistId;
String? sid; String? sid;
Future? _authorizing; Future<bool>? _authorizing;
//Get headers //Get headers
Map<String, String> get headers => { Map<String, String> get headers => {
@ -76,12 +76,7 @@ class DeezerAPI {
} }
//Wrapper so it can be globally awaited //Wrapper so it can be globally awaited
Future? authorize() async { Future<bool> authorize() async => this._authorizing ??= this.rawAuthorize();
if (_authorizing == null) {
this._authorizing = this.rawAuthorize();
}
return _authorizing;
}
//Login with email //Login with email
static Future<String?> getArlByEmail(String? email, String password) async { static Future<String?> getArlByEmail(String? email, String password) async {

View file

@ -1,10 +1,16 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/cupertino.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:freezer/api/cache.dart'; import 'package:freezer/api/cache.dart';
import 'package:freezer/page_routes/blur_slide.dart';
import 'package:freezer/page_routes/fade.dart';
import 'package:freezer/settings.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:just_audio/just_audio.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:freezer/translations.i18n.dart'; import 'package:freezer/translations.i18n.dart';
@ -760,8 +766,8 @@ class HomePageSection {
} }
class HomePageItem { class HomePageItem {
HomePageItemType? type; final HomePageItemType? type;
dynamic value; final value;
HomePageItem({this.type, this.value}); HomePageItem({this.type, this.value});
@ -831,7 +837,7 @@ class HomePageItem {
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
String type = this.type.toString().split('.').last; String type = describeEnum(this.type!);
return {'type': type, 'value': value.toJson()}; return {'type': type, 'value': value.toJson()};
} }
} }
@ -1077,17 +1083,95 @@ Map<String, dynamic> mediaItemToJson(MediaItem mi) => {
'displayDescription': mi.displayDescription, 'displayDescription': mi.displayDescription,
}; };
MediaItem mediaItemFromJson(Map<String, dynamic> json) => MediaItem( MediaItem mediaItemFromJson(Map<String, dynamic> json) => MediaItem(
id: json['id'], id: json['id'] as String,
title: json['title'], title: json['title'] as String,
artUri: json['artUri'] == null ? null : Uri.parse(json['artUri']), artUri: json['artUri'] == null ? null : Uri.parse(json['artUri']),
playable: json['playable'] as bool, playable: json['playable'] as bool?,
duration: json['duration'] == null duration: json['duration'] == null
? null ? null
: Duration(milliseconds: json['duration'] as int), : Duration(milliseconds: json['duration'] as int),
extras: json['extras'] as Map<String, dynamic>, extras: json['extras'] as Map<String, dynamic>?,
album: json['album'], album: json['album'] as String?,
artist: json['artist'], artist: json['artist'] as String?,
displayTitle: json['displayTitle'], displayTitle: json['displayTitle'] as String?,
displaySubtitle: json['displaySubtitle'], displaySubtitle: json['displaySubtitle'] as String?,
displayDescription: json['displayDescription'], displayDescription: json['displayDescription'] as String?,
); );
/// Will generate a new darkened color by [percent], and leaves the opacity untouched
///
/// [percent] is a double which value is from 0 to 1, the closer to one, the darker the color is
Color darken(Color color, {double percent = 0.25}) =>
Color.lerp(color, Colors.black, percent)!;
extension LastItem<T> on List<T> {
T get lastItem => this[length - 1];
}
extension ToLoopMode on AudioServiceRepeatMode {
LoopMode toLoopMode() {
switch (this) {
case AudioServiceRepeatMode.none:
return LoopMode.off;
case AudioServiceRepeatMode.one:
return LoopMode.one;
case AudioServiceRepeatMode.group:
case AudioServiceRepeatMode.all:
return LoopMode.all;
}
}
}
// extension ToAudioServiceRepeatMode on LoopMode {
// AudioServiceRepeatMode toAudioServiceRepeatMode() {
// switch (this) {
// case LoopMode.off:
// return AudioServiceRepeatMode.none;
// case LoopMode.one:
// return AudioServiceRepeatMode.one;
// case LoopMode.all:
// return AudioServiceRepeatMode.all;
// }
// }
// }
extension PushRoute on NavigatorState {
Future<T?> pushRoute<T extends Object?>({required WidgetBuilder builder}) {
final PageRoute<T> route;
switch (settings.navigatorRouteType) {
case NavigatorRouteType.blur_slide:
route = BlurSlidePageRoute<T>(builder: builder);
break;
case NavigatorRouteType.material:
route = MaterialPageRoute<T>(builder: builder);
break;
case NavigatorRouteType.cupertino:
route = CupertinoPageRoute<T>(builder: builder);
break;
case NavigatorRouteType.fade:
route = FadePageRoute<T>(builder: builder);
break;
case NavigatorRouteType.fade_blur:
route = FadePageRoute<T>(builder: builder, blur: true);
break;
}
return push(route);
}
}
enum NavigatorRouteType {
/// Slide from the bottom, with a backdrop filter on the previous screen
blur_slide,
/// Fade
fade,
/// Fade with blur
fade_blur,
/// Standard material route look
material,
/// Standard cupertino route look
cupertino,
}

View file

@ -25,7 +25,7 @@ class DownloadManager {
static EventChannel eventChannel = static EventChannel eventChannel =
const EventChannel('f.f.freezer/downloads'); const EventChannel('f.f.freezer/downloads');
bool? running = false; bool running = false;
int? queueSize = 0; int? queueSize = 0;
StreamController serviceEvents = StreamController.broadcast(); StreamController serviceEvents = StreamController.broadcast();
@ -92,8 +92,7 @@ class DownloadManager {
//Get all downloads from db //Get all downloads from db
Future<List<Download>> getDownloads() async { Future<List<Download>> getDownloads() async {
List raw = await (platform.invokeMethod('getDownloads') List raw = await platform.invokeMethod('getDownloads');
as FutureOr<List<dynamic>>);
return raw.map((d) => Download.fromJson(d)).toList(); return raw.map((d) => Download.fromJson(d)).toList();
} }
@ -535,14 +534,14 @@ class DownloadManager {
//Download path //Download path
path = settings.downloadPath; path = settings.downloadPath;
if (settings.playlistFolder! && playlistName != null) if (settings.playlistFolder && playlistName != null)
path = p.join(path!, sanitize(playlistName)); path = p.join(path!, sanitize(playlistName));
if (settings.artistFolder!) path = p.join(path!, '%albumArtist%'); if (settings.artistFolder) path = p.join(path!, '%albumArtist%');
//Album folder / with disk number //Album folder / with disk number
if (settings.albumFolder!) { if (settings.albumFolder) {
if (settings.albumDiscFolder!) { if (settings.albumDiscFolder) {
path = p.join(path!, path = p.join(path!,
'%album%' + ' - Disk ' + (track!.diskNumber ?? 1).toString()); '%album%' + ' - Disk ' + (track!.diskNumber ?? 1).toString());
} else { } else {

View file

@ -28,21 +28,51 @@ class PlayerHelper {
late StreamSubscription _mediaItemSubscription; late StreamSubscription _mediaItemSubscription;
late StreamSubscription _playbackStateStreamSubscription; late StreamSubscription _playbackStateStreamSubscription;
QueueSource? queueSource; QueueSource? queueSource;
LoopMode repeatType = LoopMode.off; AudioServiceRepeatMode repeatType = AudioServiceRepeatMode.none;
Timer? _timer; Timer? _timer;
int? audioSession; int? audioSession;
int? _prevAudioSession; int? _prevAudioSession;
bool equalizerOpen = false; bool equalizerOpen = false;
bool _shuffleEnabled = false;
int _queueIndex = 0;
//Visualizer //Visualizer
StreamController _visualizerController = StreamController.broadcast(); StreamController _visualizerController = StreamController.broadcast();
Stream get visualizerStream => _visualizerController.stream; Stream get visualizerStream => _visualizerController.stream;
//Find queue index by id /// Find queue index by id
int get queueIndex => audioHandler.queue.value ///
.indexWhere((mi) => mi.id == audioHandler.mediaItem.value?.id); /// The function gets more expensive the longer the queue is and the further the element is from the beginning.
int getQueueIndexFromId() => audioHandler.mediaItem.value == null
? -1
: audioHandler.queue.value
.indexWhere((mi) => mi.id == audioHandler.mediaItem.value!.id);
Future start() async { int getQueueIndex() =>
audioHandler.playbackState.value.queueIndex ?? getQueueIndexFromId();
int get queueIndex => _queueIndex;
Future<void> initAudioHandler() async {
// initialize our audiohandler instance
audioHandler = await AudioService.init(
builder: () => AudioPlayerTask(),
config: AudioServiceConfig(
notificationColor: settings.primaryColor,
androidStopForegroundOnPause: false,
androidNotificationOngoing: false,
androidNotificationClickStartsActivity: true,
androidNotificationChannelDescription: 'Freezer',
androidNotificationChannelName: 'Freezer',
androidNotificationIcon: 'drawable/ic_logo',
preloadArtwork: false,
),
);
}
Future<void> start() async {
audioHandler.customAction(
'start', {'ignoreInterruptions': settings.ignoreInterruptions});
//Subscribe to custom events //Subscribe to custom events
_customEventSubscription = audioHandler.customEvent.listen((event) async { _customEventSubscription = audioHandler.customEvent.listen((event) async {
if (!(event is Map)) return; if (!(event is Map)) return;
@ -50,13 +80,14 @@ class PlayerHelper {
case 'onLoad': case 'onLoad':
//After audio_service is loaded, load queue, set quality //After audio_service is loaded, load queue, set quality
await settings.updateAudioServiceQuality(); await settings.updateAudioServiceQuality();
await audioHandler.customAction('load', {}); await audioHandler.customAction('load');
await authorizeLastFM(); await authorizeLastFM();
break; break;
case 'onRestore': case 'onRestore':
//Load queueSource from isolate //Load queueSource from isolate
this.queueSource = QueueSource.fromJson(event['queueSource']); this.queueSource = QueueSource.fromJson(event['queueSource']);
repeatType = LoopMode.values[event['loopMode']]; repeatType = AudioServiceRepeatMode.values[event['loopMode']];
_queueIndex = getQueueIndex();
break; break;
case 'queueEnd': case 'queueEnd':
//If last song is played, load more queue //If last song is played, load more queue
@ -74,7 +105,7 @@ class PlayerHelper {
await androidAuto.playItem(event['id']); await androidAuto.playItem(event['id']);
break; break;
case 'audioSession': case 'audioSession':
if (!settings.enableEqualizer!) break; if (!settings.enableEqualizer) break;
//Save //Save
_prevAudioSession = audioSession; _prevAudioSession = audioSession;
audioSession = event['id']; audioSession = event['id'];
@ -98,16 +129,21 @@ class PlayerHelper {
break; break;
} }
}); });
_mediaItemSubscription = audioHandler.mediaItem.listen((event) { _mediaItemSubscription = audioHandler.mediaItem.listen((mediaItem) async {
if (event == null) return; if (mediaItem == null) return;
//Load more flow if index-1 song final queue = audioHandler.queue.value;
if (queueIndex == audioHandler.queue.value.length - 1) onQueueEnd(); final nextIndex = (_queueIndex + 1) % queue.length;
print('animating $nextIndex');
_queueIndex = getQueueIndex();
//Load more flow if last song (not using .last since it iterates through previous elements first)
if (mediaItem.id == queue[queue.length - 1].id) await onQueueEnd();
//Save queue //Save queue
audioHandler.customAction('saveQueue', {}); await audioHandler.customAction('saveQueue', {});
//Add to history //Add to history
if (cache.history.length > 0 && cache.history.last.id == event.id) return; if (cache.history.length > 0 && cache.history.last.id == mediaItem.id)
cache.history.add(Track.fromMediaItem(event)); return;
cache.history.add(Track.fromMediaItem(mediaItem));
cache.save(); cache.save();
}); });
@ -141,26 +177,25 @@ class PlayerHelper {
}); });
} }
Future toggleShuffle() async { Future<bool> toggleShuffle() async {
await audioHandler.customAction('shuffle'); await audioHandler.setShuffleMode((_shuffleEnabled = !_shuffleEnabled)
? AudioServiceShuffleMode.all
: AudioServiceShuffleMode.none);
return _shuffleEnabled;
} }
bool get shuffleEnabled => _shuffleEnabled;
//Repeat toggle //Repeat toggle
Future changeRepeat() async { Future changeRepeat() async {
//Change to next repeat type //Change to next repeat type
switch (repeatType) { repeatType = repeatType == AudioServiceRepeatMode.all
case LoopMode.one: ? AudioServiceRepeatMode.none
repeatType = LoopMode.off; : repeatType == AudioServiceRepeatMode.none
break; ? AudioServiceRepeatMode.one
case LoopMode.all: : AudioServiceRepeatMode.all;
repeatType = LoopMode.one;
break;
default:
repeatType = LoopMode.all;
break;
}
//Set repeat type //Set repeat type
await audioHandler.customAction('repeatType', {'type': repeatType.index}); await audioHandler.setRepeatMode(repeatType);
} }
//Executed before exit //Executed before exit
@ -183,11 +218,11 @@ class PlayerHelper {
//Called when queue ends to load more tracks //Called when queue ends to load more tracks
Future onQueueEnd() async { Future onQueueEnd() async {
//Flow
if (queueSource == null) return; if (queueSource == null) return;
List<Track>? tracks = []; List<Track>? tracks;
switch (queueSource!.source) { switch (queueSource!.source) {
//Flow
case 'flow': case 'flow':
tracks = await deezerAPI.flow(); tracks = await deezerAPI.flow();
break; break;
@ -209,13 +244,13 @@ class PlayerHelper {
tracks?.removeWhere((track) => queueIds.contains(track.id)); tracks?.removeWhere((track) => queueIds.contains(track.id));
break; break;
default: default:
return;
// print(queueSource.toJson()); // print(queueSource.toJson());
break;
} }
if (tracks == null) { if (tracks == null) {
// try again i guess? throw Exception(
return await onQueueEnd(); 'Failed to fetch more queue songs (queueSource: ${queueSource!.toJson()})');
} }
List<MediaItem> mi = tracks.map<MediaItem>((t) => t.toMediaItem()).toList(); List<MediaItem> mi = tracks.map<MediaItem>((t) => t.toMediaItem()).toList();
@ -231,8 +266,7 @@ class PlayerHelper {
//Play mix by track //Play mix by track
Future playMix(String trackId, String trackTitle) async { Future playMix(String trackId, String trackTitle) async {
List<Track> tracks = List<Track> tracks = (await deezerAPI.playMix(trackId))!;
await (deezerAPI.playMix(trackId) as FutureOr<List<Track>>);
playFromTrackList( playFromTrackList(
tracks, tracks,
tracks[0].id, tracks[0].id,
@ -337,9 +371,7 @@ class AudioPlayerTask extends BaseAudioHandler {
late AudioPlayer _player; late AudioPlayer _player;
//Queue //Queue
List<MediaItem>? _queue = <MediaItem>[];
List<MediaItem>? _originalQueue; List<MediaItem>? _originalQueue;
bool _shuffle = false;
int _queueIndex = 0; int _queueIndex = 0;
bool _isInitialized = false; bool _isInitialized = false;
late ConcatenatingAudioSource _audioSource; late ConcatenatingAudioSource _audioSource;
@ -356,7 +388,7 @@ class AudioPlayerTask extends BaseAudioHandler {
int? wifiQuality; int? wifiQuality;
QueueSource? queueSource; QueueSource? queueSource;
Duration? _lastPosition; Duration? _lastPosition;
LoopMode _loopMode = LoopMode.off; AudioServiceRepeatMode _repeatMode = AudioServiceRepeatMode.none;
Completer<List<MediaItem>>? _androidAutoCallback; Completer<List<MediaItem>>? _androidAutoCallback;
Scrobblenaut? _scrobblenaut; Scrobblenaut? _scrobblenaut;
@ -364,11 +396,7 @@ class AudioPlayerTask extends BaseAudioHandler {
// Last logged track id // Last logged track id
String? _loggedTrackId; String? _loggedTrackId;
MediaItem get currentMediaItem => _queue![_queueIndex]; MediaItem get currentMediaItem => queue.value[_queueIndex];
AudioPlayerTask() {
onStart({}); // workaround i guess?
}
Future onStart(Map<String, dynamic>? params) async { Future onStart(Map<String, dynamic>? params) async {
final session = await AudioSession.instance; final session = await AudioSession.instance;
@ -391,10 +419,10 @@ class AudioPlayerTask extends BaseAudioHandler {
//Update state on all clients on change //Update state on all clients on change
_eventSub = _player.playbackEventStream.listen((event) { _eventSub = _player.playbackEventStream.listen((event) {
//Quality string //Quality string
if (_queueIndex != -1 && _queueIndex < _queue!.length) { if (_queueIndex != -1 && _queueIndex < queue.value.length) {
Map extras = currentMediaItem.extras!; Map extras = currentMediaItem.extras!;
extras['qualityString'] = ''; extras['qualityString'] = '';
_queue![_queueIndex] = queue.value[_queueIndex] =
currentMediaItem.copyWith(extras: extras as Map<String, dynamic>?); currentMediaItem.copyWith(extras: extras as Map<String, dynamic>?);
} }
//Update //Update
@ -404,7 +432,7 @@ class AudioPlayerTask extends BaseAudioHandler {
switch (state) { switch (state) {
case ProcessingState.completed: case ProcessingState.completed:
//Player ended, get more songs //Player ended, get more songs
if (_queueIndex == _queue!.length - 1) if (_queueIndex == queue.value.length - 1)
customEvent.add({ customEvent.add({
'action': 'queueEnd', 'action': 'queueEnd',
'queueSource': (queueSource ?? QueueSource()).toJson() 'queueSource': (queueSource ?? QueueSource()).toJson()
@ -421,7 +449,7 @@ class AudioPlayerTask extends BaseAudioHandler {
}); });
//Load queue //Load queue
queue.add(_queue!); // queue.add(_queue);
customEvent.add({'action': 'onLoad'}); customEvent.add({'action': 'onLoad'});
} }
@ -477,25 +505,25 @@ class AudioPlayerTask extends BaseAudioHandler {
//Remove item from queue //Remove item from queue
@override @override
Future<void> removeQueueItem(MediaItem mediaItem) async { Future<void> removeQueueItem(MediaItem mediaItem) async {
int index = _queue!.indexWhere((m) => m.id == mediaItem.id); int index = queue.value.indexWhere((m) => m.id == mediaItem.id);
removeQueueItemAt(index); removeQueueItemAt(index);
} }
@override @override
Future<void> removeQueueItemAt(int index) async { Future<void> removeQueueItemAt(int index) async {
_queue!.removeAt(index);
if (index <= _queueIndex) { if (index <= _queueIndex) {
_queueIndex--; _queueIndex--;
} }
await _audioSource.removeAt(index); await _audioSource.removeAt(index);
queue.add(_queue!); queue.add(queue.value..removeAt(index));
} }
@override @override
Future<void> skipToNext() async { Future<void> skipToNext() async {
_lastPosition = null; _lastPosition = null;
if (_queueIndex == _queue!.length - 1) return; if (_queueIndex == queue.value.length - 1) return;
//Update buffering state //Update buffering state
_queueIndex++; _queueIndex++;
await _player.seekToNext(); await _player.seekToNext();
@ -550,14 +578,15 @@ class AudioPlayerTask extends BaseAudioHandler {
void _broadcastState() { void _broadcastState() {
playbackState.add(PlaybackState( playbackState.add(PlaybackState(
controls: [ controls: [
if (_queueIndex != 0) MediaControl.skipToPrevious, /*if (_queueIndex != 0)*/ MediaControl.skipToPrevious,
_player.playing ? MediaControl.pause : MediaControl.play, _player.playing ? MediaControl.pause : MediaControl.play,
if (_queueIndex != _queue!.length - 1) MediaControl.skipToNext, /*if (_queueIndex != _queue!.length - 1)*/ MediaControl.skipToNext,
//Stop //Stop
MediaControl( // MediaControl(
androidIcon: 'drawable/ic_action_stop', // androidIcon: 'drawable/ic_action_stop',
label: 'stop', // label: 'stop',
action: MediaAction.stop), // action: MediaAction.stop),
// i mean, the user can just swipe the notification away to stop
], ],
systemActions: const { systemActions: const {
MediaAction.seek, MediaAction.seek,
@ -569,26 +598,21 @@ class AudioPlayerTask extends BaseAudioHandler {
playing: _player.playing, playing: _player.playing,
updatePosition: _player.position, updatePosition: _player.position,
bufferedPosition: _player.bufferedPosition, bufferedPosition: _player.bufferedPosition,
speed: _player.speed)); speed: _player.speed,
queueIndex: _queueIndex,
));
} }
//just_audio state -> audio_service state. If skipping, use _skipState //just_audio state -> audio_service state. If skipping, use _skipState
AudioProcessingState _getProcessingState() { AudioProcessingState _getProcessingState() {
//SRC: audio_service example return const <ProcessingState, AudioProcessingState>{
switch (_player.processingState) { ProcessingState.idle: AudioProcessingState.idle,
case ProcessingState.idle: ProcessingState.loading: AudioProcessingState.loading,
return AudioProcessingState.idle; ProcessingState.buffering: AudioProcessingState.buffering,
case ProcessingState.loading: ProcessingState.ready: AudioProcessingState.ready,
return AudioProcessingState.loading; ProcessingState.completed: AudioProcessingState.completed
case ProcessingState.buffering: }[_player.processingState] ??
return AudioProcessingState.buffering; AudioProcessingState.idle;
case ProcessingState.ready:
return AudioProcessingState.ready;
case ProcessingState.completed:
return AudioProcessingState.completed;
default:
throw Exception("Invalid state: ${_player.processingState}");
}
} }
//Replace current queue //Replace current queue
@ -596,19 +620,16 @@ class AudioPlayerTask extends BaseAudioHandler {
Future updateQueue(List<MediaItem> q) async { Future updateQueue(List<MediaItem> q) async {
_lastPosition = null; _lastPosition = null;
//just_audio //just_audio
_shuffle = false;
_originalQueue = null; _originalQueue = null;
_player.stop(); _player.stop();
if (_isInitialized) _audioSource.clear(); if (_isInitialized) _audioSource.clear();
//Filter duplicate IDs //Filter duplicate IDs
List<MediaItem> newQueue = q.toSet().toList(); List<MediaItem> newQueue = q.toSet().toList();
_queue = newQueue;
//Load
await _loadQueue();
// broadcast to ui // broadcast to ui
queue.add(newQueue); queue.add(newQueue);
//Load
await _loadQueue();
//await _player.seek(Duration.zero, index: 0); //await _player.seek(Duration.zero, index: 0);
} }
@ -618,12 +639,12 @@ class AudioPlayerTask extends BaseAudioHandler {
int? qi = _queueIndex; int? qi = _queueIndex;
List<AudioSource> sources = []; List<AudioSource> sources = [];
for (int i = 0; i < _queue!.length; i++) { for (int i = 0; i < queue.value.length; i++) {
AudioSource s = await _mediaItemToAudioSource(_queue![i]); AudioSource s = await _mediaItemToAudioSource(queue.value[i]);
sources.add(s); sources.add(s);
} }
_audioSource = ConcatenatingAudioSource(children: sources); _audioSource = ConcatenatingAudioSource(children: sources);
//Load in just_audio //Load in just_audio
try { try {
await _player.setAudioSource(_audioSource, await _player.setAudioSource(_audioSource,
@ -678,6 +699,9 @@ class AudioPlayerTask extends BaseAudioHandler {
@override @override
Future customAction(String name, [Map<String, dynamic>? args]) async { Future customAction(String name, [Map<String, dynamic>? args]) async {
switch (name) { switch (name) {
case 'start':
onStart(args);
break;
case 'updateQuality': case 'updateQuality':
//Pass wifi & mobile quality by custom action //Pass wifi & mobile quality by custom action
//Isolate can't access globals //Isolate can't access globals
@ -689,10 +713,10 @@ class AudioPlayerTask extends BaseAudioHandler {
this.queueSource = QueueSource.fromJson(args!); this.queueSource = QueueSource.fromJson(args!);
break; break;
//Looping //Looping
case 'repeatType': // case 'repeatType':
_loopMode = LoopMode.values[args!['type']]; // _loopMode = LoopMode.values[args!['type']];
_player.setLoopMode(_loopMode); // _player.setLoopMode(_loopMode);
break; // break;
//Save queue //Save queue
case 'saveQueue': case 'saveQueue':
await this._saveQueue(); await this._saveQueue();
@ -701,29 +725,6 @@ class AudioPlayerTask extends BaseAudioHandler {
case 'load': case 'load':
await this._loadQueueFile(); await this._loadQueueFile();
break; break;
case 'shuffle':
/// TODO: maybe use [_player.setShuffleModeEnabled] instead?
// why is this even a thing?
// String originalId = mediaItem.id;
if (!_shuffle) {
_shuffle = true;
_originalQueue = List.from(_queue!);
_queue!.shuffle();
} else {
_shuffle = false;
_queue = _originalQueue;
_originalQueue = null;
}
//Broken
// _queueIndex = _queue.indexWhere((mi) => mi.id == originalId);
_queueIndex = 0;
queue.add(_queue!);
// AudioServiceBackground.setMediaItem(mediaItem);
await _player.stop();
await _loadQueue();
await _player.play();
break;
//Android audio callback //Android audio callback
case 'screenAndroidAuto': case 'screenAndroidAuto':
@ -738,14 +739,11 @@ class AudioPlayerTask extends BaseAudioHandler {
final oldIndex = args!['oldIndex']! as int; final oldIndex = args!['oldIndex']! as int;
final newIndex = args['newIndex']! as int; final newIndex = args['newIndex']! as int;
await _audioSource.move(oldIndex, newIndex); await _audioSource.move(oldIndex, newIndex);
//Switch in queue queue.add(queue.value..reorder(oldIndex, newIndex));
_queue!.reorder(oldIndex, newIndex);
//Update UI
queue.add(_queue!);
_broadcastState(); _broadcastState();
break; break;
//Set index without affecting playback for loading //Set index without affecting playback for loading
case 'setIndex': // i really don't get what this is for case 'setIndex': // editor's note: i really don't get what this is for
this._queueIndex = args!['index']; this._queueIndex = args!['index'];
break; break;
//Start visualizer //Start visualizer
@ -824,7 +822,7 @@ class AudioPlayerTask extends BaseAudioHandler {
//Export queue to JSON //Export queue to JSON
Future<void> _saveQueue() async { Future<void> _saveQueue() async {
if (_queueIndex == 0 && _queue!.length == 0) return; if (_queueIndex == 0 && queue.value.length == 0) return;
String path = await _getQueuePath(); String path = await _getQueuePath();
File f = File(path); File f = File(path);
@ -834,10 +832,10 @@ class AudioPlayerTask extends BaseAudioHandler {
} }
Map data = { Map data = {
'index': _queueIndex, 'index': _queueIndex,
'queue': _queue!.map<Map<String, dynamic>>(mediaItemToJson).toList(), 'queue': queue.value.map<Map<String, dynamic>>(mediaItemToJson).toList(),
'position': _player.position.inMilliseconds, 'position': _player.position.inMilliseconds,
'queueSource': (queueSource ?? QueueSource()).toJson(), 'queueSource': (queueSource ?? QueueSource()).toJson(),
'loopMode': LoopMode.values.indexOf(_loopMode) 'loopMode': _repeatMode.index,
}; };
await f.writeAsString(jsonEncode(data)); await f.writeAsString(jsonEncode(data));
} }
@ -847,25 +845,29 @@ class AudioPlayerTask extends BaseAudioHandler {
File f = File(await _getQueuePath()); File f = File(await _getQueuePath());
if (await f.exists()) { if (await f.exists()) {
Map<String, dynamic> json = jsonDecode(await f.readAsString()); Map<String, dynamic> json = jsonDecode(await f.readAsString());
this._queue = List<MediaItem>? _queue = (json['queue'] as List?)
(json['queue'] ?? []).map<MediaItem>(mediaItemFromJson).toList(); ?.cast<Map>()
this._queueIndex = json['index'] ?? 0; .map<MediaItem>(
this._lastPosition = Duration(milliseconds: json['position'] ?? 0); (json) => mediaItemFromJson(json.cast<String, dynamic>()))
this.queueSource = QueueSource.fromJson(json['queueSource'] ?? {}); .toList();
this._loopMode = LoopMode.values[(json['loopMode'] ?? 0)]; _queueIndex = json['index'] ?? 0;
_lastPosition = Duration(milliseconds: json['position'] ?? 0);
queueSource = QueueSource.fromJson(json['queueSource'] ?? {});
_repeatMode = AudioServiceRepeatMode.values[(json['loopMode'] ?? 0)];
//Restore queue //Restore queue
if (_queue != null) { if (_queue != null) {
queue.add(_queue!); queue.add(_queue);
await _loadQueue(); await _loadQueue();
mediaItem.add(currentMediaItem); mediaItem.add(currentMediaItem);
} }
}
//Send restored queue source to ui //Send restored queue source to ui
customEvent.add({ customEvent.add({
'action': 'onRestore', 'action': 'onRestore',
'queueSource': (queueSource ?? QueueSource()).toJson(), 'queueSource': (queueSource ?? QueueSource()).toJson(),
'loopMode': LoopMode.values.indexOf(_loopMode) 'loopMode': _repeatMode.index
}); });
}
return true; return true;
} }
@ -874,8 +876,7 @@ class AudioPlayerTask extends BaseAudioHandler {
//-1 == play next //-1 == play next
if (index == -1) index = _queueIndex + 1; if (index == -1) index = _queueIndex + 1;
_queue!.insert(index, mi); queue.add(queue.value..insert(index, mi));
queue.add(_queue!);
AudioSource? _newSource = await _mediaItemToAudioSource(mi); AudioSource? _newSource = await _mediaItemToAudioSource(mi);
await _audioSource.insert(index, _newSource); await _audioSource.insert(index, _newSource);
@ -886,10 +887,9 @@ class AudioPlayerTask extends BaseAudioHandler {
@override @override
Future<void> addQueueItem(MediaItem mediaItem, Future<void> addQueueItem(MediaItem mediaItem,
{bool shouldSaveQueue = true}) async { {bool shouldSaveQueue = true}) async {
if (_queue!.indexWhere((m) => m.id == mediaItem.id) != -1) return; if (queue.value.indexWhere((m) => m.id == mediaItem.id) != -1) return;
_queue!.add(mediaItem); queue.add(queue.value..add(mediaItem));
queue.add(_queue!);
AudioSource _newSource = await _mediaItemToAudioSource(mediaItem); AudioSource _newSource = await _mediaItemToAudioSource(mediaItem);
await _audioSource.add(_newSource); await _audioSource.add(_newSource);
if (shouldSaveQueue) _saveQueue(); if (shouldSaveQueue) _saveQueue();
@ -917,25 +917,40 @@ class AudioPlayerTask extends BaseAudioHandler {
//Does the same thing //Does the same thing
await this await this
.skipToQueueItem(_queue!.indexWhere((item) => item.id == mediaId)); .skipToQueueItem(queue.value.indexWhere((item) => item.id == mediaId));
} }
@override @override
Future<MediaItem?> getMediaItem(String mediaId) async => Future<MediaItem?> getMediaItem(String mediaId) async =>
_queue!.firstWhereOrNull((mediaItem) => mediaItem.id == mediaId); queue.value.firstWhereOrNull((mediaItem) => mediaItem.id == mediaId);
@override @override
Future<void> playMediaItem(MediaItem mediaItem) => Future<void> playMediaItem(MediaItem mediaItem) =>
playFromMediaId(mediaItem.id); playFromMediaId(mediaItem.id);
// TODO: implement shuffle and repeat
@override @override
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) => Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) =>
super.setRepeatMode(repeatMode); _player.setLoopMode(repeatMode.toLoopMode());
@override @override
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) => Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) async {
super.setShuffleMode(shuffleMode); switch (shuffleMode) {
case AudioServiceShuffleMode.none:
queue.add(_originalQueue!);
_originalQueue = null;
break;
case AudioServiceShuffleMode.group:
case AudioServiceShuffleMode.all:
_originalQueue = List.from(queue.value, growable: false);
queue.add(queue.value..shuffle());
break;
}
_queueIndex = 0;
await _player.stop();
await _loadQueue();
await _player.play();
}
} }
//Seeker from audio_service example (why reinvent the wheel?) //Seeker from audio_service example (why reinvent the wheel?)

View file

@ -41,21 +41,7 @@ void main() async {
//Do on BG //Do on BG
playerHelper.authorizeLastFM(); playerHelper.authorizeLastFM();
await playerHelper.initAudioHandler();
// initialize our audiohandler instance
audioHandler = await AudioService.init<AudioPlayerTask>(
builder: () => AudioPlayerTask(),
config: AudioServiceConfig(
notificationColor: settings.primaryColor,
androidStopForegroundOnPause: false,
androidNotificationOngoing: false,
androidNotificationClickStartsActivity: true,
androidNotificationChannelDescription: 'Freezer',
androidNotificationChannelName: 'Freezer',
androidNotificationIcon: 'drawable/ic_logo',
preloadArtwork: false,
),
);
runApp(FreezerApp()); runApp(FreezerApp());
} }
@ -66,15 +52,33 @@ class FreezerApp extends StatefulWidget {
} }
class _FreezerAppState extends State<FreezerApp> { class _FreezerAppState extends State<FreezerApp> {
late StreamSubscription _playbackStateSub;
@override @override
void initState() { void initState() {
_initStateAsync();
//Make update theme global //Make update theme global
updateTheme = _updateTheme; updateTheme = _updateTheme;
super.initState(); super.initState();
} }
Future<void> _initStateAsync() async {
_playbackStateChanged(audioHandler.playbackState.value);
_playbackStateSub =
audioHandler.playbackState.listen(_playbackStateChanged);
}
Future<void> _playbackStateChanged(PlaybackState playbackState) async {
if (playbackState.processingState == AudioProcessingState.idle ||
playbackState.processingState == AudioProcessingState.error) {
// reconnect maybe?
return;
}
}
@override @override
void dispose() { void dispose() {
_playbackStateSub.cancel();
super.dispose(); super.dispose();
} }
@ -143,7 +147,7 @@ class _LoginMainWrapperState extends State<LoginMainWrapper> {
//Load token on background //Load token on background
deezerAPI.arl = settings.arl; deezerAPI.arl = settings.arl;
settings.offlineMode = true; settings.offlineMode = true;
deezerAPI.authorize()!.then((b) async { deezerAPI.authorize().then((b) async {
if (b) setState(() => settings.offlineMode = false); if (b) setState(() => settings.offlineMode = false);
}); });
} }

View file

@ -0,0 +1,20 @@
import 'package:flutter/widgets.dart';
abstract class BasicPageRoute<T> extends PageRoute<T> {
final Duration transitionDuration;
final bool maintainState;
BasicPageRoute({
this.transitionDuration = const Duration(milliseconds: 300),
this.maintainState = true,
});
@override
bool get barrierDismissible => false;
@override
Color? get barrierColor => null;
@override
String? get barrierLabel => null;
}

View file

@ -0,0 +1,43 @@
import 'dart:ui';
import 'package:flutter/widgets.dart';
import 'package:freezer/page_routes/basic_page_route.dart';
import 'package:freezer/ui/animated_blur.dart';
class BlurSlidePageRoute<T> extends BasicPageRoute<T> {
final WidgetBuilder builder;
final Curve animationCurve;
final _animationTween = Tween(
begin: const Offset(0.0, 1.0),
end: Offset.zero,
);
BlurSlidePageRoute({
required this.builder,
this.animationCurve = Curves.linearToEaseOut,
transitionDuration = const Duration(milliseconds: 300),
maintainState = true,
}) : super(
transitionDuration: transitionDuration,
maintainState: maintainState);
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) =>
builder(context);
@override
Widget buildTransitions(BuildContext context, Animation<double> _animation,
Animation<double> secondaryAnimation, Widget child) {
final animation =
CurvedAnimation(parent: _animation, curve: animationCurve);
return Stack(children: [
Positioned.fill(
child: AnimatedBlur(
animation: animation, multiplier: 10.0, child: const SizedBox()),
),
SlideTransition(
position: _animationTween.animate(animation), child: child),
]);
}
}

41
lib/page_routes/fade.dart Normal file
View file

@ -0,0 +1,41 @@
import 'package:flutter/cupertino.dart';
import 'package:freezer/page_routes/basic_page_route.dart';
import 'package:freezer/ui/animated_blur.dart';
class FadePageRoute<T> extends BasicPageRoute<T> {
final WidgetBuilder builder;
final bool blur;
FadePageRoute({
required this.builder,
this.blur = false,
transitionDuration = const Duration(milliseconds: 300),
maintainState = true,
}) : super(
transitionDuration: transitionDuration,
maintainState: maintainState);
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) =>
builder(context);
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
final baseTransition = FadeTransition(
opacity: animation,
child: child,
);
if (blur) {
return Stack(children: [
Positioned.fill(
child: AnimatedBlur(
animation: animation,
multiplier: 10.0,
child: const SizedBox())),
baseTransition,
]);
}
return baseTransition;
}
}

View file

@ -1,4 +1,6 @@
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/download.dart'; import 'package:freezer/api/download.dart';
import 'package:freezer/api/player.dart'; import 'package:freezer/api/player.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
@ -23,9 +25,9 @@ class Settings {
//Main //Main
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool? ignoreInterruptions; late bool ignoreInterruptions;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool? enableEqualizer; late bool enableEqualizer;
//Account //Account
String? arl; String? arl;
@ -34,45 +36,45 @@ class Settings {
//Quality //Quality
@JsonKey(defaultValue: AudioQuality.MP3_320) @JsonKey(defaultValue: AudioQuality.MP3_320)
AudioQuality? wifiQuality; late AudioQuality wifiQuality;
@JsonKey(defaultValue: AudioQuality.MP3_128) @JsonKey(defaultValue: AudioQuality.MP3_128)
AudioQuality? mobileQuality; late AudioQuality mobileQuality;
@JsonKey(defaultValue: AudioQuality.FLAC) @JsonKey(defaultValue: AudioQuality.FLAC)
AudioQuality? offlineQuality; late AudioQuality offlineQuality;
@JsonKey(defaultValue: AudioQuality.FLAC) @JsonKey(defaultValue: AudioQuality.FLAC)
AudioQuality? downloadQuality; late AudioQuality downloadQuality;
//Download options //Download options
String? downloadPath; String? downloadPath;
@JsonKey(defaultValue: "%artist% - %title%") @JsonKey(defaultValue: "%artist% - %title%")
String? downloadFilename; late String downloadFilename;
@JsonKey(defaultValue: true) @JsonKey(defaultValue: true)
bool? albumFolder; late bool albumFolder;
@JsonKey(defaultValue: true) @JsonKey(defaultValue: true)
bool? artistFolder; late bool artistFolder;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool? albumDiscFolder; late bool albumDiscFolder;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool? overwriteDownload; late bool overwriteDownload;
@JsonKey(defaultValue: 2) @JsonKey(defaultValue: 2)
int? downloadThreads; late int downloadThreads;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool? playlistFolder; late bool playlistFolder;
@JsonKey(defaultValue: true) @JsonKey(defaultValue: true)
bool? downloadLyrics; late bool downloadLyrics;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool? trackCover; late bool trackCover;
@JsonKey(defaultValue: true) @JsonKey(defaultValue: true)
bool? albumCover; late bool albumCover;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool? nomediaFiles; late bool nomediaFiles;
@JsonKey(defaultValue: ", ") @JsonKey(defaultValue: ", ")
String? artistSeparator; late String artistSeparator;
@JsonKey(defaultValue: "%artist% - %title%") @JsonKey(defaultValue: "%artist% - %title%")
String? singletonFilename; late String singletonFilename;
@JsonKey(defaultValue: 1400) @JsonKey(defaultValue: 1400)
int? albumArtResolution; late int albumArtResolution;
@JsonKey(defaultValue: [ @JsonKey(defaultValue: [
"title", "title",
"album", "album",
@ -91,23 +93,29 @@ class Settings {
"contributors", "contributors",
"art" "art"
]) ])
List<String>? tags; late List<String> tags;
//Appearance //Appearance
@JsonKey(defaultValue: Themes.Dark) @JsonKey(defaultValue: Themes.Dark)
Themes? theme; late Themes theme;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool? useSystemTheme; late bool useSystemTheme;
@JsonKey(defaultValue: true) @JsonKey(defaultValue: true)
bool? colorGradientBackground; late bool colorGradientBackground;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool? blurPlayerBackground; late bool blurPlayerBackground;
@JsonKey(defaultValue: "Deezer") @JsonKey(defaultValue: "Deezer")
String? font; late String font;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool? lyricsVisualizer; late bool lyricsVisualizer;
@JsonKey(defaultValue: null) @JsonKey(defaultValue: null)
int? displayMode; int? displayMode;
@JsonKey(defaultValue: true)
late bool enableFilledPlayButton;
@JsonKey(defaultValue: false)
late bool playerBackgroundOnLyrics;
@JsonKey(defaultValue: NavigatorRouteType.material)
late NavigatorRouteType navigatorRouteType;
//Colors //Colors
@JsonKey(toJson: _colorToJson, fromJson: _colorFromJson) @JsonKey(toJson: _colorToJson, fromJson: _colorFromJson)
@ -147,17 +155,17 @@ class Settings {
ThemeData? get themeData { ThemeData? get themeData {
//System theme //System theme
if (useSystemTheme!) { if (useSystemTheme) {
if (SchedulerBinding.instance!.window.platformBrightness == if (SchedulerBinding.instance!.window.platformBrightness ==
Brightness.light) { Brightness.light) {
return _themeData[Themes.Light]; return _themeData[Themes.Light];
} else { } else {
if (theme == Themes.Light) return _themeData[Themes.Dark]; if (theme == Themes.Light) return _themeData[Themes.Dark];
return _themeData[theme!]; return _themeData[theme];
} }
} }
//Theme //Theme
return _themeData[theme!] ?? ThemeData(); return _themeData[theme] ?? ThemeData();
} }
//Get all available fonts //Get all available fonts
@ -258,7 +266,7 @@ class Settings {
//Check if is dark, can't use theme directly, because of system themes, and Theme.of(context).brightness broke //Check if is dark, can't use theme directly, because of system themes, and Theme.of(context).brightness broke
bool get isDark { bool get isDark {
if (useSystemTheme!) { if (useSystemTheme) {
if (SchedulerBinding.instance!.window.platformBrightness == if (SchedulerBinding.instance!.window.platformBrightness ==
Brightness.light) return false; Brightness.light) return false;
return true; return true;
@ -272,7 +280,7 @@ class Settings {
TextTheme? get _textTheme => (font == 'Deezer') TextTheme? get _textTheme => (font == 'Deezer')
? null ? null
: GoogleFonts.getTextTheme( : GoogleFonts.getTextTheme(
font!, font,
this.isDark this.isDark
? ThemeData.dark().textTheme ? ThemeData.dark().textTheme
: ThemeData.light().textTheme); : ThemeData.light().textTheme);
@ -292,6 +300,8 @@ class Settings {
sliderTheme: _sliderTheme, sliderTheme: _sliderTheme,
toggleableActiveColor: primaryColor, toggleableActiveColor: primaryColor,
bottomAppBarColor: Color(0xfff5f5f5), bottomAppBarColor: Color(0xfff5f5f5),
appBarTheme:
AppBarTheme(systemOverlayStyle: SystemUiOverlayStyle.light),
), ),
Themes.Dark: ThemeData( Themes.Dark: ThemeData(
textTheme: _textTheme, textTheme: _textTheme,

View file

@ -70,6 +70,12 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) => Settings(
..font = json['font'] as String? ?? 'Deezer' ..font = json['font'] as String? ?? 'Deezer'
..lyricsVisualizer = json['lyricsVisualizer'] as bool? ?? false ..lyricsVisualizer = json['lyricsVisualizer'] as bool? ?? false
..displayMode = json['displayMode'] as int? ..displayMode = json['displayMode'] as int?
..enableFilledPlayButton = json['enableFilledPlayButton'] as bool? ?? true
..playerBackgroundOnLyrics =
json['playerBackgroundOnLyrics'] as bool? ?? false
..navigatorRouteType = _$enumDecodeNullable(
_$NavigatorRouteTypeEnumMap, json['navigatorRouteType']) ??
NavigatorRouteType.material
..primaryColor = Settings._colorFromJson(json['primaryColor'] as int?) ..primaryColor = Settings._colorFromJson(json['primaryColor'] as int?)
..useArtColor = json['useArtColor'] as bool? ?? false ..useArtColor = json['useArtColor'] as bool? ?? false
..deezerLanguage = json['deezerLanguage'] as String? ?? 'en' ..deezerLanguage = json['deezerLanguage'] as String? ?? 'en'
@ -117,6 +123,10 @@ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
'font': instance.font, 'font': instance.font,
'lyricsVisualizer': instance.lyricsVisualizer, 'lyricsVisualizer': instance.lyricsVisualizer,
'displayMode': instance.displayMode, 'displayMode': instance.displayMode,
'enableFilledPlayButton': instance.enableFilledPlayButton,
'playerBackgroundOnLyrics': instance.playerBackgroundOnLyrics,
'navigatorRouteType':
_$NavigatorRouteTypeEnumMap[instance.navigatorRouteType],
'primaryColor': Settings._colorToJson(instance.primaryColor), 'primaryColor': Settings._colorToJson(instance.primaryColor),
'useArtColor': instance.useArtColor, 'useArtColor': instance.useArtColor,
'deezerLanguage': instance.deezerLanguage, 'deezerLanguage': instance.deezerLanguage,
@ -181,6 +191,12 @@ const _$ThemesEnumMap = {
Themes.Black: 'Black', Themes.Black: 'Black',
}; };
const _$NavigatorRouteTypeEnumMap = {
NavigatorRouteType.blur_slide: 'blur_slide',
NavigatorRouteType.material: 'material',
NavigatorRouteType.cupertino: 'cupertino',
};
SpotifyCredentialsSave _$SpotifyCredentialsSaveFromJson( SpotifyCredentialsSave _$SpotifyCredentialsSaveFromJson(
Map<String, dynamic> json) => Map<String, dynamic> json) =>
SpotifyCredentialsSave( SpotifyCredentialsSave(

28
lib/ui/animated_blur.dart Normal file
View file

@ -0,0 +1,28 @@
import 'dart:ui';
import 'package:flutter/widgets.dart';
class AnimatedBlur extends StatelessWidget {
final Animation animation;
final double multiplier;
final Widget? child;
const AnimatedBlur({
Key? key,
required this.animation,
required this.multiplier,
this.child,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation,
child: child,
builder: (context, child) {
final sigma = animation.value * multiplier;
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: sigma, sigmaY: sigma),
child: child);
});
}
}

View file

@ -21,7 +21,7 @@ class ImagesDatabase {
Future<Color> getPrimaryColor(String url) async { Future<Color> getPrimaryColor(String url) async {
PaletteGenerator paletteGenerator = await getPaletteGenerator(url); PaletteGenerator paletteGenerator = await getPaletteGenerator(url);
return paletteGenerator.colors.first; return paletteGenerator.dominantColor!.color;
} }
Future<bool> isDark(String url) async { Future<bool> isDark(String url) async {
@ -113,8 +113,16 @@ class ZoomableImage extends StatefulWidget {
final String? url; final String? url;
final bool rounded; final bool rounded;
final double? width; final double? width;
final bool enableHero;
final Object? heroTag;
ZoomableImage({required this.url, this.rounded = false, this.width}); ZoomableImage({
required this.url,
this.rounded = false,
this.width,
this.enableHero = true,
this.heroTag,
});
@override @override
_ZoomableImageState createState() => _ZoomableImageState(); _ZoomableImageState createState() => _ZoomableImageState();
@ -123,6 +131,8 @@ class ZoomableImage extends StatefulWidget {
class _ZoomableImageState extends State<ZoomableImage> { class _ZoomableImageState extends State<ZoomableImage> {
PhotoViewController? controller; PhotoViewController? controller;
bool photoViewOpened = false; bool photoViewOpened = false;
late final Object? _key =
widget.enableHero ? (widget.heroTag ?? UniqueKey()) : null;
@override @override
void initState() { void initState() {
@ -141,28 +151,43 @@ class _ZoomableImageState extends State<ZoomableImage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( print('key: ' + _key.toString());
child: Semantics( final image = CachedImage(
child: CachedImage(
url: widget.url, url: widget.url,
rounded: widget.rounded, rounded: widget.rounded,
width: widget.width, width: widget.width,
fullThumb: true, fullThumb: true,
), );
final child = _key != null
? Hero(
tag: _key!,
child: image,
)
: image;
return GestureDetector(
child: Semantics(
child: child,
label: "Album art".i18n, label: "Album art".i18n,
), ),
onTap: () { onTap: () {
Navigator.of(context).push(PageRouteBuilder( Navigator.of(context).push(PageRouteBuilder(
opaque: false, // transparent background opaque: false, // transparent background
pageBuilder: (context, _, __) { pageBuilder: (context, animation, __) {
print('key: ' + _key.toString());
photoViewOpened = true; photoViewOpened = true;
return PhotoView( return FadeTransition(
opacity: animation,
child: PhotoView(
imageProvider: CachedNetworkImageProvider(widget.url!), imageProvider: CachedNetworkImageProvider(widget.url!),
maxScale: 8.0, maxScale: 8.0,
minScale: 0.2, minScale: 0.2,
controller: controller, controller: controller,
backgroundDecoration: heroAttributes: _key == null
BoxDecoration(color: Color.fromARGB(0x90, 0, 0, 0))); ? null
: PhotoViewHeroAttributes(tag: _key!),
backgroundDecoration: const BoxDecoration(
color: Color.fromARGB(0x90, 0, 0, 0))),
);
})); }));
}, },
); );

View file

@ -202,10 +202,7 @@ class _AlbumDetailsState extends State<AlbumDetails> {
//Add to library //Add to library
if (!album!.library!) { if (!album!.library!) {
await deezerAPI.addFavoriteAlbum(album!.id); await deezerAPI.addFavoriteAlbum(album!.id);
Fluttertoast.showToast( ScaffoldMessenger.of(context).snack
msg: 'Added to library'.i18n,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM);
setState(() => album!.library = true); setState(() => album!.library = true);
return; return;
} }
@ -260,7 +257,7 @@ class _AlbumDetailsState extends State<AlbumDetails> {
), ),
...List.generate( ...List.generate(
tracks.length, tracks.length,
(i) => TrackTile(tracks[i], onTap: () { (i) => TrackTile(tracks[i]!, onTap: () {
playerHelper.playFromAlbum( playerHelper.playFromAlbum(
album!, tracks[i]!.id); album!, tracks[i]!.id);
}, onHold: () { }, onHold: () {
@ -349,7 +346,7 @@ class ArtistDetails extends StatelessWidget {
FutureOr<Artist> _loadArtist(Artist artist) { FutureOr<Artist> _loadArtist(Artist artist) {
//Load artist from api if no albums //Load artist from api if no albums
if ((this.artist.albums ?? []).length == 0) { if ((artist.albums ?? []).length == 0) {
return deezerAPI.artist(artist.id); return deezerAPI.artist(artist.id);
} }
return artist; return artist;
@ -364,9 +361,7 @@ class ArtistDetails extends StatelessWidget {
//Error / not done //Error / not done
if (snapshot.hasError) return ErrorScreen(); if (snapshot.hasError) return ErrorScreen();
if (snapshot.connectionState != ConnectionState.done) if (snapshot.connectionState != ConnectionState.done)
return Center( return const Center(child: CircularProgressIndicator());
child: CircularProgressIndicator(),
);
return ListView( return ListView(
children: <Widget>[ children: <Widget>[
@ -499,9 +494,9 @@ class ArtistDetails extends StatelessWidget {
AlbumTile( AlbumTile(
artist.highlight!.data, artist.highlight!.data,
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => builder: (context) =>
AlbumDetails(artist.highlight!.data))); AlbumDetails(artist.highlight!.data));
}, },
), ),
const SizedBox(height: 8.0) const SizedBox(height: 8.0)
@ -536,13 +531,13 @@ class ArtistDetails extends StatelessWidget {
ListTile( ListTile(
title: Text('Show more tracks'.i18n), title: Text('Show more tracks'.i18n),
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => TrackListScreen( builder: (context) => TrackListScreen(
artist.topTracks, artist.topTracks,
QueueSource( QueueSource(
id: artist.id, id: artist.id,
text: 'Top'.i18n + '${artist.name}', text: 'Top'.i18n + '${artist.name}',
source: 'topTracks')))); source: 'topTracks')));
}), }),
FreezerDivider(), FreezerDivider(),
//Albums //Albums
@ -562,10 +557,10 @@ class ArtistDetails extends StatelessWidget {
return ListTile( return ListTile(
title: Text('Show all albums'.i18n), title: Text('Show all albums'.i18n),
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => DiscographyScreen( builder: (context) => DiscographyScreen(
artist: artist, artist: artist,
))); ));
}); });
} }
//Top albums //Top albums
@ -573,8 +568,8 @@ class ArtistDetails extends StatelessWidget {
return AlbumTile( return AlbumTile(
a, a,
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context)
builder: (context) => AlbumDetails(a))); .pushRoute(builder: (context) => AlbumDetails(a));
}, },
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
@ -1232,7 +1227,7 @@ class _ShowScreenState extends State<ShowScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: FreezerAppBar(_show!.name), appBar: FreezerAppBar(_show!.name!),
body: ListView( body: ListView(
children: [ children: [
Padding( Padding(

View file

@ -18,7 +18,7 @@ class DownloadsScreen extends StatefulWidget {
class _DownloadsScreenState extends State<DownloadsScreen> { class _DownloadsScreenState extends State<DownloadsScreen> {
List<Download> downloads = []; List<Download> downloads = [];
StreamSubscription? _stateSubscription; late StreamSubscription _stateSubscription;
//Sublists //Sublists
List<Download> get downloading => downloads List<Download> get downloading => downloads
@ -70,8 +70,7 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
@override @override
void dispose() { void dispose() {
_stateSubscription?.cancel(); _stateSubscription.cancel();
_stateSubscription = null;
super.dispose(); super.dispose();
} }
@ -96,13 +95,13 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
), ),
IconButton( IconButton(
icon: Icon( icon: Icon(
downloadManager.running! ? Icons.stop : Icons.play_arrow, downloadManager.running ? Icons.stop : Icons.play_arrow,
semanticLabel: semanticLabel:
downloadManager.running! ? "Stop".i18n : "Start".i18n, downloadManager.running ? "Stop".i18n : "Start".i18n,
), ),
onPressed: () { onPressed: () {
setState(() { setState(() {
if (downloadManager.running!) if (downloadManager.running)
downloadManager.stop(); downloadManager.stop();
else else
downloadManager.start(); downloadManager.start();

View file

@ -32,41 +32,45 @@ class EmptyLeading extends StatelessWidget {
} }
class FreezerAppBar extends StatelessWidget implements PreferredSizeWidget { class FreezerAppBar extends StatelessWidget implements PreferredSizeWidget {
final String? title; final String title;
final List<Widget> actions; final List<Widget>? actions;
final Widget? bottom; final PreferredSizeWidget? bottom;
//Should be specified if bottom is specified //Should be specified if bottom is specified
final double height; final double height;
final SystemUiOverlayStyle? systemUiOverlayStyle; final SystemUiOverlayStyle? systemUiOverlayStyle;
const FreezerAppBar(this.title, /// The appbar's backgroundColor, if left null,
{this.actions = const [], /// it defaults to [ThemeData.scaffoldBackgroundColor]
final Color? backgroundColor;
final Color? foregroundColor;
final Brightness? brightness;
const FreezerAppBar(
this.title, {
this.actions,
this.bottom, this.bottom,
this.height = 56.0, this.height = 56.0,
this.systemUiOverlayStyle}); this.systemUiOverlayStyle,
this.backgroundColor,
this.brightness,
this.foregroundColor,
});
Size get preferredSize => Size.fromHeight(this.height); Size get preferredSize => Size.fromHeight(this.height);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Theme( return AppBar(
data: ThemeData(
primaryColor: (Theme.of(context).brightness == Brightness.light)
? Colors.white
: Colors.black),
child: AppBar(
systemOverlayStyle: systemUiOverlayStyle, systemOverlayStyle: systemUiOverlayStyle,
elevation: 0.0, elevation: 0.0,
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor:
title: Text( backgroundColor ?? Theme.of(context).scaffoldBackgroundColor,
title!, title: Text(title, style: TextStyle(fontWeight: FontWeight.w900)),
style: TextStyle(
fontWeight: FontWeight.w900,
),
),
actions: actions, actions: actions,
bottom: bottom as PreferredSizeWidget?, bottom: bottom,
), foregroundColor:
foregroundColor ?? (settings.isDark ? Colors.white : Colors.black),
); );
} }
} }

View file

@ -175,13 +175,13 @@ class HomepageRowSection extends StatelessWidget {
style: TextStyle(fontSize: 20.0), style: TextStyle(fontSize: 20.0),
), ),
onPressed: () => onPressed: () =>
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => Scaffold( builder: (context) => Scaffold(
appBar: FreezerAppBar(section.title), appBar: FreezerAppBar(section.title!),
body: SingleChildScrollView( body: SingleChildScrollView(
child: HomePageScreen( child: HomePageScreen(
channel: channel:
DeezerChannel(target: section.pagePath))), DeezerChannel(target: section.pagePath)),
), ),
)), )),
); );
@ -245,8 +245,8 @@ class HomePageItemWidget extends StatelessWidget {
return AlbumCard( return AlbumCard(
item.value, item.value,
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => AlbumDetails(item.value))); builder: (context) => AlbumDetails(item.value));
}, },
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
@ -257,8 +257,8 @@ class HomePageItemWidget extends StatelessWidget {
return ArtistTile( return ArtistTile(
item.value, item.value,
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => ArtistDetails(item.value))); builder: (context) => ArtistDetails(item.value));
}, },
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
@ -269,8 +269,8 @@ class HomePageItemWidget extends StatelessWidget {
return PlaylistCardTile( return PlaylistCardTile(
item.value, item.value,
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => PlaylistDetails(item.value))); builder: (context) => PlaylistDetails(item.value));
}, },
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
@ -281,22 +281,22 @@ class HomePageItemWidget extends StatelessWidget {
return ChannelTile( return ChannelTile(
item.value, item.value,
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => Scaffold( builder: (context) => Scaffold(
appBar: FreezerAppBar(item.value.title.toString()), appBar: FreezerAppBar(item.value.title.toString()),
body: SingleChildScrollView( body: SingleChildScrollView(
child: HomePageScreen( child: HomePageScreen(
channel: item.value, channel: item.value,
)), )),
))); ));
}, },
); );
case HomePageItemType.SHOW: case HomePageItemType.SHOW:
return ShowCard( return ShowCard(
item.value, item.value,
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => ShowScreen(item.value))); builder: (context) => ShowScreen(item.value));
}, },
); );
default: default:

View file

@ -36,8 +36,8 @@ class LibraryAppBar extends StatelessWidget implements PreferredSizeWidget {
semanticLabel: "Download".i18n, semanticLabel: "Download".i18n,
), ),
onPressed: () { onPressed: () {
Navigator.of(context).push( Navigator.of(context)
MaterialPageRoute(builder: (context) => DownloadsScreen())); .pushRoute(builder: (context) => DownloadsScreen());
}, },
), ),
IconButton( IconButton(
@ -46,8 +46,8 @@ class LibraryAppBar extends StatelessWidget implements PreferredSizeWidget {
semanticLabel: "Settings".i18n, semanticLabel: "Settings".i18n,
), ),
onPressed: () { onPressed: () {
Navigator.of(context).push( Navigator.of(context)
MaterialPageRoute(builder: (context) => SettingsScreen())); .pushRoute(builder: (context) => SettingsScreen());
}, },
), ),
], ],
@ -65,7 +65,7 @@ class LibraryScreen extends StatelessWidget {
Container( Container(
height: 4.0, height: 4.0,
), ),
if (!downloadManager.running! && downloadManager.queueSize! > 0) if (!downloadManager.running && downloadManager.queueSize! > 0)
ListTile( ListTile(
title: Text('Downloads'.i18n), title: Text('Downloads'.i18n),
leading: LeadingIcon(Icons.file_download, color: Colors.grey), leading: LeadingIcon(Icons.file_download, color: Colors.grey),
@ -74,8 +74,8 @@ class LibraryScreen extends StatelessWidget {
.i18n), .i18n),
onTap: () { onTap: () {
downloadManager.start(); downloadManager.start();
Navigator.of(context).push( Navigator.of(context)
MaterialPageRoute(builder: (context) => DownloadsScreen())); .pushRoute(builder: (context) => DownloadsScreen());
}, },
), ),
ListTile( ListTile(
@ -97,32 +97,32 @@ class LibraryScreen extends StatelessWidget {
title: Text('Tracks'.i18n), title: Text('Tracks'.i18n),
leading: LeadingIcon(Icons.audiotrack, color: Color(0xffbe3266)), leading: LeadingIcon(Icons.audiotrack, color: Color(0xffbe3266)),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context)
MaterialPageRoute(builder: (context) => LibraryTracks())); .pushRoute(builder: (context) => LibraryTracks());
}, },
), ),
ListTile( ListTile(
title: Text('Albums'.i18n), title: Text('Albums'.i18n),
leading: LeadingIcon(Icons.album, color: Color(0xff4b2e7e)), leading: LeadingIcon(Icons.album, color: Color(0xff4b2e7e)),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context)
MaterialPageRoute(builder: (context) => LibraryAlbums())); .pushRoute(builder: (context) => LibraryAlbums());
}, },
), ),
ListTile( ListTile(
title: Text('Artists'.i18n), title: Text('Artists'.i18n),
leading: LeadingIcon(Icons.recent_actors, color: Color(0xff384697)), leading: LeadingIcon(Icons.recent_actors, color: Color(0xff384697)),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context)
MaterialPageRoute(builder: (context) => LibraryArtists())); .pushRoute(builder: (context) => LibraryArtists());
}, },
), ),
ListTile( ListTile(
title: Text('Playlists'.i18n), title: Text('Playlists'.i18n),
leading: LeadingIcon(Icons.playlist_play, color: Color(0xff0880b5)), leading: LeadingIcon(Icons.playlist_play, color: Color(0xff0880b5)),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context)
MaterialPageRoute(builder: (context) => LibraryPlaylists())); .pushRoute(builder: (context) => LibraryPlaylists());
}, },
), ),
FreezerDivider(), FreezerDivider(),
@ -130,8 +130,8 @@ class LibraryScreen extends StatelessWidget {
title: Text('History'.i18n), title: Text('History'.i18n),
leading: LeadingIcon(Icons.history, color: Color(0xff009a85)), leading: LeadingIcon(Icons.history, color: Color(0xff009a85)),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context)
MaterialPageRoute(builder: (context) => HistoryScreen())); .pushRoute(builder: (context) => HistoryScreen());
}, },
), ),
FreezerDivider(), FreezerDivider(),
@ -142,8 +142,8 @@ class LibraryScreen extends StatelessWidget {
onTap: () { onTap: () {
//Show progress //Show progress
if (importer.done || importer.busy) { if (importer.done || importer.busy) {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context)
builder: (context) => ImporterStatusScreen())); .pushRoute(builder: (context) => ImporterStatusScreen());
return; return;
} }
@ -161,8 +161,8 @@ class LibraryScreen extends StatelessWidget {
.i18n), .i18n),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => SpotifyImporterV1())); builder: (context) => SpotifyImporterV1());
}, },
), ),
ListTile( ListTile(
@ -173,8 +173,8 @@ class LibraryScreen extends StatelessWidget {
.i18n), .i18n),
onTap: () { onTap: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => SpotifyImporterV2())); builder: (context) => SpotifyImporterV2());
}, },
) )
], ],
@ -509,13 +509,13 @@ class _LibraryTracksState extends State<LibraryTracks> {
? _sorted[i] ? _sorted[i]
: tracks![i]; : tracks![i];
return TrackTile( return TrackTile(
t, t!,
onTap: () { onTap: () {
playerHelper.playFromTrackList( playerHelper.playFromTrackList(
(tracks!.length == (trackCount ?? 0)) (tracks!.length == (trackCount ?? 0))
? _sorted ? _sorted
: tracks!, : tracks!,
t!.id, t.id,
QueueSource( QueueSource(
id: deezerAPI.favoritesPlaylistId, id: deezerAPI.favoritesPlaylistId,
text: 'Favorites'.i18n, text: 'Favorites'.i18n,
@ -523,7 +523,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
}, },
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
m.defaultTrackMenu(t!, onRemove: () { m.defaultTrackMenu(t, onRemove: () {
setState(() { setState(() {
tracks!.removeWhere((track) => t.id == track!.id); tracks!.removeWhere((track) => t.id == track!.id);
}); });
@ -553,19 +553,18 @@ class _LibraryTracksState extends State<LibraryTracks> {
...List.generate(allTracks.length, (i) { ...List.generate(allTracks.length, (i) {
Track? t = allTracks[i]; Track? t = allTracks[i];
return TrackTile( return TrackTile(
t, t!,
onTap: () { onTap: () {
playerHelper.playFromTrackList( playerHelper.playFromTrackList(
allTracks, allTracks,
t!.id, t.id,
QueueSource( QueueSource(
id: 'allTracks', id: 'allTracks',
text: 'All offline tracks'.i18n, text: 'All offline tracks'.i18n,
source: 'offline')); source: 'offline'));
}, },
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet(context).defaultTrackMenu(t);
m.defaultTrackMenu(t!);
}, },
); );
}) })
@ -714,8 +713,8 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
return AlbumTile( return AlbumTile(
a, a,
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context)
builder: (context) => AlbumDetails(a))); .pushRoute(builder: (context) => AlbumDetails(a));
}, },
onHold: () async { onHold: () async {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
@ -751,8 +750,8 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
return AlbumTile( return AlbumTile(
a, a,
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => AlbumDetails(a))); builder: (context) => AlbumDetails(a));
}, },
onHold: () async { onHold: () async {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
@ -919,8 +918,8 @@ class _LibraryArtistsState extends State<LibraryArtists> {
return ArtistHorizontalTile( return ArtistHorizontalTile(
a, a,
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context)
builder: (context) => ArtistDetails(a))); .pushRoute(builder: (context) => ArtistDetails(a));
}, },
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
@ -1118,9 +1117,8 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
PlaylistTile( PlaylistTile(
favoritesPlaylist, favoritesPlaylist,
onTap: () async { onTap: () async {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => builder: (context) => PlaylistDetails(favoritesPlaylist));
PlaylistDetails(favoritesPlaylist)));
}, },
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
@ -1134,8 +1132,8 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
Playlist p = _sorted[i]; Playlist p = _sorted[i];
return PlaylistTile( return PlaylistTile(
p, p,
onTap: () => Navigator.of(context).push(MaterialPageRoute( onTap: () => Navigator.of(context)
builder: (context) => PlaylistDetails(p))), .pushRoute(builder: (context) => PlaylistDetails(p)),
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
m.defaultPlaylistMenu(p, onRemove: () { m.defaultPlaylistMenu(p, onRemove: () {
@ -1175,9 +1173,8 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
Playlist p = playlists[i]; Playlist p = playlists[i];
return PlaylistTile( return PlaylistTile(
p, p,
onTap: () => Navigator.of(context).push( onTap: () => Navigator.of(context).pushRoute(
MaterialPageRoute( builder: (context) => PlaylistDetails(p)),
builder: (context) => PlaylistDetails(p))),
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
m.defaultPlaylistMenu(p, onRemove: () { m.defaultPlaylistMenu(p, onRemove: () {

View file

@ -215,9 +215,9 @@ class _LoginWidgetState extends State<LoginWidget> {
OutlinedButton( OutlinedButton(
child: Text('Login using browser'.i18n), child: Text('Login using browser'.i18n),
onPressed: () { onPressed: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => builder: (context) =>
LoginBrowser(_update))); LoginBrowser(_update));
}, },
), ),
OutlinedButton( OutlinedButton(

View file

@ -1,257 +0,0 @@
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/settings.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/player_bar.dart';
class LyricsScreen extends StatefulWidget {
LyricsScreen({Key? key}) : super(key: key);
@override
_LyricsScreenState createState() => _LyricsScreenState();
}
class _LyricsScreenState extends State<LyricsScreen> {
late StreamSubscription _mediaItemSub;
late StreamSubscription _playbackStateSub;
int? _currentIndex = -1;
int? _prevIndex = -1;
ScrollController _controller = ScrollController();
final double height = 90;
Lyrics? lyrics;
bool _loading = true;
Object? _error;
bool _freeScroll = false;
bool _animatedScroll = false;
Future _loadForId(String trackId) async {
//Fetch
if (_loading == false && lyrics != null) {
setState(() {
_freeScroll = false;
_loading = true;
lyrics = null;
});
}
try {
Lyrics l = await deezerAPI.lyrics(trackId);
setState(() {
_loading = false;
lyrics = l;
});
_scrollToLyric();
} catch (e) {
setState(() {
_error = e;
});
}
}
Future<void> _scrollToLyric() async {
//Lyric height, screen height, appbar height
double _scrollTo = (height * _currentIndex!) -
(MediaQuery.of(context).size.height / 2) +
(height / 2) +
56;
if (0 > _scrollTo) return;
_animatedScroll = true;
await _controller.animateTo(_scrollTo,
duration: Duration(milliseconds: 250), curve: Curves.ease);
_animatedScroll = false;
}
@override
void initState() {
SchedulerBinding.instance!.addPostFrameCallback((_) {
//Enable visualizer
// if (settings.lyricsVisualizer) playerHelper.startVisualizer();
_playbackStateSub = AudioService.position.listen((position) {
if (_loading) return;
_currentIndex =
lyrics?.lyrics?.lastIndexWhere((l) => l.offset! <= position);
//Scroll to current lyric
if (_currentIndex! < 0) return;
if (_prevIndex == _currentIndex) return;
//Update current lyric index
setState(() => null);
_prevIndex = _currentIndex;
if (_freeScroll) return;
_scrollToLyric();
});
});
if (audioHandler.mediaItem.value != null)
_loadForId(audioHandler.mediaItem.value!.id);
/// Track change = ~exit~ reload lyrics
_mediaItemSub = audioHandler.mediaItem.listen((mediaItem) {
if (mediaItem == null) return;
if (_controller.hasClients) _controller.jumpTo(0.0);
_loadForId(mediaItem.id);
});
super.initState();
}
@override
void dispose() {
_mediaItemSub.cancel();
_playbackStateSub.cancel();
//Stop visualizer
// if (settings.lyricsVisualizer) playerHelper.stopVisualizer();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar('Lyrics'.i18n,
systemUiOverlayStyle: 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,
)),
body: SafeArea(
child: Column(
children: [
Theme(
data: settings.themeData!.copyWith(
textButtonTheme: TextButtonThemeData(
style: ButtonStyle(
foregroundColor:
MaterialStateProperty.all(Colors.white)))),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_freeScroll && !_loading)
TextButton(
onPressed: () {
setState(() => _freeScroll = false);
_scrollToLyric();
},
child: Text(
_currentIndex! >= 0
? (lyrics?.lyrics?[_currentIndex!].text ?? '...')
: '...',
textAlign: TextAlign.center,
),
style: ButtonStyle(
foregroundColor:
MaterialStateProperty.all(Colors.white)))
],
),
),
Expanded(
child: Stack(children: [
//Lyrics
_error != null
?
//Shouldn't really happen, empty lyrics have own text
ErrorScreen(message: _error.toString())
:
// Loading lyrics
_loading
? Center(child: CircularProgressIndicator())
: NotificationListener(
onNotification: (Notification notification) {
if (_freeScroll ||
notification is! ScrollStartNotification)
return false;
if (!_animatedScroll && !_loading)
setState(() => _freeScroll = true);
return false;
},
child: ListView.builder(
controller: _controller,
padding: EdgeInsets.fromLTRB(
0,
0,
0,
settings.lyricsVisualizer! && false
? 100
: 0),
itemCount: lyrics!.lyrics!.length,
itemBuilder: (BuildContext context, int i) {
return Padding(
padding:
EdgeInsets.symmetric(horizontal: 8.0),
child: Container(
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(8.0),
color: _currentIndex == i
? Colors.grey.withOpacity(0.25)
: Colors.transparent,
),
height: height,
child: InkWell(
borderRadius:
BorderRadius.circular(8.0),
onTap: lyrics!.id != null
? () => audioHandler.seek(
lyrics!.lyrics![i].offset!)
: null,
child: Center(
child: Text(
lyrics!.lyrics![i].text!,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26.0,
fontWeight:
(_currentIndex == i)
? FontWeight.bold
: FontWeight
.normal),
),
))));
},
)),
//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,
// )),
// );
// }),
// ),
]),
),
PlayerBar(shouldHandleClicks: false),
],
),
),
);
}
}

248
lib/ui/lyrics_screen.dart Normal file
View file

@ -0,0 +1,248 @@
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/settings.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/player_bar.dart';
import 'package:freezer/ui/player_screen.dart';
class LyricsScreen extends StatefulWidget {
LyricsScreen({Key? key}) : super(key: key);
@override
_LyricsScreenState createState() => _LyricsScreenState();
}
class _LyricsScreenState extends State<LyricsScreen> {
late StreamSubscription _mediaItemSub;
late StreamSubscription _playbackStateSub;
int? _currentIndex = -1;
int? _prevIndex = -1;
ScrollController _controller = ScrollController();
final double height = 90;
Lyrics? lyrics;
bool _loading = true;
Object? _error;
bool _freeScroll = false;
bool _animatedScroll = false;
Future _loadForId(String trackId) async {
//Fetch
if (_loading == false && lyrics != null) {
setState(() {
_freeScroll = false;
_loading = true;
lyrics = null;
});
}
try {
Lyrics l = await deezerAPI.lyrics(trackId);
setState(() {
_loading = false;
lyrics = l;
});
_scrollToLyric();
} catch (e) {
setState(() {
_error = e;
});
}
}
Future<void> _scrollToLyric() async {
if (!_controller.hasClients) return;
//Lyric height, screen height, appbar height
double _scrollTo = (height * _currentIndex!) -
(MediaQuery.of(context).size.height / 4 + height / 2);
print(
'${height * _currentIndex!}, ${MediaQuery.of(context).size.height / 2}');
if (0 > _scrollTo) return;
if (_scrollTo > _controller.position.maxScrollExtent)
_scrollTo = _controller.position.maxScrollExtent;
_animatedScroll = true;
await _controller.animateTo(_scrollTo,
duration: Duration(milliseconds: 250), curve: Curves.ease);
_animatedScroll = false;
}
@override
void initState() {
SchedulerBinding.instance!.addPostFrameCallback((_) {
//Enable visualizer
// if (settings.lyricsVisualizer) playerHelper.startVisualizer();
_playbackStateSub = AudioService.position.listen((position) {
if (_loading) return;
_currentIndex =
lyrics?.lyrics?.lastIndexWhere((l) => l.offset! <= position);
//Scroll to current lyric
if (_currentIndex! < 0) return;
if (_prevIndex == _currentIndex) return;
//Update current lyric index
setState(() => null);
_prevIndex = _currentIndex;
if (_freeScroll) return;
_scrollToLyric();
});
});
if (audioHandler.mediaItem.value != null)
_loadForId(audioHandler.mediaItem.value!.id);
/// Track change = ~exit~ reload lyrics
_mediaItemSub = audioHandler.mediaItem.listen((mediaItem) {
if (mediaItem == null) return;
if (_controller.hasClients) _controller.jumpTo(0.0);
_loadForId(mediaItem.id);
});
super.initState();
}
@override
void dispose() {
_mediaItemSub.cancel();
_playbackStateSub.cancel();
//Stop visualizer
// if (settings.lyricsVisualizer) playerHelper.stopVisualizer();
super.dispose();
}
@override
Widget build(BuildContext context) {
return PlayerScreenBackground(
enabled: settings.playerBackgroundOnLyrics,
appBar: FreezerAppBar(
'Lyrics'.i18n,
systemUiOverlayStyle: PlayerScreenBackground.getSystemUiOverlayStyle(
context,
enabled: settings.playerBackgroundOnLyrics),
backgroundColor: Colors.transparent,
),
child: Column(
children: [
if (_freeScroll && !_loading)
Center(
child: TextButton(
onPressed: () {
setState(() => _freeScroll = false);
_scrollToLyric();
},
child: Text(
_currentIndex! >= 0
? (lyrics?.lyrics?[_currentIndex!].text ?? '...')
: '...',
textAlign: TextAlign.center,
),
style: ButtonStyle(
foregroundColor:
MaterialStateProperty.all(Colors.white))),
),
Expanded(
child: Stack(children: [
//Lyrics
_error != null
?
//Shouldn't really happen, empty lyrics have own text
ErrorScreen(message: _error.toString())
:
// Loading lyrics
_loading
? Center(child: CircularProgressIndicator())
: NotificationListener<ScrollStartNotification>(
onNotification:
(ScrollStartNotification notification) {
final extentDiff =
(notification.metrics.extentBefore -
notification.metrics.extentAfter)
.abs();
// avoid accidental clicks
const extentThreshold = 10.0;
if (extentDiff >= extentThreshold &&
!_animatedScroll &&
!_loading &&
!_freeScroll) {
setState(() => _freeScroll = true);
}
return false;
},
child: ListView.builder(
controller: _controller,
itemCount: lyrics!.lyrics!.length,
itemBuilder: (BuildContext context, int i) {
return Padding(
padding:
EdgeInsets.symmetric(horizontal: 8.0),
child: Container(
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(8.0),
color: _currentIndex == i
? Colors.grey.withOpacity(0.25)
: Colors.transparent,
),
height: height,
child: InkWell(
borderRadius:
BorderRadius.circular(8.0),
onTap: lyrics!.id != null
? () => audioHandler.seek(
lyrics!.lyrics![i].offset!)
: null,
child: Center(
child: Text(
lyrics!.lyrics![i].text!,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26.0,
fontWeight:
(_currentIndex == i)
? FontWeight.bold
: FontWeight.normal),
),
))));
},
)),
//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,
// )),
// );
// }),
// ),
]),
),
Divider(height: 1.0, thickness: 1.0),
PlayerBar(
shouldHandleClicks: false, backgroundColor: Colors.transparent),
],
),
);
}
}

View file

@ -2,6 +2,8 @@ import 'dart:async';
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:freezer/api/definitions.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';
@ -11,7 +13,14 @@ import 'player_screen.dart';
class PlayerBar extends StatefulWidget { class PlayerBar extends StatefulWidget {
final bool shouldHandleClicks; final bool shouldHandleClicks;
const PlayerBar({Key? key, this.shouldHandleClicks = true}) : super(key: key); final bool shouldHaveHero;
final Color? backgroundColor;
const PlayerBar({
Key? key,
this.shouldHandleClicks = true,
this.shouldHaveHero = true,
this.backgroundColor,
}) : super(key: key);
@override @override
_PlayerBarState createState() => _PlayerBarState(); _PlayerBarState createState() => _PlayerBarState();
@ -21,6 +30,7 @@ class _PlayerBarState extends State<PlayerBar> {
final double iconSize = 28; final double iconSize = 28;
late StreamSubscription mediaItemSub; late StreamSubscription mediaItemSub;
late bool _isNothingPlaying = audioHandler.mediaItem.value == null; late bool _isNothingPlaying = audioHandler.mediaItem.value == null;
final focusNode = FocusNode();
double parsePosition(Duration position) { double parsePosition(Duration position) {
if (audioHandler.mediaItem.value == null) return 0.0; if (audioHandler.mediaItem.value == null) return 0.0;
@ -40,8 +50,12 @@ class _PlayerBarState extends State<PlayerBar> {
super.initState(); super.initState();
} }
Color get backgroundColor =>
widget.backgroundColor ?? Theme.of(context).bottomAppBarColor;
@override @override
void dispose() { void dispose() {
focusNode.dispose();
mediaItemSub.cancel(); mediaItemSub.cancel();
super.dispose(); super.dispose();
} }
@ -50,7 +64,6 @@ class _PlayerBarState extends State<PlayerBar> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var focusNode = FocusNode();
return _isNothingPlaying return _isNothingPlaying
? const SizedBox() ? const SizedBox()
: GestureDetector( : GestureDetector(
@ -72,35 +85,35 @@ class _PlayerBarState extends State<PlayerBar> {
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[ child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
StreamBuilder<MediaItem?>( StreamBuilder<MediaItem?>(
stream: audioHandler.mediaItem, stream: audioHandler.mediaItem,
initialData: audioHandler.mediaItem.valueOrNull,
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) return const SizedBox(); if (!snapshot.hasData) return const SizedBox();
final currentMediaItem = snapshot.data!; final currentMediaItem = snapshot.data!;
return DecoratedBox( final image = CachedImage(
width: 50,
height: 50,
url: currentMediaItem.extras!['thumb'] ??
audioHandler.mediaItem.value!.artUri as String?,
);
final leadingWidget = widget.shouldHaveHero
? Hero(tag: currentMediaItem.id, child: image)
: image;
return Material(
// For Android TV: indicate focus by grey // For Android TV: indicate focus by grey
decoration: BoxDecoration(
color: focusNode.hasFocus color: focusNode.hasFocus
? Colors.black26 ? Color.lerp(backgroundColor, Colors.grey, 0.26)
: Theme.of(context).bottomAppBarColor), : backgroundColor,
child: ListTile( child: ListTile(
dense: true, dense: true,
focusNode: focusNode, focusNode: focusNode,
contentPadding: contentPadding:
EdgeInsets.symmetric(horizontal: 8.0), EdgeInsets.symmetric(horizontal: 8.0),
onTap: widget.shouldHandleClicks onTap: widget.shouldHandleClicks
? () { ? _pushPlayerScreen
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) =>
PlayerScreen()));
}
: null, : null,
leading: CachedImage( leading: AnimatedSwitcher(
width: 50, duration: const Duration(milliseconds: 250),
height: 50, child: leadingWidget),
url: currentMediaItem.extras!['thumb'] ??
audioHandler.mediaItem.value!.artUri
as String?,
),
title: Text( title: Text(
currentMediaItem.displayTitle!, currentMediaItem.displayTitle!,
overflow: TextOverflow.clip, overflow: TextOverflow.clip,
@ -135,8 +148,6 @@ class _PlayerBarState extends State<PlayerBar> {
stream: AudioService.position, stream: AudioService.position,
builder: (context, snapshot) { builder: (context, snapshot) {
return LinearProgressIndicator( return LinearProgressIndicator(
backgroundColor:
Theme.of(context).primaryColor.withOpacity(0.1),
value: parsePosition(snapshot.data ?? Duration.zero), value: parsePosition(snapshot.data ?? Duration.zero),
); );
}), }),
@ -144,6 +155,15 @@ class _PlayerBarState extends State<PlayerBar> {
]), ]),
); );
} }
void _pushPlayerScreen() {
final builder = (BuildContext context) => PlayerScreen();
if (settings.blurPlayerBackground) {
Navigator.of(context).push(FadePageRoute(builder: builder));
return;
}
Navigator.of(context).pushRoute(builder: builder);
}
} }
class PrevNextButton extends StatelessWidget { class PrevNextButton extends StatelessWidget {
@ -154,8 +174,8 @@ class PrevNextButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StreamBuilder<List<MediaItem?>>( return StreamBuilder<MediaItem?>(
stream: audioHandler.queue, stream: audioHandler.mediaItem,
builder: (context, snapshot) { builder: (context, snapshot) {
if (!prev) { if (!prev) {
return IconButton( return IconButton(
@ -165,7 +185,7 @@ class PrevNextButton extends StatelessWidget {
), ),
iconSize: size, iconSize: size,
onPressed: onPressed:
playerHelper.queueIndex == (snapshot.data ?? []).length - 1 playerHelper.queueIndex == audioHandler.queue.value.length - 1
? null ? null
: () => audioHandler.skipToNext(), : () => audioHandler.skipToNext(),
); );
@ -189,7 +209,18 @@ class PrevNextButton extends StatelessWidget {
class PlayPauseButton extends StatefulWidget { class PlayPauseButton extends StatefulWidget {
final double size; final double size;
PlayPauseButton(this.size, {Key? key}) : super(key: key); final bool filled;
final Color? iconColor;
/// The color of the card if [filled] is true
final Color? color;
const PlayPauseButton(
this.size, {
Key? key,
this.filled = false,
this.color,
this.iconColor,
}) : super(key: key);
@override @override
_PlayPauseButtonState createState() => _PlayPauseButtonState(); _PlayPauseButtonState createState() => _PlayPauseButtonState();
@ -197,64 +228,91 @@ class PlayPauseButton extends StatefulWidget {
class _PlayPauseButtonState extends State<PlayPauseButton> class _PlayPauseButtonState extends State<PlayPauseButton>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late AnimationController _controller; late AnimationController _controller =
late Animation<double> _animation; AnimationController(vsync: this, duration: Duration(milliseconds: 200));
late Animation<double> _animation =
CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
late StreamSubscription _subscription;
late bool _canPlay = audioHandler.playbackState.value.playing ||
audioHandler.playbackState.value.processingState ==
AudioProcessingState.ready;
@override @override
void initState() { void initState() {
_controller = _subscription = audioHandler.playbackState.listen((playbackState) {
AnimationController(vsync: this, duration: Duration(milliseconds: 200)); if (playbackState.playing ||
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut); playbackState.processingState == AudioProcessingState.ready) {
if (playbackState.playing)
_controller.forward();
else
_controller.reverse();
if (!_canPlay) setState(() => _canPlay = true);
return;
}
setState(() => _canPlay = false);
});
super.initState(); super.initState();
} }
@override @override
void dispose() { void dispose() {
_subscription.cancel();
_controller.dispose(); _controller.dispose();
super.dispose(); super.dispose();
} }
@override void _playPause() {
Widget build(BuildContext context) { if (audioHandler.playbackState.value.playing)
return StreamBuilder( audioHandler.pause();
stream: audioHandler.playbackState,
builder: (context, snapshot) {
//Animated icon by pato05
bool _playing = audioHandler.playbackState.value.playing;
if (_playing ||
audioHandler.playbackState.value.processingState ==
AudioProcessingState.ready) {
if (_playing)
_controller.forward();
else else
_controller.reverse(); audioHandler.play();
return IconButton(
icon: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _animation,
semanticLabel: _playing ? "Pause".i18n : "Play".i18n,
),
iconSize: widget.size,
onPressed: _playing
? () => audioHandler.pause()
: () => audioHandler.play());
} }
@override
Widget build(BuildContext context) {
final Widget? child;
if (_canPlay) {
final icon = AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _animation,
semanticLabel: audioHandler.playbackState.value.playing
? 'Pause'.i18n
: 'Play'.i18n,
);
if (!widget.filled)
return IconButton(
color: widget.iconColor,
icon: icon,
iconSize: widget.size,
onPressed: _playPause);
child = InkWell(
borderRadius: BorderRadius.circular(50.0),
child: IconTheme.merge(
child: Center(child: icon),
data: IconThemeData(
size: widget.size / 2, color: widget.iconColor)),
onTap: _playPause);
} else
switch (audioHandler.playbackState.value.processingState) { switch (audioHandler.playbackState.value.processingState) {
//Stopped/Error //Stopped/Error
case AudioProcessingState.error: case AudioProcessingState.error:
case AudioProcessingState.idle: case AudioProcessingState.idle:
return SizedBox(width: widget.size, height: widget.size); child = null;
break;
//Loading, connecting, rewinding... //Loading, connecting, rewinding...
default: default:
return SizedBox( child = const Center(child: CircularProgressIndicator());
width: widget.size, break;
height: widget.size,
child: const CircularProgressIndicator(),
);
} }
}, if (widget.filled)
); return SizedBox.square(
dimension: widget.size,
child: Card(
color: widget.color,
elevation: 2.0,
shape: CircleBorder(),
child: child));
else
return SizedBox.square(dimension: widget.size, child: child);
} }
} }

View file

@ -1,5 +1,6 @@
import 'dart:math'; import 'dart:ui';
import 'dart:convert';
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/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -7,166 +8,200 @@ import 'package:audio_service/audio_service.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:fluttertoast/fluttertoast.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';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/download.dart'; import 'package:freezer/api/download.dart';
import 'package:freezer/api/player.dart'; import 'package:freezer/api/player.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';
import 'package:freezer/ui/elements.dart'; import 'package:freezer/ui/cached_image.dart';
import 'package:freezer/ui/lyrics.dart'; import 'package:freezer/ui/lyrics_screen.dart';
import 'package:freezer/ui/menu.dart'; import 'package:freezer/ui/menu.dart';
import 'package:freezer/ui/player_bar.dart';
import 'package:freezer/ui/queue_screen.dart';
import 'package:freezer/ui/settings_screen.dart'; import 'package:freezer/ui/settings_screen.dart';
import 'package:freezer/ui/tiles.dart';
import 'package:just_audio/just_audio.dart';
import 'package:marquee/marquee.dart'; import 'package:marquee/marquee.dart';
import 'package:palette_generator/palette_generator.dart'; import 'package:palette_generator/palette_generator.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
import 'package:provider/provider.dart';
import 'cached_image.dart';
import '../api/definitions.dart';
import 'player_bar.dart';
import 'dart:ui';
import 'dart:convert';
import 'dart:async';
//Changing item in queue view and pressing back causes the pageView to skip song //Changing item in queue view and pressing back causes the pageView to skip song
bool pageViewLock = false; bool pageViewLock = false;
//So can be updated when going back from lyrics const _blurStrength = 90.0;
late Function updateColor;
class PlayerScreen extends StatefulWidget { /// A simple [ChangeNotifier] that listens to the [AudioHandler.mediaItem] stream and
static const _blurStrength = 50.0; /// notifies its listeners when background changes
class BackgroundProvider extends ChangeNotifier {
Color _dominantColor;
ImageProvider? _imageProvider;
StreamSubscription? _mediaItemSub;
BackgroundProvider(this._dominantColor);
@override /// Calculate background color from [mediaItem]
_PlayerScreenState createState() => _PlayerScreenState(); ///
} /// Warning: this function is expensive to call, and should only be called when songs change!
Future _updateColor(MediaItem mediaItem) async {
class _PlayerScreenState extends State<PlayerScreen> { if (!settings.colorGradientBackground && !settings.blurPlayerBackground)
LinearGradient? _bgGradient;
late StreamSubscription _mediaItemSub;
late StreamSubscription _playerStateSub;
ImageProvider? _blurImage;
bool _wasConnected = true;
//Calculate background color
Future _updateColor() async {
if (!settings.colorGradientBackground! && !settings.blurPlayerBackground!)
return; return;
final imageProvider = CachedNetworkImageProvider( final imageProvider = CachedNetworkImageProvider(
audioHandler.mediaItem.value!.extras!['thumb'] ?? mediaItem.extras!['thumb'] ?? mediaItem.artUri as String);
audioHandler.mediaItem.value!.artUri as String);
//BG Image
if (settings.blurPlayerBackground!)
setState(() => _blurImage = imageProvider);
if (settings.colorGradientBackground!) {
//Run in isolate //Run in isolate
PaletteGenerator palette = PaletteGenerator palette =
await PaletteGenerator.fromImageProvider(imageProvider); await PaletteGenerator.fromImageProvider(imageProvider);
setState(() => _bgGradient = LinearGradient( _dominantColor = palette.dominantColor!.color;
begin: Alignment.topCenter, _imageProvider = settings.blurPlayerBackground ? imageProvider : null;
end: Alignment.bottomCenter, notifyListeners();
colors: [
palette.dominantColor!.color.withOpacity(0.7),
Color.fromARGB(0, 0, 0, 0)
],
stops: [
0.0,
0.6
]));
}
}
void _playbackStateChanged() {
// if (audioHandler.mediaItem.value == null) {
// //playerHelper.startService();
// setState(() => _wasConnected = false);
// } else if (!_wasConnected) setState(() => _wasConnected = true);
} }
@override @override
void initState() { void addListener(VoidCallback listener) {
Future.delayed(Duration(milliseconds: 600), _updateColor); _mediaItemSub ??= audioHandler.mediaItem.listen((mediaItem) {
_playbackStateChanged(); if (mediaItem == null) return;
_mediaItemSub = audioHandler.mediaItem.listen((event) { _updateColor(mediaItem);
_playbackStateChanged();
_updateColor();
}); });
_playerStateSub = super.addListener(listener);
audioHandler.playbackState.listen((_) => _playbackStateChanged()); }
updateColor = this._updateColor; @override
super.initState(); void removeListener(VoidCallback listener) {
super.removeListener(listener);
if (!hasListeners && _mediaItemSub != null) {
_mediaItemSub!.cancel();
_mediaItemSub = null;
}
} }
@override @override
void dispose() { void dispose() {
_mediaItemSub.cancel(); _mediaItemSub?.cancel();
_playerStateSub.cancel();
super.dispose(); super.dispose();
} }
Color get dominantColor => _dominantColor;
ImageProvider<Object>? get imageProvider => _imageProvider;
}
class PlayerScreen extends StatelessWidget {
const PlayerScreen({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final hasBackground = final defaultColor = Theme.of(context).cardColor;
settings.blurPlayerBackground! || settings.colorGradientBackground!; return ChangeNotifierProvider(
final color = hasBackground create: (context) => BackgroundProvider(defaultColor),
? Colors.transparent child: PlayerScreenBackground(
: Theme.of(context).scaffoldBackgroundColor;
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: color,
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.light,
systemNavigationBarIconBrightness: Brightness.light,
systemNavigationBarColor: color,
systemNavigationBarDividerColor: color,
),
child: Stack(
children: [
if (hasBackground)
Positioned.fill(
child: ImageFiltered(
imageFilter: ImageFilter.blur(
sigmaX: PlayerScreen._blurStrength,
sigmaY: PlayerScreen._blurStrength,
tileMode: TileMode.mirror),
child: DecoratedBox(
decoration: BoxDecoration(
gradient: _bgGradient,
image: _blurImage == null
? null
: DecorationImage(
image: _blurImage!,
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
Colors.white.withOpacity(0.5),
BlendMode.dstATop))),
),
),
),
Scaffold(
backgroundColor: hasBackground ? Colors.transparent : null,
body: _wasConnected
? SafeArea(
child: OrientationBuilder( child: OrientationBuilder(
builder: (context, orientation) => builder: (context, orientation) =>
orientation == Orientation.landscape orientation == Orientation.landscape
? PlayerScreenHorizontal() ? PlayerScreenHorizontal()
: PlayerScreenVertical(), : PlayerScreenVertical())),
);
}
}
/// Will change the background based on [BackgroundProvider],
/// it will wrap the [child] in a [Scaffold] and [SafeArea] widget
class PlayerScreenBackground extends StatelessWidget {
final Widget child;
final bool enabled;
final PreferredSizeWidget? appBar;
const PlayerScreenBackground({
required this.child,
this.enabled = true,
this.appBar,
});
Widget _buildChild(
BuildContext context, BackgroundProvider provider, Widget child) {
return Stack(children: [
if (provider.imageProvider != null || settings.colorGradientBackground)
Positioned.fill(
child: provider.imageProvider != null
? ImageFiltered(
imageFilter: ImageFilter.blur(
sigmaX: _blurStrength,
sigmaY: _blurStrength,
),
child: DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: provider.imageProvider!,
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
Colors.white
.withOpacity(settings.isDark ? 0.5 : 0.8),
BlendMode.dstATop),
)),
), ),
) )
: Center(child: CircularProgressIndicator()), : DecoratedBox(
), decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
provider.dominantColor,
Theme.of(context).scaffoldBackgroundColor,
], ],
), stops: [0.0, 0.6],
)),
)),
child,
]);
}
static SystemUiOverlayStyle getSystemUiOverlayStyle(BuildContext context,
{bool enabled = true}) {
final hasBackground = enabled &&
(settings.blurPlayerBackground || settings.colorGradientBackground);
final color = hasBackground
? Colors.transparent
: Theme.of(context).scaffoldBackgroundColor;
final brightness = hasBackground
? Brightness.light
: (ThemeData.estimateBrightnessForColor(color) == Brightness.light
? Brightness.dark
: Brightness.light);
return SystemUiOverlayStyle(
statusBarColor: color,
statusBarBrightness: brightness,
statusBarIconBrightness: brightness,
systemNavigationBarIconBrightness: brightness,
systemNavigationBarColor: color,
systemNavigationBarDividerColor: color,
); );
} }
@override
Widget build(BuildContext context) {
final hasBackground = enabled &&
(settings.blurPlayerBackground || settings.colorGradientBackground);
final color = hasBackground
? Colors.transparent
: Theme.of(context).scaffoldBackgroundColor;
Widget widgetChild = Scaffold(
appBar: appBar,
backgroundColor: color,
body: SafeArea(child: child),
);
if (enabled)
widgetChild = Consumer<BackgroundProvider>(
builder: (context, provider, child) {
return _buildChild(context, provider, child!);
},
child: widgetChild,
);
if (appBar == null)
widgetChild = AnnotatedRegion<SystemUiOverlayStyle>(
value: getSystemUiOverlayStyle(context, enabled: enabled),
child: widgetChild,
);
return widgetChild;
}
} }
//Landscape //Landscape
@ -285,45 +320,8 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
height: 1000.w, height: 1000.w,
), ),
), ),
PlayerTextSubtext(textSize: 64.sp),
const SizedBox(height: 4.0), const SizedBox(height: 4.0),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
height: ScreenUtil().setSp(80),
child: audioHandler.mediaItem.value!.displayTitle!.length >=
26
? Marquee(
text: audioHandler.mediaItem.value!.displayTitle!,
style: TextStyle(
fontSize: ScreenUtil().setSp(64),
fontWeight: FontWeight.bold),
blankSpace: 32.0,
startPadding: 10.0,
accelerationDuration: Duration(seconds: 1),
pauseAfterRound: Duration(seconds: 2),
)
: Text(
audioHandler.mediaItem.value!.displayTitle!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: ScreenUtil().setSp(64),
fontWeight: FontWeight.bold),
)),
const SizedBox(height: 4),
Text(
audioHandler.mediaItem.value!.displaySubtitle ?? '',
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.clip,
style: TextStyle(
fontSize: ScreenUtil().setSp(52),
color: Theme.of(context).primaryColor,
),
),
],
),
SeekBar(), SeekBar(),
PlaybackControls(86.sp), PlaybackControls(86.sp),
Padding( Padding(
@ -335,6 +333,58 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
} }
} }
class PlayerTextSubtext extends StatelessWidget {
final double textSize;
const PlayerTextSubtext({Key? key, required this.textSize}) : super(key: key);
@override
Widget build(BuildContext context) {
return StreamBuilder<MediaItem?>(
stream: audioHandler.mediaItem,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox();
}
final currentMediaItem = snapshot.data!;
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox(
height: textSize * 1.5,
child: currentMediaItem.displayTitle!.length >= 26
? Marquee(
text: currentMediaItem.displayTitle!,
style: TextStyle(
fontSize: textSize, fontWeight: FontWeight.bold),
blankSpace: 32.0,
startPadding: 10.0,
accelerationDuration: Duration(seconds: 1),
pauseAfterRound: Duration(seconds: 2),
)
: Text(
currentMediaItem.displayTitle!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: textSize, fontWeight: FontWeight.bold),
)),
const SizedBox(height: 4),
Text(
currentMediaItem.displaySubtitle ?? '',
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.clip,
style: TextStyle(
fontSize: textSize * 0.8, // 20% smaller
color: Theme.of(context).primaryColor,
),
),
],
);
});
}
}
class QualityInfoWidget extends StatefulWidget { class QualityInfoWidget extends StatefulWidget {
@override @override
_QualityInfoWidgetState createState() => _QualityInfoWidgetState(); _QualityInfoWidgetState createState() => _QualityInfoWidgetState();
@ -433,32 +483,32 @@ class _RepeatButtonState extends State<RepeatButton> {
// ignore: missing_return // ignore: missing_return
Icon get repeatIcon { Icon get repeatIcon {
switch (playerHelper.repeatType) { switch (playerHelper.repeatType) {
case LoopMode.off: case AudioServiceRepeatMode.none:
return Icon( return Icon(
Icons.repeat, Icons.repeat,
size: widget.iconSize,
semanticLabel: "Repeat off".i18n, semanticLabel: "Repeat off".i18n,
); );
case LoopMode.all: case AudioServiceRepeatMode.one:
return Icon(
Icons.repeat,
color: Theme.of(context).primaryColor,
size: widget.iconSize,
semanticLabel: "Repeat".i18n,
);
case LoopMode.one:
return Icon( return Icon(
Icons.repeat_one, Icons.repeat_one,
color: Theme.of(context).primaryColor,
size: widget.iconSize,
semanticLabel: "Repeat one".i18n, semanticLabel: "Repeat one".i18n,
); );
case AudioServiceRepeatMode.group:
case AudioServiceRepeatMode.all:
return Icon(
Icons.repeat,
semanticLabel: "Repeat".i18n,
);
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IconButton( return IconButton(
color: playerHelper.repeatType == AudioServiceRepeatMode.none
? null
: Theme.of(context).primaryColor,
iconSize: widget.iconSize,
icon: repeatIcon, icon: repeatIcon,
onPressed: () async { onPressed: () async {
await playerHelper.changeRepeat(); await playerHelper.changeRepeat();
@ -468,15 +518,38 @@ class _RepeatButtonState extends State<RepeatButton> {
} }
} }
class PlaybackControls extends StatefulWidget { class ShuffleButton extends StatefulWidget {
final double iconSize; final double iconSize;
PlaybackControls(this.iconSize, {Key? key}) : super(key: key); const ShuffleButton({Key? key, required this.iconSize}) : super(key: key);
@override @override
_PlaybackControlsState createState() => _PlaybackControlsState(); _ShuffleButtonState createState() => _ShuffleButtonState();
} }
class _PlaybackControlsState extends State<PlaybackControls> { class _ShuffleButtonState extends State<ShuffleButton> {
@override
Widget build(BuildContext context) => IconButton(
icon: Icon(Icons.shuffle),
iconSize: widget.iconSize,
color:
playerHelper.shuffleEnabled ? Theme.of(context).primaryColor : null,
onPressed: _toggleShuffle,
);
void _toggleShuffle() {
playerHelper.toggleShuffle().then((_) => setState(() => null));
}
}
class FavoriteButton extends StatefulWidget {
final double size;
const FavoriteButton({Key? key, required this.size}) : super(key: key);
@override
_FavoriteButtonState createState() => _FavoriteButtonState();
}
class _FavoriteButtonState extends State<FavoriteButton> {
Icon get libraryIcon { Icon get libraryIcon {
if (cache.checkTrackFavorite( if (cache.checkTrackFavorite(
Track.fromMediaItem(audioHandler.mediaItem.value!))) { Track.fromMediaItem(audioHandler.mediaItem.value!))) {
@ -491,6 +564,34 @@ class _PlaybackControlsState extends State<PlaybackControls> {
); );
} }
@override
Widget build(BuildContext context) => IconButton(
icon: libraryIcon,
iconSize: widget.size,
onPressed: () async {
if (cache.libraryTracks == null) cache.libraryTracks = [];
if (cache.checkTrackFavorite(
Track.fromMediaItem(audioHandler.mediaItem.value!))) {
//Remove from library
setState(() =>
cache.libraryTracks!.remove(audioHandler.mediaItem.value!.id));
await deezerAPI.removeFavorite(audioHandler.mediaItem.value!.id);
await cache.save();
} else {
//Add
setState(() =>
cache.libraryTracks!.add(audioHandler.mediaItem.value!.id));
await deezerAPI.addFavoriteTrack(audioHandler.mediaItem.value!.id);
await cache.save();
}
},
);
}
class PlaybackControls extends StatelessWidget {
final double size;
PlaybackControls(this.size, {Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
@ -499,46 +600,28 @@ class _PlaybackControlsState extends State<PlaybackControls> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
children: [ children: [
IconButton( ShuffleButton(iconSize: size * 0.75),
icon: Icon( PrevNextButton(size, prev: true),
Icons.sentiment_very_dissatisfied, if (settings.enableFilledPlayButton)
semanticLabel: "Dislike".i18n, Consumer<BackgroundProvider>(builder: (context, provider, _) {
), final color = Theme.of(context).brightness == Brightness.light
iconSize: widget.iconSize * 0.75, ? provider.dominantColor
onPressed: () async { : darken(provider.dominantColor);
await deezerAPI.dislikeTrack(audioHandler.mediaItem.value!.id); return PlayPauseButton(size * 2.25,
if (playerHelper.queueIndex < filled: true,
audioHandler.queue.value.length - 1) { color: color,
audioHandler.skipToNext(); iconColor: Color.lerp(
} (ThemeData.estimateBrightnessForColor(color) ==
}), Brightness.light
PrevNextButton(widget.iconSize, prev: true), ? Colors.black
PlayPauseButton(widget.iconSize * 1.25), : Colors.white),
PrevNextButton(widget.iconSize), color,
IconButton( 0.25));
icon: libraryIcon, })
iconSize: widget.iconSize * 0.75, else
onPressed: () async { PlayPauseButton(size * 1.25),
if (cache.libraryTracks == null) cache.libraryTracks = []; PrevNextButton(size),
RepeatButton(size * 0.75),
if (cache.checkTrackFavorite(
Track.fromMediaItem(audioHandler.mediaItem.value!))) {
//Remove from library
setState(() => cache.libraryTracks!
.remove(audioHandler.mediaItem.value!.id));
await deezerAPI
.removeFavorite(audioHandler.mediaItem.value!.id);
await cache.save();
} else {
//Add
setState(() =>
cache.libraryTracks!.add(audioHandler.mediaItem.value!.id));
await deezerAPI
.addFavoriteTrack(audioHandler.mediaItem.value!.id);
await cache.save();
}
},
)
], ],
), ),
); );
@ -551,18 +634,24 @@ class BigAlbumArt extends StatefulWidget {
} }
class _BigAlbumArtState extends State<BigAlbumArt> { class _BigAlbumArtState extends State<BigAlbumArt> {
PageController _pageController = PageController( final _pageController = PageController(
initialPage: playerHelper.queueIndex, initialPage: playerHelper.queueIndex,
viewportFraction: 1.0, viewportFraction: 1.0,
); );
StreamSubscription? _currentItemSub; StreamSubscription? _currentItemSub;
bool _animationLock = true; bool _animationLock = false;
bool _initiatedByUser = false;
@override @override
void initState() { void initState() {
_currentItemSub = audioHandler.mediaItem.listen((event) async { _currentItemSub = audioHandler.mediaItem.listen((event) async {
if (_initiatedByUser) {
_initiatedByUser = false;
return;
}
if (!_pageController.hasClients) return;
print('animating controller to page');
_animationLock = true; _animationLock = true;
// TODO: a lookup in the entire queue isn't that good, this can definitely be improved in some way
await _pageController.animateToPage(playerHelper.queueIndex, await _pageController.animateToPage(playerHelper.queueIndex,
duration: Duration(milliseconds: 300), curve: Curves.easeInOut); duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
_animationLock = false; _animationLock = false;
@ -589,8 +678,10 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
PageRouteBuilder( PageRouteBuilder(
opaque: false, // transparent background opaque: false, // transparent background
barrierDismissible: true, barrierDismissible: true,
pageBuilder: (context, _, __) { pageBuilder: (context, animation, __) {
return PhotoView( return FadeTransition(
opacity: animation,
child: PhotoView(
imageProvider: CachedNetworkImageProvider( imageProvider: CachedNetworkImageProvider(
audioHandler.mediaItem.value!.artUri.toString()), audioHandler.mediaItem.value!.artUri.toString()),
maxScale: 8.0, maxScale: 8.0,
@ -598,30 +689,39 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
heroAttributes: PhotoViewHeroAttributes( heroAttributes: PhotoViewHeroAttributes(
tag: audioHandler.mediaItem.value!.id), tag: audioHandler.mediaItem.value!.id),
backgroundDecoration: backgroundDecoration:
BoxDecoration(color: Color.fromARGB(0x90, 0, 0, 0))); BoxDecoration(color: Color.fromARGB(0x90, 0, 0, 0))),
);
})), })),
child: PageView( child: StreamBuilder<List<MediaItem>>(
stream: audioHandler.queue,
initialData: audioHandler.queue.valueOrNull,
builder: (context, snapshot) {
if (!snapshot.hasData)
return const Center(child: CircularProgressIndicator());
final queue = snapshot.data!;
return PageView(
controller: _pageController, controller: _pageController,
onPageChanged: (int index) { onPageChanged: (int index) {
if (pageViewLock) { if (pageViewLock || _animationLock) return;
pageViewLock = false; _initiatedByUser = true;
return;
}
if (_animationLock) return;
audioHandler.skipToQueueItem(index); audioHandler.skipToQueueItem(index);
}, },
children: List.generate( children: List.generate(
audioHandler.queue.value.length, queue.length,
(i) => Padding( (i) => Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Hero( child: Hero(
tag: audioHandler.queue.value[i].id, tag: queue[i].id,
child: ClipRRect(
borderRadius: BorderRadius.circular(5.0),
child: CachedImage( child: CachedImage(
url: audioHandler.queue.value[i].artUri.toString(), url: queue[i].artUri.toString(),
),
), ),
), ),
)), )),
), );
}),
); );
} }
} }
@ -664,7 +764,7 @@ class PlayerScreenTopRow extends StatelessWidget {
iconSize: this.iconSize ?? ScreenUtil().setSp(52), iconSize: this.iconSize ?? ScreenUtil().setSp(52),
splashRadius: this.iconSize ?? ScreenUtil().setWidth(52), splashRadius: this.iconSize ?? ScreenUtil().setWidth(52),
onPressed: () => Navigator.of(context) onPressed: () => Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => QueueScreen())), .pushRoute(builder: (context) => QueueScreen()),
), ),
], ],
); );
@ -776,116 +876,6 @@ class _SeekBarState extends State<SeekBar> {
} }
} }
class QueueScreen extends StatefulWidget {
@override
_QueueScreenState createState() => _QueueScreenState();
}
class _QueueScreenState extends State<QueueScreen> {
late StreamSubscription _queueSub;
static const _dismissibleBackground = DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Align(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.0),
child: Icon(Icons.delete)),
alignment: Alignment.centerLeft));
static const _dismissibleSecondaryBackground = DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Align(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.0),
child: Icon(Icons.delete)),
alignment: Alignment.centerRight));
/// Basically a simple list that keeps itself synchronized with [AudioHandler.queue],
/// so that the [ReorderableListView] is updated instanly (as it should be)
List<MediaItem> _queueCache = [];
@override
void initState() {
_queueCache = audioHandler.queue.value;
_queueSub = audioHandler.queue.listen((newQueue) {
print('got queue $newQueue');
// avoid rebuilding if the cache has got the right update
if (listEquals(_queueCache, newQueue)) {
print('avoiding rebuilding queue since they are the same');
return;
}
setState(() => _queueCache = newQueue);
});
super.initState();
}
@override
void dispose() {
_queueSub.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar(
'Queue'.i18n,
systemUiOverlayStyle: 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(
onReorder: (int oldIndex, int newIndex) {
if (oldIndex == playerHelper.queueIndex) return;
setState(() => _queueCache.reorder(oldIndex, newIndex));
playerHelper.reorder(oldIndex, newIndex);
},
itemCount: _queueCache.length,
itemBuilder: (BuildContext context, int i) {
Track track = Track.fromMediaItem(audioHandler.queue.value[i]);
return Dismissible(
key: Key(track.id),
background: _dismissibleBackground,
secondaryBackground: _dismissibleSecondaryBackground,
onDismissed: (_) {
audioHandler.removeQueueItemAt(i);
setState(() => _queueCache.removeAt(i));
},
child: TrackTile(
track,
onTap: () {
pageViewLock = true;
audioHandler
.skipToQueueItem(i)
.then((value) => Navigator.of(context).pop());
},
key: Key(track.id),
),
);
},
),
),
);
}
}
class BottomBarControls extends StatelessWidget { class BottomBarControls extends StatelessWidget {
final double size; final double size;
const BottomBarControls({Key? key, required this.size}) : super(key: key); const BottomBarControls({Key? key, required this.size}) : super(key: key);
@ -902,34 +892,51 @@ class BottomBarControls extends StatelessWidget {
size: size, size: size,
semanticLabel: "Lyrics".i18n, semanticLabel: "Lyrics".i18n,
), ),
onPressed: () async { onPressed: () => _pushLyrics(context)),
await Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => LyricsScreen()));
updateColor();
},
),
IconButton( IconButton(
iconSize: size,
icon: Icon( icon: Icon(
Icons.file_download, Icons.sentiment_very_dissatisfied,
semanticLabel: "Download".i18n, semanticLabel: "Dislike".i18n,
), ),
iconSize: size * 0.85,
onPressed: () async { onPressed: () async {
Track t = Track.fromMediaItem(audioHandler.mediaItem.value!); await deezerAPI.dislikeTrack(audioHandler.mediaItem.value!.id);
if (await downloadManager.addOfflineTrack(t, if (playerHelper.queueIndex <
private: false, context: context, isSingleton: true) != audioHandler.queue.value.length - 1) {
false) audioHandler.skipToNext();
Fluttertoast.showToast( }
msg: 'Downloads added!'.i18n, }),
gravity: ToastGravity.BOTTOM, // IconButton(
toastLength: Toast.LENGTH_SHORT); // iconSize: size,
}, // icon: Icon(
), // Icons.file_download,
// semanticLabel: "Download".i18n,
// ),
// onPressed: () async {
// Track t = Track.fromMediaItem(audioHandler.mediaItem.value!);
// if (await downloadManager.addOfflineTrack(t,
// private: false, context: context, isSingleton: true) !=
// false)
// Fluttertoast.showToast(
// msg: 'Downloads added!'.i18n,
// gravity: ToastGravity.BOTTOM,
// toastLength: Toast.LENGTH_SHORT);
// },
// ),
QualityInfoWidget(), QualityInfoWidget(),
RepeatButton(size), FavoriteButton(size: size * 0.85),
PlayerMenuButton(size: size) PlayerMenuButton(size: size)
], ],
); );
} }
void _pushLyrics(BuildContext context) {
final builder = (ctx) => ChangeNotifierProvider<BackgroundProvider>.value(
value: Provider.of<BackgroundProvider>(context), child: LyricsScreen());
if (settings.playerBackgroundOnLyrics) {
Navigator.of(context).push(FadePageRoute(builder: builder));
return;
}
Navigator.of(context).pushRoute(builder: builder);
}
} }

141
lib/ui/queue_screen.dart Normal file
View file

@ -0,0 +1,141 @@
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/player_screen.dart';
import 'package:freezer/ui/tiles.dart';
class QueueScreen extends StatefulWidget {
@override
_QueueScreenState createState() => _QueueScreenState();
}
class _QueueScreenState extends State<QueueScreen> {
late StreamSubscription _queueSub;
static const _dismissibleBackground = DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Align(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.0),
child: Icon(Icons.delete)),
alignment: Alignment.centerLeft));
static const _dismissibleSecondaryBackground = DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Align(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.0),
child: Icon(Icons.delete)),
alignment: Alignment.centerRight));
/// Basically a simple list that keeps itself synchronized with [AudioHandler.queue],
/// so that the [ReorderableListView] is updated instanly (as it should be)
List<MediaItem> _queueCache = [];
@override
void initState() {
_queueCache = List.from(audioHandler.queue.value); // avoid shadow-copying
_queueSub = audioHandler.queue.listen((newQueue) {
print('got new queue!');
// avoid rebuilding if the cache has got the right update
// if (listEquals(_queueCache, newQueue)) {
// print('avoiding rebuilding queue since they are the same');
// return;
// }
setState(() => _queueCache = List.from(newQueue));
});
super.initState();
}
@override
void dispose() {
_queueSub.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar(
'Queue'.i18n,
systemUiOverlayStyle: 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(
onReorder: (int oldIndex, int newIndex) {
if (oldIndex == playerHelper.queueIndex) return;
setState(() => _queueCache..reorder(oldIndex, newIndex));
playerHelper.reorder(oldIndex, newIndex);
},
itemCount: _queueCache.length,
itemBuilder: (BuildContext context, int i) {
Track track = Track.fromMediaItem(audioHandler.queue.value[i]);
return Dismissible(
key: ValueKey<String>(track.id),
background: _dismissibleBackground,
secondaryBackground: _dismissibleSecondaryBackground,
onDismissed: (_) {
audioHandler.removeQueueItemAt(i);
setState(() => _queueCache.removeAt(i));
},
confirmDismiss: (_) {
if (i == playerHelper.queueIndex)
return audioHandler.skipToNext().then((value) => true);
return Future.value(true);
// final completer = Completer<bool>();
// ScaffoldMessenger.of(context).clearSnackBars();
// ScaffoldMessenger.of(context)
// .showSnackBar(SnackBar(
// behavior: SnackBarBehavior.floating,
// content: Text('Song deleted from queue'),
// action: SnackBarAction(
// label: 'UNDO',
// onPressed: () => completer.complete(false))))
// .closed
// .then((value) {
// if (value == SnackBarClosedReason.action) return;
// completer.complete(true);
// });
// return completer.future;
},
child: TrackTile(
track,
onTap: () {
pageViewLock = true;
audioHandler.skipToQueueItem(i).then((value) {
Navigator.of(context).pop();
pageViewLock = false;
});
},
key: Key(track.id),
),
);
},
),
),
);
}
}

View file

@ -75,11 +75,11 @@ class _SearchScreenState extends State<SearchScreen> {
return; return;
} }
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => SearchResultsScreen( builder: (context) => SearchResultsScreen(
_query, _query,
offline: _offline, offline: _offline,
))); ));
} }
@override @override
@ -253,14 +253,14 @@ class _SearchScreenState extends State<SearchScreen> {
color: Color(0xff7c42bb), color: Color(0xff7c42bb),
text: 'Shows'.i18n, text: 'Shows'.i18n,
icon: Icon(FontAwesome5.podcast), icon: Icon(FontAwesome5.podcast),
onTap: () => Navigator.of(context).push(MaterialPageRoute( onTap: () => Navigator.of(context).pushRoute(
builder: (context) => Scaffold( builder: (context) => Scaffold(
appBar: FreezerAppBar('Shows'.i18n), appBar: FreezerAppBar('Shows'.i18n),
body: SingleChildScrollView( body: SingleChildScrollView(
child: HomePageScreen( child: HomePageScreen(
channel: DeezerChannel(target: 'shows'))), channel: DeezerChannel(target: 'shows'))),
), ),
)), ),
) )
], ],
), ),
@ -272,7 +272,7 @@ class _SearchScreenState extends State<SearchScreen> {
color: Color(0xffff555d), color: Color(0xffff555d),
icon: Icon(FontAwesome5.chart_line), icon: Icon(FontAwesome5.chart_line),
text: 'Charts'.i18n, text: 'Charts'.i18n,
onTap: () => Navigator.of(context).push(MaterialPageRoute( onTap: () => Navigator.of(context).pushRoute(
builder: (context) => Scaffold( builder: (context) => Scaffold(
appBar: FreezerAppBar('Charts'.i18n), appBar: FreezerAppBar('Charts'.i18n),
body: SingleChildScrollView( body: SingleChildScrollView(
@ -280,13 +280,13 @@ class _SearchScreenState extends State<SearchScreen> {
channel: channel:
DeezerChannel(target: 'channels/charts'))), DeezerChannel(target: 'channels/charts'))),
), ),
)), ),
), ),
SearchBrowseCard( SearchBrowseCard(
color: Color(0xff2c4ea7), color: Color(0xff2c4ea7),
text: 'Browse'.i18n, text: 'Browse'.i18n,
icon: Image.asset('assets/browse_icon.png', width: 26.0), icon: Image.asset('assets/browse_icon.png', width: 26.0),
onTap: () => Navigator.of(context).push(MaterialPageRoute( onTap: () => Navigator.of(context).pushRoute(
builder: (context) => Scaffold( builder: (context) => Scaffold(
appBar: FreezerAppBar('Browse'.i18n), appBar: FreezerAppBar('Browse'.i18n),
body: SingleChildScrollView( body: SingleChildScrollView(
@ -294,7 +294,7 @@ class _SearchScreenState extends State<SearchScreen> {
channel: channel:
DeezerChannel(target: 'channels/explore'))), DeezerChannel(target: 'channels/explore'))),
), ),
)), ),
) )
], ],
) )
@ -337,8 +337,8 @@ class _SearchScreenState extends State<SearchScreen> {
return AlbumTile( return AlbumTile(
data, data,
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context)
builder: (context) => AlbumDetails(data))); .pushRoute(builder: (context) => AlbumDetails(data));
}, },
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
@ -350,8 +350,8 @@ class _SearchScreenState extends State<SearchScreen> {
return ArtistHorizontalTile( return ArtistHorizontalTile(
data, data,
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context)
builder: (context) => ArtistDetails(data))); .pushRoute(builder: (context) => ArtistDetails(data));
}, },
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
@ -363,8 +363,8 @@ class _SearchScreenState extends State<SearchScreen> {
return PlaylistTile( return PlaylistTile(
data, data,
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => PlaylistDetails(data))); builder: (context) => PlaylistDetails(data));
}, },
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
@ -535,13 +535,13 @@ class SearchResultsScreen extends StatelessWidget {
ListTile( ListTile(
title: Text('Show all tracks'.i18n), title: Text('Show all tracks'.i18n),
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => TrackListScreen( builder: (context) => TrackListScreen(
results.tracks, results.tracks,
QueueSource( QueueSource(
id: query, id: query,
source: 'search', source: 'search',
text: 'Search'.i18n)))); text: 'Search'.i18n)));
}, },
), ),
FreezerDivider() FreezerDivider()
@ -577,16 +577,16 @@ class SearchResultsScreen extends StatelessWidget {
}, },
onTap: () { onTap: () {
cache.addToSearchHistory(a); cache.addToSearchHistory(a);
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context)
builder: (context) => AlbumDetails(a))); .pushRoute(builder: (context) => AlbumDetails(a));
}, },
); );
}), }),
ListTile( ListTile(
title: Text('Show all albums'.i18n), title: Text('Show all albums'.i18n),
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => AlbumListScreen(results.albums))); builder: (context) => AlbumListScreen(results.albums));
}, },
), ),
FreezerDivider() FreezerDivider()
@ -617,8 +617,8 @@ class SearchResultsScreen extends StatelessWidget {
a, a,
onTap: () { onTap: () {
cache.addToSearchHistory(a); cache.addToSearchHistory(a);
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => ArtistDetails(a))); builder: (context) => ArtistDetails(a));
}, },
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
@ -656,8 +656,8 @@ class SearchResultsScreen extends StatelessWidget {
p, p,
onTap: () { onTap: () {
cache.addToSearchHistory(p); cache.addToSearchHistory(p);
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context)
builder: (context) => PlaylistDetails(p))); .pushRoute(builder: (context) => PlaylistDetails(p));
}, },
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
@ -668,9 +668,9 @@ class SearchResultsScreen extends StatelessWidget {
ListTile( ListTile(
title: Text('Show all playlists'.i18n), title: Text('Show all playlists'.i18n),
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => builder: (context) =>
SearchResultPlaylists(results.playlists))); SearchResultPlaylists(results.playlists));
}, },
), ),
FreezerDivider() FreezerDivider()
@ -701,16 +701,16 @@ class SearchResultsScreen extends StatelessWidget {
return ShowTile( return ShowTile(
s, s,
onTap: () async { onTap: () async {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context)
builder: (context) => ShowScreen(s))); .pushRoute(builder: (context) => ShowScreen(s));
}, },
); );
}), }),
ListTile( ListTile(
title: Text('Show all shows'.i18n), title: Text('Show all shows'.i18n),
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => ShowListScreen(results.shows))); builder: (context) => ShowListScreen(results.shows));
}, },
), ),
FreezerDivider() FreezerDivider()
@ -762,9 +762,9 @@ class SearchResultsScreen extends StatelessWidget {
ListTile( ListTile(
title: Text('Show all episodes'.i18n), title: Text('Show all episodes'.i18n),
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).pushRoute(
builder: (context) => builder: (context) =>
EpisodeListScreen(results.episodes))); EpisodeListScreen(results.episodes));
}) })
]; ];
} }
@ -816,15 +816,15 @@ class TrackListScreen extends StatelessWidget {
body: ListView.builder( body: ListView.builder(
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(
t, t,
onTap: () { onTap: () {
playerHelper.playFromTrackList(tracks!, t!.id, queueSource); playerHelper.playFromTrackList(tracks!, t.id, queueSource);
}, },
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
m.defaultTrackMenu(t!); m.defaultTrackMenu(t);
}, },
); );
}, },
@ -849,8 +849,8 @@ class AlbumListScreen extends StatelessWidget {
return AlbumTile( return AlbumTile(
a, a,
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context)
MaterialPageRoute(builder: (context) => AlbumDetails(a))); .pushRoute(builder: (context) => AlbumDetails(a));
}, },
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
@ -878,8 +878,8 @@ class SearchResultPlaylists extends StatelessWidget {
return PlaylistTile( return PlaylistTile(
p, p,
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context)
MaterialPageRoute(builder: (context) => PlaylistDetails(p))); .pushRoute(builder: (context) => PlaylistDetails(p));
}, },
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);

View file

@ -7,6 +7,7 @@ import 'package:flutter_material_color_picker/flutter_material_color_picker.dart
import 'package:fluttericon/font_awesome5_icons.dart'; import 'package:fluttericon/font_awesome5_icons.dart';
import 'package:fluttericon/web_symbols_icons.dart'; import 'package:fluttericon/web_symbols_icons.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:freezer/api/definitions.dart';
import 'package:package_info/package_info.dart'; import 'package:package_info/package_info.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:scrobblenaut/scrobblenaut.dart'; import 'package:scrobblenaut/scrobblenaut.dart';
@ -39,33 +40,33 @@ class _SettingsScreenState extends State<SettingsScreen> {
ListTile( ListTile(
title: Text('General'.i18n), title: Text('General'.i18n),
leading: LeadingIcon(Icons.settings, color: Color(0xffeca704)), leading: LeadingIcon(Icons.settings, color: Color(0xffeca704)),
onTap: () => Navigator.of(context).push( onTap: () => Navigator.of(context)
MaterialPageRoute(builder: (context) => GeneralSettings())), .pushRoute(builder: (context) => GeneralSettings()),
), ),
ListTile( ListTile(
title: Text('Download Settings'.i18n), title: Text('Download Settings'.i18n),
leading: leading:
LeadingIcon(Icons.cloud_download, color: Color(0xffbe3266)), LeadingIcon(Icons.cloud_download, color: Color(0xffbe3266)),
onTap: () => Navigator.of(context).push( onTap: () => Navigator.of(context)
MaterialPageRoute(builder: (context) => DownloadsSettings())), .pushRoute(builder: (context) => DownloadsSettings()),
), ),
ListTile( ListTile(
title: Text('Appearance'.i18n), title: Text('Appearance'.i18n),
leading: LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)), leading: LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)),
onTap: () => Navigator.push(context, onTap: () => Navigator.of(context)
MaterialPageRoute(builder: (context) => AppearanceSettings())), .pushRoute(builder: (context) => AppearanceSettings()),
), ),
ListTile( ListTile(
title: Text('Quality'.i18n), title: Text('Quality'.i18n),
leading: LeadingIcon(Icons.high_quality, color: Color(0xff384697)), leading: LeadingIcon(Icons.high_quality, color: Color(0xff384697)),
onTap: () => Navigator.push(context, onTap: () => Navigator.of(context)
MaterialPageRoute(builder: (context) => QualitySettings())), .pushRoute(builder: (context) => QualitySettings()),
), ),
ListTile( ListTile(
title: Text('Deezer'.i18n), title: Text('Deezer'.i18n),
leading: LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)), leading: LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)),
onTap: () => Navigator.push(context, onTap: () => Navigator.of(context)
MaterialPageRoute(builder: (context) => DeezerSettings())), .pushRoute(builder: (context) => DeezerSettings()),
), ),
//Language select //Language select
ListTile( ListTile(
@ -111,14 +112,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
ListTile( ListTile(
title: Text('Updates'.i18n), title: Text('Updates'.i18n),
leading: LeadingIcon(Icons.update, color: Color(0xff2ba766)), leading: LeadingIcon(Icons.update, color: Color(0xff2ba766)),
onTap: () => Navigator.push(context, onTap: () => Navigator.of(context)
MaterialPageRoute(builder: (context) => UpdaterScreen())), .pushRoute(builder: (context) => UpdaterScreen()),
), ),
ListTile( ListTile(
title: Text('About'.i18n), title: Text('About'.i18n),
leading: LeadingIcon(Icons.info, color: Colors.grey), leading: LeadingIcon(Icons.info, color: Colors.grey),
onTap: () => Navigator.push(context, onTap: () => Navigator.of(context)
MaterialPageRoute(builder: (context) => CreditsScreen())), .pushRoute(builder: (context) => CreditsScreen()),
), ),
], ],
), ),
@ -143,7 +144,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
ListTile( ListTile(
title: Text('Theme'.i18n), title: Text('Theme'.i18n),
subtitle: Text('Currently'.i18n + subtitle: Text('Currently'.i18n +
': ${settings.theme.toString().split('.').last}'), ': ${settings.theme.toString().split('.').lastItem}'),
leading: Icon(Icons.color_lens), leading: Icon(Icons.color_lens),
onTap: () { onTap: () {
showDialog( showDialog(
@ -195,7 +196,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
), ),
SwitchListTile( SwitchListTile(
title: Text('Use system theme'.i18n), title: Text('Use system theme'.i18n),
value: settings.useSystemTheme!, value: settings.useSystemTheme,
onChanged: (bool v) async { onChanged: (bool v) async {
settings.useSystemTheme = v; settings.useSystemTheme = v;
@ -206,7 +207,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
ListTile( ListTile(
title: Text('Font'.i18n), title: Text('Font'.i18n),
leading: Icon(Icons.font_download), leading: Icon(Icons.font_download),
subtitle: Text(settings.font!), subtitle: Text(settings.font),
onTap: () { onTap: () {
showDialog( showDialog(
context: context, context: context,
@ -217,7 +218,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
SwitchListTile( SwitchListTile(
title: Text('Player gradient background'.i18n), title: Text('Player gradient background'.i18n),
secondary: Icon(Icons.colorize), secondary: Icon(Icons.colorize),
value: settings.colorGradientBackground!, value: settings.colorGradientBackground,
onChanged: (bool v) async { onChanged: (bool v) async {
setState(() => settings.colorGradientBackground = v); setState(() => settings.colorGradientBackground = v);
await settings.save(); await settings.save();
@ -227,19 +228,97 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
title: Text('Blur player background'.i18n), title: Text('Blur player background'.i18n),
subtitle: Text('Might have impact on performance'.i18n), subtitle: Text('Might have impact on performance'.i18n),
secondary: Icon(Icons.blur_on), secondary: Icon(Icons.blur_on),
value: settings.blurPlayerBackground!, value: settings.blurPlayerBackground,
onChanged: (bool v) async { onChanged: (bool v) async {
setState(() => settings.blurPlayerBackground = v); setState(() => settings.blurPlayerBackground = v);
await settings.save(); await settings.save();
}, },
), ),
SwitchListTile(
title: Text('Use player background on lyrics page'),
value: settings.playerBackgroundOnLyrics,
secondary: Icon(Icons.wallpaper),
onChanged: settings.blurPlayerBackground ||
settings.colorGradientBackground
? (bool v) {
setState(() => settings.playerBackgroundOnLyrics = v);
settings.save();
}
: null),
ListTile(
title: Text('Screens style'),
subtitle:
Text('Style of the transition between screens within the app'),
leading: Icon(Icons.auto_awesome_motion),
onTap: () => showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: Text('Select screens style'),
children: <Widget>[
SimpleDialogOption(
child: Text('Blur slide (might be laggy!)'),
onPressed: () {
settings.navigatorRouteType =
NavigatorRouteType.blur_slide;
settings.save();
Navigator.of(context).pop();
},
),
SimpleDialogOption(
child: Text('Fade'),
onPressed: () {
settings.navigatorRouteType = NavigatorRouteType.fade;
settings.save();
Navigator.of(context).pop();
},
),
SimpleDialogOption(
child: Text('Fade with blur (might be laggy!)'),
onPressed: () {
settings.navigatorRouteType =
NavigatorRouteType.fade_blur;
settings.save();
Navigator.of(context).pop();
},
),
SimpleDialogOption(
child: Text('Material (default)'),
onPressed: () {
settings.navigatorRouteType =
NavigatorRouteType.material;
settings.save();
Navigator.of(context).pop();
},
),
SimpleDialogOption(
child: Text('Cupertino (iOS)'),
onPressed: () {
settings.navigatorRouteType =
NavigatorRouteType.cupertino;
settings.save();
Navigator.of(context).pop();
},
),
],
);
}),
),
SwitchListTile(
title: Text('Enable filled play button'),
secondary: Icon(Icons.play_circle),
value: settings.enableFilledPlayButton,
onChanged: (bool v) {
setState(() => settings.enableFilledPlayButton = v);
settings.save();
}),
SwitchListTile( SwitchListTile(
title: Text('Visualizer'.i18n), title: Text('Visualizer'.i18n),
subtitle: Text( subtitle: Text(
'Show visualizers on lyrics page. WARNING: Requires microphone permission!' 'Show visualizers on lyrics page. WARNING: Requires microphone permission!'
.i18n), .i18n),
secondary: Icon(Icons.equalizer), secondary: Icon(Icons.equalizer),
value: settings.lyricsVisualizer!, value: settings.lyricsVisualizer,
onChanged: null, // TODO: visualizer onChanged: null, // TODO: visualizer
//(bool v) async { //(bool v) async {
// if (await Permission.microphone.request().isGranted) { // if (await Permission.microphone.request().isGranted) {
@ -454,7 +533,7 @@ class QualityPicker extends StatefulWidget {
} }
class _QualityPickerState extends State<QualityPicker> { class _QualityPickerState extends State<QualityPicker> {
AudioQuality? _quality; late AudioQuality _quality;
@override @override
void initState() { void initState() {
@ -481,7 +560,7 @@ class _QualityPickerState extends State<QualityPicker> {
} }
//Update quality in settings //Update quality in settings
void _updateQuality(AudioQuality? q) async { void _updateQuality(AudioQuality q) async {
setState(() { setState(() {
_quality = q; _quality = q;
}); });
@ -786,7 +865,7 @@ class DownloadsSettings extends StatefulWidget {
} }
class _DownloadsSettingsState extends State<DownloadsSettings> { class _DownloadsSettingsState extends State<DownloadsSettings> {
double _downloadThreads = settings.downloadThreads!.toDouble(); double _downloadThreads = settings.downloadThreads.toDouble();
TextEditingController _artistSeparatorController = TextEditingController _artistSeparatorController =
TextEditingController(text: settings.artistSeparator); TextEditingController(text: settings.artistSeparator);
@ -809,14 +888,14 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
settings.save(); settings.save();
}); });
//Navigate //Navigate
// Navigator.of(context).push(MaterialPageRoute( // Navigator.of(context).pushRoute(
// builder: (context) => DirectoryPicker( // builder: (context) => DirectoryPicker(
// settings.downloadPath, // settings.downloadPath,
// onSelect: (String p) async { // onSelect: (String p) async {
// setState(() => settings.downloadPath = p); // setState(() => settings.downloadPath = p);
// await settings.save(); // await settings.save();
// }, // },
// ))); // ));
}, },
), ),
ListTile( ListTile(
@ -871,7 +950,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
_downloadThreads = val; _downloadThreads = val;
setState(() { setState(() {
settings.downloadThreads = _downloadThreads.round(); settings.downloadThreads = _downloadThreads.round();
_downloadThreads = settings.downloadThreads!.toDouble(); _downloadThreads = settings.downloadThreads.toDouble();
}); });
await settings.save(); await settings.save();
@ -902,12 +981,12 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
ListTile( ListTile(
title: Text('Tags'.i18n), title: Text('Tags'.i18n),
leading: Icon(Icons.label), leading: Icon(Icons.label),
onTap: () => Navigator.of(context).push( onTap: () => Navigator.of(context)
MaterialPageRoute(builder: (context) => TagSelectionScreen())), .pushRoute(builder: (context) => TagSelectionScreen()),
), ),
SwitchListTile( SwitchListTile(
title: Text('Create folders for artist'.i18n), title: Text('Create folders for artist'.i18n),
value: settings.artistFolder!, value: settings.artistFolder,
onChanged: (v) { onChanged: (v) {
setState(() => settings.artistFolder = v); setState(() => settings.artistFolder = v);
settings.save(); settings.save();
@ -916,7 +995,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
), ),
SwitchListTile( SwitchListTile(
title: Text('Create folders for albums'.i18n), title: Text('Create folders for albums'.i18n),
value: settings.albumFolder!, value: settings.albumFolder,
onChanged: (v) { onChanged: (v) {
setState(() => settings.albumFolder = v); setState(() => settings.albumFolder = v);
settings.save(); settings.save();
@ -924,7 +1003,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
secondary: Icon(Icons.folder)), secondary: Icon(Icons.folder)),
SwitchListTile( SwitchListTile(
title: Text('Create folder for playlist'.i18n), title: Text('Create folder for playlist'.i18n),
value: settings.playlistFolder!, value: settings.playlistFolder,
onChanged: (v) { onChanged: (v) {
setState(() => settings.playlistFolder = v); setState(() => settings.playlistFolder = v);
settings.save(); settings.save();
@ -933,7 +1012,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
FreezerDivider(), FreezerDivider(),
SwitchListTile( SwitchListTile(
title: Text('Separate albums by discs'.i18n), title: Text('Separate albums by discs'.i18n),
value: settings.albumDiscFolder!, value: settings.albumDiscFolder,
onChanged: (v) { onChanged: (v) {
setState(() => settings.albumDiscFolder = v); setState(() => settings.albumDiscFolder = v);
settings.save(); settings.save();
@ -941,7 +1020,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
secondary: Icon(Icons.album)), secondary: Icon(Icons.album)),
SwitchListTile( SwitchListTile(
title: Text('Overwrite already downloaded files'.i18n), title: Text('Overwrite already downloaded files'.i18n),
value: settings.overwriteDownload!, value: settings.overwriteDownload,
onChanged: (v) { onChanged: (v) {
setState(() => settings.overwriteDownload = v); setState(() => settings.overwriteDownload = v);
settings.save(); settings.save();
@ -949,7 +1028,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
secondary: Icon(Icons.delete)), secondary: Icon(Icons.delete)),
SwitchListTile( SwitchListTile(
title: Text('Download .LRC lyrics'.i18n), title: Text('Download .LRC lyrics'.i18n),
value: settings.downloadLyrics!, value: settings.downloadLyrics,
onChanged: (v) { onChanged: (v) {
setState(() => settings.downloadLyrics = v); setState(() => settings.downloadLyrics = v);
settings.save(); settings.save();
@ -958,7 +1037,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
FreezerDivider(), FreezerDivider(),
SwitchListTile( SwitchListTile(
title: Text('Save cover file for every track'.i18n), title: Text('Save cover file for every track'.i18n),
value: settings.trackCover!, value: settings.trackCover,
onChanged: (v) { onChanged: (v) {
setState(() => settings.trackCover = v); setState(() => settings.trackCover = v);
settings.save(); settings.save();
@ -966,7 +1045,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
secondary: Icon(Icons.image)), secondary: Icon(Icons.image)),
SwitchListTile( SwitchListTile(
title: Text('Save album cover'.i18n), title: Text('Save album cover'.i18n),
value: settings.albumCover!, value: settings.albumCover,
onChanged: (v) { onChanged: (v) {
setState(() => settings.albumCover = v); setState(() => settings.albumCover = v);
settings.save(); settings.save();
@ -990,6 +1069,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
)) ))
.toList(), .toList(),
onChanged: (int? n) async { onChanged: (int? n) async {
if (n == null) return;
setState(() { setState(() {
settings.albumArtResolution = n; settings.albumArtResolution = n;
}); });
@ -1000,7 +1080,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
title: Text('Create .nomedia files'.i18n), title: Text('Create .nomedia files'.i18n),
subtitle: subtitle:
Text('To prevent gallery being filled with album art'.i18n), Text('To prevent gallery being filled with album art'.i18n),
value: settings.nomediaFiles!, value: settings.nomediaFiles,
onChanged: (v) { onChanged: (v) {
setState(() => settings.nomediaFiles = v); setState(() => settings.nomediaFiles = v);
settings.save(); settings.save();
@ -1024,8 +1104,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
ListTile( ListTile(
title: Text('Download Log'.i18n), title: Text('Download Log'.i18n),
leading: Icon(Icons.sticky_note_2), leading: Icon(Icons.sticky_note_2),
onTap: () => Navigator.of(context).push( onTap: () => Navigator.of(context)
MaterialPageRoute(builder: (context) => DownloadLogViewer())), .pushRoute(builder: (context) => DownloadLogViewer()),
) )
], ],
), ),
@ -1074,13 +1154,13 @@ class _TagSelectionScreenState extends State<TagSelectionScreen> {
(i) => ListTile( (i) => ListTile(
title: Text(tags[i].title), title: Text(tags[i].title),
leading: Switch( leading: Switch(
value: settings.tags!.contains(tags[i].value), value: settings.tags.contains(tags[i].value),
onChanged: (v) async { onChanged: (v) async {
//Update //Update
if (v) if (v)
settings.tags!.add(tags[i].value); settings.tags.add(tags[i].value);
else else
settings.tags!.remove(tags[i].value); settings.tags.remove(tags[i].value);
setState(() {}); setState(() {});
await settings.save(); await settings.save();
}, },
@ -1116,7 +1196,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
deezerAPI.authorize()!.then((v) { deezerAPI.authorize().then((v) {
if (v) { if (v) {
setState(() => settings.offlineMode = false); setState(() => settings.offlineMode = false);
} else { } else {
@ -1131,11 +1211,8 @@ class _GeneralSettingsState extends State<GeneralSettings> {
}); });
return AlertDialog( return AlertDialog(
title: Text('Logging in...'.i18n), title: Text('Logging in...'.i18n),
content: Row( content:
mainAxisSize: MainAxisSize.max, const Center(child: CircularProgressIndicator()));
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[CircularProgressIndicator()],
));
}); });
}, },
), ),
@ -1145,7 +1222,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
'Might enable some equalizer apps to work. Requires restart of Freezer' 'Might enable some equalizer apps to work. Requires restart of Freezer'
.i18n), .i18n),
secondary: Icon(Icons.equalizer), secondary: Icon(Icons.equalizer),
value: settings.enableEqualizer!, value: settings.enableEqualizer,
onChanged: (v) async { onChanged: (v) async {
setState(() => settings.enableEqualizer = v); setState(() => settings.enableEqualizer = v);
settings.save(); settings.save();
@ -1155,7 +1232,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
title: Text('Ignore interruptions'.i18n), title: Text('Ignore interruptions'.i18n),
subtitle: Text('Requires app restart to apply!'.i18n), subtitle: Text('Requires app restart to apply!'.i18n),
secondary: Icon(Icons.not_interested), secondary: Icon(Icons.not_interested),
value: settings.ignoreInterruptions!, value: settings.ignoreInterruptions,
onChanged: (bool v) async { onChanged: (bool v) async {
setState(() => settings.ignoreInterruptions = v); setState(() => settings.ignoreInterruptions = v);
await settings.save(); await settings.save();

View file

@ -12,7 +12,7 @@ import 'cached_image.dart';
import 'dart:async'; import 'dart:async';
class TrackTile extends StatefulWidget { class TrackTile extends StatefulWidget {
final Track? track; final Track track;
final void Function()? onTap; final void Function()? onTap;
final void Function()? onHold; final void Function()? onHold;
final Widget? trailing; final Widget? trailing;
@ -25,7 +25,7 @@ class TrackTile extends StatefulWidget {
} }
class _TrackTileState extends State<TrackTile> { class _TrackTileState extends State<TrackTile> {
StreamSubscription? _subscription; late StreamSubscription _subscription;
bool _isOffline = false; bool _isOffline = false;
bool _isHighlighted = false; bool _isHighlighted = false;
@ -34,7 +34,7 @@ class _TrackTileState extends State<TrackTile> {
//Listen to media item changes, update text color if currently playing //Listen to media item changes, update text color if currently playing
_subscription = audioHandler.mediaItem.listen((mediaItem) { _subscription = audioHandler.mediaItem.listen((mediaItem) {
if (mediaItem == null) return; if (mediaItem == null) return;
if (mediaItem.id == widget.track?.id) if (mediaItem.id == widget.track.id && !_isHighlighted)
setState(() => _isHighlighted = true); setState(() => _isHighlighted = true);
else if (_isHighlighted) setState(() => _isHighlighted = false); else if (_isHighlighted) setState(() => _isHighlighted = false);
}); });
@ -48,7 +48,7 @@ class _TrackTileState extends State<TrackTile> {
@override @override
void dispose() { void dispose() {
_subscription?.cancel(); _subscription.cancel();
super.dispose(); super.dispose();
} }
@ -56,18 +56,18 @@ class _TrackTileState extends State<TrackTile> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
title: Text( title: Text(
widget.track!.title!, widget.track.title!,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.clip, overflow: TextOverflow.clip,
style: TextStyle( style: TextStyle(
color: _isHighlighted ? Theme.of(context).primaryColor : null), color: _isHighlighted ? Theme.of(context).primaryColor : null),
), ),
subtitle: Text( subtitle: Text(
widget.track!.artistString, widget.track.artistString,
maxLines: 1, maxLines: 1,
), ),
leading: CachedImage( leading: CachedImage(
url: widget.track!.albumArt!.thumb!, url: widget.track.albumArt!.thumb!,
width: 48, width: 48,
), ),
onTap: widget.onTap, onTap: widget.onTap,
@ -84,7 +84,7 @@ class _TrackTileState extends State<TrackTile> {
size: 12.0, size: 12.0,
), ),
), ),
if (widget.track!.explicit ?? false) if (widget.track.explicit ?? false)
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 2.0), padding: EdgeInsets.symmetric(horizontal: 2.0),
child: Text( child: Text(
@ -95,7 +95,7 @@ class _TrackTileState extends State<TrackTile> {
Container( Container(
width: 42.0, width: 42.0,
child: Text( child: Text(
widget.track!.durationString, widget.track.durationString,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
@ -108,8 +108,8 @@ class _TrackTileState extends State<TrackTile> {
class AlbumTile extends StatelessWidget { class AlbumTile extends StatelessWidget {
final Album? album; final Album? album;
final Function? onTap; final void Function()? onTap;
final Function? onHold; final void Function()? onHold;
final Widget? trailing; final Widget? trailing;
AlbumTile(this.album, {this.onTap, this.onHold, this.trailing}); AlbumTile(this.album, {this.onTap, this.onHold, this.trailing});
@ -129,8 +129,8 @@ class AlbumTile extends StatelessWidget {
url: album!.art!.thumb, url: album!.art!.thumb,
width: 48, width: 48,
), ),
onTap: onTap as void Function()?, onTap: onTap,
onLongPress: onHold as void Function()?, onLongPress: onHold,
trailing: trailing, trailing: trailing,
); );
} }
@ -138,8 +138,8 @@ class AlbumTile extends StatelessWidget {
class ArtistTile extends StatelessWidget { class ArtistTile extends StatelessWidget {
final Artist? artist; final Artist? artist;
final Function? onTap; final void Function()? onTap;
final Function? onHold; final void Function()? onHold;
ArtistTile(this.artist, {this.onTap, this.onHold}); ArtistTile(this.artist, {this.onTap, this.onHold});
@ -147,45 +147,33 @@ class ArtistTile extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
width: 150, width: 150,
child: Container(
child: InkWell( child: InkWell(
onTap: onTap as void Function()?, onTap: onTap,
onLongPress: onHold as void Function()?, onLongPress: onHold,
child: Column( child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
mainAxisSize: MainAxisSize.min, const SizedBox(height: 4),
children: <Widget>[
Container(
height: 4,
),
CachedImage( CachedImage(
url: artist!.picture!.thumb, url: artist!.picture!.thumb,
circular: true, circular: true,
width: 100, width: 100,
), ),
Container( const SizedBox(height: 8),
height: 8,
),
Text( Text(
artist!.name!, artist!.name!,
maxLines: 1, maxLines: 1,
textAlign: TextAlign.center, textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 14.0), style: const TextStyle(fontSize: 14.0),
), ),
Container( const SizedBox(height: 4),
height: 4, ])));
),
],
),
),
));
} }
} }
class PlaylistTile extends StatelessWidget { class PlaylistTile extends StatelessWidget {
final Playlist? playlist; final Playlist? playlist;
final Function? onTap; final void Function()? onTap;
final Function? onHold; final void Function()? onHold;
final Widget? trailing; final Widget? trailing;
PlaylistTile(this.playlist, {this.onHold, this.onTap, this.trailing}); PlaylistTile(this.playlist, {this.onHold, this.onTap, this.trailing});
@ -216,8 +204,8 @@ class PlaylistTile extends StatelessWidget {
url: playlist!.image!.thumb, url: playlist!.image!.thumb,
width: 48, width: 48,
), ),
onTap: onTap as void Function()?, onTap: onTap,
onLongPress: onHold as void Function()?, onLongPress: onHold,
trailing: trailing, trailing: trailing,
); );
} }
@ -225,8 +213,8 @@ class PlaylistTile extends StatelessWidget {
class ArtistHorizontalTile extends StatelessWidget { class ArtistHorizontalTile extends StatelessWidget {
final Artist? artist; final Artist? artist;
final Function? onTap; final void Function()? onTap;
final Function? onHold; final void Function()? onHold;
final Widget? trailing; final Widget? trailing;
ArtistHorizontalTile(this.artist, {this.onHold, this.onTap, this.trailing}); ArtistHorizontalTile(this.artist, {this.onHold, this.onTap, this.trailing});
@ -244,8 +232,8 @@ class ArtistHorizontalTile extends StatelessWidget {
url: artist!.picture!.thumb, url: artist!.picture!.thumb,
circular: true, circular: true,
), ),
onTap: onTap as void Function()?, onTap: onTap,
onLongPress: onHold as void Function()?, onLongPress: onHold,
trailing: trailing, trailing: trailing,
), ),
); );

View file

@ -608,6 +608,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
numberpicker: numberpicker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -762,6 +769,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.2.3" version: "4.2.3"
provider:
dependency: "direct main"
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.0"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:

View file

@ -82,6 +82,7 @@ dependencies:
url: https://github.com/ryanheise/just_audio.git url: https://github.com/ryanheise/just_audio.git
ref: dev ref: dev
path: just_audio/ path: just_audio/
provider: ^6.0.0
dependency_overrides: dependency_overrides:
analyzer: ^2.0.0 analyzer: ^2.0.0