0.6.7 - shows in search, album art resolution

This commit is contained in:
exttex 2020-12-14 18:29:28 +01:00
parent babd12bae2
commit c3a26b0e3b
18 changed files with 369 additions and 28 deletions

View File

@ -400,8 +400,8 @@ public class Deezer {
PictureTypes.DEFAULT_ID, PictureTypes.DEFAULT_ID,
ImageFormats.MIME_TYPE_JPEG, ImageFormats.MIME_TYPE_JPEG,
"cover", "cover",
1400, settings.albumArtResolution,
1400, settings.albumArtResolution,
24, 24,
0 0
)); ));

View File

@ -489,7 +489,7 @@ public class DownloadService extends Service {
File coverFile = new File(outFile.getPath().substring(0, outFile.getPath().lastIndexOf('.')) + ".jpg"); File coverFile = new File(outFile.getPath().substring(0, outFile.getPath().lastIndexOf('.')) + ".jpg");
try { try {
URL url = new URL("http://e-cdn-images.deezer.com/images/cover/" + trackJson.getString("md5_image") + "/1400x1400-000000-80-0-0.jpg"); URL url = new URL("http://e-cdn-images.deezer.com/images/cover/" + albumJson.getString("md5_image") + "/" + Integer.toString(settings.albumArtResolution) + "x" + Integer.toString(settings.albumArtResolution) + "-000000-80-0-0.jpg");
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); HttpURLConnection connection = (HttpURLConnection) url.openConnection();
//Set headers //Set headers
connection.setRequestMethod("GET"); connection.setRequestMethod("GET");
@ -568,7 +568,7 @@ public class DownloadService extends Service {
//Create to lock //Create to lock
coverFile.createNewFile(); coverFile.createNewFile();
URL url = new URL("http://e-cdn-images.deezer.com/images/cover/" + albumJson.getString("md5_image") + "/1400x1400-000000-80-0-0.jpg"); URL url = new URL("http://e-cdn-images.deezer.com/images/cover/" + albumJson.getString("md5_image") + "/" + Integer.toString(settings.albumArtResolution) + "x" + Integer.toString(settings.albumArtResolution) + "-000000-80-0-0.jpg");
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); HttpURLConnection connection = (HttpURLConnection) url.openConnection();
//Set headers //Set headers
connection.setRequestMethod("GET"); connection.setRequestMethod("GET");
@ -808,8 +808,9 @@ public class DownloadService extends Service {
boolean albumCover; boolean albumCover;
boolean nomediaFiles; boolean nomediaFiles;
String artistSeparator; String artistSeparator;
int albumArtResolution;
private DownloadSettings(int downloadThreads, boolean overwriteDownload, boolean downloadLyrics, boolean trackCover, String arl, boolean albumCover, boolean nomediaFiles, String artistSeparator) { private DownloadSettings(int downloadThreads, boolean overwriteDownload, boolean downloadLyrics, boolean trackCover, String arl, boolean albumCover, boolean nomediaFiles, String artistSeparator, int albumArtResolution) {
this.downloadThreads = downloadThreads; this.downloadThreads = downloadThreads;
this.overwriteDownload = overwriteDownload; this.overwriteDownload = overwriteDownload;
this.downloadLyrics = downloadLyrics; this.downloadLyrics = downloadLyrics;
@ -818,6 +819,7 @@ public class DownloadService extends Service {
this.albumCover = albumCover; this.albumCover = albumCover;
this.nomediaFiles = nomediaFiles; this.nomediaFiles = nomediaFiles;
this.artistSeparator = artistSeparator; this.artistSeparator = artistSeparator;
this.albumArtResolution = albumArtResolution;
} }
//Parse settings from bundle sent from UI //Parse settings from bundle sent from UI
@ -833,7 +835,8 @@ public class DownloadService extends Service {
json.getString("arl"), json.getString("arl"),
json.getBoolean("albumCover"), json.getBoolean("albumCover"),
json.getBoolean("nomediaFiles"), json.getBoolean("nomediaFiles"),
json.getString("artistSeparator") json.getString("artistSeparator"),
json.getInt("albumArtResolution")
); );
} catch (Exception e) { } catch (Exception e) {
//Shouldn't happen //Shouldn't happen

View File

@ -51,6 +51,9 @@ class Cache {
@JsonKey(defaultValue: 0) @JsonKey(defaultValue: 0)
int lastUpdateCheck; int lastUpdateCheck;
@JsonKey(ignore: true)
bool wakelock = false;
Cache({this.libraryTracks}); Cache({this.libraryTracks});
//Wrapper to test if track is favorite against cache //Wrapper to test if track is favorite against cache

View File

@ -392,7 +392,7 @@ class Playlist {
id: json['PLAYLIST_ID'].toString(), id: json['PLAYLIST_ID'].toString(),
title: json['TITLE'], title: json['TITLE'],
trackCount: json['NB_SONG']??songsJson['total'], trackCount: json['NB_SONG']??songsJson['total'],
image: ImageDetails.fromPrivateString(json['PLAYLIST_PICTURE'], type: 'playlist'), image: ImageDetails.fromPrivateString(json['PLAYLIST_PICTURE'], type: json['PICTURE_TYPE']),
fans: json['NB_FAN'], fans: json['NB_FAN'],
duration: Duration(seconds: json['DURATION']??0), duration: Duration(seconds: json['DURATION']??0),
description: json['DESCRIPTION'], description: json['DESCRIPTION'],
@ -464,7 +464,7 @@ class ImageDetails {
//JSON //JSON
factory ImageDetails.fromPrivateString(String art, {String type='cover'}) => ImageDetails( factory ImageDetails.fromPrivateString(String art, {String type='cover'}) => ImageDetails(
fullUrl: 'https://e-cdns-images.dzcdn.net/images/$type/$art/1400x1400-000000-80-0-0.jpg', fullUrl: 'https://e-cdns-images.dzcdn.net/images/$type/$art/1000x1000-000000-80-0-0.jpg',
thumbUrl: 'https://e-cdns-images.dzcdn.net/images/$type/$art/140x140-000000-80-0-0.jpg' thumbUrl: 'https://e-cdns-images.dzcdn.net/images/$type/$art/140x140-000000-80-0-0.jpg'
); );
factory ImageDetails.fromPrivateJson(Map<dynamic, dynamic> json) => ImageDetails.fromPrivateString( factory ImageDetails.fromPrivateJson(Map<dynamic, dynamic> json) => ImageDetails.fromPrivateString(
@ -481,22 +481,28 @@ class SearchResults {
List<Album> albums; List<Album> albums;
List<Artist> artists; List<Artist> artists;
List<Playlist> playlists; List<Playlist> playlists;
List<Show> shows;
List<ShowEpisode> episodes;
SearchResults({this.tracks, this.albums, this.artists, this.playlists}); SearchResults({this.tracks, this.albums, this.artists, this.playlists, this.shows, this.episodes});
//Check if no search results //Check if no search results
bool get empty { bool get empty {
return ((tracks == null || tracks.length == 0) && return ((tracks == null || tracks.length == 0) &&
(albums == null || albums.length == 0) && (albums == null || albums.length == 0) &&
(artists == null || artists.length == 0) && (artists == null || artists.length == 0) &&
(playlists == null || playlists.length == 0)); (playlists == null || playlists.length == 0) &&
(shows == null || shows.length == 0) &&
(episodes == null || episodes.length == 0));
} }
factory SearchResults.fromPrivateJson(Map<dynamic, dynamic> json) => SearchResults( factory SearchResults.fromPrivateJson(Map<dynamic, dynamic> json) => SearchResults(
tracks: json['TRACK']['data'].map<Track>((dynamic data) => Track.fromPrivateJson(data)).toList(), tracks: json['TRACK']['data'].map<Track>((dynamic data) => Track.fromPrivateJson(data)).toList(),
albums: json['ALBUM']['data'].map<Album>((dynamic data) => Album.fromPrivateJson(data)).toList(), albums: json['ALBUM']['data'].map<Album>((dynamic data) => Album.fromPrivateJson(data)).toList(),
artists: json['ARTIST']['data'].map<Artist>((dynamic data) => Artist.fromPrivateJson(data)).toList(), artists: json['ARTIST']['data'].map<Artist>((dynamic data) => Artist.fromPrivateJson(data)).toList(),
playlists: json['PLAYLIST']['data'].map<Playlist>((dynamic data) => Playlist.fromPrivateJson(data)).toList() playlists: json['PLAYLIST']['data'].map<Playlist>((dynamic data) => Playlist.fromPrivateJson(data)).toList(),
shows: json['SHOW']['data'].map<Show>((dynamic data) => Show.fromPrivateJson(data)).toList(),
episodes: json['EPISODE']['data'].map<ShowEpisode>((dynamic data) => ShowEpisode.fromPrivateJson(data)).toList()
); );
} }
@ -897,8 +903,10 @@ class ShowEpisode {
String url; String url;
Duration duration; Duration duration;
String publishedDate; String publishedDate;
//Might not be fully available
Show show;
ShowEpisode({this.id, this.title, this.description, this.url, this.duration, this.publishedDate}); ShowEpisode({this.id, this.title, this.description, this.url, this.duration, this.publishedDate, this.show});
String get durationString => "${duration.inMinutes}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}"; String get durationString => "${duration.inMinutes}:${duration.inSeconds.remainder(60).toString().padLeft(2, '0')}";
@ -917,7 +925,7 @@ class ShowEpisode {
}, },
displayDescription: description, displayDescription: description,
duration: duration, duration: duration,
artUri: show.art.full artUri: show.art.full,
); );
} }
factory ShowEpisode.fromMediaItem(MediaItem mi) { factory ShowEpisode.fromMediaItem(MediaItem mi) {
@ -927,6 +935,7 @@ class ShowEpisode {
description: mi.displayDescription, description: mi.displayDescription,
url: mi.extras['showUrl'], url: mi.extras['showUrl'],
duration: mi.duration, duration: mi.duration,
show: Show.fromPrivateJson(mi.extras['show'])
); );
} }
@ -937,7 +946,8 @@ class ShowEpisode {
description: json['EPISODE_DESCRIPTION'], description: json['EPISODE_DESCRIPTION'],
url: json['EPISODE_DIRECT_STREAM_URL'], url: json['EPISODE_DIRECT_STREAM_URL'],
duration: Duration(seconds: int.parse(json['DURATION'].toString())), duration: Duration(seconds: int.parse(json['DURATION'].toString())),
publishedDate: json['EPISODE_PUBLISHED_TIMESTAMP'] publishedDate: json['EPISODE_PUBLISHED_TIMESTAMP'],
show: Show.fromPrivateJson(json)
); );
factory ShowEpisode.fromJson(Map<String, dynamic> json) => _$ShowEpisodeFromJson(json); factory ShowEpisode.fromJson(Map<String, dynamic> json) => _$ShowEpisodeFromJson(json);

View File

@ -453,6 +453,9 @@ ShowEpisode _$ShowEpisodeFromJson(Map<String, dynamic> json) {
? null ? null
: Duration(microseconds: json['duration'] as int), : Duration(microseconds: json['duration'] as int),
publishedDate: json['publishedDate'] as String, publishedDate: json['publishedDate'] as String,
show: json['show'] == null
? null
: Show.fromJson(json['show'] as Map<String, dynamic>),
); );
} }
@ -464,4 +467,5 @@ Map<String, dynamic> _$ShowEpisodeToJson(ShowEpisode instance) =>
'url': instance.url, 'url': instance.url,
'duration': instance.duration?.inMicroseconds, 'duration': instance.duration?.inMicroseconds,
'publishedDate': instance.publishedDate, 'publishedDate': instance.publishedDate,
'show': instance.show,
}; };

View File

@ -45,6 +45,7 @@ class PlayerHelper {
if (event['action'] == 'onRestore') { if (event['action'] == 'onRestore') {
//Load queueSource from isolate //Load queueSource from isolate
this.queueSource = QueueSource.fromJson(event['queueSource']); this.queueSource = QueueSource.fromJson(event['queueSource']);
repeatType = LoopMode.values[event['loopMode']];
} }
if (event['action'] == 'queueEnd') { if (event['action'] == 'queueEnd') {
//If last song is played, load more queue //If last song is played, load more queue
@ -332,6 +333,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
int wifiQuality; int wifiQuality;
QueueSource queueSource; QueueSource queueSource;
Duration _lastPosition; Duration _lastPosition;
LoopMode _loopMode = LoopMode.off;
Completer _androidAutoCallback; Completer _androidAutoCallback;
@ -439,6 +441,19 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override @override
Future<void> onSeekBackward(bool begin) async => _seekContinuously(begin, -1); Future<void> onSeekBackward(bool begin) async => _seekContinuously(begin, -1);
//Remove item from queue
@override
Future<void> onRemoveQueueItem(MediaItem mediaItem) async {
int index = _queue.indexWhere((m) => m.id == mediaItem.id);
_queue.removeAt(index);
if (index <= _queueIndex) {
_queueIndex--;
}
_audioSource.removeAt(index);
AudioServiceBackground.setQueue(_queue);
}
@override @override
Future<void> onSkipToNext() async { Future<void> onSkipToNext() async {
if (_queueIndex == _queue.length-1) return; if (_queueIndex == _queue.length-1) return;
@ -630,7 +645,8 @@ class AudioPlayerTask extends BackgroundAudioTask {
} }
//Looping //Looping
if (name == 'repeatType') { if (name == 'repeatType') {
_player.setLoopMode(LoopMode.values[args]); _loopMode = LoopMode.values[args];
_player.setLoopMode(_loopMode);
} }
if (name == 'saveQueue') if (name == 'saveQueue')
await this._saveQueue(); await this._saveQueue();
@ -708,6 +724,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
'queue': _queue.map<Map<String, dynamic>>((mi) => mi.toJson()).toList(), 'queue': _queue.map<Map<String, dynamic>>((mi) => mi.toJson()).toList(),
'position': _player.position.inMilliseconds, 'position': _player.position.inMilliseconds,
'queueSource': (queueSource??QueueSource()).toJson(), 'queueSource': (queueSource??QueueSource()).toJson(),
'loopMode': LoopMode.values.indexOf(_loopMode??LoopMode.off)
}; };
await f.writeAsString(jsonEncode(data)); await f.writeAsString(jsonEncode(data));
} }
@ -721,6 +738,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
this._queueIndex = json['index'] ?? 0; this._queueIndex = json['index'] ?? 0;
this._lastPosition = Duration(milliseconds: json['position']??0); this._lastPosition = Duration(milliseconds: json['position']??0);
this.queueSource = QueueSource.fromJson(json['queueSource']??{}); this.queueSource = QueueSource.fromJson(json['queueSource']??{});
this._loopMode = LoopMode.values[(json['loopMode']??0)];
//Restore queue //Restore queue
if (_queue != null) { if (_queue != null) {
await AudioServiceBackground.setQueue(_queue); await AudioServiceBackground.setQueue(_queue);
@ -731,7 +749,8 @@ class AudioPlayerTask extends BackgroundAudioTask {
//Send restored queue source to ui //Send restored queue source to ui
AudioServiceBackground.sendCustomEvent({ AudioServiceBackground.sendCustomEvent({
'action': 'onRestore', 'action': 'onRestore',
'queueSource': (queueSource??QueueSource()).toJson() 'queueSource': (queueSource??QueueSource()).toJson(),
'loopMode': LoopMode.values.indexOf(_loopMode)
}); });
return true; return true;
} }

File diff suppressed because one or more lines are too long

View File

@ -297,6 +297,16 @@ const language_en_us = {
//0.6.6 //0.6.6
"Restart of app is required to properly log out!": "Restart of app is required to properly log out!", "Restart of app is required to properly log out!": "Restart of app is required to properly log out!",
"Artist separator": "Artist separator", "Artist separator": "Artist separator",
"Singleton naming": "Standalone tracks filename" "Singleton naming": "Standalone tracks filename",
//0.6.7
"Keep the screen on": "Keep the screen on",
"Wakelock enabled!": "Wakelock enabled!",
"Wakelock disabled!": "Wakelock disabled!",
"Show all shows": "Show all shows",
"Episodes": "Episodes",
"Show all episodes": "Show all episodes",
"Album cover resolution": "Album cover resolution",
"WARNING: Resolutions above 1200 aren't officially supported": "WARNING: Resolutions above 1200 aren't officially supported"
} }
}; };

View File

@ -71,6 +71,8 @@ class Settings {
String artistSeparator; String artistSeparator;
@JsonKey(defaultValue: "%artist% - %title%") @JsonKey(defaultValue: "%artist% - %title%")
String singletonFilename; String singletonFilename;
@JsonKey(defaultValue: 1400)
int albumArtResolution;
//Appearance //Appearance
@JsonKey(defaultValue: Themes.Dark) @JsonKey(defaultValue: Themes.Dark)

View File

@ -40,6 +40,7 @@ Settings _$SettingsFromJson(Map<String, dynamic> json) {
..artistSeparator = json['artistSeparator'] as String ?? ', ' ..artistSeparator = json['artistSeparator'] as String ?? ', '
..singletonFilename = ..singletonFilename =
json['singletonFilename'] as String ?? '%artist% - %title%' json['singletonFilename'] as String ?? '%artist% - %title%'
..albumArtResolution = json['albumArtResolution'] as int ?? 1400
..theme = ..theme =
_$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Dark _$enumDecodeNullable(_$ThemesEnumMap, json['theme']) ?? Themes.Dark
..useSystemTheme = json['useSystemTheme'] as bool ?? false ..useSystemTheme = json['useSystemTheme'] as bool ?? false
@ -76,6 +77,7 @@ Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
'nomediaFiles': instance.nomediaFiles, 'nomediaFiles': instance.nomediaFiles,
'artistSeparator': instance.artistSeparator, 'artistSeparator': instance.artistSeparator,
'singletonFilename': instance.singletonFilename, 'singletonFilename': instance.singletonFilename,
'albumArtResolution': instance.albumArtResolution,
'theme': _$ThemesEnumMap[instance.theme], 'theme': _$ThemesEnumMap[instance.theme],
'useSystemTheme': instance.useSystemTheme, 'useSystemTheme': instance.useSystemTheme,
'colorGradientBackground': instance.colorGradientBackground, 'colorGradientBackground': instance.colorGradientBackground,

View File

@ -28,6 +28,8 @@ const supportedLocales = [
const Locale('sk', 'SK'), const Locale('sk', 'SK'),
const Locale('cs', 'CZ'), const Locale('cs', 'CZ'),
const Locale('vi', 'VI'), const Locale('vi', 'VI'),
const Locale('nl', 'NL'),
const Locale('sl', 'SL'),
const Locale('fil', 'PH'), const Locale('fil', 'PH'),
const Locale('uwu', 'UWU') const Locale('uwu', 'UWU')
]; ];

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:wakelock/wakelock.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
@ -585,6 +586,33 @@ class MenuSheet {
}, },
); );
Widget wakelock() => ListTile(
title: Text('Keep the screen on'.i18n),
leading: Icon(Icons.screen_lock_portrait),
onTap: () async {
_close();
if (cache.wakelock == null)
cache.wakelock = false;
//Enable
if (!cache.wakelock) {
Wakelock.enable();
Fluttertoast.showToast(
msg: 'Wakelock enabled!'.i18n,
gravity: ToastGravity.BOTTOM
);
cache.wakelock = true;
return;
}
//Disable
Wakelock.disable();
Fluttertoast.showToast(
msg: 'Wakelock disabled!'.i18n,
gravity: ToastGravity.BOTTOM
);
cache.wakelock = false;
},
);
void _close() => Navigator.of(context).pop(); void _close() => Navigator.of(context).pop();
} }

View File

@ -280,7 +280,7 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
children: <Widget>[ children: <Widget>[
Container( Container(
height: ScreenUtil().setSp(64), height: ScreenUtil().setSp(64),
child: AudioService.currentMediaItem.displayTitle.length >= 24 ? child: AudioService.currentMediaItem.displayTitle.length >= 26 ?
Marquee( Marquee(
text: AudioService.currentMediaItem.displayTitle, text: AudioService.currentMediaItem.displayTitle,
style: TextStyle( style: TextStyle(
@ -410,12 +410,12 @@ class PlayerMenuButton extends StatelessWidget {
Track t = Track.fromMediaItem(AudioService.currentMediaItem); Track t = Track.fromMediaItem(AudioService.currentMediaItem);
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
if (AudioService.currentMediaItem.extras['show'] == null) if (AudioService.currentMediaItem.extras['show'] == null)
m.defaultTrackMenu(t, options: [m.sleepTimer()]); m.defaultTrackMenu(t, options: [m.sleepTimer(), m.wakelock()]);
else else
m.defaultShowEpisodeMenu( m.defaultShowEpisodeMenu(
Show.fromJson(jsonDecode(AudioService.currentMediaItem.extras['show'])), Show.fromJson(jsonDecode(AudioService.currentMediaItem.extras['show'])),
ShowEpisode.fromMediaItem(AudioService.currentMediaItem), ShowEpisode.fromMediaItem(AudioService.currentMediaItem),
options: [m.sleepTimer()] options: [m.sleepTimer(), m.wakelock()]
); );
}, },
); );
@ -771,6 +771,13 @@ class _QueueScreenState extends State<QueueScreen> {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
key: Key(t.id), key: Key(t.id),
trailing: IconButton(
icon: Icon(Icons.close),
onPressed: () async {
await AudioService.removeQueueItem(t.toMediaItem());
setState(() {});
},
),
); );
}), }),
) )

View File

@ -7,6 +7,7 @@ import 'package:flutter/src/services/keyboard_key.dart';
import 'package:freezer/api/cache.dart'; import 'package:freezer/api/cache.dart';
import 'package:freezer/api/download.dart'; import 'package:freezer/api/download.dart';
import 'package:freezer/api/player.dart'; import 'package:freezer/api/player.dart';
import 'package:freezer/main.dart';
import 'package:freezer/ui/details_screens.dart'; import 'package:freezer/ui/details_screens.dart';
import 'package:freezer/ui/elements.dart'; import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/home_screen.dart'; import 'package:freezer/ui/home_screen.dart';
@ -657,6 +658,91 @@ class SearchResultsScreen extends StatelessWidget {
MaterialPageRoute(builder: (context) => SearchResultPlaylists(results.playlists)) MaterialPageRoute(builder: (context) => SearchResultPlaylists(results.playlists))
); );
}, },
),
FreezerDivider()
];
}
//Shows
List<Widget> shows = [];
if (results.shows != null && results.shows.length != 0) {
shows = [
Padding(
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
child: Text(
'Shows'.i18n,
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold
),
),
),
...List.generate(3, (i) {
if (results.shows.length <= i) return Container(height: 0, width: 0,);
Show s = results.shows[i];
return ShowTile(
s,
onTap: () async {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ShowScreen(s)
));
},
);
}),
ListTile(
title: Text('Show all shows'.i18n),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ShowListScreen(results.shows))
);
},
),
FreezerDivider()
];
}
//Episodes
List<Widget> episodes = [];
if (results.episodes != null && results.episodes.length != 0) {
episodes = [
Padding(
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 16.0),
child: Text(
'Episodes'.i18n,
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold
),
),
),
...List.generate(3, (i) {
if (results.episodes.length <= i) return Container(height: 0, width: 0,);
ShowEpisode e = results.episodes[i];
return ShowEpisodeTile(
e,
trailing: IconButton(
icon: Icon(Icons.more_vert),
onPressed: () {
MenuSheet m = MenuSheet(context);
m.defaultShowEpisodeMenu(e.show, e);
},
),
onTap: () async {
//Load entire show, then play
List<ShowEpisode> episodes = await deezerAPI.allShowEpisodes(e.show.id);
await playerHelper.playShowEpisode(e.show, episodes, index: episodes.indexWhere((ep) => e.id == ep.id));
},
);
}),
ListTile(
title: Text('Show all episodes'.i18n),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => EpisodeListScreen(results.episodes))
);
}
) )
]; ];
} }
@ -670,7 +756,11 @@ class SearchResultsScreen extends StatelessWidget {
Container(height: 8.0,), Container(height: 8.0,),
...artists, ...artists,
Container(height: 8.0,), Container(height: 8.0,),
...playlists ...playlists,
Container(height: 8.0,),
...shows,
Container(height: 8.0,),
...episodes
], ],
); );
}, },
@ -773,3 +863,64 @@ class SearchResultPlaylists extends StatelessWidget {
); );
} }
} }
class ShowListScreen extends StatelessWidget {
final List<Show> shows;
ShowListScreen(this.shows);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar('Shows'.i18n),
body: ListView.builder(
itemCount: shows.length,
itemBuilder: (context, i) {
Show s = shows[i];
return ShowTile(
s,
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ShowScreen(s)
));
},
);
},
),
);
}
}
class EpisodeListScreen extends StatelessWidget {
final List<ShowEpisode> episodes;
EpisodeListScreen(this.episodes);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar('Episodes'.i18n),
body: ListView.builder(
itemCount: episodes.length,
itemBuilder: (context, i) {
ShowEpisode e = episodes[i];
return ShowEpisodeTile(
e,
trailing: IconButton(
icon: Icon(Icons.more_vert),
onPressed: () {
MenuSheet m = MenuSheet(context);
m.defaultShowEpisodeMenu(e.show, e);
},
),
onTap: () async {
//Load entire show, then play
List<ShowEpisode> episodes = await deezerAPI.allShowEpisodes(e.show.id);
await playerHelper.playShowEpisode(e.show, episodes, index: episodes.indexWhere((ep) => e.id == ep.id));
},
);
},
)
);
}
}

View File

@ -856,6 +856,27 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
), ),
leading: Icon(Icons.image) leading: Icon(Icons.image)
), ),
ListTile(
title: Text('Album cover resolution'.i18n),
subtitle: Text("WARNING: Resolutions above 1200 aren't officially supported".i18n),
leading: Icon(Icons.image),
trailing: Container(
width: 75.0,
child: DropdownButton<int>(
value: settings.albumArtResolution,
items: [400, 800, 1000, 1200, 1400, 1600, 1800].map<DropdownMenuItem<int>>((int i) => DropdownMenuItem<int>(
value: i,
child: Text(i.toString()),
)).toList(),
onChanged: (int n) async {
setState(() {
settings.albumArtResolution = n;
});
await settings.save();
},
)
)
),
ListTile( ListTile(
title: Text('Create .nomedia files'.i18n), title: Text('Create .nomedia files'.i18n),
subtitle: Text('To prevent gallery being filled with album art'.i18n), subtitle: Text('To prevent gallery being filled with album art'.i18n),
@ -872,7 +893,7 @@ class _DownloadsSettingsState extends State<DownloadsSettings> {
title: Text('Artist separator'.i18n), title: Text('Artist separator'.i18n),
leading: Icon(WebSymbols.tag), leading: Icon(WebSymbols.tag),
trailing: Container( trailing: Container(
width: 100.0, width: 75.0,
child: TextField( child: TextField(
controller: _artistSeparatorController, controller: _artistSeparatorController,
onChanged: (s) async { onChanged: (s) async {

View File

@ -1,6 +1,8 @@
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttericon/octicons_icons.dart';
import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/download.dart';
import 'package:freezer/translations.i18n.dart'; import 'package:freezer/translations.i18n.dart';
import '../api/definitions.dart'; import '../api/definitions.dart';
@ -25,6 +27,7 @@ class TrackTile extends StatefulWidget {
class _TrackTileState extends State<TrackTile> { class _TrackTileState extends State<TrackTile> {
StreamSubscription _subscription; StreamSubscription _subscription;
bool _isOffline = false;
bool get nowPlaying { bool get nowPlaying {
if (AudioService.currentMediaItem == null) return false; if (AudioService.currentMediaItem == null) return false;
@ -37,6 +40,9 @@ class _TrackTileState extends State<TrackTile> {
_subscription = AudioService.currentMediaItemStream.listen((event) { _subscription = AudioService.currentMediaItemStream.listen((event) {
setState(() {}); setState(() {});
}); });
//Check if offline
downloadManager.checkOffline(track: widget.track).then((b) => setState(() => _isOffline = b));
super.initState(); super.initState();
} }
@ -70,9 +76,18 @@ class _TrackTileState extends State<TrackTile> {
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if ((_isOffline??false))
Padding(
padding: EdgeInsets.symmetric(horizontal: 2.0),
child: Icon(
Octicons.primitive_dot,
color: Colors.green,
size: 12.0,
),
),
if (widget.track.explicit??false) if (widget.track.explicit??false)
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 4.0), padding: EdgeInsets.symmetric(horizontal: 2.0),
child: Text( child: Text(
'E', 'E',
style: TextStyle( style: TextStyle(
@ -80,9 +95,12 @@ class _TrackTileState extends State<TrackTile> {
), ),
), ),
), ),
Padding( Container(
padding: EdgeInsets.symmetric(horizontal: 2.0), width: 42.0,
child: Text(widget.track.durationString), child: Text(
widget.track.durationString,
textAlign: TextAlign.center,
),
), ),
widget.trailing??Container(width: 0, height: 0) widget.trailing??Container(width: 0, height: 0)
], ],
@ -497,6 +515,38 @@ class ShowCard extends StatelessWidget {
} }
} }
class ShowTile extends StatelessWidget {
final Show show;
final Function onTap;
final Function onHold;
ShowTile(this.show, {this.onTap, this.onHold});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(
show.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
show.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: onTap,
onLongPress: onHold,
leading: CachedImage(
url: show.art.thumb,
width: 48,
),
);
}
}
class ShowEpisodeTile extends StatelessWidget { class ShowEpisodeTile extends StatelessWidget {
final ShowEpisode episode; final ShowEpisode episode;

View File

@ -462,6 +462,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.4.5" version: "1.4.5"
import_js_library:
dependency: transitive
description:
name: import_js_library
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
infinite_listview: infinite_listview:
dependency: transitive dependency: transitive
description: description:
@ -985,6 +992,27 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
wakelock:
dependency: "direct main"
description:
name: wakelock
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.1+1"
wakelock_platform_interface:
dependency: transitive
description:
name: wakelock_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0+1"
wakelock_web:
dependency: transitive
description:
name: wakelock_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.0+3"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:

View File

@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.6.6+1 version: 0.6.7+1
environment: environment:
sdk: ">=2.8.0 <3.0.0" sdk: ">=2.8.0 <3.0.0"
@ -74,6 +74,7 @@ dependencies:
scrobblenaut: ^2.0.4 scrobblenaut: ^2.0.4
open_file: ^3.0.3 open_file: ^3.0.3
version: ^1.2.0 version: ^1.2.0
wakelock: ^0.2.1+1
audio_session: ^0.0.9 audio_session: ^0.0.9
audio_service: audio_service: