0.5.6 - Android Auto updates, option to disable nomedia, shuffle fix, minor fixes

This commit is contained in:
exttex 2020-10-15 20:37:36 +02:00
parent 11d93482ff
commit e775e74d8e
35 changed files with 433 additions and 153 deletions

View File

@ -306,12 +306,15 @@ public class Deezer {
tag.setField(FieldKey.DISC_NO, Integer.toString(publicTrack.getInt("disk_number"))); tag.setField(FieldKey.DISC_NO, Integer.toString(publicTrack.getInt("disk_number")));
tag.setField(FieldKey.ALBUM_ARTIST, publicAlbum.getJSONObject("artist").getString("name")); tag.setField(FieldKey.ALBUM_ARTIST, publicAlbum.getJSONObject("artist").getString("name"));
tag.setField(FieldKey.YEAR, publicTrack.getString("release_date").substring(0, 4)); tag.setField(FieldKey.YEAR, publicTrack.getString("release_date").substring(0, 4));
tag.setField(FieldKey.BPM, Integer.toString((int)publicTrack.getDouble("bpm")));
tag.setField(FieldKey.RECORD_LABEL, publicAlbum.getString("label")); tag.setField(FieldKey.RECORD_LABEL, publicAlbum.getString("label"));
tag.setField(FieldKey.ISRC, publicTrack.getString("isrc")); tag.setField(FieldKey.ISRC, publicTrack.getString("isrc"));
tag.setField(FieldKey.BARCODE, publicAlbum.getString("upc")); tag.setField(FieldKey.BARCODE, publicAlbum.getString("upc"));
tag.setField(FieldKey.TRACK_TOTAL, Integer.toString(publicAlbum.getInt("nb_tracks"))); tag.setField(FieldKey.TRACK_TOTAL, Integer.toString(publicAlbum.getInt("nb_tracks")));
//BPM
if (publicTrack.has("bpm") && (int)publicTrack.getDouble("bpm") > 0)
tag.setField(FieldKey.BPM, Integer.toString((int)publicTrack.getDouble("bpm")));
//Unsynced lyrics //Unsynced lyrics
if (lyricsData != null) { if (lyricsData != null) {
try { try {

View File

@ -610,6 +610,7 @@ public class DownloadService extends Service {
connection.disconnect(); connection.disconnect();
} catch (Exception ignored) {} } catch (Exception ignored) {}
//Create .nomedia to not spam gallery //Create .nomedia to not spam gallery
if (settings.nomediaFiles)
new File(parentDir, ".nomedia").createNewFile(); new File(parentDir, ".nomedia").createNewFile();
} catch (Exception e) { } catch (Exception e) {
logger.warn("Error downloading album cover! " + e.toString(), download); logger.warn("Error downloading album cover! " + e.toString(), download);
@ -826,19 +827,21 @@ public class DownloadService extends Service {
boolean trackCover; boolean trackCover;
String arl; String arl;
boolean albumCover; boolean albumCover;
boolean nomediaFiles;
private DownloadSettings(int downloadThreads, boolean overwriteDownload, boolean downloadLyrics, boolean trackCover, String arl, boolean albumCover) { private DownloadSettings(int downloadThreads, boolean overwriteDownload, boolean downloadLyrics, boolean trackCover, String arl, boolean albumCover, boolean nomediaFiles) {
this.downloadThreads = downloadThreads; this.downloadThreads = downloadThreads;
this.overwriteDownload = overwriteDownload; this.overwriteDownload = overwriteDownload;
this.downloadLyrics = downloadLyrics; this.downloadLyrics = downloadLyrics;
this.trackCover = trackCover; this.trackCover = trackCover;
this.arl = arl; this.arl = arl;
this.albumCover = albumCover; this.albumCover = albumCover;
this.nomediaFiles = nomediaFiles;
} }
//Parse settings from bundle sent from UI //Parse settings from bundle sent from UI
static DownloadSettings fromBundle(Bundle b) { static DownloadSettings fromBundle(Bundle b) {
return new DownloadSettings(b.getInt("downloadThreads"), b.getBoolean("overwriteDownload"), b.getBoolean("downloadLyrics"), b.getBoolean("trackCover"), b.getString("arl"), b.getBoolean("albumCover")); return new DownloadSettings(b.getInt("downloadThreads"), b.getBoolean("overwriteDownload"), b.getBoolean("downloadLyrics"), b.getBoolean("trackCover"), b.getString("arl"), b.getBoolean("albumCover"), b.getBoolean("nomediaFiles"));
} }
} }

View File

@ -49,6 +49,16 @@ public class MainActivity extends FlutterActivity {
Messenger activityMessenger; Messenger activityMessenger;
SQLiteDatabase db; SQLiteDatabase db;
//Data if started from intent
String intentPreload;
@Override
public void onCreate(Bundle savedInstanceState) {
Intent intent = getIntent();
intentPreload = intent.getStringExtra("preload");
super.onCreate(savedInstanceState);
}
@Override @Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine); GeneratedPluginRegistrant.registerWith(flutterEngine);
@ -118,6 +128,7 @@ public class MainActivity extends FlutterActivity {
bundle.putBoolean("trackCover", (boolean)call.argument("trackCover")); bundle.putBoolean("trackCover", (boolean)call.argument("trackCover"));
bundle.putString("arl", (String)call.argument("arl")); bundle.putString("arl", (String)call.argument("arl"));
bundle.putBoolean("albumCover", (boolean)call.argument("albumCover")); bundle.putBoolean("albumCover", (boolean)call.argument("albumCover"));
bundle.putBoolean("nomediaFiles", (boolean)call.argument("nomediaFiles"));
sendMessage(DownloadService.SERVICE_SETTINGS_UPDATE, bundle); sendMessage(DownloadService.SERVICE_SETTINGS_UPDATE, bundle);
result.success(null); result.success(null);
@ -163,6 +174,12 @@ public class MainActivity extends FlutterActivity {
result.success(null); result.success(null);
return; return;
} }
//If app was started with preload info (Android Auto)
if (call.method.equals("getPreloadInfo")) {
result.success(intentPreload);
intentPreload = null;
return;
}
result.error("0", "Not implemented!", "Not implemented!"); result.error("0", "Not implemented!", "Not implemented!");
}))); })));

View File

@ -35,12 +35,11 @@ class DeezerAPI {
"Connection": "keep-alive" "Connection": "keep-alive"
}; };
Future _authorizing; Future _authorizing;
Dio dio = Dio();
CookieJar _cookieJar = new CookieJar(); CookieJar _cookieJar = new CookieJar();
//Call private api //Call private api
Future<Map<dynamic, dynamic>> callApi(String method, {Map<dynamic, dynamic> params, String gatewayInput}) async { Future<Map<dynamic, dynamic>> callApi(String method, {Map<dynamic, dynamic> params, String gatewayInput}) async {
Dio dio = Dio();
//Add headers //Add headers
dio.interceptors.add(InterceptorsWrapper( dio.interceptors.add(InterceptorsWrapper(
@ -70,7 +69,6 @@ class DeezerAPI {
'api_token': this.token, 'api_token': this.token,
'input': '3', 'input': '3',
'method': method, 'method': method,
//Used for homepage //Used for homepage
if (gatewayInput != null) if (gatewayInput != null)
'gateway_input': gatewayInput 'gateway_input': gatewayInput

View File

@ -387,17 +387,17 @@ class DownloadManager {
Future<SearchResults> search(String query) async { Future<SearchResults> search(String query) async {
SearchResults results = SearchResults(tracks: [], albums: [], artists: [], playlists: []); SearchResults results = SearchResults(tracks: [], albums: [], artists: [], playlists: []);
//Tracks //Tracks
List tracksData = await db.rawQuery('SELECT * FROM tracks WHERE offline == 1 AND title like "%$query%"'); List tracksData = await db.rawQuery('SELECT * FROM Tracks WHERE offline == 1 AND title like "%$query%"');
for (Map trackData in tracksData) { for (Map trackData in tracksData) {
results.tracks.add(await getOfflineTrack(trackData['id'])); results.tracks.add(await getOfflineTrack(trackData['id']));
} }
//Albums //Albums
List albumsData = await db.rawQuery('SELECT (id) FROM albums WHERE offline == 1 AND title like "%$query%"'); List albumsData = await db.rawQuery('SELECT (id) FROM Albums WHERE offline == 1 AND title like "%$query%"');
for (Map rawAlbum in albumsData) { for (Map rawAlbum in albumsData) {
results.albums.add(await getOfflineAlbum(rawAlbum['id'])); results.albums.add(await getOfflineAlbum(rawAlbum['id']));
} }
//Playlists //Playlists
List playlists = await db.rawQuery('SELECT * FROM playlists WHERE title like "%$query%"'); List playlists = await db.rawQuery('SELECT * FROM Playlists WHERE title like "%$query%"');
for (Map playlist in playlists) { for (Map playlist in playlists) {
results.playlists.add(await getPlaylist(playlist['id'])); results.playlists.add(await getPlaylist(playlist['id']));
} }
@ -428,7 +428,7 @@ class DownloadManager {
//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, '%album%' + ' - Disk ' + (track.diskNumber??null).toString()); path = p.join(path, '%album%' + ' - Disk ' + (track.diskNumber??1).toString());
} else { } else {
path = p.join(path, '%album%'); path = p.join(path, '%album%');
} }
@ -450,9 +450,9 @@ class DownloadManager {
//Get stats for library screen //Get stats for library screen
Future<List<String>> getStats() async { Future<List<String>> getStats() async {
//Get offline counts //Get offline counts
int trackCount = (await db.rawQuery('SELECT COUNT(*) FROM tracks WHERE offline == 1'))[0]['COUNT(*)']; int trackCount = (await db.rawQuery('SELECT COUNT(*) FROM Tracks WHERE offline == 1'))[0]['COUNT(*)'];
int albumCount = (await db.rawQuery('SELECT COUNT(*) FROM albums WHERE offline == 1'))[0]['COUNT(*)']; int albumCount = (await db.rawQuery('SELECT COUNT(*) FROM Albums WHERE offline == 1'))[0]['COUNT(*)'];
int playlistCount = (await db.rawQuery('SELECT COUNT(*) FROM albums WHERE offline == 1'))[0]['COUNT(*)']; int playlistCount = (await db.rawQuery('SELECT COUNT(*) FROM Playlists'))[0]['COUNT(*)'];
//Free space //Free space
double diskSpace = await DiskSpace.getFreeDiskSpace; double diskSpace = await DiskSpace.getFreeDiskSpace;
//Used space //Used space

View File

@ -28,8 +28,6 @@ class PlayerHelper {
StreamSubscription _playbackStateStreamSubscription; StreamSubscription _playbackStateStreamSubscription;
QueueSource queueSource; QueueSource queueSource;
LoopMode repeatType = LoopMode.off; LoopMode repeatType = LoopMode.off;
bool shuffle = false;
//Find queue index by id //Find queue index by id
int get queueIndex => AudioService.queue.indexWhere((mi) => mi.id == AudioService.currentMediaItem?.id??'Random string so it returns -1'); int get queueIndex => AudioService.queue.indexWhere((mi) => mi.id == AudioService.currentMediaItem?.id??'Random string so it returns -1');
@ -49,8 +47,8 @@ class PlayerHelper {
} }
if (event['action'] == 'queueEnd') { if (event['action'] == 'queueEnd') {
//If last song is played, load more queue //If last song is played, load more queue
onQueueEnd();
this.queueSource = QueueSource.fromJson(event['queueSource']); this.queueSource = QueueSource.fromJson(event['queueSource']);
onQueueEnd();
return; return;
} }
//Android auto get screen //Android auto get screen
@ -110,8 +108,7 @@ class PlayerHelper {
} }
Future toggleShuffle() async { Future toggleShuffle() async {
this.shuffle = !this.shuffle; await AudioService.customAction('shuffle');
await AudioService.customAction('shuffle', this.shuffle);
} }
//Repeat toggle //Repeat toggle
@ -148,6 +145,7 @@ class PlayerHelper {
Future onQueueEnd() async { Future onQueueEnd() async {
//Flow //Flow
if (queueSource == null) return; if (queueSource == null) return;
print('test');
if (queueSource.id == 'flow') { if (queueSource.id == 'flow') {
List<Track> tracks = await deezerAPI.flow(); List<Track> tracks = await deezerAPI.flow();
List<MediaItem> mi = tracks.map<MediaItem>((t) => t.toMediaItem()).toList(); List<MediaItem> mi = tracks.map<MediaItem>((t) => t.toMediaItem()).toList();
@ -246,7 +244,6 @@ class AudioPlayerTask extends BackgroundAudioTask {
//Queue //Queue
List<MediaItem> _queue = <MediaItem>[]; List<MediaItem> _queue = <MediaItem>[];
List<int> _shuffleHistory = [];
int _queueIndex = 0; int _queueIndex = 0;
ConcatenatingAudioSource _audioSource; ConcatenatingAudioSource _audioSource;
@ -294,6 +291,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
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)
AudioServiceBackground.sendCustomEvent({ AudioServiceBackground.sendCustomEvent({
'action': 'queueEnd', 'action': 'queueEnd',
'queueSource': (queueSource??QueueSource()).toJson() 'queueSource': (queueSource??QueueSource()).toJson()
@ -320,7 +318,6 @@ class AudioPlayerTask extends BackgroundAudioTask {
//Calculate new index //Calculate new index
final newIndex = _queue.indexWhere((i) => i.id == mediaId); final newIndex = _queue.indexWhere((i) => i.id == mediaId);
if (newIndex == -1) return; if (newIndex == -1) return;
//Update buffering state //Update buffering state
_skipState = newIndex > _queueIndex _skipState = newIndex > _queueIndex
? AudioProcessingState.skippingToNext ? AudioProcessingState.skippingToNext
@ -362,22 +359,8 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override @override
Future<void> onSkipToNext() async { Future<void> onSkipToNext() async {
//Shuffle print('skipping');
if (_player.shuffleModeEnabled??false) { if (_queueIndex == _queue.length-1) return;
int newIndex = Random().nextInt(_queue.length-1);
//Update state
_skipState = newIndex > _queueIndex
? AudioProcessingState.skippingToNext
: AudioProcessingState.skippingToPrevious;
if (_shuffleHistory.length == 0) _shuffleHistory.add(_queueIndex);
_queueIndex = newIndex;
_shuffleHistory.add(newIndex);
await _player.seek(Duration.zero, index: _queueIndex);
_skipState = null;
return;
}
//Update buffering state //Update buffering state
_skipState = AudioProcessingState.skippingToNext; _skipState = AudioProcessingState.skippingToNext;
_queueIndex++; _queueIndex++;
@ -388,23 +371,10 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override @override
Future<void> onSkipToPrevious() async { Future<void> onSkipToPrevious() async {
if (_queueIndex == 0 && !(_player.shuffleModeEnabled??false)) return; if (_queueIndex == 0) return;
//Update buffering state //Update buffering state
_skipState = AudioProcessingState.skippingToPrevious; _skipState = AudioProcessingState.skippingToPrevious;
//Shuffle history
if ((_player.shuffleModeEnabled??false) && _shuffleHistory.length > 1) {
_shuffleHistory.removeLast();
if (_shuffleHistory.last < _queue.length) {
_queueIndex = _shuffleHistory.last;
await _player.seek(Duration.zero, index: _queueIndex);
_skipState = null;
return;
} else {
_shuffleHistory = [];
}
}
//Normal skip to previous //Normal skip to previous
_queueIndex--; _queueIndex--;
await _player.seekToPrevious(); await _player.seekToPrevious();
@ -582,7 +552,10 @@ class AudioPlayerTask extends BackgroundAudioTask {
await this._loadQueueFile(); await this._loadQueueFile();
//Shuffle //Shuffle
if (name == 'shuffle') { if (name == 'shuffle') {
await _player.setShuffleModeEnabled(args); _queue.shuffle();
AudioServiceBackground.setQueue(_queue);
_queueIndex = 0;
await _loadQueue();
} }
//Android auto callback //Android auto callback
if (name == 'screenAndroidAuto' && _androidAutoCallback != null) { if (name == 'screenAndroidAuto' && _androidAutoCallback != null) {

File diff suppressed because one or more lines are too long

View File

@ -234,6 +234,10 @@ const language_en_us = {
"Share": "Share", "Share": "Share",
"Save album cover": "Save album cover", "Save album cover": "Save album cover",
"Warning": "Warning", "Warning": "Warning",
"Using too many concurrent downloads on older/weaker devices might cause crashes!": "Using too many concurrent downloads on older/weaker devices might cause crashes!" "Using too many concurrent downloads on older/weaker devices might cause crashes!": "Using too many concurrent downloads on older/weaker devices might cause crashes!",
//0.5.6 Strings:
"Create .nomedia files": "Create .nomedia files",
"To prevent gallery being filled with album art": "To prevent gallery being filled with album art"
} }
}; };

View File

@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:freezer/api/cache.dart'; import 'package:freezer/api/cache.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/ui/library.dart'; import 'package:freezer/ui/library.dart';
import 'package:freezer/ui/login_screen.dart'; import 'package:freezer/ui/login_screen.dart';
import 'package:freezer/ui/search.dart'; import 'package:freezer/ui/search.dart';
@ -160,9 +161,28 @@ class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateM
//Setup URLs //Setup URLs
setupUniLinks(); setupUniLinks();
_loadPreloadInfo();
super.initState(); super.initState();
} }
void _loadPreloadInfo() async {
String info = await DownloadManager.platform.invokeMethod('getPreloadInfo');
if (info != null) {
//Used if started from android auto
await deezerAPI.authorize();
if (info == 'flow') {
await playerHelper.playFromSmartTrackList(SmartTrackList(id: 'flow'));
return;
}
if (info == 'favorites') {
Playlist p = await deezerAPI.fullPlaylist(deezerAPI.favoritesPlaylistId);
playerHelper.playFromPlaylist(p, p.tracks[0].id);
}
}
}
@override @override
void dispose() { void dispose() {
if (_urlLinkStream != null) if (_urlLinkStream != null)

View File

@ -62,6 +62,8 @@ class Settings {
bool trackCover; bool trackCover;
@JsonKey(defaultValue: true) @JsonKey(defaultValue: true)
bool albumCover; bool albumCover;
@JsonKey(defaultValue: false)
bool nomediaFiles;
//Appearance //Appearance
@JsonKey(defaultValue: Themes.Dark) @JsonKey(defaultValue: Themes.Dark)
@ -114,7 +116,8 @@ class Settings {
"downloadLyrics": downloadLyrics, "downloadLyrics": downloadLyrics,
"trackCover": trackCover, "trackCover": trackCover,
"arl": arl, "arl": arl,
"albumCover": albumCover "albumCover": albumCover,
"nomediaFiles": nomediaFiles
}; };
} }

View File

@ -35,6 +35,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
..downloadLyrics = json['downloadLyrics'] as bool ?? true ..downloadLyrics = json['downloadLyrics'] as bool ?? true
..trackCover = json['trackCover'] as bool ?? false ..trackCover = json['trackCover'] as bool ?? false
..albumCover = json['albumCover'] as bool ?? true ..albumCover = json['albumCover'] as bool ?? true
..nomediaFiles = json['nomediaFiles'] as bool ?? false
..theme = ..theme =
_$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Dark _$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Dark
..useSystemTheme = json['useSystemTheme'] as bool ?? false ..useSystemTheme = json['useSystemTheme'] as bool ?? false
@ -64,6 +65,7 @@ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
'downloadLyrics': instance.downloadLyrics, 'downloadLyrics': instance.downloadLyrics,
'trackCover': instance.trackCover, 'trackCover': instance.trackCover,
'albumCover': instance.albumCover, 'albumCover': instance.albumCover,
'nomediaFiles': instance.nomediaFiles,
'theme': _$ThemesEnumMap[instance.theme], 'theme': _$ThemesEnumMap[instance.theme],
'useSystemTheme': instance.useSystemTheme, 'useSystemTheme': instance.useSystemTheme,
'primaryColor': Settings._colorToJson(instance.primaryColor), 'primaryColor': Settings._colorToJson(instance.primaryColor),

View File

@ -1,20 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:freezer/languages/ar_ar.dart'; import 'package:freezer/languages/crowdin.dart';
import 'package:freezer/languages/de_de.dart';
import 'package:freezer/languages/el_gr.dart';
import 'package:freezer/languages/en_us.dart'; import 'package:freezer/languages/en_us.dart';
import 'package:freezer/languages/es_es.dart';
import 'package:freezer/languages/fil_ph.dart';
import 'package:freezer/languages/fr_fr.dart';
import 'package:freezer/languages/he_il.dart';
import 'package:freezer/languages/hr_hr.dart';
import 'package:freezer/languages/id_id.dart';
import 'package:freezer/languages/it_it.dart';
import 'package:freezer/languages/ko_ko.dart';
import 'package:freezer/languages/pt_br.dart';
import 'package:freezer/languages/ro_ro.dart';
import 'package:freezer/languages/ru_ru.dart';
import 'package:freezer/languages/tr_tr.dart';
import 'package:i18n_extension/i18n_extension.dart'; import 'package:i18n_extension/i18n_extension.dart';
const supportedLocales = [ const supportedLocales = [
@ -33,14 +19,13 @@ const supportedLocales = [
const Locale('tr', 'TR'), const Locale('tr', 'TR'),
const Locale('ro', 'RO'), const Locale('ro', 'RO'),
const Locale('id', 'ID'), const Locale('id', 'ID'),
const Locale('fa', 'IR'),
const Locale('pl', 'PL'),
const Locale('fil', 'PH') const Locale('fil', 'PH')
]; ];
extension Localization on String { extension Localization on String {
static var _t = Translations.byLocale("en_US") + static var _t = Translations.byLocale("en_US") + language_en_us + crowdin;
language_en_us + language_ar_ar + language_pt_br + language_it_it + language_de_de + language_ru_ru +
language_fil_ph + language_es_es + language_el_gr + language_hr_hr + language_ko_ko + language_fr_fr +
language_he_il + language_tr_tr + language_ro_ro + language_id_id;
String get i18n => localize(this, _t); String get i18n => localize(this, _t);
} }

View File

@ -72,6 +72,15 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
appBar: AppBar( appBar: AppBar(
title: Text('Downloads'.i18n), title: Text('Downloads'.i18n),
actions: [ actions: [
IconButton(
icon: Icon(Icons.delete_sweep),
onPressed: () async {
await downloadManager.removeDownloads(DownloadState.ERROR);
await downloadManager.removeDownloads(DownloadState.DEEZER_ERROR);
await downloadManager.removeDownloads(DownloadState.DONE);
await _load();
},
),
IconButton( IconButton(
icon: icon:
Icon(downloadManager.running ? Icons.stop : Icons.play_arrow), Icon(downloadManager.running ? Icons.stop : Icons.play_arrow),

View File

@ -383,6 +383,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
), ),
], ],
), ),
Container(width: 8.0),
], ],
), ),
body: ListView( body: ListView(
@ -573,6 +574,7 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
), ),
], ],
), ),
Container(width: 8.0),
], ],
), ),
body: ListView( body: ListView(
@ -745,7 +747,8 @@ class _LibraryArtistsState extends State<LibraryArtists> {
child: Text('Popularity'.i18n), child: Text('Popularity'.i18n),
), ),
], ],
) ),
Container(width: 8.0),
], ],
), ),
body: ListView( body: ListView(
@ -888,7 +891,8 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
child: Text('Alphabetic'.i18n), child: Text('Alphabetic'.i18n),
), ),
], ],
) ),
Container(width: 8.0),
], ],
), ),
body: ListView( body: ListView(

View File

@ -30,6 +30,7 @@ class PlayerBar extends StatelessWidget {
} }
//Left //Left
if (details.delta.dx < -sensitivity) { if (details.delta.dx < -sensitivity) {
await AudioService.skipToNext(); await AudioService.skipToNext();
} }
_gestureRegistered = false; _gestureRegistered = false;

View File

@ -71,25 +71,6 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
double iconSize = ScreenUtil().setWidth(64); double iconSize = ScreenUtil().setWidth(64);
bool _lyrics = false; bool _lyrics = false;
PageController _pageController = PageController(
initialPage: playerHelper.queueIndex,
);
StreamSubscription _currentItemSub;
@override
void initState() {
_currentItemSub = AudioService.currentMediaItemStream.listen((event) {
_pageController.animateToPage(playerHelper.queueIndex, duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
});
super.initState();
}
@override
void dispose() {
if (_currentItemSub != null)
_currentItemSub.cancel();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -103,16 +84,7 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
width: ScreenUtil().setWidth(500), width: ScreenUtil().setWidth(500),
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
PageView( BigAlbumArt(),
controller: _pageController,
onPageChanged: (int index) {
AudioService.skipToQueueItem(AudioService.queue[index].id);
},
children: List.generate(AudioService.queue.length, (i) => CachedImage(
url: AudioService.queue[i].artUri,
fullThumb: true,
)),
),
if (_lyrics) LyricsWidget( if (_lyrics) LyricsWidget(
artUri: AudioService.currentMediaItem.extras['thumb'], artUri: AudioService.currentMediaItem.extras['thumb'],
trackId: AudioService.currentMediaItem.id, trackId: AudioService.currentMediaItem.id,
@ -251,25 +223,6 @@ class PlayerScreenVertical extends StatefulWidget {
class _PlayerScreenVerticalState extends State<PlayerScreenVertical> { class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
double iconSize = ScreenUtil().setWidth(100); double iconSize = ScreenUtil().setWidth(100);
bool _lyrics = false; bool _lyrics = false;
PageController _pageController = PageController(
initialPage: playerHelper.queueIndex,
);
StreamSubscription _currentItemSub;
@override
void initState() {
_currentItemSub = AudioService.currentMediaItemStream.listen((event) {
_pageController.animateToPage(playerHelper.queueIndex, duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
});
super.initState();
}
@override
void dispose() {
if (_currentItemSub != null)
_currentItemSub.cancel();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -287,16 +240,7 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
height: ScreenUtil().setHeight(1050), height: ScreenUtil().setHeight(1050),
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
PageView( BigAlbumArt(),
controller: _pageController,
onPageChanged: (int index) {
AudioService.skipToQueueItem(AudioService.queue[index].id);
},
children: List.generate(AudioService.queue.length, (i) => CachedImage(
url: AudioService.queue[i].artUri,
fullThumb: true,
)),
),
if (_lyrics) LyricsWidget( if (_lyrics) LyricsWidget(
artUri: AudioService.currentMediaItem.extras['thumb'], artUri: AudioService.currentMediaItem.extras['thumb'],
trackId: AudioService.currentMediaItem.id, trackId: AudioService.currentMediaItem.id,
@ -398,6 +342,51 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
} }
} }
class BigAlbumArt extends StatefulWidget {
@override
_BigAlbumArtState createState() => _BigAlbumArtState();
}
class _BigAlbumArtState extends State<BigAlbumArt> {
PageController _pageController = PageController(
initialPage: playerHelper.queueIndex,
);
StreamSubscription _currentItemSub;
bool _animationLock = true;
@override
void initState() {
_currentItemSub = AudioService.currentMediaItemStream.listen((event) async {
_animationLock = true;
await _pageController.animateToPage(playerHelper.queueIndex, duration: Duration(milliseconds: 300), curve: Curves.easeInOut);
_animationLock = false;
});
super.initState();
}
@override
void dispose() {
if (_currentItemSub != null)
_currentItemSub.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return PageView(
controller: _pageController,
onPageChanged: (int index) {
if (_animationLock) return;
AudioService.skipToQueueItem(AudioService.queue[index].id);
},
children: List.generate(AudioService.queue.length, (i) => CachedImage(
url: AudioService.queue[i].artUri,
fullThumb: true,
)),
);
}
}
class LyricsWidget extends StatefulWidget { class LyricsWidget extends StatefulWidget {
@ -773,17 +762,6 @@ class QueueScreen extends StatefulWidget {
class _QueueScreenState extends State<QueueScreen> { class _QueueScreenState extends State<QueueScreen> {
//Get proper icon color by theme
Color get shuffleIconColor {
Color og = Theme.of(context).primaryColor;
if (og.computeLuminance() > 0.5) {
if (playerHelper.shuffle) return Theme.of(context).primaryColorLight;
return Colors.black;
}
if (playerHelper.shuffle) return Theme.of(context).primaryColorDark;
return Colors.white;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -793,7 +771,6 @@ class _QueueScreenState extends State<QueueScreen> {
IconButton( IconButton(
icon: Icon( icon: Icon(
Icons.shuffle, Icons.shuffle,
color: shuffleIconColor
), ),
onPressed: () async { onPressed: () async {
await playerHelper.toggleShuffle(); await playerHelper.toggleShuffle();

View File

@ -803,6 +803,20 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
), ),
), ),
), ),
ListTile(
title: Text('Create .nomedia files'.i18n),
subtitle: Text('To prevent gallery being filled with album art'.i18n),
leading: Container(
width: 30.0,
child: Checkbox(
value: settings.nomediaFiles,
onChanged: (v) {
setState(() => settings.nomediaFiles = v);
settings.save();
},
),
),
),
Divider(), Divider(),
ListTile( ListTile(
title: Text('Download Log'.i18n), title: Text('Download Log'.i18n),

View File

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.5.5+1 version: 0.5.6+1
environment: environment:
sdk: ">=2.8.0 <3.0.0" sdk: ">=2.8.0 <3.0.0"
@ -27,11 +27,11 @@ dependencies:
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
dio: ^3.0.9 dio: ^3.0.10
dio_cookie_manager: ^1.0.0 dio_cookie_manager: ^1.0.0
cookie_jar: ^1.0.1 cookie_jar: ^1.0.1
json_annotation: ^3.0.1 json_annotation: ^3.0.1
path_provider: ^1.6.9 path_provider: 1.6.10
path: ^1.6.4 path: ^1.6.4
sqflite: ^1.3.0+1 sqflite: ^1.3.0+1
crypto: ^2.1.4 crypto: ^2.1.4
@ -69,7 +69,7 @@ dependencies:
uni_links: ^0.4.0 uni_links: ^0.4.0
share: ^0.6.5+2 share: ^0.6.5+2
audio_session: ^0.0.7 audio_session: ^0.0.9
audio_service: audio_service:
path: ./audio_service path: ./audio_service
just_audio: just_audio:

40
translations/crowdin.py Normal file
View File

@ -0,0 +1,40 @@
import zipfile
import json
lang_crowdin = {
'ar': 'ar_ar',
'de': 'de_de',
'el': 'el_gr',
'es-ES': 'es_es',
'fa': 'fa_ir',
'fil': 'fil_ph',
'fr': 'fr_fr',
'he': 'he_il',
'hr': 'hr_hr',
'id': 'id_id',
'it': 'it_id',
'ko': 'ko_ko',
'pt-BR': 'pt_br',
'ro': 'ro_ro',
'ru': 'ru_ru',
'tr': 'tr_tr',
'pl': 'pl_pl'
}
def generate_dart():
out = {}
with zipfile.ZipFile('translations.zip') as zip:
for file in zip.namelist():
if 'freezer.json' in file:
data = zip.open(file).read()
lang = file.split('/')[0]
out[lang_crowdin[lang]] = json.loads(data)
with open('crowdin.dart', 'w') as f:
data = json.dumps(out, ensure_ascii=False).replace('$', '\\$')
out = f'const crowdin = {data};'
f.write(out)
if __name__ == '__main__':
generate_dart()

226
translations/freezer.json Normal file
View File

@ -0,0 +1,226 @@
{
"Home": "Home",
"Search": "Search",
"Library": "Library",
"Offline mode, can't play flow or smart track lists.":
"Offline mode, can't play flow or smart track lists.",
"Added to library": "Added to library",
"Download": "Download",
"Disk": "Disk",
"Offline": "Offline",
"Top Tracks": "Top Tracks",
"Show more tracks": "Show more tracks",
"Top": "Top",
"Top Albums": "Top Albums",
"Show all albums": "Show all albums",
"Discography": "Discography",
"Default": "Default",
"Reverse": "Reverse",
"Alphabetic": "Alphabetic",
"Artist": "Artist",
"Post processing...": "Post processing...",
"Done": "Done",
"Delete": "Delete",
"Are you sure you want to delete this download?":
"Are you sure you want to delete this download?",
"Cancel": "Cancel",
"Downloads": "Downloads",
"Clear queue": "Clear queue",
"This won't delete currently downloading item":
"This won't delete currently downloading item",
"Are you sure you want to delete all queued downloads?":
"Are you sure you want to delete all queued downloads?",
"Clear downloads history": "Clear downloads history",
"WARNING: This will only clear non-offline (external downloads)":
"WARNING: This will only clear non-offline (external downloads)",
"Please check your connection and try again later...":
"Please check your connection and try again later...",
"Show more": "Show more",
"Importer": "Importer",
"Currently supporting only Spotify, with 100 tracks limit":
"Currently supporting only Spotify, with 100 tracks limit",
"Due to API limitations": "Due to API limitations",
"Enter your playlist link below": "Enter your playlist link below",
"Error loading URL!": "Error loading URL!",
"Convert": "Convert",
"Download only": "Download only",
"Downloading is currently stopped, click here to resume.":
"Downloading is currently stopped, click here to resume.",
"Tracks": "Tracks",
"Albums": "Albums",
"Artists": "Artists",
"Playlists": "Playlists",
"Import": "Import",
"Import playlists from Spotify": "Import playlists from Spotify",
"Statistics": "Statistics",
"Offline tracks": "Offline tracks",
"Offline albums": "Offline albums",
"Offline playlists": "Offline playlists",
"Offline size": "Offline size",
"Free space": "Free space",
"Loved tracks": "Loved tracks",
"Favorites": "Favorites",
"All offline tracks": "All offline tracks",
"Create new playlist": "Create new playlist",
"Cannot create playlists in offline mode":
"Cannot create playlists in offline mode",
"Error": "Error",
"Error logging in! Please check your token and internet connection and try again.":
"Error logging in! Please check your token and internet connection and try again.",
"Dismiss": "Dismiss",
"Welcome to": "Welcome to",
"Please login using your Deezer account.":
"Please login using your Deezer account.",
"Login using browser": "Login using browser",
"Login using token": "Login using token",
"Enter ARL": "Enter ARL",
"Token (ARL)": "Token (ARL)",
"Save": "Save",
"If you don't have account, you can register on deezer.com for free.":
"If you don't have account, you can register on deezer.com for free.",
"Open in browser": "Open in browser",
"By using this app, you don't agree with the Deezer ToS":
"By using this app, you don't agree with the Deezer ToS",
"Play next": "Play next",
"Add to queue": "Add to queue",
"Add track to favorites": "Add track to favorites",
"Add to playlist": "Add to playlist",
"Select playlist": "Select playlist",
"Track added to": "Track added to",
"Remove from playlist": "Remove from playlist",
"Track removed from": "Track removed from",
"Remove favorite": "Remove favorite",
"Track removed from library": "Track removed from library",
"Go to": "Go to",
"Make offline": "Make offline",
"Add to library": "Add to library",
"Remove album": "Remove album",
"Album removed": "Album removed",
"Remove from favorites": "Remove from favorites",
"Artist removed from library": "Artist removed from library",
"Add to favorites": "Add to favorites",
"Remove from library": "Remove from library",
"Add playlist to library": "Add playlist to library",
"Added playlist to library": "Added playlist to library",
"Make playlist offline": "Make playlist offline",
"Download playlist": "Download playlist",
"Create playlist": "Create playlist",
"Title": "Title",
"Description": "Description",
"Private": "Private",
"Collaborative": "Collaborative",
"Create": "Create",
"Playlist created!": "Playlist created!",
"Playing from:": "Playing from:",
"Queue": "Queue",
"Offline search": "Offline search",
"Search Results": "Search Results",
"No results!": "No results!",
"Show all tracks": "Show all tracks",
"Show all playlists": "Show all playlists",
"Settings": "Settings",
"General": "General",
"Appearance": "Appearance",
"Quality": "Quality",
"Deezer": "Deezer",
"Theme": "Theme",
"Currently": "Currently",
"Select theme": "Select theme",
"Dark": "Dark",
"Black (AMOLED)": "Black (AMOLED)",
"Deezer (Dark)": "Deezer (Dark)",
"Primary color": "Primary color",
"Selected color": "Selected color",
"Use album art primary color": "Use album art primary color",
"Warning: might be buggy": "Warning: might be buggy",
"Mobile streaming": "Mobile streaming",
"Wifi streaming": "Wifi streaming",
"External downloads": "External downloads",
"Content language": "Content language",
"Not app language, used in headers. Now":
"Not app language, used in headers. Now",
"Select language": "Select language",
"Content country": "Content country",
"Country used in headers. Now": "Country used in headers. Now",
"Log tracks": "Log tracks",
"Send track listen logs to Deezer, enable it for features like Flow to work properly":
"Send track listen logs to Deezer, enable it for features like Flow to work properly",
"Offline mode": "Offline mode",
"Will be overwritten on start.": "Will be overwritten on start.",
"Error logging in, check your internet connections.":
"Error logging in, check your internet connections.",
"Logging in...": "Logging in...",
"Download path": "Download path",
"Downloads naming": "Downloads naming",
"Downloaded tracks filename": "Downloaded tracks filename",
"Valid variables are": "Valid variables are",
"Reset": "Reset",
"Clear": "Clear",
"Create folders for artist": "Create folders for artist",
"Create folders for albums": "Create folders for albums",
"Separate albums by discs": "Separate albums by disks",
"Overwrite already downloaded files": "Overwrite already downloaded files",
"Copy ARL": "Copy ARL",
"Copy userToken/ARL Cookie for use in other apps.":
"Copy userToken/ARL Cookie for use in other apps.",
"Copied": "Copied",
"Log out": "Log out",
"Due to plugin incompatibility, login using browser is unavailable without restart.":
"Due to plugin incompatibility, login using browser is unavailable without restart.",
"(ARL ONLY) Continue": "(ARL ONLY) Continue",
"Log out & Exit": "Log out & Exit",
"Pick-a-Path": "Pick-a-Path",
"Select storage": "Select storage",
"Go up": "Go up",
"Permission denied": "Permission denied",
"Language": "Language",
"Language changed, please restart Freezer to apply!":
"Language changed, please restart Freezer to apply!",
"Importing...": "Importing...",
"Radio": "Radio",
"Flow": "Flow",
"Track is not available on Deezer!": "Track is not available on Deezer!",
"Failed to download track! Please restart.": "Failed to download track! Please restart.",
"Storage permission denied!": "Storage permission denied!",
"Failed": "Failed",
"Queued": "Queued",
"External": "Storage",
"Restart failed downloads": "Restart failed downloads",
"Clear failed": "Clear failed",
"Download Settings": "Download Settings",
"Create folder for playlist": "Create folder for playlist",
"Download .LRC lyrics": "Download .LRC lyrics",
"Proxy": "Proxy",
"Not set": "Not set",
"Search or paste URL": "Search or paste URL",
"History": "History",
"Download threads": "Concurrent downloads",
"Lyrics unavailable, empty or failed to load!": "Lyrics unavailable, empty or failed to load!",
"About": "About",
"Telegram Channel": "Telegram Channel",
"To get latest releases": "To get latest releases",
"Official chat": "Official chat",
"Telegram Group": "Telegram Group",
"Huge thanks to all the contributors! <3": "Huge thanks to all the contributors! <3",
"Edit playlist": "Edit playlist",
"Update": "Update",
"Playlist updated!": "Playlist updated!",
"Downloads added!": "Downloads added!",
"Save cover file for every track": "Save cover file for every track",
"Download Log": "Download Log",
"Repository": "Repository",
"Source code, report issues there.": "Source code, report issues there.",
"Use system theme": "Use system theme",
"Light": "Light",
"Popularity": "Popularity",
"User": "User",
"Track count": "Track count",
"If you want to use custom directory naming - use '/' as directory separator.": "If you want to use custom directory naming - use '/' as directory separator.",
"Share": "Share",
"Save album cover": "Save album cover",
"Warning": "Warning",
"Using too many concurrent downloads on older/weaker devices might cause crashes!": "Using too many concurrent downloads on older/weaker devices might cause crashes!",
"Create .nomedia files": "Create .nomedia files",
"To prevent gallery being filled with album art": "To prevent gallery being filled with album art"
}