New git, updated dependencies
This commit is contained in:
parent
66bfd5eb70
commit
f49475e5a3
|
@ -2,7 +2,17 @@
|
||||||
freezerkey.jsk
|
freezerkey.jsk
|
||||||
android/key.properties
|
android/key.properties
|
||||||
|
|
||||||
|
audio_service/.idea
|
||||||
|
audio_service/.dart_tool
|
||||||
|
|
||||||
|
android/local.properties
|
||||||
just_audio/
|
just_audio/
|
||||||
|
.gradle/
|
||||||
|
android/.gradle
|
||||||
|
android/.idea
|
||||||
|
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
*.class
|
*.class
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[submodule "audio_service"]
|
[submodule "audio_service"]
|
||||||
path = audio_service
|
path = audio_service
|
||||||
url = https://git.rip/freezer/audio_service
|
url = https://git.freezer.life/exttex/audio_service.git
|
||||||
[submodule "just_audio"]
|
[submodule "just_audio"]
|
||||||
path = 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
|
//Search
|
||||||
Future<SearchResults> search(String query) async {
|
Future<SearchResults> search(String query) async {
|
||||||
Map<dynamic, dynamic> data = await callApi('deezer.pageSearch', params: {
|
Map<dynamic, dynamic> data = await callApi('deezer.pageSearch', params: {
|
||||||
|
|
|
@ -1,21 +1,14 @@
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
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:freezer/api/cache.dart';
|
import 'package:freezer/api/cache.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:path_provider/path_provider.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:path/path.dart' as p;
|
||||||
import 'package:freezer/translations.i18n.dart';
|
import 'package:freezer/translations.i18n.dart';
|
||||||
import 'package:crypto/crypto.dart' as crypto;
|
|
||||||
|
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
part 'definitions.g.dart';
|
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_service/audio_service.dart';
|
||||||
import 'package:audio_session/audio_session.dart';
|
import 'package:audio_session/audio_session.dart';
|
||||||
import 'package:equalizer/equalizer.dart';
|
import 'package:equalizer/equalizer.dart';
|
||||||
|
@ -21,6 +19,8 @@ import '../settings.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
|
||||||
PlayerHelper playerHelper = PlayerHelper();
|
PlayerHelper playerHelper = PlayerHelper();
|
||||||
|
|
||||||
|
@ -776,9 +776,9 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
||||||
_visualizerSubscription = _player.visualizerFftStream.listen((event) {
|
_visualizerSubscription = _player.visualizerFftStream.listen((event) {
|
||||||
//Calculate actual values
|
//Calculate actual values
|
||||||
List<double> out = [];
|
List<double> out = [];
|
||||||
for (int i=0; i<event.data.length/2; i++) {
|
for (int i=0; i<event.length/2; i++) {
|
||||||
int rfk = event.data[i*2].toSigned(8);
|
int rfk = event[i*2].toSigned(8);
|
||||||
int ifk = event.data[i*2+1].toSigned(8);
|
int ifk = event[i*2+1].toSigned(8);
|
||||||
out.add(log(hypot(rfk, ifk) + 1) / 5.2);
|
out.add(log(hypot(rfk, ifk) + 1) / 5.2);
|
||||||
}
|
}
|
||||||
AudioServiceBackground.sendCustomEvent({"action": "visualizer", "data": out});
|
AudioServiceBackground.sendCustomEvent({"action": "visualizer", "data": out});
|
||||||
|
|
|
@ -338,6 +338,11 @@ const language_en_us = {
|
||||||
"Unsynchronized lyrics": "Unsynchronized lyrics",
|
"Unsynchronized lyrics": "Unsynchronized lyrics",
|
||||||
"Genre": "Genre",
|
"Genre": "Genre",
|
||||||
"Contributors": "Contributors",
|
"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;
|
final BuildContext primaryContext = primaryFocus?.context;
|
||||||
Intent intent = shortcuts[LogicalKeySet(event.logicalKey)];
|
Intent intent = shortcuts[LogicalKeySet(event.logicalKey)];
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
Actions.invoke(primaryContext, intent, nullOk: true);
|
Actions.invoke(primaryContext, intent);
|
||||||
}
|
}
|
||||||
// WA for "Search field -> navigator -> UP -> DOWN" case. Prevents focus hanging.
|
// WA for "Search field -> navigator -> UP -> DOWN" case. Prevents focus hanging.
|
||||||
FocusNode newFocus = FocusManager.instance.primaryFocus;
|
FocusNode newFocus = FocusManager.instance.primaryFocus;
|
||||||
|
|
|
@ -3,41 +3,51 @@ import 'package:freezer/languages/crowdin.dart';
|
||||||
import 'package:freezer/languages/en_us.dart';
|
import 'package:freezer/languages/en_us.dart';
|
||||||
import 'package:i18n_extension/i18n_extension.dart';
|
import 'package:i18n_extension/i18n_extension.dart';
|
||||||
|
|
||||||
const supportedLocales = [
|
List<Language> languages = [
|
||||||
const Locale('en', 'US'),
|
Language('en', 'US', "English"),
|
||||||
const Locale('ar', 'AR'),
|
Language('ar', 'AR', "Arabic"),
|
||||||
const Locale('pt', 'BR'),
|
Language('pt', 'BR', "Brazil"),
|
||||||
const Locale('it', 'IT'),
|
Language('it', 'IT', "Italian"),
|
||||||
const Locale('de', 'DE'),
|
Language('de', 'DE', "German"),
|
||||||
const Locale('ru', 'RU'),
|
Language('ru', 'RU', "Russian"),
|
||||||
const Locale('es', 'ES'),
|
Language('es', 'ES', "Spanish"),
|
||||||
const Locale('hr', 'HR'),
|
Language('hr', 'HR', "Croatian"),
|
||||||
const Locale('el', 'GR'),
|
Language('el', 'GR', "Greek"),
|
||||||
const Locale('ko', 'KO'),
|
Language('ko', 'KO', "Korean"),
|
||||||
const Locale('fr', 'FR'),
|
Language('fr', 'FR', "Baguette"),
|
||||||
const Locale('he', 'IL'),
|
Language('he', 'IL', "Hebrew"),
|
||||||
const Locale('tr', 'TR'),
|
Language('tr', 'TR', "Turkish"),
|
||||||
const Locale('ro', 'RO'),
|
Language('ro', 'RO', "Romanian"),
|
||||||
const Locale('id', 'ID'),
|
Language('id', 'ID', "Indonesian"),
|
||||||
const Locale('fa', 'IR'),
|
Language('fa', 'IR', "Persian"),
|
||||||
const Locale('pl', 'PL'),
|
Language('pl', 'PL', "Polish"),
|
||||||
const Locale('uk', 'UA'),
|
Language('uk', 'UA', "Ukrainian"),
|
||||||
const Locale('hu', 'HU'),
|
Language('hu', 'HU', "Hungarian"),
|
||||||
const Locale('ur', 'PK'),
|
Language('ur', 'PK', "Urdu"),
|
||||||
const Locale('hi', 'IN'),
|
Language('hi', 'IN', "Hindi"),
|
||||||
const Locale('sk', 'SK'),
|
Language('sk', 'SK', "Slovak"),
|
||||||
const Locale('cs', 'CZ'),
|
Language('cs', 'CZ', "Czech"),
|
||||||
const Locale('vi', 'VI'),
|
Language('vi', 'VI', "Vietnamese"),
|
||||||
const Locale('nl', 'NL'),
|
Language('nl', 'NL', "Dutch"),
|
||||||
const Locale('sl', 'SL'),
|
Language('sl', 'SL', "Slovenian"),
|
||||||
const Locale('zh', 'CN'),
|
Language('zh', 'CN', "Chinese"),
|
||||||
const Locale('fil', 'PH'),
|
Language('fil', 'PH', "Filipino"),
|
||||||
const Locale('ast', 'ES'),
|
Language('ast', 'ES', "Asturian"),
|
||||||
const Locale('uwu', 'UWU')
|
Language('uwu', 'UWU', "Furry")
|
||||||
];
|
];
|
||||||
|
List<Locale> get supportedLocales => languages.map((l) => l.getLocale).toList();
|
||||||
|
|
||||||
extension Localization on String {
|
extension Localization on String {
|
||||||
static var _t = Translations.byLocale("en_US") + language_en_us + crowdin;
|
static var _t = Translations.byLocale("en_US") + language_en_us + crowdin;
|
||||||
|
|
||||||
String get i18n => localize(this, _t);
|
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
|
@override
|
||||||
void didUpdateWidget(LoginWidget oldWidget) {
|
void didUpdateWidget(LoginWidget oldWidget) {
|
||||||
_start();
|
_start();
|
||||||
|
@ -55,6 +75,7 @@ class _LoginWidgetState extends State<LoginWidget> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_start();
|
_start();
|
||||||
|
_checkAvailability();
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,9 @@ import 'dart:async';
|
||||||
//Changing item in queue view and pressing back causes the pageView to skip song
|
//Changing item in queue view and pressing back causes the pageView to skip song
|
||||||
bool pageViewLock = false;
|
bool pageViewLock = false;
|
||||||
|
|
||||||
|
//So can be updated when going back from lyrics
|
||||||
|
Function updateColor;
|
||||||
|
|
||||||
class PlayerScreen extends StatefulWidget {
|
class PlayerScreen extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
_PlayerScreenState createState() => _PlayerScreenState();
|
_PlayerScreenState createState() => _PlayerScreenState();
|
||||||
|
@ -87,6 +90,7 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
||||||
_updateColor();
|
_updateColor();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateColor = this._updateColor;
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -361,10 +365,18 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.subtitles, size: ScreenUtil().setWidth(46)),
|
icon: Icon(Icons.subtitles, size: ScreenUtil().setWidth(46)),
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
//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)
|
builder: (context) => LyricsScreen(trackId: AudioService.currentMediaItem.id)
|
||||||
));
|
));
|
||||||
|
|
||||||
|
updateColor();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
QualityInfoWidget(),
|
QualityInfoWidget(),
|
||||||
|
@ -656,10 +668,18 @@ class PlayerScreenTopRow extends StatelessWidget {
|
||||||
icon: Icon(Icons.menu),
|
icon: Icon(Icons.menu),
|
||||||
iconSize: this.iconSize??ScreenUtil().setSp(52),
|
iconSize: this.iconSize??ScreenUtil().setSp(52),
|
||||||
splashRadius: this.iconSize??ScreenUtil().setWidth(52),
|
splashRadius: this.iconSize??ScreenUtil().setWidth(52),
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
Navigator.of(context).push(MaterialPageRoute(
|
//Fix bottom buttons
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||||
|
systemNavigationBarColor: settings.themeData.bottomAppBarColor,
|
||||||
|
statusBarColor: Colors.transparent
|
||||||
|
));
|
||||||
|
//Navigate
|
||||||
|
await Navigator.of(context).push(MaterialPageRoute(
|
||||||
builder: (context) => QueueScreen()
|
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/error.dart';
|
||||||
import 'package:freezer/ui/home_screen.dart';
|
import 'package:freezer/ui/home_screen.dart';
|
||||||
import 'package:freezer/ui/updater.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: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';
|
||||||
|
@ -40,35 +38,6 @@ class SettingsScreen extends StatefulWidget {
|
||||||
|
|
||||||
class _SettingsScreenState extends State<SettingsScreen> {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
@ -121,13 +90,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => SimpleDialog(
|
builder: (context) => SimpleDialog(
|
||||||
title: Text('Select language'.i18n),
|
title: Text('Select language'.i18n),
|
||||||
children: List.generate(_languages().length, (int i) {
|
children: List.generate(languages.length, (int i) {
|
||||||
Map l = _languages()[i];
|
Language l = languages[i];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(l['name']),
|
title: Text(l.name),
|
||||||
subtitle: Text(l['locale']),
|
subtitle: Text("${l.locale}-${l.country}"),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
setState(() => settings.language = l['locale']);
|
setState(() => settings.language = "${l.locale}_${l.country}");
|
||||||
await settings.save();
|
await settings.save();
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
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 {
|
class DeezerSettings extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
_DeezerSettingsState createState() => _DeezerSettingsState();
|
_DeezerSettingsState createState() => _DeezerSettingsState();
|
||||||
|
@ -597,17 +610,17 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => LanguagePickerDialog(
|
builder: (context) => SimpleDialog(
|
||||||
titlePadding: EdgeInsets.all(8.0),
|
|
||||||
isSearchable: true,
|
|
||||||
title: Text('Select language'.i18n),
|
title: Text('Select language'.i18n),
|
||||||
languagesList: defaultLanguagesList.map<Map<String, String>>((l) => {
|
children: List.generate(ContentLanguage.all.length, (i) => ListTile(
|
||||||
'isoCode': l['isoCode'], 'name': l['name'] + ' (${l["isoCode"]})'
|
title: Text(ContentLanguage.all[i].name),
|
||||||
}).toList(),
|
subtitle: Text(ContentLanguage.all[i].code),
|
||||||
onValuePicked: (Language language) {
|
onTap: () async {
|
||||||
setState(() => settings.deezerLanguage = language.isoCode);
|
setState(() => settings.deezerLanguage = ContentLanguage.all[i].code);
|
||||||
settings.save();
|
await settings.save();
|
||||||
},
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
101
pubspec.lock
101
pubspec.lock
|
@ -21,14 +21,14 @@ packages:
|
||||||
name: args
|
name: args
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.6.0"
|
version: "2.0.0"
|
||||||
async:
|
async:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.0-nullsafety.1"
|
version: "2.5.0"
|
||||||
audio_service:
|
audio_service:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -49,7 +49,7 @@ packages:
|
||||||
name: boolean_selector
|
name: boolean_selector
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0-nullsafety.1"
|
version: "2.1.0"
|
||||||
build:
|
build:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -119,14 +119,14 @@ packages:
|
||||||
name: characters
|
name: characters
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0-nullsafety.3"
|
version: "1.1.0"
|
||||||
charcode:
|
charcode:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: charcode
|
name: charcode
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0-nullsafety.1"
|
version: "1.2.0"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -154,7 +154,7 @@ packages:
|
||||||
name: clock
|
name: clock
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0-nullsafety.1"
|
version: "1.1.0"
|
||||||
code_builder:
|
code_builder:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -168,7 +168,7 @@ packages:
|
||||||
name: collection
|
name: collection
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.15.0-nullsafety.3"
|
version: "1.15.0"
|
||||||
connectivity:
|
connectivity:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -217,9 +217,9 @@ packages:
|
||||||
name: country_pickers
|
name: country_pickers
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "2.0.0"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
|
@ -235,17 +235,19 @@ packages:
|
||||||
custom_navigator:
|
custom_navigator:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: custom_navigator
|
path: "."
|
||||||
url: "https://pub.dartlang.org"
|
ref: HEAD
|
||||||
source: hosted
|
resolved-ref: "84bc85880abaa0d4a0f37098c9e6f4bd58b19b0a"
|
||||||
version: "0.3.0"
|
url: "https://github.com/kjawadDeveloper/Custom-navigator.git"
|
||||||
|
source: git
|
||||||
|
version: "0.2.0"
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dart_style
|
name: dart_style
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.11"
|
version: "1.3.12"
|
||||||
dio:
|
dio:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -301,7 +303,7 @@ packages:
|
||||||
name: fake_async
|
name: fake_async
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0-nullsafety.1"
|
version: "1.2.0"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -315,7 +317,7 @@ packages:
|
||||||
name: file
|
name: file
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.2.1"
|
version: "6.1.0"
|
||||||
filesize:
|
filesize:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -426,7 +428,7 @@ packages:
|
||||||
name: gettext_parser
|
name: gettext_parser
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.0+1"
|
version: "0.2.0"
|
||||||
glob:
|
glob:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -448,13 +450,6 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.2.0"
|
||||||
hex:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: hex
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.1.2"
|
|
||||||
html:
|
html:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -489,7 +484,7 @@ packages:
|
||||||
name: i18n_extension
|
name: i18n_extension
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.1"
|
version: "4.0.0"
|
||||||
import_js_library:
|
import_js_library:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -510,7 +505,7 @@ packages:
|
||||||
name: intl
|
name: intl
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.16.1"
|
version: "0.17.0"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -524,7 +519,7 @@ packages:
|
||||||
name: js
|
name: js
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.2"
|
version: "0.6.3"
|
||||||
json_annotation:
|
json_annotation:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -545,7 +540,7 @@ packages:
|
||||||
path: "just_audio/just_audio"
|
path: "just_audio/just_audio"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.6.9"
|
version: "0.6.5"
|
||||||
just_audio_platform_interface:
|
just_audio_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -559,14 +554,7 @@ packages:
|
||||||
path: "just_audio/just_audio_web"
|
path: "just_audio/just_audio_web"
|
||||||
relative: true
|
relative: true
|
||||||
source: path
|
source: path
|
||||||
version: "0.2.3"
|
version: "0.2.1"
|
||||||
language_pickers:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: language_pickers
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.0+1"
|
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -587,14 +575,14 @@ packages:
|
||||||
name: matcher
|
name: matcher
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.10-nullsafety.1"
|
version: "0.12.10"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0-nullsafety.3"
|
version: "1.3.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -622,7 +610,7 @@ packages:
|
||||||
name: node_io
|
name: node_io
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.1.1"
|
||||||
numberpicker:
|
numberpicker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -671,7 +659,7 @@ packages:
|
||||||
name: path
|
name: path
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.0-nullsafety.1"
|
version: "1.8.0"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -748,7 +736,7 @@ packages:
|
||||||
name: platform
|
name: platform
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "3.0.0"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -756,13 +744,6 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.3"
|
version: "1.0.3"
|
||||||
pointycastle:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: pointycastle
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.2"
|
|
||||||
pool:
|
pool:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -776,7 +757,7 @@ packages:
|
||||||
name: process
|
name: process
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.13"
|
version: "4.1.0"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -865,14 +846,14 @@ packages:
|
||||||
name: source_span
|
name: source_span
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.0-nullsafety.2"
|
version: "1.8.0"
|
||||||
sprintf:
|
sprintf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sprintf
|
name: sprintf
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "6.0.0"
|
||||||
sqflite:
|
sqflite:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -893,14 +874,14 @@ packages:
|
||||||
name: stack_trace
|
name: stack_trace
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.0-nullsafety.1"
|
version: "1.10.0"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stream_channel
|
name: stream_channel
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0-nullsafety.1"
|
version: "2.1.0"
|
||||||
stream_transform:
|
stream_transform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -914,7 +895,7 @@ packages:
|
||||||
name: string_scanner
|
name: string_scanner
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0-nullsafety.1"
|
version: "1.1.0"
|
||||||
synchronized:
|
synchronized:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -928,14 +909,14 @@ packages:
|
||||||
name: term_glyph
|
name: term_glyph
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0-nullsafety.1"
|
version: "1.2.0"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.19-nullsafety.2"
|
version: "0.2.19"
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -949,7 +930,7 @@ packages:
|
||||||
name: typed_data
|
name: typed_data
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0-nullsafety.3"
|
version: "1.3.0"
|
||||||
uni_links:
|
uni_links:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -1012,7 +993,7 @@ packages:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0-nullsafety.3"
|
version: "2.1.0"
|
||||||
version:
|
version:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -1077,5 +1058,5 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.10.2 <2.11.0"
|
dart: ">=2.12.0 <3.0.0"
|
||||||
flutter: ">=1.22.2 <2.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.
|
# 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.6.9+1
|
version: 0.6.10+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.8.0 <3.0.0"
|
sdk: ">=2.8.0 <3.0.0"
|
||||||
|
@ -33,21 +33,16 @@ dependencies:
|
||||||
path_provider: 1.6.10
|
path_provider: 1.6.10
|
||||||
path: ^1.6.4
|
path: ^1.6.4
|
||||||
sqflite: ^1.3.0+1
|
sqflite: ^1.3.0+1
|
||||||
crypto: ^2.1.4
|
|
||||||
hex: ^0.1.2
|
|
||||||
pointycastle: ^1.0.2
|
|
||||||
ext_storage: ^1.0.3
|
ext_storage: ^1.0.3
|
||||||
permission_handler: ^5.0.0+hotfix.6
|
permission_handler: ^5.0.0+hotfix.6
|
||||||
connectivity: ^0.4.8+6
|
connectivity: ^0.4.8+6
|
||||||
intl: ^0.16.1
|
intl: ^0.17.0
|
||||||
filesize: ^1.0.4
|
filesize: ^1.0.4
|
||||||
fluttertoast: 7.0.4
|
fluttertoast: 7.0.4
|
||||||
palette_generator: ^0.2.3
|
palette_generator: ^0.2.3
|
||||||
flutter_material_color_picker: ^1.0.5
|
flutter_material_color_picker: ^1.0.5
|
||||||
flutter_inappwebview: ^4.0.0
|
flutter_inappwebview: ^4.0.0
|
||||||
custom_navigator: ^0.3.0
|
country_pickers: ^2.0.0
|
||||||
language_pickers: ^0.2.0+1
|
|
||||||
country_pickers: ^1.3.0
|
|
||||||
package_info: ^0.4.1
|
package_info: ^0.4.1
|
||||||
move_to_background: ^1.0.1
|
move_to_background: ^1.0.1
|
||||||
flutter_local_notifications: ^1.4.4+1
|
flutter_local_notifications: ^1.4.4+1
|
||||||
|
@ -62,13 +57,13 @@ dependencies:
|
||||||
flutter_cache_manager: ^1.4.1
|
flutter_cache_manager: ^1.4.1
|
||||||
cached_network_image: ^2.3.2+1
|
cached_network_image: ^2.3.2+1
|
||||||
clipboard: ^0.1.2+8
|
clipboard: ^0.1.2+8
|
||||||
i18n_extension: ^1.4.4
|
i18n_extension: ^4.0.0
|
||||||
fluttericon: ^1.0.7
|
fluttericon: ^1.0.7
|
||||||
url_launcher: ^5.7.2
|
url_launcher: ^5.7.2
|
||||||
uni_links: ^0.4.0
|
uni_links: ^0.4.0
|
||||||
share: ^0.6.5+2
|
share: ^0.6.5+2
|
||||||
numberpicker: ^1.2.1
|
numberpicker: ^1.2.1
|
||||||
quick_actions: ^0.4.0+10
|
quick_actions: 0.4.0+10
|
||||||
photo_view: ^0.10.2
|
photo_view: ^0.10.2
|
||||||
draggable_scrollbar: ^0.0.4
|
draggable_scrollbar: ^0.0.4
|
||||||
scrobblenaut: ^2.0.4
|
scrobblenaut: ^2.0.4
|
||||||
|
@ -78,6 +73,9 @@ dependencies:
|
||||||
google_fonts: ^1.1.2
|
google_fonts: ^1.1.2
|
||||||
equalizer: ^0.0.2+2
|
equalizer: ^0.0.2+2
|
||||||
extended_math: ^0.0.29+1
|
extended_math: ^0.0.29+1
|
||||||
|
custom_navigator:
|
||||||
|
git:
|
||||||
|
url: https://github.com/kjawadDeveloper/Custom-navigator.git
|
||||||
|
|
||||||
audio_session: ^0.0.9
|
audio_session: ^0.0.9
|
||||||
audio_service:
|
audio_service:
|
||||||
|
|
Loading…
Reference in New Issue