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

4
.gitmodules vendored
View File

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

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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. # 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: