0.4.0 - translations, download fallback, android auto, radio, infinite flow, bugfixes

This commit is contained in:
exttex 2020-09-18 19:36:41 +02:00
parent a5381f0fed
commit e984621eeb
88 changed files with 2911 additions and 379 deletions

216
lib/ui/android_auto.dart Normal file
View file

@ -0,0 +1,216 @@
import 'package:audio_service/audio_service.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/translations.i18n.dart';
class AndroidAuto {
//Prefix for "playable" MediaItem
static const prefix = '_aa_';
//Get media items for parent id
Future<List<MediaItem>> getScreen(String parentId) async {
print(parentId);
//Homescreen
if (parentId == 'root' || parentId == null) return homeScreen();
//Playlists screen
if (parentId == 'playlists') {
//Fetch
List<Playlist> playlists = await deezerAPI.getPlaylists();
List<MediaItem> out = playlists.map<MediaItem>((p) => MediaItem(
id: '${prefix}playlist${p.id}',
displayTitle: p.title,
displaySubtitle: p.trackCount.toString() + ' ' + 'Tracks'.i18n,
playable: true,
artUri: p.image.thumb
)).toList();
return out;
}
//Albums screen
if (parentId == 'albums') {
List<Album> albums = await deezerAPI.getAlbums();
List<MediaItem> out = albums.map<MediaItem>((a) => MediaItem(
id: '${prefix}album${a.id}',
displayTitle: a.title,
displaySubtitle: a.artistString,
playable: true,
artUri: a.art.thumb,
)).toList();
return out;
}
//Artists screen
if (parentId == 'artists') {
List<Artist> artists = await deezerAPI.getArtists();
List<MediaItem> out = artists.map<MediaItem>((a) => MediaItem(
id: 'albums${a.id}',
displayTitle: a.name,
playable: false,
artUri: a.picture.thumb
)).toList();
return out;
}
//Artist screen (albums, etc)
if (parentId.startsWith('albums')) {
List<Album> albums = await deezerAPI.discographyPage(parentId.replaceFirst('albums', ''));
List<MediaItem> out = albums.map<MediaItem>((a) => MediaItem(
id: '${prefix}album${a.id}',
displayTitle: a.title,
displaySubtitle: a.artistString,
playable: true,
artUri: a.art.thumb
)).toList();
return out;
}
//Homescreen
if (parentId == 'homescreen') {
HomePage hp = await deezerAPI.homePage();
List<MediaItem> out = [];
for (HomePageSection section in hp.sections) {
for (int i=0; i<section.items.length; i++) {
//Limit to max 5 items
if (i == 5) break;
//Check type
var data = section.items[i].value;
switch (section.items[i].type) {
case HomePageItemType.PLAYLIST:
out.add(MediaItem(
id: '${prefix}playlist${data.id}',
displayTitle: data.title,
playable: true,
artUri: data.image.thumb
));
break;
case HomePageItemType.ALBUM:
out.add(MediaItem(
id: '${prefix}album${data.id}',
displayTitle: data.title,
displaySubtitle: data.artistString,
playable: true,
artUri: data.art.thumb
));
break;
case HomePageItemType.ARTIST:
out.add(MediaItem(
id: 'albums${data.id}',
displayTitle: data.name,
playable: false,
artUri: data.picture.thumb
));
break;
case HomePageItemType.SMARTTRACKLIST:
out.add(MediaItem(
id: '${prefix}stl${data.id}',
displayTitle: data.title,
displaySubtitle: data.subtitle,
playable: true,
artUri: data.cover.thumb
));
}
}
}
return out;
}
return [];
}
//Load virtual mediaItem
Future playItem(String id) async {
print(id);
//Play flow
if (id == 'flow' || id == 'stlflow') {
await playerHelper.playFromSmartTrackList(SmartTrackList(id: 'flow', title: 'Flow'.i18n));
return;
}
//Play library tracks
if (id == 'tracks') {
//Load tracks
Playlist favPlaylist;
try {
favPlaylist = await deezerAPI.fullPlaylist(deezerAPI.favoritesPlaylistId);
} catch (e) {print(e);}
if (favPlaylist == null || favPlaylist.tracks.length == 0) return;
await playerHelper.playFromTrackList(favPlaylist.tracks, favPlaylist.tracks[0].id, QueueSource(
id: 'allTracks',
text: 'All offline tracks'.i18n,
source: 'offline'
));
return;
}
//Play playlists
if (id.startsWith('playlist')) {
Playlist p = await deezerAPI.fullPlaylist(id.replaceFirst('playlist', ''));
await playerHelper.playFromPlaylist(p, p.tracks[0].id);
return;
}
//Play albums
if (id.startsWith('album')) {
Album a = await deezerAPI.album(id.replaceFirst('album', ''));
await playerHelper.playFromAlbum(a, a.tracks[0].id);
return;
}
//Play smart track list
if (id.startsWith('stl')) {
SmartTrackList stl = await deezerAPI.smartTrackList(id.replaceFirst('stl', ''));
await playerHelper.playFromSmartTrackList(stl);
return;
}
}
//Homescreen items
List<MediaItem> homeScreen() {
return [
MediaItem(
id: '${prefix}flow',
displayTitle: 'Flow'.i18n,
playable: true
),
MediaItem(
id: 'homescreen',
displayTitle: 'Home'.i18n,
playable: false,
),
MediaItem(
id: '${prefix}tracks',
displayTitle: 'Loved tracks'.i18n,
playable: true,
),
MediaItem(
id: 'playlists',
displayTitle: 'Playlists'.i18n,
playable: false,
),
MediaItem(
id: 'albums',
displayTitle: 'Albums'.i18n,
playable: false,
),
MediaItem(
id: 'artists',
displayTitle: 'Artists'.i18n,
playable: false,
),
];
}
}

View file

@ -48,10 +48,18 @@ class CachedImage extends StatefulWidget {
class _CachedImageState extends State<CachedImage> {
@override
Widget build(BuildContext context) {
if (widget.circular) return ClipOval(
child: CachedImage(url: widget.url, height: widget.height, width: widget.width, circular: false)
);
if (!widget.url.startsWith('http'))
return Image.asset(
widget.url,
width: widget.width,
height: widget.height,
);
return CachedNetworkImage(
imageUrl: widget.url,
width: widget.width,

View file

@ -6,9 +6,9 @@ import 'package:freezer/api/download.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/search.dart';
import 'package:freezer/translations.i18n.dart';
import '../api/definitions.dart';
import 'player_bar.dart';
import 'cached_image.dart';
import 'tiles.dart';
import 'menu.dart';
@ -134,13 +134,13 @@ class AlbumDetails extends StatelessWidget {
children: <Widget>[
Icon(Icons.favorite, size: 32),
Container(width: 4,),
Text('Library')
Text('Library'.i18n)
],
),
onPressed: () async {
await deezerAPI.addFavoriteAlbum(album.id);
Fluttertoast.showToast(
msg: 'Added to library',
msg: 'Added to library'.i18n,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM
);
@ -152,7 +152,7 @@ class AlbumDetails extends StatelessWidget {
children: <Widget>[
Icon(Icons.file_download, size: 32.0,),
Container(width: 4,),
Text('Download')
Text('Download'.i18n)
],
),
onPressed: () {
@ -168,7 +168,7 @@ class AlbumDetails extends StatelessWidget {
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 4.0),
child: Text('Disk ${cdi + 1}'),
child: Text('Disk'.i18n + ' ${cdi + 1}'),
),
...List.generate(tracks.length, (i) => TrackTile(
tracks[i],
@ -237,7 +237,7 @@ class _MakeAlbumOfflineState extends State<MakeAlbumOffline> {
),
Container(width: 4.0,),
Text(
'Offline',
'Offline'.i18n,
style: TextStyle(fontSize: 16),
)
],
@ -345,25 +345,43 @@ class ArtistDetails extends StatelessWidget {
children: <Widget>[
Icon(Icons.favorite, size: 32),
Container(width: 4,),
Text('Library')
Text('Library'.i18n)
],
),
onPressed: () async {
await deezerAPI.addFavoriteArtist(artist.id);
Fluttertoast.showToast(
msg: 'Added to library',
msg: 'Added to library'.i18n,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM
);
},
),
if ((artist.radio??false))
FlatButton(
child: Row(
children: <Widget>[
Icon(Icons.radio, size: 32),
Container(width: 4,),
Text('Radio'.i18n)
],
),
onPressed: () async {
List<Track> tracks = await deezerAPI.smartRadio(artist.id);
playerHelper.playFromTrackList(tracks, tracks[0].id, QueueSource(
id: artist.id,
text: 'Radio'.i18n + ' ${artist.name}',
source: 'smartradio'
));
},
)
],
),
),
Container(height: 16.0,),
//Top tracks
Text(
'Top Tracks',
'Top Tracks'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
@ -390,12 +408,12 @@ class ArtistDetails extends StatelessWidget {
);
}),
ListTile(
title: Text('Show more tracks'),
title: Text('Show more tracks'.i18n),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => TrackListScreen(artist.topTracks, QueueSource(
id: artist.id,
text: 'Top ${artist.name}',
text: 'Top'.i18n + '${artist.name}',
source: 'topTracks'
)))
);
@ -404,7 +422,7 @@ class ArtistDetails extends StatelessWidget {
Divider(),
//Albums
Text(
'Top Albums',
'Top Albums'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
@ -415,7 +433,7 @@ class ArtistDetails extends StatelessWidget {
//Show discography
if (i == 10 || i == artist.albums.length) {
return ListTile(
title: Text('Show all albums'),
title: Text('Show all albums'.i18n),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => DiscographyScreen(artist: artist,))
@ -552,7 +570,7 @@ class _DiscographyScreenState extends State<DiscographyScreen> {
return Scaffold(
appBar: AppBar(
title: Text('Discography'),
title: Text('Discography'.i18n),
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.album)),
@ -603,6 +621,7 @@ class _DiscographyScreenState extends State<DiscographyScreen> {
enum SortType {
DEFAULT,
REVERSE,
ALPHABETIC,
ARTIST
}
@ -634,6 +653,8 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
case SortType.ARTIST:
tracks.sort((a, b) => a.artists[0].name.compareTo(b.artists[0].name));
return tracks;
case SortType.REVERSE:
return tracks.reversed.toList();
case SortType.DEFAULT:
default:
return tracks;
@ -782,32 +803,20 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
FlatButton(
child: Row(
children: <Widget>[
Icon(Icons.favorite, size: 32),
Container(width: 4,),
Text('Library')
],
),
MakePlaylistOffline(playlist),
IconButton(
icon: Icon(Icons.favorite, size: 32),
onPressed: () async {
await deezerAPI.addFavoriteAlbum(playlist.id);
Fluttertoast.showToast(
msg: 'Added to library',
msg: 'Added to library'.i18n,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM
);
},
),
MakePlaylistOffline(playlist),
FlatButton(
child: Row(
children: <Widget>[
Icon(Icons.file_download, size: 32.0,),
Container(width: 4,),
Text('Download')
],
),
IconButton(
icon: Icon(Icons.file_download, size: 32.0,),
onPressed: () {
downloadManager.addOfflinePlaylist(playlist, private: false);
},
@ -816,17 +825,21 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
child: Icon(Icons.sort, size: 32.0),
onSelected: (SortType s) => setState(() => _sort = s),
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
const PopupMenuItem(
PopupMenuItem(
value: SortType.DEFAULT,
child: Text('Default'),
child: Text('Default'.i18n),
),
const PopupMenuItem(
PopupMenuItem(
value: SortType.REVERSE,
child: Text('Reverse'.i18n),
),
PopupMenuItem(
value: SortType.ALPHABETIC,
child: Text('Alphabetic'),
child: Text('Alphabetic'.i18n),
),
const PopupMenuItem(
PopupMenuItem(
value: SortType.ARTIST,
child: Text('Artist'),
child: Text('Artist'.i18n),
),
],
),
@ -914,7 +927,7 @@ class _MakePlaylistOfflineState extends State<MakePlaylistOffline> {
),
Container(width: 4.0,),
Text(
'Offline',
'Offline'.i18n,
style: TextStyle(fontSize: 16),
)
],

View file

@ -1,5 +1,6 @@
import 'package:filesize/filesize.dart';
import 'package:flutter/material.dart';
import 'package:freezer/translations.i18n.dart';
import 'cached_image.dart';
import '../api/download.dart';
@ -17,9 +18,9 @@ class DownloadTile extends StatelessWidget {
case DownloadState.DOWNLOADING:
return '${filesize(download.received)} / ${filesize(download.total)}';
case DownloadState.POST:
return 'Post processing...';
return 'Post processing...'.i18n;
case DownloadState.DONE:
return 'Done'; //Shouldn't be visible
return 'Done'.i18n; //Shouldn't be visible
}
return '';
}
@ -27,6 +28,7 @@ class DownloadTile extends StatelessWidget {
Widget get progressBar {
switch (download.state) {
case DownloadState.DOWNLOADING:
print(download.track.id);
return LinearProgressIndicator(value: download.received / download.total);
case DownloadState.POST:
return LinearProgressIndicator();
@ -62,15 +64,15 @@ class DownloadTile extends StatelessWidget {
context: context,
builder: (context) {
return AlertDialog(
title: Text('Delete'),
content: Text('Are you sure, you want to delete this download?'),
title: Text('Delete'.i18n),
content: Text('Are you sure you want to delete this download?'.i18n),
actions: [
FlatButton(
child: Text('Cancel'),
child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text('Delete'),
child: Text('Delete'.i18n),
onPressed: () {
downloadManager.removeDownload(download);
if (this.onDelete != null) this.onDelete();
@ -100,7 +102,7 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Downloads'),
title: Text('Downloads'.i18n),
actions: [
IconButton(
icon: Icon(downloadManager.stopped ? Icons.play_arrow : Icons.stop),
@ -129,23 +131,23 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
}),
if (downloadManager.queue.length > 1 || (downloadManager.stopped && downloadManager.queue.length > 0))
ListTile(
title: Text('Clear queue'),
subtitle: Text("This won't delete currently downloading item"),
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'),
content: Text('Are you sure, you want to delete all queued downloads?'),
title: Text('Delete'.i18n),
content: Text('Are you sure you want to delete all queued downloads?'.i18n),
actions: [
FlatButton(
child: Text('Cancel'),
child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text('Delete'),
child: Text('Delete'.i18n),
onPressed: () async {
await downloadManager.clearQueue();
Navigator.of(context).pop();
@ -181,9 +183,9 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
return DownloadTile(d);
}),
ListTile(
title: Text('Clear downloads history'),
title: Text('Clear downloads history'.i18n),
leading: Icon(Icons.delete),
subtitle: Text('WARNING: This will only clear non-offline (external downloads)'),
subtitle: Text('WARNING: This will only clear non-offline (external downloads)'.i18n),
onTap: () async {
await downloadManager.cleanDownloadHistory();
setState(() {});

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:freezer/translations.i18n.dart';
class ErrorScreen extends StatelessWidget {
@ -18,7 +19,7 @@ class ErrorScreen extends StatelessWidget {
size: 64.0,
),
Container(height: 4.0,),
Text(message ?? 'Please check your connection and try again later...')
Text(message ?? 'Please check your connection and try again later...'.i18n)
],
),
);

View file

@ -4,6 +4,7 @@ import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/menu.dart';
import 'package:freezer/translations.i18n.dart';
import 'tiles.dart';
import 'details_screens.dart';
import '../settings.dart';
@ -175,7 +176,7 @@ class _HomePageScreenState extends State<HomePageScreen> {
if (section.hasMore??false) {
return FlatButton(
child: Text(
'Show more',
'Show more'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.0

View file

@ -4,6 +4,7 @@ import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/spotify.dart';
import 'package:freezer/ui/menu.dart';
import 'package:freezer/translations.i18n.dart';
class ImporterScreen extends StatefulWidget {
@override
@ -34,8 +35,8 @@ class _ImporterScreenState extends State<ImporterScreen> {
setState(() => _data = data);
return;
} catch (e) {
print(e);
} catch (e, st) {
print('$e, $st');
setState(() {
_error = true;
_loading = false;
@ -49,13 +50,13 @@ class _ImporterScreenState extends State<ImporterScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Importer'),
title: Text('Importer'.i18n),
),
body: ListView(
children: <Widget>[
ListTile(
title: Text('Currently supporting only Spotify, with 100 tracks limit'),
subtitle: Text('Due to API limitations'),
title: Text('Currently supporting only Spotify, with 100 tracks limit'.i18n),
subtitle: Text('Due to API limitations'.i18n),
leading: Icon(
Icons.warning,
color: Colors.deepOrangeAccent,
@ -64,7 +65,7 @@ class _ImporterScreenState extends State<ImporterScreen> {
Divider(),
Container(height: 16.0,),
Text(
'Enter your playlist link below',
'Enter your playlist link below'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.0
@ -104,7 +105,7 @@ class _ImporterScreenState extends State<ImporterScreen> {
),
if (_error)
ListTile(
title: Text('Error loading URL!'),
title: Text('Error loading URL!'.i18n),
leading: Icon(Icons.error, color: Colors.red,),
),
if (_data != null)
@ -133,13 +134,14 @@ class _ImporterWidgetState extends State<ImporterWidget> {
ListTile(
title: Text(widget.playlist.name),
subtitle: Text(widget.playlist.description),
leading: Image.network(widget.playlist.image),
//Default image
leading: Image.network(widget.playlist.image??'http://cdn-images.deezer.com/images/cover//256x256-000000-80-0-0.jpg'),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
RaisedButton(
child: Text('Convert'),
child: Text('Convert'.i18n),
color: Theme.of(context).primaryColor,
onPressed: () {
spotify.convertPlaylist(widget.playlist);
@ -149,7 +151,7 @@ class _ImporterWidgetState extends State<ImporterWidget> {
},
),
RaisedButton(
child: Text('Download only'),
child: Text('Download only'.i18n),
color: Theme.of(context).primaryColor,
onPressed: () {
spotify.convertPlaylist(widget.playlist, downloadOnly: true);
@ -197,7 +199,7 @@ class CurrentlyImportingScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Importing...'),),
appBar: AppBar(title: Text('Importing...'.i18n),),
body: StreamBuilder(
stream: spotify.importingStream.stream,
builder: (context, snapshot) {
@ -254,7 +256,7 @@ class CurrentlyImportingScreen extends StatelessWidget {
),
if (snapshot.data != null)
FlatButton(
child: Text('Playlist menu'),
child: Text('Playlist menu'.i18n),
onPressed: () async {
Playlist p = await deezerAPI.playlist(snapshot.data);
p.library = true;

View file

@ -10,6 +10,7 @@ import 'package:freezer/ui/downloads_screen.dart';
import 'package:freezer/ui/error.dart';
import 'package:freezer/ui/importer_screen.dart';
import 'package:freezer/ui/tiles.dart';
import 'package:freezer/translations.i18n.dart';
import 'menu.dart';
import 'settings_screen.dart';
@ -24,7 +25,7 @@ class LibraryAppBar extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
return AppBar(
title: Text('Library'),
title: Text('Library'.i18n),
actions: <Widget>[
IconButton(
icon: Icon(Icons.file_download),
@ -58,9 +59,9 @@ class LibraryScreen extends StatelessWidget {
Container(height: 4.0,),
if (downloadManager.stopped && downloadManager.queue.length > 0)
ListTile(
title: Text('Downloads'),
title: Text('Downloads'.i18n),
leading: Icon(Icons.file_download),
subtitle: Text('Downloading is currently stopped, click here to resume.'),
subtitle: Text('Downloading is currently stopped, click here to resume.'.i18n),
onTap: () {
downloadManager.start();
Navigator.of(context).push(MaterialPageRoute(
@ -73,7 +74,7 @@ class LibraryScreen extends StatelessWidget {
Divider(),
ListTile(
title: Text('Tracks'),
title: Text('Tracks'.i18n),
leading: Icon(Icons.audiotrack),
onTap: () {
Navigator.of(context).push(
@ -82,7 +83,7 @@ class LibraryScreen extends StatelessWidget {
},
),
ListTile(
title: Text('Albums'),
title: Text('Albums'.i18n),
leading: Icon(Icons.album),
onTap: () {
Navigator.of(context).push(
@ -91,7 +92,7 @@ class LibraryScreen extends StatelessWidget {
},
),
ListTile(
title: Text('Artists'),
title: Text('Artists'.i18n),
leading: Icon(Icons.recent_actors),
onTap: () {
Navigator.of(context).push(
@ -100,7 +101,7 @@ class LibraryScreen extends StatelessWidget {
},
),
ListTile(
title: Text('Playlists'),
title: Text('Playlists'.i18n),
leading: Icon(Icons.playlist_play),
onTap: () {
Navigator.of(context).push(
@ -110,9 +111,9 @@ class LibraryScreen extends StatelessWidget {
),
Divider(),
ListTile(
title: Text('Import'),
title: Text('Import'.i18n),
leading: Icon(Icons.import_export),
subtitle: Text('Import playlists from Spotify'),
subtitle: Text('Import playlists from Spotify'.i18n),
onTap: () {
if (spotify.doneImporting != null) {
Navigator.of(context).push(
@ -128,7 +129,7 @@ class LibraryScreen extends StatelessWidget {
},
),
ExpansionTile(
title: Text('Statistics'),
title: Text('Statistics'.i18n),
leading: Icon(Icons.insert_chart),
children: <Widget>[
FutureBuilder(
@ -148,27 +149,27 @@ class LibraryScreen extends StatelessWidget {
return Column(
children: <Widget>[
ListTile(
title: Text('Offline tracks'),
title: Text('Offline tracks'.i18n),
leading: Icon(Icons.audiotrack),
trailing: Text(data[0]),
),
ListTile(
title: Text('Offline albums'),
title: Text('Offline albums'.i18n),
leading: Icon(Icons.album),
trailing: Text(data[1]),
),
ListTile(
title: Text('Offline playlists'),
title: Text('Offline playlists'.i18n),
leading: Icon(Icons.playlist_add),
trailing: Text(data[2]),
),
ListTile(
title: Text('Offline size'),
title: Text('Offline size'.i18n),
leading: Icon(Icons.sd_card),
trailing: Text(data[3]),
),
ListTile(
title: Text('Free space'),
title: Text('Free space'.i18n),
leading: Icon(Icons.disc_full),
trailing: Text(data[4]),
),
@ -254,7 +255,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Tracks'),),
appBar: AppBar(title: Text('Tracks'.i18n),),
body: ListView(
children: <Widget>[
Card(
@ -263,7 +264,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
children: <Widget>[
Container(height: 8.0,),
Text(
'Loved tracks',
'Loved tracks'.i18n,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 24
@ -279,7 +280,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
children: <Widget>[
Icon(Icons.file_download, size: 32.0,),
Container(width: 4,),
Text('Download')
Text('Download'.i18n)
],
),
onPressed: () {
@ -299,7 +300,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
onTap: () {
playerHelper.playFromTrackList(tracks, t.id, QueueSource(
id: deezerAPI.favoritesPlaylistId,
text: 'Favorites',
text: 'Favorites'.i18n,
source: 'playlist'
));
},
@ -328,7 +329,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
),
Divider(),
Text(
'All offline tracks',
'All offline tracks'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
@ -343,7 +344,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
onTap: () {
playerHelper.playFromTrackList(allTracks, t.id, QueueSource(
id: 'allTracks',
text: 'All offline tracks',
text: 'All offline tracks'.i18n,
source: 'offline'
));
},
@ -386,7 +387,7 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Albums'),),
appBar: AppBar(title: Text('Albums'.i18n),),
body: ListView(
children: <Widget>[
Container(height: 8.0,),
@ -427,7 +428,7 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
children: <Widget>[
Divider(),
Text(
'Offline albums',
'Offline albums'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
@ -473,7 +474,7 @@ class _LibraryArtistsState extends State<LibraryArtists> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Artists'),),
appBar: AppBar(title: Text('Artists'.i18n),),
body: FutureBuilder(
future: deezerAPI.getArtists(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
@ -533,20 +534,30 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
super.initState();
}
Playlist get favoritesPlaylist => Playlist(
id: deezerAPI.favoritesPlaylistId,
title: 'Favorites'.i18n,
user: User(name: deezerAPI.userName),
image: ImageDetails(thumbUrl: 'assets/favorites_thumb.jpg'),
tracks: [],
trackCount: 1,
duration: Duration(seconds: 0)
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Playlists'),),
appBar: AppBar(title: Text('Playlists'.i18n),),
body: ListView(
children: <Widget>[
ListTile(
title: Text('Create new playlist'),
title: Text('Create new playlist'.i18n),
leading: Icon(Icons.playlist_add),
onTap: () {
if (settings.offlineMode) {
Fluttertoast.showToast(
msg: 'Cannot create playlists in offline mode',
msg: 'Cannot create playlists in offline mode'.i18n,
gravity: ToastGravity.BOTTOM
);
return;
@ -565,6 +576,20 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
],
),
//Favorites playlist
PlaylistTile(
favoritesPlaylist,
onTap: () async {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => PlaylistDetails(favoritesPlaylist)
));
},
onHold: () {
MenuSheet m = MenuSheet(context);
m.defaultPlaylistMenu(favoritesPlaylist);
},
),
if (_playlists != null)
...List.generate(_playlists.length, (int i) {
Playlist p = _playlists[i];
@ -593,7 +618,7 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
children: <Widget>[
Divider(),
Text(
'Offline playlists',
'Offline playlists'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24.0,

View file

@ -1,10 +1,9 @@
import 'package:audio_service/audio_service.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/main.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:freezer/translations.i18n.dart';
import '../settings.dart';
import '../api/definitions.dart';
@ -62,11 +61,11 @@ class _LoginWidgetState extends State<LoginWidget> {
context: context,
builder: (context) {
return AlertDialog(
title: Text('Error'),
content: Text('Error logging in! Please check your token and internet connection and try again.'),
title: Text('Error'.i18n),
content: Text('Error logging in! Please check your token and internet connection and try again.'.i18n),
actions: <Widget>[
FlatButton(
child: Text('Dismiss'),
child: Text('Dismiss'.i18n),
onPressed: () {
Navigator.of(context).pop();
},
@ -117,7 +116,7 @@ class _LoginWidgetState extends State<LoginWidget> {
children: <Widget>[
Container(height: 16.0,),
Text(
'Welcome to',
'Welcome to'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.0
@ -126,7 +125,7 @@ class _LoginWidgetState extends State<LoginWidget> {
FreezerTitle(),
Container(height: 8.0,),
Text(
"Please login using your Deezer account.",
"Please login using your Deezer account.".i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.0
@ -136,7 +135,7 @@ class _LoginWidgetState extends State<LoginWidget> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 32.0),
child: OutlineButton(
child: Text('Login using browser'),
child: Text('Login using browser'.i18n),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => LoginBrowser(_update))
@ -147,18 +146,18 @@ class _LoginWidgetState extends State<LoginWidget> {
Padding(
padding: EdgeInsets.symmetric(horizontal: 32.0),
child: OutlineButton(
child: Text('Login using token'),
child: Text('Login using token'.i18n),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Enter ARL'),
title: Text('Enter ARL'.i18n),
content: Container(
child: TextField(
onChanged: (String s) => _arl = s,
decoration: InputDecoration(
labelText: 'Token (ARL)'
labelText: 'Token (ARL)'.i18n
),
),
),
@ -166,7 +165,7 @@ class _LoginWidgetState extends State<LoginWidget> {
FlatButton(
child: Text('Save'),
onPressed: () {
settings.arl = _arl;
settings.arl = _arl.trim();
Navigator.of(context).pop();
_update();
},
@ -180,7 +179,7 @@ class _LoginWidgetState extends State<LoginWidget> {
),
Container(height: 16.0,),
Text(
"If you don't have account, you can register on deezer.com for free.",
"If you don't have account, you can register on deezer.com for free.".i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.0
@ -199,7 +198,7 @@ class _LoginWidgetState extends State<LoginWidget> {
Divider(),
Container(height: 8.0,),
Text(
"By using this app, you don't agree with the Deezer ToS",
"By using this app, you don't agree with the Deezer ToS".i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.0

View file

@ -6,9 +6,9 @@ import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/download.dart';
import 'package:freezer/ui/details_screens.dart';
import 'package:freezer/ui/error.dart';
import 'package:freezer/translations.i18n.dart';
import '../api/definitions.dart';
import '../api/player.dart';
import 'cached_image.dart';
class MenuSheet {
@ -137,7 +137,7 @@ class MenuSheet {
//===================
Widget addToQueueNext(Track t) => ListTile(
title: Text('Play next'),
title: Text('Play next'.i18n),
leading: Icon(Icons.playlist_play),
onTap: () async {
//-1 = next
@ -146,7 +146,7 @@ class MenuSheet {
});
Widget addToQueue(Track t) => ListTile(
title: Text('Add to queue'),
title: Text('Add to queue'.i18n),
leading: Icon(Icons.playlist_add),
onTap: () async {
await AudioService.addQueueItem(t.toMediaItem());
@ -155,7 +155,7 @@ class MenuSheet {
);
Widget addTrackFavorite(Track t) => ListTile(
title: Text('Add track to favorites'),
title: Text('Add track to favorites'.i18n),
leading: Icon(Icons.favorite),
onTap: () async {
await deezerAPI.addFavoriteTrack(t.id);
@ -165,7 +165,7 @@ class MenuSheet {
downloadManager.addOfflinePlaylist(p);
}
Fluttertoast.showToast(
msg: 'Added to library!',
msg: 'Added to library'.i18n,
gravity: ToastGravity.BOTTOM,
toastLength: Toast.LENGTH_SHORT
);
@ -174,7 +174,7 @@ class MenuSheet {
);
Widget downloadTrack(Track t) => ListTile(
title: Text('Download'),
title: Text('Download'.i18n),
leading: Icon(Icons.file_download),
onTap: () async {
await downloadManager.addOfflineTrack(t, private: false);
@ -183,7 +183,7 @@ class MenuSheet {
);
Widget addToPlaylist(Track t) => ListTile(
title: Text('Add to playlist'),
title: Text('Add to playlist'.i18n),
leading: Icon(Icons.playlist_add),
onTap: () async {
@ -194,7 +194,7 @@ class MenuSheet {
context: context,
builder: (context) {
return AlertDialog(
title: Text('Select playlist'),
title: Text('Select playlist'.i18n),
content: FutureBuilder(
future: deezerAPI.getPlaylists(),
builder: (context, snapshot) {
@ -224,7 +224,7 @@ class MenuSheet {
},
)),
ListTile(
title: Text('Create new playlist'),
title: Text('Create new playlist'.i18n),
leading: Icon(Icons.add),
onTap: () {
Navigator.of(context).pop();
@ -250,7 +250,7 @@ class MenuSheet {
downloadManager.addOfflinePlaylist(p);
}
Fluttertoast.showToast(
msg: "Track added to ${p.title}",
msg: "Track added to".i18n + " ${p.title}",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
@ -261,12 +261,12 @@ class MenuSheet {
);
Widget removeFromPlaylist(Track t, Playlist p) => ListTile(
title: Text('Remove from playlist'),
title: Text('Remove from playlist'.i18n),
leading: Icon(Icons.delete),
onTap: () async {
await deezerAPI.removeFromPlaylist(t.id, p.id);
Fluttertoast.showToast(
msg: 'Track removed from ${p.title}',
msg: 'Track removed from'.i18n + ' ${p.title}',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
@ -275,7 +275,7 @@ class MenuSheet {
);
Widget removeFavoriteTrack(Track t, {onUpdate}) => ListTile(
title: Text('Remove favorite'),
title: Text('Remove favorite'.i18n),
leading: Icon(Icons.delete),
onTap: () async {
await deezerAPI.removeFavorite(t.id);
@ -285,7 +285,7 @@ class MenuSheet {
await downloadManager.addOfflinePlaylist(p);
}
Fluttertoast.showToast(
msg: 'Track removed from library',
msg: 'Track removed from library'.i18n,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM
);
@ -297,7 +297,7 @@ class MenuSheet {
//Redirect to artist page (ie from track)
Widget showArtist(Artist a) => ListTile(
title: Text(
'Go to ${a.name}',
'Go to'.i18n + ' ${a.name}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@ -312,7 +312,7 @@ class MenuSheet {
Widget showAlbum(Album a) => ListTile(
title: Text(
'Go to ${a.title}',
'Go to'.i18n + ' ${a.title}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@ -345,7 +345,7 @@ class MenuSheet {
//===================
Widget downloadAlbum(Album a) => ListTile(
title: Text('Download'),
title: Text('Download'.i18n),
leading: Icon(Icons.file_download),
onTap: () async {
await downloadManager.addOfflineAlbum(a, private: false);
@ -354,7 +354,7 @@ class MenuSheet {
);
Widget offlineAlbum(Album a) => ListTile(
title: Text('Make offline'),
title: Text('Make offline'.i18n),
leading: Icon(Icons.offline_pin),
onTap: () async {
await deezerAPI.addFavoriteAlbum(a.id);
@ -364,12 +364,12 @@ class MenuSheet {
);
Widget libraryAlbum(Album a) => ListTile(
title: Text('Add to library'),
title: Text('Add to library'.i18n),
leading: Icon(Icons.library_music),
onTap: () async {
await deezerAPI.addFavoriteAlbum(a.id);
Fluttertoast.showToast(
msg: 'Added to library',
msg: 'Added to library'.i18n,
gravity: ToastGravity.BOTTOM
);
_close();
@ -378,13 +378,13 @@ class MenuSheet {
//Remove album from favorites
Widget removeAlbum(Album a, {Function onRemove}) => ListTile(
title: Text('Remove album'),
title: Text('Remove album'.i18n),
leading: Icon(Icons.delete),
onTap: () async {
await deezerAPI.removeAlbum(a.id);
await downloadManager.removeOfflineAlbum(a.id);
Fluttertoast.showToast(
msg: 'Album removed',
msg: 'Album removed'.i18n,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
@ -409,12 +409,12 @@ class MenuSheet {
//===================
Widget removeArtist(Artist a, {Function onRemove}) => ListTile(
title: Text('Remove from favorites'),
title: Text('Remove from favorites'.i18n),
leading: Icon(Icons.delete),
onTap: () async {
await deezerAPI.removeArtist(a.id);
Fluttertoast.showToast(
msg: 'Artist removed from library',
msg: 'Artist removed from library'.i18n,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM
);
@ -424,12 +424,12 @@ class MenuSheet {
);
Widget favoriteArtist(Artist a) => ListTile(
title: Text('Add to favorites'),
title: Text('Add to favorites'.i18n),
leading: Icon(Icons.favorite),
onTap: () async {
await deezerAPI.addFavoriteArtist(a.id);
Fluttertoast.showToast(
msg: 'Added to library',
msg: 'Added to library'.i18n,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM
);
@ -455,7 +455,7 @@ class MenuSheet {
//===================
Widget removePlaylistLibrary(Playlist p, {Function onRemove}) => ListTile(
title: Text('Remove from library'),
title: Text('Remove from library'.i18n),
leading: Icon(Icons.delete),
onTap: () async {
if (p.user.id.trim() == deezerAPI.userId) {
@ -472,12 +472,12 @@ class MenuSheet {
);
Widget addPlaylistLibrary(Playlist p) => ListTile(
title: Text('Add playlist to library'),
title: Text('Add playlist to library'.i18n),
leading: Icon(Icons.favorite),
onTap: () async {
await deezerAPI.addPlaylist(p.id);
Fluttertoast.showToast(
msg: 'Added playlist to library',
msg: 'Added playlist to library'.i18n,
gravity: ToastGravity.BOTTOM
);
_close();
@ -485,7 +485,7 @@ class MenuSheet {
);
Widget addPlaylistOffline(Playlist p) => ListTile(
title: Text('Make playlist offline'),
title: Text('Make playlist offline'.i18n),
leading: Icon(Icons.offline_pin),
onTap: () async {
//Add to library
@ -496,7 +496,7 @@ class MenuSheet {
);
Widget downloadPlaylist(Playlist p) => ListTile(
title: Text('Download playlist'),
title: Text('Download playlist'.i18n),
leading: Icon(Icons.file_download),
onTap: () async {
downloadManager.addOfflinePlaylist(p, private: false);
@ -542,20 +542,20 @@ class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Create playlist'),
title: Text('Create playlist'.i18n),
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextField(
decoration: InputDecoration(
labelText: 'Title'
labelText: 'Title'.i18n
),
onChanged: (String s) => _title = s,
),
TextField(
onChanged: (String s) => _description = s,
decoration: InputDecoration(
labelText: 'Description'
labelText: 'Description'.i18n
),
),
Container(height: 4.0,),
@ -567,11 +567,11 @@ class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
items: [
DropdownMenuItem<int>(
value: 1,
child: Text('Private'),
child: Text('Private'.i18n),
),
DropdownMenuItem<int>(
value: 2,
child: Text('Collaborative'),
child: Text('Collaborative'.i18n),
),
],
),
@ -579,11 +579,11 @@ class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
),
actions: <Widget>[
FlatButton(
child: Text('Cancel'),
child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text('Create'),
child: Text('Create'.i18n),
onPressed: () async {
List<String> tracks = [];
if (widget.tracks != null) {
@ -596,7 +596,7 @@ class _CreatePlaylistDialogState extends State<CreatePlaylistDialog> {
trackIds: tracks
);
Fluttertoast.showToast(
msg: 'Playlist created!',
msg: 'Playlist created!'.i18n,
gravity: ToastGravity.BOTTOM
);
Navigator.of(context).pop();

View file

@ -137,7 +137,7 @@ class PlayPauseButton extends StatelessWidget {
}
//Paused
if ((!AudioService.playbackState.playing &&
if ((!(AudioService.playbackState?.playing??false) &&
AudioService.playbackState.processingState == AudioProcessingState.ready) ||
//None state (stopped)
AudioService.playbackState.processingState == AudioProcessingState.none) {

View file

@ -1,12 +1,9 @@
import 'dart:ui';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:audio_service/audio_service.dart';
import 'package:flutter_screenutil/screenutil.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/settings.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:freezer/ui/menu.dart';
import 'package:freezer/ui/settings_screen.dart';
import 'package:freezer/ui/tiles.dart';
@ -18,6 +15,9 @@ import 'cached_image.dart';
import '../api/definitions.dart';
import 'player_bar.dart';
import 'dart:ui';
import 'dart:async';
class PlayerScreen extends StatefulWidget {
@override
_PlayerScreenState createState() => _PlayerScreenState();
@ -527,9 +527,9 @@ class PlayerScreenTopRow extends StatelessWidget {
),
),
Container(
width: this.textWidth??ScreenUtil().setWidth(600),
width: this.textWidth??ScreenUtil().setWidth(550),
child: Text(
(short??false)?playerHelper.queueSource.text:'Playing from: ' + playerHelper.queueSource.text,
(short??false)?playerHelper.queueSource.text:'Playing from:'.i18n + ' ' + playerHelper.queueSource.text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.left,
@ -728,7 +728,7 @@ class _QueueScreenState extends State<QueueScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Queue'),
title: Text('Queue'.i18n),
actions: <Widget>[
IconButton(
icon: Icon(

View file

@ -4,6 +4,7 @@ import 'package:freezer/api/download.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/ui/details_screens.dart';
import 'package:freezer/ui/menu.dart';
import 'package:freezer/translations.i18n.dart';
import 'tiles.dart';
import '../api/deezer.dart';
@ -20,6 +21,7 @@ class _SearchScreenState extends State<SearchScreen> {
String _query;
bool _offline = false;
TextEditingController _controller = new TextEditingController();
List _suggestions = [];
void _submit(BuildContext context, {String query}) {
if (query != null) _query = query;
@ -41,10 +43,21 @@ class _SearchScreenState extends State<SearchScreen> {
super.initState();
}
//Load search suggestions
Future<List<String>> _loadSuggestions() async {
if (_query == null || _query.length < 2) return null;
String q = _query;
await Future.delayed(Duration(milliseconds: 300));
if (q != _query) return null;
//Load
List sugg = await deezerAPI.searchSuggestions(_query);
setState(() => _suggestions = sugg);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Search'),),
appBar: AppBar(title: Text('Search'.i18n),),
body: ListView(
children: <Widget>[
Container(height: 16.0),
@ -57,9 +70,12 @@ class _SearchScreenState extends State<SearchScreen> {
alignment: Alignment(1.0, 1.0),
children: [
TextField(
onChanged: (String s) => _query = s,
onChanged: (String s) {
setState(() => _query = s);
_loadSuggestions();
},
decoration: InputDecoration(
labelText: 'Search'
labelText: 'Search'.i18n
),
controller: _controller,
onSubmitted: (String s) => _submit(context, query: s),
@ -73,7 +89,6 @@ class _SearchScreenState extends State<SearchScreen> {
],
)
),
Padding(
padding: EdgeInsets.fromLTRB(0, 8, 0, 0),
child: IconButton(
@ -85,14 +100,23 @@ class _SearchScreenState extends State<SearchScreen> {
),
),
ListTile(
title: Text('Offline search'),
title: Text('Offline search'.i18n),
leading: Switch(
value: _offline,
onChanged: (v) {
setState(() => _offline = !_offline);
},
),
)
),
Divider(),
...List.generate((_suggestions??[]).length, (i) => ListTile(
title: Text(_suggestions[i]),
leading: Icon(Icons.search),
onTap: () {
setState(() => _query = _suggestions[i]);
_submit(context);
},
))
],
),
);
@ -119,7 +143,7 @@ class SearchResultsScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Search Results'),
title: Text('Search Results'.i18n),
),
body: FutureBuilder(
future: _search(),
@ -139,7 +163,7 @@ class SearchResultsScreen extends StatelessWidget {
Icons.warning,
size: 64,
),
Text('No results!')
Text('No results!'.i18n)
],
),
);
@ -149,7 +173,7 @@ class SearchResultsScreen extends StatelessWidget {
if (results.tracks != null && results.tracks.length != 0) {
tracks = [
Text(
'Tracks',
'Tracks'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26.0,
@ -163,7 +187,7 @@ class SearchResultsScreen extends StatelessWidget {
t,
onTap: () {
playerHelper.playFromTrackList(results.tracks, t.id, QueueSource(
text: 'Search',
text: 'Search'.i18n,
id: query,
source: 'search'
));
@ -175,13 +199,13 @@ class SearchResultsScreen extends StatelessWidget {
);
}),
ListTile(
title: Text('Show all tracks'),
title: Text('Show all tracks'.i18n),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => TrackListScreen(results.tracks, QueueSource(
id: query,
source: 'search',
text: 'Search'
text: 'Search'.i18n
)))
);
},
@ -194,7 +218,7 @@ class SearchResultsScreen extends StatelessWidget {
if (results.albums != null && results.albums.length != 0) {
albums = [
Text(
'Albums',
'Albums'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26.0,
@ -218,7 +242,7 @@ class SearchResultsScreen extends StatelessWidget {
);
}),
ListTile(
title: Text('Show all albums'),
title: Text('Show all albums'.i18n),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => AlbumListScreen(results.albums))
@ -233,7 +257,7 @@ class SearchResultsScreen extends StatelessWidget {
if (results.artists != null && results.artists.length != 0) {
artists = [
Text(
'Artists',
'Artists'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26.0,
@ -269,7 +293,7 @@ class SearchResultsScreen extends StatelessWidget {
if (results.playlists != null && results.playlists.length != 0) {
playlists = [
Text(
'Playlists',
'Playlists'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26.0,
@ -293,7 +317,7 @@ class SearchResultsScreen extends StatelessWidget {
);
}),
ListTile(
title: Text('Show all playlists'),
title: Text('Show all playlists'.i18n),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => SearchResultPlaylists(results.playlists))
@ -332,7 +356,7 @@ class TrackListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Tracks'),),
appBar: AppBar(title: Text('Tracks'.i18n),),
body: ListView.builder(
itemCount: tracks.length,
itemBuilder: (BuildContext context, int i) {
@ -362,7 +386,7 @@ class AlbumListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Albums'),),
appBar: AppBar(title: Text('Albums'.i18n),),
body: ListView.builder(
itemCount: albums.length,
itemBuilder: (context, i) {
@ -393,7 +417,7 @@ class SearchResultPlaylists extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Playlists'),),
appBar: AppBar(title: Text('Playlists'.i18n),),
body: ListView.builder(
itemCount: playlists.length,
itemBuilder: (context, i) {

View file

@ -9,11 +9,13 @@ import 'package:flutter_material_color_picker/flutter_material_color_picker.dart
import 'package:fluttertoast/fluttertoast.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/ui/error.dart';
import 'package:i18n_extension/i18n_widget.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';
import 'package:freezer/translations.i18n.dart';
import 'package:clipboard/clipboard.dart';
import '../settings.dart';
@ -44,18 +46,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Settings'),),
appBar: AppBar(title: Text('Settings'.i18n),),
body: ListView(
children: <Widget>[
ListTile(
title: Text('General'),
title: Text('General'.i18n),
leading: Icon(Icons.settings),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => GeneralSettings()
)),
),
ListTile(
title: Text('Appearance'),
title: Text('Appearance'.i18n),
leading: Icon(Icons.color_lens),
onTap: () => Navigator.push(
context,
@ -63,7 +65,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
),
ListTile(
title: Text('Quality'),
title: Text('Quality'.i18n),
leading: Icon(Icons.high_quality),
onTap: () => Navigator.push(
context,
@ -71,12 +73,54 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
),
ListTile(
title: Text('Deezer'),
title: Text('Deezer'.i18n),
leading: Icon(Icons.equalizer),
onTap: () => Navigator.push(context, MaterialPageRoute(
builder: (context) => DeezerSettings()
)),
),
//Language select
ListTile(
title: Text('Language'.i18n),
leading: Icon(Icons.language),
onTap: () {
showDialog(
context: context,
builder: (context) => LanguagePickerDialog(
titlePadding: EdgeInsets.all(8.0),
title: Text('Select language'.i18n),
isSearchable: false,
languagesList: supportedLocales.map<Map<String, String>>((l) {
Map _lang = defaultLanguagesList.firstWhere((lang) => lang['isoCode'] == l.languageCode);
_lang['name'] = _lang['name'] + ' (${l.toString()})';
return _lang;
}).toList(),
onValuePicked: (Language l) async {
setState(() {
Locale locale = supportedLocales.firstWhere((_l) => _l.languageCode == l.isoCode);
settings.language = locale.toString();
});
await settings.save();
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Language'.i18n),
content: Text('Language changed, please restart Freezer to apply!'.i18n),
actions: [
FlatButton(
child: Text('OK'),
onPressed: () => Navigator.of(context).pop(),
)
],
);
}
);
},
)
);
},
),
Divider(),
Text(
_about,
@ -97,22 +141,22 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Appearance'),),
appBar: AppBar(title: Text('Appearance'.i18n),),
body: ListView(
children: <Widget>[
ListTile(
title: Text('Theme'),
subtitle: Text('Currently: ${settings.theme.toString().split('.').last}'),
title: Text('Theme'.i18n),
subtitle: Text('Currently'.i18n + ': ${settings.theme.toString().split('.').last}'),
leading: Icon(Icons.color_lens),
onTap: () {
showDialog(
context: context,
builder: (context) {
return SimpleDialog(
title: Text('Select theme'),
title: Text('Select theme'.i18n),
children: <Widget>[
SimpleDialogOption(
child: Text('Light (default)'),
child: Text('Light (default)'.i18n),
onPressed: () {
setState(() => settings.theme = Themes.Light);
settings.save();
@ -121,7 +165,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
},
),
SimpleDialogOption(
child: Text('Dark'),
child: Text('Dark'.i18n),
onPressed: () {
setState(() => settings.theme = Themes.Dark);
settings.save();
@ -130,7 +174,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
},
),
SimpleDialogOption(
child: Text('Black (AMOLED)'),
child: Text('Black (AMOLED)'.i18n),
onPressed: () {
setState(() => settings.theme = Themes.Black);
settings.save();
@ -139,7 +183,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
},
),
SimpleDialogOption(
child: Text('Deezer (Dark)'),
child: Text('Deezer (Dark)'.i18n),
onPressed: () {
setState(() => settings.theme = Themes.Deezer);
settings.save();
@ -154,10 +198,10 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
},
),
ListTile(
title: Text('Primary color'),
title: Text('Primary color'.i18n),
leading: Icon(Icons.format_paint),
subtitle: Text(
'Selected color',
'Selected color'.i18n,
style: TextStyle(
color: settings.primaryColor
),
@ -167,7 +211,7 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
context: context,
builder: (context) {
return AlertDialog(
title: Text('Primary color'),
title: Text('Primary color'.i18n),
content: Container(
height: 200,
child: MaterialColorPicker(
@ -189,8 +233,8 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
},
),
ListTile(
title: Text('Use album art primary color'),
subtitle: Text('Warning: might be buggy'),
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)),
@ -212,29 +256,29 @@ class _QualitySettingsState extends State<QualitySettings> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Quality'),),
appBar: AppBar(title: Text('Quality'.i18n),),
body: ListView(
children: <Widget>[
ListTile(
title: Text('Mobile streaming'),
title: Text('Mobile streaming'.i18n),
leading: Icon(Icons.network_cell),
),
QualityPicker('mobile'),
Divider(),
ListTile(
title: Text('Wifi streaming'),
title: Text('Wifi streaming'.i18n),
leading: Icon(Icons.network_wifi),
),
QualityPicker('wifi'),
Divider(),
ListTile(
title: Text('Offline'),
title: Text('Offline'.i18n),
leading: Icon(Icons.offline_pin),
),
QualityPicker('offline'),
Divider(),
ListTile(
title: Text('External downloads'),
title: Text('External downloads'.i18n),
leading: Icon(Icons.file_download),
),
QualityPicker('download'),
@ -349,12 +393,12 @@ class _DeezerSettingsState extends State<DeezerSettings> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Deezer'),),
appBar: AppBar(title: Text('Deezer'.i18n),),
body: ListView(
children: <Widget>[
ListTile(
title: Text('Content language'),
subtitle: Text('Not app language, used in headers. Now: ${settings.deezerLanguage}'),
title: Text('Content language'.i18n),
subtitle: Text('Not app language, used in headers. Now'.i18n + ': ${settings.deezerLanguage}'),
leading: Icon(Icons.language),
onTap: () {
showDialog(
@ -362,7 +406,7 @@ class _DeezerSettingsState extends State<DeezerSettings> {
builder: (context) => LanguagePickerDialog(
titlePadding: EdgeInsets.all(8.0),
isSearchable: true,
title: Text('Select language'),
title: Text('Select language'.i18n),
onValuePicked: (Language language) {
setState(() => settings.deezerLanguage = language.isoCode);
settings.save();
@ -372,8 +416,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
},
),
ListTile(
title: Text('Content country'),
subtitle: Text('Country used in headers. Now: ${settings.deezerCountry}'),
title: Text('Content country'.i18n),
subtitle: Text('Country used in headers. Now'.i18n + ': ${settings.deezerCountry}'),
leading: Icon(Icons.vpn_lock),
onTap: () {
showDialog(
@ -390,8 +434,8 @@ class _DeezerSettingsState extends State<DeezerSettings> {
},
),
ListTile(
title: Text('Log tracks'),
subtitle: Text('Send track listen logs to Deezer, enable it for features like Flow to work properly'),
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) {
@ -415,12 +459,12 @@ class _GeneralSettingsState extends State<GeneralSettings> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('General'),),
appBar: AppBar(title: Text('General'.i18n),),
body: ListView(
children: <Widget>[
ListTile(
title: Text('Offline mode'),
subtitle: Text('Will be overwritten on start.'),
title: Text('Offline mode'.i18n),
subtitle: Text('Will be overwritten on start.'.i18n),
leading: Switch(
value: settings.offlineMode,
onChanged: (bool v) {
@ -436,7 +480,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
setState(() => settings.offlineMode = false);
} else {
Fluttertoast.showToast(
msg: 'Error logging in, check your internet connections.',
msg: 'Error logging in, check your internet connections.'.i18n,
gravity: ToastGravity.BOTTOM,
toastLength: Toast.LENGTH_SHORT
);
@ -444,7 +488,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
Navigator.of(context).pop();
});
return AlertDialog(
title: Text('Logging in...'),
title: Text('Logging in...'.i18n),
content: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
@ -459,7 +503,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
),
ListTile(
title: Text('Download path'),
title: Text('Download path'.i18n),
leading: Icon(Icons.folder),
subtitle: Text(settings.downloadPath),
onTap: () async {
@ -474,8 +518,8 @@ class _GeneralSettingsState extends State<GeneralSettings> {
},
),
ListTile(
title: Text('Downloads naming'),
subtitle: Text('Currently: ${settings.downloadFilename}'),
title: Text('Downloads naming'.i18n),
subtitle: Text('Currently'.i18n + ': ${settings.downloadFilename}'),
leading: Icon(Icons.text_format),
onTap: () {
showDialog(
@ -485,19 +529,21 @@ class _GeneralSettingsState extends State<GeneralSettings> {
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'),
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: %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%',
'Valid variables are'.i18n + ': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%',
style: TextStyle(
fontSize: 12.0,
),
@ -506,29 +552,30 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
actions: [
FlatButton(
child: Text('Cancel'),
child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text('Reset'),
child: Text('Reset'.i18n),
onPressed: () {
_controller.value = _controller.value.copyWith(
text: '%artists% - %title%'
);
_new = '%artists% - %title%';
},
),
FlatButton(
child: Text('Clear'),
child: Text('Clear'.i18n),
onPressed: () => _controller.clear(),
),
FlatButton(
child: Text('Save'),
onPressed: () {
child: Text('Save'.i18n),
onPressed: () async {
setState(() {
settings.downloadFilename = _controller.text;
settings.save();
Navigator.of(context).pop();
settings.downloadFilename = _new;
});
await settings.save();
Navigator.of(context).pop();
},
)
],
@ -538,7 +585,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
},
),
ListTile(
title: Text('Create folders for artist'),
title: Text('Create folders for artist'.i18n),
leading: Switch(
value: settings.artistFolder,
onChanged: (v) {
@ -548,7 +595,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
),
ListTile(
title: Text('Create folders for albums'),
title: Text('Create folders for albums'.i18n),
leading: Switch(
value: settings.albumFolder,
onChanged: (v) {
@ -558,7 +605,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
),
ListTile(
title: Text('Separate albums by discs'),
title: Text('Separate albums by discs'.i18n),
leading: Switch(
value: settings.albumDiscFolder,
onChanged: (v) {
@ -568,7 +615,7 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
),
ListTile(
title: Text('Overwrite already downloaded files'),
title: Text('Overwrite already downloaded files'.i18n),
leading: Switch(
value: settings.overwriteDownload,
onChanged: (v) {
@ -578,40 +625,40 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
),
ListTile(
title: Text('Copy ARL'),
subtitle: Text('Copy userToken/ARL Cookie for use in other apps.'),
title: Text('Copy ARL'.i18n),
subtitle: Text('Copy userToken/ARL Cookie for use in other apps.'.i18n),
leading: Icon(Icons.lock),
onTap: () async {
await FlutterClipboard.copy(settings.arl);
await Fluttertoast.showToast(
msg: 'Copied',
msg: 'Copied'.i18n,
);
},
),
ListTile(
title: Text('Log out', style: TextStyle(color: Colors.red),),
title: Text('Log out'.i18n, style: TextStyle(color: Colors.red),),
leading: Icon(Icons.exit_to_app),
onTap: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Log out'),
content: Text('Due to plugin incompatibility, login using browser is unavailable without restart.'),
title: Text('Log out'.i18n),
content: Text('Due to plugin incompatibility, login using browser is unavailable without restart.'.i18n),
actions: <Widget>[
FlatButton(
child: Text('Cancel'),
child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text('(ARL ONLY) Continue'),
child: Text('(ARL ONLY) Continue'.i18n),
onPressed: () {
logOut();
Navigator.of(context).pop();
},
),
FlatButton(
child: Text('Log out & Exit'),
child: Text('Log out & Exit'.i18n),
onPressed: () async {
try {AudioService.stop();} catch (e) {}
await logOut();
@ -656,7 +703,7 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Pick-a-Path'),
title: Text('Pick-a-Path'.i18n),
actions: <Widget>[
IconButton(
icon: Icon(Icons.sd_card),
@ -667,7 +714,7 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
context: context,
builder: (context) {
return AlertDialog(
title: Text('Select storage'),
title: Text('Select storage'.i18n),
content: FutureBuilder(
future: PathProviderEx.getStorageInfo(),
builder: (context, snapshot) {
@ -736,13 +783,13 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
title: Text(_path),
),
ListTile(
title: Text('Go up'),
title: Text('Go up'.i18n),
leading: Icon(Icons.arrow_upward),
onTap: () {
setState(() {
if (_root == _path) {
Fluttertoast.showToast(
msg: 'Permission denied',
msg: 'Permission denied'.i18n,
gravity: ToastGravity.BOTTOM
);
return;