Split discography, copy arl button, small fixes
This commit is contained in:
parent
4e5e3a3059
commit
b9004c3004
@ -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,
|
||||||
|
@ -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,6 +80,45 @@ 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) {
|
||||||
@ -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',
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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,);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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(
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user