0.5.0 - Rewritten downloads, many bugfixes
This commit is contained in:
parent
f7cbb09bc1
commit
f2f6b202d1
38 changed files with 5176 additions and 1365 deletions
|
@ -1,6 +1,9 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
|
@ -692,6 +695,22 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
|||
}
|
||||
}
|
||||
|
||||
//Load cached playlist sorting
|
||||
void _restoreSort() async {
|
||||
if (cache.playlistSort == null) {
|
||||
cache.playlistSort = {};
|
||||
await cache.save();
|
||||
return;
|
||||
}
|
||||
if (cache.playlistSort[playlist.id] != null) {
|
||||
//Preload tracks
|
||||
if (playlist.tracks.length < playlist.trackCount) {
|
||||
playlist = await deezerAPI.fullPlaylist(playlist.id);
|
||||
}
|
||||
setState(() => _sort = cache.playlistSort[playlist.id]);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
playlist = widget.playlist;
|
||||
|
@ -717,6 +736,8 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
|||
});
|
||||
}
|
||||
|
||||
_restoreSort();
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -817,7 +838,7 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
|||
IconButton(
|
||||
icon: Icon(Icons.favorite, size: 32),
|
||||
onPressed: () async {
|
||||
await deezerAPI.addFavoriteAlbum(playlist.id);
|
||||
await deezerAPI.addPlaylist(playlist.id);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Added to library'.i18n,
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
|
@ -833,7 +854,17 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
|
|||
),
|
||||
PopupMenuButton(
|
||||
child: Icon(Icons.sort, size: 32.0),
|
||||
onSelected: (SortType s) => setState(() => _sort = s),
|
||||
onSelected: (SortType s) async {
|
||||
if (playlist.tracks.length < playlist.trackCount) {
|
||||
//Preload whole playlist
|
||||
playlist = await deezerAPI.fullPlaylist(playlist.id);
|
||||
}
|
||||
setState(() => _sort = s);
|
||||
|
||||
//Save sort type to cache
|
||||
cache.playlistSort[playlist.id] = s;
|
||||
cache.save();
|
||||
},
|
||||
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
|
||||
PopupMenuItem(
|
||||
value: SortType.DEFAULT,
|
||||
|
|
|
@ -1,99 +1,11 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:filesize/filesize.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
|
||||
import 'cached_image.dart';
|
||||
import '../api/download.dart';
|
||||
|
||||
|
||||
class DownloadTile extends StatelessWidget {
|
||||
|
||||
final Download download;
|
||||
Function onDelete;
|
||||
DownloadTile(this.download, {this.onDelete});
|
||||
|
||||
String get subtitle {
|
||||
switch (download.state) {
|
||||
case DownloadState.NONE: return '';
|
||||
case DownloadState.DOWNLOADING:
|
||||
return '${filesize(download.received)} / ${filesize(download.total)}';
|
||||
case DownloadState.POST:
|
||||
return 'Post processing...'.i18n;
|
||||
case DownloadState.DONE:
|
||||
return 'Done'.i18n; //Shouldn't be visible
|
||||
case DownloadState.DEEZER_ERROR:
|
||||
return 'Track is not available on Deezer!'.i18n;
|
||||
case DownloadState.ERROR:
|
||||
return 'Failed to download track! Please restart.'.i18n;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
Widget get progressBar {
|
||||
switch (download.state) {
|
||||
case DownloadState.DOWNLOADING:
|
||||
return LinearProgressIndicator(value: download.received / download.total);
|
||||
case DownloadState.POST:
|
||||
return LinearProgressIndicator();
|
||||
default:
|
||||
return Container(height: 0, width: 0,);
|
||||
}
|
||||
}
|
||||
|
||||
Widget get trailing {
|
||||
if (download.private) {
|
||||
return Icon(Icons.offline_pin);
|
||||
}
|
||||
return Icon(Icons.sd_card);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text(download.track.title),
|
||||
subtitle: Text(subtitle),
|
||||
leading: CachedImage(
|
||||
url: download.track.albumArt.thumb,
|
||||
width: 48.0,
|
||||
),
|
||||
trailing: trailing,
|
||||
onTap: () {
|
||||
//Delete if none
|
||||
if (download.state == DownloadState.NONE) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('Delete'.i18n),
|
||||
content: Text('Are you sure you want to delete this download?'.i18n),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('Cancel'.i18n),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('Delete'.i18n),
|
||||
onPressed: () {
|
||||
downloadManager.removeDownload(download);
|
||||
if (this.onDelete != null) this.onDelete();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
progressBar
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadsScreen extends StatefulWidget {
|
||||
@override
|
||||
|
@ -101,6 +13,55 @@ class DownloadsScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _DownloadsScreenState extends State<DownloadsScreen> {
|
||||
|
||||
List<Download> downloads = [];
|
||||
StreamSubscription _stateSubscription;
|
||||
|
||||
//Sublists
|
||||
List<Download> get downloading => downloads.where((d) => d.state == DownloadState.DOWNLOADING || d.state == DownloadState.POST).toList();
|
||||
List<Download> get queued => downloads.where((d) => d.state == DownloadState.NONE).toList();
|
||||
List<Download> get failed => downloads.where((d) => d.state == DownloadState.ERROR || d.state == DownloadState.DEEZER_ERROR).toList();
|
||||
List<Download> get finished => downloads.where((d) => d.state == DownloadState.DONE).toList();
|
||||
|
||||
Future _load() async {
|
||||
//Load downloads
|
||||
List<Download> _d = await downloadManager.getDownloads();
|
||||
setState(() {
|
||||
downloads = _d;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_load();
|
||||
|
||||
//Subscribe to state update
|
||||
_stateSubscription = downloadManager.serviceEvents.stream.listen((e) {
|
||||
//State change = update
|
||||
if (e['action'] == 'onStateChange') {
|
||||
setState(() => downloadManager.running = downloadManager.running);
|
||||
}
|
||||
//Progress change
|
||||
if (e['action'] == 'onProgress') {
|
||||
setState(() {
|
||||
for (Map su in e['data']) {
|
||||
downloads.firstWhere((d) => d.id == su['id'], orElse: () => Download()).updateFromJson(su);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_stateSubscription != null)
|
||||
_stateSubscription.cancel();
|
||||
_stateSubscription = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -108,100 +69,216 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
|
|||
title: Text('Downloads'.i18n),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(downloadManager.stopped ? Icons.play_arrow : Icons.stop),
|
||||
icon:
|
||||
Icon(downloadManager.running ? Icons.stop : Icons.play_arrow),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
if (downloadManager.stopped) downloadManager.start();
|
||||
else downloadManager.stop();
|
||||
if (downloadManager.running)
|
||||
downloadManager.stop();
|
||||
else
|
||||
downloadManager.start();
|
||||
});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
StreamBuilder(
|
||||
stream: Stream.periodic(Duration(milliseconds: 500)).asBroadcastStream(), //Periodic to get current download progress
|
||||
builder: (BuildContext context, AsyncSnapshot snapshot) {
|
||||
children: [
|
||||
//Now downloading
|
||||
Container(height: 2.0),
|
||||
Column(children: List.generate(downloading.length, (int i) => DownloadTile(
|
||||
downloading[i],
|
||||
updateCallback: () => _load(),
|
||||
))),
|
||||
Container(height: 8.0),
|
||||
|
||||
if (downloadManager.queue.length == 0)
|
||||
return Container(width: 0, height: 0,);
|
||||
//Queued
|
||||
if (queued.length > 0)
|
||||
Text(
|
||||
'Queued'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 24.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
Column(children: List.generate(queued.length, (int i) => DownloadTile(
|
||||
queued[i],
|
||||
updateCallback: () => _load(),
|
||||
))),
|
||||
if (queued.length > 0)
|
||||
ListTile(
|
||||
title: Text('Clear queue'.i18n),
|
||||
leading: Icon(Icons.delete),
|
||||
onTap: () async {
|
||||
await downloadManager.removeDownloads(DownloadState.NONE);
|
||||
await _load();
|
||||
},
|
||||
),
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
...List.generate(downloadManager.queue.length, (i) {
|
||||
return DownloadTile(downloadManager.queue[i], onDelete: () => setState(() => {}));
|
||||
}),
|
||||
if (downloadManager.queue.length > 1 || (downloadManager.stopped && downloadManager.queue.length > 0))
|
||||
ListTile(
|
||||
title: Text('Clear queue'.i18n),
|
||||
subtitle: Text("This won't delete currently downloading item".i18n),
|
||||
leading: Icon(Icons.delete),
|
||||
onTap: () async {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('Delete'.i18n),
|
||||
content: Text('Are you sure you want to delete all queued downloads?'.i18n),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('Cancel'.i18n),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('Delete'.i18n),
|
||||
onPressed: () async {
|
||||
await downloadManager.clearQueue();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
)
|
||||
]
|
||||
);
|
||||
},
|
||||
),
|
||||
FutureBuilder(
|
||||
future: downloadManager.getFinishedDownloads(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data.length == 0) return Container(height: 0, width: 0,);
|
||||
//Failed
|
||||
if (failed.length > 0)
|
||||
Text(
|
||||
'Failed'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 24.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
Column(children: List.generate(failed.length, (int i) => DownloadTile(
|
||||
failed[i],
|
||||
updateCallback: () => _load(),
|
||||
))),
|
||||
//Restart failed
|
||||
if (failed.length > 0)
|
||||
ListTile(
|
||||
title: Text('Restart failed downloads'.i18n),
|
||||
leading: Icon(Icons.restore),
|
||||
onTap: () async {
|
||||
await downloadManager.retryDownloads();
|
||||
await _load();
|
||||
},
|
||||
),
|
||||
if (failed.length > 0)
|
||||
ListTile(
|
||||
title: Text('Clear failed'.i18n),
|
||||
leading: Icon(Icons.delete),
|
||||
onTap: () async {
|
||||
await downloadManager.removeDownloads(DownloadState.ERROR);
|
||||
await downloadManager.removeDownloads(DownloadState.DEEZER_ERROR);
|
||||
await _load();
|
||||
},
|
||||
),
|
||||
|
||||
//Finished
|
||||
if (finished.length > 0)
|
||||
Text(
|
||||
'Done'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 24.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
Column(children: List.generate(finished.length, (int i) => DownloadTile(
|
||||
finished[i],
|
||||
updateCallback: () => _load(),
|
||||
))),
|
||||
if (finished.length > 0)
|
||||
ListTile(
|
||||
title: Text('Clear downloads history'.i18n),
|
||||
leading: Icon(Icons.delete),
|
||||
onTap: () async {
|
||||
await downloadManager.removeDownloads(DownloadState.DONE);
|
||||
await _load();
|
||||
},
|
||||
),
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Divider(),
|
||||
Text(
|
||||
'History',
|
||||
style: TextStyle(
|
||||
fontSize: 24.0,
|
||||
fontWeight: FontWeight.bold
|
||||
),
|
||||
),
|
||||
...List.generate(snapshot.data.length, (i) {
|
||||
Download d = snapshot.data[i];
|
||||
return DownloadTile(d);
|
||||
}),
|
||||
ListTile(
|
||||
title: Text('Clear downloads history'.i18n),
|
||||
leading: Icon(Icons.delete),
|
||||
subtitle: Text('WARNING: This will only clear non-offline (external downloads)'.i18n),
|
||||
onTap: () async {
|
||||
await downloadManager.cleanDownloadHistory();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadTile extends StatelessWidget {
|
||||
|
||||
final Download download;
|
||||
final Function updateCallback;
|
||||
DownloadTile(this.download, {this.updateCallback});
|
||||
|
||||
String subtitle() {
|
||||
String out = '';
|
||||
//Download type
|
||||
if (download.private) out += 'Offline'.i18n;
|
||||
else out += 'External'.i18n;
|
||||
out += ' | ';
|
||||
//Quality
|
||||
if (download.quality == 9) out += 'FLAC';
|
||||
if (download.quality == 3) out += 'MP3 320kbps';
|
||||
if (download.quality == 1) out += 'MP3 128kbps';
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
Future onClick(BuildContext context) async {
|
||||
if (download.state != DownloadState.DOWNLOADING && download.state != DownloadState.POST) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('Delete'.i18n),
|
||||
content: Text('Are you sure you want to delete this download?'.i18n),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('Cancel'.i18n),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('Delete'.i18n),
|
||||
onPressed: () async {
|
||||
await downloadManager.removeDownload(download.id);
|
||||
if (updateCallback != null) updateCallback();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//Trailing icon with state
|
||||
Widget trailing() {
|
||||
switch (download.state) {
|
||||
case DownloadState.NONE:
|
||||
return Icon(
|
||||
Icons.query_builder,
|
||||
);
|
||||
case DownloadState.DOWNLOADING:
|
||||
return Icon(
|
||||
Icons.download_rounded
|
||||
);
|
||||
case DownloadState.POST:
|
||||
return Icon(
|
||||
Icons.miscellaneous_services
|
||||
);
|
||||
case DownloadState.DONE:
|
||||
return Icon(
|
||||
Icons.done,
|
||||
color: Colors.green,
|
||||
);
|
||||
case DownloadState.DEEZER_ERROR:
|
||||
return Icon(
|
||||
Icons.error,
|
||||
color: Colors.blue
|
||||
);
|
||||
case DownloadState.ERROR:
|
||||
return Icon(
|
||||
Icons.error,
|
||||
color: Colors.red
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(download.title),
|
||||
leading: CachedImage(url: download.image),
|
||||
subtitle: Text(subtitle()),
|
||||
trailing: trailing(),
|
||||
onTap: () => onClick(context),
|
||||
),
|
||||
if (download.state == DownloadState.DOWNLOADING)
|
||||
LinearProgressIndicator(value: download.progress),
|
||||
if (download.state == DownloadState.POST)
|
||||
LinearProgressIndicator(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:connectivity/connectivity.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/definitions.dart';
|
||||
import 'package:freezer/api/player.dart';
|
||||
|
@ -57,7 +58,7 @@ class LibraryScreen extends StatelessWidget {
|
|||
body: ListView(
|
||||
children: <Widget>[
|
||||
Container(height: 4.0,),
|
||||
if (downloadManager.stopped && downloadManager.queue.length > 0)
|
||||
if (!downloadManager.running && downloadManager.queueSize > 0)
|
||||
ListTile(
|
||||
title: Text('Downloads'.i18n),
|
||||
leading: Icon(Icons.file_download),
|
||||
|
@ -70,7 +71,7 @@ class LibraryScreen extends StatelessWidget {
|
|||
},
|
||||
),
|
||||
//Dirty if to not use columns
|
||||
if (downloadManager.stopped && downloadManager.queue.length > 0)
|
||||
if (!downloadManager.running && downloadManager.queueSize > 0)
|
||||
Divider(),
|
||||
|
||||
ListTile(
|
||||
|
@ -109,6 +110,15 @@ class LibraryScreen extends StatelessWidget {
|
|||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('History'.i18n),
|
||||
leading: Icon(Icons.history),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => HistoryScreen())
|
||||
);
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
ListTile(
|
||||
title: Text('Import'.i18n),
|
||||
|
@ -196,14 +206,49 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
ScrollController _scrollController = ScrollController();
|
||||
List<Track> tracks = [];
|
||||
List<Track> allTracks = [];
|
||||
int trackCount;
|
||||
|
||||
Playlist get _playlist => Playlist(id: deezerAPI.favoritesPlaylistId);
|
||||
|
||||
Future _load() async {
|
||||
//Already loaded
|
||||
if (trackCount != null && tracks.length >= trackCount) {
|
||||
//Update tracks cache if fully loaded
|
||||
if (cache.libraryTracks == null || cache.libraryTracks.length != trackCount) {
|
||||
setState(() {
|
||||
cache.libraryTracks = tracks.map((t) => t.id).toList();
|
||||
});
|
||||
await cache.save();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ConnectivityResult connectivity = await Connectivity().checkConnectivity();
|
||||
if (connectivity != ConnectivityResult.none) {
|
||||
setState(() => _loading = true);
|
||||
int pos = tracks.length;
|
||||
|
||||
if (trackCount == null || tracks.length == 0) {
|
||||
//Load tracks as a playlist
|
||||
Playlist favPlaylist;
|
||||
try {
|
||||
favPlaylist = await deezerAPI.playlist(deezerAPI.favoritesPlaylistId);
|
||||
} catch (e) {}
|
||||
//Error loading
|
||||
if (favPlaylist == null) {
|
||||
setState(() => _loading = false);
|
||||
return;
|
||||
}
|
||||
//Update
|
||||
setState(() {
|
||||
trackCount = favPlaylist.trackCount;
|
||||
tracks = favPlaylist.tracks;
|
||||
_makeFavorite();
|
||||
_loading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//Load another page of tracks from deezer
|
||||
List<Track> _t;
|
||||
try {
|
||||
|
@ -216,6 +261,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
}
|
||||
setState(() {
|
||||
tracks.addAll(_t);
|
||||
_makeFavorite();
|
||||
_loading = false;
|
||||
});
|
||||
|
||||
|
@ -236,6 +282,12 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
});
|
||||
}
|
||||
|
||||
//Update tracks with favorite true
|
||||
void _makeFavorite() {
|
||||
for (int i=0; i<tracks.length; i++)
|
||||
tracks[i].favorite = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_scrollController.addListener(() {
|
||||
|
@ -257,6 +309,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
|
|||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Tracks'.i18n),),
|
||||
body: ListView(
|
||||
controller: _scrollController,
|
||||
children: <Widget>[
|
||||
Card(
|
||||
child: Column(
|
||||
|
@ -554,7 +607,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
ListTile(
|
||||
title: Text('Create new playlist'.i18n),
|
||||
leading: Icon(Icons.playlist_add),
|
||||
onTap: () {
|
||||
onTap: () async {
|
||||
if (settings.offlineMode) {
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Cannot create playlists in offline mode'.i18n,
|
||||
|
@ -563,7 +616,8 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
return;
|
||||
}
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.createPlaylist();
|
||||
await m.createPlaylist();
|
||||
await _load();
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
|
@ -586,6 +640,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
favoritesPlaylist.library = true;
|
||||
m.defaultPlaylistMenu(favoritesPlaylist);
|
||||
},
|
||||
),
|
||||
|
@ -600,9 +655,10 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
)),
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultPlaylistMenu(p, onRemove: () {
|
||||
setState(() => _playlists.remove(p));
|
||||
});
|
||||
m.defaultPlaylistMenu(
|
||||
p,
|
||||
onRemove: () {setState(() => _playlists.remove(p));},
|
||||
onUpdate: () {_load();});
|
||||
},
|
||||
);
|
||||
}),
|
||||
|
@ -653,3 +709,49 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryScreen extends StatefulWidget {
|
||||
@override
|
||||
_HistoryScreenState createState() => _HistoryScreenState();
|
||||
}
|
||||
|
||||
class _HistoryScreenState extends State<HistoryScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('History'.i18n),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete_sweep),
|
||||
onPressed: () {
|
||||
setState(() => cache.history = []);
|
||||
cache.save();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: ListView.builder(
|
||||
itemCount: (cache.history??[]).length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
Track t = cache.history[i];
|
||||
return TrackTile(
|
||||
t,
|
||||
onTap: () {
|
||||
playerHelper.playFromTrackList(cache.history, t.id, QueueSource(
|
||||
id: null,
|
||||
text: 'History'.i18n,
|
||||
source: 'history'
|
||||
));
|
||||
},
|
||||
onHold: () {
|
||||
MenuSheet m = MenuSheet(context);
|
||||
m.defaultTrackMenu(t);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ class LoginWidget extends StatefulWidget {
|
|||
class _LoginWidgetState extends State<LoginWidget> {
|
||||
|
||||
String _arl;
|
||||
String _error;
|
||||
|
||||
//Initialize deezer etc
|
||||
Future _init() async {
|
||||
|
@ -62,7 +63,14 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('Error'.i18n),
|
||||
content: Text('Error logging in! Please check your token and internet connection and try again.'.i18n),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Error logging in! Please check your token and internet connection and try again.'.i18n),
|
||||
if (_error != null)
|
||||
Text('\n\n$_error')
|
||||
],
|
||||
),
|
||||
actions: <Widget>[
|
||||
FlatButton(
|
||||
child: Text('Dismiss'.i18n),
|
||||
|
@ -82,13 +90,15 @@ class _LoginWidgetState extends State<LoginWidget> {
|
|||
//Try logging in
|
||||
try {
|
||||
deezerAPI.arl = settings.arl;
|
||||
bool resp = await deezerAPI.authorize();
|
||||
bool resp = await deezerAPI.rawAuthorize(onError: (e) => _error = e.toString());
|
||||
if (resp == false) { //false, not null
|
||||
setState(() => settings.arl = null);
|
||||
errorDialog();
|
||||
}
|
||||
//On error show dialog and reset to null
|
||||
} catch (e) {
|
||||
_error = e;
|
||||
print('Login error: ' + e);
|
||||
setState(() => settings.arl = null);
|
||||
errorDialog();
|
||||
}
|
||||
|
|
277
lib/ui/menu.dart
277
lib/ui/menu.dart
|
@ -1,7 +1,10 @@
|
|||
import 'dart:ffi';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezer/api/cache.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/ui/details_screens.dart';
|
||||
|
@ -123,7 +126,7 @@ class MenuSheet {
|
|||
showWithTrack(track, [
|
||||
addToQueueNext(track),
|
||||
addToQueue(track),
|
||||
(track.favorite??false)?removeFavoriteTrack(track, onUpdate: onRemove):addTrackFavorite(track),
|
||||
(cache.checkTrackFavorite(track))?removeFavoriteTrack(track, onUpdate: onRemove):addTrackFavorite(track),
|
||||
addToPlaylist(track),
|
||||
downloadTrack(track),
|
||||
showAlbum(track.album),
|
||||
|
@ -169,6 +172,11 @@ class MenuSheet {
|
|||
gravity: ToastGravity.BOTTOM,
|
||||
toastLength: Toast.LENGTH_SHORT
|
||||
);
|
||||
//Add to cache
|
||||
if (cache.libraryTracks == null)
|
||||
cache.libraryTracks = [];
|
||||
cache.libraryTracks.add(t.id);
|
||||
|
||||
_close();
|
||||
}
|
||||
);
|
||||
|
@ -179,6 +187,7 @@ class MenuSheet {
|
|||
onTap: () async {
|
||||
await downloadManager.addOfflineTrack(t, private: false);
|
||||
_close();
|
||||
showDownloadStartedToast();
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -186,76 +195,24 @@ class MenuSheet {
|
|||
title: Text('Add to playlist'.i18n),
|
||||
leading: Icon(Icons.playlist_add),
|
||||
onTap: () async {
|
||||
|
||||
Playlist p;
|
||||
|
||||
//Show dialog to pick playlist
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('Select playlist'.i18n),
|
||||
content: FutureBuilder(
|
||||
future: deezerAPI.getPlaylists(),
|
||||
builder: (context, snapshot) {
|
||||
|
||||
if (snapshot.hasError) SizedBox(
|
||||
height: 100,
|
||||
child: ErrorScreen(),
|
||||
);
|
||||
if (snapshot.connectionState != ConnectionState.done) return SizedBox(
|
||||
height: 100,
|
||||
child: Center(child: CircularProgressIndicator(),),
|
||||
);
|
||||
|
||||
List<Playlist> playlists = snapshot.data;
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...List.generate(playlists.length, (i) => ListTile(
|
||||
title: Text(playlists[i].title),
|
||||
leading: CachedImage(
|
||||
url: playlists[i].image.thumb,
|
||||
),
|
||||
onTap: () {
|
||||
p = playlists[i];
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)),
|
||||
ListTile(
|
||||
title: Text('Create new playlist'.i18n),
|
||||
leading: Icon(Icons.add),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => CreatePlaylistDialog(tracks: [t],)
|
||||
);
|
||||
},
|
||||
)
|
||||
]
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return SelectPlaylistDialog(track: t, callback: (Playlist p) async {
|
||||
await deezerAPI.addToPlaylist(t.id, p.id);
|
||||
//Update the playlist if offline
|
||||
if (await downloadManager.checkOffline(playlist: p)) {
|
||||
downloadManager.addOfflinePlaylist(p);
|
||||
}
|
||||
Fluttertoast.showToast(
|
||||
msg: "Track added to".i18n + " ${p.title}",
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
}
|
||||
);
|
||||
//Add to playlist, show toast
|
||||
if (p != null) {
|
||||
await deezerAPI.addToPlaylist(t.id, p.id);
|
||||
//Update the playlist if offline
|
||||
if (await downloadManager.checkOffline(playlist: p)) {
|
||||
downloadManager.addOfflinePlaylist(p);
|
||||
});
|
||||
}
|
||||
Fluttertoast.showToast(
|
||||
msg: "Track added to".i18n + " ${p.title}",
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
);
|
||||
}
|
||||
|
||||
);
|
||||
_close();
|
||||
},
|
||||
);
|
||||
|
@ -284,12 +241,16 @@ class MenuSheet {
|
|||
if (await downloadManager.checkOffline(playlist: p)) {
|
||||
await downloadManager.addOfflinePlaylist(p);
|
||||
}
|
||||
//Remove from cache
|
||||
if (cache.libraryTracks != null)
|
||||
cache.libraryTracks.removeWhere((i) => i == t.id);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Track removed from library'.i18n,
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
gravity: ToastGravity.BOTTOM
|
||||
);
|
||||
onUpdate();
|
||||
if (onUpdate != null)
|
||||
onUpdate();
|
||||
_close();
|
||||
},
|
||||
);
|
||||
|
@ -348,8 +309,9 @@ class MenuSheet {
|
|||
title: Text('Download'.i18n),
|
||||
leading: Icon(Icons.file_download),
|
||||
onTap: () async {
|
||||
await downloadManager.addOfflineAlbum(a, private: false);
|
||||
_close();
|
||||
await downloadManager.addOfflineAlbum(a, private: false);
|
||||
showDownloadStartedToast();
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -360,6 +322,7 @@ class MenuSheet {
|
|||
await deezerAPI.addFavoriteAlbum(a.id);
|
||||
await downloadManager.addOfflineAlbum(a, private: true);
|
||||
_close();
|
||||
showDownloadStartedToast();
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -441,11 +404,13 @@ class MenuSheet {
|
|||
// PLAYLIST
|
||||
//===================
|
||||
|
||||
void defaultPlaylistMenu(Playlist playlist, {List<Widget> options = const [], Function onRemove}) {
|
||||
void defaultPlaylistMenu(Playlist playlist, {List<Widget> options = const [], Function onRemove, Function onUpdate}) {
|
||||
show([
|
||||
playlist.library?removePlaylistLibrary(playlist, onRemove: onRemove):addPlaylistLibrary(playlist),
|
||||
addPlaylistOffline(playlist),
|
||||
downloadPlaylist(playlist),
|
||||
if (playlist.user.id == deezerAPI.userId)
|
||||
editPlaylist(playlist, onUpdate: onUpdate),
|
||||
...options
|
||||
]);
|
||||
}
|
||||
|
@ -492,6 +457,7 @@ class MenuSheet {
|
|||
await deezerAPI.addPlaylist(p.id);
|
||||
downloadManager.addOfflinePlaylist(p, private: true);
|
||||
_close();
|
||||
showDownloadStartedToast();
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -501,6 +467,21 @@ class MenuSheet {
|
|||
onTap: () async {
|
||||
downloadManager.addOfflinePlaylist(p, private: false);
|
||||
_close();
|
||||
showDownloadStartedToast();
|
||||
},
|
||||
);
|
||||
|
||||
Widget editPlaylist(Playlist p, {Function onUpdate}) => ListTile(
|
||||
title: Text('Edit playlist'.i18n),
|
||||
leading: Icon(Icons.edit),
|
||||
onTap: () async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => CreatePlaylistDialog(playlist: p)
|
||||
);
|
||||
_close();
|
||||
if (onUpdate != null)
|
||||
onUpdate();
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -509,9 +490,17 @@ class MenuSheet {
|
|||
// OTHER
|
||||
//===================
|
||||
|
||||
showDownloadStartedToast() {
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Downloads added!'.i18n,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastLength: Toast.LENGTH_SHORT
|
||||
);
|
||||
}
|
||||
|
||||
//Create playlist
|
||||
void createPlaylist() {
|
||||
showDialog(
|
||||
Future createPlaylist() async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CreatePlaylistDialog();
|
||||
|
@ -523,11 +512,90 @@ class MenuSheet {
|
|||
void _close() => Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
class SelectPlaylistDialog extends StatefulWidget {
|
||||
|
||||
final Track track;
|
||||
final Function callback;
|
||||
SelectPlaylistDialog({this.track, this.callback, Key key}): super(key: key);
|
||||
|
||||
@override
|
||||
_SelectPlaylistDialogState createState() => _SelectPlaylistDialogState();
|
||||
}
|
||||
|
||||
class _SelectPlaylistDialogState extends State<SelectPlaylistDialog> {
|
||||
|
||||
bool createNew = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
//Create new playlist
|
||||
if (createNew) {
|
||||
if (widget.track == null) {
|
||||
return CreatePlaylistDialog();
|
||||
}
|
||||
return CreatePlaylistDialog(tracks: [widget.track]);
|
||||
}
|
||||
|
||||
|
||||
return AlertDialog(
|
||||
title: Text('Select playlist'.i18n),
|
||||
content: FutureBuilder(
|
||||
future: deezerAPI.getPlaylists(),
|
||||
builder: (context, snapshot) {
|
||||
|
||||
if (snapshot.hasError) SizedBox(
|
||||
height: 100,
|
||||
child: ErrorScreen(),
|
||||
);
|
||||
if (snapshot.connectionState != ConnectionState.done) return SizedBox(
|
||||
height: 100,
|
||||
child: Center(child: CircularProgressIndicator(),),
|
||||
);
|
||||
|
||||
List<Playlist> playlists = snapshot.data;
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...List.generate(playlists.length, (i) => ListTile(
|
||||
title: Text(playlists[i].title),
|
||||
leading: CachedImage(
|
||||
url: playlists[i].image.thumb,
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.callback != null) {
|
||||
widget.callback(playlists[i]);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)),
|
||||
ListTile(
|
||||
title: Text('Create new playlist'.i18n),
|
||||
leading: Icon(Icons.add),
|
||||
onTap: () async {
|
||||
setState(() {
|
||||
createNew = true;
|
||||
});
|
||||
},
|
||||
)
|
||||
]
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class CreatePlaylistDialog extends StatefulWidget {
|
||||
|
||||
final List<Track> tracks;
|
||||
CreatePlaylistDialog({this.tracks, Key key}): super(key: key);
|
||||
//If playlist not null, update
|
||||
final Playlist playlist;
|
||||
CreatePlaylistDialog({this.tracks, this.playlist, Key key}): super(key: key);
|
||||
|
||||
@override
|
||||
_CreatePlaylistDialogState createState() => _CreatePlaylistDialogState();
|
||||
|
@ -538,11 +606,28 @@ class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
|
|||
int _playlistType = 1;
|
||||
String _title = '';
|
||||
String _description = '';
|
||||
TextEditingController _titleController;
|
||||
TextEditingController _descController;
|
||||
|
||||
//Create or edit mode
|
||||
bool get edit => widget.playlist != null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
||||
//Edit playlist mode
|
||||
if (edit) {
|
||||
_titleController = TextEditingController(text: widget.playlist.title);
|
||||
_descController = TextEditingController(text: widget.playlist.description);
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Create playlist'.i18n),
|
||||
title: Text(edit ? 'Edit playlist'.i18n : 'Create playlist'.i18n),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
|
@ -550,10 +635,12 @@ class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
|
|||
decoration: InputDecoration(
|
||||
labelText: 'Title'.i18n
|
||||
),
|
||||
controller: _titleController ?? TextEditingController(),
|
||||
onChanged: (String s) => _title = s,
|
||||
),
|
||||
TextField(
|
||||
onChanged: (String s) => _description = s,
|
||||
controller: _descController ?? TextEditingController(),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Description'.i18n
|
||||
),
|
||||
|
@ -583,22 +670,36 @@ class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
|
|||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('Create'.i18n),
|
||||
child: Text(edit ? 'Update'.i18n : 'Create'.i18n),
|
||||
onPressed: () async {
|
||||
List<String> tracks = [];
|
||||
if (widget.tracks != null) {
|
||||
tracks = widget.tracks.map<String>((t) => t.id).toList();
|
||||
if (edit) {
|
||||
//Update
|
||||
await deezerAPI.updatePlaylist(
|
||||
widget.playlist.id,
|
||||
_titleController.value.text,
|
||||
_descController.value.text,
|
||||
status: _playlistType
|
||||
);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Playlist updated!'.i18n,
|
||||
gravity: ToastGravity.BOTTOM
|
||||
);
|
||||
} else {
|
||||
List<String> tracks = [];
|
||||
if (widget.tracks != null) {
|
||||
tracks = widget.tracks.map<String>((t) => t.id).toList();
|
||||
}
|
||||
await deezerAPI.createPlaylist(
|
||||
_title,
|
||||
status: _playlistType,
|
||||
description: _description,
|
||||
trackIds: tracks
|
||||
);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Playlist created!'.i18n,
|
||||
gravity: ToastGravity.BOTTOM
|
||||
);
|
||||
}
|
||||
await deezerAPI.createPlaylist(
|
||||
_title,
|
||||
status: _playlistType,
|
||||
description: _description,
|
||||
trackIds: tracks
|
||||
);
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Playlist created!'.i18n,
|
||||
gravity: ToastGravity.BOTTOM
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
|
|
|
@ -42,6 +42,7 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||
playerHelper.startService();
|
||||
return Center(child: CircularProgressIndicator(),);
|
||||
}
|
||||
|
||||
return OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
//Landscape
|
||||
|
@ -388,9 +389,19 @@ class _LyricsWidgetState extends State<LyricsWidget> {
|
|||
_l = await deezerAPI.lyrics(_trackId);
|
||||
setState(() => _loading = false);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
//Error Lyrics
|
||||
setState(() => _l = Lyrics().error);
|
||||
setState(() => _l = Lyrics.error());
|
||||
}
|
||||
|
||||
//Empty lyrics
|
||||
if (_l.lyrics.length == 0) {
|
||||
setState(() {
|
||||
_l = Lyrics.error();
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
//Use provided lyrics
|
||||
_l = widget.lyrics;
|
||||
|
|
|
@ -11,6 +11,32 @@ import '../api/deezer.dart';
|
|||
import '../api/definitions.dart';
|
||||
import 'error.dart';
|
||||
|
||||
|
||||
openScreenByURL(BuildContext context, String url) async {
|
||||
DeezerLinkResponse res = await deezerAPI.parseLink(url);
|
||||
if (res == null) return;
|
||||
|
||||
switch (res.type) {
|
||||
case DeezerLinkType.TRACK:
|
||||
Track t = await deezerAPI.track(res.id);
|
||||
MenuSheet(context).defaultTrackMenu(t);
|
||||
break;
|
||||
case DeezerLinkType.ALBUM:
|
||||
Album a = await deezerAPI.album(res.id);
|
||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) => AlbumDetails(a)));
|
||||
break;
|
||||
case DeezerLinkType.ARTIST:
|
||||
Artist a = await deezerAPI.artist(res.id);
|
||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) => ArtistDetails(a)));
|
||||
break;
|
||||
case DeezerLinkType.PLAYLIST:
|
||||
Playlist p = await deezerAPI.playlist(res.id);
|
||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) => PlaylistDetails(p)));
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SearchScreen extends StatefulWidget {
|
||||
@override
|
||||
_SearchScreenState createState() => _SearchScreenState();
|
||||
|
@ -20,11 +46,23 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
|
||||
String _query;
|
||||
bool _offline = false;
|
||||
bool _loading = false;
|
||||
TextEditingController _controller = new TextEditingController();
|
||||
List _suggestions = [];
|
||||
|
||||
void _submit(BuildContext context, {String query}) {
|
||||
void _submit(BuildContext context, {String query}) async {
|
||||
if (query != null) _query = query;
|
||||
|
||||
//URL
|
||||
if (_query.startsWith('http')) {
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
await openScreenByURL(context, _query);
|
||||
} catch (e) {}
|
||||
setState(() => _loading = false);
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => SearchResultsScreen(_query, offline: _offline,))
|
||||
);
|
||||
|
@ -45,7 +83,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
|
||||
//Load search suggestions
|
||||
Future<List<String>> _loadSuggestions() async {
|
||||
if (_query == null || _query.length < 2) return null;
|
||||
if (_query == null || _query.length < 2 || _query.startsWith('http')) return null;
|
||||
String q = _query;
|
||||
await Future.delayed(Duration(milliseconds: 300));
|
||||
if (q != _query) return null;
|
||||
|
@ -75,7 +113,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
_loadSuggestions();
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Search'.i18n
|
||||
labelText: 'Search or paste URL'.i18n
|
||||
),
|
||||
controller: _controller,
|
||||
onSubmitted: (String s) => _submit(context, query: s),
|
||||
|
@ -112,6 +150,8 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
},
|
||||
),
|
||||
),
|
||||
if (_loading)
|
||||
LinearProgressIndicator(),
|
||||
Divider(),
|
||||
...List.generate((_suggestions??[]).length, (i) => ListTile(
|
||||
title: Text(_suggestions[i]),
|
||||
|
|
|
@ -6,9 +6,12 @@ import 'package:flutter/cupertino.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
|
||||
import 'package:fluttericon/font_awesome5_icons.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezer/api/deezer.dart';
|
||||
import 'package:freezer/api/download.dart';
|
||||
import 'package:freezer/ui/error.dart';
|
||||
import 'package:freezer/ui/home_screen.dart';
|
||||
import 'package:i18n_extension/i18n_widget.dart';
|
||||
import 'package:language_pickers/language_pickers.dart';
|
||||
import 'package:language_pickers/languages.dart';
|
||||
|
@ -17,6 +20,7 @@ import 'package:path_provider_ex/path_provider_ex.dart';
|
|||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:freezer/translations.i18n.dart';
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../settings.dart';
|
||||
import '../main.dart';
|
||||
|
@ -30,20 +34,8 @@ class SettingsScreen extends StatefulWidget {
|
|||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
String _about = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
//Load about text
|
||||
PackageInfo.fromPlatform().then((PackageInfo info) {
|
||||
setState(() {
|
||||
_about = '${info.appName}';
|
||||
});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
List<Map<String, String>> _languages() {
|
||||
//Missing language
|
||||
defaultLanguagesList.add({
|
||||
'name': 'Filipino',
|
||||
'isoCode': 'fil'
|
||||
|
@ -71,6 +63,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
builder: (context) => GeneralSettings()
|
||||
)),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Download Settings'.i18n),
|
||||
leading: Icon(Icons.cloud_download),
|
||||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => DownloadsSettings()
|
||||
)),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Appearance'.i18n),
|
||||
leading: Icon(Icons.color_lens),
|
||||
|
@ -132,11 +131,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
);
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
Text(
|
||||
_about,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
ListTile(
|
||||
title: Text('About'.i18n),
|
||||
leading: Icon(Icons.info),
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) => CreditsScreen()
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -149,6 +150,10 @@ class AppearanceSettings extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _AppearanceSettingsState extends State<AppearanceSettings> {
|
||||
|
||||
|
||||
ColorSwatch<dynamic> _swatch(int c) => ColorSwatch(c, {500: Color(c)});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -224,8 +229,19 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
return AlertDialog(
|
||||
title: Text('Primary color'.i18n),
|
||||
content: Container(
|
||||
height: 200,
|
||||
height: 240,
|
||||
child: MaterialColorPicker(
|
||||
colors: [
|
||||
...Colors.primaries,
|
||||
//Logo colors
|
||||
_swatch(0xffeca704),
|
||||
_swatch(0xffbe3266),
|
||||
_swatch(0xff4b2e7e),
|
||||
_swatch(0xff384697),
|
||||
_swatch(0xff0880b5),
|
||||
_swatch(0xff009a85),
|
||||
_swatch(0xff2ba766)
|
||||
],
|
||||
allowShades: false,
|
||||
selectedColor: settings.primaryColor,
|
||||
onMainColorChange: (ColorSwatch color) {
|
||||
|
@ -246,9 +262,12 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
|
|||
ListTile(
|
||||
title: Text('Use album art primary color'.i18n),
|
||||
subtitle: Text('Warning: might be buggy'.i18n),
|
||||
leading: Switch(
|
||||
value: settings.useArtColor,
|
||||
onChanged: (v) => setState(() => settings.updateUseArtColor(v)),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.useArtColor,
|
||||
onChanged: (v) => setState(() => settings.updateUseArtColor(v)),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
@ -450,13 +469,64 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
|||
ListTile(
|
||||
title: Text('Log tracks'.i18n),
|
||||
subtitle: Text('Send track listen logs to Deezer, enable it for features like Flow to work properly'.i18n),
|
||||
leading: Checkbox(
|
||||
value: settings.logListen,
|
||||
onChanged: (bool v) {
|
||||
setState(() => settings.logListen = v);
|
||||
settings.save();
|
||||
},
|
||||
leading: Container(
|
||||
width: 30,
|
||||
child: Checkbox(
|
||||
value: settings.logListen,
|
||||
onChanged: (bool v) {
|
||||
setState(() => settings.logListen = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Proxy'.i18n),
|
||||
leading: Icon(Icons.vpn_key),
|
||||
subtitle: Text(settings.proxyAddress??'Not set'),
|
||||
onTap: () {
|
||||
String _new;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Proxy'.i18n),
|
||||
content: TextField(
|
||||
onChanged: (String v) => _new = v,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'IP:PORT'
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('Cancel'.i18n),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('Reset'.i18n),
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
settings.proxyAddress = null;
|
||||
});
|
||||
await settings.save();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('Save'.i18n),
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
settings.proxyAddress = _new;
|
||||
});
|
||||
await settings.save();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
|
@ -464,6 +534,213 @@ class _DeezerSettingsState extends State<DeezerSettings> {
|
|||
}
|
||||
}
|
||||
|
||||
class DownloadsSettings extends StatefulWidget {
|
||||
@override
|
||||
_DownloadsSettingsState createState() => _DownloadsSettingsState();
|
||||
}
|
||||
|
||||
class _DownloadsSettingsState extends State<DownloadsSettings> {
|
||||
|
||||
double _downloadThreads = settings.downloadThreads.toDouble();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Download Settings'.i18n),),
|
||||
body: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text('Download path'.i18n),
|
||||
leading: Icon(Icons.folder),
|
||||
subtitle: Text(settings.downloadPath),
|
||||
onTap: () async {
|
||||
//Check permissions
|
||||
if (!(await Permission.storage.request().isGranted)) return;
|
||||
//Navigate
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => DirectoryPicker(settings.downloadPath, onSelect: (String p) {
|
||||
setState(() => settings.downloadPath = p);
|
||||
},)
|
||||
));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Downloads naming'.i18n),
|
||||
subtitle: Text('Currently'.i18n + ': ${settings.downloadFilename}'),
|
||||
leading: Icon(Icons.text_format),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
|
||||
TextEditingController _controller = TextEditingController();
|
||||
String filename = settings.downloadFilename;
|
||||
_controller.value = _controller.value.copyWith(text: filename);
|
||||
String _new = _controller.value.text;
|
||||
|
||||
//Dialog with filename format
|
||||
return AlertDialog(
|
||||
title: Text('Downloaded tracks filename'.i18n),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _controller,
|
||||
onChanged: (String s) => _new = s,
|
||||
),
|
||||
Container(height: 8.0),
|
||||
Text(
|
||||
'Valid variables are'.i18n + ': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%, %playlistTrackNumber%, %0playlistTrackNumber%, %year%',
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('Cancel'.i18n),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('Reset'.i18n),
|
||||
onPressed: () {
|
||||
_controller.value = _controller.value.copyWith(
|
||||
text: '%artists% - %title%'
|
||||
);
|
||||
_new = '%artists% - %title%';
|
||||
},
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('Clear'.i18n),
|
||||
onPressed: () => _controller.clear(),
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('Save'.i18n),
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
settings.downloadFilename = _new;
|
||||
});
|
||||
await settings.save();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text(
|
||||
'Download threads'.i18n + ': ${_downloadThreads.round().toString()}',
|
||||
style: TextStyle(
|
||||
fontSize: 16.0
|
||||
),
|
||||
),
|
||||
),
|
||||
Slider(
|
||||
min: 1,
|
||||
max: 6,
|
||||
divisions: 5,
|
||||
value: _downloadThreads,
|
||||
label: _downloadThreads.round().toString(),
|
||||
onChanged: (double v) => setState(() => _downloadThreads = v),
|
||||
onChangeEnd: (double val) async {
|
||||
_downloadThreads = val;
|
||||
setState(() {
|
||||
settings.downloadThreads = _downloadThreads.round();
|
||||
_downloadThreads = settings.downloadThreads.toDouble();
|
||||
});
|
||||
await settings.save();
|
||||
}
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Create folders for artist'.i18n),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.artistFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.artistFolder = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Create folders for albums'.i18n),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.albumFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.albumFolder = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Separate albums by discs'.i18n),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.albumDiscFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.albumDiscFolder = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Overwrite already downloaded files'.i18n),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.overwriteDownload,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.overwriteDownload = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Create folder for playlist'.i18n),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.playlistFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.playlistFolder = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Download .LRC lyrics'.i18n),
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.downloadLyrics,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.downloadLyrics = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class GeneralSettings extends StatefulWidget {
|
||||
@override
|
||||
_GeneralSettingsState createState() => _GeneralSettingsState();
|
||||
|
@ -479,163 +756,44 @@ class _GeneralSettingsState extends State<GeneralSettings> {
|
|||
ListTile(
|
||||
title: Text('Offline mode'.i18n),
|
||||
subtitle: Text('Will be overwritten on start.'.i18n),
|
||||
leading: Switch(
|
||||
value: settings.offlineMode,
|
||||
onChanged: (bool v) {
|
||||
if (v) {
|
||||
setState(() => settings.offlineMode = true);
|
||||
return;
|
||||
}
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
deezerAPI.authorize().then((v) {
|
||||
if (v) {
|
||||
setState(() => settings.offlineMode = false);
|
||||
} else {
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Error logging in, check your internet connections.'.i18n,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastLength: Toast.LENGTH_SHORT
|
||||
leading: Container(
|
||||
width: 30.0,
|
||||
child: Checkbox(
|
||||
value: settings.offlineMode,
|
||||
onChanged: (bool v) {
|
||||
if (v) {
|
||||
setState(() => settings.offlineMode = true);
|
||||
return;
|
||||
}
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
deezerAPI.authorize().then((v) {
|
||||
if (v) {
|
||||
setState(() => settings.offlineMode = false);
|
||||
} else {
|
||||
Fluttertoast.showToast(
|
||||
msg: 'Error logging in, check your internet connections.'.i18n,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastLength: Toast.LENGTH_SHORT
|
||||
);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
return AlertDialog(
|
||||
title: Text('Logging in...'.i18n),
|
||||
content: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator()
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
return AlertDialog(
|
||||
title: Text('Logging in...'.i18n),
|
||||
content: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator()
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Download path'.i18n),
|
||||
leading: Icon(Icons.folder),
|
||||
subtitle: Text(settings.downloadPath),
|
||||
onTap: () async {
|
||||
//Check permissions
|
||||
if (!(await Permission.storage.request().isGranted)) return;
|
||||
//Navigate
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => DirectoryPicker(settings.downloadPath, onSelect: (String p) {
|
||||
setState(() => settings.downloadPath = p);
|
||||
},)
|
||||
));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Downloads naming'.i18n),
|
||||
subtitle: Text('Currently'.i18n + ': ${settings.downloadFilename}'),
|
||||
leading: Icon(Icons.text_format),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
|
||||
TextEditingController _controller = TextEditingController();
|
||||
String filename = settings.downloadFilename;
|
||||
_controller.value = _controller.value.copyWith(text: filename);
|
||||
String _new = _controller.value.text;
|
||||
|
||||
//Dialog with filename format
|
||||
return AlertDialog(
|
||||
title: Text('Downloaded tracks filename'.i18n),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _controller,
|
||||
onChanged: (String s) => _new = s,
|
||||
),
|
||||
Container(height: 8.0),
|
||||
Text(
|
||||
'Valid variables are'.i18n + ': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%',
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
FlatButton(
|
||||
child: Text('Cancel'.i18n),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('Reset'.i18n),
|
||||
onPressed: () {
|
||||
_controller.value = _controller.value.copyWith(
|
||||
text: '%artists% - %title%'
|
||||
);
|
||||
_new = '%artists% - %title%';
|
||||
},
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('Clear'.i18n),
|
||||
onPressed: () => _controller.clear(),
|
||||
),
|
||||
FlatButton(
|
||||
child: Text('Save'.i18n),
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
settings.downloadFilename = _new;
|
||||
});
|
||||
await settings.save();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Create folders for artist'.i18n),
|
||||
leading: Switch(
|
||||
value: settings.artistFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.artistFolder = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Create folders for albums'.i18n),
|
||||
leading: Switch(
|
||||
value: settings.albumFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.albumFolder = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Separate albums by discs'.i18n),
|
||||
leading: Switch(
|
||||
value: settings.albumDiscFolder,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.albumDiscFolder = v);
|
||||
settings.save();
|
||||
},
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Overwrite already downloaded files'.i18n),
|
||||
leading: Switch(
|
||||
value: settings.overwriteDownload,
|
||||
onChanged: (v) {
|
||||
setState(() => settings.overwriteDownload = v);
|
||||
settings.save();
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
|
@ -836,3 +994,110 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CreditsScreen extends StatefulWidget {
|
||||
@override
|
||||
_CreditsScreenState createState() => _CreditsScreenState();
|
||||
}
|
||||
|
||||
class _CreditsScreenState extends State<CreditsScreen> {
|
||||
|
||||
String _version = '';
|
||||
|
||||
//Title, Subtitle, URL
|
||||
static final List<List<String>> credits = [
|
||||
['exttex', 'Developer'],
|
||||
['Bas Curtiz', 'Icon, logo, banner, design suggestions, tester'],
|
||||
['Deemix', 'Better app <3', 'https://codeberg.org/RemixDev/deemix'],
|
||||
['Tobs, Homam Al-Rawi, Francesco', 'Beta testers'],
|
||||
['Annexhack', 'Android Auto help']
|
||||
];
|
||||
|
||||
static final List<List<String>> translators = [
|
||||
['Homam Al-Rawi', 'Arabic'],
|
||||
['Markus', 'German'],
|
||||
['Andrea', 'Italian'],
|
||||
['Diego Hiro', 'Portuguese'],
|
||||
['Annexhack', 'Russian'],
|
||||
['Chino Pacia', 'Filipino'],
|
||||
['ArcherDelta & PetFix', 'Spanish'],
|
||||
['Shazzaam', 'Croatian'],
|
||||
['VIRGIN_KLM', 'Greek'],
|
||||
['koreezzz', 'Korean'],
|
||||
['Fwwwwwwwwwweze', 'French'],
|
||||
['kobyrevah', 'Hebrew']
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
PackageInfo.fromPlatform().then((info) {
|
||||
setState(() {
|
||||
_version = 'v${info.version}';
|
||||
});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('About'.i18n),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
FreezerTitle(),
|
||||
Text(
|
||||
_version,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
ListTile(
|
||||
title: Text('Telegram Channel'.i18n),
|
||||
subtitle: Text('To get latest releases'.i18n),
|
||||
leading: Icon(FontAwesome5.telegram, color: Color(0xFF27A2DF), size: 36.0),
|
||||
onTap: () {
|
||||
launch('https://t.me/freezereleases');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Telegram Group'.i18n),
|
||||
subtitle: Text('Official chat'.i18n),
|
||||
leading: Icon(FontAwesome5.telegram, color: Colors.cyan, size: 36.0),
|
||||
onTap: () {
|
||||
launch('https://t.me/freezerandroid');
|
||||
},
|
||||
),
|
||||
Divider(),
|
||||
...List.generate(credits.length, (i) => ListTile(
|
||||
title: Text(credits[i][0]),
|
||||
subtitle: Text(credits[i][1]),
|
||||
onTap: () {
|
||||
if (credits[i].length >= 3) {
|
||||
launch(credits[i][2]);
|
||||
}
|
||||
},
|
||||
)),
|
||||
Divider(),
|
||||
...List.generate(translators.length, (i) => ListTile(
|
||||
title: Text(translators[i][0]),
|
||||
subtitle: Text(translators[i][1]),
|
||||
)),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0, 4, 0, 8),
|
||||
child: Text(
|
||||
'Huge thanks to all the contributors! <3'.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16.0
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue