0.6.5 - Local streaming http server
This commit is contained in:
parent
28c2de55fb
commit
21e7f55017
31 changed files with 1744 additions and 460 deletions
|
@ -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);
|
||||
},
|
||||
);
|
||||
})
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -291,6 +291,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);
|
||||
}
|
||||
|
|
|
@ -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()),
|
||||
),
|
||||
],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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), Theme.of(context).bottomAppBarColor],
|
||||
stops: [
|
||||
0.0,
|
||||
0.4
|
||||
]
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [palette.dominantColor.color.withOpacity(0.5), Theme.of(context).bottomAppBarColor],
|
||||
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 {
|
||||
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
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: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';
|
||||
|
||||
|
@ -52,6 +56,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;
|
||||
|
@ -139,6 +145,10 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||
setState(() => _query = s);
|
||||
_loadSuggestions();
|
||||
},
|
||||
onTap: () {
|
||||
setState(() => _showCards = false);
|
||||
},
|
||||
focusNode: _focus,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Search or paste URL'.i18n,
|
||||
fillColor: Theme.of(context).bottomAppBarColor,
|
||||
|
@ -195,8 +205,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) {
|
||||
|
@ -290,6 +379,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 {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -321,11 +321,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
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -454,3 +455,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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue