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 *.iws
.idea/ .idea/
android/.idea/ android/.idea/
android/local.properties
# The .vscode folder contains launch configuration and tasks you configure in # 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 # 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. If you want to report bug or request feature, please open an issue.
## Downloads: ## Downloads:
Under releases tab 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. **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 ## Compile from source
Install flutter SDK: https://flutter.dev/docs/get-started/install Install flutter SDK: https://flutter.dev/docs/get-started/install
(Optional) Generate keys for release build: https://flutter.dev/docs/deployment/android (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: Compile:
``` ```
flutter pub get flutter pub get
flutter build apk flutter build apk
``` ```
## Telegram ## Telegram group
https://t.me/freezerandroid https://t.me/freezerandroid
## Credits ## Credits
Tobs: Beta tester Tobs: Beta tester
Bas Curtiz: Icon, Logo, Banner, Design suggestions Bas Curtiz: Icon, Logo, Banner, Design suggestions
Deemix: https://notabug.org/RemixDev/deemix 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 ## Support me
BTC: `14hcr4PGbgqeXd3SoXY9QyJFNpyurgrL9y` BTC: `14hcr4PGbgqeXd3SoXY9QyJFNpyurgrL9y`
ETH: `0xb4D1893195404E1F4b45e5BDA77F202Ac4012288` 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 ## Disclaimer
``` ```

View File

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

View File

@ -31,6 +31,7 @@
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme"
/> />
<!-- Displays an Android View that continues showing the launch screen <!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual screen fades out. A splash screen is useful to avoid any visual
@ -64,5 +65,8 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<meta-data android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"/>
</application> </application>
</manifest> </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.FileOutputStream;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.function.Function; import java.util.function.Function;
import javax.crypto.Cipher; import javax.crypto.Cipher;
@ -73,10 +74,33 @@ public class MainActivity extends FlutterActivity {
tag.setField(FieldKey.ALBUM, call.argument("album").toString()); tag.setField(FieldKey.ALBUM, call.argument("album").toString());
tag.setField(FieldKey.ARTIST, call.argument("artists").toString()); tag.setField(FieldKey.ARTIST, call.argument("artists").toString());
tag.setField(FieldKey.TRACK, call.argument("trackNumber").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 //Album art
String cover = call.argument("cover").toString(); String cover = call.argument("cover").toString();
if (isFlac) { 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 //FLAC requires different cover adding
RandomAccessFile imageFile = new RandomAccessFile(new File(cover), "r"); RandomAccessFile imageFile = new RandomAccessFile(new File(cover), "r");
byte[] imageData = new byte[(int) imageFile.length()]; 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 token;
String userId; String userId;
String userName;
String favoritesPlaylistId; String favoritesPlaylistId;
String privateUrl = 'http://www.deezer.com/ajax/gw-light.php'; String privateUrl = 'http://www.deezer.com/ajax/gw-light.php';
Map<String, String> headers = { Map<String, String> headers = {
@ -95,6 +96,7 @@ class DeezerAPI {
} else { } else {
this.token = data['results']['checkForm']; this.token = data['results']['checkForm'];
this.userId = data['results']['USER']['USER_ID'].toString(); this.userId = data['results']['USER']['USER_ID'].toString();
this.userName = data['results']['USER']['BLOG_NAME'];
this.favoritesPlaylistId = data['results']['USER']['LOVEDTRACKS_ID']; this.favoritesPlaylistId = data['results']['USER']['LOVEDTRACKS_ID'];
return true; return true;
} }
@ -384,5 +386,20 @@ class DeezerAPI {
return data['results']['data'].map<Album>((a) => Album.fromPrivateJson(a)).toList(); 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 offline;
bool library; 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); String get fansString => NumberFormat.compact().format(fans);
@ -264,16 +267,23 @@ class Artist {
Map<dynamic, dynamic> albumsJson = const {}, Map<dynamic, dynamic> albumsJson = const {},
Map<dynamic, dynamic> topJson = const {}, Map<dynamic, dynamic> topJson = const {},
bool library = false bool library = false
}) => Artist( }) {
id: json['ART_ID'].toString(), //Get wether radio is available
name: json['ART_NAME'], bool _radio = false;
fans: json['NB_FAN'], if (json['SMARTRADIO'] == true || json['SMARTRADIO'] == 1) _radio = true;
picture: ImageDetails.fromPrivateString(json['ART_PICTURE'], type: 'artist'),
albumCount: albumsJson['total'], return Artist(
albums: (albumsJson['data']??[]).map<Album>((dynamic data) => Album.fromPrivateJson(data)).toList(), id: json['ART_ID'].toString(),
topTracks: (topJson['data']??[]).map<Track>((dynamic data) => Track.fromPrivateJson(data)).toList(), name: json['ART_NAME'],
library: library 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}) => { Map<String, dynamic> toSQL({off = false}) => {
'id': id, 'id': id,
'name': name, 'name': name,

View File

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

View File

@ -122,14 +122,6 @@ class DownloadManager {
onDone: () async { onDone: () async {
//On download finished //On download finished
await db.rawUpdate('UPDATE downloads SET state = 1 WHERE trackId = ?', [queue[0].track.id]); 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); queue.removeAt(0);
_download = null; _download = null;
//Remove notification if no more downloads //Remove notification if no more downloads
@ -144,7 +136,11 @@ class DownloadManager {
//Catch download errors //Catch download errors
_download = null; _download = null;
_cancelNotifications = true; _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(); await _showError();
}); });
//Show download progress notifications //Show download progress notifications
@ -318,14 +314,12 @@ class DownloadManager {
track = await deezerAPI.track(track.id); track = await deezerAPI.track(track.id);
} }
String url = track.getUrl(settings.getQualityInt(settings.offlineQuality));
if (!private) { if (!private) {
//Check permissions //Check permissions
if (!(await Permission.storage.request().isGranted)) { if (!(await Permission.storage.request().isGranted)) {
return; return;
} }
//If saving to external //If saving to external
url = track.getUrl(settings.getQualityInt(settings.downloadQuality));
//Save just extension to path, will be generated before download //Save just extension to path, will be generated before download
path = 'mp3'; path = 'mp3';
if (settings.downloadQuality == AudioQuality.FLAC) { if (settings.downloadQuality == AudioQuality.FLAC) {
@ -339,7 +333,7 @@ class DownloadManager {
} catch (e) {} } catch (e) {}
} }
Download download = Download(track: track, path: path, url: url, private: private); Download download = Download(track: track, path: path, private: private);
//Database //Database
Batch b = db.batch(); Batch b = db.batch();
b.insert('downloads', download.toSQL()); b.insert('downloads', download.toSQL());
@ -539,12 +533,17 @@ class Download {
//TODO: Check for internet before downloading //TODO: Check for internet before downloading
Map rawTrackPublic = {};
Map rawAlbumPublic = {};
if (!this.private && !(this.path.endsWith('.mp3') || this.path.endsWith('.flac'))) { if (!this.private && !(this.path.endsWith('.mp3') || this.path.endsWith('.flac'))) {
String ext = this.path; String ext = this.path;
//Get track details //Get track details
Map _rawTrackData = await deezerAPI.callApi('song.getListData', params: {'sng_ids': [track.id]}); Map _rawTrackData = await deezerAPI.callApi('song.getListData', params: {'sng_ids': [track.id]});
Map rawTrack = _rawTrackData['results']['data'][0]; Map rawTrack = _rawTrackData['results']['data'][0];
this.track = Track.fromPrivateJson(rawTrack); 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 //Get path if public
RegExp sanitize = RegExp(r'[\/\\\?\%\*\:\|\"\<\>]'); RegExp sanitize = RegExp(r'[\/\\\?\%\*\:\|\"\<\>]');
@ -620,6 +619,11 @@ class Download {
//Create file if doesnt exist //Create file if doesnt exist
await downloadFile.create(recursive: true); await downloadFile.create(recursive: true);
} }
//Quality fallback
if (this.url == null)
await _fallback();
//Download //Download
_cancel = CancelToken(); _cancel = CancelToken();
Response response = await dio.get( Response response = await dio.get(
@ -658,6 +662,10 @@ class Download {
//Tag //Tag
if (!private) { if (!private) {
//Tag track in native //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', { await platformChannel.invokeMethod('tagTrack', {
'path': path, 'path': path,
'title': track.title, 'title': track.title,
@ -665,7 +673,16 @@ class Download {
'artists': track.artistString, 'artists': track.artistString,
'artist': track.artists[0].name, 'artist': track.artists[0].name,
'cover': _cover, '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 //Rescan android library
await platformChannel.invokeMethod('rescanLibrary', { await platformChannel.invokeMethod('rescanLibrary', {
@ -702,6 +719,36 @@ class Download {
return; 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 //JSON
Map<String, dynamic> toSQL() => { Map<String, dynamic> toSQL() => {
'trackId': track.id, 'trackId': track.id,

View File

@ -1,11 +1,13 @@
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:freezer/api/deezer.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:just_audio/just_audio.dart';
import 'package:connectivity/connectivity.dart'; import 'package:connectivity/connectivity.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:freezer/translations.i18n.dart';
import 'definitions.dart'; import 'definitions.dart';
import '../settings.dart'; import '../settings.dart';
@ -43,9 +45,21 @@ class PlayerHelper {
} }
if (event['action'] == 'queueEnd') { if (event['action'] == 'queueEnd') {
//If last song is played, load more queue //If last song is played, load more queue
onQueueEnd();
this.queueSource = QueueSource.fromJson(event['queueSource']); this.queueSource = QueueSource.fromJson(event['queueSource']);
return; 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) { _playbackStateStreamSubscription = AudioService.playbackStateStream.listen((event) {
//Log song (if allowed) //Log song (if allowed)
@ -106,6 +120,30 @@ class PlayerHelper {
await AudioService.skipToQueueItem(trackId); 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 //Play track from album
Future playFromAlbum(Album album, String trackId) async { Future playFromAlbum(Album album, String trackId) async {
await playFromTrackList(album.tracks, trackId, QueueSource( await playFromTrackList(album.tracks, trackId, QueueSource(
@ -144,7 +182,7 @@ class PlayerHelper {
if (stl.tracks == null || stl.tracks.length == 0) { if (stl.tracks == null || stl.tracks.length == 0) {
if (settings.offlineMode) { if (settings.offlineMode) {
Fluttertoast.showToast( 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, gravity: ToastGravity.BOTTOM,
toastLength: Toast.LENGTH_SHORT toastLength: Toast.LENGTH_SHORT
); );
@ -188,7 +226,6 @@ class AudioPlayerTask extends BackgroundAudioTask {
ConcatenatingAudioSource _audioSource; ConcatenatingAudioSource _audioSource;
AudioProcessingState _skipState; AudioProcessingState _skipState;
bool _interrupted;
Seeker _seeker; Seeker _seeker;
//Stream subscriptions //Stream subscriptions
@ -200,10 +237,15 @@ class AudioPlayerTask extends BackgroundAudioTask {
QueueSource queueSource; QueueSource queueSource;
Duration _lastPosition; Duration _lastPosition;
Completer _androidAutoCallback;
MediaItem get mediaItem => _queue[_queueIndex]; MediaItem get mediaItem => _queue[_queueIndex];
@override @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 //Update track index
_player.currentIndexStream.listen((index) { _player.currentIndexStream.listen((index) {
@ -285,6 +327,20 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override @override
Future<void> onSeekBackward(bool begin) async => _seekContinuously(begin, -1); 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 //While seeking, jump 10s every 1s
void _seekContinuously(bool begin, int direction) { void _seekContinuously(bool begin, int direction) {
_seeker?.stop(); _seeker?.stop();
@ -430,46 +486,14 @@ class AudioPlayerTask extends BackgroundAudioTask {
if (name == 'load') await this._loadQueueFile(); if (name == 'load') await this._loadQueueFile();
//Shuffle //Shuffle
if (name == 'shuffle') await _player.setShuffleModeEnabled(args); 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; 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 @override
Future onTaskRemoved() async { Future onTaskRemoved() async {
await onStop(); await onStop();
@ -561,6 +585,16 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override @override
Future onPlayFromMediaId(String mediaId) async { 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 //Does the same thing
await this.onSkipToQueueItem(mediaId); await this.onSkipToQueueItem(mediaId);
} }

View File

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

View File

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

View File

@ -11,6 +11,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
downloadPath: json['downloadPath'] as String, downloadPath: json['downloadPath'] as String,
arl: json['arl'] as String, arl: json['arl'] as String,
) )
..language = json['language'] as String
..wifiQuality = ..wifiQuality =
_$enumDecodeNullable(_$AudioQualityEnumMap, json['wifiQuality']) ?? _$enumDecodeNullable(_$AudioQualityEnumMap, json['wifiQuality']) ??
AudioQuality.MP3_320 AudioQuality.MP3_320
@ -39,6 +40,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
} }
Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
'language': instance.language,
'arl': instance.arl, 'arl': instance.arl,
'wifiQuality': _$AudioQualityEnumMap[instance.wifiQuality], 'wifiQuality': _$AudioQualityEnumMap[instance.wifiQuality],
'mobileQuality': _$AudioQualityEnumMap[instance.mobileQuality], '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> { class _CachedImageState extends State<CachedImage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.circular) return ClipOval( if (widget.circular) return ClipOval(
child: CachedImage(url: widget.url, height: widget.height, width: widget.width, circular: false) 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( return CachedNetworkImage(
imageUrl: widget.url, imageUrl: widget.url,
width: widget.width, width: widget.width,

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:freezer/translations.i18n.dart';
class ErrorScreen extends StatelessWidget { class ErrorScreen extends StatelessWidget {
@ -18,7 +19,7 @@ class ErrorScreen extends StatelessWidget {
size: 64.0, size: 64.0,
), ),
Container(height: 4.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/api/player.dart';
import 'package:freezer/ui/error.dart'; import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/menu.dart'; import 'package:freezer/ui/menu.dart';
import 'package:freezer/translations.i18n.dart';
import 'tiles.dart'; import 'tiles.dart';
import 'details_screens.dart'; import 'details_screens.dart';
import '../settings.dart'; import '../settings.dart';
@ -175,7 +176,7 @@ class _HomePageScreenState extends State<HomePageScreen> {
if (section.hasMore??false) { if (section.hasMore??false) {
return FlatButton( return FlatButton(
child: Text( child: Text(
'Show more', 'Show more'.i18n,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 20.0 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/definitions.dart';
import 'package:freezer/api/spotify.dart'; import 'package:freezer/api/spotify.dart';
import 'package:freezer/ui/menu.dart'; import 'package:freezer/ui/menu.dart';
import 'package:freezer/translations.i18n.dart';
class ImporterScreen extends StatefulWidget { class ImporterScreen extends StatefulWidget {
@override @override
@ -34,8 +35,8 @@ class _ImporterScreenState extends State<ImporterScreen> {
setState(() => _data = data); setState(() => _data = data);
return; return;
} catch (e) { } catch (e, st) {
print(e); print('$e, $st');
setState(() { setState(() {
_error = true; _error = true;
_loading = false; _loading = false;
@ -49,13 +50,13 @@ class _ImporterScreenState extends State<ImporterScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Importer'), title: Text('Importer'.i18n),
), ),
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
title: Text('Currently supporting only Spotify, with 100 tracks limit'), title: Text('Currently supporting only Spotify, with 100 tracks limit'.i18n),
subtitle: Text('Due to API limitations'), subtitle: Text('Due to API limitations'.i18n),
leading: Icon( leading: Icon(
Icons.warning, Icons.warning,
color: Colors.deepOrangeAccent, color: Colors.deepOrangeAccent,
@ -64,7 +65,7 @@ class _ImporterScreenState extends State<ImporterScreen> {
Divider(), Divider(),
Container(height: 16.0,), Container(height: 16.0,),
Text( Text(
'Enter your playlist link below', 'Enter your playlist link below'.i18n,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 20.0 fontSize: 20.0
@ -104,7 +105,7 @@ class _ImporterScreenState extends State<ImporterScreen> {
), ),
if (_error) if (_error)
ListTile( ListTile(
title: Text('Error loading URL!'), title: Text('Error loading URL!'.i18n),
leading: Icon(Icons.error, color: Colors.red,), leading: Icon(Icons.error, color: Colors.red,),
), ),
if (_data != null) if (_data != null)
@ -133,13 +134,14 @@ class _ImporterWidgetState extends State<ImporterWidget> {
ListTile( ListTile(
title: Text(widget.playlist.name), title: Text(widget.playlist.name),
subtitle: Text(widget.playlist.description), 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( Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[ children: <Widget>[
RaisedButton( RaisedButton(
child: Text('Convert'), child: Text('Convert'.i18n),
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
onPressed: () { onPressed: () {
spotify.convertPlaylist(widget.playlist); spotify.convertPlaylist(widget.playlist);
@ -149,7 +151,7 @@ class _ImporterWidgetState extends State<ImporterWidget> {
}, },
), ),
RaisedButton( RaisedButton(
child: Text('Download only'), child: Text('Download only'.i18n),
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
onPressed: () { onPressed: () {
spotify.convertPlaylist(widget.playlist, downloadOnly: true); spotify.convertPlaylist(widget.playlist, downloadOnly: true);
@ -197,7 +199,7 @@ class CurrentlyImportingScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Importing...'),), appBar: AppBar(title: Text('Importing...'.i18n),),
body: StreamBuilder( body: StreamBuilder(
stream: spotify.importingStream.stream, stream: spotify.importingStream.stream,
builder: (context, snapshot) { builder: (context, snapshot) {
@ -254,7 +256,7 @@ class CurrentlyImportingScreen extends StatelessWidget {
), ),
if (snapshot.data != null) if (snapshot.data != null)
FlatButton( FlatButton(
child: Text('Playlist menu'), child: Text('Playlist menu'.i18n),
onPressed: () async { onPressed: () async {
Playlist p = await deezerAPI.playlist(snapshot.data); Playlist p = await deezerAPI.playlist(snapshot.data);
p.library = true; 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/error.dart';
import 'package:freezer/ui/importer_screen.dart'; import 'package:freezer/ui/importer_screen.dart';
import 'package:freezer/ui/tiles.dart'; import 'package:freezer/ui/tiles.dart';
import 'package:freezer/translations.i18n.dart';
import 'menu.dart'; import 'menu.dart';
import 'settings_screen.dart'; import 'settings_screen.dart';
@ -24,7 +25,7 @@ class LibraryAppBar extends StatelessWidget implements PreferredSizeWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppBar( return AppBar(
title: Text('Library'), title: Text('Library'.i18n),
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
icon: Icon(Icons.file_download), icon: Icon(Icons.file_download),
@ -58,9 +59,9 @@ class LibraryScreen extends StatelessWidget {
Container(height: 4.0,), Container(height: 4.0,),
if (downloadManager.stopped && downloadManager.queue.length > 0) if (downloadManager.stopped && downloadManager.queue.length > 0)
ListTile( ListTile(
title: Text('Downloads'), title: Text('Downloads'.i18n),
leading: Icon(Icons.file_download), 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: () { onTap: () {
downloadManager.start(); downloadManager.start();
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).push(MaterialPageRoute(
@ -73,7 +74,7 @@ class LibraryScreen extends StatelessWidget {
Divider(), Divider(),
ListTile( ListTile(
title: Text('Tracks'), title: Text('Tracks'.i18n),
leading: Icon(Icons.audiotrack), leading: Icon(Icons.audiotrack),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
@ -82,7 +83,7 @@ class LibraryScreen extends StatelessWidget {
}, },
), ),
ListTile( ListTile(
title: Text('Albums'), title: Text('Albums'.i18n),
leading: Icon(Icons.album), leading: Icon(Icons.album),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
@ -91,7 +92,7 @@ class LibraryScreen extends StatelessWidget {
}, },
), ),
ListTile( ListTile(
title: Text('Artists'), title: Text('Artists'.i18n),
leading: Icon(Icons.recent_actors), leading: Icon(Icons.recent_actors),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
@ -100,7 +101,7 @@ class LibraryScreen extends StatelessWidget {
}, },
), ),
ListTile( ListTile(
title: Text('Playlists'), title: Text('Playlists'.i18n),
leading: Icon(Icons.playlist_play), leading: Icon(Icons.playlist_play),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
@ -110,9 +111,9 @@ class LibraryScreen extends StatelessWidget {
), ),
Divider(), Divider(),
ListTile( ListTile(
title: Text('Import'), title: Text('Import'.i18n),
leading: Icon(Icons.import_export), leading: Icon(Icons.import_export),
subtitle: Text('Import playlists from Spotify'), subtitle: Text('Import playlists from Spotify'.i18n),
onTap: () { onTap: () {
if (spotify.doneImporting != null) { if (spotify.doneImporting != null) {
Navigator.of(context).push( Navigator.of(context).push(
@ -128,7 +129,7 @@ class LibraryScreen extends StatelessWidget {
}, },
), ),
ExpansionTile( ExpansionTile(
title: Text('Statistics'), title: Text('Statistics'.i18n),
leading: Icon(Icons.insert_chart), leading: Icon(Icons.insert_chart),
children: <Widget>[ children: <Widget>[
FutureBuilder( FutureBuilder(
@ -148,27 +149,27 @@ class LibraryScreen extends StatelessWidget {
return Column( return Column(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
title: Text('Offline tracks'), title: Text('Offline tracks'.i18n),
leading: Icon(Icons.audiotrack), leading: Icon(Icons.audiotrack),
trailing: Text(data[0]), trailing: Text(data[0]),
), ),
ListTile( ListTile(
title: Text('Offline albums'), title: Text('Offline albums'.i18n),
leading: Icon(Icons.album), leading: Icon(Icons.album),
trailing: Text(data[1]), trailing: Text(data[1]),
), ),
ListTile( ListTile(
title: Text('Offline playlists'), title: Text('Offline playlists'.i18n),
leading: Icon(Icons.playlist_add), leading: Icon(Icons.playlist_add),
trailing: Text(data[2]), trailing: Text(data[2]),
), ),
ListTile( ListTile(
title: Text('Offline size'), title: Text('Offline size'.i18n),
leading: Icon(Icons.sd_card), leading: Icon(Icons.sd_card),
trailing: Text(data[3]), trailing: Text(data[3]),
), ),
ListTile( ListTile(
title: Text('Free space'), title: Text('Free space'.i18n),
leading: Icon(Icons.disc_full), leading: Icon(Icons.disc_full),
trailing: Text(data[4]), trailing: Text(data[4]),
), ),
@ -254,7 +255,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Tracks'),), appBar: AppBar(title: Text('Tracks'.i18n),),
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
Card( Card(
@ -263,7 +264,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
children: <Widget>[ children: <Widget>[
Container(height: 8.0,), Container(height: 8.0,),
Text( Text(
'Loved tracks', 'Loved tracks'.i18n,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 24 fontSize: 24
@ -279,7 +280,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
children: <Widget>[ children: <Widget>[
Icon(Icons.file_download, size: 32.0,), Icon(Icons.file_download, size: 32.0,),
Container(width: 4,), Container(width: 4,),
Text('Download') Text('Download'.i18n)
], ],
), ),
onPressed: () { onPressed: () {
@ -299,7 +300,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
onTap: () { onTap: () {
playerHelper.playFromTrackList(tracks, t.id, QueueSource( playerHelper.playFromTrackList(tracks, t.id, QueueSource(
id: deezerAPI.favoritesPlaylistId, id: deezerAPI.favoritesPlaylistId,
text: 'Favorites', text: 'Favorites'.i18n,
source: 'playlist' source: 'playlist'
)); ));
}, },
@ -328,7 +329,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
), ),
Divider(), Divider(),
Text( Text(
'All offline tracks', 'All offline tracks'.i18n,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 24, fontSize: 24,
@ -343,7 +344,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
onTap: () { onTap: () {
playerHelper.playFromTrackList(allTracks, t.id, QueueSource( playerHelper.playFromTrackList(allTracks, t.id, QueueSource(
id: 'allTracks', id: 'allTracks',
text: 'All offline tracks', text: 'All offline tracks'.i18n,
source: 'offline' source: 'offline'
)); ));
}, },
@ -386,7 +387,7 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Albums'),), appBar: AppBar(title: Text('Albums'.i18n),),
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
Container(height: 8.0,), Container(height: 8.0,),
@ -427,7 +428,7 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
children: <Widget>[ children: <Widget>[
Divider(), Divider(),
Text( Text(
'Offline albums', 'Offline albums'.i18n,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -473,7 +474,7 @@ class _LibraryArtistsState extends State<LibraryArtists> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Artists'),), appBar: AppBar(title: Text('Artists'.i18n),),
body: FutureBuilder( body: FutureBuilder(
future: deezerAPI.getArtists(), future: deezerAPI.getArtists(),
builder: (BuildContext context, AsyncSnapshot snapshot) { builder: (BuildContext context, AsyncSnapshot snapshot) {
@ -533,20 +534,30 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
super.initState(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Playlists'),), appBar: AppBar(title: Text('Playlists'.i18n),),
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
title: Text('Create new playlist'), title: Text('Create new playlist'.i18n),
leading: Icon(Icons.playlist_add), leading: Icon(Icons.playlist_add),
onTap: () { onTap: () {
if (settings.offlineMode) { if (settings.offlineMode) {
Fluttertoast.showToast( Fluttertoast.showToast(
msg: 'Cannot create playlists in offline mode', msg: 'Cannot create playlists in offline mode'.i18n,
gravity: ToastGravity.BOTTOM gravity: ToastGravity.BOTTOM
); );
return; 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) if (_playlists != null)
...List.generate(_playlists.length, (int i) { ...List.generate(_playlists.length, (int i) {
Playlist p = _playlists[i]; Playlist p = _playlists[i];
@ -593,7 +618,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
children: <Widget>[ children: <Widget>[
Divider(), Divider(),
Text( Text(
'Offline playlists', 'Offline playlists'.i18n,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 24.0, fontSize: 24.0,

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import 'package:freezer/api/download.dart';
import 'package:freezer/api/player.dart'; import 'package:freezer/api/player.dart';
import 'package:freezer/ui/details_screens.dart'; import 'package:freezer/ui/details_screens.dart';
import 'package:freezer/ui/menu.dart'; import 'package:freezer/ui/menu.dart';
import 'package:freezer/translations.i18n.dart';
import 'tiles.dart'; import 'tiles.dart';
import '../api/deezer.dart'; import '../api/deezer.dart';
@ -20,6 +21,7 @@ class _SearchScreenState extends State<SearchScreen> {
String _query; String _query;
bool _offline = false; bool _offline = false;
TextEditingController _controller = new TextEditingController(); TextEditingController _controller = new TextEditingController();
List _suggestions = [];
void _submit(BuildContext context, {String query}) { void _submit(BuildContext context, {String query}) {
if (query != null) _query = query; if (query != null) _query = query;
@ -41,10 +43,21 @@ class _SearchScreenState extends State<SearchScreen> {
super.initState(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Search'),), appBar: AppBar(title: Text('Search'.i18n),),
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
Container(height: 16.0), Container(height: 16.0),
@ -57,9 +70,12 @@ class _SearchScreenState extends State<SearchScreen> {
alignment: Alignment(1.0, 1.0), alignment: Alignment(1.0, 1.0),
children: [ children: [
TextField( TextField(
onChanged: (String s) => _query = s, onChanged: (String s) {
setState(() => _query = s);
_loadSuggestions();
},
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Search' labelText: 'Search'.i18n
), ),
controller: _controller, controller: _controller,
onSubmitted: (String s) => _submit(context, query: s), onSubmitted: (String s) => _submit(context, query: s),
@ -73,7 +89,6 @@ class _SearchScreenState extends State<SearchScreen> {
], ],
) )
), ),
Padding( Padding(
padding: EdgeInsets.fromLTRB(0, 8, 0, 0), padding: EdgeInsets.fromLTRB(0, 8, 0, 0),
child: IconButton( child: IconButton(
@ -85,14 +100,23 @@ class _SearchScreenState extends State<SearchScreen> {
), ),
), ),
ListTile( ListTile(
title: Text('Offline search'), title: Text('Offline search'.i18n),
leading: Switch( leading: Switch(
value: _offline, value: _offline,
onChanged: (v) { onChanged: (v) {
setState(() => _offline = !_offline); 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) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Search Results'), title: Text('Search Results'.i18n),
), ),
body: FutureBuilder( body: FutureBuilder(
future: _search(), future: _search(),
@ -139,7 +163,7 @@ class SearchResultsScreen extends StatelessWidget {
Icons.warning, Icons.warning,
size: 64, 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) { if (results.tracks != null && results.tracks.length != 0) {
tracks = [ tracks = [
Text( Text(
'Tracks', 'Tracks'.i18n,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 26.0, fontSize: 26.0,
@ -163,7 +187,7 @@ class SearchResultsScreen extends StatelessWidget {
t, t,
onTap: () { onTap: () {
playerHelper.playFromTrackList(results.tracks, t.id, QueueSource( playerHelper.playFromTrackList(results.tracks, t.id, QueueSource(
text: 'Search', text: 'Search'.i18n,
id: query, id: query,
source: 'search' source: 'search'
)); ));
@ -175,13 +199,13 @@ class SearchResultsScreen extends StatelessWidget {
); );
}), }),
ListTile( ListTile(
title: Text('Show all tracks'), title: Text('Show all tracks'.i18n),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute(builder: (context) => TrackListScreen(results.tracks, QueueSource( MaterialPageRoute(builder: (context) => TrackListScreen(results.tracks, QueueSource(
id: query, id: query,
source: 'search', source: 'search',
text: 'Search' text: 'Search'.i18n
))) )))
); );
}, },
@ -194,7 +218,7 @@ class SearchResultsScreen extends StatelessWidget {
if (results.albums != null && results.albums.length != 0) { if (results.albums != null && results.albums.length != 0) {
albums = [ albums = [
Text( Text(
'Albums', 'Albums'.i18n,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 26.0, fontSize: 26.0,
@ -218,7 +242,7 @@ class SearchResultsScreen extends StatelessWidget {
); );
}), }),
ListTile( ListTile(
title: Text('Show all albums'), title: Text('Show all albums'.i18n),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute(builder: (context) => AlbumListScreen(results.albums)) MaterialPageRoute(builder: (context) => AlbumListScreen(results.albums))
@ -233,7 +257,7 @@ class SearchResultsScreen extends StatelessWidget {
if (results.artists != null && results.artists.length != 0) { if (results.artists != null && results.artists.length != 0) {
artists = [ artists = [
Text( Text(
'Artists', 'Artists'.i18n,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 26.0, fontSize: 26.0,
@ -269,7 +293,7 @@ class SearchResultsScreen extends StatelessWidget {
if (results.playlists != null && results.playlists.length != 0) { if (results.playlists != null && results.playlists.length != 0) {
playlists = [ playlists = [
Text( Text(
'Playlists', 'Playlists'.i18n,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 26.0, fontSize: 26.0,
@ -293,7 +317,7 @@ class SearchResultsScreen extends StatelessWidget {
); );
}), }),
ListTile( ListTile(
title: Text('Show all playlists'), title: Text('Show all playlists'.i18n),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute(builder: (context) => SearchResultPlaylists(results.playlists)) MaterialPageRoute(builder: (context) => SearchResultPlaylists(results.playlists))
@ -332,7 +356,7 @@ class TrackListScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Tracks'),), appBar: AppBar(title: Text('Tracks'.i18n),),
body: ListView.builder( body: ListView.builder(
itemCount: tracks.length, itemCount: tracks.length,
itemBuilder: (BuildContext context, int i) { itemBuilder: (BuildContext context, int i) {
@ -362,7 +386,7 @@ class AlbumListScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Albums'),), appBar: AppBar(title: Text('Albums'.i18n),),
body: ListView.builder( body: ListView.builder(
itemCount: albums.length, itemCount: albums.length,
itemBuilder: (context, i) { itemBuilder: (context, i) {
@ -393,7 +417,7 @@ class SearchResultPlaylists extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Playlists'),), appBar: AppBar(title: Text('Playlists'.i18n),),
body: ListView.builder( body: ListView.builder(
itemCount: playlists.length, itemCount: playlists.length,
itemBuilder: (context, i) { 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:fluttertoast/fluttertoast.dart';
import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/deezer.dart';
import 'package:freezer/ui/error.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/language_pickers.dart';
import 'package:language_pickers/languages.dart'; import 'package:language_pickers/languages.dart';
import 'package:package_info/package_info.dart'; import 'package:package_info/package_info.dart';
import 'package:path_provider_ex/path_provider_ex.dart'; import 'package:path_provider_ex/path_provider_ex.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:clipboard/clipboard.dart'; import 'package:clipboard/clipboard.dart';
import '../settings.dart'; import '../settings.dart';
@ -44,18 +46,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Settings'),), appBar: AppBar(title: Text('Settings'.i18n),),
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
title: Text('General'), title: Text('General'.i18n),
leading: Icon(Icons.settings), leading: Icon(Icons.settings),
onTap: () => Navigator.of(context).push(MaterialPageRoute( onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => GeneralSettings() builder: (context) => GeneralSettings()
)), )),
), ),
ListTile( ListTile(
title: Text('Appearance'), title: Text('Appearance'.i18n),
leading: Icon(Icons.color_lens), leading: Icon(Icons.color_lens),
onTap: () => Navigator.push( onTap: () => Navigator.push(
context, context,
@ -63,7 +65,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
), ),
ListTile( ListTile(
title: Text('Quality'), title: Text('Quality'.i18n),
leading: Icon(Icons.high_quality), leading: Icon(Icons.high_quality),
onTap: () => Navigator.push( onTap: () => Navigator.push(
context, context,
@ -71,12 +73,54 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
), ),
ListTile( ListTile(
title: Text('Deezer'), title: Text('Deezer'.i18n),
leading: Icon(Icons.equalizer), leading: Icon(Icons.equalizer),
onTap: () => Navigator.push(context, MaterialPageRoute( onTap: () => Navigator.push(context, MaterialPageRoute(
builder: (context) => DeezerSettings() 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(), Divider(),
Text( Text(
_about, _about,
@ -97,22 +141,22 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Appearance'),), appBar: AppBar(title: Text('Appearance'.i18n),),
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
title: Text('Theme'), title: Text('Theme'.i18n),
subtitle: Text('Currently: ${settings.theme.toString().split('.').last}'), subtitle: Text('Currently'.i18n + ': ${settings.theme.toString().split('.').last}'),
leading: Icon(Icons.color_lens), leading: Icon(Icons.color_lens),
onTap: () { onTap: () {
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
return SimpleDialog( return SimpleDialog(
title: Text('Select theme'), title: Text('Select theme'.i18n),
children: <Widget>[ children: <Widget>[
SimpleDialogOption( SimpleDialogOption(
child: Text('Light (default)'), child: Text('Light (default)'.i18n),
onPressed: () { onPressed: () {
setState(() => settings.theme = Themes.Light); setState(() => settings.theme = Themes.Light);
settings.save(); settings.save();
@ -121,7 +165,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
}, },
), ),
SimpleDialogOption( SimpleDialogOption(
child: Text('Dark'), child: Text('Dark'.i18n),
onPressed: () { onPressed: () {
setState(() => settings.theme = Themes.Dark); setState(() => settings.theme = Themes.Dark);
settings.save(); settings.save();
@ -130,7 +174,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
}, },
), ),
SimpleDialogOption( SimpleDialogOption(
child: Text('Black (AMOLED)'), child: Text('Black (AMOLED)'.i18n),
onPressed: () { onPressed: () {
setState(() => settings.theme = Themes.Black); setState(() => settings.theme = Themes.Black);
settings.save(); settings.save();
@ -139,7 +183,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
}, },
), ),
SimpleDialogOption( SimpleDialogOption(
child: Text('Deezer (Dark)'), child: Text('Deezer (Dark)'.i18n),
onPressed: () { onPressed: () {
setState(() => settings.theme = Themes.Deezer); setState(() => settings.theme = Themes.Deezer);
settings.save(); settings.save();
@ -154,10 +198,10 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
}, },
), ),
ListTile( ListTile(
title: Text('Primary color'), title: Text('Primary color'.i18n),
leading: Icon(Icons.format_paint), leading: Icon(Icons.format_paint),
subtitle: Text( subtitle: Text(
'Selected color', 'Selected color'.i18n,
style: TextStyle( style: TextStyle(
color: settings.primaryColor color: settings.primaryColor
), ),
@ -167,7 +211,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
context: context, context: context,
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: Text('Primary color'), title: Text('Primary color'.i18n),
content: Container( content: Container(
height: 200, height: 200,
child: MaterialColorPicker( child: MaterialColorPicker(
@ -189,8 +233,8 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
}, },
), ),
ListTile( ListTile(
title: Text('Use album art primary color'), title: Text('Use album art primary color'.i18n),
subtitle: Text('Warning: might be buggy'), subtitle: Text('Warning: might be buggy'.i18n),
leading: Switch( leading: Switch(
value: settings.useArtColor, value: settings.useArtColor,
onChanged: (v) => setState(() => settings.updateUseArtColor(v)), onChanged: (v) => setState(() => settings.updateUseArtColor(v)),
@ -212,29 +256,29 @@ class _QualitySettingsState extends State<QualitySettings> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Quality'),), appBar: AppBar(title: Text('Quality'.i18n),),
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
title: Text('Mobile streaming'), title: Text('Mobile streaming'.i18n),
leading: Icon(Icons.network_cell), leading: Icon(Icons.network_cell),
), ),
QualityPicker('mobile'), QualityPicker('mobile'),
Divider(), Divider(),
ListTile( ListTile(
title: Text('Wifi streaming'), title: Text('Wifi streaming'.i18n),
leading: Icon(Icons.network_wifi), leading: Icon(Icons.network_wifi),
), ),
QualityPicker('wifi'), QualityPicker('wifi'),
Divider(), Divider(),
ListTile( ListTile(
title: Text('Offline'), title: Text('Offline'.i18n),
leading: Icon(Icons.offline_pin), leading: Icon(Icons.offline_pin),
), ),
QualityPicker('offline'), QualityPicker('offline'),
Divider(), Divider(),
ListTile( ListTile(
title: Text('External downloads'), title: Text('External downloads'.i18n),
leading: Icon(Icons.file_download), leading: Icon(Icons.file_download),
), ),
QualityPicker('download'), QualityPicker('download'),
@ -349,12 +393,12 @@ class _DeezerSettingsState extends State<DeezerSettings> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Deezer'),), appBar: AppBar(title: Text('Deezer'.i18n),),
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
title: Text('Content language'), title: Text('Content language'.i18n),
subtitle: Text('Not app language, used in headers. Now: ${settings.deezerLanguage}'), subtitle: Text('Not app language, used in headers. Now'.i18n + ': ${settings.deezerLanguage}'),
leading: Icon(Icons.language), leading: Icon(Icons.language),
onTap: () { onTap: () {
showDialog( showDialog(
@ -362,7 +406,7 @@ class _DeezerSettingsState extends State<DeezerSettings> {
builder: (context) => LanguagePickerDialog( builder: (context) => LanguagePickerDialog(
titlePadding: EdgeInsets.all(8.0), titlePadding: EdgeInsets.all(8.0),
isSearchable: true, isSearchable: true,
title: Text('Select language'), title: Text('Select language'.i18n),
onValuePicked: (Language language) { onValuePicked: (Language language) {
setState(() => settings.deezerLanguage = language.isoCode); setState(() => settings.deezerLanguage = language.isoCode);
settings.save(); settings.save();
@ -372,8 +416,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
}, },
), ),
ListTile( ListTile(
title: Text('Content country'), title: Text('Content country'.i18n),
subtitle: Text('Country used in headers. Now: ${settings.deezerCountry}'), subtitle: Text('Country used in headers. Now'.i18n + ': ${settings.deezerCountry}'),
leading: Icon(Icons.vpn_lock), leading: Icon(Icons.vpn_lock),
onTap: () { onTap: () {
showDialog( showDialog(
@ -390,8 +434,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
}, },
), ),
ListTile( ListTile(
title: Text('Log tracks'), title: Text('Log tracks'.i18n),
subtitle: Text('Send track listen logs to Deezer, enable it for features like Flow to work properly'), subtitle: Text('Send track listen logs to Deezer, enable it for features like Flow to work properly'.i18n),
leading: Checkbox( leading: Checkbox(
value: settings.logListen, value: settings.logListen,
onChanged: (bool v) { onChanged: (bool v) {
@ -415,12 +459,12 @@ class _GeneralSettingsState extends State<GeneralSettings> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('General'),), appBar: AppBar(title: Text('General'.i18n),),
body: ListView( body: ListView(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
title: Text('Offline mode'), title: Text('Offline mode'.i18n),
subtitle: Text('Will be overwritten on start.'), subtitle: Text('Will be overwritten on start.'.i18n),
leading: Switch( leading: Switch(
value: settings.offlineMode, value: settings.offlineMode,
onChanged: (bool v) { onChanged: (bool v) {
@ -436,7 +480,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
setState(() => settings.offlineMode = false); setState(() => settings.offlineMode = false);
} else { } else {
Fluttertoast.showToast( Fluttertoast.showToast(
msg: 'Error logging in, check your internet connections.', msg: 'Error logging in, check your internet connections.'.i18n,
gravity: ToastGravity.BOTTOM, gravity: ToastGravity.BOTTOM,
toastLength: Toast.LENGTH_SHORT toastLength: Toast.LENGTH_SHORT
); );
@ -444,7 +488,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
Navigator.of(context).pop(); Navigator.of(context).pop();
}); });
return AlertDialog( return AlertDialog(
title: Text('Logging in...'), title: Text('Logging in...'.i18n),
content: Row( content: Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -459,7 +503,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
), ),
), ),
ListTile( ListTile(
title: Text('Download path'), title: Text('Download path'.i18n),
leading: Icon(Icons.folder), leading: Icon(Icons.folder),
subtitle: Text(settings.downloadPath), subtitle: Text(settings.downloadPath),
onTap: () async { onTap: () async {
@ -474,8 +518,8 @@ class _GeneralSettingsState extends State<GeneralSettings> {
}, },
), ),
ListTile( ListTile(
title: Text('Downloads naming'), title: Text('Downloads naming'.i18n),
subtitle: Text('Currently: ${settings.downloadFilename}'), subtitle: Text('Currently'.i18n + ': ${settings.downloadFilename}'),
leading: Icon(Icons.text_format), leading: Icon(Icons.text_format),
onTap: () { onTap: () {
showDialog( showDialog(
@ -485,19 +529,21 @@ class _GeneralSettingsState extends State<GeneralSettings> {
TextEditingController _controller = TextEditingController(); TextEditingController _controller = TextEditingController();
String filename = settings.downloadFilename; String filename = settings.downloadFilename;
_controller.value = _controller.value.copyWith(text: filename); _controller.value = _controller.value.copyWith(text: filename);
String _new = _controller.value.text;
//Dialog with filename format //Dialog with filename format
return AlertDialog( return AlertDialog(
title: Text('Downloaded tracks filename'), title: Text('Downloaded tracks filename'.i18n),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TextField( TextField(
controller: _controller, controller: _controller,
onChanged: (String s) => _new = s,
), ),
Container(height: 8.0), Container(height: 8.0),
Text( Text(
'Valid variables are: %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%', 'Valid variables are'.i18n + ': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%',
style: TextStyle( style: TextStyle(
fontSize: 12.0, fontSize: 12.0,
), ),
@ -506,29 +552,30 @@ class _GeneralSettingsState extends State<GeneralSettings> {
), ),
actions: [ actions: [
FlatButton( FlatButton(
child: Text('Cancel'), child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
), ),
FlatButton( FlatButton(
child: Text('Reset'), child: Text('Reset'.i18n),
onPressed: () { onPressed: () {
_controller.value = _controller.value.copyWith( _controller.value = _controller.value.copyWith(
text: '%artists% - %title%' text: '%artists% - %title%'
); );
_new = '%artists% - %title%';
}, },
), ),
FlatButton( FlatButton(
child: Text('Clear'), child: Text('Clear'.i18n),
onPressed: () => _controller.clear(), onPressed: () => _controller.clear(),
), ),
FlatButton( FlatButton(
child: Text('Save'), child: Text('Save'.i18n),
onPressed: () { onPressed: () async {
setState(() { setState(() {
settings.downloadFilename = _controller.text; settings.downloadFilename = _new;
settings.save();
Navigator.of(context).pop();
}); });
await settings.save();
Navigator.of(context).pop();
}, },
) )
], ],
@ -538,7 +585,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
}, },
), ),
ListTile( ListTile(
title: Text('Create folders for artist'), title: Text('Create folders for artist'.i18n),
leading: Switch( leading: Switch(
value: settings.artistFolder, value: settings.artistFolder,
onChanged: (v) { onChanged: (v) {
@ -548,7 +595,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
), ),
), ),
ListTile( ListTile(
title: Text('Create folders for albums'), title: Text('Create folders for albums'.i18n),
leading: Switch( leading: Switch(
value: settings.albumFolder, value: settings.albumFolder,
onChanged: (v) { onChanged: (v) {
@ -558,7 +605,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
), ),
), ),
ListTile( ListTile(
title: Text('Separate albums by discs'), title: Text('Separate albums by discs'.i18n),
leading: Switch( leading: Switch(
value: settings.albumDiscFolder, value: settings.albumDiscFolder,
onChanged: (v) { onChanged: (v) {
@ -568,7 +615,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
), ),
), ),
ListTile( ListTile(
title: Text('Overwrite already downloaded files'), title: Text('Overwrite already downloaded files'.i18n),
leading: Switch( leading: Switch(
value: settings.overwriteDownload, value: settings.overwriteDownload,
onChanged: (v) { onChanged: (v) {
@ -578,40 +625,40 @@ class _GeneralSettingsState extends State<GeneralSettings> {
), ),
), ),
ListTile( ListTile(
title: Text('Copy ARL'), title: Text('Copy ARL'.i18n),
subtitle: Text('Copy userToken/ARL Cookie for use in other apps.'), subtitle: Text('Copy userToken/ARL Cookie for use in other apps.'.i18n),
leading: Icon(Icons.lock), leading: Icon(Icons.lock),
onTap: () async { onTap: () async {
await FlutterClipboard.copy(settings.arl); await FlutterClipboard.copy(settings.arl);
await Fluttertoast.showToast( await Fluttertoast.showToast(
msg: 'Copied', msg: 'Copied'.i18n,
); );
}, },
), ),
ListTile( 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), leading: Icon(Icons.exit_to_app),
onTap: () { onTap: () {
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: Text('Log out'), title: Text('Log out'.i18n),
content: Text('Due to plugin incompatibility, login using browser is unavailable without restart.'), content: Text('Due to plugin incompatibility, login using browser is unavailable without restart.'.i18n),
actions: <Widget>[ actions: <Widget>[
FlatButton( FlatButton(
child: Text('Cancel'), child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
), ),
FlatButton( FlatButton(
child: Text('(ARL ONLY) Continue'), child: Text('(ARL ONLY) Continue'.i18n),
onPressed: () { onPressed: () {
logOut(); logOut();
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
FlatButton( FlatButton(
child: Text('Log out & Exit'), child: Text('Log out & Exit'.i18n),
onPressed: () async { onPressed: () async {
try {AudioService.stop();} catch (e) {} try {AudioService.stop();} catch (e) {}
await logOut(); await logOut();
@ -656,7 +703,7 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Pick-a-Path'), title: Text('Pick-a-Path'.i18n),
actions: <Widget>[ actions: <Widget>[
IconButton( IconButton(
icon: Icon(Icons.sd_card), icon: Icon(Icons.sd_card),
@ -667,7 +714,7 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
context: context, context: context,
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: Text('Select storage'), title: Text('Select storage'.i18n),
content: FutureBuilder( content: FutureBuilder(
future: PathProviderEx.getStorageInfo(), future: PathProviderEx.getStorageInfo(),
builder: (context, snapshot) { builder: (context, snapshot) {
@ -736,13 +783,13 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
title: Text(_path), title: Text(_path),
), ),
ListTile( ListTile(
title: Text('Go up'), title: Text('Go up'.i18n),
leading: Icon(Icons.arrow_upward), leading: Icon(Icons.arrow_upward),
onTap: () { onTap: () {
setState(() { setState(() {
if (_root == _path) { if (_root == _path) {
Fluttertoast.showToast( Fluttertoast.showToast(
msg: 'Permission denied', msg: 'Permission denied'.i18n,
gravity: ToastGravity.BOTTOM gravity: ToastGravity.BOTTOM
); );
return; 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. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.2.0 version: 0.4.0+1
environment: environment:
sdk: ">=2.6.0 <3.0.0" sdk: ">=2.8.0 <3.0.0"
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations:
sdk: flutter
dio: ^3.0.9 dio: ^3.0.9
dio_cookie_manager: ^1.0.0 dio_cookie_manager: ^1.0.0
cookie_jar: ^1.0.1 cookie_jar: ^1.0.1
@ -60,8 +63,11 @@ dependencies:
flutter_cache_manager: ^1.4.1 flutter_cache_manager: ^1.4.1
cached_network_image: ^2.2.0+1 cached_network_image: ^2.2.0+1
clipboard: ^0.1.2+8 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: just_audio:
path: ./just_audio path: ./just_audio
@ -94,6 +100,7 @@ flutter:
- assets/cover.jpg - assets/cover.jpg
- assets/cover_thumb.jpg - assets/cover_thumb.jpg
- assets/icon.png - assets/icon.png
- assets/favorites_thumb.jpg
fonts: fonts:
# - family: Montserrat # - family: Montserrat