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