Split discography, copy arl button, small fixes

This commit is contained in:
exttex 2020-09-03 21:17:53 +02:00
parent 4e5e3a3059
commit b9004c3004
7 changed files with 227 additions and 102 deletions

View File

@ -31,10 +31,13 @@ class Track {
Lyrics lyrics; Lyrics lyrics;
bool favorite; bool favorite;
//TODO: Not in DB
int diskNumber;
List<dynamic> playbackDetails; List<dynamic> playbackDetails;
Track({this.id, this.title, this.duration, this.album, this.playbackDetails, this.albumArt, Track({this.id, this.title, this.duration, this.album, this.playbackDetails, this.albumArt,
this.artists, this.trackNumber, this.offline, this.lyrics, this.favorite}); this.artists, this.trackNumber, this.offline, this.lyrics, this.favorite, this.diskNumber});
String get artistString => artists.map<String>((art) => art.name).join(', '); String get artistString => artists.map<String>((art) => art.name).join(', ');
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')}";
@ -130,7 +133,8 @@ class Track {
trackNumber: int.parse((json['TRACK_NUMBER']??'0').toString()), trackNumber: int.parse((json['TRACK_NUMBER']??'0').toString()),
playbackDetails: [json['MD5_ORIGIN'], json['MEDIA_VERSION']], playbackDetails: [json['MD5_ORIGIN'], json['MEDIA_VERSION']],
lyrics: Lyrics(id: json['LYRICS_ID'].toString()), lyrics: Lyrics(id: json['LYRICS_ID'].toString()),
favorite: favorite favorite: favorite,
diskNumber: int.parse(json['DISK_NUMBER']??'1')
); );
} }
Map<String, dynamic> toSQL({off = false}) => { Map<String, dynamic> toSQL({off = false}) => {
@ -164,6 +168,12 @@ class Track {
Map<String, dynamic> toJson() => _$TrackToJson(this); Map<String, dynamic> toJson() => _$TrackToJson(this);
} }
enum AlbumType {
ALBUM,
SINGLE,
FEATURED
}
@JsonSerializable() @JsonSerializable()
class Album { class Album {
String id; String id;
@ -174,8 +184,10 @@ class Album {
int fans; int fans;
bool offline; //If the album is offline, or just saved in db as metadata bool offline; //If the album is offline, or just saved in db as metadata
bool library; bool library;
//TODO: Not in DB
AlbumType type;
Album({this.id, this.title, this.art, this.artists, this.tracks, this.fans, this.offline, this.library}); Album({this.id, this.title, this.art, this.artists, this.tracks, this.fans, this.offline, this.library, this.type});
String get artistString => artists.map<String>((art) => art.name).join(', '); String get artistString => artists.map<String>((art) => art.name).join(', ');
Duration get duration => Duration(seconds: tracks.fold(0, (v, t) => v += t.duration.inSeconds)); Duration get duration => Duration(seconds: tracks.fold(0, (v, t) => v += t.duration.inSeconds));
@ -183,15 +195,22 @@ class Album {
String get fansString => NumberFormat.compact().format(fans); String get fansString => NumberFormat.compact().format(fans);
//JSON //JSON
factory Album.fromPrivateJson(Map<dynamic, dynamic> json, {Map<dynamic, dynamic> songsJson = const {}, bool library = false}) => Album( factory Album.fromPrivateJson(Map<dynamic, dynamic> json, {Map<dynamic, dynamic> songsJson = const {}, bool library = false}) {
AlbumType type = AlbumType.ALBUM;
if (json['TYPE'] != null && json['TYPE'].toString() == "0") type = AlbumType.SINGLE;
if (json['ROLE_ID'] == 5) type = AlbumType.FEATURED;
return Album(
id: json['ALB_ID'].toString(), id: json['ALB_ID'].toString(),
title: json['ALB_TITLE'], title: json['ALB_TITLE'],
art: ImageDetails.fromPrivateString(json['ALB_PICTURE']), art: ImageDetails.fromPrivateString(json['ALB_PICTURE']),
artists: (json['ARTISTS']??[json]).map<Artist>((dynamic art) => Artist.fromPrivateJson(art)).toList(), artists: (json['ARTISTS']??[json]).map<Artist>((dynamic art) => Artist.fromPrivateJson(art)).toList(),
tracks: (songsJson['data']??[]).map<Track>((dynamic track) => Track.fromPrivateJson(track)).toList(), tracks: (songsJson['data']??[]).map<Track>((dynamic track) => Track.fromPrivateJson(track)).toList(),
fans: json['NB_FAN'], fans: json['NB_FAN'],
library: library library: library,
type: type
); );
}
Map<String, dynamic> toSQL({off = false}) => { Map<String, dynamic> toSQL({off = false}) => {
'id': id, 'id': id,
'title': title, 'title': title,

View File

@ -30,6 +30,7 @@ Track _$TrackFromJson(Map<String, dynamic> json) {
? null ? null
: Lyrics.fromJson(json['lyrics'] as Map<String, dynamic>), : Lyrics.fromJson(json['lyrics'] as Map<String, dynamic>),
favorite: json['favorite'] as bool, favorite: json['favorite'] as bool,
diskNumber: json['diskNumber'] as int,
); );
} }
@ -44,6 +45,7 @@ Map<String, dynamic> _$TrackToJson(Track instance) => <String, dynamic>{
'offline': instance.offline, 'offline': instance.offline,
'lyrics': instance.lyrics, 'lyrics': instance.lyrics,
'favorite': instance.favorite, 'favorite': instance.favorite,
'diskNumber': instance.diskNumber,
'playbackDetails': instance.playbackDetails, 'playbackDetails': instance.playbackDetails,
}; };
@ -65,6 +67,7 @@ Album _$AlbumFromJson(Map<String, dynamic> json) {
fans: json['fans'] as int, fans: json['fans'] as int,
offline: json['offline'] as bool, offline: json['offline'] as bool,
library: json['library'] as bool, library: json['library'] as bool,
type: _$enumDecodeNullable(_$AlbumTypeEnumMap, json['type']),
); );
} }
@ -77,8 +80,47 @@ Map<String, dynamic> _$AlbumToJson(Album instance) => <String, dynamic>{
'fans': instance.fans, 'fans': instance.fans,
'offline': instance.offline, 'offline': instance.offline,
'library': instance.library, 'library': instance.library,
'type': _$AlbumTypeEnumMap[instance.type],
}; };
T _$enumDecode<T>(
Map<T, dynamic> enumValues,
dynamic source, {
T unknownValue,
}) {
if (source == null) {
throw ArgumentError('A value must be provided. Supported values: '
'${enumValues.values.join(', ')}');
}
final value = enumValues.entries
.singleWhere((e) => e.value == source, orElse: () => null)
?.key;
if (value == null && unknownValue == null) {
throw ArgumentError('`$source` is not one of the supported values: '
'${enumValues.values.join(', ')}');
}
return value ?? unknownValue;
}
T _$enumDecodeNullable<T>(
Map<T, dynamic> enumValues,
dynamic source, {
T unknownValue,
}) {
if (source == null) {
return null;
}
return _$enumDecode<T>(enumValues, source, unknownValue: unknownValue);
}
const _$AlbumTypeEnumMap = {
AlbumType.ALBUM: 'ALBUM',
AlbumType.SINGLE: 'SINGLE',
AlbumType.FEATURED: 'FEATURED',
};
Artist _$ArtistFromJson(Map<String, dynamic> json) { Artist _$ArtistFromJson(Map<String, dynamic> json) {
return Artist( return Artist(
id: json['id'] as String, id: json['id'] as String,
@ -287,38 +329,6 @@ Map<String, dynamic> _$HomePageSectionToJson(HomePageSection instance) =>
'items': HomePageSection._homePageItemToJson(instance.items), 'items': HomePageSection._homePageItemToJson(instance.items),
}; };
T _$enumDecode<T>(
Map<T, dynamic> enumValues,
dynamic source, {
T unknownValue,
}) {
if (source == null) {
throw ArgumentError('A value must be provided. Supported values: '
'${enumValues.values.join(', ')}');
}
final value = enumValues.entries
.singleWhere((e) => e.value == source, orElse: () => null)
?.key;
if (value == null && unknownValue == null) {
throw ArgumentError('`$source` is not one of the supported values: '
'${enumValues.values.join(', ')}');
}
return value ?? unknownValue;
}
T _$enumDecodeNullable<T>(
Map<T, dynamic> enumValues,
dynamic source, {
T unknownValue,
}) {
if (source == null) {
return null;
}
return _$enumDecode<T>(enumValues, source, unknownValue: unknownValue);
}
const _$HomePageSectionLayoutEnumMap = { const _$HomePageSectionLayoutEnumMap = {
HomePageSectionLayout.ROW: 'ROW', HomePageSectionLayout.ROW: 'ROW',
}; };

View File

@ -511,7 +511,7 @@ class Download {
if (settings.albumFolder) { if (settings.albumFolder) {
String folderName = track.album.title.replaceAll(sanitize, ''); String folderName = track.album.title.replaceAll(sanitize, '');
//Add disk number //Add disk number
if (settings.albumDiscFolder) folderName += ' - Disk ${rawTrack["DISK_NUMBER"]}'; if (settings.albumDiscFolder) folderName += ' - Disk ${track.diskNumber}';
this.path = p.join(this.path, folderName); this.path = p.join(this.path, folderName);
} }

View File

@ -26,6 +26,15 @@ class AlbumDetails extends StatelessWidget {
} }
} }
//Get count of CDs in album
int get cdCount {
int c = 1;
for (Track t in album.tracks) {
if (t.diskNumber > c) c = t.diskNumber;
}
return c;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -153,19 +162,27 @@ class AlbumDetails extends StatelessWidget {
], ],
), ),
), ),
...List.generate(album.tracks.length, (i) { ...List.generate(cdCount, (cdi) {
Track t = album.tracks[i]; List<Track> tracks = album.tracks.where((t) => t.diskNumber == cdi + 1).toList();
return TrackTile( return Column(
t, children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 4.0),
child: Text('Disk ${cdi + 1}'),
),
...List.generate(tracks.length, (i) => TrackTile(
tracks[i],
onTap: () { onTap: () {
playerHelper.playFromAlbum(album, t.id); playerHelper.playFromAlbum(album, tracks[i].id);
}, },
onHold: () { onHold: () {
MenuSheet m = MenuSheet(context); MenuSheet m = MenuSheet(context);
m.defaultTrackMenu(t); m.defaultTrackMenu(tracks[i]);
} }
))
],
); );
}) }),
], ],
); );
}, },
@ -433,7 +450,7 @@ class ArtistDetails extends StatelessWidget {
class DiscographyScreen extends StatefulWidget { class DiscographyScreen extends StatefulWidget {
Artist artist; final Artist artist;
DiscographyScreen({@required this.artist, Key key}): super(key: key); DiscographyScreen({@required this.artist, Key key}): super(key: key);
@override @override
@ -445,7 +462,11 @@ class _DiscographyScreenState extends State<DiscographyScreen> {
Artist artist; Artist artist;
bool _loading = false; bool _loading = false;
bool _error = false; bool _error = false;
ScrollController _scrollController = ScrollController(); List<ScrollController> _controllers = [
ScrollController(),
ScrollController(),
ScrollController()
];
Future _load() async { Future _load() async {
if (artist.albums.length >= artist.albumCount || _loading) return; if (artist.albums.length >= artist.albumCount || _loading) return;
@ -471,35 +492,24 @@ class _DiscographyScreenState extends State<DiscographyScreen> {
} }
@override //Get album tile
void initState() { Widget _tile(Album a) => AlbumTile(
artist = widget.artist; a,
onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => AlbumDetails(a))),
onHold: () {
MenuSheet m = MenuSheet(context);
m.defaultAlbumMenu(a);
},
);
//Lazy loading scroll Widget get _loadingWidget {
_scrollController.addListener(() {
double off = _scrollController.position.maxScrollExtent * 0.90;
if (_scrollController.position.pixels > off) {
_load();
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Discography'),),
body: ListView.builder(
controller: _scrollController,
itemCount: artist.albums.length + 1,
itemBuilder: (context, i) {
//Loading
if (i == artist.albums.length) {
if (_loading) if (_loading)
return Row( return Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [CircularProgressIndicator()], children: [CircularProgressIndicator()],
),
); );
//Error //Error
if (_error) if (_error)
@ -508,21 +518,85 @@ class _DiscographyScreenState extends State<DiscographyScreen> {
return Container(width: 0, height: 0,); return Container(width: 0, height: 0,);
} }
Album a = artist.albums[i]; @override
return AlbumTile( void initState() {
a, artist = widget.artist;
onTap: () {
Navigator.of(context).push( //Lazy loading scroll
MaterialPageRoute(builder: (context) => AlbumDetails(a)) _controllers.forEach((_c) {
); _c.addListener(() {
}, double off = _c.position.maxScrollExtent * 0.85;
onHold: () { if (_c.position.pixels > off) _load();
MenuSheet m = MenuSheet(context); });
m.defaultAlbumMenu(a); });
},
); super.initState();
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Builder(builder: (BuildContext context) {
final TabController tabController = DefaultTabController.of(context);
tabController.addListener(() {
if (!tabController.indexIsChanging) {
//Load data if empty tabs
int nSingles = artist.albums.where((a) => a.type == AlbumType.SINGLE).length;
int nFeatures = artist.albums.where((a) => a.type == AlbumType.FEATURED).length;
if ((nSingles == 0 || nFeatures == 0) && !_loading) _load();
}
});
return Scaffold(
appBar: AppBar(
title: Text('Discography'),
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.album)),
Tab(icon: Icon(Icons.audiotrack)),
Tab(icon: Icon(Icons.recent_actors))
],
),
),
body: TabBarView(
children: [
//Albums
ListView.builder(
controller: _controllers[0],
itemCount: artist.albums.length + 1,
itemBuilder: (context, i) {
if (i == artist.albums.length) return _loadingWidget;
if (artist.albums[i].type == AlbumType.ALBUM) return _tile(artist.albums[i]);
return Container(width: 0, height: 0,);
}, },
), ),
//Singles
ListView.builder(
controller: _controllers[1],
itemCount: artist.albums.length + 1,
itemBuilder: (context, i) {
if (i == artist.albums.length) return _loadingWidget;
if (artist.albums[i].type == AlbumType.SINGLE) return _tile(artist.albums[i]);
return Container(width: 0, height: 0,);
},
),
//Featured
ListView.builder(
controller: _controllers[2],
itemCount: artist.albums.length + 1,
itemBuilder: (context, i) {
if (i == artist.albums.length) return _loadingWidget;
if (artist.albums[i].type == AlbumType.FEATURED) return _tile(artist.albums[i]);
return Container(width: 0, height: 0,);
},
),
],
),
);
})
); );
} }
} }

View File

@ -14,6 +14,7 @@ import 'package:language_pickers/languages.dart';
import 'package:package_info/package_info.dart'; import 'package:package_info/package_info.dart';
import 'package:path_provider_ex/path_provider_ex.dart'; import 'package:path_provider_ex/path_provider_ex.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:clipboard/clipboard.dart';
import '../settings.dart'; import '../settings.dart';
import '../main.dart'; import '../main.dart';
@ -34,7 +35,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
//Load about text //Load about text
PackageInfo.fromPlatform().then((PackageInfo info) { PackageInfo.fromPlatform().then((PackageInfo info) {
setState(() { setState(() {
_about = '${info.appName} ${info.version}'; _about = '${info.appName}';
}); });
}); });
super.initState(); super.initState();
@ -566,6 +567,17 @@ class _GeneralSettingsState extends State<GeneralSettings> {
}, },
), ),
), ),
ListTile(
title: Text('Copy ARL'),
subtitle: Text('Copy userToken/ARL Cookie for use in other apps.'),
leading: Icon(Icons.lock),
onTap: () async {
await FlutterClipboard.copy(settings.arl);
await Fluttertoast.showToast(
msg: 'Copied',
);
},
),
ListTile( ListTile(
title: Text('Log out', style: TextStyle(color: Colors.red),), title: Text('Log out', style: TextStyle(color: Colors.red),),
leading: Icon(Icons.exit_to_app), leading: Icon(Icons.exit_to_app),

View File

@ -62,7 +62,16 @@ class _TrackTileState extends State<TrackTile> {
), ),
onTap: widget.onTap, onTap: widget.onTap,
onLongPress: widget.onHold, onLongPress: widget.onHold,
trailing: widget.trailing, trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 2.0),
child: Text(widget.track.durationString),
),
widget.trailing??Container(width: 0, height: 0)
],
),
); );
} }
} }
@ -120,7 +129,7 @@ class ArtistTile extends StatelessWidget {
CachedImage( CachedImage(
url: artist.picture.thumb, url: artist.picture.thumb,
circular: true, circular: true,
width: 64, width: 100,
), ),
Container(height: 4,), Container(height: 4,),
Text( Text(

View File

@ -39,7 +39,7 @@ dependencies:
connectivity: ^0.4.8+6 connectivity: ^0.4.8+6
intl: ^0.16.1 intl: ^0.16.1
filesize: ^1.0.4 filesize: ^1.0.4
fluttertoast: ^7.0.2 fluttertoast: ^7.0.4
palette_generator: ^0.2.3 palette_generator: ^0.2.3
flutter_material_color_picker: ^1.0.5 flutter_material_color_picker: ^1.0.5
flutter_inappwebview: ^4.0.0 flutter_inappwebview: ^4.0.0
@ -59,6 +59,7 @@ dependencies:
marquee: ^1.5.2 marquee: ^1.5.2
flutter_cache_manager: ^1.4.1 flutter_cache_manager: ^1.4.1
cached_network_image: ^2.2.0+1 cached_network_image: ^2.2.0+1
clipboard: ^0.1.2+8
audio_service: ^0.13.0 audio_service: ^0.13.0
just_audio: just_audio: