0.6.9
This commit is contained in:
parent
ff239aaf86
commit
66bfd5eb70
173 changed files with 912 additions and 32459 deletions
|
@ -45,34 +45,6 @@ class Track {
|
|||
String get artistString => artists.map<String>((art) => art.name).join(', ');
|
||||
String get durationString => "${duration.inMinutes}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
|
||||
|
||||
String getUrl(int quality) {
|
||||
var md5 = crypto.md5;
|
||||
int magic = 164;
|
||||
List<int> _s1 = [
|
||||
...utf8.encode(playbackDetails[0]),
|
||||
magic,
|
||||
...utf8.encode(quality.toString()),
|
||||
magic,
|
||||
...utf8.encode(id),
|
||||
magic,
|
||||
...utf8.encode(playbackDetails[1])
|
||||
];
|
||||
List<int> _s2 = [
|
||||
...utf8.encode(HEX.encode(md5.convert(_s1).bytes)),
|
||||
magic,
|
||||
..._s1,
|
||||
magic
|
||||
];
|
||||
while(_s2.length%16 > 0) _s2.add(46);
|
||||
String _s3 = '';
|
||||
BlockCipher cipher = ECBBlockCipher(AESFastEngine());
|
||||
cipher.init(true, KeyParameter(Uint8List.fromList('jo6aey6haid2Teih'.codeUnits)));
|
||||
for (int i=0; i<_s2.length/16; i++) {
|
||||
_s3 += HEX.encode(cipher.process(Uint8List.fromList(_s2.sublist(i*16, i*16+16))));
|
||||
}
|
||||
return 'https://e-cdns-proxy-${playbackDetails[0][0]}.dzcdn.net/mobile/1/$_s3';
|
||||
}
|
||||
|
||||
//MediaItem
|
||||
MediaItem toMediaItem() => MediaItem(
|
||||
title: this.title,
|
||||
|
|
|
@ -496,7 +496,7 @@ class DownloadManager {
|
|||
path = p.join(path, sanitize(playlistName));
|
||||
|
||||
if (settings.artistFolder)
|
||||
path = p.join(path, '%artist%');
|
||||
path = p.join(path, '%albumArtist%');
|
||||
|
||||
//Album folder / with disk number
|
||||
if (settings.albumFolder) {
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
import 'package:equalizer/equalizer.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
|
@ -10,6 +13,7 @@ import 'package:path/path.dart' as p;
|
|||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
import 'package:scrobblenaut/scrobblenaut.dart';
|
||||
import 'package:extended_math/extended_math.dart';
|
||||
|
||||
import 'definitions.dart';
|
||||
import '../settings.dart';
|
||||
|
@ -29,6 +33,14 @@ class PlayerHelper {
|
|||
LoopMode repeatType = LoopMode.off;
|
||||
Timer _timer;
|
||||
Scrobblenaut scrobblenaut;
|
||||
int audioSession;
|
||||
int _prevAudioSession;
|
||||
bool equalizerOpen = false;
|
||||
|
||||
//Visualizer
|
||||
StreamController _visualizerController = StreamController.broadcast();
|
||||
Stream get visualizerStream => _visualizerController.stream;
|
||||
|
||||
//Find queue index by id
|
||||
int get queueIndex => AudioService.queue == null ? 0 : AudioService.queue.indexWhere((mi) => mi.id == AudioService.currentMediaItem?.id??'Random string so it returns -1');
|
||||
|
||||
|
@ -36,37 +48,63 @@ class PlayerHelper {
|
|||
//Subscribe to custom events
|
||||
_customEventSubscription = AudioService.customEventStream.listen((event) async {
|
||||
if (!(event is Map)) return;
|
||||
if (event['action'] == 'onLoad') {
|
||||
//After audio_service is loaded, load queue, set quality
|
||||
await settings.updateAudioServiceQuality();
|
||||
await AudioService.customAction('load');
|
||||
return;
|
||||
}
|
||||
if (event['action'] == 'onRestore') {
|
||||
//Load queueSource from isolate
|
||||
this.queueSource = QueueSource.fromJson(event['queueSource']);
|
||||
repeatType = LoopMode.values[event['loopMode']];
|
||||
}
|
||||
if (event['action'] == 'queueEnd') {
|
||||
//If last song is played, load more queue
|
||||
this.queueSource = QueueSource.fromJson(event['queueSource']);
|
||||
onQueueEnd();
|
||||
return;
|
||||
}
|
||||
//Android auto get screen
|
||||
if (event['action'] == 'screenAndroidAuto') {
|
||||
AndroidAuto androidAuto = AndroidAuto();
|
||||
List<MediaItem> data = await androidAuto.getScreen(event['id']);
|
||||
await AudioService.customAction('screenAndroidAuto', jsonEncode(data));
|
||||
}
|
||||
//Android auto play list
|
||||
if (event['action'] == 'tracksAndroidAuto') {
|
||||
AndroidAuto androidAuto = AndroidAuto();
|
||||
await androidAuto.playItem(event['id']);
|
||||
switch (event['action']) {
|
||||
case 'onLoad':
|
||||
//After audio_service is loaded, load queue, set quality
|
||||
await settings.updateAudioServiceQuality();
|
||||
await AudioService.customAction('load');
|
||||
break;
|
||||
case 'onRestore':
|
||||
//Load queueSource from isolate
|
||||
this.queueSource = QueueSource.fromJson(event['queueSource']);
|
||||
repeatType = LoopMode.values[event['loopMode']];
|
||||
break;
|
||||
case 'queueEnd':
|
||||
//If last song is played, load more queue
|
||||
this.queueSource = QueueSource.fromJson(event['queueSource']);
|
||||
// onQueueEnd();
|
||||
break;
|
||||
case 'screenAndroidAuto':
|
||||
AndroidAuto androidAuto = AndroidAuto();
|
||||
List<MediaItem> data = await androidAuto.getScreen(event['id']);
|
||||
await AudioService.customAction('screenAndroidAuto', jsonEncode(data));
|
||||
break;
|
||||
case 'tracksAndroidAuto':
|
||||
AndroidAuto androidAuto = AndroidAuto();
|
||||
await androidAuto.playItem(event['id']);
|
||||
break;
|
||||
case 'audioSession':
|
||||
if (!settings.enableEqualizer) break;
|
||||
//Save
|
||||
_prevAudioSession = audioSession;
|
||||
audioSession = event['id'];
|
||||
if (audioSession == null)
|
||||
break;
|
||||
//Open EQ
|
||||
if (!equalizerOpen) {
|
||||
Equalizer.open(event['id']);
|
||||
equalizerOpen = true;
|
||||
break;
|
||||
}
|
||||
//Change session id
|
||||
if (_prevAudioSession != audioSession) {
|
||||
if (_prevAudioSession != null) Equalizer.removeAudioSessionId(_prevAudioSession);
|
||||
Equalizer.setAudioSessionId(audioSession);
|
||||
}
|
||||
break;
|
||||
//Visualizer data
|
||||
case 'visualizer':
|
||||
_visualizerController.add(event['data']);
|
||||
break;
|
||||
}
|
||||
|
||||
});
|
||||
_mediaItemSubscription = AudioService.currentMediaItemStream.listen((event) {
|
||||
if (event == null) return;
|
||||
//Load more flow if index-1 song
|
||||
if (queueIndex == AudioService.queue.length-1)
|
||||
onQueueEnd();
|
||||
|
||||
//Save queue
|
||||
AudioService.customAction('saveQueue');
|
||||
//Add to history
|
||||
|
@ -184,10 +222,12 @@ class PlayerHelper {
|
|||
case 'flow':
|
||||
tracks = await deezerAPI.flow();
|
||||
break;
|
||||
case 'smartradio': //SmartRadio/Artist radio
|
||||
//SmartRadio/Artist radio
|
||||
case 'smartradio':
|
||||
tracks = await deezerAPI.smartRadio(queueSource.id);
|
||||
break;
|
||||
case 'libraryshuffle': //Library shuffle
|
||||
//Library shuffle
|
||||
case 'libraryshuffle':
|
||||
tracks = await deezerAPI.libraryShuffle(start: AudioService.queue.length);
|
||||
break;
|
||||
case 'mix':
|
||||
|
@ -197,12 +237,13 @@ class PlayerHelper {
|
|||
tracks.removeWhere((track) => queueIds.contains(track.id));
|
||||
break;
|
||||
default:
|
||||
print(queueSource.toJson());
|
||||
// print(queueSource.toJson());
|
||||
break;
|
||||
}
|
||||
|
||||
List<MediaItem> mi = tracks.map<MediaItem>((t) => t.toMediaItem()).toList();
|
||||
await AudioService.addQueueItems(mi);
|
||||
AudioService.skipToNext();
|
||||
// AudioService.skipToNext();
|
||||
}
|
||||
|
||||
//Play track from album
|
||||
|
@ -308,6 +349,15 @@ class PlayerHelper {
|
|||
await AudioService.customAction('reorder', [oldIndex, newIndex]);
|
||||
}
|
||||
|
||||
//Start visualizer
|
||||
Future startVisualizer() async {
|
||||
await AudioService.customAction('startVisualizer');
|
||||
}
|
||||
//Stop visualizer
|
||||
Future stopVisualizer() async {
|
||||
await AudioService.customAction('stopVisualizer');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void backgroundTaskEntrypoint() async {
|
||||
|
@ -329,6 +379,8 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
|
||||
//Stream subscriptions
|
||||
StreamSubscription _eventSub;
|
||||
StreamSubscription _audioSessionSub;
|
||||
StreamSubscription _visualizerSubscription;
|
||||
|
||||
//Loaded from file/frontend
|
||||
int mobileQuality;
|
||||
|
@ -391,6 +443,11 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
}
|
||||
});
|
||||
|
||||
//Audio session
|
||||
_audioSessionSub = _player.androidAudioSessionIdStream.listen((event) {
|
||||
AudioServiceBackground.sendCustomEvent({"action": 'audioSession', "id": event});
|
||||
});
|
||||
|
||||
//Load queue
|
||||
AudioServiceBackground.setQueue(_queue);
|
||||
AudioServiceBackground.sendCustomEvent({'action': 'onLoad'});
|
||||
|
@ -472,8 +529,6 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
if (_queueIndex == 0) return;
|
||||
//Update buffering state
|
||||
_skipState = AudioProcessingState.skippingToPrevious;
|
||||
|
||||
|
||||
//Normal skip to previous
|
||||
_queueIndex--;
|
||||
await _player.seekToPrevious();
|
||||
|
@ -566,9 +621,14 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
//just_audio
|
||||
_player.stop();
|
||||
if (_audioSource != null) _audioSource.clear();
|
||||
//audio_service
|
||||
this._queue = q;
|
||||
AudioServiceBackground.setQueue(_queue);
|
||||
//Filter duplicate IDs
|
||||
List<MediaItem> queue = [];
|
||||
for (MediaItem mi in q) {
|
||||
if (queue.indexWhere((m) => mi.id == m.id) == -1)
|
||||
queue.add(mi);
|
||||
}
|
||||
this._queue = queue;
|
||||
AudioServiceBackground.setQueue(queue);
|
||||
//Load
|
||||
await _loadQueue();
|
||||
//await _player.seek(Duration.zero, index: 0);
|
||||
|
@ -635,68 +695,102 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
//Custom actions
|
||||
@override
|
||||
Future onCustomAction(String name, dynamic args) async {
|
||||
if (name == 'updateQuality') {
|
||||
//Pass wifi & mobile quality by custom action
|
||||
//Isolate can't access globals
|
||||
this.wifiQuality = args['wifiQuality'];
|
||||
this.mobileQuality = args['mobileQuality'];
|
||||
}
|
||||
//Change queue source
|
||||
if (name == 'queueSource') {
|
||||
this.queueSource = QueueSource.fromJson(Map<String, dynamic>.from(args));
|
||||
}
|
||||
//Looping
|
||||
if (name == 'repeatType') {
|
||||
_loopMode = LoopMode.values[args];
|
||||
_player.setLoopMode(_loopMode);
|
||||
}
|
||||
if (name == 'saveQueue')
|
||||
await this._saveQueue();
|
||||
//Load queue after some initialization in frontend
|
||||
if (name == 'load')
|
||||
await this._loadQueueFile();
|
||||
//Shuffle
|
||||
if (name == 'shuffle') {
|
||||
String originalId = mediaItem.id;
|
||||
if (!_shuffle) {
|
||||
_shuffle = true;
|
||||
_originalQueue = List.from(_queue);
|
||||
_queue.shuffle();
|
||||
switch (name) {
|
||||
case 'updateQuality':
|
||||
//Pass wifi & mobile quality by custom action
|
||||
//Isolate can't access globals
|
||||
this.wifiQuality = args['wifiQuality'];
|
||||
this.mobileQuality = args['mobileQuality'];
|
||||
break;
|
||||
//Update queue source
|
||||
case 'queueSource':
|
||||
this.queueSource = QueueSource.fromJson(Map<String, dynamic>.from(args));
|
||||
break;
|
||||
//Looping
|
||||
case 'repeatType':
|
||||
_loopMode = LoopMode.values[args];
|
||||
_player.setLoopMode(_loopMode);
|
||||
break;
|
||||
//Save queue
|
||||
case 'saveQueue':
|
||||
await this._saveQueue();
|
||||
break;
|
||||
//Load queue after some initialization in frontend
|
||||
case 'load':
|
||||
await this._loadQueueFile();
|
||||
break;
|
||||
case 'shuffle':
|
||||
String originalId = mediaItem.id;
|
||||
if (!_shuffle) {
|
||||
_shuffle = true;
|
||||
_originalQueue = List.from(_queue);
|
||||
_queue.shuffle();
|
||||
|
||||
} else {
|
||||
_shuffle = false;
|
||||
_queue = _originalQueue;
|
||||
_originalQueue = null;
|
||||
}
|
||||
} else {
|
||||
_shuffle = false;
|
||||
_queue = _originalQueue;
|
||||
_originalQueue = null;
|
||||
}
|
||||
|
||||
//Broken
|
||||
//Broken
|
||||
// _queueIndex = _queue.indexWhere((mi) => mi.id == originalId);
|
||||
_queueIndex = 0;
|
||||
AudioServiceBackground.setQueue(_queue);
|
||||
AudioServiceBackground.setMediaItem(mediaItem);
|
||||
await _player.stop();
|
||||
await _loadQueue();
|
||||
await _player.play();
|
||||
}
|
||||
//Android auto callback
|
||||
if (name == 'screenAndroidAuto' && _androidAutoCallback != null) {
|
||||
_androidAutoCallback.complete(jsonDecode(args).map<MediaItem>((m) => MediaItem.fromJson(m)).toList());
|
||||
}
|
||||
//Reorder tracks, args = [old, new]
|
||||
if (name == 'reorder') {
|
||||
await _audioSource.move(args[0], args[1]);
|
||||
//Switch in queue
|
||||
List<MediaItem> newQueue = List.from(_queue);
|
||||
newQueue.removeAt(args[0]);
|
||||
newQueue.insert(args[1], _queue[args[0]]);
|
||||
_queue = newQueue;
|
||||
//Update UI
|
||||
AudioServiceBackground.setQueue(_queue);
|
||||
_broadcastState();
|
||||
}
|
||||
//Set index without affecting playback for loading
|
||||
if (name == 'setIndex') {
|
||||
this._queueIndex = args;
|
||||
_queueIndex = 0;
|
||||
AudioServiceBackground.setQueue(_queue);
|
||||
AudioServiceBackground.setMediaItem(mediaItem);
|
||||
await _player.stop();
|
||||
await _loadQueue();
|
||||
await _player.play();
|
||||
break;
|
||||
|
||||
//Android audio callback
|
||||
case 'screenAndroidAuto':
|
||||
if (_androidAutoCallback != null)
|
||||
_androidAutoCallback.complete(jsonDecode(args).map<MediaItem>((m) => MediaItem.fromJson(m)).toList());
|
||||
break;
|
||||
//Reorder tracks, args = [old, new]
|
||||
case 'reorder':
|
||||
await _audioSource.move(args[0], args[1]);
|
||||
//Switch in queue
|
||||
List<MediaItem> newQueue = List.from(_queue);
|
||||
newQueue.removeAt(args[0]);
|
||||
newQueue.insert(args[1], _queue[args[0]]);
|
||||
_queue = newQueue;
|
||||
//Update UI
|
||||
AudioServiceBackground.setQueue(_queue);
|
||||
_broadcastState();
|
||||
break;
|
||||
//Set index without affecting playback for loading
|
||||
case 'setIndex':
|
||||
this._queueIndex = args;
|
||||
break;
|
||||
//Start visualizer
|
||||
case 'startVisualizer':
|
||||
if (_visualizerSubscription != null) break;
|
||||
|
||||
_player.startVisualizer(
|
||||
enableWaveform: false,
|
||||
enableFft: true,
|
||||
captureRate: 15000,
|
||||
captureSize: 128
|
||||
);
|
||||
_visualizerSubscription = _player.visualizerFftStream.listen((event) {
|
||||
//Calculate actual values
|
||||
List<double> out = [];
|
||||
for (int i=0; i<event.data.length/2; i++) {
|
||||
int rfk = event.data[i*2].toSigned(8);
|
||||
int ifk = event.data[i*2+1].toSigned(8);
|
||||
out.add(log(hypot(rfk, ifk) + 1) / 5.2);
|
||||
}
|
||||
AudioServiceBackground.sendCustomEvent({"action": "visualizer", "data": out});
|
||||
});
|
||||
break;
|
||||
//Stop visualizer
|
||||
case 'stopVisualizer':
|
||||
if (_visualizerSubscription != null) {
|
||||
_visualizerSubscription.cancel();
|
||||
_visualizerSubscription = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -717,6 +811,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
await _saveQueue();
|
||||
_player.stop();
|
||||
if (_eventSub != null) _eventSub.cancel();
|
||||
if (_audioSessionSub != null) _audioSessionSub.cancel();
|
||||
|
||||
await super.onStop();
|
||||
}
|
||||
|
@ -790,6 +885,9 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
//Add at end of queue
|
||||
@override
|
||||
Future onAddQueueItem(MediaItem mi) async {
|
||||
if (_queue.indexWhere((m) => m.id == mi.id) != -1)
|
||||
return;
|
||||
|
||||
_queue.add(mi);
|
||||
await AudioServiceBackground.setQueue(_queue);
|
||||
AudioSource _newSource = await _mediaItemToAudioSource(mi);
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -312,6 +312,32 @@ const language_en_us = {
|
|||
//0.6.8:
|
||||
"Album removed from library!": "Album removed from library!",
|
||||
"Remove offline": "Remove offline",
|
||||
"Playlist removed from library!": "Playlist removed from library!"
|
||||
"Playlist removed from library!": "Playlist removed from library!",
|
||||
|
||||
//0.6.9:
|
||||
"Blur player background": "Blur player background",
|
||||
"Might have impact on performance": "Might have impact on performance",
|
||||
"Font": "Font",
|
||||
"Select font": "Select font",
|
||||
"This app isn't made for supporting many fonts, it can break layouts and overflow. Use at your own risk!": "This app isn't made for supporting many fonts, it can break layouts and overflow. Use at your own risk!",
|
||||
"Enable equalizer": "Enable equalizer",
|
||||
"Might enable some equalizer apps to work. Requires restart of Freezer": "Might enable some equalizer apps to work. Requires restart of Freezer",
|
||||
"Visualizer": "Visualizer",
|
||||
"Show visualizers on lyrics page. WARNING: Requires microphone permission!": "Show visualizers on lyrics page. WARNING: Requires microphone permission!",
|
||||
"Tags": "Tags",
|
||||
"Album": "Album",
|
||||
"Track number": "Track number",
|
||||
"Disc number": "Disc number",
|
||||
"Album artist": "Album artist",
|
||||
"Date/Year": "Date/Year",
|
||||
"Label": "Label",
|
||||
"ISRC": "ISRC",
|
||||
"UPC": "UPC",
|
||||
"Track total": "Track total",
|
||||
"BPM": "BPM",
|
||||
"Unsynchronized lyrics": "Unsynchronized lyrics",
|
||||
"Genre": "Genre",
|
||||
"Contributors": "Contributors",
|
||||
"Album art": "Album art"
|
||||
}
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter/scheduler.dart';
|
|||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/main.dart';
|
||||
import 'package:freezer/ui/cached_image.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:ext_storage/ext_storage.dart';
|
||||
|
@ -24,8 +25,11 @@ class Settings {
|
|||
@JsonKey(defaultValue: null)
|
||||
String language;
|
||||
|
||||
//Main
|
||||
@JsonKey(defaultValue: false)
|
||||
bool ignoreInterruptions;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool enableEqualizer;
|
||||
|
||||
//Account
|
||||
String arl;
|
||||
|
@ -42,6 +46,7 @@ class Settings {
|
|||
@JsonKey(defaultValue: AudioQuality.FLAC)
|
||||
AudioQuality downloadQuality;
|
||||
|
||||
|
||||
//Download options
|
||||
String downloadPath;
|
||||
|
||||
|
@ -73,6 +78,11 @@ class Settings {
|
|||
String singletonFilename;
|
||||
@JsonKey(defaultValue: 1400)
|
||||
int albumArtResolution;
|
||||
@JsonKey(defaultValue: ["title", "album", "artist", "track", "disc",
|
||||
"albumArtist", "date", "label", "isrc", "upc", "trackTotal", "bpm",
|
||||
"lyrics", "genre", "contributors", "art"])
|
||||
List<String> tags;
|
||||
|
||||
|
||||
//Appearance
|
||||
@JsonKey(defaultValue: Themes.Dark)
|
||||
|
@ -81,6 +91,12 @@ class Settings {
|
|||
bool useSystemTheme;
|
||||
@JsonKey(defaultValue: true)
|
||||
bool colorGradientBackground;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool blurPlayerBackground;
|
||||
@JsonKey(defaultValue: "Deezer")
|
||||
String font;
|
||||
@JsonKey(defaultValue: false)
|
||||
bool lyricsVisualizer;
|
||||
|
||||
//Colors
|
||||
@JsonKey(toJson: _colorToJson, fromJson: _colorFromJson)
|
||||
|
@ -126,6 +142,11 @@ class Settings {
|
|||
return _themeData[theme]??ThemeData();
|
||||
}
|
||||
|
||||
//Get all available fonts
|
||||
List<String> get fonts {
|
||||
return ['Deezer', ...GoogleFonts.asMap().keys];
|
||||
}
|
||||
|
||||
//JSON to forward into download service
|
||||
Map getServiceSettings() {
|
||||
return {"json": jsonEncode(this.toJson())};
|
||||
|
@ -206,10 +227,13 @@ class Settings {
|
|||
|
||||
static const deezerBg = Color(0xFF1F1A16);
|
||||
static const deezerBottom = Color(0xFF1b1714);
|
||||
static const font = 'MabryPro';
|
||||
TextTheme get _textTheme => (font == 'Deezer') ? null : GoogleFonts.getTextTheme(font);
|
||||
String get _fontFamily => (font == 'Deezer') ? 'MabryPro' : null;
|
||||
|
||||
Map<Themes, ThemeData> get _themeData => {
|
||||
Themes.Light: ThemeData(
|
||||
fontFamily: font,
|
||||
textTheme: _textTheme,
|
||||
fontFamily: _fontFamily,
|
||||
brightness: Brightness.light,
|
||||
primaryColor: primaryColor,
|
||||
accentColor: primaryColor,
|
||||
|
@ -218,7 +242,8 @@ class Settings {
|
|||
bottomAppBarColor: Color(0xfff5f5f5),
|
||||
),
|
||||
Themes.Dark: ThemeData(
|
||||
fontFamily: font,
|
||||
textTheme: _textTheme,
|
||||
fontFamily: _fontFamily,
|
||||
brightness: Brightness.dark,
|
||||
primaryColor: primaryColor,
|
||||
accentColor: primaryColor,
|
||||
|
@ -226,7 +251,8 @@ class Settings {
|
|||
toggleableActiveColor: primaryColor,
|
||||
),
|
||||
Themes.Deezer: ThemeData(
|
||||
fontFamily: font,
|
||||
textTheme: _textTheme,
|
||||
fontFamily: _fontFamily,
|
||||
brightness: Brightness.dark,
|
||||
primaryColor: primaryColor,
|
||||
accentColor: primaryColor,
|
||||
|
@ -242,7 +268,8 @@ class Settings {
|
|||
cardColor: deezerBg
|
||||
),
|
||||
Themes.Black: ThemeData(
|
||||
fontFamily: font,
|
||||
textTheme: _textTheme,
|
||||
fontFamily: _fontFamily,
|
||||
brightness: Brightness.dark,
|
||||
primaryColor: primaryColor,
|
||||
accentColor: primaryColor,
|
||||
|
|
|
@ -13,6 +13,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
|
|||
)
|
||||
..language = json['language'] as String
|
||||
..ignoreInterruptions = json['ignoreInterruptions'] as bool ?? false
|
||||
..enableEqualizer = json['enableEqualizer'] as bool ?? false
|
||||
..wifiQuality =
|
||||
_$enumDecodeNullable(_$AudioQualityEnumMap, json['wifiQuality']) ??
|
||||
AudioQuality.MP3_320
|
||||
|
@ -41,10 +42,32 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
|
|||
..singletonFilename =
|
||||
json['singletonFilename'] as String ?? '%artist% - %title%'
|
||||
..albumArtResolution = json['albumArtResolution'] as int ?? 1400
|
||||
..tags = (json['tags'] as List)?.map((e) => e as String)?.toList() ??
|
||||
[
|
||||
'title',
|
||||
'album',
|
||||
'artist',
|
||||
'track',
|
||||
'disc',
|
||||
'albumArtist',
|
||||
'date',
|
||||
'label',
|
||||
'isrc',
|
||||
'upc',
|
||||
'trackTotal',
|
||||
'bpm',
|
||||
'lyrics',
|
||||
'genre',
|
||||
'contributors',
|
||||
'art'
|
||||
]
|
||||
..theme =
|
||||
_$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Dark
|
||||
..useSystemTheme = json['useSystemTheme'] as bool ?? false
|
||||
..colorGradientBackground = json['colorGradientBackground'] as bool ?? true
|
||||
..blurPlayerBackground = json['blurPlayerBackground'] as bool ?? false
|
||||
..font = json['font'] as String ?? 'Deezer'
|
||||
..lyricsVisualizer = json['lyricsVisualizer'] as bool ?? false
|
||||
..primaryColor = Settings._colorFromJson(json['primaryColor'] as int)
|
||||
..useArtColor = json['useArtColor'] as bool ?? false
|
||||
..deezerLanguage = json['deezerLanguage'] as String ?? 'en'
|
||||
|
@ -58,6 +81,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
|
|||
Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
|
||||
'language': instance.language,
|
||||
'ignoreInterruptions': instance.ignoreInterruptions,
|
||||
'enableEqualizer': instance.enableEqualizer,
|
||||
'arl': instance.arl,
|
||||
'wifiQuality': _$AudioQualityEnumMap[instance.wifiQuality],
|
||||
'mobileQuality': _$AudioQualityEnumMap[instance.mobileQuality],
|
||||
|
@ -78,9 +102,13 @@ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
|
|||
'artistSeparator': instance.artistSeparator,
|
||||
'singletonFilename': instance.singletonFilename,
|
||||
'albumArtResolution': instance.albumArtResolution,
|
||||
'tags': instance.tags,
|
||||
'theme': _$ThemesEnumMap[instance.theme],
|
||||
'useSystemTheme': instance.useSystemTheme,
|
||||
'colorGradientBackground': instance.colorGradientBackground,
|
||||
'blurPlayerBackground': instance.blurPlayerBackground,
|
||||
'font': instance.font,
|
||||
'lyricsVisualizer': instance.lyricsVisualizer,
|
||||
'primaryColor': Settings._colorToJson(instance.primaryColor),
|
||||
'useArtColor': instance.useArtColor,
|
||||
'deezerLanguage': instance.deezerLanguage,
|
||||
|
|
|
@ -30,7 +30,9 @@ const supportedLocales = [
|
|||
const Locale('vi', 'VI'),
|
||||
const Locale('nl', 'NL'),
|
||||
const Locale('sl', 'SL'),
|
||||
const Locale('zh', 'CN'),
|
||||
const Locale('fil', 'PH'),
|
||||
const Locale('ast', 'ES'),
|
||||
const Locale('uwu', 'UWU')
|
||||
];
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ import 'package:flutter/cupertino.dart';
|
|||
import 'package:flutter/material.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';
|
||||
|
@ -62,6 +64,10 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
|||
void initState() {
|
||||
_load();
|
||||
|
||||
//Enable visualizer
|
||||
if (settings.lyricsVisualizer)
|
||||
playerHelper.startVisualizer();
|
||||
|
||||
Timer.periodic(Duration(milliseconds: 350), (timer) {
|
||||
_timer = timer;
|
||||
if (_loading) return;
|
||||
|
@ -82,7 +88,7 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
|||
});
|
||||
|
||||
//Track change = exit lyrics
|
||||
AudioService.currentMediaItemStream.listen((event) {
|
||||
_mediaItemSub = AudioService.currentMediaItemStream.listen((event) {
|
||||
if (event.id != widget.trackId)
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
|
@ -96,6 +102,9 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
|||
_timer.cancel();
|
||||
if (_mediaItemSub != null)
|
||||
_mediaItemSub.cancel();
|
||||
//Stop visualizer
|
||||
if (settings.lyricsVisualizer)
|
||||
playerHelper.stopVisualizer();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -104,48 +113,79 @@ class _LyricsScreenState extends State<LyricsScreen> {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: FreezerAppBar('Lyrics'.i18n),
|
||||
body: ListView(
|
||||
controller: _controller,
|
||||
body: Stack(
|
||||
children: [
|
||||
//Shouldn't really happen, empty lyrics have own text
|
||||
if (_error)
|
||||
ErrorScreen(),
|
||||
|
||||
//Loading
|
||||
if (_loading)
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator()
|
||||
],
|
||||
//Visualizer
|
||||
if (settings.lyricsVisualizer)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
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,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(data.length, (i) => AnimatedContainer(
|
||||
duration: Duration(milliseconds: 130),
|
||||
color: Theme.of(context).primaryColor,
|
||||
height: data[i] * 100,
|
||||
width: width,
|
||||
)),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
|
||||
if (lyrics != null)
|
||||
...List.generate(lyrics.lyrics.length, (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: Center(
|
||||
child: Text(
|
||||
lyrics.lyrics[i].text,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 26.0,
|
||||
fontWeight: (_currentIndex == i) ? FontWeight.bold : FontWeight.normal
|
||||
),
|
||||
//Lyrics
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0, 0, 0, settings.lyricsVisualizer ? 100 : 0),
|
||||
child: ListView(
|
||||
controller: _controller,
|
||||
children: [
|
||||
//Shouldn't really happen, empty lyrics have own text
|
||||
if (_error)
|
||||
ErrorScreen(),
|
||||
|
||||
//Loading
|
||||
if (_loading)
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator()
|
||||
],
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
if (lyrics != null)
|
||||
...List.generate(lyrics.lyrics.length, (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: Center(
|
||||
child: Text(
|
||||
lyrics.lyrics[i].text,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 26.0,
|
||||
fontWeight: (_currentIndex == i) ? FontWeight.bold : FontWeight.normal
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
|
|
|
@ -102,9 +102,7 @@ class PlayerBar extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
class PrevNextButton extends StatelessWidget {
|
||||
|
||||
final double size;
|
||||
final bool prev;
|
||||
final bool hidePrev;
|
||||
|
@ -113,38 +111,43 @@ class PrevNextButton extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!prev) {
|
||||
if (playerHelper.queueIndex == (AudioService.queue??[]).length - 1) {
|
||||
return IconButton(
|
||||
icon: Icon(Icons.skip_next),
|
||||
iconSize: size,
|
||||
onPressed: null,
|
||||
);
|
||||
}
|
||||
return IconButton(
|
||||
icon: Icon(Icons.skip_next),
|
||||
iconSize: size,
|
||||
onPressed: () => AudioService.skipToNext(),
|
||||
);
|
||||
}
|
||||
if (prev) {
|
||||
if (i == 0) {
|
||||
if (hidePrev) {
|
||||
return Container(height: 0, width: 0,);
|
||||
return StreamBuilder(
|
||||
stream: AudioService.queueStream,
|
||||
builder: (context, _snapshot) {
|
||||
if (!prev) {
|
||||
if (playerHelper.queueIndex == (AudioService.queue??[]).length - 1) {
|
||||
return IconButton(
|
||||
icon: Icon(Icons.skip_next),
|
||||
iconSize: size,
|
||||
onPressed: null,
|
||||
);
|
||||
}
|
||||
return IconButton(
|
||||
icon: Icon(Icons.skip_next),
|
||||
iconSize: size,
|
||||
onPressed: () => AudioService.skipToNext(),
|
||||
);
|
||||
}
|
||||
return IconButton(
|
||||
icon: Icon(Icons.skip_previous),
|
||||
iconSize: size,
|
||||
onPressed: null,
|
||||
);
|
||||
}
|
||||
return IconButton(
|
||||
icon: Icon(Icons.skip_previous),
|
||||
iconSize: size,
|
||||
onPressed: () => AudioService.skipToPrevious(),
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
if (prev) {
|
||||
if (i == 0) {
|
||||
if (hidePrev) {
|
||||
return Container(height: 0, width: 0,);
|
||||
}
|
||||
return IconButton(
|
||||
icon: Icon(Icons.skip_previous),
|
||||
iconSize: size,
|
||||
onPressed: null,
|
||||
);
|
||||
}
|
||||
return IconButton(
|
||||
icon: Icon(Icons.skip_previous),
|
||||
iconSize: size,
|
||||
onPressed: () => AudioService.skipToPrevious(),
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -28,8 +25,11 @@ 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
|
||||
bool pageViewLock = false;
|
||||
|
||||
class PlayerScreen extends StatefulWidget {
|
||||
@override
|
||||
|
@ -40,37 +40,53 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||
|
||||
LinearGradient _bgGradient;
|
||||
StreamSubscription _mediaItemSub;
|
||||
ImageProvider _blurImage;
|
||||
|
||||
//Calculate background color
|
||||
Future _updateColor() async {
|
||||
if (!settings.colorGradientBackground)
|
||||
if (!settings.colorGradientBackground && !settings.blurPlayerBackground)
|
||||
return;
|
||||
|
||||
//BG Image
|
||||
if (settings.blurPlayerBackground)
|
||||
setState(() {
|
||||
_blurImage = NetworkImage(AudioService.currentMediaItem.extras['thumb'] ?? AudioService.currentMediaItem.artUri);
|
||||
});
|
||||
|
||||
//Run in isolate
|
||||
PaletteGenerator palette = await PaletteGenerator.fromImageProvider(CachedNetworkImageProvider(AudioService.currentMediaItem.extras['thumb'] ?? AudioService.currentMediaItem.artUri));
|
||||
|
||||
//Update notification
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
statusBarColor: palette.dominantColor.color.withOpacity(0.7)
|
||||
));
|
||||
if (settings.blurPlayerBackground)
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
statusBarColor: palette.dominantColor.color.withOpacity(0.25),
|
||||
systemNavigationBarColor: Color.alphaBlend(palette.dominantColor.color.withOpacity(0.25), Theme.of(context).scaffoldBackgroundColor)
|
||||
));
|
||||
|
||||
setState(() => _bgGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [palette.dominantColor.color.withOpacity(0.7), Color.fromARGB(0, 0, 0, 0)],
|
||||
stops: [
|
||||
0.0,
|
||||
0.6
|
||||
]
|
||||
));
|
||||
//Color gradient
|
||||
if (!settings.blurPlayerBackground) {
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
statusBarColor: palette.dominantColor.color.withOpacity(0.7),
|
||||
));
|
||||
setState(() => _bgGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [palette.dominantColor.color.withOpacity(0.7), Color.fromARGB(0, 0, 0, 0)],
|
||||
stops: [
|
||||
0.0,
|
||||
0.6
|
||||
]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future.delayed(Duration(milliseconds: 1000), _updateColor);
|
||||
Future.delayed(Duration(milliseconds: 600), _updateColor);
|
||||
_mediaItemSub = AudioService.currentMediaItemStream.listen((event) {
|
||||
_updateColor();
|
||||
});
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -95,31 +111,51 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||
body: SafeArea(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: _bgGradient
|
||||
gradient: settings.blurPlayerBackground ? null : _bgGradient
|
||||
),
|
||||
child: StreamBuilder(
|
||||
stream: StreamZip([AudioService.playbackStateStream, AudioService.currentMediaItemStream]),
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
child: Stack(
|
||||
children: [
|
||||
if (settings.blurPlayerBackground && _blurImage != null)
|
||||
ClipRect(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: _blurImage,
|
||||
fit: BoxFit.fill,
|
||||
colorFilter: ColorFilter.mode(Colors.black.withOpacity(0.25), BlendMode.dstATop)
|
||||
)
|
||||
),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: Container(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
),
|
||||
StreamBuilder(
|
||||
stream: StreamZip([AudioService.playbackStateStream, AudioService.currentMediaItemStream]),
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
|
||||
//When disconnected
|
||||
if (AudioService.currentMediaItem == null) {
|
||||
playerHelper.startService();
|
||||
return Center(child: CircularProgressIndicator(),);
|
||||
}
|
||||
|
||||
return OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
//Landscape
|
||||
if (orientation == Orientation.landscape) {
|
||||
return PlayerScreenHorizontal();
|
||||
//When disconnected
|
||||
if (AudioService.currentMediaItem == null) {
|
||||
playerHelper.startService();
|
||||
return Center(child: CircularProgressIndicator(),);
|
||||
}
|
||||
//Portrait
|
||||
return PlayerScreenVertical();
|
||||
},
|
||||
);
|
||||
|
||||
},
|
||||
),
|
||||
return OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
//Landscape
|
||||
if (orientation == Orientation.landscape) {
|
||||
return PlayerScreenHorizontal();
|
||||
}
|
||||
//Portrait
|
||||
return PlayerScreenVertical();
|
||||
},
|
||||
);
|
||||
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
@ -172,28 +208,28 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
height: ScreenUtil().setSp(40),
|
||||
height: ScreenUtil().setSp(50),
|
||||
child: AudioService.currentMediaItem.displayTitle.length >= 22 ?
|
||||
Marquee(
|
||||
text: AudioService.currentMediaItem.displayTitle,
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(40),
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
blankSpace: 32.0,
|
||||
startPadding: 10.0,
|
||||
accelerationDuration: Duration(seconds: 1),
|
||||
pauseAfterRound: Duration(seconds: 2),
|
||||
):
|
||||
Text(
|
||||
AudioService.currentMediaItem.displayTitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(40),
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
)
|
||||
Marquee(
|
||||
text: AudioService.currentMediaItem.displayTitle,
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(40),
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
blankSpace: 32.0,
|
||||
startPadding: 10.0,
|
||||
accelerationDuration: Duration(seconds: 1),
|
||||
pauseAfterRound: Duration(seconds: 2),
|
||||
):
|
||||
Text(
|
||||
AudioService.currentMediaItem.displayTitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(40),
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
)
|
||||
),
|
||||
Container(height: 4,),
|
||||
Text(
|
||||
|
@ -279,28 +315,28 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
height: ScreenUtil().setSp(64),
|
||||
height: ScreenUtil().setSp(80),
|
||||
child: AudioService.currentMediaItem.displayTitle.length >= 26 ?
|
||||
Marquee(
|
||||
text: AudioService.currentMediaItem.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(
|
||||
AudioService.currentMediaItem.displayTitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(64),
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
)
|
||||
Marquee(
|
||||
text: AudioService.currentMediaItem.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(
|
||||
AudioService.currentMediaItem.displayTitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: ScreenUtil().setSp(64),
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
)
|
||||
),
|
||||
Container(height: 4,),
|
||||
Text(
|
||||
|
@ -577,6 +613,10 @@ class _BigAlbumArtState extends State<BigAlbumArt> {
|
|||
child: PageView(
|
||||
controller: _pageController,
|
||||
onPageChanged: (int index) {
|
||||
if (pageViewLock) {
|
||||
pageViewLock = false;
|
||||
return;
|
||||
}
|
||||
if (_animationLock) return;
|
||||
AudioService.skipToQueueItem(AudioService.queue[index].id);
|
||||
},
|
||||
|
@ -769,10 +809,11 @@ class _QueueScreenState extends State<QueueScreen> {
|
|||
return TrackTile(
|
||||
t,
|
||||
onTap: () async {
|
||||
await AudioService.playFromMediaId(t.id);
|
||||
pageViewLock = true;
|
||||
await AudioService.skipToQueueItem(t.id);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
key: Key(t.id),
|
||||
key: Key(i.toString()),
|
||||
trailing: IconButton(
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: () async {
|
||||
|
|
|
@ -18,7 +18,6 @@ import 'package:freezer/ui/elements.dart';
|
|||
import 'package:freezer/ui/error.dart';
|
||||
import 'package:freezer/ui/home_screen.dart';
|
||||
import 'package:freezer/ui/updater.dart';
|
||||
import 'package:i18n_extension/i18n_widget.dart';
|
||||
import 'package:language_pickers/language_pickers.dart';
|
||||
import 'package:language_pickers/languages.dart';
|
||||
import 'package:package_info/package_info.dart';
|
||||
|
@ -51,6 +50,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
'name': 'Furry',
|
||||
'isoCode': 'uwu'
|
||||
});
|
||||
defaultLanguagesList.add({
|
||||
'name': 'Asturian',
|
||||
'isoCode': 'ast'
|
||||
});
|
||||
defaultLanguagesList.add({
|
||||
'name': 'Chinese',
|
||||
'isoCode': 'zh'
|
||||
});
|
||||
List<Map<String, String>> _l = supportedLocales.map<Map<String, String>>((l) {
|
||||
Map _lang = defaultLanguagesList.firstWhere((lang) => lang['isoCode'] == l.languageCode);
|
||||
return {
|
||||
|
@ -250,6 +257,17 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
),
|
||||
leading: Icon(Icons.android)
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Font'.i18n),
|
||||
leading: Icon(Icons.font_download),
|
||||
subtitle: Text(settings.font),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => FontSelector(() => Navigator.of(context).pop())
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Player gradient background'.i18n),
|
||||
leading: Icon(Icons.colorize),
|
||||
|
@ -261,6 +279,33 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Blur player background'.i18n),
|
||||
subtitle: Text('Might have impact on performance'.i18n),
|
||||
leading: Icon(Icons.blur_on),
|
||||
trailing: Switch(
|
||||
value: settings.blurPlayerBackground,
|
||||
onChanged: (bool v) async {
|
||||
setState(() => settings.blurPlayerBackground = v);
|
||||
await settings.save();
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Visualizer'.i18n),
|
||||
subtitle: Text('Show visualizers on lyrics page. WARNING: Requires microphone permission!'.i18n),
|
||||
leading: Icon(Icons.equalizer),
|
||||
trailing: Switch(
|
||||
value: settings.lyricsVisualizer,
|
||||
onChanged: (bool v) async {
|
||||
if (await Permission.microphone.request().isGranted) {
|
||||
setState(() => settings.lyricsVisualizer = v);
|
||||
await settings.save();
|
||||
return;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Primary color'.i18n),
|
||||
leading: Icon(Icons.format_paint),
|
||||
|
@ -322,6 +367,77 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
}
|
||||
}
|
||||
|
||||
class FontSelector extends StatefulWidget {
|
||||
final Function callback;
|
||||
|
||||
FontSelector(this.callback, {Key key}): super(key: key);
|
||||
|
||||
@override
|
||||
_FontSelectorState createState() => _FontSelectorState();
|
||||
}
|
||||
|
||||
class _FontSelectorState extends State<FontSelector> {
|
||||
|
||||
String query = '';
|
||||
List<String> get fonts {
|
||||
return settings.fonts.where((f) => f.toLowerCase().contains(query)).toList();
|
||||
}
|
||||
|
||||
//Font selected
|
||||
void onTap(String font) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Warning'.i18n),
|
||||
content: Text("This app isn't made for supporting many fonts, it can break layouts and overflow. Use at your own risk!".i18n),
|
||||
actions: [
|
||||
FlatButton(
|
||||
onPressed: () async {
|
||||
setState(() => settings.font = font);
|
||||
await settings.save();
|
||||
Navigator.of(context).pop();
|
||||
widget.callback();
|
||||
//Global setState
|
||||
updateTheme();
|
||||
},
|
||||
child: Text('Apply'.i18n),
|
||||
),
|
||||
FlatButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
widget.callback();
|
||||
},
|
||||
child: Text('Cancel'),
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SimpleDialog(
|
||||
title: Text("Select font".i18n),
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search'.i18n
|
||||
),
|
||||
onChanged: (q) => setState(() => query = q),
|
||||
),
|
||||
),
|
||||
...List.generate(fonts.length, (i) => SimpleDialogOption(
|
||||
child: Text(fonts[i]),
|
||||
onPressed: () => onTap(fonts[i]),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class QualitySettings extends StatefulWidget {
|
||||
@override
|
||||
|
@ -766,6 +882,13 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
}
|
||||
),
|
||||
FreezerDivider(),
|
||||
ListTile(
|
||||
title: Text('Tags'.i18n),
|
||||
leading: Icon(Icons.label),
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => TagSelectionScreen()
|
||||
)),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Create folders for artist'.i18n),
|
||||
trailing: Switch(
|
||||
|
@ -917,6 +1040,62 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
|
|||
}
|
||||
}
|
||||
|
||||
class TagOption {
|
||||
String title;
|
||||
String value;
|
||||
TagOption(this.title, this.value);
|
||||
}
|
||||
|
||||
class TagSelectionScreen extends StatefulWidget {
|
||||
@override
|
||||
_TagSelectionScreenState createState() => _TagSelectionScreenState();
|
||||
}
|
||||
|
||||
class _TagSelectionScreenState extends State<TagSelectionScreen> {
|
||||
|
||||
List<TagOption> tags = [
|
||||
TagOption("Title".i18n, 'title'),
|
||||
TagOption("Album".i18n, 'album'),
|
||||
TagOption('Artist'.i18n, 'artist'),
|
||||
TagOption('Track number'.i18n, 'track'),
|
||||
TagOption('Disc number'.i18n, 'disc'),
|
||||
TagOption('Album artist'.i18n, 'albumArtist'),
|
||||
TagOption('Date/Year'.i18n, 'date'),
|
||||
TagOption('Label'.i18n, 'label'),
|
||||
TagOption('ISRC'.i18n, 'isrc'),
|
||||
TagOption('UPC'.i18n, 'upc'),
|
||||
TagOption('Track total'.i18n, 'trackTotal'),
|
||||
TagOption('BPM'.i18n, 'bpm'),
|
||||
TagOption('Unsynchronized lyrics'.i18n, 'lyrics'),
|
||||
TagOption('Genre'.i18n, 'genre'),
|
||||
TagOption('Contributors'.i18n, 'contributors'),
|
||||
TagOption('Album art'.i18n, 'art')
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: FreezerAppBar('Tags'.i18n),
|
||||
body: ListView(
|
||||
children: List.generate(tags.length, (i) => ListTile(
|
||||
title: Text(tags[i].title),
|
||||
leading: Switch(
|
||||
value: settings.tags.contains(tags[i].value),
|
||||
onChanged: (v) async {
|
||||
//Update
|
||||
if (v) settings.tags.add(tags[i].value);
|
||||
else settings.tags.remove(tags[i].value);
|
||||
setState((){});
|
||||
await settings.save();
|
||||
},
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class GeneralSettings extends StatefulWidget {
|
||||
@override
|
||||
|
@ -982,6 +1161,18 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Enable equalizer'.i18n),
|
||||
subtitle: Text('Might enable some equalizer apps to work. Requires restart of Freezer'.i18n),
|
||||
leading: Icon(Icons.equalizer),
|
||||
trailing: Switch(
|
||||
value: settings.enableEqualizer,
|
||||
onChanged: (v) async {
|
||||
setState(() => settings.enableEqualizer = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('LastFM'.i18n),
|
||||
subtitle: Text(
|
||||
|
|
|
@ -308,7 +308,7 @@ class SmartTrackListTile extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 200.0,
|
||||
height: 210.0,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onHold,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue