diff --git a/.gitignore b/.gitignore index 0558ba1..3a6a336 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.gitmodules b/.gitmodules index ff77a75..3fe9d41 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/audio_service b/audio_service index ff4f5f6..e205269 160000 --- a/audio_service +++ b/audio_service @@ -1 +1 @@ -Subproject commit ff4f5f656adb8a66e6f0ab91966ee6e3532b4dc4 +Subproject commit e205269a86afc5a17715465664fe908dae8b82f6 diff --git a/lib/api/deezer.dart b/lib/api/deezer.dart index 076b5a0..82a7850 100644 --- a/lib/api/deezer.dart +++ b/lib/api/deezer.dart @@ -134,6 +134,16 @@ class DeezerAPI { } } + //Check if Deezer available in country + static Future 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 search(String query) async { Map data = await callApi('deezer.pageSearch', params: { diff --git a/lib/api/definitions.dart b/lib/api/definitions.dart index 7cb0b77..f51fc48 100644 --- a/lib/api/definitions.dart +++ b/lib/api/definitions.dart @@ -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'; diff --git a/lib/api/download-out.dart b/lib/api/download-out.dart deleted file mode 100644 index 87286a6..0000000 --- a/lib/api/download-out.dart +++ /dev/null @@ -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 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 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> allOfflineTracks() async { - List data = await db.query('tracks', where: 'offline == 1'); - List tracks = []; - //Load track data - for (var t in data) { - tracks.add(await getTrack(t['id'])); - } - return tracks; - } - - //Get all offline playlists - Future> getOfflinePlaylists() async { - List data = await db.query('playlists'); - List playlists = []; - //Load playlists - for (var p in data) { - playlists.add(await getPlaylist(p['id'])); - } - return playlists; - } - - //Get playlist metadata with tracks - Future 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 getFavorites() async { - return await getPlaylist('FAVORITES'); - } - - Future> getOfflineAlbums({List albumsData}) async { - //Load albums - if (albumsData == null) { - albumsData = await db.query('albums', where: 'offline == 1'); - } - List albums = albumsData.map((alb) => Album.fromSQL(alb)).toList(); - for(int i=0; i((a) => Artist.fromSQL(a)).toList(); - } - return albums; - } - - //Get track with metadata from db - Future getTrack(String id, {Album album, List 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 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 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 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 getOfflineTrackPath(String id) async { - List 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 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 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> getFinishedDownloads() async { - //Fetch from db - List data = await db.rawQuery("SELECT * FROM downloads INNER JOIN tracks ON tracks.id = downloads.trackId WHERE downloads.state = 1 OR downloads.state > 3"); - List downloads = data.map((d) => Download.fromSQL(d, parseTrack: true)).toList(); - return downloads; - } - - //Get stats for library screen - Future> 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 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 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 _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 toSQL() => { - 'trackId': track.id, - 'path': path, - 'url': url, - 'state': state.index, - 'private': private?1:0 - }; - factory Download.fromSQL(Map 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 -} \ No newline at end of file diff --git a/lib/api/player.dart b/lib/api/player.dart index b9c1d61..32df6ea 100644 --- a/lib/api/player.dart +++ b/lib/api/player.dart @@ -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 out = []; - for (int i=0; i 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; diff --git a/lib/translations.i18n.dart b/lib/translations.i18n.dart index 8b78dd8..11def31 100644 --- a/lib/translations.i18n.dart +++ b/lib/translations.i18n.dart @@ -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 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 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); +} \ No newline at end of file diff --git a/lib/ui/login_screen.dart b/lib/ui/login_screen.dart index 069d807..a53baed 100644 --- a/lib/ui/login_screen.dart +++ b/lib/ui/login_screen.dart @@ -46,6 +46,26 @@ class _LoginWidgetState extends State { } } + //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 { @override void initState() { _start(); + _checkAvailability(); super.initState(); } diff --git a/lib/ui/player_screen.dart b/lib/ui/player_screen.dart index df40bda..5fe8c45 100644 --- a/lib/ui/player_screen.dart +++ b/lib/ui/player_screen.dart @@ -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 { _mediaItemSub = AudioService.currentMediaItemStream.listen((event) { _updateColor(); }); - + + updateColor = this._updateColor; super.initState(); } @@ -361,10 +365,18 @@ class _PlayerScreenVerticalState extends State { children: [ 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(); }, ), ], diff --git a/lib/ui/settings_screen.dart b/lib/ui/settings_screen.dart index 2a10c24..f648f65 100644 --- a/lib/ui/settings_screen.dart +++ b/lib/ui/settings_screen.dart @@ -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 { - List> _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> _l = supportedLocales.map>((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 { 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 { } } +class ContentLanguage { + String code; + String name; + ContentLanguage(this.code, this.name); + + static List 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 { 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>((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(); + }, + )), ) ); }, diff --git a/pubspec.lock b/pubspec.lock index 1bf81d9..3d22882 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,14 +21,14 @@ packages: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.6.0" + version: "2.0.0" async: dependency: "direct main" description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0-nullsafety.1" + version: "2.5.0" audio_service: dependency: "direct main" description: @@ -49,7 +49,7 @@ packages: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.1" + version: "2.1.0" build: dependency: transitive description: @@ -119,14 +119,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.3" + version: "1.1.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0" checked_yaml: dependency: transitive description: @@ -154,7 +154,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.1" + version: "1.1.0" code_builder: dependency: transitive description: @@ -168,7 +168,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0-nullsafety.3" + version: "1.15.0" connectivity: dependency: "direct main" description: @@ -217,9 +217,9 @@ packages: name: country_pickers url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "2.0.0" crypto: - dependency: "direct main" + dependency: transitive description: name: crypto url: "https://pub.dartlang.org" @@ -235,17 +235,19 @@ packages: custom_navigator: dependency: "direct main" description: - name: custom_navigator - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.0" + path: "." + ref: HEAD + resolved-ref: "84bc85880abaa0d4a0f37098c9e6f4bd58b19b0a" + url: "https://github.com/kjawadDeveloper/Custom-navigator.git" + source: git + version: "0.2.0" dart_style: dependency: transitive description: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "1.3.11" + version: "1.3.12" dio: dependency: transitive description: @@ -301,7 +303,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0" ffi: dependency: transitive description: @@ -315,7 +317,7 @@ packages: name: file url: "https://pub.dartlang.org" source: hosted - version: "5.2.1" + version: "6.1.0" filesize: dependency: "direct main" description: @@ -426,7 +428,7 @@ packages: name: gettext_parser url: "https://pub.dartlang.org" source: hosted - version: "0.1.0+1" + version: "0.2.0" glob: dependency: transitive description: @@ -448,13 +450,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.0" - hex: - dependency: "direct main" - description: - name: hex - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.2" html: dependency: "direct main" description: @@ -489,7 +484,7 @@ packages: name: i18n_extension url: "https://pub.dartlang.org" source: hosted - version: "1.5.1" + version: "4.0.0" import_js_library: dependency: transitive description: @@ -510,7 +505,7 @@ packages: name: intl url: "https://pub.dartlang.org" source: hosted - version: "0.16.1" + version: "0.17.0" io: dependency: transitive description: @@ -524,7 +519,7 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.2" + version: "0.6.3" json_annotation: dependency: "direct main" description: @@ -545,7 +540,7 @@ packages: path: "just_audio/just_audio" relative: true source: path - version: "0.6.9" + version: "0.6.5" just_audio_platform_interface: dependency: transitive description: @@ -559,14 +554,7 @@ packages: path: "just_audio/just_audio_web" relative: true source: path - version: "0.2.3" - language_pickers: - dependency: "direct main" - description: - name: language_pickers - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.0+1" + version: "0.2.1" logging: dependency: transitive description: @@ -587,14 +575,14 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10-nullsafety.1" + version: "0.12.10" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "1.3.0" mime: dependency: transitive description: @@ -622,7 +610,7 @@ packages: name: node_io url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.1.1" numberpicker: dependency: "direct main" description: @@ -671,7 +659,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.1" + version: "1.8.0" path_provider: dependency: "direct main" description: @@ -748,7 +736,7 @@ packages: name: platform url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "3.0.0" plugin_platform_interface: dependency: transitive description: @@ -756,13 +744,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" - pointycastle: - dependency: "direct main" - description: - name: pointycastle - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" pool: dependency: transitive description: @@ -776,7 +757,7 @@ packages: name: process url: "https://pub.dartlang.org" source: hosted - version: "3.0.13" + version: "4.1.0" pub_semver: dependency: transitive description: @@ -865,14 +846,14 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.0-nullsafety.2" + version: "1.8.0" sprintf: dependency: transitive description: name: sprintf url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "6.0.0" sqflite: dependency: "direct main" description: @@ -893,14 +874,14 @@ packages: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.10.0-nullsafety.1" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.1" + version: "2.1.0" stream_transform: dependency: transitive description: @@ -914,7 +895,7 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0-nullsafety.1" + version: "1.1.0" synchronized: dependency: transitive description: @@ -928,14 +909,14 @@ packages: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.1" + version: "1.2.0" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19-nullsafety.2" + version: "0.2.19" timing: dependency: transitive description: @@ -949,7 +930,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0-nullsafety.3" + version: "1.3.0" uni_links: dependency: "direct main" description: @@ -1012,7 +993,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.3" + version: "2.1.0" version: dependency: "direct main" description: @@ -1077,5 +1058,5 @@ packages: source: hosted version: "2.2.1" sdks: - dart: ">=2.10.2 <2.11.0" - flutter: ">=1.22.2 <2.0.0" + dart: ">=2.12.0 <3.0.0" + flutter: ">=1.22.2" diff --git a/pubspec.yaml b/pubspec.yaml index ad8d948..bbfbea2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.6.9+1 +version: 0.6.10+1 environment: sdk: ">=2.8.0 <3.0.0" @@ -33,21 +33,16 @@ dependencies: path_provider: 1.6.10 path: ^1.6.4 sqflite: ^1.3.0+1 - crypto: ^2.1.4 - hex: ^0.1.2 - pointycastle: ^1.0.2 ext_storage: ^1.0.3 permission_handler: ^5.0.0+hotfix.6 connectivity: ^0.4.8+6 - intl: ^0.16.1 + intl: ^0.17.0 filesize: ^1.0.4 fluttertoast: 7.0.4 palette_generator: ^0.2.3 flutter_material_color_picker: ^1.0.5 flutter_inappwebview: ^4.0.0 - custom_navigator: ^0.3.0 - language_pickers: ^0.2.0+1 - country_pickers: ^1.3.0 + country_pickers: ^2.0.0 package_info: ^0.4.1 move_to_background: ^1.0.1 flutter_local_notifications: ^1.4.4+1 @@ -62,13 +57,13 @@ dependencies: flutter_cache_manager: ^1.4.1 cached_network_image: ^2.3.2+1 clipboard: ^0.1.2+8 - i18n_extension: ^1.4.4 + i18n_extension: ^4.0.0 fluttericon: ^1.0.7 url_launcher: ^5.7.2 uni_links: ^0.4.0 share: ^0.6.5+2 numberpicker: ^1.2.1 - quick_actions: ^0.4.0+10 + quick_actions: 0.4.0+10 photo_view: ^0.10.2 draggable_scrollbar: ^0.0.4 scrobblenaut: ^2.0.4 @@ -78,6 +73,9 @@ dependencies: google_fonts: ^1.1.2 equalizer: ^0.0.2+2 extended_math: ^0.0.29+1 + custom_navigator: + git: + url: https://github.com/kjawadDeveloper/Custom-navigator.git audio_session: ^0.0.9 audio_service: