New git, updated dependencies

This commit is contained in:
exttex 2021-03-16 20:35:50 +01:00
parent 66bfd5eb70
commit f49475e5a3
15 changed files with 231 additions and 974 deletions

10
.gitignore vendored
View File

@ -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

4
.gitmodules vendored
View File

@ -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

View File

@ -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: {

View File

@ -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';

View File

@ -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
}

View File

@ -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});

View File

@ -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"
}
};

View File

@ -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;

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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();
@ -86,7 +89,8 @@ class _PlayerScreenState extends State<PlayerScreen> {
_mediaItemSub = AudioService.currentMediaItemStream.listen((event) {
_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();
},
),
],

View File

@ -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();
},
)),
)
);
},

View File

@ -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"

View File

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# 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: