pre-new design
This commit is contained in:
parent
1ce60e70de
commit
c792daea19
|
|
@ -19,7 +19,7 @@ class DeezerAPI {
|
|||
String? favoritesPlaylistId;
|
||||
String? sid;
|
||||
|
||||
Future? _authorizing;
|
||||
Future<bool>? _authorizing;
|
||||
|
||||
//Get headers
|
||||
Map<String, String> get headers => {
|
||||
|
|
@ -76,12 +76,7 @@ class DeezerAPI {
|
|||
}
|
||||
|
||||
//Wrapper so it can be globally awaited
|
||||
Future? authorize() async {
|
||||
if (_authorizing == null) {
|
||||
this._authorizing = this.rawAuthorize();
|
||||
}
|
||||
return _authorizing;
|
||||
}
|
||||
Future<bool> authorize() async => this._authorizing ??= this.rawAuthorize();
|
||||
|
||||
//Login with email
|
||||
static Future<String?> getArlByEmail(String? email, String password) async {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:audio_service/audio_service.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:intl/intl.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
|
|
@ -760,8 +766,8 @@ class HomePageSection {
|
|||
}
|
||||
|
||||
class HomePageItem {
|
||||
HomePageItemType? type;
|
||||
dynamic value;
|
||||
final HomePageItemType? type;
|
||||
final value;
|
||||
|
||||
HomePageItem({this.type, this.value});
|
||||
|
||||
|
|
@ -831,7 +837,7 @@ class HomePageItem {
|
|||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
String type = this.type.toString().split('.').last;
|
||||
String type = describeEnum(this.type!);
|
||||
return {'type': type, 'value': value.toJson()};
|
||||
}
|
||||
}
|
||||
|
|
@ -1077,17 +1083,95 @@ Map<String, dynamic> mediaItemToJson(MediaItem mi) => {
|
|||
'displayDescription': mi.displayDescription,
|
||||
};
|
||||
MediaItem mediaItemFromJson(Map<String, dynamic> json) => MediaItem(
|
||||
id: json['id'],
|
||||
title: json['title'],
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
artUri: json['artUri'] == null ? null : Uri.parse(json['artUri']),
|
||||
playable: json['playable'] as bool,
|
||||
playable: json['playable'] as bool?,
|
||||
duration: json['duration'] == null
|
||||
? null
|
||||
: Duration(milliseconds: json['duration'] as int),
|
||||
extras: json['extras'] as Map<String, dynamic>,
|
||||
album: json['album'],
|
||||
artist: json['artist'],
|
||||
displayTitle: json['displayTitle'],
|
||||
displaySubtitle: json['displaySubtitle'],
|
||||
displayDescription: json['displayDescription'],
|
||||
extras: json['extras'] as Map<String, dynamic>?,
|
||||
album: json['album'] as String?,
|
||||
artist: json['artist'] as String?,
|
||||
displayTitle: json['displayTitle'] as String?,
|
||||
displaySubtitle: json['displaySubtitle'] as String?,
|
||||
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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class DownloadManager {
|
|||
static EventChannel eventChannel =
|
||||
const EventChannel('f.f.freezer/downloads');
|
||||
|
||||
bool? running = false;
|
||||
bool running = false;
|
||||
int? queueSize = 0;
|
||||
|
||||
StreamController serviceEvents = StreamController.broadcast();
|
||||
|
|
@ -92,8 +92,7 @@ class DownloadManager {
|
|||
|
||||
//Get all downloads from db
|
||||
Future<List<Download>> getDownloads() async {
|
||||
List raw = await (platform.invokeMethod('getDownloads')
|
||||
as FutureOr<List<dynamic>>);
|
||||
List raw = await platform.invokeMethod('getDownloads');
|
||||
return raw.map((d) => Download.fromJson(d)).toList();
|
||||
}
|
||||
|
||||
|
|
@ -535,14 +534,14 @@ class DownloadManager {
|
|||
//Download path
|
||||
path = settings.downloadPath;
|
||||
|
||||
if (settings.playlistFolder! && playlistName != null)
|
||||
if (settings.playlistFolder && playlistName != null)
|
||||
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
|
||||
if (settings.albumFolder!) {
|
||||
if (settings.albumDiscFolder!) {
|
||||
if (settings.albumFolder) {
|
||||
if (settings.albumDiscFolder) {
|
||||
path = p.join(path!,
|
||||
'%album%' + ' - Disk ' + (track!.diskNumber ?? 1).toString());
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -28,21 +28,51 @@ class PlayerHelper {
|
|||
late StreamSubscription _mediaItemSubscription;
|
||||
late StreamSubscription _playbackStateStreamSubscription;
|
||||
QueueSource? queueSource;
|
||||
LoopMode repeatType = LoopMode.off;
|
||||
AudioServiceRepeatMode repeatType = AudioServiceRepeatMode.none;
|
||||
Timer? _timer;
|
||||
int? audioSession;
|
||||
int? _prevAudioSession;
|
||||
bool equalizerOpen = false;
|
||||
bool _shuffleEnabled = false;
|
||||
int _queueIndex = 0;
|
||||
|
||||
//Visualizer
|
||||
StreamController _visualizerController = StreamController.broadcast();
|
||||
Stream get visualizerStream => _visualizerController.stream;
|
||||
|
||||
//Find queue index by id
|
||||
int get queueIndex => audioHandler.queue.value
|
||||
.indexWhere((mi) => mi.id == audioHandler.mediaItem.value?.id);
|
||||
/// Find queue index by 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
|
||||
_customEventSubscription = audioHandler.customEvent.listen((event) async {
|
||||
if (!(event is Map)) return;
|
||||
|
|
@ -50,13 +80,14 @@ class PlayerHelper {
|
|||
case 'onLoad':
|
||||
//After audio_service is loaded, load queue, set quality
|
||||
await settings.updateAudioServiceQuality();
|
||||
await audioHandler.customAction('load', {});
|
||||
await audioHandler.customAction('load');
|
||||
await authorizeLastFM();
|
||||
break;
|
||||
case 'onRestore':
|
||||
//Load queueSource from isolate
|
||||
this.queueSource = QueueSource.fromJson(event['queueSource']);
|
||||
repeatType = LoopMode.values[event['loopMode']];
|
||||
repeatType = AudioServiceRepeatMode.values[event['loopMode']];
|
||||
_queueIndex = getQueueIndex();
|
||||
break;
|
||||
case 'queueEnd':
|
||||
//If last song is played, load more queue
|
||||
|
|
@ -74,7 +105,7 @@ class PlayerHelper {
|
|||
await androidAuto.playItem(event['id']);
|
||||
break;
|
||||
case 'audioSession':
|
||||
if (!settings.enableEqualizer!) break;
|
||||
if (!settings.enableEqualizer) break;
|
||||
//Save
|
||||
_prevAudioSession = audioSession;
|
||||
audioSession = event['id'];
|
||||
|
|
@ -98,16 +129,21 @@ class PlayerHelper {
|
|||
break;
|
||||
}
|
||||
});
|
||||
_mediaItemSubscription = audioHandler.mediaItem.listen((event) {
|
||||
if (event == null) return;
|
||||
//Load more flow if index-1 song
|
||||
if (queueIndex == audioHandler.queue.value.length - 1) onQueueEnd();
|
||||
_mediaItemSubscription = audioHandler.mediaItem.listen((mediaItem) async {
|
||||
if (mediaItem == null) return;
|
||||
final queue = audioHandler.queue.value;
|
||||
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
|
||||
audioHandler.customAction('saveQueue', {});
|
||||
await audioHandler.customAction('saveQueue', {});
|
||||
//Add to history
|
||||
if (cache.history.length > 0 && cache.history.last.id == event.id) return;
|
||||
cache.history.add(Track.fromMediaItem(event));
|
||||
if (cache.history.length > 0 && cache.history.last.id == mediaItem.id)
|
||||
return;
|
||||
cache.history.add(Track.fromMediaItem(mediaItem));
|
||||
cache.save();
|
||||
});
|
||||
|
||||
|
|
@ -141,26 +177,25 @@ class PlayerHelper {
|
|||
});
|
||||
}
|
||||
|
||||
Future toggleShuffle() async {
|
||||
await audioHandler.customAction('shuffle');
|
||||
Future<bool> toggleShuffle() async {
|
||||
await audioHandler.setShuffleMode((_shuffleEnabled = !_shuffleEnabled)
|
||||
? AudioServiceShuffleMode.all
|
||||
: AudioServiceShuffleMode.none);
|
||||
return _shuffleEnabled;
|
||||
}
|
||||
|
||||
bool get shuffleEnabled => _shuffleEnabled;
|
||||
|
||||
//Repeat toggle
|
||||
Future changeRepeat() async {
|
||||
//Change to next repeat type
|
||||
switch (repeatType) {
|
||||
case LoopMode.one:
|
||||
repeatType = LoopMode.off;
|
||||
break;
|
||||
case LoopMode.all:
|
||||
repeatType = LoopMode.one;
|
||||
break;
|
||||
default:
|
||||
repeatType = LoopMode.all;
|
||||
break;
|
||||
}
|
||||
repeatType = repeatType == AudioServiceRepeatMode.all
|
||||
? AudioServiceRepeatMode.none
|
||||
: repeatType == AudioServiceRepeatMode.none
|
||||
? AudioServiceRepeatMode.one
|
||||
: AudioServiceRepeatMode.all;
|
||||
//Set repeat type
|
||||
await audioHandler.customAction('repeatType', {'type': repeatType.index});
|
||||
await audioHandler.setRepeatMode(repeatType);
|
||||
}
|
||||
|
||||
//Executed before exit
|
||||
|
|
@ -183,11 +218,11 @@ class PlayerHelper {
|
|||
|
||||
//Called when queue ends to load more tracks
|
||||
Future onQueueEnd() async {
|
||||
//Flow
|
||||
if (queueSource == null) return;
|
||||
|
||||
List<Track>? tracks = [];
|
||||
List<Track>? tracks;
|
||||
switch (queueSource!.source) {
|
||||
//Flow
|
||||
case 'flow':
|
||||
tracks = await deezerAPI.flow();
|
||||
break;
|
||||
|
|
@ -209,13 +244,13 @@ class PlayerHelper {
|
|||
tracks?.removeWhere((track) => queueIds.contains(track.id));
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
// print(queueSource.toJson());
|
||||
break;
|
||||
}
|
||||
|
||||
if (tracks == null) {
|
||||
// try again i guess?
|
||||
return await onQueueEnd();
|
||||
throw Exception(
|
||||
'Failed to fetch more queue songs (queueSource: ${queueSource!.toJson()})');
|
||||
}
|
||||
|
||||
List<MediaItem> mi = tracks.map<MediaItem>((t) => t.toMediaItem()).toList();
|
||||
|
|
@ -231,8 +266,7 @@ class PlayerHelper {
|
|||
|
||||
//Play mix by track
|
||||
Future playMix(String trackId, String trackTitle) async {
|
||||
List<Track> tracks =
|
||||
await (deezerAPI.playMix(trackId) as FutureOr<List<Track>>);
|
||||
List<Track> tracks = (await deezerAPI.playMix(trackId))!;
|
||||
playFromTrackList(
|
||||
tracks,
|
||||
tracks[0].id,
|
||||
|
|
@ -337,9 +371,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
late AudioPlayer _player;
|
||||
|
||||
//Queue
|
||||
List<MediaItem>? _queue = <MediaItem>[];
|
||||
List<MediaItem>? _originalQueue;
|
||||
bool _shuffle = false;
|
||||
int _queueIndex = 0;
|
||||
bool _isInitialized = false;
|
||||
late ConcatenatingAudioSource _audioSource;
|
||||
|
|
@ -356,7 +388,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
int? wifiQuality;
|
||||
QueueSource? queueSource;
|
||||
Duration? _lastPosition;
|
||||
LoopMode _loopMode = LoopMode.off;
|
||||
AudioServiceRepeatMode _repeatMode = AudioServiceRepeatMode.none;
|
||||
|
||||
Completer<List<MediaItem>>? _androidAutoCallback;
|
||||
Scrobblenaut? _scrobblenaut;
|
||||
|
|
@ -364,11 +396,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
// Last logged track id
|
||||
String? _loggedTrackId;
|
||||
|
||||
MediaItem get currentMediaItem => _queue![_queueIndex];
|
||||
|
||||
AudioPlayerTask() {
|
||||
onStart({}); // workaround i guess?
|
||||
}
|
||||
MediaItem get currentMediaItem => queue.value[_queueIndex];
|
||||
|
||||
Future onStart(Map<String, dynamic>? params) async {
|
||||
final session = await AudioSession.instance;
|
||||
|
|
@ -391,10 +419,10 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
//Update state on all clients on change
|
||||
_eventSub = _player.playbackEventStream.listen((event) {
|
||||
//Quality string
|
||||
if (_queueIndex != -1 && _queueIndex < _queue!.length) {
|
||||
if (_queueIndex != -1 && _queueIndex < queue.value.length) {
|
||||
Map extras = currentMediaItem.extras!;
|
||||
extras['qualityString'] = '';
|
||||
_queue![_queueIndex] =
|
||||
queue.value[_queueIndex] =
|
||||
currentMediaItem.copyWith(extras: extras as Map<String, dynamic>?);
|
||||
}
|
||||
//Update
|
||||
|
|
@ -404,7 +432,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
switch (state) {
|
||||
case ProcessingState.completed:
|
||||
//Player ended, get more songs
|
||||
if (_queueIndex == _queue!.length - 1)
|
||||
if (_queueIndex == queue.value.length - 1)
|
||||
customEvent.add({
|
||||
'action': 'queueEnd',
|
||||
'queueSource': (queueSource ?? QueueSource()).toJson()
|
||||
|
|
@ -421,7 +449,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
});
|
||||
|
||||
//Load queue
|
||||
queue.add(_queue!);
|
||||
// queue.add(_queue);
|
||||
customEvent.add({'action': 'onLoad'});
|
||||
}
|
||||
|
||||
|
|
@ -477,25 +505,25 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
//Remove item from queue
|
||||
@override
|
||||
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);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeQueueItemAt(int index) async {
|
||||
_queue!.removeAt(index);
|
||||
if (index <= _queueIndex) {
|
||||
_queueIndex--;
|
||||
}
|
||||
|
||||
await _audioSource.removeAt(index);
|
||||
|
||||
queue.add(_queue!);
|
||||
queue.add(queue.value..removeAt(index));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> skipToNext() async {
|
||||
_lastPosition = null;
|
||||
if (_queueIndex == _queue!.length - 1) return;
|
||||
if (_queueIndex == queue.value.length - 1) return;
|
||||
//Update buffering state
|
||||
_queueIndex++;
|
||||
await _player.seekToNext();
|
||||
|
|
@ -550,14 +578,15 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
void _broadcastState() {
|
||||
playbackState.add(PlaybackState(
|
||||
controls: [
|
||||
if (_queueIndex != 0) MediaControl.skipToPrevious,
|
||||
/*if (_queueIndex != 0)*/ MediaControl.skipToPrevious,
|
||||
_player.playing ? MediaControl.pause : MediaControl.play,
|
||||
if (_queueIndex != _queue!.length - 1) MediaControl.skipToNext,
|
||||
/*if (_queueIndex != _queue!.length - 1)*/ MediaControl.skipToNext,
|
||||
//Stop
|
||||
MediaControl(
|
||||
androidIcon: 'drawable/ic_action_stop',
|
||||
label: 'stop',
|
||||
action: MediaAction.stop),
|
||||
// MediaControl(
|
||||
// androidIcon: 'drawable/ic_action_stop',
|
||||
// label: 'stop',
|
||||
// action: MediaAction.stop),
|
||||
// i mean, the user can just swipe the notification away to stop
|
||||
],
|
||||
systemActions: const {
|
||||
MediaAction.seek,
|
||||
|
|
@ -569,26 +598,21 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
playing: _player.playing,
|
||||
updatePosition: _player.position,
|
||||
bufferedPosition: _player.bufferedPosition,
|
||||
speed: _player.speed));
|
||||
speed: _player.speed,
|
||||
queueIndex: _queueIndex,
|
||||
));
|
||||
}
|
||||
|
||||
//just_audio state -> audio_service state. If skipping, use _skipState
|
||||
AudioProcessingState _getProcessingState() {
|
||||
//SRC: audio_service example
|
||||
switch (_player.processingState) {
|
||||
case ProcessingState.idle:
|
||||
return AudioProcessingState.idle;
|
||||
case ProcessingState.loading:
|
||||
return AudioProcessingState.loading;
|
||||
case ProcessingState.buffering:
|
||||
return AudioProcessingState.buffering;
|
||||
case ProcessingState.ready:
|
||||
return AudioProcessingState.ready;
|
||||
case ProcessingState.completed:
|
||||
return AudioProcessingState.completed;
|
||||
default:
|
||||
throw Exception("Invalid state: ${_player.processingState}");
|
||||
}
|
||||
return const <ProcessingState, AudioProcessingState>{
|
||||
ProcessingState.idle: AudioProcessingState.idle,
|
||||
ProcessingState.loading: AudioProcessingState.loading,
|
||||
ProcessingState.buffering: AudioProcessingState.buffering,
|
||||
ProcessingState.ready: AudioProcessingState.ready,
|
||||
ProcessingState.completed: AudioProcessingState.completed
|
||||
}[_player.processingState] ??
|
||||
AudioProcessingState.idle;
|
||||
}
|
||||
|
||||
//Replace current queue
|
||||
|
|
@ -596,19 +620,16 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
Future updateQueue(List<MediaItem> q) async {
|
||||
_lastPosition = null;
|
||||
//just_audio
|
||||
_shuffle = false;
|
||||
_originalQueue = null;
|
||||
_player.stop();
|
||||
if (_isInitialized) _audioSource.clear();
|
||||
//Filter duplicate IDs
|
||||
List<MediaItem> newQueue = q.toSet().toList();
|
||||
|
||||
_queue = newQueue;
|
||||
|
||||
//Load
|
||||
await _loadQueue();
|
||||
// broadcast to ui
|
||||
queue.add(newQueue);
|
||||
//Load
|
||||
await _loadQueue();
|
||||
//await _player.seek(Duration.zero, index: 0);
|
||||
}
|
||||
|
||||
|
|
@ -618,12 +639,12 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
int? qi = _queueIndex;
|
||||
|
||||
List<AudioSource> sources = [];
|
||||
for (int i = 0; i < _queue!.length; i++) {
|
||||
AudioSource s = await _mediaItemToAudioSource(_queue![i]);
|
||||
for (int i = 0; i < queue.value.length; i++) {
|
||||
AudioSource s = await _mediaItemToAudioSource(queue.value[i]);
|
||||
sources.add(s);
|
||||
}
|
||||
|
||||
_audioSource = ConcatenatingAudioSource(children: sources);
|
||||
|
||||
//Load in just_audio
|
||||
try {
|
||||
await _player.setAudioSource(_audioSource,
|
||||
|
|
@ -678,6 +699,9 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
@override
|
||||
Future customAction(String name, [Map<String, dynamic>? args]) async {
|
||||
switch (name) {
|
||||
case 'start':
|
||||
onStart(args);
|
||||
break;
|
||||
case 'updateQuality':
|
||||
//Pass wifi & mobile quality by custom action
|
||||
//Isolate can't access globals
|
||||
|
|
@ -689,10 +713,10 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
this.queueSource = QueueSource.fromJson(args!);
|
||||
break;
|
||||
//Looping
|
||||
case 'repeatType':
|
||||
_loopMode = LoopMode.values[args!['type']];
|
||||
_player.setLoopMode(_loopMode);
|
||||
break;
|
||||
// case 'repeatType':
|
||||
// _loopMode = LoopMode.values[args!['type']];
|
||||
// _player.setLoopMode(_loopMode);
|
||||
// break;
|
||||
//Save queue
|
||||
case 'saveQueue':
|
||||
await this._saveQueue();
|
||||
|
|
@ -701,29 +725,6 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
case 'load':
|
||||
await this._loadQueueFile();
|
||||
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
|
||||
case 'screenAndroidAuto':
|
||||
|
|
@ -738,14 +739,11 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
final oldIndex = args!['oldIndex']! as int;
|
||||
final newIndex = args['newIndex']! as int;
|
||||
await _audioSource.move(oldIndex, newIndex);
|
||||
//Switch in queue
|
||||
_queue!.reorder(oldIndex, newIndex);
|
||||
//Update UI
|
||||
queue.add(_queue!);
|
||||
queue.add(queue.value..reorder(oldIndex, newIndex));
|
||||
_broadcastState();
|
||||
break;
|
||||
//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'];
|
||||
break;
|
||||
//Start visualizer
|
||||
|
|
@ -824,7 +822,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
|
||||
//Export queue to JSON
|
||||
Future<void> _saveQueue() async {
|
||||
if (_queueIndex == 0 && _queue!.length == 0) return;
|
||||
if (_queueIndex == 0 && queue.value.length == 0) return;
|
||||
|
||||
String path = await _getQueuePath();
|
||||
File f = File(path);
|
||||
|
|
@ -834,10 +832,10 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
}
|
||||
Map data = {
|
||||
'index': _queueIndex,
|
||||
'queue': _queue!.map<Map<String, dynamic>>(mediaItemToJson).toList(),
|
||||
'queue': queue.value.map<Map<String, dynamic>>(mediaItemToJson).toList(),
|
||||
'position': _player.position.inMilliseconds,
|
||||
'queueSource': (queueSource ?? QueueSource()).toJson(),
|
||||
'loopMode': LoopMode.values.indexOf(_loopMode)
|
||||
'loopMode': _repeatMode.index,
|
||||
};
|
||||
await f.writeAsString(jsonEncode(data));
|
||||
}
|
||||
|
|
@ -847,25 +845,29 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
File f = File(await _getQueuePath());
|
||||
if (await f.exists()) {
|
||||
Map<String, dynamic> json = jsonDecode(await f.readAsString());
|
||||
this._queue =
|
||||
(json['queue'] ?? []).map<MediaItem>(mediaItemFromJson).toList();
|
||||
this._queueIndex = json['index'] ?? 0;
|
||||
this._lastPosition = Duration(milliseconds: json['position'] ?? 0);
|
||||
this.queueSource = QueueSource.fromJson(json['queueSource'] ?? {});
|
||||
this._loopMode = LoopMode.values[(json['loopMode'] ?? 0)];
|
||||
List<MediaItem>? _queue = (json['queue'] as List?)
|
||||
?.cast<Map>()
|
||||
.map<MediaItem>(
|
||||
(json) => mediaItemFromJson(json.cast<String, dynamic>()))
|
||||
.toList();
|
||||
_queueIndex = json['index'] ?? 0;
|
||||
_lastPosition = Duration(milliseconds: json['position'] ?? 0);
|
||||
queueSource = QueueSource.fromJson(json['queueSource'] ?? {});
|
||||
_repeatMode = AudioServiceRepeatMode.values[(json['loopMode'] ?? 0)];
|
||||
//Restore queue
|
||||
if (_queue != null) {
|
||||
queue.add(_queue!);
|
||||
queue.add(_queue);
|
||||
await _loadQueue();
|
||||
mediaItem.add(currentMediaItem);
|
||||
}
|
||||
}
|
||||
//Send restored queue source to ui
|
||||
customEvent.add({
|
||||
'action': 'onRestore',
|
||||
'queueSource': (queueSource ?? QueueSource()).toJson(),
|
||||
'loopMode': LoopMode.values.indexOf(_loopMode)
|
||||
'loopMode': _repeatMode.index
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -874,8 +876,7 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
//-1 == play next
|
||||
if (index == -1) index = _queueIndex + 1;
|
||||
|
||||
_queue!.insert(index, mi);
|
||||
queue.add(_queue!);
|
||||
queue.add(queue.value..insert(index, mi));
|
||||
AudioSource? _newSource = await _mediaItemToAudioSource(mi);
|
||||
await _audioSource.insert(index, _newSource);
|
||||
|
||||
|
|
@ -886,10 +887,9 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
@override
|
||||
Future<void> addQueueItem(MediaItem mediaItem,
|
||||
{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!);
|
||||
queue.add(queue.value..add(mediaItem));
|
||||
AudioSource _newSource = await _mediaItemToAudioSource(mediaItem);
|
||||
await _audioSource.add(_newSource);
|
||||
if (shouldSaveQueue) _saveQueue();
|
||||
|
|
@ -917,25 +917,40 @@ class AudioPlayerTask extends BaseAudioHandler {
|
|||
|
||||
//Does the same thing
|
||||
await this
|
||||
.skipToQueueItem(_queue!.indexWhere((item) => item.id == mediaId));
|
||||
.skipToQueueItem(queue.value.indexWhere((item) => item.id == mediaId));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MediaItem?> getMediaItem(String mediaId) async =>
|
||||
_queue!.firstWhereOrNull((mediaItem) => mediaItem.id == mediaId);
|
||||
queue.value.firstWhereOrNull((mediaItem) => mediaItem.id == mediaId);
|
||||
|
||||
@override
|
||||
Future<void> playMediaItem(MediaItem mediaItem) =>
|
||||
playFromMediaId(mediaItem.id);
|
||||
|
||||
// TODO: implement shuffle and repeat
|
||||
@override
|
||||
Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) =>
|
||||
super.setRepeatMode(repeatMode);
|
||||
_player.setLoopMode(repeatMode.toLoopMode());
|
||||
|
||||
@override
|
||||
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) =>
|
||||
super.setShuffleMode(shuffleMode);
|
||||
Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) async {
|
||||
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?)
|
||||
|
|
|
|||
|
|
@ -41,21 +41,7 @@ void main() async {
|
|||
|
||||
//Do on BG
|
||||
playerHelper.authorizeLastFM();
|
||||
|
||||
// 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,
|
||||
),
|
||||
);
|
||||
await playerHelper.initAudioHandler();
|
||||
|
||||
runApp(FreezerApp());
|
||||
}
|
||||
|
|
@ -66,15 +52,33 @@ class FreezerApp extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _FreezerAppState extends State<FreezerApp> {
|
||||
late StreamSubscription _playbackStateSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_initStateAsync();
|
||||
//Make update theme global
|
||||
updateTheme = _updateTheme;
|
||||
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
|
||||
void dispose() {
|
||||
_playbackStateSub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -143,7 +147,7 @@ class _LoginMainWrapperState extends State<LoginMainWrapper> {
|
|||
//Load token on background
|
||||
deezerAPI.arl = settings.arl;
|
||||
settings.offlineMode = true;
|
||||
deezerAPI.authorize()!.then((b) async {
|
||||
deezerAPI.authorize().then((b) async {
|
||||
if (b) setState(() => settings.offlineMode = false);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
20
lib/page_routes/basic_page_route.dart
Normal file
20
lib/page_routes/basic_page_route.dart
Normal 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;
|
||||
}
|
||||
43
lib/page_routes/blur_slide.dart
Normal file
43
lib/page_routes/blur_slide.dart
Normal 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
41
lib/page_routes/fade.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
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/player.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
|
@ -23,9 +25,9 @@ class Settings {
|
|||
|
||||
//Main
|
||||
@JsonKey(defaultValue: false)
|
||||
bool? ignoreInterruptions;
|
||||
late bool ignoreInterruptions;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool? enableEqualizer;
|
||||
late bool enableEqualizer;
|
||||
|
||||
//Account
|
||||
String? arl;
|
||||
|
|
@ -34,45 +36,45 @@ class Settings {
|
|||
|
||||
//Quality
|
||||
@JsonKey(defaultValue: AudioQuality.MP3_320)
|
||||
AudioQuality? wifiQuality;
|
||||
late AudioQuality wifiQuality;
|
||||
@JsonKey(defaultValue: AudioQuality.MP3_128)
|
||||
AudioQuality? mobileQuality;
|
||||
late AudioQuality mobileQuality;
|
||||
@JsonKey(defaultValue: AudioQuality.FLAC)
|
||||
AudioQuality? offlineQuality;
|
||||
late AudioQuality offlineQuality;
|
||||
@JsonKey(defaultValue: AudioQuality.FLAC)
|
||||
AudioQuality? downloadQuality;
|
||||
late AudioQuality downloadQuality;
|
||||
|
||||
//Download options
|
||||
String? downloadPath;
|
||||
|
||||
@JsonKey(defaultValue: "%artist% - %title%")
|
||||
String? downloadFilename;
|
||||
late String downloadFilename;
|
||||
@JsonKey(defaultValue: true)
|
||||
bool? albumFolder;
|
||||
late bool albumFolder;
|
||||
@JsonKey(defaultValue: true)
|
||||
bool? artistFolder;
|
||||
late bool artistFolder;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool? albumDiscFolder;
|
||||
late bool albumDiscFolder;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool? overwriteDownload;
|
||||
late bool overwriteDownload;
|
||||
@JsonKey(defaultValue: 2)
|
||||
int? downloadThreads;
|
||||
late int downloadThreads;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool? playlistFolder;
|
||||
late bool playlistFolder;
|
||||
@JsonKey(defaultValue: true)
|
||||
bool? downloadLyrics;
|
||||
late bool downloadLyrics;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool? trackCover;
|
||||
late bool trackCover;
|
||||
@JsonKey(defaultValue: true)
|
||||
bool? albumCover;
|
||||
late bool albumCover;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool? nomediaFiles;
|
||||
late bool nomediaFiles;
|
||||
@JsonKey(defaultValue: ", ")
|
||||
String? artistSeparator;
|
||||
late String artistSeparator;
|
||||
@JsonKey(defaultValue: "%artist% - %title%")
|
||||
String? singletonFilename;
|
||||
late String singletonFilename;
|
||||
@JsonKey(defaultValue: 1400)
|
||||
int? albumArtResolution;
|
||||
late int albumArtResolution;
|
||||
@JsonKey(defaultValue: [
|
||||
"title",
|
||||
"album",
|
||||
|
|
@ -91,23 +93,29 @@ class Settings {
|
|||
"contributors",
|
||||
"art"
|
||||
])
|
||||
List<String>? tags;
|
||||
late List<String> tags;
|
||||
|
||||
//Appearance
|
||||
@JsonKey(defaultValue: Themes.Dark)
|
||||
Themes? theme;
|
||||
late Themes theme;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool? useSystemTheme;
|
||||
late bool useSystemTheme;
|
||||
@JsonKey(defaultValue: true)
|
||||
bool? colorGradientBackground;
|
||||
late bool colorGradientBackground;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool? blurPlayerBackground;
|
||||
late bool blurPlayerBackground;
|
||||
@JsonKey(defaultValue: "Deezer")
|
||||
String? font;
|
||||
late String font;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool? lyricsVisualizer;
|
||||
late bool lyricsVisualizer;
|
||||
@JsonKey(defaultValue: null)
|
||||
int? displayMode;
|
||||
@JsonKey(defaultValue: true)
|
||||
late bool enableFilledPlayButton;
|
||||
@JsonKey(defaultValue: false)
|
||||
late bool playerBackgroundOnLyrics;
|
||||
@JsonKey(defaultValue: NavigatorRouteType.material)
|
||||
late NavigatorRouteType navigatorRouteType;
|
||||
|
||||
//Colors
|
||||
@JsonKey(toJson: _colorToJson, fromJson: _colorFromJson)
|
||||
|
|
@ -147,17 +155,17 @@ class Settings {
|
|||
|
||||
ThemeData? get themeData {
|
||||
//System theme
|
||||
if (useSystemTheme!) {
|
||||
if (useSystemTheme) {
|
||||
if (SchedulerBinding.instance!.window.platformBrightness ==
|
||||
Brightness.light) {
|
||||
return _themeData[Themes.Light];
|
||||
} else {
|
||||
if (theme == Themes.Light) return _themeData[Themes.Dark];
|
||||
return _themeData[theme!];
|
||||
return _themeData[theme];
|
||||
}
|
||||
}
|
||||
//Theme
|
||||
return _themeData[theme!] ?? ThemeData();
|
||||
return _themeData[theme] ?? ThemeData();
|
||||
}
|
||||
|
||||
//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
|
||||
bool get isDark {
|
||||
if (useSystemTheme!) {
|
||||
if (useSystemTheme) {
|
||||
if (SchedulerBinding.instance!.window.platformBrightness ==
|
||||
Brightness.light) return false;
|
||||
return true;
|
||||
|
|
@ -272,7 +280,7 @@ class Settings {
|
|||
TextTheme? get _textTheme => (font == 'Deezer')
|
||||
? null
|
||||
: GoogleFonts.getTextTheme(
|
||||
font!,
|
||||
font,
|
||||
this.isDark
|
||||
? ThemeData.dark().textTheme
|
||||
: ThemeData.light().textTheme);
|
||||
|
|
@ -292,6 +300,8 @@ class Settings {
|
|||
sliderTheme: _sliderTheme,
|
||||
toggleableActiveColor: primaryColor,
|
||||
bottomAppBarColor: Color(0xfff5f5f5),
|
||||
appBarTheme:
|
||||
AppBarTheme(systemOverlayStyle: SystemUiOverlayStyle.light),
|
||||
),
|
||||
Themes.Dark: ThemeData(
|
||||
textTheme: _textTheme,
|
||||
|
|
|
|||
|
|
@ -70,6 +70,12 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) => Settings(
|
|||
..font = json['font'] as String? ?? 'Deezer'
|
||||
..lyricsVisualizer = json['lyricsVisualizer'] as bool? ?? false
|
||||
..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?)
|
||||
..useArtColor = json['useArtColor'] as bool? ?? false
|
||||
..deezerLanguage = json['deezerLanguage'] as String? ?? 'en'
|
||||
|
|
@ -117,6 +123,10 @@ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
|
|||
'font': instance.font,
|
||||
'lyricsVisualizer': instance.lyricsVisualizer,
|
||||
'displayMode': instance.displayMode,
|
||||
'enableFilledPlayButton': instance.enableFilledPlayButton,
|
||||
'playerBackgroundOnLyrics': instance.playerBackgroundOnLyrics,
|
||||
'navigatorRouteType':
|
||||
_$NavigatorRouteTypeEnumMap[instance.navigatorRouteType],
|
||||
'primaryColor': Settings._colorToJson(instance.primaryColor),
|
||||
'useArtColor': instance.useArtColor,
|
||||
'deezerLanguage': instance.deezerLanguage,
|
||||
|
|
@ -181,6 +191,12 @@ const _$ThemesEnumMap = {
|
|||
Themes.Black: 'Black',
|
||||
};
|
||||
|
||||
const _$NavigatorRouteTypeEnumMap = {
|
||||
NavigatorRouteType.blur_slide: 'blur_slide',
|
||||
NavigatorRouteType.material: 'material',
|
||||
NavigatorRouteType.cupertino: 'cupertino',
|
||||
};
|
||||
|
||||
SpotifyCredentialsSave _$SpotifyCredentialsSaveFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
SpotifyCredentialsSave(
|
||||
|
|
|
|||
28
lib/ui/animated_blur.dart
Normal file
28
lib/ui/animated_blur.dart
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ class ImagesDatabase {
|
|||
|
||||
Future<Color> getPrimaryColor(String url) async {
|
||||
PaletteGenerator paletteGenerator = await getPaletteGenerator(url);
|
||||
return paletteGenerator.colors.first;
|
||||
return paletteGenerator.dominantColor!.color;
|
||||
}
|
||||
|
||||
Future<bool> isDark(String url) async {
|
||||
|
|
@ -113,8 +113,16 @@ class ZoomableImage extends StatefulWidget {
|
|||
final String? url;
|
||||
final bool rounded;
|
||||
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
|
||||
_ZoomableImageState createState() => _ZoomableImageState();
|
||||
|
|
@ -123,6 +131,8 @@ class ZoomableImage extends StatefulWidget {
|
|||
class _ZoomableImageState extends State<ZoomableImage> {
|
||||
PhotoViewController? controller;
|
||||
bool photoViewOpened = false;
|
||||
late final Object? _key =
|
||||
widget.enableHero ? (widget.heroTag ?? UniqueKey()) : null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -141,28 +151,43 @@ class _ZoomableImageState extends State<ZoomableImage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
child: Semantics(
|
||||
child: CachedImage(
|
||||
print('key: ' + _key.toString());
|
||||
final image = CachedImage(
|
||||
url: widget.url,
|
||||
rounded: widget.rounded,
|
||||
width: widget.width,
|
||||
fullThumb: true,
|
||||
),
|
||||
);
|
||||
final child = _key != null
|
||||
? Hero(
|
||||
tag: _key!,
|
||||
child: image,
|
||||
)
|
||||
: image;
|
||||
return GestureDetector(
|
||||
child: Semantics(
|
||||
child: child,
|
||||
label: "Album art".i18n,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(PageRouteBuilder(
|
||||
opaque: false, // transparent background
|
||||
pageBuilder: (context, _, __) {
|
||||
pageBuilder: (context, animation, __) {
|
||||
print('key: ' + _key.toString());
|
||||
photoViewOpened = true;
|
||||
return PhotoView(
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: PhotoView(
|
||||
imageProvider: CachedNetworkImageProvider(widget.url!),
|
||||
maxScale: 8.0,
|
||||
minScale: 0.2,
|
||||
controller: controller,
|
||||
backgroundDecoration:
|
||||
BoxDecoration(color: Color.fromARGB(0x90, 0, 0, 0)));
|
||||
heroAttributes: _key == null
|
||||
? null
|
||||
: PhotoViewHeroAttributes(tag: _key!),
|
||||
backgroundDecoration: const BoxDecoration(
|
||||
color: Color.fromARGB(0x90, 0, 0, 0))),
|
||||
);
|
||||
}));
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -202,10 +202,7 @@ class _AlbumDetailsState extends State<AlbumDetails> {
|
|||
//Add to library
|
||||
if (!album!.library!) {
|
||||
await deezerAPI.addFavoriteAlbum(album!.id);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Added to library'.i18n,
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.BOTTOM);
|
||||
ScaffoldMessenger.of(context).snack
|
||||
setState(() => album!.library = true);
|
||||
return;
|
||||
}
|
||||
|
|
@ -260,7 +257,7 @@ class _AlbumDetailsState extends State<AlbumDetails> {
|
|||
),
|
||||
...List.generate(
|
||||
tracks.length,
|
||||
(i) => TrackTile(tracks[i], onTap: () {
|
||||
(i) => TrackTile(tracks[i]!, onTap: () {
|
||||
playerHelper.playFromAlbum(
|
||||
album!, tracks[i]!.id);
|
||||
}, onHold: () {
|
||||
|
|
@ -349,7 +346,7 @@ class ArtistDetails extends StatelessWidget {
|
|||
|
||||
FutureOr<Artist> _loadArtist(Artist artist) {
|
||||
//Load artist from api if no albums
|
||||
if ((this.artist.albums ?? []).length == 0) {
|
||||
if ((artist.albums ?? []).length == 0) {
|
||||
return deezerAPI.artist(artist.id);
|
||||
}
|
||||
return artist;
|
||||
|
|
@ -364,9 +361,7 @@ class ArtistDetails extends StatelessWidget {
|
|||
//Error / not done
|
||||
if (snapshot.hasError) return ErrorScreen();
|
||||
if (snapshot.connectionState != ConnectionState.done)
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
|
||||
return ListView(
|
||||
children: <Widget>[
|
||||
|
|
@ -499,9 +494,9 @@ class ArtistDetails extends StatelessWidget {
|
|||
AlbumTile(
|
||||
artist.highlight!.data,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) =>
|
||||
AlbumDetails(artist.highlight!.data)));
|
||||
AlbumDetails(artist.highlight!.data));
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8.0)
|
||||
|
|
@ -536,13 +531,13 @@ class ArtistDetails extends StatelessWidget {
|
|||
ListTile(
|
||||
title: Text('Show more tracks'.i18n),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => TrackListScreen(
|
||||
artist.topTracks,
|
||||
QueueSource(
|
||||
id: artist.id,
|
||||
text: 'Top'.i18n + '${artist.name}',
|
||||
source: 'topTracks'))));
|
||||
source: 'topTracks')));
|
||||
}),
|
||||
FreezerDivider(),
|
||||
//Albums
|
||||
|
|
@ -562,10 +557,10 @@ class ArtistDetails extends StatelessWidget {
|
|||
return ListTile(
|
||||
title: Text('Show all albums'.i18n),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => DiscographyScreen(
|
||||
artist: artist,
|
||||
)));
|
||||
));
|
||||
});
|
||||
}
|
||||
//Top albums
|
||||
|
|
@ -573,8 +568,8 @@ class ArtistDetails extends StatelessWidget {
|
|||
return AlbumTile(
|
||||
a,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => AlbumDetails(a)));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => AlbumDetails(a));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
|
|
@ -1232,7 +1227,7 @@ class _ShowScreenState extends State<ShowScreen> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: FreezerAppBar(_show!.name),
|
||||
appBar: FreezerAppBar(_show!.name!),
|
||||
body: ListView(
|
||||
children: [
|
||||
Padding(
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class DownloadsScreen extends StatefulWidget {
|
|||
|
||||
class _DownloadsScreenState extends State<DownloadsScreen> {
|
||||
List<Download> downloads = [];
|
||||
StreamSubscription? _stateSubscription;
|
||||
late StreamSubscription _stateSubscription;
|
||||
|
||||
//Sublists
|
||||
List<Download> get downloading => downloads
|
||||
|
|
@ -70,8 +70,7 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_stateSubscription?.cancel();
|
||||
_stateSubscription = null;
|
||||
_stateSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -96,13 +95,13 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
|
|||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
downloadManager.running! ? Icons.stop : Icons.play_arrow,
|
||||
downloadManager.running ? Icons.stop : Icons.play_arrow,
|
||||
semanticLabel:
|
||||
downloadManager.running! ? "Stop".i18n : "Start".i18n,
|
||||
downloadManager.running ? "Stop".i18n : "Start".i18n,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
if (downloadManager.running!)
|
||||
if (downloadManager.running)
|
||||
downloadManager.stop();
|
||||
else
|
||||
downloadManager.start();
|
||||
|
|
|
|||
|
|
@ -32,41 +32,45 @@ class EmptyLeading extends StatelessWidget {
|
|||
}
|
||||
|
||||
class FreezerAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final String? title;
|
||||
final List<Widget> actions;
|
||||
final Widget? bottom;
|
||||
final String title;
|
||||
final List<Widget>? actions;
|
||||
final PreferredSizeWidget? bottom;
|
||||
//Should be specified if bottom is specified
|
||||
final double height;
|
||||
final SystemUiOverlayStyle? systemUiOverlayStyle;
|
||||
|
||||
const FreezerAppBar(this.title,
|
||||
{this.actions = const [],
|
||||
/// The appbar's backgroundColor, if left null,
|
||||
/// it defaults to [ThemeData.scaffoldBackgroundColor]
|
||||
final Color? backgroundColor;
|
||||
final Color? foregroundColor;
|
||||
|
||||
final Brightness? brightness;
|
||||
|
||||
const FreezerAppBar(
|
||||
this.title, {
|
||||
this.actions,
|
||||
this.bottom,
|
||||
this.height = 56.0,
|
||||
this.systemUiOverlayStyle});
|
||||
this.systemUiOverlayStyle,
|
||||
this.backgroundColor,
|
||||
this.brightness,
|
||||
this.foregroundColor,
|
||||
});
|
||||
|
||||
Size get preferredSize => Size.fromHeight(this.height);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: ThemeData(
|
||||
primaryColor: (Theme.of(context).brightness == Brightness.light)
|
||||
? Colors.white
|
||||
: Colors.black),
|
||||
child: AppBar(
|
||||
return AppBar(
|
||||
systemOverlayStyle: systemUiOverlayStyle,
|
||||
elevation: 0.0,
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
title: Text(
|
||||
title!,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
backgroundColor:
|
||||
backgroundColor ?? Theme.of(context).scaffoldBackgroundColor,
|
||||
title: Text(title, style: TextStyle(fontWeight: FontWeight.w900)),
|
||||
actions: actions,
|
||||
bottom: bottom as PreferredSizeWidget?,
|
||||
),
|
||||
bottom: bottom,
|
||||
foregroundColor:
|
||||
foregroundColor ?? (settings.isDark ? Colors.white : Colors.black),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,13 +175,13 @@ class HomepageRowSection extends StatelessWidget {
|
|||
style: TextStyle(fontSize: 20.0),
|
||||
),
|
||||
onPressed: () =>
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => Scaffold(
|
||||
appBar: FreezerAppBar(section.title),
|
||||
appBar: FreezerAppBar(section.title!),
|
||||
body: SingleChildScrollView(
|
||||
child: HomePageScreen(
|
||||
channel:
|
||||
DeezerChannel(target: section.pagePath))),
|
||||
DeezerChannel(target: section.pagePath)),
|
||||
),
|
||||
)),
|
||||
);
|
||||
|
|
@ -245,8 +245,8 @@ class HomePageItemWidget extends StatelessWidget {
|
|||
return AlbumCard(
|
||||
item.value,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => AlbumDetails(item.value)));
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => AlbumDetails(item.value));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
|
|
@ -257,8 +257,8 @@ class HomePageItemWidget extends StatelessWidget {
|
|||
return ArtistTile(
|
||||
item.value,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => ArtistDetails(item.value)));
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => ArtistDetails(item.value));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
|
|
@ -269,8 +269,8 @@ class HomePageItemWidget extends StatelessWidget {
|
|||
return PlaylistCardTile(
|
||||
item.value,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => PlaylistDetails(item.value)));
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => PlaylistDetails(item.value));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
|
|
@ -281,22 +281,22 @@ class HomePageItemWidget extends StatelessWidget {
|
|||
return ChannelTile(
|
||||
item.value,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => Scaffold(
|
||||
appBar: FreezerAppBar(item.value.title.toString()),
|
||||
body: SingleChildScrollView(
|
||||
child: HomePageScreen(
|
||||
channel: item.value,
|
||||
)),
|
||||
)));
|
||||
));
|
||||
},
|
||||
);
|
||||
case HomePageItemType.SHOW:
|
||||
return ShowCard(
|
||||
item.value,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => ShowScreen(item.value)));
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => ShowScreen(item.value));
|
||||
},
|
||||
);
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ class LibraryAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||
semanticLabel: "Download".i18n,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => DownloadsScreen()));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => DownloadsScreen());
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
|
|
@ -46,8 +46,8 @@ class LibraryAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||
semanticLabel: "Settings".i18n,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => SettingsScreen()));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => SettingsScreen());
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
@ -65,7 +65,7 @@ class LibraryScreen extends StatelessWidget {
|
|||
Container(
|
||||
height: 4.0,
|
||||
),
|
||||
if (!downloadManager.running! && downloadManager.queueSize! > 0)
|
||||
if (!downloadManager.running && downloadManager.queueSize! > 0)
|
||||
ListTile(
|
||||
title: Text('Downloads'.i18n),
|
||||
leading: LeadingIcon(Icons.file_download, color: Colors.grey),
|
||||
|
|
@ -74,8 +74,8 @@ class LibraryScreen extends StatelessWidget {
|
|||
.i18n),
|
||||
onTap: () {
|
||||
downloadManager.start();
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => DownloadsScreen()));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => DownloadsScreen());
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
|
|
@ -97,32 +97,32 @@ class LibraryScreen extends StatelessWidget {
|
|||
title: Text('Tracks'.i18n),
|
||||
leading: LeadingIcon(Icons.audiotrack, color: Color(0xffbe3266)),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => LibraryTracks()));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => LibraryTracks());
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Albums'.i18n),
|
||||
leading: LeadingIcon(Icons.album, color: Color(0xff4b2e7e)),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => LibraryAlbums()));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => LibraryAlbums());
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Artists'.i18n),
|
||||
leading: LeadingIcon(Icons.recent_actors, color: Color(0xff384697)),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => LibraryArtists()));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => LibraryArtists());
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Playlists'.i18n),
|
||||
leading: LeadingIcon(Icons.playlist_play, color: Color(0xff0880b5)),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => LibraryPlaylists()));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => LibraryPlaylists());
|
||||
},
|
||||
),
|
||||
FreezerDivider(),
|
||||
|
|
@ -130,8 +130,8 @@ class LibraryScreen extends StatelessWidget {
|
|||
title: Text('History'.i18n),
|
||||
leading: LeadingIcon(Icons.history, color: Color(0xff009a85)),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => HistoryScreen()));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => HistoryScreen());
|
||||
},
|
||||
),
|
||||
FreezerDivider(),
|
||||
|
|
@ -142,8 +142,8 @@ class LibraryScreen extends StatelessWidget {
|
|||
onTap: () {
|
||||
//Show progress
|
||||
if (importer.done || importer.busy) {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => ImporterStatusScreen()));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => ImporterStatusScreen());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -161,8 +161,8 @@ class LibraryScreen extends StatelessWidget {
|
|||
.i18n),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => SpotifyImporterV1()));
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => SpotifyImporterV1());
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
|
|
@ -173,8 +173,8 @@ class LibraryScreen extends StatelessWidget {
|
|||
.i18n),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => SpotifyImporterV2()));
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => SpotifyImporterV2());
|
||||
},
|
||||
)
|
||||
],
|
||||
|
|
@ -509,13 +509,13 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
? _sorted[i]
|
||||
: tracks![i];
|
||||
return TrackTile(
|
||||
t,
|
||||
t!,
|
||||
onTap: () {
|
||||
playerHelper.playFromTrackList(
|
||||
(tracks!.length == (trackCount ?? 0))
|
||||
? _sorted
|
||||
: tracks!,
|
||||
t!.id,
|
||||
t.id,
|
||||
QueueSource(
|
||||
id: deezerAPI.favoritesPlaylistId,
|
||||
text: 'Favorites'.i18n,
|
||||
|
|
@ -523,7 +523,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultTrackMenu(t!, onRemove: () {
|
||||
m.defaultTrackMenu(t, onRemove: () {
|
||||
setState(() {
|
||||
tracks!.removeWhere((track) => t.id == track!.id);
|
||||
});
|
||||
|
|
@ -553,19 +553,18 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
...List.generate(allTracks.length, (i) {
|
||||
Track? t = allTracks[i];
|
||||
return TrackTile(
|
||||
t,
|
||||
t!,
|
||||
onTap: () {
|
||||
playerHelper.playFromTrackList(
|
||||
allTracks,
|
||||
t!.id,
|
||||
t.id,
|
||||
QueueSource(
|
||||
id: 'allTracks',
|
||||
text: 'All offline tracks'.i18n,
|
||||
source: 'offline'));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultTrackMenu(t!);
|
||||
MenuSheet(context).defaultTrackMenu(t);
|
||||
},
|
||||
);
|
||||
})
|
||||
|
|
@ -714,8 +713,8 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
|
|||
return AlbumTile(
|
||||
a,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => AlbumDetails(a)));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => AlbumDetails(a));
|
||||
},
|
||||
onHold: () async {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
|
|
@ -751,8 +750,8 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
|
|||
return AlbumTile(
|
||||
a,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => AlbumDetails(a)));
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => AlbumDetails(a));
|
||||
},
|
||||
onHold: () async {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
|
|
@ -919,8 +918,8 @@ class _LibraryArtistsState extends State<LibraryArtists> {
|
|||
return ArtistHorizontalTile(
|
||||
a,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => ArtistDetails(a)));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => ArtistDetails(a));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
|
|
@ -1118,9 +1117,8 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
PlaylistTile(
|
||||
favoritesPlaylist,
|
||||
onTap: () async {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
PlaylistDetails(favoritesPlaylist)));
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => PlaylistDetails(favoritesPlaylist));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
|
|
@ -1134,8 +1132,8 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
Playlist p = _sorted[i];
|
||||
return PlaylistTile(
|
||||
p,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => PlaylistDetails(p))),
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushRoute(builder: (context) => PlaylistDetails(p)),
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultPlaylistMenu(p, onRemove: () {
|
||||
|
|
@ -1175,9 +1173,8 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
Playlist p = playlists[i];
|
||||
return PlaylistTile(
|
||||
p,
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PlaylistDetails(p))),
|
||||
onTap: () => Navigator.of(context).pushRoute(
|
||||
builder: (context) => PlaylistDetails(p)),
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultPlaylistMenu(p, onRemove: () {
|
||||
|
|
|
|||
|
|
@ -215,9 +215,9 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
OutlinedButton(
|
||||
child: Text('Login using browser'.i18n),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) =>
|
||||
LoginBrowser(_update)));
|
||||
LoginBrowser(_update));
|
||||
},
|
||||
),
|
||||
OutlinedButton(
|
||||
|
|
|
|||
|
|
@ -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
248
lib/ui/lyrics_screen.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ import 'dart:async';
|
|||
|
||||
import 'package:audio_service/audio_service.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/translations.i18n.dart';
|
||||
|
||||
|
|
@ -11,7 +13,14 @@ import 'player_screen.dart';
|
|||
|
||||
class PlayerBar extends StatefulWidget {
|
||||
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
|
||||
_PlayerBarState createState() => _PlayerBarState();
|
||||
|
|
@ -21,6 +30,7 @@ class _PlayerBarState extends State<PlayerBar> {
|
|||
final double iconSize = 28;
|
||||
late StreamSubscription mediaItemSub;
|
||||
late bool _isNothingPlaying = audioHandler.mediaItem.value == null;
|
||||
final focusNode = FocusNode();
|
||||
|
||||
double parsePosition(Duration position) {
|
||||
if (audioHandler.mediaItem.value == null) return 0.0;
|
||||
|
|
@ -40,8 +50,12 @@ class _PlayerBarState extends State<PlayerBar> {
|
|||
super.initState();
|
||||
}
|
||||
|
||||
Color get backgroundColor =>
|
||||
widget.backgroundColor ?? Theme.of(context).bottomAppBarColor;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.dispose();
|
||||
mediaItemSub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
|
@ -50,7 +64,6 @@ class _PlayerBarState extends State<PlayerBar> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var focusNode = FocusNode();
|
||||
return _isNothingPlaying
|
||||
? const SizedBox()
|
||||
: GestureDetector(
|
||||
|
|
@ -72,35 +85,35 @@ class _PlayerBarState extends State<PlayerBar> {
|
|||
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||
StreamBuilder<MediaItem?>(
|
||||
stream: audioHandler.mediaItem,
|
||||
initialData: audioHandler.mediaItem.valueOrNull,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) return const SizedBox();
|
||||
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
|
||||
decoration: BoxDecoration(
|
||||
color: focusNode.hasFocus
|
||||
? Colors.black26
|
||||
: Theme.of(context).bottomAppBarColor),
|
||||
? Color.lerp(backgroundColor, Colors.grey, 0.26)
|
||||
: backgroundColor,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
focusNode: focusNode,
|
||||
contentPadding:
|
||||
EdgeInsets.symmetric(horizontal: 8.0),
|
||||
onTap: widget.shouldHandleClicks
|
||||
? () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) =>
|
||||
PlayerScreen()));
|
||||
}
|
||||
? _pushPlayerScreen
|
||||
: null,
|
||||
leading: CachedImage(
|
||||
width: 50,
|
||||
height: 50,
|
||||
url: currentMediaItem.extras!['thumb'] ??
|
||||
audioHandler.mediaItem.value!.artUri
|
||||
as String?,
|
||||
),
|
||||
leading: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: leadingWidget),
|
||||
title: Text(
|
||||
currentMediaItem.displayTitle!,
|
||||
overflow: TextOverflow.clip,
|
||||
|
|
@ -135,8 +148,6 @@ class _PlayerBarState extends State<PlayerBar> {
|
|||
stream: AudioService.position,
|
||||
builder: (context, snapshot) {
|
||||
return LinearProgressIndicator(
|
||||
backgroundColor:
|
||||
Theme.of(context).primaryColor.withOpacity(0.1),
|
||||
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 {
|
||||
|
|
@ -154,8 +174,8 @@ class PrevNextButton extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<List<MediaItem?>>(
|
||||
stream: audioHandler.queue,
|
||||
return StreamBuilder<MediaItem?>(
|
||||
stream: audioHandler.mediaItem,
|
||||
builder: (context, snapshot) {
|
||||
if (!prev) {
|
||||
return IconButton(
|
||||
|
|
@ -165,7 +185,7 @@ class PrevNextButton extends StatelessWidget {
|
|||
),
|
||||
iconSize: size,
|
||||
onPressed:
|
||||
playerHelper.queueIndex == (snapshot.data ?? []).length - 1
|
||||
playerHelper.queueIndex == audioHandler.queue.value.length - 1
|
||||
? null
|
||||
: () => audioHandler.skipToNext(),
|
||||
);
|
||||
|
|
@ -189,7 +209,18 @@ class PrevNextButton extends StatelessWidget {
|
|||
|
||||
class PlayPauseButton extends StatefulWidget {
|
||||
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
|
||||
_PlayPauseButtonState createState() => _PlayPauseButtonState();
|
||||
|
|
@ -197,64 +228,91 @@ class PlayPauseButton extends StatefulWidget {
|
|||
|
||||
class _PlayPauseButtonState extends State<PlayPauseButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
late AnimationController _controller =
|
||||
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
|
||||
void initState() {
|
||||
_controller =
|
||||
AnimationController(vsync: this, duration: Duration(milliseconds: 200));
|
||||
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
|
||||
_subscription = audioHandler.playbackState.listen((playbackState) {
|
||||
if (playbackState.playing ||
|
||||
playbackState.processingState == AudioProcessingState.ready) {
|
||||
if (playbackState.playing)
|
||||
_controller.forward();
|
||||
else
|
||||
_controller.reverse();
|
||||
if (!_canPlay) setState(() => _canPlay = true);
|
||||
return;
|
||||
}
|
||||
setState(() => _canPlay = false);
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription.cancel();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder(
|
||||
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();
|
||||
void _playPause() {
|
||||
if (audioHandler.playbackState.value.playing)
|
||||
audioHandler.pause();
|
||||
else
|
||||
_controller.reverse();
|
||||
|
||||
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());
|
||||
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) {
|
||||
//Stopped/Error
|
||||
case AudioProcessingState.error:
|
||||
case AudioProcessingState.idle:
|
||||
return SizedBox(width: widget.size, height: widget.size);
|
||||
child = null;
|
||||
break;
|
||||
//Loading, connecting, rewinding...
|
||||
default:
|
||||
return SizedBox(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
child = const Center(child: CircularProgressIndicator());
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:flutter/foundation.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/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/definitions.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
import 'package:freezer/page_routes/fade.dart';
|
||||
import 'package:freezer/settings.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
import 'package:freezer/ui/elements.dart';
|
||||
import 'package:freezer/ui/lyrics.dart';
|
||||
import 'package:freezer/ui/cached_image.dart';
|
||||
import 'package:freezer/ui/lyrics_screen.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/tiles.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:marquee/marquee.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
|
||||
import 'cached_image.dart';
|
||||
import '../api/definitions.dart';
|
||||
import 'player_bar.dart';
|
||||
|
||||
import 'dart:ui';
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
//Changing item in queue view and pressing back causes the pageView to skip song
|
||||
bool pageViewLock = false;
|
||||
|
||||
//So can be updated when going back from lyrics
|
||||
late Function updateColor;
|
||||
const _blurStrength = 90.0;
|
||||
|
||||
class PlayerScreen extends StatefulWidget {
|
||||
static const _blurStrength = 50.0;
|
||||
/// A simple [ChangeNotifier] that listens to the [AudioHandler.mediaItem] stream and
|
||||
/// notifies its listeners when background changes
|
||||
class BackgroundProvider extends ChangeNotifier {
|
||||
Color _dominantColor;
|
||||
ImageProvider? _imageProvider;
|
||||
StreamSubscription? _mediaItemSub;
|
||||
BackgroundProvider(this._dominantColor);
|
||||
|
||||
@override
|
||||
_PlayerScreenState createState() => _PlayerScreenState();
|
||||
}
|
||||
|
||||
class _PlayerScreenState extends State<PlayerScreen> {
|
||||
LinearGradient? _bgGradient;
|
||||
late StreamSubscription _mediaItemSub;
|
||||
late StreamSubscription _playerStateSub;
|
||||
ImageProvider? _blurImage;
|
||||
bool _wasConnected = true;
|
||||
|
||||
//Calculate background color
|
||||
Future _updateColor() async {
|
||||
if (!settings.colorGradientBackground! && !settings.blurPlayerBackground!)
|
||||
/// Calculate background color from [mediaItem]
|
||||
///
|
||||
/// Warning: this function is expensive to call, and should only be called when songs change!
|
||||
Future _updateColor(MediaItem mediaItem) async {
|
||||
if (!settings.colorGradientBackground && !settings.blurPlayerBackground)
|
||||
return;
|
||||
final imageProvider = CachedNetworkImageProvider(
|
||||
audioHandler.mediaItem.value!.extras!['thumb'] ??
|
||||
audioHandler.mediaItem.value!.artUri as String);
|
||||
//BG Image
|
||||
if (settings.blurPlayerBackground!)
|
||||
setState(() => _blurImage = imageProvider);
|
||||
|
||||
if (settings.colorGradientBackground!) {
|
||||
mediaItem.extras!['thumb'] ?? mediaItem.artUri as String);
|
||||
//Run in isolate
|
||||
PaletteGenerator palette =
|
||||
await PaletteGenerator.fromImageProvider(imageProvider);
|
||||
|
||||
setState(() => _bgGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
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);
|
||||
_dominantColor = palette.dominantColor!.color;
|
||||
_imageProvider = settings.blurPlayerBackground ? imageProvider : null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future.delayed(Duration(milliseconds: 600), _updateColor);
|
||||
_playbackStateChanged();
|
||||
_mediaItemSub = audioHandler.mediaItem.listen((event) {
|
||||
_playbackStateChanged();
|
||||
_updateColor();
|
||||
void addListener(VoidCallback listener) {
|
||||
_mediaItemSub ??= audioHandler.mediaItem.listen((mediaItem) {
|
||||
if (mediaItem == null) return;
|
||||
_updateColor(mediaItem);
|
||||
});
|
||||
_playerStateSub =
|
||||
audioHandler.playbackState.listen((_) => _playbackStateChanged());
|
||||
super.addListener(listener);
|
||||
}
|
||||
|
||||
updateColor = this._updateColor;
|
||||
super.initState();
|
||||
@override
|
||||
void removeListener(VoidCallback listener) {
|
||||
super.removeListener(listener);
|
||||
if (!hasListeners && _mediaItemSub != null) {
|
||||
_mediaItemSub!.cancel();
|
||||
_mediaItemSub = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_mediaItemSub.cancel();
|
||||
_playerStateSub.cancel();
|
||||
_mediaItemSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Color get dominantColor => _dominantColor;
|
||||
ImageProvider<Object>? get imageProvider => _imageProvider;
|
||||
}
|
||||
|
||||
class PlayerScreen extends StatelessWidget {
|
||||
const PlayerScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasBackground =
|
||||
settings.blurPlayerBackground! || settings.colorGradientBackground!;
|
||||
final color = hasBackground
|
||||
? Colors.transparent
|
||||
: 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(
|
||||
final defaultColor = Theme.of(context).cardColor;
|
||||
return ChangeNotifierProvider(
|
||||
create: (context) => BackgroundProvider(defaultColor),
|
||||
child: PlayerScreenBackground(
|
||||
child: OrientationBuilder(
|
||||
builder: (context, orientation) =>
|
||||
orientation == Orientation.landscape
|
||||
? 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
|
||||
|
|
@ -285,45 +320,8 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||
height: 1000.w,
|
||||
),
|
||||
),
|
||||
PlayerTextSubtext(textSize: 64.sp),
|
||||
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(),
|
||||
PlaybackControls(86.sp),
|
||||
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 {
|
||||
@override
|
||||
_QualityInfoWidgetState createState() => _QualityInfoWidgetState();
|
||||
|
|
@ -433,32 +483,32 @@ class _RepeatButtonState extends State<RepeatButton> {
|
|||
// ignore: missing_return
|
||||
Icon get repeatIcon {
|
||||
switch (playerHelper.repeatType) {
|
||||
case LoopMode.off:
|
||||
case AudioServiceRepeatMode.none:
|
||||
return Icon(
|
||||
Icons.repeat,
|
||||
size: widget.iconSize,
|
||||
semanticLabel: "Repeat off".i18n,
|
||||
);
|
||||
case LoopMode.all:
|
||||
return Icon(
|
||||
Icons.repeat,
|
||||
color: Theme.of(context).primaryColor,
|
||||
size: widget.iconSize,
|
||||
semanticLabel: "Repeat".i18n,
|
||||
);
|
||||
case LoopMode.one:
|
||||
case AudioServiceRepeatMode.one:
|
||||
return Icon(
|
||||
Icons.repeat_one,
|
||||
color: Theme.of(context).primaryColor,
|
||||
size: widget.iconSize,
|
||||
semanticLabel: "Repeat one".i18n,
|
||||
);
|
||||
case AudioServiceRepeatMode.group:
|
||||
case AudioServiceRepeatMode.all:
|
||||
return Icon(
|
||||
Icons.repeat,
|
||||
semanticLabel: "Repeat".i18n,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
color: playerHelper.repeatType == AudioServiceRepeatMode.none
|
||||
? null
|
||||
: Theme.of(context).primaryColor,
|
||||
iconSize: widget.iconSize,
|
||||
icon: repeatIcon,
|
||||
onPressed: () async {
|
||||
await playerHelper.changeRepeat();
|
||||
|
|
@ -468,15 +518,38 @@ class _RepeatButtonState extends State<RepeatButton> {
|
|||
}
|
||||
}
|
||||
|
||||
class PlaybackControls extends StatefulWidget {
|
||||
class ShuffleButton extends StatefulWidget {
|
||||
final double iconSize;
|
||||
PlaybackControls(this.iconSize, {Key? key}) : super(key: key);
|
||||
const ShuffleButton({Key? key, required this.iconSize}) : super(key: key);
|
||||
|
||||
@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 {
|
||||
if (cache.checkTrackFavorite(
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
|
|
@ -499,46 +600,28 @@ class _PlaybackControlsState extends State<PlaybackControls> {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.sentiment_very_dissatisfied,
|
||||
semanticLabel: "Dislike".i18n,
|
||||
),
|
||||
iconSize: widget.iconSize * 0.75,
|
||||
onPressed: () async {
|
||||
await deezerAPI.dislikeTrack(audioHandler.mediaItem.value!.id);
|
||||
if (playerHelper.queueIndex <
|
||||
audioHandler.queue.value.length - 1) {
|
||||
audioHandler.skipToNext();
|
||||
}
|
||||
}),
|
||||
PrevNextButton(widget.iconSize, prev: true),
|
||||
PlayPauseButton(widget.iconSize * 1.25),
|
||||
PrevNextButton(widget.iconSize),
|
||||
IconButton(
|
||||
icon: libraryIcon,
|
||||
iconSize: widget.iconSize * 0.75,
|
||||
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();
|
||||
}
|
||||
},
|
||||
)
|
||||
ShuffleButton(iconSize: size * 0.75),
|
||||
PrevNextButton(size, prev: true),
|
||||
if (settings.enableFilledPlayButton)
|
||||
Consumer<BackgroundProvider>(builder: (context, provider, _) {
|
||||
final color = Theme.of(context).brightness == Brightness.light
|
||||
? provider.dominantColor
|
||||
: darken(provider.dominantColor);
|
||||
return PlayPauseButton(size * 2.25,
|
||||
filled: true,
|
||||
color: color,
|
||||
iconColor: Color.lerp(
|
||||
(ThemeData.estimateBrightnessForColor(color) ==
|
||||
Brightness.light
|
||||
? Colors.black
|
||||
: Colors.white),
|
||||
color,
|
||||
0.25));
|
||||
})
|
||||
else
|
||||
PlayPauseButton(size * 1.25),
|
||||
PrevNextButton(size),
|
||||
RepeatButton(size * 0.75),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -551,18 +634,24 @@ class BigAlbumArt extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _BigAlbumArtState extends State<BigAlbumArt> {
|
||||
PageController _pageController = PageController(
|
||||
final _pageController = PageController(
|
||||
initialPage: playerHelper.queueIndex,
|
||||
viewportFraction: 1.0,
|
||||
);
|
||||
StreamSubscription? _currentItemSub;
|
||||
bool _animationLock = true;
|
||||
bool _animationLock = false;
|
||||
bool _initiatedByUser = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_currentItemSub = audioHandler.mediaItem.listen((event) async {
|
||||
if (_initiatedByUser) {
|
||||
_initiatedByUser = false;
|
||||
return;
|
||||
}
|
||||
if (!_pageController.hasClients) return;
|
||||
print('animating controller to page');
|
||||
_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,
|
||||
duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
|
||||
_animationLock = false;
|
||||
|
|
@ -589,8 +678,10 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
|
|||
PageRouteBuilder(
|
||||
opaque: false, // transparent background
|
||||
barrierDismissible: true,
|
||||
pageBuilder: (context, _, __) {
|
||||
return PhotoView(
|
||||
pageBuilder: (context, animation, __) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: PhotoView(
|
||||
imageProvider: CachedNetworkImageProvider(
|
||||
audioHandler.mediaItem.value!.artUri.toString()),
|
||||
maxScale: 8.0,
|
||||
|
|
@ -598,30 +689,39 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
|
|||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: audioHandler.mediaItem.value!.id),
|
||||
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,
|
||||
onPageChanged: (int index) {
|
||||
if (pageViewLock) {
|
||||
pageViewLock = false;
|
||||
return;
|
||||
}
|
||||
if (_animationLock) return;
|
||||
if (pageViewLock || _animationLock) return;
|
||||
_initiatedByUser = true;
|
||||
audioHandler.skipToQueueItem(index);
|
||||
},
|
||||
children: List.generate(
|
||||
audioHandler.queue.value.length,
|
||||
queue.length,
|
||||
(i) => Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Hero(
|
||||
tag: audioHandler.queue.value[i].id,
|
||||
tag: queue[i].id,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
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),
|
||||
splashRadius: this.iconSize ?? ScreenUtil().setWidth(52),
|
||||
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 {
|
||||
final double size;
|
||||
const BottomBarControls({Key? key, required this.size}) : super(key: key);
|
||||
|
|
@ -902,34 +892,51 @@ class BottomBarControls extends StatelessWidget {
|
|||
size: size,
|
||||
semanticLabel: "Lyrics".i18n,
|
||||
),
|
||||
onPressed: () async {
|
||||
await Navigator.of(context)
|
||||
.push(MaterialPageRoute(builder: (context) => LyricsScreen()));
|
||||
|
||||
updateColor();
|
||||
},
|
||||
),
|
||||
onPressed: () => _pushLyrics(context)),
|
||||
IconButton(
|
||||
iconSize: size,
|
||||
icon: Icon(
|
||||
Icons.file_download,
|
||||
semanticLabel: "Download".i18n,
|
||||
Icons.sentiment_very_dissatisfied,
|
||||
semanticLabel: "Dislike".i18n,
|
||||
),
|
||||
iconSize: size * 0.85,
|
||||
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);
|
||||
},
|
||||
),
|
||||
await deezerAPI.dislikeTrack(audioHandler.mediaItem.value!.id);
|
||||
if (playerHelper.queueIndex <
|
||||
audioHandler.queue.value.length - 1) {
|
||||
audioHandler.skipToNext();
|
||||
}
|
||||
}),
|
||||
// IconButton(
|
||||
// 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(),
|
||||
RepeatButton(size),
|
||||
FavoriteButton(size: size * 0.85),
|
||||
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
141
lib/ui/queue_screen.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -75,11 +75,11 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => SearchResultsScreen(
|
||||
_query,
|
||||
offline: _offline,
|
||||
)));
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -253,14 +253,14 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
color: Color(0xff7c42bb),
|
||||
text: 'Shows'.i18n,
|
||||
icon: Icon(FontAwesome5.podcast),
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
onTap: () => Navigator.of(context).pushRoute(
|
||||
builder: (context) => Scaffold(
|
||||
appBar: FreezerAppBar('Shows'.i18n),
|
||||
body: SingleChildScrollView(
|
||||
child: HomePageScreen(
|
||||
channel: DeezerChannel(target: 'shows'))),
|
||||
),
|
||||
)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
|
@ -272,7 +272,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
color: Color(0xffff555d),
|
||||
icon: Icon(FontAwesome5.chart_line),
|
||||
text: 'Charts'.i18n,
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
onTap: () => Navigator.of(context).pushRoute(
|
||||
builder: (context) => Scaffold(
|
||||
appBar: FreezerAppBar('Charts'.i18n),
|
||||
body: SingleChildScrollView(
|
||||
|
|
@ -280,13 +280,13 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
channel:
|
||||
DeezerChannel(target: 'channels/charts'))),
|
||||
),
|
||||
)),
|
||||
),
|
||||
),
|
||||
SearchBrowseCard(
|
||||
color: Color(0xff2c4ea7),
|
||||
text: 'Browse'.i18n,
|
||||
icon: Image.asset('assets/browse_icon.png', width: 26.0),
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
onTap: () => Navigator.of(context).pushRoute(
|
||||
builder: (context) => Scaffold(
|
||||
appBar: FreezerAppBar('Browse'.i18n),
|
||||
body: SingleChildScrollView(
|
||||
|
|
@ -294,7 +294,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
channel:
|
||||
DeezerChannel(target: 'channels/explore'))),
|
||||
),
|
||||
)),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
@ -337,8 +337,8 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
return AlbumTile(
|
||||
data,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => AlbumDetails(data)));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => AlbumDetails(data));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
|
|
@ -350,8 +350,8 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
return ArtistHorizontalTile(
|
||||
data,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => ArtistDetails(data)));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => ArtistDetails(data));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
|
|
@ -363,8 +363,8 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
return PlaylistTile(
|
||||
data,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => PlaylistDetails(data)));
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => PlaylistDetails(data));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
|
|
@ -535,13 +535,13 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
ListTile(
|
||||
title: Text('Show all tracks'.i18n),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => TrackListScreen(
|
||||
results.tracks,
|
||||
QueueSource(
|
||||
id: query,
|
||||
source: 'search',
|
||||
text: 'Search'.i18n))));
|
||||
text: 'Search'.i18n)));
|
||||
},
|
||||
),
|
||||
FreezerDivider()
|
||||
|
|
@ -577,16 +577,16 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
},
|
||||
onTap: () {
|
||||
cache.addToSearchHistory(a);
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => AlbumDetails(a)));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => AlbumDetails(a));
|
||||
},
|
||||
);
|
||||
}),
|
||||
ListTile(
|
||||
title: Text('Show all albums'.i18n),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => AlbumListScreen(results.albums)));
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => AlbumListScreen(results.albums));
|
||||
},
|
||||
),
|
||||
FreezerDivider()
|
||||
|
|
@ -617,8 +617,8 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
a,
|
||||
onTap: () {
|
||||
cache.addToSearchHistory(a);
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => ArtistDetails(a)));
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => ArtistDetails(a));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
|
|
@ -656,8 +656,8 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
p,
|
||||
onTap: () {
|
||||
cache.addToSearchHistory(p);
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => PlaylistDetails(p)));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => PlaylistDetails(p));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
|
|
@ -668,9 +668,9 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
ListTile(
|
||||
title: Text('Show all playlists'.i18n),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) =>
|
||||
SearchResultPlaylists(results.playlists)));
|
||||
SearchResultPlaylists(results.playlists));
|
||||
},
|
||||
),
|
||||
FreezerDivider()
|
||||
|
|
@ -701,16 +701,16 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
return ShowTile(
|
||||
s,
|
||||
onTap: () async {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => ShowScreen(s)));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => ShowScreen(s));
|
||||
},
|
||||
);
|
||||
}),
|
||||
ListTile(
|
||||
title: Text('Show all shows'.i18n),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => ShowListScreen(results.shows)));
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) => ShowListScreen(results.shows));
|
||||
},
|
||||
),
|
||||
FreezerDivider()
|
||||
|
|
@ -762,9 +762,9 @@ class SearchResultsScreen extends StatelessWidget {
|
|||
ListTile(
|
||||
title: Text('Show all episodes'.i18n),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
Navigator.of(context).pushRoute(
|
||||
builder: (context) =>
|
||||
EpisodeListScreen(results.episodes)));
|
||||
EpisodeListScreen(results.episodes));
|
||||
})
|
||||
];
|
||||
}
|
||||
|
|
@ -816,15 +816,15 @@ class TrackListScreen extends StatelessWidget {
|
|||
body: ListView.builder(
|
||||
itemCount: tracks!.length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
Track? t = tracks![i];
|
||||
Track t = tracks![i]!;
|
||||
return TrackTile(
|
||||
t,
|
||||
onTap: () {
|
||||
playerHelper.playFromTrackList(tracks!, t!.id, queueSource);
|
||||
playerHelper.playFromTrackList(tracks!, t.id, queueSource);
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultTrackMenu(t!);
|
||||
m.defaultTrackMenu(t);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
@ -849,8 +849,8 @@ class AlbumListScreen extends StatelessWidget {
|
|||
return AlbumTile(
|
||||
a,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => AlbumDetails(a)));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => AlbumDetails(a));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
|
|
@ -878,8 +878,8 @@ class SearchResultPlaylists extends StatelessWidget {
|
|||
return PlaylistTile(
|
||||
p,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => PlaylistDetails(p)));
|
||||
Navigator.of(context)
|
||||
.pushRoute(builder: (context) => PlaylistDetails(p));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
|
|
|
|||
|
|
@ -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/web_symbols_icons.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezer/api/definitions.dart';
|
||||
import 'package:package_info/package_info.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:scrobblenaut/scrobblenaut.dart';
|
||||
|
|
@ -39,33 +40,33 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
ListTile(
|
||||
title: Text('General'.i18n),
|
||||
leading: LeadingIcon(Icons.settings, color: Color(0xffeca704)),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => GeneralSettings())),
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushRoute(builder: (context) => GeneralSettings()),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Download Settings'.i18n),
|
||||
leading:
|
||||
LeadingIcon(Icons.cloud_download, color: Color(0xffbe3266)),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => DownloadsSettings())),
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushRoute(builder: (context) => DownloadsSettings()),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Appearance'.i18n),
|
||||
leading: LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)),
|
||||
onTap: () => Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => AppearanceSettings())),
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushRoute(builder: (context) => AppearanceSettings()),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Quality'.i18n),
|
||||
leading: LeadingIcon(Icons.high_quality, color: Color(0xff384697)),
|
||||
onTap: () => Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => QualitySettings())),
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushRoute(builder: (context) => QualitySettings()),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Deezer'.i18n),
|
||||
leading: LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)),
|
||||
onTap: () => Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => DeezerSettings())),
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushRoute(builder: (context) => DeezerSettings()),
|
||||
),
|
||||
//Language select
|
||||
ListTile(
|
||||
|
|
@ -111,14 +112,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
ListTile(
|
||||
title: Text('Updates'.i18n),
|
||||
leading: LeadingIcon(Icons.update, color: Color(0xff2ba766)),
|
||||
onTap: () => Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => UpdaterScreen())),
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushRoute(builder: (context) => UpdaterScreen()),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('About'.i18n),
|
||||
leading: LeadingIcon(Icons.info, color: Colors.grey),
|
||||
onTap: () => Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => CreditsScreen())),
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushRoute(builder: (context) => CreditsScreen()),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -143,7 +144,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
ListTile(
|
||||
title: Text('Theme'.i18n),
|
||||
subtitle: Text('Currently'.i18n +
|
||||
': ${settings.theme.toString().split('.').last}'),
|
||||
': ${settings.theme.toString().split('.').lastItem}'),
|
||||
leading: Icon(Icons.color_lens),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
|
|
@ -195,7 +196,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
),
|
||||
SwitchListTile(
|
||||
title: Text('Use system theme'.i18n),
|
||||
value: settings.useSystemTheme!,
|
||||
value: settings.useSystemTheme,
|
||||
onChanged: (bool v) async {
|
||||
settings.useSystemTheme = v;
|
||||
|
||||
|
|
@ -206,7 +207,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
ListTile(
|
||||
title: Text('Font'.i18n),
|
||||
leading: Icon(Icons.font_download),
|
||||
subtitle: Text(settings.font!),
|
||||
subtitle: Text(settings.font),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
|
|
@ -217,7 +218,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
SwitchListTile(
|
||||
title: Text('Player gradient background'.i18n),
|
||||
secondary: Icon(Icons.colorize),
|
||||
value: settings.colorGradientBackground!,
|
||||
value: settings.colorGradientBackground,
|
||||
onChanged: (bool v) async {
|
||||
setState(() => settings.colorGradientBackground = v);
|
||||
await settings.save();
|
||||
|
|
@ -227,19 +228,97 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
title: Text('Blur player background'.i18n),
|
||||
subtitle: Text('Might have impact on performance'.i18n),
|
||||
secondary: Icon(Icons.blur_on),
|
||||
value: settings.blurPlayerBackground!,
|
||||
value: settings.blurPlayerBackground,
|
||||
onChanged: (bool v) async {
|
||||
setState(() => settings.blurPlayerBackground = v);
|
||||
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(
|
||||
title: Text('Visualizer'.i18n),
|
||||
subtitle: Text(
|
||||
'Show visualizers on lyrics page. WARNING: Requires microphone permission!'
|
||||
.i18n),
|
||||
secondary: Icon(Icons.equalizer),
|
||||
value: settings.lyricsVisualizer!,
|
||||
value: settings.lyricsVisualizer,
|
||||
onChanged: null, // TODO: visualizer
|
||||
//(bool v) async {
|
||||
// if (await Permission.microphone.request().isGranted) {
|
||||
|
|
@ -454,7 +533,7 @@ class QualityPicker extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _QualityPickerState extends State<QualityPicker> {
|
||||
AudioQuality? _quality;
|
||||
late AudioQuality _quality;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -481,7 +560,7 @@ class _QualityPickerState extends State<QualityPicker> {
|
|||
}
|
||||
|
||||
//Update quality in settings
|
||||
void _updateQuality(AudioQuality? q) async {
|
||||
void _updateQuality(AudioQuality q) async {
|
||||
setState(() {
|
||||
_quality = q;
|
||||
});
|
||||
|
|
@ -786,7 +865,7 @@ class DownloadsSettings extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _DownloadsSettingsState extends State<DownloadsSettings> {
|
||||
double _downloadThreads = settings.downloadThreads!.toDouble();
|
||||
double _downloadThreads = settings.downloadThreads.toDouble();
|
||||
TextEditingController _artistSeparatorController =
|
||||
TextEditingController(text: settings.artistSeparator);
|
||||
|
||||
|
|
@ -809,14 +888,14 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
settings.save();
|
||||
});
|
||||
//Navigate
|
||||
// Navigator.of(context).push(MaterialPageRoute(
|
||||
// Navigator.of(context).pushRoute(
|
||||
// builder: (context) => DirectoryPicker(
|
||||
// settings.downloadPath,
|
||||
// onSelect: (String p) async {
|
||||
// setState(() => settings.downloadPath = p);
|
||||
// await settings.save();
|
||||
// },
|
||||
// )));
|
||||
// ));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
|
|
@ -871,7 +950,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
_downloadThreads = val;
|
||||
setState(() {
|
||||
settings.downloadThreads = _downloadThreads.round();
|
||||
_downloadThreads = settings.downloadThreads!.toDouble();
|
||||
_downloadThreads = settings.downloadThreads.toDouble();
|
||||
});
|
||||
await settings.save();
|
||||
|
||||
|
|
@ -902,12 +981,12 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
ListTile(
|
||||
title: Text('Tags'.i18n),
|
||||
leading: Icon(Icons.label),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => TagSelectionScreen())),
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushRoute(builder: (context) => TagSelectionScreen()),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text('Create folders for artist'.i18n),
|
||||
value: settings.artistFolder!,
|
||||
value: settings.artistFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.artistFolder = v);
|
||||
settings.save();
|
||||
|
|
@ -916,7 +995,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
),
|
||||
SwitchListTile(
|
||||
title: Text('Create folders for albums'.i18n),
|
||||
value: settings.albumFolder!,
|
||||
value: settings.albumFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.albumFolder = v);
|
||||
settings.save();
|
||||
|
|
@ -924,7 +1003,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
secondary: Icon(Icons.folder)),
|
||||
SwitchListTile(
|
||||
title: Text('Create folder for playlist'.i18n),
|
||||
value: settings.playlistFolder!,
|
||||
value: settings.playlistFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.playlistFolder = v);
|
||||
settings.save();
|
||||
|
|
@ -933,7 +1012,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
FreezerDivider(),
|
||||
SwitchListTile(
|
||||
title: Text('Separate albums by discs'.i18n),
|
||||
value: settings.albumDiscFolder!,
|
||||
value: settings.albumDiscFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.albumDiscFolder = v);
|
||||
settings.save();
|
||||
|
|
@ -941,7 +1020,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
secondary: Icon(Icons.album)),
|
||||
SwitchListTile(
|
||||
title: Text('Overwrite already downloaded files'.i18n),
|
||||
value: settings.overwriteDownload!,
|
||||
value: settings.overwriteDownload,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.overwriteDownload = v);
|
||||
settings.save();
|
||||
|
|
@ -949,7 +1028,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
secondary: Icon(Icons.delete)),
|
||||
SwitchListTile(
|
||||
title: Text('Download .LRC lyrics'.i18n),
|
||||
value: settings.downloadLyrics!,
|
||||
value: settings.downloadLyrics,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.downloadLyrics = v);
|
||||
settings.save();
|
||||
|
|
@ -958,7 +1037,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
FreezerDivider(),
|
||||
SwitchListTile(
|
||||
title: Text('Save cover file for every track'.i18n),
|
||||
value: settings.trackCover!,
|
||||
value: settings.trackCover,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.trackCover = v);
|
||||
settings.save();
|
||||
|
|
@ -966,7 +1045,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
secondary: Icon(Icons.image)),
|
||||
SwitchListTile(
|
||||
title: Text('Save album cover'.i18n),
|
||||
value: settings.albumCover!,
|
||||
value: settings.albumCover,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.albumCover = v);
|
||||
settings.save();
|
||||
|
|
@ -990,6 +1069,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
))
|
||||
.toList(),
|
||||
onChanged: (int? n) async {
|
||||
if (n == null) return;
|
||||
setState(() {
|
||||
settings.albumArtResolution = n;
|
||||
});
|
||||
|
|
@ -1000,7 +1080,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
title: Text('Create .nomedia files'.i18n),
|
||||
subtitle:
|
||||
Text('To prevent gallery being filled with album art'.i18n),
|
||||
value: settings.nomediaFiles!,
|
||||
value: settings.nomediaFiles,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.nomediaFiles = v);
|
||||
settings.save();
|
||||
|
|
@ -1024,8 +1104,8 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
ListTile(
|
||||
title: Text('Download Log'.i18n),
|
||||
leading: Icon(Icons.sticky_note_2),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => DownloadLogViewer())),
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushRoute(builder: (context) => DownloadLogViewer()),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
|
@ -1074,13 +1154,13 @@ class _TagSelectionScreenState extends State<TagSelectionScreen> {
|
|||
(i) => ListTile(
|
||||
title: Text(tags[i].title),
|
||||
leading: Switch(
|
||||
value: settings.tags!.contains(tags[i].value),
|
||||
value: settings.tags.contains(tags[i].value),
|
||||
onChanged: (v) async {
|
||||
//Update
|
||||
if (v)
|
||||
settings.tags!.add(tags[i].value);
|
||||
settings.tags.add(tags[i].value);
|
||||
else
|
||||
settings.tags!.remove(tags[i].value);
|
||||
settings.tags.remove(tags[i].value);
|
||||
setState(() {});
|
||||
await settings.save();
|
||||
},
|
||||
|
|
@ -1116,7 +1196,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
deezerAPI.authorize()!.then((v) {
|
||||
deezerAPI.authorize().then((v) {
|
||||
if (v) {
|
||||
setState(() => settings.offlineMode = false);
|
||||
} else {
|
||||
|
|
@ -1131,11 +1211,8 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||
});
|
||||
return AlertDialog(
|
||||
title: Text('Logging in...'.i18n),
|
||||
content: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[CircularProgressIndicator()],
|
||||
));
|
||||
content:
|
||||
const Center(child: CircularProgressIndicator()));
|
||||
});
|
||||
},
|
||||
),
|
||||
|
|
@ -1145,7 +1222,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||
'Might enable some equalizer apps to work. Requires restart of Freezer'
|
||||
.i18n),
|
||||
secondary: Icon(Icons.equalizer),
|
||||
value: settings.enableEqualizer!,
|
||||
value: settings.enableEqualizer,
|
||||
onChanged: (v) async {
|
||||
setState(() => settings.enableEqualizer = v);
|
||||
settings.save();
|
||||
|
|
@ -1155,7 +1232,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||
title: Text('Ignore interruptions'.i18n),
|
||||
subtitle: Text('Requires app restart to apply!'.i18n),
|
||||
secondary: Icon(Icons.not_interested),
|
||||
value: settings.ignoreInterruptions!,
|
||||
value: settings.ignoreInterruptions,
|
||||
onChanged: (bool v) async {
|
||||
setState(() => settings.ignoreInterruptions = v);
|
||||
await settings.save();
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import 'cached_image.dart';
|
|||
import 'dart:async';
|
||||
|
||||
class TrackTile extends StatefulWidget {
|
||||
final Track? track;
|
||||
final Track track;
|
||||
final void Function()? onTap;
|
||||
final void Function()? onHold;
|
||||
final Widget? trailing;
|
||||
|
|
@ -25,7 +25,7 @@ class TrackTile extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _TrackTileState extends State<TrackTile> {
|
||||
StreamSubscription? _subscription;
|
||||
late StreamSubscription _subscription;
|
||||
bool _isOffline = false;
|
||||
bool _isHighlighted = false;
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ class _TrackTileState extends State<TrackTile> {
|
|||
//Listen to media item changes, update text color if currently playing
|
||||
_subscription = audioHandler.mediaItem.listen((mediaItem) {
|
||||
if (mediaItem == null) return;
|
||||
if (mediaItem.id == widget.track?.id)
|
||||
if (mediaItem.id == widget.track.id && !_isHighlighted)
|
||||
setState(() => _isHighlighted = true);
|
||||
else if (_isHighlighted) setState(() => _isHighlighted = false);
|
||||
});
|
||||
|
|
@ -48,7 +48,7 @@ class _TrackTileState extends State<TrackTile> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
_subscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -56,18 +56,18 @@ class _TrackTileState extends State<TrackTile> {
|
|||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
widget.track!.title!,
|
||||
widget.track.title!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.clip,
|
||||
style: TextStyle(
|
||||
color: _isHighlighted ? Theme.of(context).primaryColor : null),
|
||||
),
|
||||
subtitle: Text(
|
||||
widget.track!.artistString,
|
||||
widget.track.artistString,
|
||||
maxLines: 1,
|
||||
),
|
||||
leading: CachedImage(
|
||||
url: widget.track!.albumArt!.thumb!,
|
||||
url: widget.track.albumArt!.thumb!,
|
||||
width: 48,
|
||||
),
|
||||
onTap: widget.onTap,
|
||||
|
|
@ -84,7 +84,7 @@ class _TrackTileState extends State<TrackTile> {
|
|||
size: 12.0,
|
||||
),
|
||||
),
|
||||
if (widget.track!.explicit ?? false)
|
||||
if (widget.track.explicit ?? false)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 2.0),
|
||||
child: Text(
|
||||
|
|
@ -95,7 +95,7 @@ class _TrackTileState extends State<TrackTile> {
|
|||
Container(
|
||||
width: 42.0,
|
||||
child: Text(
|
||||
widget.track!.durationString,
|
||||
widget.track.durationString,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
|
@ -108,8 +108,8 @@ class _TrackTileState extends State<TrackTile> {
|
|||
|
||||
class AlbumTile extends StatelessWidget {
|
||||
final Album? album;
|
||||
final Function? onTap;
|
||||
final Function? onHold;
|
||||
final void Function()? onTap;
|
||||
final void Function()? onHold;
|
||||
final Widget? trailing;
|
||||
|
||||
AlbumTile(this.album, {this.onTap, this.onHold, this.trailing});
|
||||
|
|
@ -129,8 +129,8 @@ class AlbumTile extends StatelessWidget {
|
|||
url: album!.art!.thumb,
|
||||
width: 48,
|
||||
),
|
||||
onTap: onTap as void Function()?,
|
||||
onLongPress: onHold as void Function()?,
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
|
|
@ -138,8 +138,8 @@ class AlbumTile extends StatelessWidget {
|
|||
|
||||
class ArtistTile extends StatelessWidget {
|
||||
final Artist? artist;
|
||||
final Function? onTap;
|
||||
final Function? onHold;
|
||||
final void Function()? onTap;
|
||||
final void Function()? onHold;
|
||||
|
||||
ArtistTile(this.artist, {this.onTap, this.onHold});
|
||||
|
||||
|
|
@ -147,45 +147,33 @@ class ArtistTile extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 150,
|
||||
child: Container(
|
||||
child: InkWell(
|
||||
onTap: onTap as void Function()?,
|
||||
onLongPress: onHold as void Function()?,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
height: 4,
|
||||
),
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||
const SizedBox(height: 4),
|
||||
CachedImage(
|
||||
url: artist!.picture!.thumb,
|
||||
circular: true,
|
||||
width: 100,
|
||||
),
|
||||
Container(
|
||||
height: 8,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
artist!.name!,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 14.0),
|
||||
style: const TextStyle(fontSize: 14.0),
|
||||
),
|
||||
Container(
|
||||
height: 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
const SizedBox(height: 4),
|
||||
])));
|
||||
}
|
||||
}
|
||||
|
||||
class PlaylistTile extends StatelessWidget {
|
||||
final Playlist? playlist;
|
||||
final Function? onTap;
|
||||
final Function? onHold;
|
||||
final void Function()? onTap;
|
||||
final void Function()? onHold;
|
||||
final Widget? trailing;
|
||||
|
||||
PlaylistTile(this.playlist, {this.onHold, this.onTap, this.trailing});
|
||||
|
|
@ -216,8 +204,8 @@ class PlaylistTile extends StatelessWidget {
|
|||
url: playlist!.image!.thumb,
|
||||
width: 48,
|
||||
),
|
||||
onTap: onTap as void Function()?,
|
||||
onLongPress: onHold as void Function()?,
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
trailing: trailing,
|
||||
);
|
||||
}
|
||||
|
|
@ -225,8 +213,8 @@ class PlaylistTile extends StatelessWidget {
|
|||
|
||||
class ArtistHorizontalTile extends StatelessWidget {
|
||||
final Artist? artist;
|
||||
final Function? onTap;
|
||||
final Function? onHold;
|
||||
final void Function()? onTap;
|
||||
final void Function()? onHold;
|
||||
final Widget? trailing;
|
||||
|
||||
ArtistHorizontalTile(this.artist, {this.onHold, this.onTap, this.trailing});
|
||||
|
|
@ -244,8 +232,8 @@ class ArtistHorizontalTile extends StatelessWidget {
|
|||
url: artist!.picture!.thumb,
|
||||
circular: true,
|
||||
),
|
||||
onTap: onTap as void Function()?,
|
||||
onLongPress: onHold as void Function()?,
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
trailing: trailing,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
14
pubspec.lock
14
pubspec.lock
|
|
@ -608,6 +608,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
numberpicker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -762,6 +769,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.2.3"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ dependencies:
|
|||
url: https://github.com/ryanheise/just_audio.git
|
||||
ref: dev
|
||||
path: just_audio/
|
||||
provider: ^6.0.0
|
||||
|
||||
dependency_overrides:
|
||||
analyzer: ^2.0.0
|
||||
|
|
|
|||
Loading…
Reference in a new issue