0.4.0 - translations, download fallback, android auto, radio, infinite flow, bugfixes

This commit is contained in:
exttex 2020-09-18 19:36:41 +02:00
parent a5381f0fed
commit e984621eeb
88 changed files with 2911 additions and 379 deletions

1
.gitignore vendored
View File

@ -19,6 +19,7 @@ android/key.properties
*.iws
.idea/
android/.idea/
android/local.properties
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line

View File

@ -7,36 +7,56 @@ This app is still in BETA, so it is missing features and contains bugs.
If you want to report bug or request feature, please open an issue.
## Downloads:
Under releases tab
**You might get Play Protect warning - just select install anyway or disable Play Protect** - it is because the keys used for signing this app are new.
Downloads are currently distributed in Telegram channel: https://t.me/freezereleases
**You might get Play Protect warning - just select install anyway or disable Play Protect** - it is because the keys used for signing this app are new.
**App not installed** error - try different version (arm32/64) or uninstall old version.
## Compile from source
Install flutter SDK: https://flutter.dev/docs/get-started/install
(Optional) Generate keys for release build: https://flutter.dev/docs/deployment/android
Download source:
```
git clone https://notabug.org/exttex/freezer
git submodule init
git submodule update
```
Compile:
```
flutter pub get
flutter build apk
```
## Telegram
## Telegram group
https://t.me/freezerandroid
## Credits
Tobs: Beta tester
Bas Curtiz: Icon, Logo, Banner, Design suggestions
Deemix: https://notabug.org/RemixDev/deemix
just_audio && audio_service: https://github.com/ryanheise/just_audio
Annexhack: Android Auto help and resources
### Translators:
Homam Al-Rawi: Arabic
Markus: German
Andrea: Italian
Diego Hiro: Portuguese
Annexhack: Russian
### just_audio, audio_service
This app depends on modified just_audio and audio_service plugins with Deezer support.
Both plugins were originally written by ryanheise, all credits to him.
Forked versions for Freezer:
https://notabug.org/exttex/just_audio/
https://notabug.org/exttex/audio_service/
## Support me
BTC: `14hcr4PGbgqeXd3SoXY9QyJFNpyurgrL9y`
ETH: `0xb4D1893195404E1F4b45e5BDA77F202Ac4012288`
## just_audio
This app depends on modified just_audio plugin with Deezer support.
The fork repo is deprecated, current version available in this repo.
## Disclaimer
```

View File

@ -41,7 +41,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "f.f.freezer"
minSdkVersion 20
minSdkVersion 21
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

View File

@ -31,6 +31,7 @@
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
@ -64,5 +65,8 @@
</intent-filter>
</receiver>
<meta-data android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"/>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -25,6 +25,7 @@ import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.RandomAccessFile;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.function.Function;
import javax.crypto.Cipher;
@ -73,10 +74,33 @@ public class MainActivity extends FlutterActivity {
tag.setField(FieldKey.ALBUM, call.argument("album").toString());
tag.setField(FieldKey.ARTIST, call.argument("artists").toString());
tag.setField(FieldKey.TRACK, call.argument("trackNumber").toString());
if (call.argument("albumArtist") != null)
tag.setField(FieldKey.ALBUM_ARTIST, call.argument("albumArtist").toString());
if (call.argument("diskNumber") != null)
tag.setField(FieldKey.DISC_NO, call.argument("diskNumber").toString());
if (call.argument("year") != null)
tag.setField(FieldKey.YEAR, call.argument("year").toString());
if (call.argument("bpm") != null)
tag.setField(FieldKey.BPM, call.argument("bpm").toString());
if (call.argument("label") != null)
tag.setField(FieldKey.RECORD_LABEL, call.argument("label").toString());
//Genres
ArrayList<String> genres = call.argument("genres");
for(int i=0; i<genres.size(); i++) {
tag.addField(FieldKey.GENRE, genres.get(i));
}
//Album art
String cover = call.argument("cover").toString();
if (isFlac) {
//FLAC Specific tags
if (call.argument("date") != null)
((FlacTag) tag).setField("DATE", call.argument("date").toString());
if (call.argument("albumTracks") != null)
((FlacTag) tag).setField("TRACKTOTAL", call.argument("albumTracks").toString());
((FlacTag) tag).setField("ITUNESADVISORY", call.argument("explicit").toString());
//FLAC requires different cover adding
RandomAccessFile imageFile = new RandomAccessFile(new File(cover), "r");
byte[] imageData = new byte[(int) imageFile.length()];

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 B

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#1F1A16</color>
</resources>

View File

@ -0,0 +1,3 @@
<automotiveApp>
<uses name="media"/>
</automotiveApp>

BIN
assets/favorites_thumb.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -20,6 +20,7 @@ class DeezerAPI {
String token;
String userId;
String userName;
String favoritesPlaylistId;
String privateUrl = 'http://www.deezer.com/ajax/gw-light.php';
Map<String, String> headers = {
@ -95,6 +96,7 @@ class DeezerAPI {
} else {
this.token = data['results']['checkForm'];
this.userId = data['results']['USER']['USER_ID'].toString();
this.userName = data['results']['USER']['BLOG_NAME'];
this.favoritesPlaylistId = data['results']['USER']['LOVEDTRACKS_ID'];
return true;
}
@ -384,5 +386,20 @@ class DeezerAPI {
return data['results']['data'].map<Album>((a) => Album.fromPrivateJson(a)).toList();
}
Future<List> searchSuggestions(String query) async {
Map data = await callApi('search_getSuggestedQueries', params: {
'QUERY': query
});
return data['results']['SUGGESTION'].map((s) => s['QUERY']).toList();
}
//Get smart radio for artist id
Future<List<Track>> smartRadio(String artistId) async {
Map data = await callApi('smart.getSmartRadio', params: {
'art_id': int.parse(artistId)
});
return data['results']['data'].map<Track>((t) => Track.fromPrivateJson(t)).toList();
}
}

View File

@ -254,7 +254,10 @@ class Artist {
bool offline;
bool library;
Artist({this.id, this.name, this.albums, this.albumCount, this.topTracks, this.picture, this.fans, this.offline, this.library});
//TODO: NOT IN DB
bool radio;
Artist({this.id, this.name, this.albums, this.albumCount, this.topTracks, this.picture, this.fans, this.offline, this.library, this.radio});
String get fansString => NumberFormat.compact().format(fans);
@ -264,16 +267,23 @@ class Artist {
Map<dynamic, dynamic> albumsJson = const {},
Map<dynamic, dynamic> topJson = const {},
bool library = false
}) => Artist(
id: json['ART_ID'].toString(),
name: json['ART_NAME'],
fans: json['NB_FAN'],
picture: ImageDetails.fromPrivateString(json['ART_PICTURE'], type: 'artist'),
albumCount: albumsJson['total'],
albums: (albumsJson['data']??[]).map<Album>((dynamic data) => Album.fromPrivateJson(data)).toList(),
topTracks: (topJson['data']??[]).map<Track>((dynamic data) => Track.fromPrivateJson(data)).toList(),
library: library
);
}) {
//Get wether radio is available
bool _radio = false;
if (json['SMARTRADIO'] == true || json['SMARTRADIO'] == 1) _radio = true;
return Artist(
id: json['ART_ID'].toString(),
name: json['ART_NAME'],
fans: json['NB_FAN'],
picture: ImageDetails.fromPrivateString(json['ART_PICTURE'], type: 'artist'),
albumCount: albumsJson['total'],
albums: (albumsJson['data']??[]).map<Album>((dynamic data) => Album.fromPrivateJson(data)).toList(),
topTracks: (topJson['data']??[]).map<Track>((dynamic data) => Track.fromPrivateJson(data)).toList(),
library: library,
radio: _radio
);
}
Map<String, dynamic> toSQL({off = false}) => {
'id': id,
'name': name,

View File

@ -142,6 +142,7 @@ Artist _$ArtistFromJson(Map<String, dynamic> json) {
fans: json['fans'] as int,
offline: json['offline'] as bool,
library: json['library'] as bool,
radio: json['radio'] as bool,
);
}
@ -155,6 +156,7 @@ Map<String, dynamic> _$ArtistToJson(Artist instance) => <String, dynamic>{
'fans': instance.fans,
'offline': instance.offline,
'library': instance.library,
'radio': instance.radio,
};
Playlist _$PlaylistFromJson(Map<String, dynamic> json) {

View File

@ -122,14 +122,6 @@ class DownloadManager {
onDone: () async {
//On download finished
await db.rawUpdate('UPDATE downloads SET state = 1 WHERE trackId = ?', [queue[0].track.id]);
/*
if (queue[0].private) {
await db.rawUpdate('UPDATE downloads SET state = 1 WHERE trackId = ?', [queue[0].track.id]);
} else {
//Remove on db if public
await db.delete('downloads', where: 'trackId = ?', whereArgs: [queue[0].track.id]);
}
*/
queue.removeAt(0);
_download = null;
//Remove notification if no more downloads
@ -144,7 +136,11 @@ class DownloadManager {
//Catch download errors
_download = null;
_cancelNotifications = true;
//Cancellation error i guess
queue[0].state = DownloadState.NONE;
//Shift to end
queue.add(queue[0]);
queue.removeAt(0);
//Show error
await _showError();
});
//Show download progress notifications
@ -318,14 +314,12 @@ class DownloadManager {
track = await deezerAPI.track(track.id);
}
String url = track.getUrl(settings.getQualityInt(settings.offlineQuality));
if (!private) {
//Check permissions
if (!(await Permission.storage.request().isGranted)) {
return;
}
//If saving to external
url = track.getUrl(settings.getQualityInt(settings.downloadQuality));
//Save just extension to path, will be generated before download
path = 'mp3';
if (settings.downloadQuality == AudioQuality.FLAC) {
@ -339,7 +333,7 @@ class DownloadManager {
} catch (e) {}
}
Download download = Download(track: track, path: path, url: url, private: private);
Download download = Download(track: track, path: path, private: private);
//Database
Batch b = db.batch();
b.insert('downloads', download.toSQL());
@ -539,12 +533,17 @@ class Download {
//TODO: Check for internet before downloading
Map rawTrackPublic = {};
Map rawAlbumPublic = {};
if (!this.private && !(this.path.endsWith('.mp3') || this.path.endsWith('.flac'))) {
String ext = this.path;
//Get track details
Map _rawTrackData = await deezerAPI.callApi('song.getListData', params: {'sng_ids': [track.id]});
Map rawTrack = _rawTrackData['results']['data'][0];
this.track = Track.fromPrivateJson(rawTrack);
//RAW Public API call (for genre and other tags)
try {rawTrackPublic = await deezerAPI.callPublicApi('track/${this.track.id}');} catch (e) {rawTrackPublic = {};}
try {rawAlbumPublic = await deezerAPI.callPublicApi('album/${this.track.album.id}');} catch (e) {rawAlbumPublic = {};}
//Get path if public
RegExp sanitize = RegExp(r'[\/\\\?\%\*\:\|\"\<\>]');
@ -620,6 +619,11 @@ class Download {
//Create file if doesnt exist
await downloadFile.create(recursive: true);
}
//Quality fallback
if (this.url == null)
await _fallback();
//Download
_cancel = CancelToken();
Response response = await dio.get(
@ -658,6 +662,10 @@ class Download {
//Tag
if (!private) {
//Tag track in native
String year;
if (rawTrackPublic['release_date'] != null && rawTrackPublic['release_date'].length >= 4)
year = rawTrackPublic['release_date'].substring(0, 4);
await platformChannel.invokeMethod('tagTrack', {
'path': path,
'title': track.title,
@ -665,7 +673,16 @@ class Download {
'artists': track.artistString,
'artist': track.artists[0].name,
'cover': _cover,
'trackNumber': track.trackNumber
'trackNumber': track.trackNumber,
'diskNumber': track.diskNumber,
'genres': ((rawAlbumPublic['genres']??{})['data']??[]).map((g) => g['name']).toList(),
'year': year,
'bpm': rawTrackPublic['bpm'],
'explicit': (track.explicit??false) ? "1":"0",
'label': rawAlbumPublic['label'],
'albumTracks': rawAlbumPublic['nb_tracks'],
'date': rawTrackPublic['release_date'],
'albumArtist': (rawAlbumPublic['artist']??{})['name']
});
//Rescan android library
await platformChannel.invokeMethod('rescanLibrary', {
@ -702,6 +719,36 @@ class Download {
return;
}
Future _fallback({fallback}) async {
//Get quality
AudioQuality quality = private ? settings.offlineQuality : settings.downloadQuality;
if (fallback == AudioQuality.MP3_320) quality = AudioQuality.MP3_128;
if (fallback == AudioQuality.FLAC) {
quality = AudioQuality.MP3_320;
if (this.path.toLowerCase().endsWith('flac'))
this.path = this.path.substring(0, this.path.length - 4) + 'mp3';
}
//No more fallback
if (quality == AudioQuality.MP3_128) {
url = track.getUrl(settings.getQualityInt(quality));
return;
}
//Check
int q = settings.getQualityInt(quality);
try {
Response res = await Dio().head(track.getUrl(q));
if (res.statusCode == 200 || res.statusCode == 206) {
this.url = track.getUrl(q);
return;
}
} catch (e) {}
//Fallback
return _fallback(fallback: quality);
}
//JSON
Map<String, dynamic> toSQL() => {
'trackId': track.id,

View File

@ -1,11 +1,13 @@
import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/ui/details_screens.dart';
import 'package:freezer/ui/android_auto.dart';
import 'package:just_audio/just_audio.dart';
import 'package:connectivity/connectivity.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:freezer/translations.i18n.dart';
import 'definitions.dart';
import '../settings.dart';
@ -43,9 +45,21 @@ class PlayerHelper {
}
if (event['action'] == 'queueEnd') {
//If last song is played, load more queue
onQueueEnd();
this.queueSource = QueueSource.fromJson(event['queueSource']);
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']);
}
});
_playbackStateStreamSubscription = AudioService.playbackStateStream.listen((event) {
//Log song (if allowed)
@ -106,6 +120,30 @@ class PlayerHelper {
await AudioService.skipToQueueItem(trackId);
}
//Called when queue ends to load more tracks
Future onQueueEnd() async {
//Flow
if (queueSource == null) return;
if (queueSource.id == 'flow') {
List<Track> tracks = await deezerAPI.flow();
List<MediaItem> mi = tracks.map<MediaItem>((t) => t.toMediaItem()).toList();
await AudioService.addQueueItems(mi);
AudioService.skipToNext();
return;
}
//SmartRadio/Artist radio
if (queueSource.source == 'smartradio') {
List<Track> tracks = await deezerAPI.smartRadio(queueSource.id);
List<MediaItem> mi = tracks.map<MediaItem>((t) => t.toMediaItem()).toList();
await AudioService.addQueueItems(mi);
AudioService.skipToNext();
return;
}
print(queueSource.toJson());
}
//Play track from album
Future playFromAlbum(Album album, String trackId) async {
await playFromTrackList(album.tracks, trackId, QueueSource(
@ -144,7 +182,7 @@ class PlayerHelper {
if (stl.tracks == null || stl.tracks.length == 0) {
if (settings.offlineMode) {
Fluttertoast.showToast(
msg: "Offline mode, can't play flow/smart track lists.",
msg: "Offline mode, can't play flow or smart track lists.".i18n,
gravity: ToastGravity.BOTTOM,
toastLength: Toast.LENGTH_SHORT
);
@ -188,7 +226,6 @@ class AudioPlayerTask extends BackgroundAudioTask {
ConcatenatingAudioSource _audioSource;
AudioProcessingState _skipState;
bool _interrupted;
Seeker _seeker;
//Stream subscriptions
@ -200,10 +237,15 @@ class AudioPlayerTask extends BackgroundAudioTask {
QueueSource queueSource;
Duration _lastPosition;
Completer _androidAutoCallback;
MediaItem get mediaItem => _queue[_queueIndex];
@override
Future onStart(Map<String, dynamic> params) {
Future onStart(Map<String, dynamic> params) async {
final session = await AudioSession.instance;
session.configure(AudioSessionConfiguration.music());
//Update track index
_player.currentIndexStream.listen((index) {
@ -285,6 +327,20 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override
Future<void> onSeekBackward(bool begin) async => _seekContinuously(begin, -1);
@override
Future<List<MediaItem>> onLoadChildren(String parentMediaId) async {
AudioServiceBackground.sendCustomEvent({
'action': 'screenAndroidAuto',
'id': parentMediaId
});
//Wait for data from main thread
_androidAutoCallback = Completer();
List<MediaItem> data = (await _androidAutoCallback.future) as List<MediaItem>;
_androidAutoCallback = null;
return data;
}
//While seeking, jump 10s every 1s
void _seekContinuously(bool begin, int direction) {
_seeker?.stop();
@ -430,46 +486,14 @@ class AudioPlayerTask extends BackgroundAudioTask {
if (name == 'load') await this._loadQueueFile();
//Shuffle
if (name == 'shuffle') await _player.setShuffleModeEnabled(args);
//Android auto callback
if (name == 'screenAndroidAuto' && _androidAutoCallback != null) {
_androidAutoCallback.complete(jsonDecode(args).map<MediaItem>((m) => MediaItem.fromJson(m)).toList());
}
return true;
}
//Audio interruptions
@override
Future onAudioFocusLost(AudioInterruption interruption) {
if (_player.playing) _interrupted = true;
switch (interruption) {
case AudioInterruption.pause:
case AudioInterruption.temporaryPause:
case AudioInterruption.unknownPause:
if (_player.playing) onPause();
break;
case AudioInterruption.temporaryDuck:
_player.setVolume(0.5);
break;
}
}
@override
Future onAudioFocusGained(AudioInterruption interruption) {
switch (interruption) {
case AudioInterruption.temporaryPause:
if (!_player.playing && _interrupted) onPlay();
break;
case AudioInterruption.temporaryDuck:
_player.setVolume(1.0);
break;
default:
break;
}
_interrupted = false;
}
@override
Future onAudioBecomingNoisy() {
onPause();
}
@override
Future onTaskRemoved() async {
await onStop();
@ -561,6 +585,16 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override
Future onPlayFromMediaId(String mediaId) async {
//Android auto load tracks
if (mediaId.startsWith(AndroidAuto.prefix)) {
AudioServiceBackground.sendCustomEvent({
'action': 'tracksAndroidAuto',
'id': mediaId.replaceFirst(AndroidAuto.prefix, '')
});
return;
}
//Does the same thing
await this.onSkipToQueueItem(mediaId);
}

View File

@ -115,7 +115,7 @@ class SpotifyPlaylist {
factory SpotifyPlaylist.fromJson(Map json) => SpotifyPlaylist(
name: json['name'],
description: json['description'],
image: json['images'][0]['url'],
image: (json['images'].length > 0) ? json['images'][0]['url'] : null,
tracks: json['tracks']['items'].map<SpotifyTrack>((j) => SpotifyTrack.fromJson(j['track'])).toList()
);
}

171
lib/languages/ar_ar.dart Normal file
View File

@ -0,0 +1,171 @@
/*
Translated by: Homam Al-Rawi
*/
const language_ar_ar = {
"ar_ar": {
"Home": "القائمة الرئيسية",
"Search": "بحث",
"Library": "المكتبة",
"Offline mode, can't play flow or smart track lists.": "وضع خارج الشبكة, لا تستطيع تشغيل اغاني من قوائم ديزر فلو",
"Added to library": "تمت الاضافة الى المكتبة",
"Download": "تحميل",
"Disk": "القرص",
"Offline": "خارج الشبكة",
"Top Tracks": "افضل الاغاني",
"Show more tracks": "اضهار المزيد من الاغاني",
"Top": "الافضل",
"Top Albums": "افضل الالبومات",
"Show all albums": "اضهار كل الالبومات",
"Discography": "كل الالومات و الاغاني",
"Default": "افتراضي",
"Reverse": "عكس",
"Alphabetic": "أبجدي",
"Artist": "فنان",
"Post processing...": "بعد المعالجة...",
"Done": "تم",
"Delete": "حذف",
"Are you sure you want to delete this download?": "هل أنت متأكد أنك تريد حذف هذا التنزيل؟",
"Cancel": "الغاء",
"Downloads": "التحميلات",
"Clear queue": "مسح قائمة الانتظار",
"This won't delete currently downloading item": "لن يؤدي هذا إلى حذف العنصر الذي يتم تنزيله حاليًا",
"Are you sure you want to delete all queued downloads?": "هل أنت متأكد أنك تريد حذف كافة التنزيلات في قائمة الانتظار؟",
"Clear downloads history": "مسح تاريخ التنزيلات",
"WARNING: This will only clear non-offline (external downloads)": "تحذير: سيؤدي هذا فقط إلى مسح الملفات غير المتصلة (التنزيلات الخارجية)",
"Please check your connection and try again later...": "يرجى التحقق من الاتصال الخاص بك والمحاولة مرة أخرى في وقت لاحق...",
"Show more": "اظهار المزيد",
"Importer": "المستورد",
"Currently supporting only Spotify, with 100 tracks limit": "حاليا يدعم سبوتفاي فقط, بحد اقصى 100 اغنية",
"Due to API limitations": "بسبب قيود API",
"Enter your playlist link below": "أدخل رابط قائمة التشغيل أدناه",
"Error loading URL!": "خطأ في تحميل الرابط!",
"Convert": "تحويل",
"Download only": "تحميل فقط",
"Downloading is currently stopped, click here to resume.": "التنزيل متوقف حاليًا ، انقر هنا للاستئناف.",
"Tracks": "اغاني",
"Albums": "البومات",
"Artists": "فنانون",
"Playlists": "قوائم تشغيل",
"Import": "استيراد",
"Import playlists from Spotify": "استيراد قائمة تشغيل من سبوتيفاي",
"Statistics": "احصائيات",
"Offline tracks": "اغاني بدون اتصال",
"Offline albums": "البومات بدون اتصال",
"Offline playlists": "قوائم تشغيل بدون اتصال",
"Offline size": "حجم بدون اتصال",
"Free space": "مساحة فارغة",
"Loved tracks": "الاغاني المحبوبة",
"Favorites": "المفضلات",
"All offline tracks": "كل الاغاني بدون اتصال",
"Create new playlist": "انشاء قائمة تشغيل جديدة",
"Cannot create playlists in offline mode": "لا يمكن إنشاء قوائم التشغيل في وضع عدم الاتصال",
"Error": "خطأ",
"Error logging in! Please check your token and internet connection and try again.": "خطأ في تسجيل الدخول! يرجى التحقق من الرمز المميز والاتصال بالإنترنت وحاول مرة أخرى.",
"Dismiss": "رفض",
"Welcome to": "مرحبا بك في",
"Please login using your Deezer account.": "يرجى تسجيل الدخول باستخدام حساب ديزر الخاص بك.",
"Login using browser": "تسجيل الدخول باستخدام المتصفح",
"Login using token": "تسجيل الدخول باستخدام الرمز المميز",
"Enter ARL": "أدخل الرمز المميز (arl)",
"Token (ARL)": "الرمز المميز (ARL)",
"Save": "حفظ",
"If you don't have account, you can register on deezer.com for free.": "إذا لم يكن لديك حساب ، يمكنك التسجيل على deezer.com مجانًا.",
"Open in browser": "افتح في المتصفح",
"By using this app, you don't agree with the Deezer ToS": "باستخدام هذا التطبيق ، أنت لا توافق على شروط خدمة ديزر",
"Play next": "شغل التالي",
"Add to queue": "إضافة إلى قائمة الانتظار",
"Add track to favorites": "اضافة الاغنية الى المفضلة",
"Add to playlist": "اضافة الى قائمة التشغيل",
"Select playlist": "اختيار قائمة التشغيل",
"Track added to": "تم اضافة الاغنية الى",
"Remove from playlist": "إزالة من قائمة التشغيل",
"Track removed from": "تم إزالة الاغنية من",
"Remove favorite": "إزالة المفضلة",
"Track removed from library": "تم إزالة الاغنية من المكتبة",
"Go to": "الذهاب الى",
"Make offline": "جعله في وضع عدم الاتصال",
"Add to library": "إضافة إلى مكتبة",
"Remove album": "إزالة الالبوم",
"Album removed": "تم إزالة الالبوم",
"Remove from favorites": "تم الإزالة من المفضلة",
"Artist removed from library": "تم إزالة الفنان من المكتبة",
"Add to favorites": "اضافة الى المفضلة",
"Remove from library": "إزالة من المكتبة",
"Add playlist to library": "أضف قائمة التشغيل إلى المكتبة",
"Added playlist to library": "تم اضافة قائمة التشغيل الى المكتبة",
"Make playlist offline": "جعل قائمة التشغيل في وضع عدم الاتصال",
"Download playlist": "تنزيل قائمة التشغيل",
"Create playlist": "إنشاء قائمة التشغيل",
"Title": "عنوان",
"Description": "وصف",
"Private": "خاص",
"Collaborative": "التعاونيه",
"Create": "إنشاء",
"Playlist created!": "تم إنشاء قائمة التشغيل",
"Playing from:": "التشغيل من:",
"Queue": "قائمة الانتظار",
"Offline search": "البحث دون اتصال",
"Search Results": "نتائج البحث",
"No results!": "لا نتائج!",
"Show all tracks": "عرض كل الاغاني",
"Show all playlists": "عرض كل قوائم التشغيل",
"Settings": "الإعدادات",
"General": "عام",
"Appearance": "المظهر",
"Quality": "الجودة",
"Deezer": "ديزر",
"Theme": "ثيم",
"Currently": "حاليا",
"Select theme": "اختر ثيم",
"Light (default)": "ابيض (افتراضي)",
"Dark": "داكن (أفضل)",
"Black (AMOLED)": "أسود",
"Deezer (Dark)": "داكن (ديزر)",
"Primary color": "اللون اساسي",
"Selected color": "اللون المحدد",
"Use album art primary color": "استخدم اللون الأساسي لصورة الألبوم",
"Warning: might be buggy": "تحذير: قد يكون غير مستقر",
"Mobile streaming": "البث عبر شبكة الجوان",
"Wifi streaming": "البث عبر الوايفاي",
"External downloads": "التنزيلات الخارجية",
"Content language": "لغة المحتوى",
"Not app language, used in headers. Now": "ليست لغة التطبيق المستخدمة في العناوين. الآن",
"Select language": "اختار اللغة",
"Content country": "بلد المحتوى",
"Country used in headers. Now": "البلد المستخدم في العناوين. الآن",
"Log tracks": "تسجيل الاغاني",
"Send track listen logs to Deezer, enable it for features like Flow to work properly": "أرسال سجلات الاستماع إلى ديزر ، قم بتمكينها لميزات مثل فلو لتعمل بشكل صحيح (ينصح تفعيلها)",
"Offline mode": "وضع عدم الاتصال",
"Will be overwritten on start.": "سيتم الكتابة فوقها في البداية.",
"Error logging in, check your internet connections.": "خطأ في تسجيل الدخول ، تحقق من اتصالات الإنترنت الخاص بك.",
"Logging in...": "جار تسجيل الدخول...",
"Download path": "مسار التنزيل",
"Downloads naming": "تسمية التنزيلات",
"Downloaded tracks filename": "اسم ملف الاغاني التي تم تنزيلها",
"Valid variables are": "المتغيرات الصالحة هي",
"Reset": "إعادة تعيين",
"Clear": "تنضيف",
"Create folders for artist": "إنشاء ملفات للفنان",
"Create folders for albums": "إنشاء ملفات للالبوم",
"Separate albums by discs": "افصل الالبومات عبر رقم الاقراص",
"Overwrite already downloaded files": "الكتابة فوق الملفات التي تم تنزيلها",
"Copy ARL": "نسخ الرمز المميز (ARL)",
"Copy userToken/ARL Cookie for use in other apps.": "انسخ ملف الرابط\الرمز المميز لاستخدامه في تطبيقات أخرى.",
"Copied": "تم النسخ",
"Log out": "تسجيل خروج",
"Due to plugin incompatibility, login using browser is unavailable without restart.": "نظرًا لعدم توافق المكون الإضافي ، لا يتوفر تسجيل الدخول باستخدام المتصفح بدون إعادة التشغيل.",
"(ARL ONLY) Continue": "استمر (رمز مميز فقط ARL)",
"Log out & Exit": "تسجيل الخروج والخروج",
"Pick-a-Path": "اختر المسار",
"Select storage": "حدد وحدة التخزين",
"Go up": "اذهب للأعلى",
"Permission denied": "طلب الاذن مرفوض",
"Language": "اللغة",
"Language changed, please restart Freezer to apply!": "تم تغيير اللغة، الرجاء إعادة تشغيل فريزر لتطبيق!",
"Importing...": "جار الاستيراد...",
"Radio": "راديو"
}
};

190
lib/languages/de_de.dart Normal file
View File

@ -0,0 +1,190 @@
/*
Translated by: Markus
*/
const language_de_de = {
"de_de": {
"Home": "Start",
"Search": "Suche",
"Library": "Mediathek",
"Offline mode, can't play flow or smart track lists.":
"Offline-Modus, kann keine Flow- oder Smart Track-Listen abspielen.",
"Added to library": "Zur Mediathek hinzufügen",
"Download": "Download",
"Disk": "Disk",
"Offline": "Offline",
"Top Tracks": "Top Titel",
"Show more tracks": "Zeige mehr Titel",
"Top": "Top",
"Top Albums": "Top Alben",
"Show all albums": "Zeige alle Alben",
"Discography": "Diskografie",
"Default": "Standard",
"Reverse": "Rückwärts",
"Alphabetic": "Alphabetisch",
"Artist": "Künstler",
"Post processing...": "Nachbearbeitung...",
"Done": "Erledigt",
"Delete": "Gelöscht",
"Are you sure you want to delete this download?":
"Bist du sicher, dass du diesen Download löschen willst?",
"Cancel": "Abbrechen",
"Downloads": "Downloads",
"Clear queue": "Warteschleife löschen",
"This won't delete currently downloading item":
"Dies löscht das derzeit heruntergeladene Element nicht",
"Are you sure you want to delete all queued downloads?":
"Bist du sicher, dass du alle Downloads aus der Warteschleife löschen willst?",
"Clear downloads history": "Download-Verlauf löschen",
"WARNING: This will only clear non-offline (external downloads)":
"ACHTUNG: (Externe Downloads) werden entfernt",
"Please check your connection and try again later...":
"Bitte überprüfe deine Verbindung und versuche es später noch einmal...",
"Show more": "Mehr anzeigen",
"Importer": "Importieren",
"Currently supporting only Spotify, with 100 tracks limit":
"Derzeit begrenzt auf maximal 100 Titel",
"Due to API limitations": "Aufgrund von API-Einschränkungen",
"Enter your playlist link below":
"Gebe deinen Wiedergabelisten-Link unten ein",
"Error loading URL!": "Fehler beim Laden der URL!",
"Convert": "Konvertieren",
"Download only": "Nur Herunterladen",
"Downloading is currently stopped, click here to resume.":
"Das Herunterladen ist derzeit gestoppt, klicke hier, um fortzufahren.",
"Tracks": "Titel",
"Albums": "Alben",
"Artists": "Künstler",
"Playlists": "Wiedergabelisten",
"Import": "Importieren",
"Import playlists from Spotify": "Wiedergabelisten aus Spotify importieren",
"Statistics": "Statistiken",
"Offline tracks": "Offline-Titel",
"Offline albums": "Offline-Alben",
"Offline playlists": "Offline-Wiedergabelisten",
"Offline size": "Offline-Größe",
"Free space": "Freier Speicherplatz",
"Loved tracks": "Beliebte Titel",
"Favorites": "Favoriten",
"All offline tracks": "Alle Offline-Titel",
"Create new playlist": "Neue Wiedergabeliste erstellen",
"Cannot create playlists in offline mode":
"Wiedergabelisten können im Offline-Modus nicht erstellt werden",
"Error": "Fehler",
"Error logging in! Please check your token and internet connection and try again.":
"Fehler beim Einloggen! Bitte überprüfe dein Token und deine Internetverbindung und versuche es erneut.",
"Dismiss": "Verwerfen",
"Welcome to": "Willkommen bei",
"Please login using your Deezer account.":
"Bitte melde dich mit deinem Deezer-Konto an.",
"Login using browser": "Anmeldung über Browser",
"Login using token": "Anmeldung per Token",
"Enter ARL": "ARL eingeben",
"Token (ARL)": "Token (ARL)",
"Save": "Speichern",
"If you don't have account, you can register on deezer.com for free.":
"Wenn Du noch kein Konto hast, kannst Du Dich kostenlos auf deezer.com registrieren.",
"Open in browser": "Im Browser öffnen",
"By using this app, you don't agree with the Deezer ToS":
"Wenn Du diese Anwendung verwendest, bist Du nicht mit den Deezer ToS einverstanden",
"Play next": "Als nächstes spielen",
"Add to queue": "Zur Warteschleife hinzufügen",
"Add track to favorites": "Titel zu Favoriten hinzufügen",
"Add to playlist": "Zur Wiedergabeliste hinzufügen",
"Select playlist": "Wiedergabeliste auswählen",
"Track added to": "Titel hinzugefügt zu",
"Remove from playlist": "Aus Wiedergabeliste entfernen",
"Track removed from": "Titel entfernt aus",
"Remove favorite": "Favorit entfernen",
"Track removed from library": "Titel aus Mediathek entfernt",
"Go to": "Gehe zu",
"Make offline": "Offline verfügbar machen",
"Add to library": "Zur Mediathek hinzufügen",
"Remove album": "Album entfernen",
"Album removed": "Album entfernt",
"Remove from favorites": "Aus Favoriten entfernen",
"Artist removed from library": "Künstler aus Bibliothek entfernt",
"Add to favorites": "Zu Favoriten hinzufügen",
"Remove from library": "Aus der Mediathek entfernen",
"Add playlist to library": "Wiedergabeliste zur Mediathek hinzufügen",
"Added playlist to library": "Wiedergabeliste zur Mediathek hinzugefügt",
"Make playlist offline": "Wiedergabeliste offline verfügbar machen",
"Download playlist": "Wiedergabeliste herunterladen",
"Create playlist": "Wiedergabeliste erstellen",
"Title": "Titel",
"Description": "Beschreibung",
"Private": "Privat",
"Collaborative": "Collaborative",
"Create": "Erstellen",
"Playlist created!": "Wiedergabeliste erstellt!",
"Playing from:": "Wiedergabe von:",
"Queue": "Warteschleife",
"Offline search": "Offline-Suche",
"Search Results": "Suchergebnisse",
"No results!": "Keine Ergebnisse!",
"Show all tracks": "Alle Titel anzeigen",
"Show all playlists": "Alle Wiedergabelisten anzeigen",
"Settings": "Einstellungen",
"General": "Allgemein",
"Appearance": "Aussehen",
"Quality": "Qualität",
"Deezer": "Deezer",
"Theme": "App-Design",
"Currently": "Aktuell",
"Select theme": "App-Design auswählen",
"Light (default)": "Heller Modus (Standard)",
"Dark": "Dunkler Modus",
"Black (AMOLED)": "Schwarz (AMOLED)",
"Deezer (Dark)": "Deezer (Dunkel)",
"Primary color": "Primärfarbe",
"Selected color": "Ausgewählte Farbe",
"Use album art primary color": "Verwende die Primärfarbe des Albumcovers",
"Warning: might be buggy": "Warnung: könnte fehlerhaft sein",
"Mobile streaming": "Wiedergabe über Mobilfunknetz",
"Wifi streaming": "Wiedergabe über WLAN",
"External downloads": "Externe Downloads",
"Content language": "Content-Sprache",
"Not app language, used in headers. Now": "Aktuell",
"Select language": "Sprache auswählen",
"Content country": "Content-Land",
"Country used in headers. Now": "Aktuell",
"Log tracks": "Protokolliere Titel",
"Send track listen logs to Deezer, enable it for features like Flow to work properly":
"Gehörte Titel-Protokolle an Deezer senden, damit Flow richtig funktioniert",
"Offline mode": "Offline-Modus",
"Will be overwritten on start.": "Wird beim Start überschrieben.",
"Error logging in, check your internet connections.":
"Fehler beim Anmelden, überprüfe deine Internetverbindung.",
"Logging in...": "Angemeldet...",
"Download path": "Download-Pfad",
"Downloads naming": "Benennung der Downloads",
"Downloaded tracks filename": "Dateiname der heruntergeladenen Titel",
"Valid variables are": "Gültige Variablen sind",
"Reset": "Zurücksetzen",
"Clear": "Löschen",
"Create folders for artist": "Ordner für Künstler erstellen",
"Create folders for albums": "Ordner für Alben erstellen",
"Separate albums by discs": "Alben nach Discs trennen",
"Overwrite already downloaded files":
"Bereits heruntergeladene Dateien überschreiben",
"Copy ARL": "ARL kopieren",
"Copy userToken/ARL Cookie for use in other apps.":
"UserToken / ARL-Cookie zur Verwendung in anderen Anwendungen kopieren.",
"Copied": "Kopiert",
"Log out": "Abmelden",
"Due to plugin incompatibility, login using browser is unavailable without restart.":
"Aufgrund von Plugin-Inkompatibilität ist die Anmeldung mit dem Browser ohne Neustart nicht möglich.",
"(ARL ONLY) Continue": "(NUR ARL) Fortfahren",
"Log out & Exit": "Abmelden & Beenden",
"Pick-a-Path": "Wähle einen Pfad",
"Select storage": "Verzeichnis auswählen",
"Go up": "Nach oben",
"Permission denied": "Zugriff verweigert",
"Language": "Sprache",
"Language changed, please restart Freezer to apply!":
"Sprache geändert, bitte Freezer neu starten!",
"Importing...": "Importiere...",
"Radio": "Radio"
}
};

187
lib/languages/en_us.dart Normal file
View File

@ -0,0 +1,187 @@
const language_en_us = {
"en_us": {
"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",
"Light (default)": "Light (Default)",
"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 discs",
"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",
}
};

192
lib/languages/it_it.dart Normal file
View File

@ -0,0 +1,192 @@
/*
Translated by: Andrea
*/
const language_it_it = {
"it_it": {
"Home": "Pagina Iniziale",
"Search": "Cerca",
"Library": "Libreria",
"Offline mode, can't play flow or smart track lists.":
"Modalità offline, non è possibile riprodurre flow o tracklist smart",
"Added to library": "Aggiunto Alla Libreria",
"Download": "Download",
"Disk": "Disco",
"Offline": "Offline",
"Top Tracks": "Top Tracks",
"Show more tracks": "Mostra più tracce",
"Top": "Top",
"Top Albums": "Top Albums",
"Show all albums": "Mostra tutti gli albums",
"Discography": "Discografia",
"Default": "Default",
"Reverse": "Reverse",
"Alphabetic": "Alfabetico",
"Artist": "Artista",
"Post processing...": "Post processing...",
"Done": "Terminato",
"Delete": "Cancellato",
"Are you sure you want to delete this download?":
"Sei sicuro di voler cancellare questo download?",
"Cancel": "Cancella",
"Downloads": "Downloads",
"Clear queue": "Pulisci la coda",
"This won't delete currently downloading item":
"Questa azione non cancellerà i downloads",
"Are you sure you want to delete all queued downloads?":
"Sei sicuro di voler cancellare tutti i downloads in coda?",
"Clear downloads history": "Pulisci la cronologia dei downloads",
"WARNING: This will only clear non-offline (external downloads)":
"ATTENZIONE: Questa azione, pulirà solo i files che non sono offline (download esterni)",
"Please check your connection and try again later...":
"Per favore controlla la tua connessione e riprova più tardi...",
"Show more": "Mostra di più",
"Importer": "Importa",
"Currently supporting only Spotify, with 100 tracks limit":
"Attualmente supporta solo Spotify, con un limite di 100 tracce",
"Due to API limitations": "A Causa delle limitazioni dell' API",
"Enter your playlist link below":
"Inserisci il link della tua playlist qui sotto",
"Error loading URL!": "Errore di caricamento dell' URL!",
"Convert": "Converti",
"Download only": "Solo Download",
"Downloading is currently stopped, click here to resume.":
"Il download è attualmente interrotto, fare clic qui per riprenderlo.",
"Tracks": "Tracce",
"Albums": "Albums",
"Artists": "Artisti",
"Playlists": "Playlists",
"Import": "Importa",
"Import playlists from Spotify": "Importa playlists da Spotify",
"Statistics": "Statistiche",
"Offline tracks": "Tracce Offline",
"Offline albums": "Offline albums",
"Offline playlists": "Offline playlists",
"Offline size": "Offline size",
"Free space": "Spazio Libero",
"Loved tracks": "Tracce Amate",
"Favorites": "Preferiti",
"All offline tracks": "tutte le tracce offline",
"Create new playlist": "Crea nuova playlist",
"Cannot create playlists in offline mode":
"Impossibile creare playlist in modalità offline",
"Error": "Errore",
"Error logging in! Please check your token and internet connection and try again.":
"Errore durante l'accesso! Controlla il token e la connessione Internet e riprova.",
"Dismiss": "Respinto",
"Welcome to": "Benvenuto In",
"Please login using your Deezer account.":
"Per favore, esegui il login utilizzando il tuo account Deezer.",
"Login using browser": "Login utilizzando il browser",
"Login using token": "Login utilizzando il token",
"Enter ARL": "Inserisci l'ARL",
"Token (ARL)": "Token (ARL)",
"Save": "Salva",
"If you don't have account, you can register on deezer.com for free.":
"Se non possiede l'account, puoi registrarti sul sito deezer.com gratuitamente.",
"Open in browser": "Apri nel browser",
"By using this app, you don't agree with the Deezer ToS":
"Utilizzando questa applicazione, non sei daccordo con i ToS di Deezer",
"Play next": "Riproduci la prossima",
"Add to queue": "Aggiungi alla coda",
"Add track to favorites": "Aggiungi la traccia ai preferiti",
"Add to playlist": "Aggiungi alla Playlist",
"Select playlist": "Seleziona playlist",
"Track added to": "Traccia aggiunta a",
"Remove from playlist": "Rimuovi dalla playlist",
"Track removed from": "Traccia Rimossa Da",
"Remove favorite": "Rimuovi preferito",
"Track removed from library": "Traccia rimossa dalla libreria",
"Go to": "Vai A",
"Make offline": "Rendi Offline",
"Add to library": "Aggiungi alla libreria",
"Remove album": "Rimuovi album",
"Album removed": "Album rimosso",
"Remove from favorites": "Rimuovi dai preferiti",
"Artist removed from library": "Artista rimosso dalla libreria",
"Add to favorites": "Aggiungi ai preferiti",
"Remove from library": "Rimuovi dalla libreria",
"Add playlist to library": "Aggiungi playlist alla libreria",
"Added playlist to library": "Playlist aggiunta alla libreria",
"Make playlist offline": "Rendi la playlist offline",
"Download playlist": "Scarica Playlist",
"Create playlist": "Crea Playlist",
"Title": "Titolo",
"Description": "Descrizione",
"Private": "Privata",
"Collaborative": "Collaborativa",
"Create": "Crea",
"Playlist created!": "Playlist creata!",
"Playing from:": "Riproducendo da:",
"Queue": "Coda",
"Offline search": "Ricerca offline",
"Search Results": "Risultati della ricerca",
"No results!": "Nessun risultato!",
"Show all tracks": "Mostra tutte le tracce",
"Show all playlists": "Mostra tutte le playlists",
"Settings": "Opzioni",
"General": "Generale",
"Appearance": "Aspetto",
"Quality": "Qualità",
"Deezer": "Deezer",
"Theme": "Tema",
"Currently": "Attuale",
"Select theme": "Seleziona Tema",
"Light (default)": "Chiaro (Default)",
"Dark": "Scuro",
"Black (AMOLED)": "Nero (AMOLED)",
"Deezer (Dark)": "Deezer (Scuro)",
"Primary color": "Colore Principale",
"Selected color": "Colore Selezionato",
"Use album art primary color":
"Usa il colore principale della copertina dell'album",
"Warning: might be buggy": "Attenzione: potrebbe essere fallato",
"Mobile streaming": "Mobile Streaming",
"Wifi streaming": "Wifi streaming",
"External downloads": "Download Esterni",
"Content language": "Lingua dei contenuti",
"Not app language, used in headers. Now":
"Non la lingua dell'app, utilizzata nelle intestazioni. Adesso",
"Select language": "Seleziona la Lingua",
"Content country": "Contenuto Del Paese",
"Country used in headers. Now": "Paese contenuto nelle intestazioni. Ora",
"Log tracks": "Log delle tracce",
"Send track listen logs to Deezer, enable it for features like Flow to work properly":
"Invia i log delle canzioni ascoltate a Deezer, abilitalo affinché funzioni come Flow funzionino correttamente ",
"Offline mode": "Modalità Offline",
"Will be overwritten on start.": "Sarà sovrascritto all'avvio.",
"Error logging in, check your internet connections.":
"Errore durante l'accesso, controlla la tua connessione Internet.",
"Logging in...": "Accesso in corso...",
"Download path": "Percorso di download",
"Downloads naming": "Denominazione dei download",
"Downloaded tracks filename": "Nome del file delle tracce scaricate",
"Valid variables are": "Le variabili valide sono",
"Reset": "Reset",
"Clear": "Pulisci",
"Create folders for artist": "Crea cartelle per artista",
"Create folders for albums": "Crea cartelle per albums",
"Separate albums by discs": "Albums separati per disco",
"Overwrite already downloaded files": "Sovrascrivi i file già scaricati",
"Copy ARL": "Copia ARL",
"Copy userToken/ARL Cookie for use in other apps.":
"Copia userToken / ARL Cookie da utilizzare in altre apps.",
"Copied": "Copiato",
"Log out": "Log out",
"Due to plugin incompatibility, login using browser is unavailable without restart.":
"A causa dell'incompatibilità del plug-in, l'accesso tramite browser non è disponibile senza riavvio.",
"(ARL ONLY) Continue": "(SOLO ARL) Continua",
"Log out & Exit": "Log out & Esci",
"Pick-a-Path": "Scegli un percorso",
"Select storage": "Seleziona spazio di archiviazione",
"Go up": "Vai su",
"Permission denied": "Permesso negato",
"Language": "Lingua",
"Language changed, please restart Freezer to apply!":
"Lingua cambiata, riavvia Freezer per la modifica della lingua!",
"Importing...": "Importando...",
"Radio": "Radio"
}
};

169
lib/languages/pt_br.dart Normal file
View File

@ -0,0 +1,169 @@
/*
Translated by: Diego Hiro
*/
const language_pt_br = {"pt_br": {
"Home": "Início",
"Search": "Pesquisar",
"Library": "Biblioteca",
"Offline mode, can't play flow or smart track lists.": "Modo offline, incapaz de reproduzir faixas do flow(personalizadas) ou playlist inteligentes.",
"Added to library": "Adicionado para sua biblioteca",
"Download": "Download",
"Disk": "Disco",
"Offline": "Offline",
"Top Tracks": "Faixas no Top",
"Show more tracks": "Exibir mais faixas",
"Top": "Top",
"Top Albums": "Álbuns no Top",
"Show all albums": "Mostrar todos os álbuns",
"Discography": "Discografia",
"Default": "Padrão",
"Reverse": "Reverter",
"Alphabetic": "Alfabética",
"Artist": "Artista",
"Post processing...": "Processando...",
"Done": "Feito",
"Delete": "Deletar",
"Are you sure you want to delete this download?": "Tem certeza que deseja excluir este download?",
"Cancel": "Cancelar",
"Downloads": "Downloads",
"Clear queue": "Limpar fila",
"This won't delete currently downloading item": "Isso não excluirá os itens que estão fazendo download",
"Are you sure you want to delete all queued downloads?": "Tem certeza que deseja excluir todos os downloads que estão na fila?",
"Clear downloads history": "Limpar histórico de downloads",
"WARNING: This will only clear non-offline (external downloads)": "Cuidado: Isso limpará apenas faixas e listas off-line (downloads externos)",
"Please check your connection and try again later...": "Verifique sua conexão e tente novamente. Caso sua rede não esteja estável, tente mais tarde...",
"Show more": "Mostrar Mais",
"Importer": "importador",
"Currently supporting only Spotify, with 100 tracks limit": "Atualmente suportando apenas Spotify, com limite de 100 faixas",
"Due to API limitations": "Devido às limitações da API",
"Enter your playlist link below": "Insira o link da sua lista de reprodução abaixo",
"Error loading URL!": "Erro ao carregar URL!",
"Convert": "Converter",
"Download only": "Somente download",
"Downloading is currently stopped, click here to resume.": "O download está parado no momento, clique aqui para retomar.",
"Tracks": "Faixas",
"Albums": "Álbuns",
"Artists": "Artistas",
"Playlists": "Playlists",
"Import": "Importar",
"Import playlists from Spotify": "Importar playlists do Spotify",
"Statistics": "Estatísticas",
"Offline tracks": "Faixas Offline",
"Offline albums": "Álbuns Offline",
"Offline playlists": "Playlists Offline",
"Offline size": "Espaço ocupado Offline",
"Free space": "Espaço livre",
"Loved tracks": "Faixas que gostou",
"Favorites": "Favoritos",
"All offline tracks": "Todas as faixas offline",
"Create new playlist": "Criar nova playlist",
"Cannot create playlists in offline mode": "Não é possível criar playlists no modo offline",
"Error": "Erro",
"Error logging in! Please check your token and internet connection and try again.": "Erro ao tentar login! Verifique seu token e sua conexão com a Internet, tente novamente.",
"Dismiss": "Dispensar",
"Welcome to": "Bem-vindo ao",
"Please login using your Deezer account.": "Faça login usando sua conta Deezer.",
"Login using browser": "Faça login usando o navegador",
"Login using token": "Faça login usando o token",
"Enter ARL": "Inserir ARL",
"Token (ARL)": "Token (ARL)",
"Save": "Salvar",
"If you don't have account, you can register on deezer.com for free.": "Se você não tem uma conta, pode se registrar em deezer.com gratuitamente.",
"Open in browser": "Abra no navegador",
"By using this app, you don't agree with the Deezer ToS": "Ao usar este aplicativo, você não concorda com os Termos de Uso com a Deezer",
"Play next": "Tocar próxima",
"Add to queue": "Adicionar à fila",
"Add track to favorites": "Adicionar faixa aos favoritos",
"Add to playlist": "Adicionar à Playlist",
"Select playlist": "Selecionar playlist",
"Track added to": "Faixa adicionada para",
"Remove from playlist": "Remover da playlist",
"Track removed from": "Faixa removida do(a)",
"Remove favorite": "Remover favorito",
"Track removed from library": "Faixa removida da biblioteca",
"Go to": "Ir para",
"Make offline": "Reproduzir offline",
"Add to library": "Adicionar à biblioteca",
"Remove album": "Remover álbum",
"Album removed": "Álbum removido",
"Remove from favorites": "Remover do favoritos",
"Artist removed from library": "Artista Removido da biblioteca",
"Add to favorites": "Adicionar para favoritos",
"Remove from library": "Remover da biblioteca",
"Add playlist to library": "Adicionar playlist para biblioteca",
"Added playlist to library": "Playlist adicionada para biblioteca",
"Make playlist offline": "Converter playlist para modo offline",
"Download playlist": " Efetuar download da playlist",
"Create playlist": "Criar playlist",
"Title": "Título",
"Description": "Descrição",
"Private": "Privado",
"Collaborative": "Colaborativo",
"Create": "Criar",
"Playlist created!": "Playlist criada!",
"Playing from:": "Playing de:",
"Queue": "Fila",
"Offline search": "Pesquisa Offline",
"Search Results": "Resultado da pesquisa",
"No results!": "Nenhum resultado encontrado!",
"Show all tracks": "Mostrar todas as faixas",
"Show all playlists": "Mostrar todas playlists",
"Settings": "Configurações",
"General": "Geral",
"Appearance": "Aparência",
"Quality": "Qualidade",
"Deezer": "Deezer",
"Theme": "Tema",
"Currently": "Atualmente",
"Select theme": "Selecionar tema",
"Light (default)": "Claro (Padrão)",
"Dark": "Escuro",
"Black (AMOLED)": "Preto (AMOLED)",
"Deezer (Dark)": "Deezer (Escuro - Dark Mode)",
"Primary color": "Cor Primária",
"Selected color": "Cor selecionada",
"Use album art primary color": "Use a cor primária da capa do álbum",
"Warning: might be buggy": "Cuidado: pode ter erros dependendo do dispositivo",
"Mobile streaming": "Streaming por dados móveis",
"Wifi streaming": "Streaming por Rede Wifi",
"External downloads": "Downloads Externos",
"Content language": "Linguagem do conteúdo",
"Not app language, used in headers. Now": "Não é o idioma do aplicativo, programação feita em outra Linguagem. Agora",
"Select language": "Selecione a linguagem",
"Content country": "País do conteúdo a Exibir",
"Country used in headers. Now": "País habilitado no banco de dados. Agora",
"Log tracks": "Log de faixas",
"Send track listen logs to Deezer, enable it for features like Flow to work properly": "Enviar registros de faixas de trilhas para o Deezer, habilite-o para o funcionamento de recursos, como o Flow para funcionar corretamente",
"Offline mode": "Modo Offline",
"Will be overwritten on start.": "Será sobrescrito no próximo início do aplicativo.",
"Error logging in, check your internet connections.": "Erro ao fazer login, verifique suas conexões de internet.",
"Logging in...": "Logando em...",
"Download path": "Caminho de download",
"Downloads naming": "Nomenclatura de downloads",
"Downloaded tracks filename": "Nome de arquivo das faixas baixadas",
"Valid variables are": "Variáveis válidas são",
"Reset": "Resetar",
"Clear": "Limpar",
"Create folders for artist": "Create folders for artist",
"Create folders for albums": "Create folders for albums",
"Separate albums by discs": "Separate albums by discs",
"Overwrite already downloaded files": "Overwrite already downloaded files",
"Copy ARL": "Copiar ARL",
"Copy userToken/ARL Cookie for use in other apps.": "Copiar userToken/ARL Cookie para uso em outros aplicativos.",
"Copied": "Copiado",
"Log out": "Deslogar",
"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": "(Somente ARL) Continuar",
"Log out & Exit": "Deslogar & Sair",
"Pick-a-Path": "Escola-um-Caminho",
"Select storage": "Selecione o armazenamento",
"Go up": "Subir",
"Permission denied": "Permissão negada",
"Language": "Linguagem",
"Language changed, please restart Freezer to apply!": "Idioma alterado, reinicie o Freezer para aplicar!",
"Importing...": "Importando..."
}
};

190
lib/languages/ru_ru.dart Normal file
View File

@ -0,0 +1,190 @@
/*
Translated by: Annexhack
*/
const language_ru_ru = {
"ru_ru": {
"Home": "Главная",
"Search": "Поиск",
"Library": "Библиотека",
"Offline mode, can't play flow or smart track lists.":
"Автономный режим, нельзя воспроизводить потоки или умные списки треков.",
"Added to library": "Добавить в библиотеку",
"Download": "Скачать",
"Disk": "Disk",
"Offline": "Офлайн",
"Top Tracks": "Лучшие треки",
"Show more tracks": "Показать больше треков",
"Top": "Top",
"Top Albums": "Лучшие альбомы",
"Show all albums": "Показать все альбомы",
"Discography": "Дискография",
"Default": "По умолчанию",
"Reverse": "Обратный",
"Alphabetic": "По алфавиту",
"Artist": "Артист",
"Post processing...": "Постобработка...",
"Done": "Готово",
"Delete": "Удалить",
"Are you sure you want to delete this download?":
"Вы действительно хотите удалить эту загрузку??",
"Cancel": "Отмена",
"Downloads": "Загрузки",
"Clear queue": "Очистить очередь",
"This won't delete currently downloading item":
"Это не удалит загружаемый в данный момент элемент",
"Are you sure you want to delete all queued downloads?":
"Вы действительно хотите удалить все загрузки в очереди?",
"Clear downloads history": "Очистить историю загрузок",
"WARNING: This will only clear non-offline (external downloads)":
"ВНИМАНИЕ: Это очистит только не офлайн(external downloads)",
"Please check your connection and try again later...":
"Пожалуйста, проверьте ваше соединение и повторите попытку позже...",
"Show more": "Показать больше",
"Importer": "Импортер",
"Currently supporting only Spotify, with 100 tracks limit":
"В настоящее время поддерживается только Spotify с ограничением 100 треков",
"Due to API limitations": "Из-за ограничений API",
"Enter your playlist link below": "Введите ссылку на свой плейлист ниже",
"Error loading URL!": "Ошибка загрузки URL!",
"Convert": "Перерабатывать",
"Download only": "Только скачиные",
"Downloading is currently stopped, click here to resume.":
"В настоящее время загрузка остановлена, нажмите здесь, чтобы возобновить.",
"Tracks": "Треки",
"Albums": "Альбомы",
"Artists": "Артисты",
"Playlists": "Плейлисты",
"Import": "Import",
"Import playlists from Spotify": "Импортировать плейлисты из Spotify",
"Statistics": "Статистика",
"Offline tracks": "Автономные треки",
"Offline albums": "Автономные альбомы",
"Offline playlists": "Офлайн-плейлисты",
"Offline size": "Автономный размер",
"Free space": "Свободное место",
"Loved tracks": "Любимые треки",
"Favorites": "Избранное",
"All offline tracks": "Все оффлайн треки",
"Create new playlist": "Создать новый плейлист",
"Cannot create playlists in offline mode":
"Невозможно создавать плейлисты в автономном режиме",
"Error": "Ошибка",
"Error logging in! Please check your token and internet connection and try again.":
"Ошибка входа! Проверьте свой токен и подключение к Интернету и повторите попытку.",
"Dismiss": "Отклонить",
"Welcome to": "Добро пожаловать в",
"Please login using your Deezer account.":
"Пожалуйста, войдите, используя свою учетную запись Deezer.",
"Login using browser": "Войти через браузер",
"Login using token": "Войти с помощью токена",
"Enter ARL": "Введите ARL",
"Token (ARL)": "Токен (ARL)",
"Save": "Сохранить",
"If you don't have account, you can register on deezer.com for free.":
"Если у вас нет учетной записи, вы можете бесплатно зарегистрироваться на deezer.com.",
"Open in browser": "Открыть в браузере",
"By using this app, you don't agree with the Deezer ToS":
"Используя это приложение, вы не соглашаетесь с Условиями использования Deezer.",
"Play next": "Следующая песня",
"Add to queue": "Добавить в очередь",
"Add track to favorites": "Добавить в избранное",
"Add to playlist": "Добавить в плейлист",
"Select playlist": "Выбрать плейлист",
"Track added to": "Трек добавлен в",
"Remove from playlist": "Удалить из плейлиста",
"Track removed from": "Трек удален из",
"Remove favorite": "Удалить избранное",
"Track removed from library": "Трек удален из библиотеки",
"Go to": "Перейти к",
"Make offline": "Сделать офлайн",
"Add to library": "Добавить в библиотеку",
"Remove album": "Удалить альбом",
"Album removed": "Альбом удален",
"Remove from favorites": "Удалить из Избранного",
"Artist removed from library": "Артист удален из библиотеки",
"Add to favorites": "Добавить в избранное",
"Remove from library": "Удалить из библиотеки",
"Add playlist to library": "Добавить плейлист в библиотеку",
"Added playlist to library": "Добавлен плейлист в библиотеку",
"Make playlist offline": "Сделать плейлист офлайн",
"Download playlist": "Скачать плейлист",
"Create playlist": "Создать плейлист",
"Title": "Название",
"Description": "Описание",
"Private": "Частный",
"Collaborative": "Совместная",
"Create": "Создать",
"Playlist created!": "Плейлист создан!",
"Playing from:": "Играя с:",
"Queue": "Очередь",
"Offline search": "Оффлайн поиск",
"Search Results": "Результаты поиска",
"No results!": "Нет результатов!",
"Show all tracks": "Показать все треки",
"Show all playlists": "Показать все плейлисты",
"Settings": "Настройки",
"General": "Общее",
"Appearance": "Внешность",
"Quality": "Качественный",
"Deezer": "Deezer",
"Theme": "Тема",
"Currently": "В настоящее время",
"Select theme": "Выберите тему",
"Light (default)": "Светлая (По умолчанию)",
"Dark": "Темная",
"Black (AMOLED)": "Черная (AMOLED)",
"Deezer (Dark)": "Deezer (Dark)",
"Primary color": "Основной цвет",
"Selected color": "Выбранный цвет",
"Use album art primary color": "Использовать основной цвет обложки альбома",
"Warning: might be buggy": "Предупреждение: может быть ошибка",
"Mobile streaming": "Мобильная трансляция",
"Wifi streaming": "Wifi трансляция",
"External downloads": "Внешние загрузки",
"Content language": "Язык содержания",
"Not app language, used in headers. Now":
"Не язык приложения, используемый в заголовках. Сейчас",
"Select language": "Выберите язык",
"Content country": "Страна содержания",
"Country used in headers. Now": "Страна, используемая в заголовках. Сейчас",
"Log tracks": "Журнал треков",
"Send track listen logs to Deezer, enable it for features like Flow to work properly":
"Отправьте журналы прослушивания треков в Deezer, включите его, чтобы такие функции, как Flow, работали правильно",
"Offline mode": "Автономный режим",
"Will be overwritten on start.": "Будет перезаписан при запуске.",
"Error logging in, check your internet connections.":
"Ошибка при входе, проверьте свои интернет-соединения.",
"Logging in...": "Происходит вход в систему...",
"Download path": "Скачать путь",
"Downloads naming": "Именование загрузок",
"Downloaded tracks filename": "Имя файла загруженных треков",
"Valid variables are": "Допустимые переменные:",
"Reset": "Сброс",
"Clear": "Очистить",
"Create folders for artist": "Создавайте папки для исполнителя",
"Create folders for albums": "Создавайте папки для альбомов",
"Separate albums by discs": "Отдельные альбомы по дискам",
"Overwrite already downloaded files": "Перезаписать уже загруженные файлы",
"Copy ARL": "Копировать ARL",
"Copy userToken/ARL Cookie for use in other apps.":
"Копировать userToken/ARL Cookie для использования в других приложениях.",
"Copied": "Скопировано",
"Log out": "Выйти",
"Due to plugin incompatibility, login using browser is unavailable without restart.":
"Из-за несовместимости плагинов вход через браузер без перезапуска невозможен.",
"(ARL ONLY) Continue": "(ARL ONLY) Продолжать",
"Log out & Exit": "Выйти и закрыть",
"Pick-a-Path": "Выбери путь",
"Select storage": "Выберите хранилище",
"Go up": "Подниматься",
"Permission denied": "Доступ запрещен",
"Language": "Язык",
"Language changed, please restart Freezer to apply!":
"Язык изменен, перезапустите Freezer, чтобы применить!",
"Importing...": "Импорт...",
"Radio": "Радио"
}
};

View File

@ -1,28 +1,29 @@
import 'package:custom_navigator/custom_navigator.dart';
import 'package:audio_service/audio_service.dart';
import 'package:custom_navigator/custom_navigator.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:freezer/ui/library.dart';
import 'package:freezer/ui/login_screen.dart';
import 'package:freezer/ui/search.dart';
import 'package:i18n_extension/i18n_widget.dart';
import 'package:move_to_background/move_to_background.dart';
import 'package:freezer/translations.i18n.dart';
import 'ui/player_bar.dart';
import 'api/deezer.dart';
import 'settings.dart';
import 'ui/cached_image.dart';
import 'api/download.dart';
import 'api/player.dart';
import 'settings.dart';
import 'ui/home_screen.dart';
import 'ui/player_bar.dart';
Function updateTheme;
Function logOut;
GlobalKey<NavigatorState> mainNavigatorKey = GlobalKey<NavigatorState>();
GlobalKey<NavigatorState> navigatorKey;
void main() async {
void main() async {
WidgetsFlutterBinding.ensureInitialized();
//Initialize globals
@ -57,11 +58,22 @@ class _FreezerAppState extends State<FreezerApp> {
});
}
Locale _locale() {
if (settings.language == null || settings.language.split('_').length < 2) return null;
return Locale(settings.language.split('_')[0], settings.language.split('_')[1]);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'freezer',
title: 'Freezer',
theme: settings.themeData,
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: supportedLocales,
home: WillPopScope(
onWillPop: () async {
//For some reason AudioServiceWidget caused the app to freeze after 2 back button presses. "fix"
@ -72,7 +84,10 @@ class _FreezerAppState extends State<FreezerApp> {
await MoveToBackground.moveTaskToBack();
return false;
},
child: LoginMainWrapper(),
child: I18n(
initialLocale: _locale(),
child: LoginMainWrapper(),
),
),
navigatorKey: mainNavigatorKey,
);
@ -86,7 +101,6 @@ class LoginMainWrapper extends StatefulWidget {
}
class _LoginMainWrapperState extends State<LoginMainWrapper> {
@override
void initState() {
if (settings.arl != null) {
@ -116,25 +130,20 @@ class _LoginMainWrapperState extends State<LoginMainWrapper> {
@override
Widget build(BuildContext context) {
if (settings.arl == null)
return LoginWidget(callback: () => setState(() => {}),);
return LoginWidget(
callback: () => setState(() => {}),
);
return MainScreen();
}
}
class MainScreen extends StatefulWidget {
@override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
List<Widget> _screens = [
HomeScreen(),
SearchScreen(),
LibraryScreen()
];
List<Widget> _screens = [HomeScreen(), SearchScreen(), LibraryScreen()];
int _selected = 0;
@override
@ -146,50 +155,45 @@ class _MainScreenState extends State<MainScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
PlayerBar(),
BottomNavigationBar(
backgroundColor: Theme.of(context).bottomAppBarColor,
currentIndex: _selected,
onTap: (int s) async {
//Pop all routes until home screen
bottomNavigationBar: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
PlayerBar(),
BottomNavigationBar(
backgroundColor: Theme.of(context).bottomAppBarColor,
currentIndex: _selected,
onTap: (int s) async {
//Pop all routes until home screen
while (navigatorKey.currentState.canPop()) {
await navigatorKey.currentState.maybePop();
}
while (navigatorKey.currentState.canPop()) {
await navigatorKey.currentState.maybePop();
}
await navigatorKey.currentState.maybePop();
setState(() {
_selected = s;
});
},
selectedItemColor: Theme.of(context).primaryColor,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home')
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
title: Text('Search'),
),
BottomNavigationBarItem(
icon: Icon(Icons.library_music),
title: Text('Library')
)
],
)
],
),
body: AudioServiceWidget(
child: CustomNavigator(
navigatorKey: navigatorKey,
home: _screens[_selected],
pageRoute: PageRoutes.materialPageRoute,
setState(() {
_selected = s;
});
},
selectedItemColor: Theme.of(context).primaryColor,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text('Home'.i18n)),
BottomNavigationBarItem(
icon: Icon(Icons.search),
title: Text('Search'.i18n),
),
BottomNavigationBarItem(
icon: Icon(Icons.library_music), title: Text('Library'.i18n))
],
)
],
),
)
);
body: AudioServiceWidget(
child: CustomNavigator(
navigatorKey: navigatorKey,
home: _screens[_selected],
pageRoute: PageRoutes.materialPageRoute,
),
));
}
}

View File

@ -19,6 +19,10 @@ Settings settings;
@JsonSerializable()
class Settings {
//Language
@JsonKey(defaultValue: null)
String language;
//Account
String arl;
@JsonKey(ignore: true)

View File

@ -11,6 +11,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
downloadPath: json['downloadPath'] as String,
arl: json['arl'] as String,
)
..language = json['language'] as String
..wifiQuality =
_$enumDecodeNullable(_$AudioQualityEnumMap, json['wifiQuality']) ??
AudioQuality.MP3_320
@ -39,6 +40,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
}
Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
'language': instance.language,
'arl': instance.arl,
'wifiQuality': _$AudioQualityEnumMap[instance.wifiQuality],
'mobileQuality': _$AudioQualityEnumMap[instance.mobileQuality],

View File

@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:freezer/languages/ar_ar.dart';
import 'package:freezer/languages/de_de.dart';
import 'package:freezer/languages/en_us.dart';
import 'package:freezer/languages/it_it.dart';
import 'package:freezer/languages/pt_br.dart';
import 'package:freezer/languages/ru_ru.dart';
import 'package:i18n_extension/i18n_extension.dart';
const supportedLocales = [
const Locale('en', 'US'),
const Locale('ar', 'AR'),
const Locale('pt', 'BR'),
const Locale('it', 'IT'),
const Locale('de', 'DE'),
const Locale('ru', 'RU')
];
extension Localization on String {
static var _t = Translations.byLocale("en_US") +
language_en_us + language_ar_ar + language_pt_br + language_it_it + language_de_de + language_ru_ru;
String get i18n => localize(this, _t);
}

216
lib/ui/android_auto.dart Normal file
View File

@ -0,0 +1,216 @@
import 'package:audio_service/audio_service.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/translations.i18n.dart';
class AndroidAuto {
//Prefix for "playable" MediaItem
static const prefix = '_aa_';
//Get media items for parent id
Future<List<MediaItem>> getScreen(String parentId) async {
print(parentId);
//Homescreen
if (parentId == 'root' || parentId == null) return homeScreen();
//Playlists screen
if (parentId == 'playlists') {
//Fetch
List<Playlist> playlists = await deezerAPI.getPlaylists();
List<MediaItem> out = playlists.map<MediaItem>((p) => MediaItem(
id: '${prefix}playlist${p.id}',
displayTitle: p.title,
displaySubtitle: p.trackCount.toString() + ' ' + 'Tracks'.i18n,
playable: true,
artUri: p.image.thumb
)).toList();
return out;
}
//Albums screen
if (parentId == 'albums') {
List<Album> albums = await deezerAPI.getAlbums();
List<MediaItem> out = albums.map<MediaItem>((a) => MediaItem(
id: '${prefix}album${a.id}',
displayTitle: a.title,
displaySubtitle: a.artistString,
playable: true,
artUri: a.art.thumb,
)).toList();
return out;
}
//Artists screen
if (parentId == 'artists') {
List<Artist> artists = await deezerAPI.getArtists();
List<MediaItem> out = artists.map<MediaItem>((a) => MediaItem(
id: 'albums${a.id}',
displayTitle: a.name,
playable: false,
artUri: a.picture.thumb
)).toList();
return out;
}
//Artist screen (albums, etc)
if (parentId.startsWith('albums')) {
List<Album> albums = await deezerAPI.discographyPage(parentId.replaceFirst('albums', ''));
List<MediaItem> out = albums.map<MediaItem>((a) => MediaItem(
id: '${prefix}album${a.id}',
displayTitle: a.title,
displaySubtitle: a.artistString,
playable: true,
artUri: a.art.thumb
)).toList();
return out;
}
//Homescreen
if (parentId == 'homescreen') {
HomePage hp = await deezerAPI.homePage();
List<MediaItem> out = [];
for (HomePageSection section in hp.sections) {
for (int i=0; i<section.items.length; i++) {
//Limit to max 5 items
if (i == 5) break;
//Check type
var data = section.items[i].value;
switch (section.items[i].type) {
case HomePageItemType.PLAYLIST:
out.add(MediaItem(
id: '${prefix}playlist${data.id}',
displayTitle: data.title,
playable: true,
artUri: data.image.thumb
));
break;
case HomePageItemType.ALBUM:
out.add(MediaItem(
id: '${prefix}album${data.id}',
displayTitle: data.title,
displaySubtitle: data.artistString,
playable: true,
artUri: data.art.thumb
));
break;
case HomePageItemType.ARTIST:
out.add(MediaItem(
id: 'albums${data.id}',
displayTitle: data.name,
playable: false,
artUri: data.picture.thumb
));
break;
case HomePageItemType.SMARTTRACKLIST:
out.add(MediaItem(
id: '${prefix}stl${data.id}',
displayTitle: data.title,
displaySubtitle: data.subtitle,
playable: true,
artUri: data.cover.thumb
));
}
}
}
return out;
}
return [];
}
//Load virtual mediaItem
Future playItem(String id) async {
print(id);
//Play flow
if (id == 'flow' || id == 'stlflow') {
await playerHelper.playFromSmartTrackList(SmartTrackList(id: 'flow', title: 'Flow'.i18n));
return;
}
//Play library tracks
if (id == 'tracks') {
//Load tracks
Playlist favPlaylist;
try {
favPlaylist = await deezerAPI.fullPlaylist(deezerAPI.favoritesPlaylistId);
} catch (e) {print(e);}
if (favPlaylist == null || favPlaylist.tracks.length == 0) return;
await playerHelper.playFromTrackList(favPlaylist.tracks, favPlaylist.tracks[0].id, QueueSource(
id: 'allTracks',
text: 'All offline tracks'.i18n,
source: 'offline'
));
return;
}
//Play playlists
if (id.startsWith('playlist')) {
Playlist p = await deezerAPI.fullPlaylist(id.replaceFirst('playlist', ''));
await playerHelper.playFromPlaylist(p, p.tracks[0].id);
return;
}
//Play albums
if (id.startsWith('album')) {
Album a = await deezerAPI.album(id.replaceFirst('album', ''));
await playerHelper.playFromAlbum(a, a.tracks[0].id);
return;
}
//Play smart track list
if (id.startsWith('stl')) {
SmartTrackList stl = await deezerAPI.smartTrackList(id.replaceFirst('stl', ''));
await playerHelper.playFromSmartTrackList(stl);
return;
}
}
//Homescreen items
List<MediaItem> homeScreen() {
return [
MediaItem(
id: '${prefix}flow',
displayTitle: 'Flow'.i18n,
playable: true
),
MediaItem(
id: 'homescreen',
displayTitle: 'Home'.i18n,
playable: false,
),
MediaItem(
id: '${prefix}tracks',
displayTitle: 'Loved tracks'.i18n,
playable: true,
),
MediaItem(
id: 'playlists',
displayTitle: 'Playlists'.i18n,
playable: false,
),
MediaItem(
id: 'albums',
displayTitle: 'Albums'.i18n,
playable: false,
),
MediaItem(
id: 'artists',
displayTitle: 'Artists'.i18n,
playable: false,
),
];
}
}

View File

@ -48,10 +48,18 @@ class CachedImage extends StatefulWidget {
class _CachedImageState extends State<CachedImage> {
@override
Widget build(BuildContext context) {
if (widget.circular) return ClipOval(
child: CachedImage(url: widget.url, height: widget.height, width: widget.width, circular: false)
);
if (!widget.url.startsWith('http'))
return Image.asset(
widget.url,
width: widget.width,
height: widget.height,
);
return CachedNetworkImage(
imageUrl: widget.url,
width: widget.width,

View File

@ -6,9 +6,9 @@ import 'package:freezer/api/download.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/search.dart';
import 'package:freezer/translations.i18n.dart';
import '../api/definitions.dart';
import 'player_bar.dart';
import 'cached_image.dart';
import 'tiles.dart';
import 'menu.dart';
@ -134,13 +134,13 @@ class AlbumDetails extends StatelessWidget {
children: <Widget>[
Icon(Icons.favorite, size: 32),
Container(width: 4,),
Text('Library')
Text('Library'.i18n)
],
),
onPressed: () async {
await deezerAPI.addFavoriteAlbum(album.id);
Fluttertoast.showToast(
msg: 'Added to library',
msg: 'Added to library'.i18n,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM
);
@ -152,7 +152,7 @@ class AlbumDetails extends StatelessWidget {
children: <Widget>[
Icon(Icons.file_download, size: 32.0,),
Container(width: 4,),
Text('Download')
Text('Download'.i18n)
],
),
onPressed: () {
@ -168,7 +168,7 @@ class AlbumDetails extends StatelessWidget {
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 4.0),
child: Text('Disk ${cdi + 1}'),
child: Text('Disk'.i18n + ' ${cdi + 1}'),
),
...List.generate(tracks.length, (i) => TrackTile(
tracks[i],
@ -237,7 +237,7 @@ class _MakeAlbumOfflineState extends State<MakeAlbumOffline> {
),
Container(width: 4.0,),
Text(
'Offline',
'Offline'.i18n,
style: TextStyle(fontSize: 16),
)
],
@ -345,25 +345,43 @@ class ArtistDetails extends StatelessWidget {
children: <Widget>[
Icon(Icons.favorite, size: 32),
Container(width: 4,),
Text('Library')
Text('Library'.i18n)
],
),
onPressed: () async {
await deezerAPI.addFavoriteArtist(artist.id);
Fluttertoast.showToast(
msg: 'Added to library',
msg: 'Added to library'.i18n,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM
);
},
),
if ((artist.radio??false))
FlatButton(
child: Row(
children: <Widget>[
Icon(Icons.radio, size: 32),
Container(width: 4,),
Text('Radio'.i18n)
],
),
onPressed: () async {
List<Track> tracks = await deezerAPI.smartRadio(artist.id);
playerHelper.playFromTrackList(tracks, tracks[0].id, QueueSource(
id: artist.id,
text: 'Radio'.i18n + ' ${artist.name}',
source: 'smartradio'
));
},
)
],
),
),
Container(height: 16.0,),
//Top tracks
Text(
'Top Tracks',
'Top Tracks'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
@ -390,12 +408,12 @@ class ArtistDetails extends StatelessWidget {
);
}),
ListTile(
title: Text('Show more tracks'),
title: Text('Show more tracks'.i18n),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => TrackListScreen(artist.topTracks, QueueSource(
id: artist.id,
text: 'Top ${artist.name}',
text: 'Top'.i18n + '${artist.name}',
source: 'topTracks'
)))
);
@ -404,7 +422,7 @@ class ArtistDetails extends StatelessWidget {
Divider(),
//Albums
Text(
'Top Albums',
'Top Albums'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
@ -415,7 +433,7 @@ class ArtistDetails extends StatelessWidget {
//Show discography
if (i == 10 || i == artist.albums.length) {
return ListTile(
title: Text('Show all albums'),
title: Text('Show all albums'.i18n),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => DiscographyScreen(artist: artist,))
@ -552,7 +570,7 @@ class _DiscographyScreenState extends State<DiscographyScreen> {
return Scaffold(
appBar: AppBar(
title: Text('Discography'),
title: Text('Discography'.i18n),
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.album)),
@ -603,6 +621,7 @@ class _DiscographyScreenState extends State<DiscographyScreen> {
enum SortType {
DEFAULT,
REVERSE,
ALPHABETIC,
ARTIST
}
@ -634,6 +653,8 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
case SortType.ARTIST:
tracks.sort((a, b) => a.artists[0].name.compareTo(b.artists[0].name));
return tracks;
case SortType.REVERSE:
return tracks.reversed.toList();
case SortType.DEFAULT:
default:
return tracks;
@ -782,32 +803,20 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
FlatButton(
child: Row(
children: <Widget>[
Icon(Icons.favorite, size: 32),
Container(width: 4,),
Text('Library')
],
),
MakePlaylistOffline(playlist),
IconButton(
icon: Icon(Icons.favorite, size: 32),
onPressed: () async {
await deezerAPI.addFavoriteAlbum(playlist.id);
Fluttertoast.showToast(
msg: 'Added to library',
msg: 'Added to library'.i18n,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM
);
},
),
MakePlaylistOffline(playlist),
FlatButton(
child: Row(
children: <Widget>[
Icon(Icons.file_download, size: 32.0,),
Container(width: 4,),
Text('Download')
],
),
IconButton(
icon: Icon(Icons.file_download, size: 32.0,),
onPressed: () {
downloadManager.addOfflinePlaylist(playlist, private: false);
},
@ -816,17 +825,21 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
child: Icon(Icons.sort, size: 32.0),
onSelected: (SortType s) => setState(() => _sort = s),
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
const PopupMenuItem(
PopupMenuItem(
value: SortType.DEFAULT,
child: Text('Default'),
child: Text('Default'.i18n),
),
const PopupMenuItem(
PopupMenuItem(
value: SortType.REVERSE,
child: Text('Reverse'.i18n),
),
PopupMenuItem(
value: SortType.ALPHABETIC,
child: Text('Alphabetic'),
child: Text('Alphabetic'.i18n),
),
const PopupMenuItem(
PopupMenuItem(
value: SortType.ARTIST,
child: Text('Artist'),
child: Text('Artist'.i18n),
),
],
),
@ -914,7 +927,7 @@ class _MakePlaylistOfflineState extends State<MakePlaylistOffline> {
),
Container(width: 4.0,),
Text(
'Offline',
'Offline'.i18n,
style: TextStyle(fontSize: 16),
)
],

View File

@ -1,5 +1,6 @@
import 'package:filesize/filesize.dart';
import 'package:flutter/material.dart';
import 'package:freezer/translations.i18n.dart';
import 'cached_image.dart';
import '../api/download.dart';
@ -17,9 +18,9 @@ class DownloadTile extends StatelessWidget {
case DownloadState.DOWNLOADING:
return '${filesize(download.received)} / ${filesize(download.total)}';
case DownloadState.POST:
return 'Post processing...';
return 'Post processing...'.i18n;
case DownloadState.DONE:
return 'Done'; //Shouldn't be visible
return 'Done'.i18n; //Shouldn't be visible
}
return '';
}
@ -27,6 +28,7 @@ class DownloadTile extends StatelessWidget {
Widget get progressBar {
switch (download.state) {
case DownloadState.DOWNLOADING:
print(download.track.id);
return LinearProgressIndicator(value: download.received / download.total);
case DownloadState.POST:
return LinearProgressIndicator();
@ -62,15 +64,15 @@ class DownloadTile extends StatelessWidget {
context: context,
builder: (context) {
return AlertDialog(
title: Text('Delete'),
content: Text('Are you sure, you want to delete this download?'),
title: Text('Delete'.i18n),
content: Text('Are you sure you want to delete this download?'.i18n),
actions: [
FlatButton(
child: Text('Cancel'),
child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text('Delete'),
child: Text('Delete'.i18n),
onPressed: () {
downloadManager.removeDownload(download);
if (this.onDelete != null) this.onDelete();
@ -100,7 +102,7 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Downloads'),
title: Text('Downloads'.i18n),
actions: [
IconButton(
icon: Icon(downloadManager.stopped ? Icons.play_arrow : Icons.stop),
@ -129,23 +131,23 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
}),
if (downloadManager.queue.length > 1 || (downloadManager.stopped && downloadManager.queue.length > 0))
ListTile(
title: Text('Clear queue'),
subtitle: Text("This won't delete currently downloading item"),
title: Text('Clear queue'.i18n),
subtitle: Text("This won't delete currently downloading item".i18n),
leading: Icon(Icons.delete),
onTap: () async {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Delete'),
content: Text('Are you sure, you want to delete all queued downloads?'),
title: Text('Delete'.i18n),
content: Text('Are you sure you want to delete all queued downloads?'.i18n),
actions: [
FlatButton(
child: Text('Cancel'),
child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text('Delete'),
child: Text('Delete'.i18n),
onPressed: () async {
await downloadManager.clearQueue();
Navigator.of(context).pop();
@ -181,9 +183,9 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
return DownloadTile(d);
}),
ListTile(
title: Text('Clear downloads history'),
title: Text('Clear downloads history'.i18n),
leading: Icon(Icons.delete),
subtitle: Text('WARNING: This will only clear non-offline (external downloads)'),
subtitle: Text('WARNING: This will only clear non-offline (external downloads)'.i18n),
onTap: () async {
await downloadManager.cleanDownloadHistory();
setState(() {});

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:freezer/translations.i18n.dart';
class ErrorScreen extends StatelessWidget {
@ -18,7 +19,7 @@ class ErrorScreen extends StatelessWidget {
size: 64.0,
),
Container(height: 4.0,),
Text(message ?? 'Please check your connection and try again later...')
Text(message ?? 'Please check your connection and try again later...'.i18n)
],
),
);

View File

@ -4,6 +4,7 @@ import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/menu.dart';
import 'package:freezer/translations.i18n.dart';
import 'tiles.dart';
import 'details_screens.dart';
import '../settings.dart';
@ -175,7 +176,7 @@ class _HomePageScreenState extends State<HomePageScreen> {
if (section.hasMore??false) {
return FlatButton(
child: Text(
'Show more',
'Show more'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.0

View File

@ -4,6 +4,7 @@ import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/spotify.dart';
import 'package:freezer/ui/menu.dart';
import 'package:freezer/translations.i18n.dart';
class ImporterScreen extends StatefulWidget {
@override
@ -34,8 +35,8 @@ class _ImporterScreenState extends State<ImporterScreen> {
setState(() => _data = data);
return;
} catch (e) {
print(e);
} catch (e, st) {
print('$e, $st');
setState(() {
_error = true;
_loading = false;
@ -49,13 +50,13 @@ class _ImporterScreenState extends State<ImporterScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Importer'),
title: Text('Importer'.i18n),
),
body: ListView(
children: <Widget>[
ListTile(
title: Text('Currently supporting only Spotify, with 100 tracks limit'),
subtitle: Text('Due to API limitations'),
title: Text('Currently supporting only Spotify, with 100 tracks limit'.i18n),
subtitle: Text('Due to API limitations'.i18n),
leading: Icon(
Icons.warning,
color: Colors.deepOrangeAccent,
@ -64,7 +65,7 @@ class _ImporterScreenState extends State<ImporterScreen> {
Divider(),
Container(height: 16.0,),
Text(
'Enter your playlist link below',
'Enter your playlist link below'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.0
@ -104,7 +105,7 @@ class _ImporterScreenState extends State<ImporterScreen> {
),
if (_error)
ListTile(
title: Text('Error loading URL!'),
title: Text('Error loading URL!'.i18n),
leading: Icon(Icons.error, color: Colors.red,),
),
if (_data != null)
@ -133,13 +134,14 @@ class _ImporterWidgetState extends State<ImporterWidget> {
ListTile(
title: Text(widget.playlist.name),
subtitle: Text(widget.playlist.description),
leading: Image.network(widget.playlist.image),
//Default image
leading: Image.network(widget.playlist.image??'http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg'),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
RaisedButton(
child: Text('Convert'),
child: Text('Convert'.i18n),
color: Theme.of(context).primaryColor,
onPressed: () {
spotify.convertPlaylist(widget.playlist);
@ -149,7 +151,7 @@ class _ImporterWidgetState extends State<ImporterWidget> {
},
),
RaisedButton(
child: Text('Download only'),
child: Text('Download only'.i18n),
color: Theme.of(context).primaryColor,
onPressed: () {
spotify.convertPlaylist(widget.playlist, downloadOnly: true);
@ -197,7 +199,7 @@ class CurrentlyImportingScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Importing...'),),
appBar: AppBar(title: Text('Importing...'.i18n),),
body: StreamBuilder(
stream: spotify.importingStream.stream,
builder: (context, snapshot) {
@ -254,7 +256,7 @@ class CurrentlyImportingScreen extends StatelessWidget {
),
if (snapshot.data != null)
FlatButton(
child: Text('Playlist menu'),
child: Text('Playlist menu'.i18n),
onPressed: () async {
Playlist p = await deezerAPI.playlist(snapshot.data);
p.library = true;

View File

@ -10,6 +10,7 @@ import 'package:freezer/ui/downloads_screen.dart';
import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/importer_screen.dart';
import 'package:freezer/ui/tiles.dart';
import 'package:freezer/translations.i18n.dart';
import 'menu.dart';
import 'settings_screen.dart';
@ -24,7 +25,7 @@ class LibraryAppBar extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
return AppBar(
title: Text('Library'),
title: Text('Library'.i18n),
actions: <Widget>[
IconButton(
icon: Icon(Icons.file_download),
@ -58,9 +59,9 @@ class LibraryScreen extends StatelessWidget {
Container(height: 4.0,),
if (downloadManager.stopped && downloadManager.queue.length > 0)
ListTile(
title: Text('Downloads'),
title: Text('Downloads'.i18n),
leading: Icon(Icons.file_download),
subtitle: Text('Downloading is currently stopped, click here to resume.'),
subtitle: Text('Downloading is currently stopped, click here to resume.'.i18n),
onTap: () {
downloadManager.start();
Navigator.of(context).push(MaterialPageRoute(
@ -73,7 +74,7 @@ class LibraryScreen extends StatelessWidget {
Divider(),
ListTile(
title: Text('Tracks'),
title: Text('Tracks'.i18n),
leading: Icon(Icons.audiotrack),
onTap: () {
Navigator.of(context).push(
@ -82,7 +83,7 @@ class LibraryScreen extends StatelessWidget {
},
),
ListTile(
title: Text('Albums'),
title: Text('Albums'.i18n),
leading: Icon(Icons.album),
onTap: () {
Navigator.of(context).push(
@ -91,7 +92,7 @@ class LibraryScreen extends StatelessWidget {
},
),
ListTile(
title: Text('Artists'),
title: Text('Artists'.i18n),
leading: Icon(Icons.recent_actors),
onTap: () {
Navigator.of(context).push(
@ -100,7 +101,7 @@ class LibraryScreen extends StatelessWidget {
},
),
ListTile(
title: Text('Playlists'),
title: Text('Playlists'.i18n),
leading: Icon(Icons.playlist_play),
onTap: () {
Navigator.of(context).push(
@ -110,9 +111,9 @@ class LibraryScreen extends StatelessWidget {
),
Divider(),
ListTile(
title: Text('Import'),
title: Text('Import'.i18n),
leading: Icon(Icons.import_export),
subtitle: Text('Import playlists from Spotify'),
subtitle: Text('Import playlists from Spotify'.i18n),
onTap: () {
if (spotify.doneImporting != null) {
Navigator.of(context).push(
@ -128,7 +129,7 @@ class LibraryScreen extends StatelessWidget {
},
),
ExpansionTile(
title: Text('Statistics'),
title: Text('Statistics'.i18n),
leading: Icon(Icons.insert_chart),
children: <Widget>[
FutureBuilder(
@ -148,27 +149,27 @@ class LibraryScreen extends StatelessWidget {
return Column(
children: <Widget>[
ListTile(
title: Text('Offline tracks'),
title: Text('Offline tracks'.i18n),
leading: Icon(Icons.audiotrack),
trailing: Text(data[0]),
),
ListTile(
title: Text('Offline albums'),
title: Text('Offline albums'.i18n),
leading: Icon(Icons.album),
trailing: Text(data[1]),
),
ListTile(
title: Text('Offline playlists'),
title: Text('Offline playlists'.i18n),
leading: Icon(Icons.playlist_add),
trailing: Text(data[2]),
),
ListTile(
title: Text('Offline size'),
title: Text('Offline size'.i18n),
leading: Icon(Icons.sd_card),
trailing: Text(data[3]),
),
ListTile(
title: Text('Free space'),
title: Text('Free space'.i18n),
leading: Icon(Icons.disc_full),
trailing: Text(data[4]),
),
@ -254,7 +255,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Tracks'),),
appBar: AppBar(title: Text('Tracks'.i18n),),
body: ListView(
children: <Widget>[
Card(
@ -263,7 +264,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
children: <Widget>[
Container(height: 8.0,),
Text(
'Loved tracks',
'Loved tracks'.i18n,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 24
@ -279,7 +280,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
children: <Widget>[
Icon(Icons.file_download, size: 32.0,),
Container(width: 4,),
Text('Download')
Text('Download'.i18n)
],
),
onPressed: () {
@ -299,7 +300,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
onTap: () {
playerHelper.playFromTrackList(tracks, t.id, QueueSource(
id: deezerAPI.favoritesPlaylistId,
text: 'Favorites',
text: 'Favorites'.i18n,
source: 'playlist'
));
},
@ -328,7 +329,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
),
Divider(),
Text(
'All offline tracks',
'All offline tracks'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
@ -343,7 +344,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
onTap: () {
playerHelper.playFromTrackList(allTracks, t.id, QueueSource(
id: 'allTracks',
text: 'All offline tracks',
text: 'All offline tracks'.i18n,
source: 'offline'
));
},
@ -386,7 +387,7 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Albums'),),
appBar: AppBar(title: Text('Albums'.i18n),),
body: ListView(
children: <Widget>[
Container(height: 8.0,),
@ -427,7 +428,7 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
children: <Widget>[
Divider(),
Text(
'Offline albums',
'Offline albums'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
@ -473,7 +474,7 @@ class _LibraryArtistsState extends State<LibraryArtists> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Artists'),),
appBar: AppBar(title: Text('Artists'.i18n),),
body: FutureBuilder(
future: deezerAPI.getArtists(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
@ -533,20 +534,30 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
super.initState();
}
Playlist get favoritesPlaylist => Playlist(
id: deezerAPI.favoritesPlaylistId,
title: 'Favorites'.i18n,
user: User(name: deezerAPI.userName),
image: ImageDetails(thumbUrl: 'assets/favorites_thumb.jpg'),
tracks: [],
trackCount: 1,
duration: Duration(seconds: 0)
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Playlists'),),
appBar: AppBar(title: Text('Playlists'.i18n),),
body: ListView(
children: <Widget>[
ListTile(
title: Text('Create new playlist'),
title: Text('Create new playlist'.i18n),
leading: Icon(Icons.playlist_add),
onTap: () {
if (settings.offlineMode) {
Fluttertoast.showToast(
msg: 'Cannot create playlists in offline mode',
msg: 'Cannot create playlists in offline mode'.i18n,
gravity: ToastGravity.BOTTOM
);
return;
@ -565,6 +576,20 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
],
),
//Favorites playlist
PlaylistTile(
favoritesPlaylist,
onTap: () async {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => PlaylistDetails(favoritesPlaylist)
));
},
onHold: () {
MenuSheet m = MenuSheet(context);
m.defaultPlaylistMenu(favoritesPlaylist);
},
),
if (_playlists != null)
...List.generate(_playlists.length, (int i) {
Playlist p = _playlists[i];
@ -593,7 +618,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
children: <Widget>[
Divider(),
Text(
'Offline playlists',
'Offline playlists'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24.0,

View File

@ -1,10 +1,9 @@
import 'package:audio_service/audio_service.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/main.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:freezer/translations.i18n.dart';
import '../settings.dart';
import '../api/definitions.dart';
@ -62,11 +61,11 @@ class _LoginWidgetState extends State<LoginWidget> {
context: context,
builder: (context) {
return AlertDialog(
title: Text('Error'),
content: Text('Error logging in! Please check your token and internet connection and try again.'),
title: Text('Error'.i18n),
content: Text('Error logging in! Please check your token and internet connection and try again.'.i18n),
actions: <Widget>[
FlatButton(
child: Text('Dismiss'),
child: Text('Dismiss'.i18n),
onPressed: () {
Navigator.of(context).pop();
},
@ -117,7 +116,7 @@ class _LoginWidgetState extends State<LoginWidget> {
children: <Widget>[
Container(height: 16.0,),
Text(
'Welcome to',
'Welcome to'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.0
@ -126,7 +125,7 @@ class _LoginWidgetState extends State<LoginWidget> {
FreezerTitle(),
Container(height: 8.0,),
Text(
"Please login using your Deezer account.",
"Please login using your Deezer account.".i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.0
@ -136,7 +135,7 @@ class _LoginWidgetState extends State<LoginWidget> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 32.0),
child: OutlineButton(
child: Text('Login using browser'),
child: Text('Login using browser'.i18n),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => LoginBrowser(_update))
@ -147,18 +146,18 @@ class _LoginWidgetState extends State<LoginWidget> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 32.0),
child: OutlineButton(
child: Text('Login using token'),
child: Text('Login using token'.i18n),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Enter ARL'),
title: Text('Enter ARL'.i18n),
content: Container(
child: TextField(
onChanged: (String s) => _arl = s,
decoration: InputDecoration(
labelText: 'Token (ARL)'
labelText: 'Token (ARL)'.i18n
),
),
),
@ -166,7 +165,7 @@ class _LoginWidgetState extends State<LoginWidget> {
FlatButton(
child: Text('Save'),
onPressed: () {
settings.arl = _arl;
settings.arl = _arl.trim();
Navigator.of(context).pop();
_update();
},
@ -180,7 +179,7 @@ class _LoginWidgetState extends State<LoginWidget> {
),
Container(height: 16.0,),
Text(
"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.".i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.0
@ -199,7 +198,7 @@ class _LoginWidgetState extends State<LoginWidget> {
Divider(),
Container(height: 8.0,),
Text(
"By using this app, you don't agree with the Deezer ToS",
"By using this app, you don't agree with the Deezer ToS".i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.0

View File

@ -6,9 +6,9 @@ import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/download.dart';
import 'package:freezer/ui/details_screens.dart';
import 'package:freezer/ui/error.dart';
import 'package:freezer/translations.i18n.dart';
import '../api/definitions.dart';
import '../api/player.dart';
import 'cached_image.dart';
class MenuSheet {
@ -137,7 +137,7 @@ class MenuSheet {
//===================
Widget addToQueueNext(Track t) => ListTile(
title: Text('Play next'),
title: Text('Play next'.i18n),
leading: Icon(Icons.playlist_play),
onTap: () async {
//-1 = next
@ -146,7 +146,7 @@ class MenuSheet {
});
Widget addToQueue(Track t) => ListTile(
title: Text('Add to queue'),
title: Text('Add to queue'.i18n),
leading: Icon(Icons.playlist_add),
onTap: () async {
await AudioService.addQueueItem(t.toMediaItem());
@ -155,7 +155,7 @@ class MenuSheet {
);
Widget addTrackFavorite(Track t) => ListTile(
title: Text('Add track to favorites'),
title: Text('Add track to favorites'.i18n),
leading: Icon(Icons.favorite),
onTap: () async {
await deezerAPI.addFavoriteTrack(t.id);
@ -165,7 +165,7 @@ class MenuSheet {
downloadManager.addOfflinePlaylist(p);
}
Fluttertoast.showToast(
msg: 'Added to library!',
msg: 'Added to library'.i18n,
gravity: ToastGravity.BOTTOM,
toastLength: Toast.LENGTH_SHORT
);
@ -174,7 +174,7 @@ class MenuSheet {
);
Widget downloadTrack(Track t) => ListTile(
title: Text('Download'),
title: Text('Download'.i18n),
leading: Icon(Icons.file_download),
onTap: () async {
await downloadManager.addOfflineTrack(t, private: false);
@ -183,7 +183,7 @@ class MenuSheet {
);
Widget addToPlaylist(Track t) => ListTile(
title: Text('Add to playlist'),
title: Text('Add to playlist'.i18n),
leading: Icon(Icons.playlist_add),
onTap: () async {
@ -194,7 +194,7 @@ class MenuSheet {
context: context,
builder: (context) {
return AlertDialog(
title: Text('Select playlist'),
title: Text('Select playlist'.i18n),
content: FutureBuilder(
future: deezerAPI.getPlaylists(),
builder: (context, snapshot) {
@ -224,7 +224,7 @@ class MenuSheet {
},
)),
ListTile(
title: Text('Create new playlist'),
title: Text('Create new playlist'.i18n),
leading: Icon(Icons.add),
onTap: () {
Navigator.of(context).pop();
@ -250,7 +250,7 @@ class MenuSheet {
downloadManager.addOfflinePlaylist(p);
}
Fluttertoast.showToast(
msg: "Track added to ${p.title}",
msg: "Track added to".i18n + " ${p.title}",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
@ -261,12 +261,12 @@ class MenuSheet {
);
Widget removeFromPlaylist(Track t, Playlist p) => ListTile(
title: Text('Remove from playlist'),
title: Text('Remove from playlist'.i18n),
leading: Icon(Icons.delete),
onTap: () async {
await deezerAPI.removeFromPlaylist(t.id, p.id);
Fluttertoast.showToast(
msg: 'Track removed from ${p.title}',
msg: 'Track removed from'.i18n + ' ${p.title}',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
@ -275,7 +275,7 @@ class MenuSheet {
);
Widget removeFavoriteTrack(Track t, {onUpdate}) => ListTile(
title: Text('Remove favorite'),
title: Text('Remove favorite'.i18n),
leading: Icon(Icons.delete),
onTap: () async {
await deezerAPI.removeFavorite(t.id);
@ -285,7 +285,7 @@ class MenuSheet {
await downloadManager.addOfflinePlaylist(p);
}
Fluttertoast.showToast(
msg: 'Track removed from library',
msg: 'Track removed from library'.i18n,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM
);
@ -297,7 +297,7 @@ class MenuSheet {
//Redirect to artist page (ie from track)
Widget showArtist(Artist a) => ListTile(
title: Text(
'Go to ${a.name}',
'Go to'.i18n + ' ${a.name}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@ -312,7 +312,7 @@ class MenuSheet {
Widget showAlbum(Album a) => ListTile(
title: Text(
'Go to ${a.title}',
'Go to'.i18n + ' ${a.title}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@ -345,7 +345,7 @@ class MenuSheet {
//===================
Widget downloadAlbum(Album a) => ListTile(
title: Text('Download'),
title: Text('Download'.i18n),
leading: Icon(Icons.file_download),
onTap: () async {
await downloadManager.addOfflineAlbum(a, private: false);
@ -354,7 +354,7 @@ class MenuSheet {
);
Widget offlineAlbum(Album a) => ListTile(
title: Text('Make offline'),
title: Text('Make offline'.i18n),
leading: Icon(Icons.offline_pin),
onTap: () async {
await deezerAPI.addFavoriteAlbum(a.id);
@ -364,12 +364,12 @@ class MenuSheet {
);
Widget libraryAlbum(Album a) => ListTile(
title: Text('Add to library'),
title: Text('Add to library'.i18n),
leading: Icon(Icons.library_music),
onTap: () async {
await deezerAPI.addFavoriteAlbum(a.id);
Fluttertoast.showToast(
msg: 'Added to library',
msg: 'Added to library'.i18n,
gravity: ToastGravity.BOTTOM
);
_close();
@ -378,13 +378,13 @@ class MenuSheet {
//Remove album from favorites
Widget removeAlbum(Album a, {Function onRemove}) => ListTile(
title: Text('Remove album'),
title: Text('Remove album'.i18n),
leading: Icon(Icons.delete),
onTap: () async {
await deezerAPI.removeAlbum(a.id);
await downloadManager.removeOfflineAlbum(a.id);
Fluttertoast.showToast(
msg: 'Album removed',
msg: 'Album removed'.i18n,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
@ -409,12 +409,12 @@ class MenuSheet {
//===================
Widget removeArtist(Artist a, {Function onRemove}) => ListTile(
title: Text('Remove from favorites'),
title: Text('Remove from favorites'.i18n),
leading: Icon(Icons.delete),
onTap: () async {
await deezerAPI.removeArtist(a.id);
Fluttertoast.showToast(
msg: 'Artist removed from library',
msg: 'Artist removed from library'.i18n,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM
);
@ -424,12 +424,12 @@ class MenuSheet {
);
Widget favoriteArtist(Artist a) => ListTile(
title: Text('Add to favorites'),
title: Text('Add to favorites'.i18n),
leading: Icon(Icons.favorite),
onTap: () async {
await deezerAPI.addFavoriteArtist(a.id);
Fluttertoast.showToast(
msg: 'Added to library',
msg: 'Added to library'.i18n,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM
);
@ -455,7 +455,7 @@ class MenuSheet {
//===================
Widget removePlaylistLibrary(Playlist p, {Function onRemove}) => ListTile(
title: Text('Remove from library'),
title: Text('Remove from library'.i18n),
leading: Icon(Icons.delete),
onTap: () async {
if (p.user.id.trim() == deezerAPI.userId) {
@ -472,12 +472,12 @@ class MenuSheet {
);
Widget addPlaylistLibrary(Playlist p) => ListTile(
title: Text('Add playlist to library'),
title: Text('Add playlist to library'.i18n),
leading: Icon(Icons.favorite),
onTap: () async {
await deezerAPI.addPlaylist(p.id);
Fluttertoast.showToast(
msg: 'Added playlist to library',
msg: 'Added playlist to library'.i18n,
gravity: ToastGravity.BOTTOM
);
_close();
@ -485,7 +485,7 @@ class MenuSheet {
);
Widget addPlaylistOffline(Playlist p) => ListTile(
title: Text('Make playlist offline'),
title: Text('Make playlist offline'.i18n),
leading: Icon(Icons.offline_pin),
onTap: () async {
//Add to library
@ -496,7 +496,7 @@ class MenuSheet {
);
Widget downloadPlaylist(Playlist p) => ListTile(
title: Text('Download playlist'),
title: Text('Download playlist'.i18n),
leading: Icon(Icons.file_download),
onTap: () async {
downloadManager.addOfflinePlaylist(p, private: false);
@ -542,20 +542,20 @@ class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Create playlist'),
title: Text('Create playlist'.i18n),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextField(
decoration: InputDecoration(
labelText: 'Title'
labelText: 'Title'.i18n
),
onChanged: (String s) => _title = s,
),
TextField(
onChanged: (String s) => _description = s,
decoration: InputDecoration(
labelText: 'Description'
labelText: 'Description'.i18n
),
),
Container(height: 4.0,),
@ -567,11 +567,11 @@ class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
items: [
DropdownMenuItem<int>(
value: 1,
child: Text('Private'),
child: Text('Private'.i18n),
),
DropdownMenuItem<int>(
value: 2,
child: Text('Collaborative'),
child: Text('Collaborative'.i18n),
),
],
),
@ -579,11 +579,11 @@ class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
),
actions: <Widget>[
FlatButton(
child: Text('Cancel'),
child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text('Create'),
child: Text('Create'.i18n),
onPressed: () async {
List<String> tracks = [];
if (widget.tracks != null) {
@ -596,7 +596,7 @@ class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
trackIds: tracks
);
Fluttertoast.showToast(
msg: 'Playlist created!',
msg: 'Playlist created!'.i18n,
gravity: ToastGravity.BOTTOM
);
Navigator.of(context).pop();

View File

@ -137,7 +137,7 @@ class PlayPauseButton extends StatelessWidget {
}
//Paused
if ((!AudioService.playbackState.playing &&
if ((!(AudioService.playbackState?.playing??false) &&
AudioService.playbackState.processingState == AudioProcessingState.ready) ||
//None state (stopped)
AudioService.playbackState.processingState == AudioProcessingState.none) {

View File

@ -1,12 +1,9 @@
import 'dart:ui';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:audio_service/audio_service.dart';
import 'package:flutter_screenutil/screenutil.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/settings.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:freezer/ui/menu.dart';
import 'package:freezer/ui/settings_screen.dart';
import 'package:freezer/ui/tiles.dart';
@ -18,6 +15,9 @@ import 'cached_image.dart';
import '../api/definitions.dart';
import 'player_bar.dart';
import 'dart:ui';
import 'dart:async';
class PlayerScreen extends StatefulWidget {
@override
_PlayerScreenState createState() => _PlayerScreenState();
@ -527,9 +527,9 @@ class PlayerScreenTopRow extends StatelessWidget {
),
),
Container(
width: this.textWidth??ScreenUtil().setWidth(600),
width: this.textWidth??ScreenUtil().setWidth(550),
child: Text(
(short??false)?playerHelper.queueSource.text:'Playing from: ' + playerHelper.queueSource.text,
(short??false)?playerHelper.queueSource.text:'Playing from:'.i18n + ' ' + playerHelper.queueSource.text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.left,
@ -728,7 +728,7 @@ class _QueueScreenState extends State<QueueScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Queue'),
title: Text('Queue'.i18n),
actions: <Widget>[
IconButton(
icon: Icon(

View File

@ -4,6 +4,7 @@ import 'package:freezer/api/download.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/ui/details_screens.dart';
import 'package:freezer/ui/menu.dart';
import 'package:freezer/translations.i18n.dart';
import 'tiles.dart';
import '../api/deezer.dart';
@ -20,6 +21,7 @@ class _SearchScreenState extends State<SearchScreen> {
String _query;
bool _offline = false;
TextEditingController _controller = new TextEditingController();
List _suggestions = [];
void _submit(BuildContext context, {String query}) {
if (query != null) _query = query;
@ -41,10 +43,21 @@ class _SearchScreenState extends State<SearchScreen> {
super.initState();
}
//Load search suggestions
Future<List<String>> _loadSuggestions() async {
if (_query == null || _query.length < 2) return null;
String q = _query;
await Future.delayed(Duration(milliseconds: 300));
if (q != _query) return null;
//Load
List sugg = await deezerAPI.searchSuggestions(_query);
setState(() => _suggestions = sugg);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Search'),),
appBar: AppBar(title: Text('Search'.i18n),),
body: ListView(
children: <Widget>[
Container(height: 16.0),
@ -57,9 +70,12 @@ class _SearchScreenState extends State<SearchScreen> {
alignment: Alignment(1.0, 1.0),
children: [
TextField(
onChanged: (String s) => _query = s,
onChanged: (String s) {
setState(() => _query = s);
_loadSuggestions();
},
decoration: InputDecoration(
labelText: 'Search'
labelText: 'Search'.i18n
),
controller: _controller,
onSubmitted: (String s) => _submit(context, query: s),
@ -73,7 +89,6 @@ class _SearchScreenState extends State<SearchScreen> {
],
)
),
Padding(
padding: EdgeInsets.fromLTRB(0, 8, 0, 0),
child: IconButton(
@ -85,14 +100,23 @@ class _SearchScreenState extends State<SearchScreen> {
),
),
ListTile(
title: Text('Offline search'),
title: Text('Offline search'.i18n),
leading: Switch(
value: _offline,
onChanged: (v) {
setState(() => _offline = !_offline);
},
),
)
),
Divider(),
...List.generate((_suggestions??[]).length, (i) => ListTile(
title: Text(_suggestions[i]),
leading: Icon(Icons.search),
onTap: () {
setState(() => _query = _suggestions[i]);
_submit(context);
},
))
],
),
);
@ -119,7 +143,7 @@ class SearchResultsScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Search Results'),
title: Text('Search Results'.i18n),
),
body: FutureBuilder(
future: _search(),
@ -139,7 +163,7 @@ class SearchResultsScreen extends StatelessWidget {
Icons.warning,
size: 64,
),
Text('No results!')
Text('No results!'.i18n)
],
),
);
@ -149,7 +173,7 @@ class SearchResultsScreen extends StatelessWidget {
if (results.tracks != null && results.tracks.length != 0) {
tracks = [
Text(
'Tracks',
'Tracks'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26.0,
@ -163,7 +187,7 @@ class SearchResultsScreen extends StatelessWidget {
t,
onTap: () {
playerHelper.playFromTrackList(results.tracks, t.id, QueueSource(
text: 'Search',
text: 'Search'.i18n,
id: query,
source: 'search'
));
@ -175,13 +199,13 @@ class SearchResultsScreen extends StatelessWidget {
);
}),
ListTile(
title: Text('Show all tracks'),
title: Text('Show all tracks'.i18n),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => TrackListScreen(results.tracks, QueueSource(
id: query,
source: 'search',
text: 'Search'
text: 'Search'.i18n
)))
);
},
@ -194,7 +218,7 @@ class SearchResultsScreen extends StatelessWidget {
if (results.albums != null && results.albums.length != 0) {
albums = [
Text(
'Albums',
'Albums'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26.0,
@ -218,7 +242,7 @@ class SearchResultsScreen extends StatelessWidget {
);
}),
ListTile(
title: Text('Show all albums'),
title: Text('Show all albums'.i18n),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => AlbumListScreen(results.albums))
@ -233,7 +257,7 @@ class SearchResultsScreen extends StatelessWidget {
if (results.artists != null && results.artists.length != 0) {
artists = [
Text(
'Artists',
'Artists'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26.0,
@ -269,7 +293,7 @@ class SearchResultsScreen extends StatelessWidget {
if (results.playlists != null && results.playlists.length != 0) {
playlists = [
Text(
'Playlists',
'Playlists'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26.0,
@ -293,7 +317,7 @@ class SearchResultsScreen extends StatelessWidget {
);
}),
ListTile(
title: Text('Show all playlists'),
title: Text('Show all playlists'.i18n),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => SearchResultPlaylists(results.playlists))
@ -332,7 +356,7 @@ class TrackListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Tracks'),),
appBar: AppBar(title: Text('Tracks'.i18n),),
body: ListView.builder(
itemCount: tracks.length,
itemBuilder: (BuildContext context, int i) {
@ -362,7 +386,7 @@ class AlbumListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Albums'),),
appBar: AppBar(title: Text('Albums'.i18n),),
body: ListView.builder(
itemCount: albums.length,
itemBuilder: (context, i) {
@ -393,7 +417,7 @@ class SearchResultPlaylists extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Playlists'),),
appBar: AppBar(title: Text('Playlists'.i18n),),
body: ListView.builder(
itemCount: playlists.length,
itemBuilder: (context, i) {

View File

@ -9,11 +9,13 @@ import 'package:flutter_material_color_picker/flutter_material_color_picker.dart
import 'package:fluttertoast/fluttertoast.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/ui/error.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';
import 'package:path_provider_ex/path_provider_ex.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:clipboard/clipboard.dart';
import '../settings.dart';
@ -44,18 +46,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Settings'),),
appBar: AppBar(title: Text('Settings'.i18n),),
body: ListView(
children: <Widget>[
ListTile(
title: Text('General'),
title: Text('General'.i18n),
leading: Icon(Icons.settings),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => GeneralSettings()
)),
),
ListTile(
title: Text('Appearance'),
title: Text('Appearance'.i18n),
leading: Icon(Icons.color_lens),
onTap: () => Navigator.push(
context,
@ -63,7 +65,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
),
ListTile(
title: Text('Quality'),
title: Text('Quality'.i18n),
leading: Icon(Icons.high_quality),
onTap: () => Navigator.push(
context,
@ -71,12 +73,54 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
),
ListTile(
title: Text('Deezer'),
title: Text('Deezer'.i18n),
leading: Icon(Icons.equalizer),
onTap: () => Navigator.push(context, MaterialPageRoute(
builder: (context) => DeezerSettings()
)),
),
//Language select
ListTile(
title: Text('Language'.i18n),
leading: Icon(Icons.language),
onTap: () {
showDialog(
context: context,
builder: (context) => LanguagePickerDialog(
titlePadding: EdgeInsets.all(8.0),
title: Text('Select language'.i18n),
isSearchable: false,
languagesList: supportedLocales.map<Map<String, String>>((l) {
Map _lang = defaultLanguagesList.firstWhere((lang) => lang['isoCode'] == l.languageCode);
_lang['name'] = _lang['name'] + ' (${l.toString()})';
return _lang;
}).toList(),
onValuePicked: (Language l) async {
setState(() {
Locale locale = supportedLocales.firstWhere((_l) => _l.languageCode == l.isoCode);
settings.language = locale.toString();
});
await settings.save();
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Language'.i18n),
content: Text('Language changed, please restart Freezer to apply!'.i18n),
actions: [
FlatButton(
child: Text('OK'),
onPressed: () => Navigator.of(context).pop(),
)
],
);
}
);
},
)
);
},
),
Divider(),
Text(
_about,
@ -97,22 +141,22 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Appearance'),),
appBar: AppBar(title: Text('Appearance'.i18n),),
body: ListView(
children: <Widget>[
ListTile(
title: Text('Theme'),
subtitle: Text('Currently: ${settings.theme.toString().split('.').last}'),
title: Text('Theme'.i18n),
subtitle: Text('Currently'.i18n + ': ${settings.theme.toString().split('.').last}'),
leading: Icon(Icons.color_lens),
onTap: () {
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: Text('Select theme'),
title: Text('Select theme'.i18n),
children: <Widget>[
SimpleDialogOption(
child: Text('Light (default)'),
child: Text('Light (default)'.i18n),
onPressed: () {
setState(() => settings.theme = Themes.Light);
settings.save();
@ -121,7 +165,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
},
),
SimpleDialogOption(
child: Text('Dark'),
child: Text('Dark'.i18n),
onPressed: () {
setState(() => settings.theme = Themes.Dark);
settings.save();
@ -130,7 +174,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
},
),
SimpleDialogOption(
child: Text('Black (AMOLED)'),
child: Text('Black (AMOLED)'.i18n),
onPressed: () {
setState(() => settings.theme = Themes.Black);
settings.save();
@ -139,7 +183,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
},
),
SimpleDialogOption(
child: Text('Deezer (Dark)'),
child: Text('Deezer (Dark)'.i18n),
onPressed: () {
setState(() => settings.theme = Themes.Deezer);
settings.save();
@ -154,10 +198,10 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
},
),
ListTile(
title: Text('Primary color'),
title: Text('Primary color'.i18n),
leading: Icon(Icons.format_paint),
subtitle: Text(
'Selected color',
'Selected color'.i18n,
style: TextStyle(
color: settings.primaryColor
),
@ -167,7 +211,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
context: context,
builder: (context) {
return AlertDialog(
title: Text('Primary color'),
title: Text('Primary color'.i18n),
content: Container(
height: 200,
child: MaterialColorPicker(
@ -189,8 +233,8 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
},
),
ListTile(
title: Text('Use album art primary color'),
subtitle: Text('Warning: might be buggy'),
title: Text('Use album art primary color'.i18n),
subtitle: Text('Warning: might be buggy'.i18n),
leading: Switch(
value: settings.useArtColor,
onChanged: (v) => setState(() => settings.updateUseArtColor(v)),
@ -212,29 +256,29 @@ class _QualitySettingsState extends State<QualitySettings> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Quality'),),
appBar: AppBar(title: Text('Quality'.i18n),),
body: ListView(
children: <Widget>[
ListTile(
title: Text('Mobile streaming'),
title: Text('Mobile streaming'.i18n),
leading: Icon(Icons.network_cell),
),
QualityPicker('mobile'),
Divider(),
ListTile(
title: Text('Wifi streaming'),
title: Text('Wifi streaming'.i18n),
leading: Icon(Icons.network_wifi),
),
QualityPicker('wifi'),
Divider(),
ListTile(
title: Text('Offline'),
title: Text('Offline'.i18n),
leading: Icon(Icons.offline_pin),
),
QualityPicker('offline'),
Divider(),
ListTile(
title: Text('External downloads'),
title: Text('External downloads'.i18n),
leading: Icon(Icons.file_download),
),
QualityPicker('download'),
@ -349,12 +393,12 @@ class _DeezerSettingsState extends State<DeezerSettings> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Deezer'),),
appBar: AppBar(title: Text('Deezer'.i18n),),
body: ListView(
children: <Widget>[
ListTile(
title: Text('Content language'),
subtitle: Text('Not app language, used in headers. Now: ${settings.deezerLanguage}'),
title: Text('Content language'.i18n),
subtitle: Text('Not app language, used in headers. Now'.i18n + ': ${settings.deezerLanguage}'),
leading: Icon(Icons.language),
onTap: () {
showDialog(
@ -362,7 +406,7 @@ class _DeezerSettingsState extends State<DeezerSettings> {
builder: (context) => LanguagePickerDialog(
titlePadding: EdgeInsets.all(8.0),
isSearchable: true,
title: Text('Select language'),
title: Text('Select language'.i18n),
onValuePicked: (Language language) {
setState(() => settings.deezerLanguage = language.isoCode);
settings.save();
@ -372,8 +416,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
},
),
ListTile(
title: Text('Content country'),
subtitle: Text('Country used in headers. Now: ${settings.deezerCountry}'),
title: Text('Content country'.i18n),
subtitle: Text('Country used in headers. Now'.i18n + ': ${settings.deezerCountry}'),
leading: Icon(Icons.vpn_lock),
onTap: () {
showDialog(
@ -390,8 +434,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
},
),
ListTile(
title: Text('Log tracks'),
subtitle: Text('Send track listen logs to Deezer, enable it for features like Flow to work properly'),
title: Text('Log tracks'.i18n),
subtitle: Text('Send track listen logs to Deezer, enable it for features like Flow to work properly'.i18n),
leading: Checkbox(
value: settings.logListen,
onChanged: (bool v) {
@ -415,12 +459,12 @@ class _GeneralSettingsState extends State<GeneralSettings> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('General'),),
appBar: AppBar(title: Text('General'.i18n),),
body: ListView(
children: <Widget>[
ListTile(
title: Text('Offline mode'),
subtitle: Text('Will be overwritten on start.'),
title: Text('Offline mode'.i18n),
subtitle: Text('Will be overwritten on start.'.i18n),
leading: Switch(
value: settings.offlineMode,
onChanged: (bool v) {
@ -436,7 +480,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
setState(() => settings.offlineMode = false);
} else {
Fluttertoast.showToast(
msg: 'Error logging in, check your internet connections.',
msg: 'Error logging in, check your internet connections.'.i18n,
gravity: ToastGravity.BOTTOM,
toastLength: Toast.LENGTH_SHORT
);
@ -444,7 +488,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
Navigator.of(context).pop();
});
return AlertDialog(
title: Text('Logging in...'),
title: Text('Logging in...'.i18n),
content: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
@ -459,7 +503,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
),
ListTile(
title: Text('Download path'),
title: Text('Download path'.i18n),
leading: Icon(Icons.folder),
subtitle: Text(settings.downloadPath),
onTap: () async {
@ -474,8 +518,8 @@ class _GeneralSettingsState extends State<GeneralSettings> {
},
),
ListTile(
title: Text('Downloads naming'),
subtitle: Text('Currently: ${settings.downloadFilename}'),
title: Text('Downloads naming'.i18n),
subtitle: Text('Currently'.i18n + ': ${settings.downloadFilename}'),
leading: Icon(Icons.text_format),
onTap: () {
showDialog(
@ -485,19 +529,21 @@ class _GeneralSettingsState extends State<GeneralSettings> {
TextEditingController _controller = TextEditingController();
String filename = settings.downloadFilename;
_controller.value = _controller.value.copyWith(text: filename);
String _new = _controller.value.text;
//Dialog with filename format
return AlertDialog(
title: Text('Downloaded tracks filename'),
title: Text('Downloaded tracks filename'.i18n),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _controller,
onChanged: (String s) => _new = s,
),
Container(height: 8.0),
Text(
'Valid variables are: %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%',
'Valid variables are'.i18n + ': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%',
style: TextStyle(
fontSize: 12.0,
),
@ -506,29 +552,30 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
actions: [
FlatButton(
child: Text('Cancel'),
child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text('Reset'),
child: Text('Reset'.i18n),
onPressed: () {
_controller.value = _controller.value.copyWith(
text: '%artists% - %title%'
);
_new = '%artists% - %title%';
},
),
FlatButton(
child: Text('Clear'),
child: Text('Clear'.i18n),
onPressed: () => _controller.clear(),
),
FlatButton(
child: Text('Save'),
onPressed: () {
child: Text('Save'.i18n),
onPressed: () async {
setState(() {
settings.downloadFilename = _controller.text;
settings.save();
Navigator.of(context).pop();
settings.downloadFilename = _new;
});
await settings.save();
Navigator.of(context).pop();
},
)
],
@ -538,7 +585,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
},
),
ListTile(
title: Text('Create folders for artist'),
title: Text('Create folders for artist'.i18n),
leading: Switch(
value: settings.artistFolder,
onChanged: (v) {
@ -548,7 +595,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
),
ListTile(
title: Text('Create folders for albums'),
title: Text('Create folders for albums'.i18n),
leading: Switch(
value: settings.albumFolder,
onChanged: (v) {
@ -558,7 +605,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
),
ListTile(
title: Text('Separate albums by discs'),
title: Text('Separate albums by discs'.i18n),
leading: Switch(
value: settings.albumDiscFolder,
onChanged: (v) {
@ -568,7 +615,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
),
ListTile(
title: Text('Overwrite already downloaded files'),
title: Text('Overwrite already downloaded files'.i18n),
leading: Switch(
value: settings.overwriteDownload,
onChanged: (v) {
@ -578,40 +625,40 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
),
ListTile(
title: Text('Copy ARL'),
subtitle: Text('Copy userToken/ARL Cookie for use in other apps.'),
title: Text('Copy ARL'.i18n),
subtitle: Text('Copy userToken/ARL Cookie for use in other apps.'.i18n),
leading: Icon(Icons.lock),
onTap: () async {
await FlutterClipboard.copy(settings.arl);
await Fluttertoast.showToast(
msg: 'Copied',
msg: 'Copied'.i18n,
);
},
),
ListTile(
title: Text('Log out', style: TextStyle(color: Colors.red),),
title: Text('Log out'.i18n, style: TextStyle(color: Colors.red),),
leading: Icon(Icons.exit_to_app),
onTap: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Log out'),
content: Text('Due to plugin incompatibility, login using browser is unavailable without restart.'),
title: Text('Log out'.i18n),
content: Text('Due to plugin incompatibility, login using browser is unavailable without restart.'.i18n),
actions: <Widget>[
FlatButton(
child: Text('Cancel'),
child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text('(ARL ONLY) Continue'),
child: Text('(ARL ONLY) Continue'.i18n),
onPressed: () {
logOut();
Navigator.of(context).pop();
},
),
FlatButton(
child: Text('Log out & Exit'),
child: Text('Log out & Exit'.i18n),
onPressed: () async {
try {AudioService.stop();} catch (e) {}
await logOut();
@ -656,7 +703,7 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Pick-a-Path'),
title: Text('Pick-a-Path'.i18n),
actions: <Widget>[
IconButton(
icon: Icon(Icons.sd_card),
@ -667,7 +714,7 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
context: context,
builder: (context) {
return AlertDialog(
title: Text('Select storage'),
title: Text('Select storage'.i18n),
content: FutureBuilder(
future: PathProviderEx.getStorageInfo(),
builder: (context, snapshot) {
@ -736,13 +783,13 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
title: Text(_path),
),
ListTile(
title: Text('Go up'),
title: Text('Go up'.i18n),
leading: Icon(Icons.arrow_upward),
onTap: () {
setState(() {
if (_root == _path) {
Fluttertoast.showToast(
msg: 'Permission denied',
msg: 'Permission denied'.i18n,
gravity: ToastGravity.BOTTOM
);
return;

878
pubspec.lock Normal file
View File

@ -0,0 +1,878 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "0.39.14"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.0"
async:
dependency: "direct main"
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.2"
audio_service:
dependency: "direct main"
description:
path: audio_service
relative: true
source: path
version: "0.15.0"
audio_session:
dependency: "direct main"
description:
name: audio_session
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.7"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
build:
dependency: transitive
description:
name: build
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
build_config:
dependency: transitive
description:
name: build_config
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.2"
build_daemon:
dependency: transitive
description:
name: build_daemon
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.11"
build_runner:
dependency: "direct dev"
description:
name: build_runner
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.1"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.1"
built_collection:
dependency: transitive
description:
name: built_collection
url: "https://pub.dartlang.org"
source: hosted
version: "4.3.2"
built_value:
dependency: transitive
description:
name: built_value
url: "https://pub.dartlang.org"
source: hosted
version: "7.1.0"
cached_network_image:
dependency: "direct main"
description:
name: cached_network_image
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.2+1"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.3"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
clipboard:
dependency: "direct main"
description:
name: clipboard
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2+8"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
code_builder:
dependency: transitive
description:
name: code_builder
url: "https://pub.dartlang.org"
source: hosted
version: "3.4.1"
collection:
dependency: "direct main"
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.13"
connectivity:
dependency: "direct main"
description:
name: connectivity
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.9+2"
connectivity_for_web:
dependency: transitive
description:
name: connectivity_for_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.1+2"
connectivity_macos:
dependency: transitive
description:
name: connectivity_macos
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0+4"
connectivity_platform_interface:
dependency: transitive
description:
name: connectivity_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.6"
convert:
dependency: transitive
description:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
cookie_jar:
dependency: "direct main"
description:
name: cookie_jar
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
country_pickers:
dependency: "direct main"
description:
name: country_pickers
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
crypto:
dependency: "direct main"
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.5"
csslib:
dependency: transitive
description:
name: csslib
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.2"
custom_navigator:
dependency: "direct main"
description:
name: custom_navigator
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
dart_style:
dependency: transitive
description:
name: dart_style
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.6"
dio:
dependency: "direct main"
description:
name: dio
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.10"
dio_cookie_manager:
dependency: "direct main"
description:
name: dio_cookie_manager
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
disk_space:
dependency: "direct main"
description:
name: disk_space
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.3"
ext_storage:
dependency: "direct main"
description:
name: ext_storage
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
fading_edge_scrollview:
dependency: transitive
description:
name: fading_edge_scrollview
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.4"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
file:
dependency: transitive
description:
name: file
url: "https://pub.dartlang.org"
source: hosted
version: "5.2.1"
filesize:
dependency: "direct main"
description:
name: filesize
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
fixnum:
dependency: transitive
description:
name: fixnum
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.11"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_blurhash:
dependency: transitive
description:
name: flutter_blurhash
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.0"
flutter_cache_manager:
dependency: "direct main"
description:
name: flutter_cache_manager
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.2"
flutter_inappwebview:
dependency: "direct main"
description:
name: flutter_inappwebview
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0+4"
flutter_isolate:
dependency: transitive
description:
name: flutter_isolate
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0+14"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.4+4"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_material_color_picker:
dependency: "direct main"
description:
name: flutter_material_color_picker
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
flutter_screenutil:
dependency: "direct main"
description:
name: flutter_screenutil
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
fluttertoast:
dependency: "direct main"
description:
name: fluttertoast
url: "https://pub.dartlang.org"
source: hosted
version: "7.1.1"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
graphs:
dependency: transitive
description:
name: graphs
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
hex:
dependency: "direct main"
description:
name: hex
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
html:
dependency: "direct main"
description:
name: html
url: "https://pub.dartlang.org"
source: hosted
version: "0.14.0+3"
http:
dependency: transitive
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.2"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.4"
i18n_extension:
dependency: "direct main"
description:
name: i18n_extension
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.4"
intl:
dependency: "direct main"
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.1"
io:
dependency: transitive
description:
name: io
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.4"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.2"
json_annotation:
dependency: "direct main"
description:
name: json_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
json_serializable:
dependency: "direct dev"
description:
name: json_serializable
url: "https://pub.dartlang.org"
source: hosted
version: "3.4.1"
just_audio:
dependency: "direct main"
description:
path: just_audio
relative: true
source: path
version: "0.3.1"
language_pickers:
dependency: "direct main"
description:
name: language_pickers
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0+1"
logging:
dependency: transitive
description:
name: logging
url: "https://pub.dartlang.org"
source: hosted
version: "0.11.4"
marquee:
dependency: "direct main"
description:
name: marquee
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.1"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.8"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.8"
mime:
dependency: transitive
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7"
move_to_background:
dependency: "direct main"
description:
name: move_to_background
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
node_interop:
dependency: transitive
description:
name: node_interop
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
node_io:
dependency: transitive
description:
name: node_io
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
octo_image:
dependency: transitive
description:
name: octo_image
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
package_info:
dependency: "direct main"
description:
name: package_info
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.3"
palette_generator:
dependency: "direct main"
description:
name: palette_generator
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.3"
path:
dependency: "direct main"
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.14"
path_provider_ex:
dependency: "direct main"
description:
name: path_provider_ex
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.1+2"
path_provider_macos:
dependency: transitive
description:
name: path_provider_macos
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4+3"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.0"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.1+1"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
platform:
dependency: transitive
description:
name: platform
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
pointycastle:
dependency: "direct main"
description:
name: pointycastle
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
pool:
dependency: transitive
description:
name: pool
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
process:
dependency: transitive
description:
name: process
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.13"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.4"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.5"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
random_string:
dependency: "direct main"
description:
name: random_string
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
rxdart:
dependency: transitive
description:
name: rxdart
url: "https://pub.dartlang.org"
source: hosted
version: "0.24.1"
shelf:
dependency: transitive
description:
name: shelf
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.9"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.3"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_gen:
dependency: transitive
description:
name: source_gen
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.6"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
sprintf:
dependency: transitive
description:
name: sprintf
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
sqflite:
dependency: "direct main"
description:
name: sqflite
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1+1"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2+1"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.5"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
stream_transform:
dependency: transitive
description:
name: stream_transform
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
synchronized:
dependency: transitive
description:
name: synchronized
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0+2"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.17"
timing:
dependency: transitive
description:
name: timing
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.1+2"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
uuid:
dependency: transitive
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.2"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7+15"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
sdks:
dart: ">=2.9.0 <3.0.0"
flutter: ">=1.20.0 <2.0.0"

View File

@ -15,15 +15,18 @@ 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.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.2.0
version: 0.4.0+1
environment:
sdk: ">=2.6.0 <3.0.0"
sdk: ">=2.8.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
dio: ^3.0.9
dio_cookie_manager: ^1.0.0
cookie_jar: ^1.0.1
@ -60,8 +63,11 @@ dependencies:
flutter_cache_manager: ^1.4.1
cached_network_image: ^2.2.0+1
clipboard: ^0.1.2+8
i18n_extension: ^1.4.4
audio_service: ^0.13.0
audio_session: ^0.0.7
audio_service:
path: ./audio_service
just_audio:
path: ./just_audio
@ -94,6 +100,7 @@ flutter:
- assets/cover.jpg
- assets/cover_thumb.jpg
- assets/icon.png
- assets/favorites_thumb.jpg
fonts:
# - family: Montserrat