Merge branch 'master' into feature/atv

# Conflicts:
#	lib/ui/player_screen.dart
#	lib/ui/search.dart
This commit is contained in:
kilowatt 2020-11-30 20:21:21 +03:00
commit 1ea904ec8d
34 changed files with 1755 additions and 486 deletions

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:fluttericon/font_awesome5_icons.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:freezer/api/cache.dart';
import 'package:freezer/api/deezer.dart';
@ -683,13 +684,6 @@ class _DiscographyScreenState extends State<DiscographyScreen> {
}
}
enum SortType {
DEFAULT,
REVERSE,
ALPHABETIC,
ARTIST
}
class PlaylistDetails extends StatefulWidget {
Playlist playlist;
@ -704,25 +698,30 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
Playlist playlist;
bool _loading = false;
bool _error = false;
SortType _sort = SortType.DEFAULT;
Sorting _sort;
ScrollController _scrollController = ScrollController();
//Get sorted playlist
List<Track> get sorted {
List<Track> tracks = new List.from(playlist.tracks??[]);
switch (_sort) {
switch (_sort.type) {
case SortType.ALPHABETIC:
tracks.sort((a, b) => a.title.compareTo(b.title));
return tracks;
break;
case SortType.ARTIST:
tracks.sort((a, b) => a.artists[0].name.toLowerCase().compareTo(b.artists[0].name.toLowerCase()));
return tracks;
case SortType.REVERSE:
return tracks.reversed.toList();
break;
case SortType.DATE_ADDED:
tracks.sort((a, b) => (a.addedDate??0) - (b.addedDate??0));
break;
case SortType.DEFAULT:
default:
return tracks;
break;
}
//Reverse
if (_sort.reverse)
return tracks.reversed.toList();
return tracks;
}
//Load tracks from api
@ -748,23 +747,40 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
//Load cached playlist sorting
void _restoreSort() async {
if (cache.playlistSort == null) {
cache.playlistSort = {};
await cache.save();
//Find index
int index = Sorting.index(SortSourceTypes.PLAYLIST, id: playlist.id);
if (index == null)
return;
//Preload tracks
if (playlist.tracks.length < playlist.trackCount) {
playlist = await deezerAPI.fullPlaylist(playlist.id);
}
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]);
setState(() => _sort = cache.sorts[index]);
}
Future _reverse() async {
setState(() => _sort.reverse = !_sort.reverse);
//Save sorting in cache
int index = Sorting.index(SortSourceTypes.TRACKS);
if (index != null) {
cache.sorts[index] = _sort;
} else {
cache.sorts.add(_sort);
}
await cache.save();
//Preload for sorting
if (playlist.tracks.length < playlist.trackCount) {
playlist = await deezerAPI.fullPlaylist(playlist.id);
}
}
@override
void initState() {
playlist = widget.playlist;
_sort = Sorting(sourceType: SortSourceTypes.PLAYLIST, id: playlist.id);
//If scrolled past 90% load next tracks
_scrollController.addListener(() {
double off = _scrollController.position.maxScrollExtent * 0.90;
@ -918,21 +934,22 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
//Preload whole playlist
playlist = await deezerAPI.fullPlaylist(playlist.id);
}
setState(() => _sort = s);
setState(() => _sort.type = s);
//Save sort type to cache
cache.playlistSort[playlist.id] = s;
cache.save();
int index = Sorting.index(SortSourceTypes.PLAYLIST, id: playlist.id);
if (index == null) {
cache.sorts.add(_sort);
} else {
cache.sorts[index] = _sort;
}
await cache.save();
},
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
PopupMenuItem(
value: SortType.DEFAULT,
child: Text('Default'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: SortType.REVERSE,
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: SortType.ALPHABETIC,
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
@ -941,8 +958,16 @@ class _PlaylistDetailsState extends State<PlaylistDetails> {
value: SortType.ARTIST,
child: Text('Artist'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: SortType.DATE_ADDED,
child: Text('Date added'.i18n, style: popupMenuTextStyle()),
),
],
),
IconButton(
icon: Icon(_sort.reverse ? FontAwesome5.sort_alpha_up : FontAwesome5.sort_alpha_down),
onPressed: () => _reverse(),
),
Container(width: 4.0)
],
),
@ -1039,3 +1064,136 @@ class _MakePlaylistOfflineState extends State<MakePlaylistOffline> {
);
}
}
class ShowScreen extends StatefulWidget {
Show show;
ShowScreen(this.show, {Key key}): super(key: key);
@override
_ShowScreenState createState() => _ShowScreenState();
}
class _ShowScreenState extends State<ShowScreen> {
Show _show;
bool _loading = true;
bool _error = false;
List<ShowEpisode> _episodes;
Future _load() async {
//Fetch
List<ShowEpisode> e;
try {
e = await deezerAPI.allShowEpisodes(_show.id);
} catch (e) {
setState(() {
_loading = false;
_error = true;
});
return;
}
setState(() {
_episodes = e;
_loading = false;
});
}
@override
void initState() {
_show = widget.show;
_load();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar(_show.name),
body: ListView(
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: [
CachedImage(
url: _show.art.full,
rounded: true,
width: MediaQuery.of(context).size.width / 2 - 16,
),
Container(
width: MediaQuery.of(context).size.width / 2 - 16,
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Text(
_show.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold
)
),
Container(height: 8.0),
Text(
_show.description,
maxLines: 6,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.0
),
)
],
),
)
],
),
),
Container(height: 4.0),
FreezerDivider(),
//Error
if (_error)
ErrorScreen(),
//Loading
if (_loading)
Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator()
],
),
),
//Data
if (!_loading && !_error)
...List.generate(_episodes.length, (i) {
ShowEpisode e = _episodes[i];
return ShowEpisodeTile(
e,
trailing: IconButton(
icon: Icon(Icons.more_vert),
onPressed: () {
MenuSheet m = MenuSheet(context);
m.defaultShowEpisodeMenu(_show, e);
},
),
onTap: () async {
await playerHelper.playShowEpisode(_show, _episodes, index: i);
},
);
})
],
),
);
}
}

View file

@ -280,6 +280,15 @@ class HomePageItemWidget extends StatelessWidget {
));
},
);
case HomePageItemType.SHOW:
return ShowCard(
item.value,
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ShowScreen(item.value)
));
},
);
}
return Container(height: 0, width: 0);
}

View file

@ -1,5 +1,6 @@
import 'package:connectivity/connectivity.dart';
import 'package:flutter/material.dart';
import 'package:fluttericon/font_awesome5_icons.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:freezer/api/cache.dart';
import 'package:freezer/api/deezer.dart';
@ -220,26 +221,44 @@ class _LibraryTracksState extends State<LibraryTracks> {
List<Track> tracks = [];
List<Track> allTracks = [];
int trackCount;
SortType _sort = SortType.DEFAULT;
Sorting _sort = Sorting(sourceType: SortSourceTypes.TRACKS);
Playlist get _playlist => Playlist(id: deezerAPI.favoritesPlaylistId);
List<Track> get _sorted {
List<Track> tcopy = List.from(tracks);
tcopy.sort((a, b) => a.favoriteDate.compareTo(b.favoriteDate));
switch (_sort) {
tcopy.sort((a, b) => a.addedDate.compareTo(b.addedDate));
switch (_sort.type) {
case SortType.ALPHABETIC:
tcopy.sort((a, b) => a.title.compareTo(b.title));
return tcopy;
break;
case SortType.ARTIST:
tcopy.sort((a, b) => a.artists[0].name.toLowerCase().compareTo(b.artists[0].name.toLowerCase()));
return tcopy;
case SortType.REVERSE:
return tcopy.reversed.toList();
break;
case SortType.DEFAULT:
default:
return tcopy;
break;
}
//Reverse
if (_sort.reverse)
return tcopy.reversed.toList();
return tcopy;
}
Future _reverse() async {
setState(() => _sort.reverse = !_sort.reverse);
//Save sorting in cache
int index = Sorting.index(SortSourceTypes.TRACKS);
if (index != null) {
cache.sorts[index] = _sort;
} else {
cache.sorts.add(_sort);
}
await cache.save();
//Preload for sorting
if (tracks.length < (trackCount??0))
_loadFull();
}
Future _load() async {
@ -274,7 +293,8 @@ class _LibraryTracksState extends State<LibraryTracks> {
//Update
setState(() {
trackCount = favPlaylist.trackCount;
tracks = favPlaylist.tracks;
if (tracks.length == 0)
tracks = favPlaylist.tracks;
_makeFavorite();
_loading = false;
});
@ -306,7 +326,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
//Load all tracks
Future _loadFull() async {
if (tracks.length < (trackCount??0)) {
if (tracks.length == 0 || tracks.length < (trackCount??0)) {
Playlist p;
try {
p = await deezerAPI.fullPlaylist(deezerAPI.favoritesPlaylistId);
@ -315,6 +335,7 @@ class _LibraryTracksState extends State<LibraryTracks> {
setState(() {
tracks = p.tracks;
trackCount = p.trackCount;
_sort = _sort;
});
}
}
@ -348,13 +369,16 @@ class _LibraryTracksState extends State<LibraryTracks> {
if (_scrollController.position.pixels > off) _load();
});
_sort = cache.trackSort??SortType.DEFAULT;
_load();
//Load all offline tracks
_loadAllOffline();
if (_sort != SortType.DEFAULT)
//Load sorting
int index = Sorting.index(SortSourceTypes.TRACKS);
if (index != null)
setState(() => _sort = cache.sorts[index]);
if (_sort.type != SortType.DEFAULT || _sort.reverse)
_loadFull();
super.initState();
@ -366,6 +390,12 @@ class _LibraryTracksState extends State<LibraryTracks> {
appBar: FreezerAppBar(
'Tracks'.i18n,
actions: [
IconButton(
icon: Icon(_sort.reverse ? FontAwesome5.sort_alpha_up : FontAwesome5.sort_alpha_down),
onPressed: () async {
await _reverse();
}
),
PopupMenuButton(
child: Icon(Icons.sort, size: 32.0),
color: Theme.of(context).scaffoldBackgroundColor,
@ -374,8 +404,14 @@ class _LibraryTracksState extends State<LibraryTracks> {
if (tracks.length < (trackCount??0))
await _loadFull();
setState(() => _sort = s);
cache.trackSort = s;
setState(() => _sort.type = s);
//Save sorting in cache
int index = Sorting.index(SortSourceTypes.TRACKS);
if (index != null) {
cache.sorts[index] = _sort;
} else {
cache.sorts.add(_sort);
}
await cache.save();
},
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
@ -383,10 +419,6 @@ class _LibraryTracksState extends State<LibraryTracks> {
value: SortType.DEFAULT,
child: Text('Default'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: SortType.REVERSE,
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: SortType.ALPHABETIC,
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
@ -498,14 +530,6 @@ class _LibraryTracksState extends State<LibraryTracks> {
}
enum AlbumSortType {
DEFAULT,
REVERSE,
ALPHABETIC,
ARTIST,
DATE
}
class LibraryAlbums extends StatefulWidget {
@override
_LibraryAlbumsState createState() => _LibraryAlbumsState();
@ -514,27 +538,28 @@ class LibraryAlbums extends StatefulWidget {
class _LibraryAlbumsState extends State<LibraryAlbums> {
List<Album> _albums;
AlbumSortType _sort = AlbumSortType.DEFAULT;
Sorting _sort = Sorting(sourceType: SortSourceTypes.ALBUMS);
ScrollController _scrollController = ScrollController();
List<Album> get _sorted {
List<Album> albums = List.from(_albums);
albums.sort((a, b) => a.favoriteDate.compareTo(b.favoriteDate));
switch (_sort) {
case AlbumSortType.DEFAULT:
return albums;
case AlbumSortType.REVERSE:
return albums.reversed.toList();
case AlbumSortType.ALPHABETIC:
switch (_sort.type) {
case SortType.DEFAULT:
break;
case SortType.ALPHABETIC:
albums.sort((a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase()));
return albums;
case AlbumSortType.ARTIST:
break;
case SortType.ARTIST:
albums.sort((a, b) => a.artists[0].name.toLowerCase().compareTo(b.artists[0].name.toLowerCase()));
return albums;
case AlbumSortType.DATE:
break;
case SortType.RELEASE_DATE:
albums.sort((a, b) => DateTime.parse(a.releaseDate).compareTo(DateTime.parse(b.releaseDate)));
return albums;
break;
}
//Reverse
if (_sort.reverse)
return albums.reversed.toList();
return albums;
}
@ -550,43 +575,65 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
@override
void initState() {
_load();
_sort = cache.albumSort??AlbumSortType.DEFAULT;
//Load sorting
int index = Sorting.index(SortSourceTypes.ALBUMS);
if (index != null)
_sort = cache.sorts[index];
super.initState();
}
Future _reverse() async {
setState(() => _sort.reverse = !_sort.reverse);
//Save sorting in cache
int index = Sorting.index(SortSourceTypes.ALBUMS);
if (index != null) {
cache.sorts[index] = _sort;
} else {
cache.sorts.add(_sort);
}
await cache.save();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: FreezerAppBar(
'Albums'.i18n,
actions: [
IconButton(
icon: Icon(_sort.reverse ? FontAwesome5.sort_alpha_up : FontAwesome5.sort_alpha_down),
onPressed: () => _reverse(),
),
PopupMenuButton(
color: Theme.of(context).scaffoldBackgroundColor,
child: Icon(Icons.sort, size: 32.0),
onSelected: (AlbumSortType s) async {
setState(() => _sort = s);
cache.albumSort = s;
onSelected: (SortType s) async {
setState(() => _sort.type = s);
//Save to cache
int index = Sorting.index(SortSourceTypes.ALBUMS);
if (index == null) {
cache.sorts.add(_sort);
} else {
cache.sorts[index] = _sort;
}
await cache.save();
},
itemBuilder: (context) => <PopupMenuEntry<AlbumSortType>>[
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
PopupMenuItem(
value: AlbumSortType.DEFAULT,
value: SortType.DEFAULT,
child: Text('Default'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: AlbumSortType.REVERSE,
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: AlbumSortType.ALPHABETIC,
value: SortType.ALPHABETIC,
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: AlbumSortType.ARTIST,
value: SortType.ARTIST,
child: Text('Artist'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: AlbumSortType.DATE,
value: SortType.RELEASE_DATE,
child: Text('Release date'.i18n, style: popupMenuTextStyle()),
),
],
@ -675,12 +722,6 @@ class _LibraryAlbumsState extends State<LibraryAlbums> {
}
}
enum ArtistSortType {
DEFAULT,
REVERSE,
POPULARITY,
ALPHABETIC
}
class LibraryArtists extends StatefulWidget {
@override
@ -690,7 +731,7 @@ class LibraryArtists extends StatefulWidget {
class _LibraryArtistsState extends State<LibraryArtists> {
List<Artist> _artists;
ArtistSortType _sort = ArtistSortType.DEFAULT;
Sorting _sort = Sorting(sourceType: SortSourceTypes.ARTISTS);
bool _loading = true;
bool _error = false;
ScrollController _scrollController = ScrollController();
@ -698,18 +739,19 @@ class _LibraryArtistsState extends State<LibraryArtists> {
List<Artist> get _sorted {
List<Artist> artists = List.from(_artists);
artists.sort((a, b) => a.favoriteDate.compareTo(b.favoriteDate));
switch (_sort) {
case ArtistSortType.DEFAULT:
return artists;
case ArtistSortType.REVERSE:
return artists.reversed.toList();
case ArtistSortType.POPULARITY:
switch (_sort.type) {
case SortType.DEFAULT:
break;
case SortType.POPULARITY:
artists.sort((a, b) => b.fans - a.fans);
return artists;
case ArtistSortType.ALPHABETIC:
break;
case SortType.ALPHABETIC:
artists.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
return artists;
break;
}
//Reverse
if (_sort.reverse)
return artists.reversed.toList();
return artists;
}
@ -732,9 +774,26 @@ class _LibraryArtistsState extends State<LibraryArtists> {
});
}
Future _reverse() async {
setState(() => _sort.reverse = !_sort.reverse);
//Save sorting in cache
int index = Sorting.index(SortSourceTypes.ARTISTS);
if (index != null) {
cache.sorts[index] = _sort;
} else {
cache.sorts.add(_sort);
}
await cache.save();
}
@override
void initState() {
_sort = cache.artistSort;
//Restore sort
int index = Sorting.index(SortSourceTypes.ARTISTS);
if (index != null)
_sort = cache.sorts[index];
_load();
super.initState();
}
@ -745,29 +804,35 @@ class _LibraryArtistsState extends State<LibraryArtists> {
appBar: FreezerAppBar(
'Artists'.i18n,
actions: [
IconButton(
icon: Icon(_sort.reverse ? FontAwesome5.sort_alpha_up : FontAwesome5.sort_alpha_down),
onPressed: () => _reverse(),
),
PopupMenuButton(
child: Icon(Icons.sort, size: 32.0),
color: Theme.of(context).scaffoldBackgroundColor,
onSelected: (ArtistSortType s) async {
setState(() => _sort = s);
cache.artistSort = s;
onSelected: (SortType s) async {
setState(() => _sort.type = s);
//Save
int index = Sorting.index(SortSourceTypes.ARTISTS);
if (index == null) {
cache.sorts.add(_sort);
} else {
cache.sorts[index] = _sort;
}
await cache.save();
},
itemBuilder: (context) => <PopupMenuEntry<ArtistSortType>>[
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
PopupMenuItem(
value: ArtistSortType.DEFAULT,
value: SortType.DEFAULT,
child: Text('Default'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: ArtistSortType.REVERSE,
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: ArtistSortType.ALPHABETIC,
value: SortType.ALPHABETIC,
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: ArtistSortType.POPULARITY,
value: SortType.POPULARITY,
child: Text('Popularity'.i18n, style: popupMenuTextStyle()),
),
],
@ -819,14 +884,6 @@ class _LibraryArtistsState extends State<LibraryArtists> {
}
}
enum PlaylistSortType {
DEFAULT,
REVERSE,
ALPHABETIC,
USER,
TRACK_COUNT
}
class LibraryPlaylists extends StatefulWidget {
@override
_LibraryPlaylistsState createState() => _LibraryPlaylistsState();
@ -835,27 +892,27 @@ class LibraryPlaylists extends StatefulWidget {
class _LibraryPlaylistsState extends State<LibraryPlaylists> {
List<Playlist> _playlists;
PlaylistSortType _sort = PlaylistSortType.DEFAULT;
Sorting _sort = Sorting(sourceType: SortSourceTypes.PLAYLISTS);
ScrollController _scrollController = ScrollController();
String _filter = '';
List<Playlist> get _sorted {
List<Playlist> playlists = List.from(_playlists.where((p) => p.title.toLowerCase().contains(_filter.toLowerCase())));
switch (_sort) {
case PlaylistSortType.DEFAULT:
return playlists;
case PlaylistSortType.REVERSE:
return playlists.reversed.toList();
case PlaylistSortType.USER:
switch (_sort.type) {
case SortType.DEFAULT:
break;
case SortType.USER:
playlists.sort((a, b) => (a.user.name??deezerAPI.userName).toLowerCase().compareTo((b.user.name??deezerAPI.userName).toLowerCase()));
return playlists;
case PlaylistSortType.TRACK_COUNT:
break;
case SortType.TRACK_COUNT:
playlists.sort((a, b) => b.trackCount - a.trackCount);
return playlists;
case PlaylistSortType.ALPHABETIC:
break;
case SortType.ALPHABETIC:
playlists.sort((a, b) => a.title.toLowerCase().compareTo(b.title.toLowerCase()));
return playlists;
break;
}
if (_sort.reverse)
return playlists.reversed.toList();
return playlists;
}
@ -868,9 +925,25 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
}
}
Future _reverse() async {
setState(() => _sort.reverse = !_sort.reverse);
//Save sorting in cache
int index = Sorting.index(SortSourceTypes.PLAYLISTS);
if (index != null) {
cache.sorts[index] = _sort;
} else {
cache.sorts.add(_sort);
}
await cache.save();
}
@override
void initState() {
_sort = cache.libraryPlaylistSort;
//Restore sort
int index = Sorting.index(SortSourceTypes.PLAYLISTS);
if (index != null)
_sort = cache.sorts[index];
_load();
super.initState();
}
@ -892,33 +965,39 @@ class _LibraryPlaylistsState extends State<LibraryPlaylists> {
appBar: FreezerAppBar(
'Playlists'.i18n,
actions: [
IconButton(
icon: Icon(_sort.reverse ? FontAwesome5.sort_alpha_up : FontAwesome5.sort_alpha_down),
onPressed: () => _reverse(),
),
PopupMenuButton(
child: Icon(Icons.sort, size: 32.0),
color: Theme.of(context).scaffoldBackgroundColor,
onSelected: (PlaylistSortType s) async {
setState(() => _sort = s);
cache.libraryPlaylistSort = s;
onSelected: (SortType s) async {
setState(() => _sort.type = s);
//Save to cache
int index = Sorting.index(SortSourceTypes.PLAYLISTS);
if (index == null)
cache.sorts.add(_sort);
else
cache.sorts[index] = _sort;
await cache.save();
},
itemBuilder: (context) => <PopupMenuEntry<PlaylistSortType>>[
itemBuilder: (context) => <PopupMenuEntry<SortType>>[
PopupMenuItem(
value: PlaylistSortType.DEFAULT,
value: SortType.DEFAULT,
child: Text('Default'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: PlaylistSortType.REVERSE,
child: Text('Reverse'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: PlaylistSortType.USER,
value: SortType.USER,
child: Text('User'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: PlaylistSortType.TRACK_COUNT,
value: SortType.TRACK_COUNT,
child: Text('Track count'.i18n, style: popupMenuTextStyle()),
),
PopupMenuItem(
value: PlaylistSortType.ALPHABETIC,
value: SortType.ALPHABETIC,
child: Text('Alphabetic'.i18n, style: popupMenuTextStyle()),
),
],

View file

@ -13,6 +13,7 @@ import 'package:freezer/ui/error.dart';
import 'package:freezer/translations.i18n.dart';
import 'package:numberpicker/numberpicker.dart';
import 'package:share/share.dart';
import 'package:url_launcher/url_launcher.dart';
import '../api/definitions.dart';
import 'cached_image.dart';
@ -501,6 +502,35 @@ class MenuSheet {
},
);
//===================
// SHOW/EPISODE
//===================
defaultShowEpisodeMenu(Show s, ShowEpisode e, {List<Widget> options = const []}) {
show([
shareTile('episode', e.id),
shareShow(s.id),
downloadExternalEpisode(e),
...options
]);
}
Widget shareShow(String id) => ListTile(
title: Text('Share show'.i18n),
leading: Icon(Icons.share),
onTap: () async {
Share.share('https://deezer.com/show/$id');
},
);
//Open direct download link in browser
Widget downloadExternalEpisode(ShowEpisode e) => ListTile(
title: Text('Download externally'.i18n),
leading: Icon(Icons.file_download),
onTap: () async {
launch(e.url);
},
);
//===================
// OTHER

View file

@ -1,10 +1,15 @@
import 'dart:convert';
import 'dart:isolate';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:audio_service/audio_service.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/screenutil.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';
import 'package:freezer/settings.dart';
import 'package:freezer/translations.i18n.dart';
@ -25,6 +30,7 @@ import 'player_bar.dart';
import 'dart:ui';
import 'dart:async';
class PlayerScreen extends StatefulWidget {
@override
_PlayerScreenState createState() => _PlayerScreenState();
@ -36,26 +42,34 @@ class _PlayerScreenState extends State<PlayerScreen> {
StreamSubscription _mediaItemSub;
//Calculate background color
Future _calculateColor() async {
Future _updateColor() async {
if (!settings.colorGradientBackground)
return;
//Run in isolate
PaletteGenerator palette = await PaletteGenerator.fromImageProvider(CachedNetworkImageProvider(AudioService.currentMediaItem.extras['thumb'] ?? AudioService.currentMediaItem.artUri));
//Update notification
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: palette.dominantColor.color.withOpacity(0.5)
));
setState(() => _bgGradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [palette.dominantColor.color.withOpacity(0.5), Color.fromARGB(0, 0, 0, 0)],
stops: [
0.0,
0.4
]
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [palette.dominantColor.color.withOpacity(0.5), Color.fromARGB(0, 0, 0, 0)],
stops: [
0.0,
0.4
]
));
}
@override
void initState() {
_calculateColor();
Future.delayed(Duration(milliseconds: 1000), _updateColor);
_mediaItemSub = AudioService.currentMediaItemStream.listen((event) {
_calculateColor();
_updateColor();
});
super.initState();
}
@ -67,6 +81,7 @@ class _PlayerScreenState extends State<PlayerScreen> {
//Fix bottom buttons
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
systemNavigationBarColor: settings.themeData.bottomAppBarColor,
statusBarColor: Colors.transparent
));
super.dispose();
}
@ -214,26 +229,9 @@ class _PlayerScreenHorizontalState extends State<PlayerScreenHorizontal> {
));
},
),
if (AudioService.currentMediaItem.extras['qualityString'] != null)
FlatButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => QualitySettings())
),
child: Text(
AudioService.currentMediaItem.extras['qualityString'] ?? '',
style: TextStyle(fontSize: ScreenUtil().setSp(24)),
),
),
QualityInfoWidget(),
RepeatButton(ScreenUtil().setWidth(32)),
IconButton(
icon: Icon(Icons.more_vert, size: ScreenUtil().setWidth(32)),
onPressed: () {
Track t = Track.fromMediaItem(AudioService.currentMediaItem);
MenuSheet m = MenuSheet(context);
m.defaultTrackMenu(t, options: [m.sleepTimer()]);
},
)
PlayerMenuButton()
],
),
)
@ -333,28 +331,9 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
));
},
),
if (AudioService.currentMediaItem.extras['qualityString'] != null)
FlatButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => QualitySettings())
),
child: Text(
AudioService.currentMediaItem.extras['qualityString'] ?? '',
style: TextStyle(
fontSize: ScreenUtil().setSp(32),
),
),
),
QualityInfoWidget(),
RepeatButton(ScreenUtil().setWidth(46)),
IconButton(
icon: Icon(Icons.more_vert, size: ScreenUtil().setWidth(46)),
onPressed: () {
Track t = Track.fromMediaItem(AudioService.currentMediaItem);
MenuSheet m = MenuSheet(context);
m.defaultTrackMenu(t, options: [m.sleepTimer()]);
},
)
PlayerMenuButton()
],
),
)
@ -363,6 +342,86 @@ class _PlayerScreenVerticalState extends State<PlayerScreenVertical> {
}
}
class QualityInfoWidget extends StatefulWidget {
@override
_QualityInfoWidgetState createState() => _QualityInfoWidgetState();
}
class _QualityInfoWidgetState extends State<QualityInfoWidget> {
String value = '';
StreamSubscription streamSubscription;
//Load data from native
void _load() async {
if (AudioService.currentMediaItem == null) return;
Map data = await DownloadManager.platform.invokeMethod("getStreamInfo", {"id": AudioService.currentMediaItem.id});
//N/A
if (data == null) {
setState(() => value = '');
//If not show, try again later
if (AudioService.currentMediaItem.extras['show'] == null)
Future.delayed(Duration(milliseconds: 200), _load);
return;
}
//Update
StreamQualityInfo info = StreamQualityInfo.fromJson(data);
setState(() {
value = '${info.format} ${info.bitrate(AudioService.currentMediaItem.duration)}kbps';
});
}
@override
void initState() {
_load();
if (streamSubscription == null)
streamSubscription = AudioService.currentMediaItemStream.listen((event) async {
await _load();
});
super.initState();
}
@override
void dispose() {
if (streamSubscription != null)
streamSubscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FlatButton(
child: Text(value),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => QualitySettings()));
},
);
}
}
class PlayerMenuButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(Icons.more_vert, size: ScreenUtil().setWidth(46)),
onPressed: () {
Track t = Track.fromMediaItem(AudioService.currentMediaItem);
MenuSheet m = MenuSheet(context);
if (AudioService.currentMediaItem.extras['show'] == null)
m.defaultTrackMenu(t, options: [m.sleepTimer()]);
else
m.defaultShowEpisodeMenu(
Show.fromJson(jsonDecode(AudioService.currentMediaItem.extras['show'])),
ShowEpisode.fromMediaItem(AudioService.currentMediaItem),
options: [m.sleepTimer()]
);
},
);
}
}
class RepeatButton extends StatefulWidget {

View file

@ -1,11 +1,15 @@
import 'package:connectivity/connectivity.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:fluttericon/font_awesome5_icons.dart';
import 'package:fluttericon/typicons_icons.dart';
import 'package:flutter/src/services/keyboard_key.dart';
import 'package:freezer/api/cache.dart';
import 'package:freezer/api/download.dart';
import 'package:freezer/api/player.dart';
import 'package:freezer/ui/details_screens.dart';
import 'package:freezer/ui/elements.dart';
import 'package:freezer/ui/home_screen.dart';
import 'package:freezer/ui/menu.dart';
import 'package:freezer/translations.i18n.dart';
@ -53,6 +57,8 @@ class _SearchScreenState extends State<SearchScreen> {
TextEditingController _controller = new TextEditingController();
List _suggestions = [];
bool _cancel = false;
bool _showCards = true;
FocusNode _focus = FocusNode();
void _submit(BuildContext context, {String query}) async {
if (query != null) _query = query;
@ -152,6 +158,9 @@ class _SearchScreenState extends State<SearchScreen> {
setState(() => _query = s);
_loadSuggestions();
},
onTap: () {
setState(() => _showCards = false);
},
focusNode: textFielFocusNode,
decoration: InputDecoration(
labelText: 'Search or paste URL'.i18n,
@ -212,8 +221,87 @@ class _SearchScreenState extends State<SearchScreen> {
LinearProgressIndicator(),
FreezerDivider(),
//"Browse" Cards
if (_showCards)
...[
Padding(
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: Text(
'Quick access',
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
SearchBrowseCard(
color: Color(0xff11b192),
text: 'Flow'.i18n,
icon: Icon(Typicons.waves),
onTap: () async {
await playerHelper.playFromSmartTrackList(SmartTrackList(id: 'flow'));
},
),
SearchBrowseCard(
color: Color(0xff7c42bb),
text: 'Shows'.i18n,
icon: Icon(FontAwesome5.podcast),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => Scaffold(
appBar: FreezerAppBar('Shows'.i18n),
body: SingleChildScrollView(
child: HomePageScreen(
channel: DeezerChannel(target: 'shows')
)
),
),
)),
)
],
),
Container(height: 4.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
SearchBrowseCard(
color: Color(0xffff555d),
icon: Icon(FontAwesome5.chart_line),
text: 'Charts'.i18n,
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => Scaffold(
appBar: FreezerAppBar('Charts'.i18n),
body: SingleChildScrollView(
child: HomePageScreen(
channel: DeezerChannel(target: 'channels/charts')
)
),
),
)),
),
SearchBrowseCard(
color: Color(0xff2c4ea7),
text: 'Browse'.i18n,
icon: Image.asset('assets/browse_icon.png', width: 26.0),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => Scaffold(
appBar: FreezerAppBar('Browse'.i18n),
body: SingleChildScrollView(
child: HomePageScreen(
channel: DeezerChannel(target: 'channels/explore')
)
),
),
)),
)
],
)
],
//History
if (cache.searchHistory != null && cache.searchHistory.length > 0 && (_query??'').length < 2)
if (!_showCards && cache.searchHistory != null && cache.searchHistory.length > 0 && (_query??'').length < 2)
...List.generate(cache.searchHistory.length > 10 ? 10 : cache.searchHistory.length, (int i) {
dynamic data = cache.searchHistory[i].data;
switch (cache.searchHistory[i].type) {
@ -308,6 +396,50 @@ class _SearchScreenState extends State<SearchScreen> {
}
}
class SearchBrowseCard extends StatelessWidget {
final Color color;
final Widget icon;
final Function onTap;
final String text;
SearchBrowseCard({@required this.color, @required this.onTap, @required this.text, this.icon});
@override
Widget build(BuildContext context) {
return Card(
color: color,
child: InkWell(
onTap: this.onTap,
child: Container(
width: MediaQuery.of(context).size.width / 2 - 32,
height: 75,
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null)
icon,
if (icon != null)
Container(width: 8.0),
Text(
text,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
color: (color.computeLuminance() > 0.5) ? Colors.black:Colors.white
),
),
],
)
),
),
)
);
}
}
class SearchResultsScreen extends StatelessWidget {

View file

@ -46,6 +46,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
'name': 'Filipino',
'isoCode': 'fil'
});
defaultLanguagesList.add({
'name': 'Furry',
'isoCode': 'uwu'
});
List<Map<String, String>> _l = supportedLocales.map<Map<String, String>>((l) {
Map _lang = defaultLanguagesList.firstWhere((lang) => lang['isoCode'] == l.languageCode);
return {
@ -445,7 +449,7 @@ class _QualityPickerState extends State<QualityPicker> {
),
if (widget.field == 'download')
ListTile(
title: Text('Ask before downloading'),
title: Text('Ask before downloading'.i18n),
leading: Radio(
groupValue: _quality,
value: AudioQuality.ASK,
@ -947,8 +951,8 @@ class _GeneralSettingsState extends State<GeneralSettings> {
),
FlatButton(
child: Text('(ARL ONLY) Continue'.i18n),
onPressed: () {
logOut();
onPressed: () async {
await logOut();
Navigator.of(context).pop();
},
),
@ -1073,6 +1077,11 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
super.initState();
}
Future _resetPath() async {
StorageInfo si = (await PathProviderEx.getStorageInfo())[0];
setState(() => _path = si.appFilesDir);
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -1147,7 +1156,13 @@ class _DirectoryPickerState extends State<DirectoryPicker> {
builder: (BuildContext context, AsyncSnapshot snapshot) {
//On error go to last good path
if (snapshot.hasError) Future.delayed(Duration(milliseconds: 50), () => setState(() => _path = _previous));
if (snapshot.hasError) Future.delayed(Duration(milliseconds: 50), () {
if (_previous == null) {
_resetPath();
return;
}
setState(() => _path = _previous);
});
if (!snapshot.hasData) return Center(child: CircularProgressIndicator(),);
List<FileSystemEntity> data = snapshot.data;
@ -1266,12 +1281,20 @@ class _CreditsScreenState extends State<CreditsScreen> {
launch('https://t.me/freezerandroid');
},
),
ListTile(
title: Text('Discord'.i18n),
subtitle: Text('Official Discord server'.i18n),
leading: Icon(FontAwesome5.discord, color: Color(0xff7289da), size: 36.0),
onTap: () {
launch('https://discord.gg/7ap654Tp3z');
},
),
ListTile(
title: Text('Repository'.i18n),
subtitle: Text('Source code, report issues there.'.i18n),
leading: Icon(Icons.code, color: Colors.green, size: 36.0),
onTap: () {
launch('https://notabug.org/exttex/freezer');
launch('https://git.rip/freezer/');
},
),
FreezerDivider(),

View file

@ -318,11 +318,12 @@ class SmartTrackListTile extends StatelessWidget {
fontSize: 18.0,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 2,
color: Colors.black
offset: Offset(1, 1),
blurRadius: 2,
color: Colors.black
)
]
],
color: Colors.white
),
),
),
@ -450,3 +451,104 @@ class ChannelTile extends StatelessWidget {
);
}
}
class ShowCard extends StatelessWidget {
final Show show;
final Function onTap;
final Function onHold;
ShowCard(this.show, {this.onTap, this.onHold});
@override
Widget build(BuildContext context) {
return Container(
child: InkWell(
onTap: onTap,
onLongPress: onHold,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.all(8.0),
child: CachedImage(
url: show.art.thumb,
width: 128.0,
height: 128.0,
rounded: true,
),
),
Container(
width: 144.0,
child: Text(
show.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.0
),
),
),
],
),
),
);
}
}
class ShowEpisodeTile extends StatelessWidget {
final ShowEpisode episode;
final Function onTap;
final Function onHold;
final Widget trailing;
ShowEpisodeTile(this.episode, {this.onTap, this.onHold, this.trailing});
@override
Widget build(BuildContext context) {
return InkWell(
onLongPress: onHold,
onTap: onTap,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(episode.title, maxLines: 2),
trailing: trailing,
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
episode.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Theme.of(context).textTheme.subtitle1.color.withOpacity(0.9)
),
),
),
Padding(
padding: EdgeInsets.fromLTRB(16, 8.0, 0, 0),
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
Text(
'${episode.publishedDate} | ${episode.durationString}',
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.subtitle1.color.withOpacity(0.6)
),
),
],
),
),
Divider(),
],
),
);
}
}

View file

@ -15,6 +15,8 @@ import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'dart:convert';
import 'package:version/version.dart';
class UpdaterScreen extends StatefulWidget {
@override
@ -109,12 +111,13 @@ class _UpdaterScreenState extends State<UpdaterScreen> {
),
),
if (!_error && !_loading && _versions.latest == _current)
if (!_error && !_loading && Version.parse(_versions.latest) <= Version.parse(_current))
Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'You are running latest version!'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 26.0
)
@ -122,17 +125,20 @@ class _UpdaterScreenState extends State<UpdaterScreen> {
)
),
if (!_error && !_loading && _versions.latest != _current)
if (!_error && !_loading && Version.parse(_versions.latest) > Version.parse(_current))
Column(
children: [
Text(
'New update available!'.i18n + ' ' + _versions.latest,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'New update available!'.i18n + ' ' + _versions.latest,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold
),
),
),
Container(height: 8.0),
Text(
'Current version: ' + _current,
style: TextStyle(
@ -204,6 +210,7 @@ class FreezerVersions {
//Fetch from website API
static Future<FreezerVersions> fetch() async {
http.Response response = await http.get('https://freezer.life/api/versions');
// http.Response response = await http.get('https://cum.freezerapp.workers.dev/api/versions');
return FreezerVersions.fromJson(jsonDecode(response.body));
}
@ -218,7 +225,7 @@ class FreezerVersions {
//Load current version
PackageInfo info = await PackageInfo.fromPlatform();
if (info.version == versions.latest) return;
if (Version.parse(versions.latest) <= Version.parse(info.version)) return;
//Get architecture
String _arch = await DownloadManager.platform.invokeMethod("arch");