diff --git a/lib/api/definitions.dart b/lib/api/definitions.dart index bc7b7f6..d30ed53 100644 --- a/lib/api/definitions.dart +++ b/lib/api/definitions.dart @@ -31,10 +31,13 @@ class Track { Lyrics lyrics; bool favorite; + //TODO: Not in DB + int diskNumber; + List playbackDetails; 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((art) => art.name).join(', '); 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()), playbackDetails: [json['MD5_ORIGIN'], json['MEDIA_VERSION']], lyrics: Lyrics(id: json['LYRICS_ID'].toString()), - favorite: favorite + favorite: favorite, + diskNumber: int.parse(json['DISK_NUMBER']??'1') ); } Map toSQL({off = false}) => { @@ -164,6 +168,12 @@ class Track { Map toJson() => _$TrackToJson(this); } +enum AlbumType { + ALBUM, + SINGLE, + FEATURED +} + @JsonSerializable() class Album { String id; @@ -174,8 +184,10 @@ class Album { int fans; bool offline; //If the album is offline, or just saved in db as metadata 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((art) => art.name).join(', '); 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); //JSON - factory Album.fromPrivateJson(Map json, {Map songsJson = const {}, bool library = false}) => Album( - id: json['ALB_ID'].toString(), - title: json['ALB_TITLE'], - art: ImageDetails.fromPrivateString(json['ALB_PICTURE']), - artists: (json['ARTISTS']??[json]).map((dynamic art) => Artist.fromPrivateJson(art)).toList(), - tracks: (songsJson['data']??[]).map((dynamic track) => Track.fromPrivateJson(track)).toList(), - fans: json['NB_FAN'], - library: library - ); + factory Album.fromPrivateJson(Map json, {Map 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(), + title: json['ALB_TITLE'], + art: ImageDetails.fromPrivateString(json['ALB_PICTURE']), + artists: (json['ARTISTS']??[json]).map((dynamic art) => Artist.fromPrivateJson(art)).toList(), + tracks: (songsJson['data']??[]).map((dynamic track) => Track.fromPrivateJson(track)).toList(), + fans: json['NB_FAN'], + library: library, + type: type + ); + } Map toSQL({off = false}) => { 'id': id, 'title': title, diff --git a/lib/api/definitions.g.dart b/lib/api/definitions.g.dart index 3d36a2d..cbd6ea9 100644 --- a/lib/api/definitions.g.dart +++ b/lib/api/definitions.g.dart @@ -30,6 +30,7 @@ Track _$TrackFromJson(Map json) { ? null : Lyrics.fromJson(json['lyrics'] as Map), favorite: json['favorite'] as bool, + diskNumber: json['diskNumber'] as int, ); } @@ -44,6 +45,7 @@ Map _$TrackToJson(Track instance) => { 'offline': instance.offline, 'lyrics': instance.lyrics, 'favorite': instance.favorite, + 'diskNumber': instance.diskNumber, 'playbackDetails': instance.playbackDetails, }; @@ -65,6 +67,7 @@ Album _$AlbumFromJson(Map json) { fans: json['fans'] as int, offline: json['offline'] as bool, library: json['library'] as bool, + type: _$enumDecodeNullable(_$AlbumTypeEnumMap, json['type']), ); } @@ -77,8 +80,47 @@ Map _$AlbumToJson(Album instance) => { 'fans': instance.fans, 'offline': instance.offline, 'library': instance.library, + 'type': _$AlbumTypeEnumMap[instance.type], }; +T _$enumDecode( + Map 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( + Map enumValues, + dynamic source, { + T unknownValue, +}) { + if (source == null) { + return null; + } + return _$enumDecode(enumValues, source, unknownValue: unknownValue); +} + +const _$AlbumTypeEnumMap = { + AlbumType.ALBUM: 'ALBUM', + AlbumType.SINGLE: 'SINGLE', + AlbumType.FEATURED: 'FEATURED', +}; + Artist _$ArtistFromJson(Map json) { return Artist( id: json['id'] as String, @@ -287,38 +329,6 @@ Map _$HomePageSectionToJson(HomePageSection instance) => 'items': HomePageSection._homePageItemToJson(instance.items), }; -T _$enumDecode( - Map 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( - Map enumValues, - dynamic source, { - T unknownValue, -}) { - if (source == null) { - return null; - } - return _$enumDecode(enumValues, source, unknownValue: unknownValue); -} - const _$HomePageSectionLayoutEnumMap = { HomePageSectionLayout.ROW: 'ROW', }; diff --git a/lib/api/download.dart b/lib/api/download.dart index 68f0aa5..506dc27 100644 --- a/lib/api/download.dart +++ b/lib/api/download.dart @@ -511,7 +511,7 @@ class Download { if (settings.albumFolder) { String folderName = track.album.title.replaceAll(sanitize, ''); //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); } diff --git a/lib/ui/details_screens.dart b/lib/ui/details_screens.dart index 0c16fda..fb72bb1 100644 --- a/lib/ui/details_screens.dart +++ b/lib/ui/details_screens.dart @@ -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 Widget build(BuildContext context) { return Scaffold( @@ -153,19 +162,27 @@ class AlbumDetails extends StatelessWidget { ], ), ), - ...List.generate(album.tracks.length, (i) { - Track t = album.tracks[i]; - return TrackTile( - t, - onTap: () { - playerHelper.playFromAlbum(album, t.id); - }, - onHold: () { - MenuSheet m = MenuSheet(context); - m.defaultTrackMenu(t); - } + ...List.generate(cdCount, (cdi) { + List tracks = album.tracks.where((t) => t.diskNumber == cdi + 1).toList(); + return Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 4.0), + child: Text('Disk ${cdi + 1}'), + ), + ...List.generate(tracks.length, (i) => TrackTile( + tracks[i], + onTap: () { + playerHelper.playFromAlbum(album, tracks[i].id); + }, + onHold: () { + MenuSheet m = MenuSheet(context); + m.defaultTrackMenu(tracks[i]); + } + )) + ], ); - }) + }), ], ); }, @@ -433,7 +450,7 @@ class ArtistDetails extends StatelessWidget { class DiscographyScreen extends StatefulWidget { - Artist artist; + final Artist artist; DiscographyScreen({@required this.artist, Key key}): super(key: key); @override @@ -445,7 +462,11 @@ class _DiscographyScreenState extends State { Artist artist; bool _loading = false; bool _error = false; - ScrollController _scrollController = ScrollController(); + List _controllers = [ + ScrollController(), + ScrollController(), + ScrollController() + ]; Future _load() async { if (artist.albums.length >= artist.albumCount || _loading) return; @@ -471,16 +492,42 @@ class _DiscographyScreenState extends State { } + //Get album tile + Widget _tile(Album a) => AlbumTile( + a, + onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => AlbumDetails(a))), + onHold: () { + MenuSheet m = MenuSheet(context); + m.defaultAlbumMenu(a); + }, + ); + + Widget get _loadingWidget { + if (_loading) + return Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [CircularProgressIndicator()], + ), + ); + //Error + if (_error) + return ErrorScreen(); + //Success + return Container(width: 0, height: 0,); + } + @override void initState() { artist = widget.artist; //Lazy loading scroll - _scrollController.addListener(() { - double off = _scrollController.position.maxScrollExtent * 0.90; - if (_scrollController.position.pixels > off) { - _load(); - } + _controllers.forEach((_c) { + _c.addListener(() { + double off = _c.position.maxScrollExtent * 0.85; + if (_c.position.pixels > off) _load(); + }); }); super.initState(); @@ -488,41 +535,68 @@ class _DiscographyScreenState extends State { @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) - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [CircularProgressIndicator()], - ); - //Error - if (_error) - return ErrorScreen(); - //Success - return Container(width: 0, height: 0,); - } - Album a = artist.albums[i]; - return AlbumTile( - a, - onTap: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => AlbumDetails(a)) - ); - }, - onHold: () { - MenuSheet m = MenuSheet(context); - m.defaultAlbumMenu(a); - }, - ); - }, - ), + 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,); + }, + ), + ], + ), + ); + }) ); } } diff --git a/lib/ui/settings_screen.dart b/lib/ui/settings_screen.dart index 8c6d706..ded1e00 100644 --- a/lib/ui/settings_screen.dart +++ b/lib/ui/settings_screen.dart @@ -14,6 +14,7 @@ import 'package:language_pickers/languages.dart'; import 'package:package_info/package_info.dart'; import 'package:path_provider_ex/path_provider_ex.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:clipboard/clipboard.dart'; import '../settings.dart'; import '../main.dart'; @@ -34,7 +35,7 @@ class _SettingsScreenState extends State { //Load about text PackageInfo.fromPlatform().then((PackageInfo info) { setState(() { - _about = '${info.appName} ${info.version}'; + _about = '${info.appName}'; }); }); super.initState(); @@ -566,6 +567,17 @@ class _GeneralSettingsState extends State { }, ), ), + 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( title: Text('Log out', style: TextStyle(color: Colors.red),), leading: Icon(Icons.exit_to_app), diff --git a/lib/ui/tiles.dart b/lib/ui/tiles.dart index 585ebfe..785a44e 100644 --- a/lib/ui/tiles.dart +++ b/lib/ui/tiles.dart @@ -62,7 +62,16 @@ class _TrackTileState extends State { ), onTap: widget.onTap, 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( url: artist.picture.thumb, circular: true, - width: 64, + width: 100, ), Container(height: 4,), Text( diff --git a/pubspec.yaml b/pubspec.yaml index be6513f..19669c7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,7 +39,7 @@ dependencies: connectivity: ^0.4.8+6 intl: ^0.16.1 filesize: ^1.0.4 - fluttertoast: ^7.0.2 + fluttertoast: ^7.0.4 palette_generator: ^0.2.3 flutter_material_color_picker: ^1.0.5 flutter_inappwebview: ^4.0.0 @@ -59,6 +59,7 @@ dependencies: marquee: ^1.5.2 flutter_cache_manager: ^1.4.1 cached_network_image: ^2.2.0+1 + clipboard: ^0.1.2+8 audio_service: ^0.13.0 just_audio: