New git, updated dependencies
This commit is contained in:
parent
66bfd5eb70
commit
f49475e5a3
|
@ -2,7 +2,17 @@
|
|||
freezerkey.jsk
|
||||
android/key.properties
|
||||
|
||||
audio_service/.idea
|
||||
audio_service/.dart_tool
|
||||
|
||||
android/local.properties
|
||||
just_audio/
|
||||
.gradle/
|
||||
android/.gradle
|
||||
android/.idea
|
||||
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
|
||||
# Miscellaneous
|
||||
*.class
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[submodule "audio_service"]
|
||||
path = audio_service
|
||||
url = https://git.rip/freezer/audio_service
|
||||
url = https://git.freezer.life/exttex/audio_service.git
|
||||
[submodule "just_audio"]
|
||||
path = just_audio
|
||||
url = https://git.rip/freezer/just_audio
|
||||
url = https://git.freezer.life/exttex/just_audio.git
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit ff4f5f656adb8a66e6f0ab91966ee6e3532b4dc4
|
||||
Subproject commit e205269a86afc5a17715465664fe908dae8b82f6
|
|
@ -134,6 +134,16 @@ class DeezerAPI {
|
|||
}
|
||||
}
|
||||
|
||||
//Check if Deezer available in country
|
||||
static Future<bool> chceckAvailability() async {
|
||||
try {
|
||||
http.Response res = await http.get("https://api.deezer.com/infos");
|
||||
return jsonDecode(res.body)["open"];
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
//Search
|
||||
Future<SearchResults> search(String query) async {
|
||||
Map<dynamic, dynamic> data = await callApi('deezer.pageSearch', params: {
|
||||
|
|
|
@ -1,21 +1,14 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:pointycastle/api.dart';
|
||||
import 'package:pointycastle/block/aes_fast.dart';
|
||||
import 'package:pointycastle/block/modes/ecb.dart';
|
||||
import 'package:hex/hex.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
|
||||
import 'dart:typed_data';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
part 'definitions.g.dart';
|
||||
|
||||
|
|
|
@ -1,804 +0,0 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:disk_space/disk_space.dart';
|
||||
import 'package:ext_storage/ext_storage.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:random_string/random_string.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:filesize/filesize.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'dart:async';
|
||||
import 'deezer.dart';
|
||||
import '../settings.dart';
|
||||
import 'definitions.dart';
|
||||
import '../ui/cached_image.dart';
|
||||
|
||||
DownloadManager downloadManager = DownloadManager();
|
||||
MethodChannel platformChannel = const MethodChannel('f.f.freezer/native');
|
||||
|
||||
class DownloadManager {
|
||||
|
||||
Database db;
|
||||
List<Download> queue = [];
|
||||
String _offlinePath;
|
||||
Future _download;
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
|
||||
bool _cancelNotifications = true;
|
||||
|
||||
bool stopped = true;
|
||||
|
||||
Future init() async {
|
||||
//Prepare DB
|
||||
String dir = await getDatabasesPath();
|
||||
String path = p.join(dir, 'offline.db');
|
||||
db = await openDatabase(
|
||||
path,
|
||||
version: 1,
|
||||
onCreate: (Database db, int version) async {
|
||||
Batch b = db.batch();
|
||||
//Create tables
|
||||
b.execute(""" CREATE TABLE downloads (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT, url TEXT, private INTEGER, state INTEGER, trackId TEXT)""");
|
||||
b.execute("""CREATE TABLE tracks (
|
||||
id TEXT PRIMARY KEY, title TEXT, album TEXT, artists TEXT, duration INTEGER, albumArt TEXT, trackNumber INTEGER, offline INTEGER, lyrics TEXT, favorite INTEGER)""");
|
||||
b.execute("""CREATE TABLE albums (
|
||||
id TEXT PRIMARY KEY, title TEXT, artists TEXT, tracks TEXT, art TEXT, fans INTEGER, offline INTEGER, library INTEGER)""");
|
||||
b.execute("""CREATE TABLE artists (
|
||||
id TEXT PRIMARY KEY, name TEXT, albums TEXT, topTracks TEXT, picture TEXT, fans INTEGER, albumCount INTEGER, offline INTEGER, library INTEGER)""");
|
||||
b.execute("""CREATE TABLE playlists (
|
||||
id TEXT PRIMARY KEY, title TEXT, tracks TEXT, image TEXT, duration INTEGER, userId TEXT, userName TEXT, fans INTEGER, library INTEGER, description TEXT)""");
|
||||
await b.commit();
|
||||
}
|
||||
);
|
||||
//Prepare folders (/sdcard/Android/data/freezer/data/)
|
||||
_offlinePath = p.join((await getExternalStorageDirectory()).path, 'offline/');
|
||||
await Directory(_offlinePath).create(recursive: true);
|
||||
|
||||
//Notifications
|
||||
await _prepareNotifications();
|
||||
|
||||
//Restore
|
||||
List<Map> downloads = await db.rawQuery("SELECT * FROM downloads INNER JOIN tracks ON tracks.id = downloads.trackId WHERE downloads.state = 0");
|
||||
downloads.forEach((download) => queue.add(Download.fromSQL(download, parseTrack: true)));
|
||||
}
|
||||
|
||||
//Initialize flutter local notification plugin
|
||||
Future _prepareNotifications() async {
|
||||
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||
AndroidInitializationSettings androidInitializationSettings = AndroidInitializationSettings('@drawable/ic_logo');
|
||||
InitializationSettings initializationSettings = InitializationSettings(androidInitializationSettings, null);
|
||||
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
|
||||
}
|
||||
|
||||
//Show download progress notification, if now/total = null, show intermediate
|
||||
Future _startProgressNotification() async {
|
||||
_cancelNotifications = false;
|
||||
Timer.periodic(Duration(milliseconds: 500), (timer) async {
|
||||
//Cancel notifications
|
||||
if (_cancelNotifications) {
|
||||
flutterLocalNotificationsPlugin.cancel(10);
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
//Not downloading
|
||||
if (this.queue.length <= 0) return;
|
||||
Download d = queue[0];
|
||||
//Prepare and show notification
|
||||
AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails(
|
||||
'download', 'Download', 'Download',
|
||||
importance: Importance.Default,
|
||||
priority: Priority.Default,
|
||||
showProgress: true,
|
||||
maxProgress: d.total??1,
|
||||
progress: d.received??1,
|
||||
playSound: false,
|
||||
enableVibration: false,
|
||||
autoCancel: true,
|
||||
//ongoing: true, //Allow dismissing
|
||||
indeterminate: (d.total == null || d.total == d.received),
|
||||
onlyAlertOnce: true
|
||||
);
|
||||
NotificationDetails notificationDetails = NotificationDetails(androidNotificationDetails, null);
|
||||
await downloadManager.flutterLocalNotificationsPlugin.show(
|
||||
10,
|
||||
'Downloading: ${d.track.title}',
|
||||
(d.state == DownloadState.POST) ? 'Post processing...' : '${filesize(d.received)} / ${filesize(d.total)} (${queue.length} in queue)',
|
||||
notificationDetails
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
//Update queue, start new download
|
||||
void updateQueue() async {
|
||||
if (_download == null && queue.length > 0 && !stopped) {
|
||||
_download = queue[0].download(
|
||||
onDone: () async {
|
||||
//On download finished
|
||||
await db.rawUpdate('UPDATE downloads SET state = 1 WHERE trackId = ?', [queue[0].track.id]);
|
||||
queue.removeAt(0);
|
||||
_download = null;
|
||||
//Remove notification if no more downloads
|
||||
if (queue.length == 0) {
|
||||
_cancelNotifications = true;
|
||||
}
|
||||
updateQueue();
|
||||
}
|
||||
).catchError((e, st) async {
|
||||
if (stopped) return;
|
||||
_cancelNotifications = true;
|
||||
|
||||
//Deezer error - track is unavailable
|
||||
if (queue[0].state == DownloadState.DEEZER_ERROR) {
|
||||
await db.rawUpdate('UPDATE downloads SET state = 4 WHERE trackId = ?', [queue[0].track.id]);
|
||||
queue.removeAt(0);
|
||||
_cancelNotifications = false;
|
||||
_download = null;
|
||||
updateQueue();
|
||||
return;
|
||||
}
|
||||
|
||||
//Clean
|
||||
_download = null;
|
||||
stopped = true;
|
||||
print('Download error: $e\n$st');
|
||||
|
||||
queue[0].state = DownloadState.NONE;
|
||||
//Shift to end
|
||||
queue.add(queue[0]);
|
||||
queue.removeAt(0);
|
||||
//Show error
|
||||
await _showError();
|
||||
});
|
||||
//Show download progress notifications
|
||||
if (_cancelNotifications == null || _cancelNotifications) _startProgressNotification();
|
||||
}
|
||||
}
|
||||
|
||||
//Stop downloading and end my life
|
||||
Future stop() async {
|
||||
stopped = true;
|
||||
if (_download != null) {
|
||||
await queue[0].stop();
|
||||
}
|
||||
_download = null;
|
||||
}
|
||||
|
||||
//Start again downloads
|
||||
Future start() async {
|
||||
if (_download != null) return;
|
||||
stopped = false;
|
||||
updateQueue();
|
||||
}
|
||||
|
||||
//Show error notification
|
||||
Future _showError() async {
|
||||
AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails(
|
||||
'downloadError', 'Download Error', 'Download Error'
|
||||
);
|
||||
NotificationDetails notificationDetails = NotificationDetails(androidNotificationDetails, null);
|
||||
flutterLocalNotificationsPlugin.show(
|
||||
11, 'Error while downloading!', 'Please restart downloads in the library', notificationDetails
|
||||
);
|
||||
}
|
||||
|
||||
//Returns all offline tracks
|
||||
Future<List<Track>> allOfflineTracks() async {
|
||||
List data = await db.query('tracks', where: 'offline == 1');
|
||||
List<Track> tracks = [];
|
||||
//Load track data
|
||||
for (var t in data) {
|
||||
tracks.add(await getTrack(t['id']));
|
||||
}
|
||||
return tracks;
|
||||
}
|
||||
|
||||
//Get all offline playlists
|
||||
Future<List<Playlist>> getOfflinePlaylists() async {
|
||||
List data = await db.query('playlists');
|
||||
List<Playlist> playlists = [];
|
||||
//Load playlists
|
||||
for (var p in data) {
|
||||
playlists.add(await getPlaylist(p['id']));
|
||||
}
|
||||
return playlists;
|
||||
}
|
||||
|
||||
//Get playlist metadata with tracks
|
||||
Future<Playlist> getPlaylist(String id) async {
|
||||
if (id == null) return null;
|
||||
List data = await db.query('playlists', where: 'id == ?', whereArgs: [id]);
|
||||
if (data.length == 0) return null;
|
||||
//Load playlist tracks
|
||||
Playlist p = Playlist.fromSQL(data[0]);
|
||||
for (int i=0; i<p.tracks.length; i++) {
|
||||
p.tracks[i] = await getTrack(p.tracks[i].id);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
//Gets favorites
|
||||
Future<Playlist> getFavorites() async {
|
||||
return await getPlaylist('FAVORITES');
|
||||
}
|
||||
|
||||
Future<List<Album>> getOfflineAlbums({List albumsData}) async {
|
||||
//Load albums
|
||||
if (albumsData == null) {
|
||||
albumsData = await db.query('albums', where: 'offline == 1');
|
||||
}
|
||||
List<Album> albums = albumsData.map((alb) => Album.fromSQL(alb)).toList();
|
||||
for(int i=0; i<albums.length; i++) {
|
||||
albums[i].library = true;
|
||||
//Load tracks
|
||||
for(int j=0; j<albums[i].tracks.length; j++) {
|
||||
albums[i].tracks[j] = await getTrack(albums[i].tracks[j].id, album: albums[i]);
|
||||
}
|
||||
//Load artists
|
||||
List artistsData = await db.rawQuery('SELECT * FROM artists WHERE id IN (${albumsData[i]['artists']})');
|
||||
albums[i].artists = artistsData.map<Artist>((a) => Artist.fromSQL(a)).toList();
|
||||
}
|
||||
return albums;
|
||||
}
|
||||
|
||||
//Get track with metadata from db
|
||||
Future<Track> getTrack(String id, {Album album, List<Artist> artists}) async {
|
||||
List tracks = await db.query('tracks', where: 'id == ?', whereArgs: [id]);
|
||||
if (tracks.length == 0) return null;
|
||||
Track t = Track.fromSQL(tracks[0]);
|
||||
//Load album from DB
|
||||
t.album = album ?? Album.fromSQL((await db.query('albums', where: 'id == ?', whereArgs: [t.album.id]))[0]);
|
||||
if (artists != null) {
|
||||
t.artists = artists;
|
||||
return t;
|
||||
}
|
||||
//Load artists from DB
|
||||
for (int i=0; i<t.artists.length; i++) {
|
||||
t.artists[i] = Artist.fromSQL(
|
||||
(await db.query('artists', where: 'id == ?', whereArgs: [t.artists[i].id]))[0]);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
Future removeOfflineTrack(String id) async {
|
||||
//Check if track present in albums
|
||||
List counter = await db.rawQuery('SELECT COUNT(*) FROM albums WHERE tracks LIKE "%$id%"');
|
||||
if (counter[0]['COUNT(*)'] > 0) return;
|
||||
//and in playlists
|
||||
counter = await db.rawQuery('SELECT COUNT(*) FROM playlists WHERE tracks LIKE "%$id%"');
|
||||
if (counter[0]['COUNT(*)'] > 0) return;
|
||||
//Remove file
|
||||
List download = await db.query('downloads', where: 'trackId == ?', whereArgs: [id]);
|
||||
await File(download[0]['path']).delete();
|
||||
//Delete from db
|
||||
await db.delete('tracks', where: 'id == ?', whereArgs: [id]);
|
||||
await db.delete('downloads', where: 'trackId == ?', whereArgs: [id]);
|
||||
}
|
||||
|
||||
//Delete offline album
|
||||
Future removeOfflineAlbum(String id) async {
|
||||
List data = await db.rawQuery('SELECT * FROM albums WHERE id == ? AND offline == 1', [id]);
|
||||
if (data.length == 0) return;
|
||||
Map<String, dynamic> album = Map.from(data[0]); //make writable
|
||||
//Remove DB
|
||||
album['offline'] = 0;
|
||||
await db.update('albums', album, where: 'id == ?', whereArgs: [id]);
|
||||
//Get track ids
|
||||
List<String> tracks = album['tracks'].split(',');
|
||||
for (String t in tracks) {
|
||||
//Remove tracks
|
||||
await removeOfflineTrack(t);
|
||||
}
|
||||
}
|
||||
|
||||
Future removeOfflinePlaylist(String id) async {
|
||||
List data = await db.query('playlists', where: 'id == ?', whereArgs: [id]);
|
||||
if (data.length == 0) return;
|
||||
Playlist p = Playlist.fromSQL(data[0]);
|
||||
//Remove db
|
||||
await db.delete('playlists', where: 'id == ?', whereArgs: [id]);
|
||||
//Remove tracks
|
||||
for(Track t in p.tracks) {
|
||||
await removeOfflineTrack(t.id);
|
||||
}
|
||||
}
|
||||
|
||||
//Get path to offline track
|
||||
Future<String> getOfflineTrackPath(String id) async {
|
||||
List<Map> tracks = await db.rawQuery('SELECT path FROM downloads WHERE state == 1 AND trackId == ?', [id]);
|
||||
if (tracks.length < 1) {
|
||||
return null;
|
||||
}
|
||||
Download d = Download.fromSQL(tracks[0]);
|
||||
return d.path;
|
||||
}
|
||||
|
||||
Future addOfflineTrack(Track track, {private = true, forceStart = true}) async {
|
||||
//Paths
|
||||
String path = p.join(_offlinePath, track.id);
|
||||
if (track.playbackDetails == null) {
|
||||
//Get track from API if download info missing
|
||||
track = await deezerAPI.track(track.id);
|
||||
}
|
||||
|
||||
if (!private) {
|
||||
//Check permissions
|
||||
if (!(await Permission.storage.request().isGranted)) {
|
||||
return;
|
||||
}
|
||||
//If saving to external
|
||||
//Save just extension to path, will be generated before download
|
||||
path = 'mp3';
|
||||
if (settings.downloadQuality == AudioQuality.FLAC) {
|
||||
path = 'flac';
|
||||
}
|
||||
} else {
|
||||
//Load lyrics for private
|
||||
try {
|
||||
Lyrics l = await deezerAPI.lyrics(track.id);
|
||||
track.lyrics = l;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
Download download = Download(track: track, path: path, private: private);
|
||||
//Database
|
||||
Batch b = db.batch();
|
||||
b.insert('downloads', download.toSQL());
|
||||
b.insert('tracks', track.toSQL(off: false), conflictAlgorithm: ConflictAlgorithm.ignore);
|
||||
|
||||
if (private) {
|
||||
//Duplicate check
|
||||
List<Map> duplicate = await db.rawQuery('SELECT * FROM downloads WHERE trackId == ?', [track.id]);
|
||||
if (duplicate.length != 0) return;
|
||||
//Save art
|
||||
//await imagesDatabase.getImage(track.albumArt.full);
|
||||
imagesDatabase.saveImage(track.albumArt.full);
|
||||
//Save to db
|
||||
b.insert('tracks', track.toSQL(off: true), conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
b.insert('albums', track.album.toSQL(), conflictAlgorithm: ConflictAlgorithm.ignore);
|
||||
track.artists.forEach((art) => b.insert('artists', art.toSQL(), conflictAlgorithm: ConflictAlgorithm.ignore));
|
||||
}
|
||||
await b.commit();
|
||||
|
||||
queue.add(download);
|
||||
if (forceStart) start();
|
||||
}
|
||||
|
||||
Future addOfflineAlbum(Album album, {private = true}) async {
|
||||
//Get full album from API if tracks are missing
|
||||
if (album.tracks == null || album.tracks.length == 0) {
|
||||
album = await deezerAPI.album(album.id);
|
||||
}
|
||||
//Update album in database
|
||||
if (private) {
|
||||
await db.insert('albums', album.toSQL(off: true), conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
//Save all tracks
|
||||
for (Track track in album.tracks) {
|
||||
await addOfflineTrack(track, private: private, forceStart: false);
|
||||
}
|
||||
start();
|
||||
}
|
||||
|
||||
//Add offline playlist, can be also used as update
|
||||
Future addOfflinePlaylist(Playlist playlist, {private = true}) async {
|
||||
//Load full playlist if missing tracks
|
||||
if (playlist.tracks == null || playlist.tracks.length != playlist.trackCount) {
|
||||
playlist = await deezerAPI.fullPlaylist(playlist.id);
|
||||
}
|
||||
playlist.library = true;
|
||||
//To DB
|
||||
if (private) {
|
||||
await db.insert('playlists', playlist.toSQL(), conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
//Download all tracks
|
||||
for (Track t in playlist.tracks) {
|
||||
await addOfflineTrack(t, private: private, forceStart: false);
|
||||
}
|
||||
start();
|
||||
}
|
||||
|
||||
|
||||
Future checkOffline({Album album, Track track, Playlist playlist}) async {
|
||||
//Check if album/track (TODO: Artist, playlist) is offline
|
||||
if (track != null) {
|
||||
List res = await db.query('tracks', where: 'id == ? AND offline == 1', whereArgs: [track.id]);
|
||||
if (res.length == 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (album != null) {
|
||||
List res = await db.query('albums', where: 'id == ? AND offline == 1', whereArgs: [album.id]);
|
||||
if (res.length == 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (playlist != null && playlist.id != null) {
|
||||
List res = await db.query('playlists', where: 'id == ?', whereArgs: [playlist.id]);
|
||||
if (res.length == 0) return false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//Offline search
|
||||
Future<SearchResults> search(String query) async {
|
||||
SearchResults results = SearchResults(
|
||||
tracks: [],
|
||||
albums: [],
|
||||
artists: [],
|
||||
playlists: []
|
||||
);
|
||||
//Tracks
|
||||
List tracksData = await db.rawQuery('SELECT * FROM tracks WHERE offline == 1 AND title like "%$query%"');
|
||||
for (Map trackData in tracksData) {
|
||||
results.tracks.add(await getTrack(trackData['id']));
|
||||
}
|
||||
//Albums
|
||||
List albumsData = await db.rawQuery('SELECT * FROM albums WHERE offline == 1 AND title like "%$query%"');
|
||||
results.albums = await getOfflineAlbums(albumsData: albumsData);
|
||||
//Artists
|
||||
//TODO: offline artists
|
||||
//Playlists
|
||||
List playlists = await db.rawQuery('SELECT * FROM playlists WHERE title like "%$query%"');
|
||||
for (Map playlist in playlists) {
|
||||
results.playlists.add(await getPlaylist(playlist['id']));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
Future<List<Download>> getFinishedDownloads() async {
|
||||
//Fetch from db
|
||||
List<Map> data = await db.rawQuery("SELECT * FROM downloads INNER JOIN tracks ON tracks.id = downloads.trackId WHERE downloads.state = 1 OR downloads.state > 3");
|
||||
List<Download> downloads = data.map<Download>((d) => Download.fromSQL(d, parseTrack: true)).toList();
|
||||
return downloads;
|
||||
}
|
||||
|
||||
//Get stats for library screen
|
||||
Future<List<String>> getStats() async {
|
||||
//Get offline counts
|
||||
int trackCount = (await db.rawQuery('SELECT COUNT(*) FROM tracks WHERE offline == 1'))[0]['COUNT(*)'];
|
||||
int albumCount = (await db.rawQuery('SELECT COUNT(*) FROM albums WHERE offline == 1'))[0]['COUNT(*)'];
|
||||
int playlistCount = (await db.rawQuery('SELECT COUNT(*) FROM albums WHERE offline == 1'))[0]['COUNT(*)'];
|
||||
//Free space
|
||||
double diskSpace = await DiskSpace.getFreeDiskSpace;
|
||||
|
||||
//Used space
|
||||
List<FileSystemEntity> offlineStat = await Directory(_offlinePath).list().toList();
|
||||
int offlineSize = 0;
|
||||
for (var fs in offlineStat) {
|
||||
offlineSize += (await fs.stat()).size;
|
||||
}
|
||||
|
||||
//Return as a list, maybe refactor in future if feature stays
|
||||
return ([
|
||||
trackCount.toString(),
|
||||
albumCount.toString(),
|
||||
playlistCount.toString(),
|
||||
filesize(offlineSize),
|
||||
filesize((diskSpace * 1000000).floor())
|
||||
]);
|
||||
}
|
||||
|
||||
//Delete download from db
|
||||
Future removeDownload(Download download) async {
|
||||
await db.delete('downloads', where: 'trackId == ?', whereArgs: [download.track.id]);
|
||||
queue.removeWhere((d) => d.track.id == download.track.id);
|
||||
//TODO: remove files for downloaded
|
||||
}
|
||||
|
||||
//Delete queue
|
||||
Future clearQueue() async {
|
||||
while (queue.length > 0) {
|
||||
if (queue.length == 1) {
|
||||
if (_download != null) break;
|
||||
await removeDownload(queue[0]);
|
||||
return;
|
||||
}
|
||||
await removeDownload(queue[1]);
|
||||
}
|
||||
}
|
||||
|
||||
//Remove non-private downloads
|
||||
Future cleanDownloadHistory() async {
|
||||
await db.delete('downloads', where: 'private == 0');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Download {
|
||||
Track track;
|
||||
String path;
|
||||
String url;
|
||||
bool private;
|
||||
DownloadState state;
|
||||
String _cover;
|
||||
|
||||
//For canceling
|
||||
IOSink _outSink;
|
||||
CancelToken _cancel;
|
||||
StreamSubscription _progressSub;
|
||||
|
||||
int received = 0;
|
||||
int total = 1;
|
||||
|
||||
Download({this.track, this.path, this.url, this.private, this.state = DownloadState.NONE});
|
||||
|
||||
//Stop download
|
||||
Future stop() async {
|
||||
if (_cancel != null) _cancel.cancel();
|
||||
//if (_outSink != null) _outSink.close();
|
||||
if (_progressSub != null) _progressSub.cancel();
|
||||
|
||||
received = 0;
|
||||
total = 1;
|
||||
state = DownloadState.NONE;
|
||||
}
|
||||
|
||||
Future download({onDone}) async {
|
||||
Dio dio = Dio();
|
||||
|
||||
//TODO: Check for internet before downloading
|
||||
|
||||
Map rawTrackPublic = {};
|
||||
Map rawAlbumPublic = {};
|
||||
if (!this.private && !(this.path.endsWith('.mp3') || this.path.endsWith('.flac'))) {
|
||||
String ext = this.path;
|
||||
//Get track details
|
||||
Map _rawTrackData = await deezerAPI.callApi('song.getListData', params: {'sng_ids': [track.id]});
|
||||
Map rawTrack = _rawTrackData['results']['data'][0];
|
||||
this.track = Track.fromPrivateJson(rawTrack);
|
||||
//RAW Public API call (for genre and other tags)
|
||||
try {rawTrackPublic = await deezerAPI.callPublicApi('track/${this.track.id}');} catch (e) {rawTrackPublic = {};}
|
||||
try {rawAlbumPublic = await deezerAPI.callPublicApi('album/${this.track.album.id}');} catch (e) {rawAlbumPublic = {};}
|
||||
|
||||
//Global block check
|
||||
if (rawTrackPublic['available_countries'] != null && rawTrackPublic['available_countries'].length == 0) {
|
||||
this.state = DownloadState.DEEZER_ERROR;
|
||||
throw Exception('Download error - not on Deezer');
|
||||
}
|
||||
|
||||
//Get path if public
|
||||
RegExp sanitize = RegExp(r'[\/\\\?\%\*\:\|\"\<\>]');
|
||||
//Download path
|
||||
this.path = settings.downloadPath ?? (await ExtStorage.getExternalStoragePublicDirectory(ExtStorage.DIRECTORY_MUSIC));
|
||||
if (settings.artistFolder)
|
||||
this.path = p.join(this.path, track.artists[0].name.replaceAll(sanitize, ''));
|
||||
if (settings.albumFolder) {
|
||||
String folderName = track.album.title.replaceAll(sanitize, '');
|
||||
//Add disk number
|
||||
if (settings.albumDiscFolder) folderName += ' - Disk ${track.diskNumber}';
|
||||
|
||||
this.path = p.join(this.path, folderName);
|
||||
}
|
||||
//Make dirs
|
||||
await Directory(this.path).create(recursive: true);
|
||||
|
||||
//Grab cover
|
||||
_cover = p.join(this.path, 'cover.jpg');
|
||||
if (!settings.albumFolder) _cover = p.join(this.path, randomAlpha(12) + '_cover.jpg');
|
||||
|
||||
if (!await File(_cover).exists()) {
|
||||
try {
|
||||
await dio.download(
|
||||
this.track.albumArt.full,
|
||||
_cover,
|
||||
);
|
||||
} catch (e) {print('Error downloading cover');}
|
||||
}
|
||||
|
||||
//Create filename
|
||||
String _filename = settings.downloadFilename;
|
||||
//Feats filter
|
||||
String feats = '';
|
||||
if (track.artists.length > 1) feats = "feat. ${track.artists.sublist(1).map((a) => a.name).join(', ')}";
|
||||
//Filters
|
||||
Map<String, String> vars = {
|
||||
'%artists%': track.artistString.replaceAll(sanitize, ''),
|
||||
'%artist%': track.artists[0].name.replaceAll(sanitize, ''),
|
||||
'%title%': track.title.replaceAll(sanitize, ''),
|
||||
'%album%': track.album.title.replaceAll(sanitize, ''),
|
||||
'%trackNumber%': track.trackNumber.toString(),
|
||||
'%0trackNumber%': track.trackNumber.toString().padLeft(2, '0'),
|
||||
'%feats%': feats
|
||||
};
|
||||
//Replace
|
||||
vars.forEach((key, value) {
|
||||
_filename = _filename.replaceAll(key, value);
|
||||
});
|
||||
_filename += '.$ext';
|
||||
|
||||
this.path = p.join(this.path, _filename);
|
||||
}
|
||||
|
||||
//Check if file exists
|
||||
if (await File(this.path).exists() && !settings.overwriteDownload) {
|
||||
this.state = DownloadState.DONE;
|
||||
onDone();
|
||||
return;
|
||||
}
|
||||
|
||||
//Download
|
||||
this.state = DownloadState.DOWNLOADING;
|
||||
|
||||
//Quality fallback
|
||||
if (this.url == null)
|
||||
await _fallback();
|
||||
|
||||
//Create download file
|
||||
File downloadFile = File(this.path + '.ENC');
|
||||
//Get start position
|
||||
int start = 0;
|
||||
if (await downloadFile.exists()) {
|
||||
FileStat stat = await downloadFile.stat();
|
||||
start = stat.size;
|
||||
} else {
|
||||
//Create file if doesn't exist
|
||||
await downloadFile.create(recursive: true);
|
||||
}
|
||||
|
||||
//Download
|
||||
_cancel = CancelToken();
|
||||
Response response;
|
||||
try {
|
||||
response = await dio.get(
|
||||
this.url,
|
||||
options: Options(
|
||||
responseType: ResponseType.stream,
|
||||
headers: {
|
||||
'Range': 'bytes=$start-'
|
||||
},
|
||||
),
|
||||
cancelToken: _cancel
|
||||
);
|
||||
} on DioError catch (e) {
|
||||
//Deezer fetch error
|
||||
if (e.response.statusCode == 403 || e.response.statusCode == 404) {
|
||||
this.state = DownloadState.DEEZER_ERROR;
|
||||
}
|
||||
throw Exception('Download error - Deezer blocked.');
|
||||
}
|
||||
|
||||
//Size
|
||||
this.total = int.parse(response.headers['Content-Length'][0]) + start;
|
||||
this.received = start;
|
||||
//Save
|
||||
_outSink = downloadFile.openWrite(mode: FileMode.append);
|
||||
Stream<Uint8List> _data = response.data.stream.asBroadcastStream();
|
||||
_progressSub = _data.listen((Uint8List c) {
|
||||
this.received += c.length;
|
||||
});
|
||||
//Pipe to file
|
||||
try {
|
||||
await _outSink.addStream(_data);
|
||||
} catch (e) {
|
||||
await _outSink.close();
|
||||
throw Exception('Download error');
|
||||
}
|
||||
await _outSink.close();
|
||||
_cancel = null;
|
||||
|
||||
this.state = DownloadState.POST;
|
||||
//Decrypt
|
||||
await platformChannel.invokeMethod('decryptTrack', {'id': track.id, 'path': path});
|
||||
//Tag
|
||||
if (!private) {
|
||||
//Tag track in native
|
||||
String year;
|
||||
if (rawTrackPublic['release_date'] != null && rawTrackPublic['release_date'].length >= 4)
|
||||
year = rawTrackPublic['release_date'].substring(0, 4);
|
||||
|
||||
await platformChannel.invokeMethod('tagTrack', {
|
||||
'path': path,
|
||||
'title': track.title,
|
||||
'album': track.album.title,
|
||||
'artists': track.artistString,
|
||||
'artist': track.artists[0].name,
|
||||
'cover': _cover,
|
||||
'trackNumber': track.trackNumber,
|
||||
'diskNumber': track.diskNumber,
|
||||
'genres': ((rawAlbumPublic['genres']??{})['data']??[]).map((g) => g['name']).toList(),
|
||||
'year': year,
|
||||
'bpm': rawTrackPublic['bpm'],
|
||||
'explicit': (track.explicit??false) ? "1":"0",
|
||||
'label': rawAlbumPublic['label'],
|
||||
'albumTracks': rawAlbumPublic['nb_tracks'],
|
||||
'date': rawTrackPublic['release_date'],
|
||||
'albumArtist': (rawAlbumPublic['artist']??{})['name']
|
||||
});
|
||||
//Rescan android library
|
||||
await platformChannel.invokeMethod('rescanLibrary', {
|
||||
'path': path
|
||||
});
|
||||
}
|
||||
//Remove encrypted
|
||||
await File(path + '.ENC').delete();
|
||||
if (!settings.albumFolder) await File(_cover).delete();
|
||||
|
||||
//Get lyrics
|
||||
Lyrics lyrics;
|
||||
try {
|
||||
lyrics = await deezerAPI.lyrics(track.id);
|
||||
} catch (e) {}
|
||||
if (lyrics != null && lyrics.lyrics != null) {
|
||||
//Create .LRC file
|
||||
String lrcPath = p.join(p.dirname(path), p.basenameWithoutExtension(path)) + '.lrc';
|
||||
File lrcFile = File(lrcPath);
|
||||
String lrcData = '';
|
||||
//Generate file
|
||||
lrcData += '[ar:${track.artistString}]\r\n';
|
||||
lrcData += '[al:${track.album.title}]\r\n';
|
||||
lrcData += '[ti:${track.title}]\r\n';
|
||||
for (Lyric l in lyrics.lyrics) {
|
||||
if (l.lrcTimestamp != null && l.lrcTimestamp != '' && l.text != null)
|
||||
lrcData += '${l.lrcTimestamp}${l.text}\r\n';
|
||||
}
|
||||
lrcFile.writeAsString(lrcData);
|
||||
}
|
||||
|
||||
this.state = DownloadState.DONE;
|
||||
onDone();
|
||||
return;
|
||||
}
|
||||
|
||||
Future _fallback({fallback}) async {
|
||||
//Get quality
|
||||
AudioQuality quality = private ? settings.offlineQuality : settings.downloadQuality;
|
||||
if (fallback == AudioQuality.MP3_320) quality = AudioQuality.MP3_128;
|
||||
if (fallback == AudioQuality.FLAC) {
|
||||
quality = AudioQuality.MP3_320;
|
||||
if (this.path.toLowerCase().endsWith('flac'))
|
||||
this.path = this.path.substring(0, this.path.length - 4) + 'mp3';
|
||||
}
|
||||
|
||||
//No more fallback
|
||||
if (quality == AudioQuality.MP3_128) {
|
||||
url = track.getUrl(settings.getQualityInt(quality));
|
||||
return;
|
||||
}
|
||||
|
||||
//Check
|
||||
int q = settings.getQualityInt(quality);
|
||||
try {
|
||||
Response res = await Dio().head(track.getUrl(q));
|
||||
if (res.statusCode == 200 || res.statusCode == 206) {
|
||||
this.url = track.getUrl(q);
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
//Fallback
|
||||
return _fallback(fallback: quality);
|
||||
}
|
||||
|
||||
//JSON
|
||||
Map<String, dynamic> toSQL() => {
|
||||
'trackId': track.id,
|
||||
'path': path,
|
||||
'url': url,
|
||||
'state': state.index,
|
||||
'private': private?1:0
|
||||
};
|
||||
factory Download.fromSQL(Map<String, dynamic> data, {parseTrack = false}) => Download(
|
||||
track: parseTrack?Track.fromSQL(data):Track(id: data['trackId']),
|
||||
path: data['path'],
|
||||
url: data['url'],
|
||||
state: DownloadState.values[data['state']],
|
||||
private: data['private'] == 1
|
||||
);
|
||||
}
|
||||
|
||||
enum DownloadState {
|
||||
NONE,
|
||||
DONE,
|
||||
DOWNLOADING,
|
||||
POST,
|
||||
DEEZER_ERROR,
|
||||
ERROR
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
import 'package:equalizer/equalizer.dart';
|
||||
|
@ -21,6 +19,8 @@ import '../settings.dart';
|
|||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
|
||||
PlayerHelper playerHelper = PlayerHelper();
|
||||
|
||||
|
@ -776,9 +776,9 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
_visualizerSubscription = _player.visualizerFftStream.listen((event) {
|
||||
//Calculate actual values
|
||||
List<double> out = [];
|
||||
for (int i=0; i<event.data.length/2; i++) {
|
||||
int rfk = event.data[i*2].toSigned(8);
|
||||
int ifk = event.data[i*2+1].toSigned(8);
|
||||
for (int i=0; i<event.length/2; i++) {
|
||||
int rfk = event[i*2].toSigned(8);
|
||||
int ifk = event[i*2+1].toSigned(8);
|
||||
out.add(log(hypot(rfk, ifk) + 1) / 5.2);
|
||||
}
|
||||
AudioServiceBackground.sendCustomEvent({"action": "visualizer", "data": out});
|
||||
|
|
|
@ -338,6 +338,11 @@ const language_en_us = {
|
|||
"Unsynchronized lyrics": "Unsynchronized lyrics",
|
||||
"Genre": "Genre",
|
||||
"Contributors": "Contributors",
|
||||
"Album art": "Album art"
|
||||
"Album art": "Album art",
|
||||
|
||||
//0.6.10
|
||||
"Deezer is unavailable in your country, Freezer might not work properly. Please use a VPN": "Deezer is unavailable in your country, Freezer might not work properly. Please use a VPN",
|
||||
"Deezer is unavailable": "Deezer is unavailable",
|
||||
"Continue": "Continue"
|
||||
}
|
||||
};
|
||||
|
|
|
@ -315,7 +315,7 @@ class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateM
|
|||
final BuildContext primaryContext = primaryFocus?.context;
|
||||
Intent intent = shortcuts[LogicalKeySet(event.logicalKey)];
|
||||
if (intent != null) {
|
||||
Actions.invoke(primaryContext, intent, nullOk: true);
|
||||
Actions.invoke(primaryContext, intent);
|
||||
}
|
||||
// WA for "Search field -> navigator -> UP -> DOWN" case. Prevents focus hanging.
|
||||
FocusNode newFocus = FocusManager.instance.primaryFocus;
|
||||
|
|
|
@ -3,41 +3,51 @@ import 'package:freezer/languages/crowdin.dart';
|
|||
import 'package:freezer/languages/en_us.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'),
|
||||
const Locale('es', 'ES'),
|
||||
const Locale('hr', 'HR'),
|
||||
const Locale('el', 'GR'),
|
||||
const Locale('ko', 'KO'),
|
||||
const Locale('fr', 'FR'),
|
||||
const Locale('he', 'IL'),
|
||||
const Locale('tr', 'TR'),
|
||||
const Locale('ro', 'RO'),
|
||||
const Locale('id', 'ID'),
|
||||
const Locale('fa', 'IR'),
|
||||
const Locale('pl', 'PL'),
|
||||
const Locale('uk', 'UA'),
|
||||
const Locale('hu', 'HU'),
|
||||
const Locale('ur', 'PK'),
|
||||
const Locale('hi', 'IN'),
|
||||
const Locale('sk', 'SK'),
|
||||
const Locale('cs', 'CZ'),
|
||||
const Locale('vi', 'VI'),
|
||||
const Locale('nl', 'NL'),
|
||||
const Locale('sl', 'SL'),
|
||||
const Locale('zh', 'CN'),
|
||||
const Locale('fil', 'PH'),
|
||||
const Locale('ast', 'ES'),
|
||||
const Locale('uwu', 'UWU')
|
||||
List<Language> languages = [
|
||||
Language('en', 'US', "English"),
|
||||
Language('ar', 'AR', "Arabic"),
|
||||
Language('pt', 'BR', "Brazil"),
|
||||
Language('it', 'IT', "Italian"),
|
||||
Language('de', 'DE', "German"),
|
||||
Language('ru', 'RU', "Russian"),
|
||||
Language('es', 'ES', "Spanish"),
|
||||
Language('hr', 'HR', "Croatian"),
|
||||
Language('el', 'GR', "Greek"),
|
||||
Language('ko', 'KO', "Korean"),
|
||||
Language('fr', 'FR', "Baguette"),
|
||||
Language('he', 'IL', "Hebrew"),
|
||||
Language('tr', 'TR', "Turkish"),
|
||||
Language('ro', 'RO', "Romanian"),
|
||||
Language('id', 'ID', "Indonesian"),
|
||||
Language('fa', 'IR', "Persian"),
|
||||
Language('pl', 'PL', "Polish"),
|
||||
Language('uk', 'UA', "Ukrainian"),
|
||||
Language('hu', 'HU', "Hungarian"),
|
||||
Language('ur', 'PK', "Urdu"),
|
||||
Language('hi', 'IN', "Hindi"),
|
||||
Language('sk', 'SK', "Slovak"),
|
||||
Language('cs', 'CZ', "Czech"),
|
||||
Language('vi', 'VI', "Vietnamese"),
|
||||
Language('nl', 'NL', "Dutch"),
|
||||
Language('sl', 'SL', "Slovenian"),
|
||||
Language('zh', 'CN', "Chinese"),
|
||||
Language('fil', 'PH', "Filipino"),
|
||||
Language('ast', 'ES', "Asturian"),
|
||||
Language('uwu', 'UWU', "Furry")
|
||||
];
|
||||
List<Locale> get supportedLocales => languages.map((l) => l.getLocale).toList();
|
||||
|
||||
extension Localization on String {
|
||||
static var _t = Translations.byLocale("en_US") + language_en_us + crowdin;
|
||||
|
||||
String get i18n => localize(this, _t);
|
||||
}
|
||||
|
||||
class Language {
|
||||
String name;
|
||||
String locale;
|
||||
String country;
|
||||
Language(this.locale, this.country, this.name);
|
||||
|
||||
Locale get getLocale => Locale(this.locale, this.country);
|
||||
}
|
|
@ -46,6 +46,26 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
}
|
||||
}
|
||||
|
||||
//Check if deezer available in current country
|
||||
void _checkAvailability() async {
|
||||
bool available = await DeezerAPI.chceckAvailability();
|
||||
if (!(available??true)) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text("Deezer is unavailable".i18n),
|
||||
content: Text("Deezer is unavailable in your country, Freezer might not work properly. Please use a VPN".i18n),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('Continue'.i18n),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(LoginWidget oldWidget) {
|
||||
_start();
|
||||
|
@ -55,6 +75,7 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
@override
|
||||
void initState() {
|
||||
_start();
|
||||
_checkAvailability();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,9 @@ import 'dart:async';
|
|||
//Changing item in queue view and pressing back causes the pageView to skip song
|
||||
bool pageViewLock = false;
|
||||
|
||||
//So can be updated when going back from lyrics
|
||||
Function updateColor;
|
||||
|
||||
class PlayerScreen extends StatefulWidget {
|
||||
@override
|
||||
_PlayerScreenState createState() => _PlayerScreenState();
|
||||
|
@ -87,6 +90,7 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||
_updateColor();
|
||||
});
|
||||
|
||||
updateColor = this._updateColor;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -361,10 +365,18 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
|||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.subtitles, size: ScreenUtil().setWidth(46)),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
onPressed: () async {
|
||||
//Fix bottom buttons
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor: settings.themeData.bottomAppBarColor,
|
||||
statusBarColor: Colors.transparent
|
||||
));
|
||||
|
||||
await Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => LyricsScreen(trackId: AudioService.currentMediaItem.id)
|
||||
));
|
||||
|
||||
updateColor();
|
||||
},
|
||||
),
|
||||
QualityInfoWidget(),
|
||||
|
@ -656,10 +668,18 @@ class PlayerScreenTopRow extends StatelessWidget {
|
|||
icon: Icon(Icons.menu),
|
||||
iconSize: this.iconSize??ScreenUtil().setSp(52),
|
||||
splashRadius: this.iconSize??ScreenUtil().setWidth(52),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
onPressed: () async {
|
||||
//Fix bottom buttons
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
systemNavigationBarColor: settings.themeData.bottomAppBarColor,
|
||||
statusBarColor: Colors.transparent
|
||||
));
|
||||
//Navigate
|
||||
await Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => QueueScreen()
|
||||
));
|
||||
//Fix colors
|
||||
updateColor();
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
@ -18,8 +18,6 @@ import 'package:freezer/ui/elements.dart';
|
|||
import 'package:freezer/ui/error.dart';
|
||||
import 'package:freezer/ui/home_screen.dart';
|
||||
import 'package:freezer/ui/updater.dart';
|
||||
import 'package:language_pickers/language_pickers.dart';
|
||||
import 'package:language_pickers/languages.dart';
|
||||
import 'package:package_info/package_info.dart';
|
||||
import 'package:path_provider_ex/path_provider_ex.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
@ -40,35 +38,6 @@ class SettingsScreen extends StatefulWidget {
|
|||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
List<Map<String, String>> _languages() {
|
||||
//Missing language
|
||||
defaultLanguagesList.add({
|
||||
'name': 'Filipino',
|
||||
'isoCode': 'fil'
|
||||
});
|
||||
defaultLanguagesList.add({
|
||||
'name': 'Furry',
|
||||
'isoCode': 'uwu'
|
||||
});
|
||||
defaultLanguagesList.add({
|
||||
'name': 'Asturian',
|
||||
'isoCode': 'ast'
|
||||
});
|
||||
defaultLanguagesList.add({
|
||||
'name': 'Chinese',
|
||||
'isoCode': 'zh'
|
||||
});
|
||||
List<Map<String, String>> _l = supportedLocales.map<Map<String, String>>((l) {
|
||||
Map _lang = defaultLanguagesList.firstWhere((lang) => lang['isoCode'] == l.languageCode);
|
||||
return {
|
||||
'name': _lang['name'],
|
||||
'isoCode': _lang['isoCode'],
|
||||
'locale': l.toString()
|
||||
};
|
||||
}).toList();
|
||||
return _l;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -121,13 +90,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
context: context,
|
||||
builder: (context) => SimpleDialog(
|
||||
title: Text('Select language'.i18n),
|
||||
children: List.generate(_languages().length, (int i) {
|
||||
Map l = _languages()[i];
|
||||
children: List.generate(languages.length, (int i) {
|
||||
Language l = languages[i];
|
||||
return ListTile(
|
||||
title: Text(l['name']),
|
||||
subtitle: Text(l['locale']),
|
||||
title: Text(l.name),
|
||||
subtitle: Text("${l.locale}-${l.country}"),
|
||||
onTap: () async {
|
||||
setState(() => settings.language = l['locale']);
|
||||
setState(() => settings.language = "${l.locale}_${l.country}");
|
||||
await settings.save();
|
||||
showDialog(
|
||||
context: context,
|
||||
|
@ -578,6 +547,50 @@ class _QualityPickerState extends State<QualityPicker> {
|
|||
}
|
||||
}
|
||||
|
||||
class ContentLanguage {
|
||||
String code;
|
||||
String name;
|
||||
ContentLanguage(this.code, this.name);
|
||||
|
||||
static List<ContentLanguage> get all => [
|
||||
ContentLanguage("cs", "Čeština"),
|
||||
ContentLanguage("da", "Dansk"),
|
||||
ContentLanguage("de", "Deutsch"),
|
||||
ContentLanguage("en", "English"),
|
||||
ContentLanguage("us", "English (us)"),
|
||||
ContentLanguage("es", "Español"),
|
||||
ContentLanguage("mx", "Español (latam)"),
|
||||
ContentLanguage("fr", "Français"),
|
||||
ContentLanguage("hr", "Hrvatski"),
|
||||
ContentLanguage("id", "Indonesia"),
|
||||
ContentLanguage("it", "Italiano"),
|
||||
ContentLanguage("hu", "Magyar"),
|
||||
ContentLanguage("ms", "Melayu"),
|
||||
ContentLanguage("nl", "Nederlands"),
|
||||
ContentLanguage("no", "Norsk"),
|
||||
ContentLanguage("pl", "Polski"),
|
||||
ContentLanguage("br", "Português (br)"),
|
||||
ContentLanguage("pt", "Português (pt)"),
|
||||
ContentLanguage("ro", "Română"),
|
||||
ContentLanguage("sk", "Slovenčina"),
|
||||
ContentLanguage("sl", "Slovenščina"),
|
||||
ContentLanguage("sq", "Shqip"),
|
||||
ContentLanguage("sr", "Srpski"),
|
||||
ContentLanguage("fi", "Suomi"),
|
||||
ContentLanguage("sv", "Svenska"),
|
||||
ContentLanguage("tr", "Türkçe"),
|
||||
ContentLanguage("bg", "Български"),
|
||||
ContentLanguage("ru", "Pусский"),
|
||||
ContentLanguage("uk", "Українська"),
|
||||
ContentLanguage("he", "עִברִית"),
|
||||
ContentLanguage("ar", "العربیة"),
|
||||
ContentLanguage("cn", "中文"),
|
||||
ContentLanguage("ja", "日本語"),
|
||||
ContentLanguage("ko", "한국어"),
|
||||
ContentLanguage("th", "ภาษาไทย"),
|
||||
];
|
||||
}
|
||||
|
||||
class DeezerSettings extends StatefulWidget {
|
||||
@override
|
||||
_DeezerSettingsState createState() => _DeezerSettingsState();
|
||||
|
@ -597,17 +610,17 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
|||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => LanguagePickerDialog(
|
||||
titlePadding: EdgeInsets.all(8.0),
|
||||
isSearchable: true,
|
||||
builder: (context) => SimpleDialog(
|
||||
title: Text('Select language'.i18n),
|
||||
languagesList: defaultLanguagesList.map<Map<String, String>>((l) => {
|
||||
'isoCode': l['isoCode'], 'name': l['name'] + ' (${l["isoCode"]})'
|
||||
}).toList(),
|
||||
onValuePicked: (Language language) {
|
||||
setState(() => settings.deezerLanguage = language.isoCode);
|
||||
settings.save();
|
||||
children: List.generate(ContentLanguage.all.length, (i) => ListTile(
|
||||
title: Text(ContentLanguage.all[i].name),
|
||||
subtitle: Text(ContentLanguage.all[i].code),
|
||||
onTap: () async {
|
||||
setState(() => settings.deezerLanguage = ContentLanguage.all[i].code);
|
||||
await settings.save();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)),
|
||||
)
|
||||
);
|
||||
},
|
||||
|
|
101
pubspec.lock
101
pubspec.lock
|
@ -21,14 +21,14 @@ packages:
|
|||
name: args
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
version: "2.0.0"
|
||||
async:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.5.0-nullsafety.1"
|
||||
version: "2.5.0"
|
||||
audio_service:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -49,7 +49,7 @@ packages:
|
|||
name: boolean_selector
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0-nullsafety.1"
|
||||
version: "2.1.0"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -119,14 +119,14 @@ packages:
|
|||
name: characters
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0-nullsafety.3"
|
||||
version: "1.1.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0-nullsafety.1"
|
||||
version: "1.2.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -154,7 +154,7 @@ packages:
|
|||
name: clock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0-nullsafety.1"
|
||||
version: "1.1.0"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -168,7 +168,7 @@ packages:
|
|||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.15.0-nullsafety.3"
|
||||
version: "1.15.0"
|
||||
connectivity:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -217,9 +217,9 @@ packages:
|
|||
name: country_pickers
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "2.0.0"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
|
@ -235,17 +235,19 @@ packages:
|
|||
custom_navigator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: custom_navigator
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "84bc85880abaa0d4a0f37098c9e6f4bd58b19b0a"
|
||||
url: "https://github.com/kjawadDeveloper/Custom-navigator.git"
|
||||
source: git
|
||||
version: "0.2.0"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.11"
|
||||
version: "1.3.12"
|
||||
dio:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -301,7 +303,7 @@ packages:
|
|||
name: fake_async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0-nullsafety.1"
|
||||
version: "1.2.0"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -315,7 +317,7 @@ packages:
|
|||
name: file
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.2.1"
|
||||
version: "6.1.0"
|
||||
filesize:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -426,7 +428,7 @@ packages:
|
|||
name: gettext_parser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.0+1"
|
||||
version: "0.2.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -448,13 +450,6 @@ packages:
|
|||
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:
|
||||
|
@ -489,7 +484,7 @@ packages:
|
|||
name: i18n_extension
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
version: "4.0.0"
|
||||
import_js_library:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -510,7 +505,7 @@ packages:
|
|||
name: intl
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.16.1"
|
||||
version: "0.17.0"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -524,7 +519,7 @@ packages:
|
|||
name: js
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.2"
|
||||
version: "0.6.3"
|
||||
json_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -545,7 +540,7 @@ packages:
|
|||
path: "just_audio/just_audio"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.6.9"
|
||||
version: "0.6.5"
|
||||
just_audio_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -559,14 +554,7 @@ packages:
|
|||
path: "just_audio/just_audio_web"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.2.3"
|
||||
language_pickers:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: language_pickers
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0+1"
|
||||
version: "0.2.1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -587,14 +575,14 @@ packages:
|
|||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.10-nullsafety.1"
|
||||
version: "0.12.10"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0-nullsafety.3"
|
||||
version: "1.3.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -622,7 +610,7 @@ packages:
|
|||
name: node_io
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.1.1"
|
||||
numberpicker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -671,7 +659,7 @@ packages:
|
|||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.0-nullsafety.1"
|
||||
version: "1.8.0"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -748,7 +736,7 @@ packages:
|
|||
name: platform
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "3.0.0"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -756,13 +744,6 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
pointycastle:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pointycastle
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -776,7 +757,7 @@ packages:
|
|||
name: process
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.13"
|
||||
version: "4.1.0"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -865,14 +846,14 @@ packages:
|
|||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.0-nullsafety.2"
|
||||
version: "1.8.0"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sprintf
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
version: "6.0.0"
|
||||
sqflite:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -893,14 +874,14 @@ packages:
|
|||
name: stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.10.0-nullsafety.1"
|
||||
version: "1.10.0"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0-nullsafety.1"
|
||||
version: "2.1.0"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -914,7 +895,7 @@ packages:
|
|||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0-nullsafety.1"
|
||||
version: "1.1.0"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -928,14 +909,14 @@ packages:
|
|||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0-nullsafety.1"
|
||||
version: "1.2.0"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.19-nullsafety.2"
|
||||
version: "0.2.19"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -949,7 +930,7 @@ packages:
|
|||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0-nullsafety.3"
|
||||
version: "1.3.0"
|
||||
uni_links:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1012,7 +993,7 @@ packages:
|
|||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0-nullsafety.3"
|
||||
version: "2.1.0"
|
||||
version:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1077,5 +1058,5 @@ packages:
|
|||
source: hosted
|
||||
version: "2.2.1"
|
||||
sdks:
|
||||
dart: ">=2.10.2 <2.11.0"
|
||||
flutter: ">=1.22.2 <2.0.0"
|
||||
dart: ">=2.12.0 <3.0.0"
|
||||
flutter: ">=1.22.2"
|
||||
|
|
18
pubspec.yaml
18
pubspec.yaml
|
@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 0.6.9+1
|
||||
version: 0.6.10+1
|
||||
|
||||
environment:
|
||||
sdk: ">=2.8.0 <3.0.0"
|
||||
|
@ -33,21 +33,16 @@ dependencies:
|
|||
path_provider: 1.6.10
|
||||
path: ^1.6.4
|
||||
sqflite: ^1.3.0+1
|
||||
crypto: ^2.1.4
|
||||
hex: ^0.1.2
|
||||
pointycastle: ^1.0.2
|
||||
ext_storage: ^1.0.3
|
||||
permission_handler: ^5.0.0+hotfix.6
|
||||
connectivity: ^0.4.8+6
|
||||
intl: ^0.16.1
|
||||
intl: ^0.17.0
|
||||
filesize: ^1.0.4
|
||||
fluttertoast: 7.0.4
|
||||
palette_generator: ^0.2.3
|
||||
flutter_material_color_picker: ^1.0.5
|
||||
flutter_inappwebview: ^4.0.0
|
||||
custom_navigator: ^0.3.0
|
||||
language_pickers: ^0.2.0+1
|
||||
country_pickers: ^1.3.0
|
||||
country_pickers: ^2.0.0
|
||||
package_info: ^0.4.1
|
||||
move_to_background: ^1.0.1
|
||||
flutter_local_notifications: ^1.4.4+1
|
||||
|
@ -62,13 +57,13 @@ dependencies:
|
|||
flutter_cache_manager: ^1.4.1
|
||||
cached_network_image: ^2.3.2+1
|
||||
clipboard: ^0.1.2+8
|
||||
i18n_extension: ^1.4.4
|
||||
i18n_extension: ^4.0.0
|
||||
fluttericon: ^1.0.7
|
||||
url_launcher: ^5.7.2
|
||||
uni_links: ^0.4.0
|
||||
share: ^0.6.5+2
|
||||
numberpicker: ^1.2.1
|
||||
quick_actions: ^0.4.0+10
|
||||
quick_actions: 0.4.0+10
|
||||
photo_view: ^0.10.2
|
||||
draggable_scrollbar: ^0.0.4
|
||||
scrobblenaut: ^2.0.4
|
||||
|
@ -78,6 +73,9 @@ dependencies:
|
|||
google_fonts: ^1.1.2
|
||||
equalizer: ^0.0.2+2
|
||||
extended_math: ^0.0.29+1
|
||||
custom_navigator:
|
||||
git:
|
||||
url: https://github.com/kjawadDeveloper/Custom-navigator.git
|
||||
|
||||
audio_session: ^0.0.9
|
||||
audio_service:
|
||||
|
|
Loading…
Reference in New Issue