ui improvements in lyrics screen animated bars when track is playing fix back button when player screen is open instantly pop when track is changed in queue list
1010 lines
31 KiB
Dart
1010 lines
31 KiB
Dart
import 'package:audio_service/audio_service.dart';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:freezer/api/deezer.dart';
|
|
import 'package:freezer/api/download.dart';
|
|
import 'package:freezer/api/player/audio_handler.dart';
|
|
import 'package:freezer/api/player/player_helper.dart';
|
|
import 'package:freezer/icons.dart';
|
|
import 'package:freezer/main.dart';
|
|
import 'package:freezer/translations.i18n.dart';
|
|
import 'package:mini_music_visualizer/mini_music_visualizer.dart';
|
|
|
|
import '../api/definitions.dart';
|
|
import 'cached_image.dart';
|
|
|
|
import 'dart:async';
|
|
|
|
typedef SecondaryTapCallback = void Function(TapUpDetails?);
|
|
|
|
VoidCallback? normalizeSecondary(SecondaryTapCallback? callback) {
|
|
if (callback == null) return null;
|
|
|
|
return () => callback.call(null);
|
|
}
|
|
|
|
class TrackCardTile extends StatelessWidget {
|
|
static const _kDefaultWidth = 424.0;
|
|
final VoidCallback onTap;
|
|
|
|
/// Hold or Right Click
|
|
final SecondaryTapCallback? onSecondary;
|
|
final Widget? trailing;
|
|
final String trackId;
|
|
final String title;
|
|
final String artist;
|
|
final String artUri;
|
|
final bool explicit;
|
|
|
|
final double? width;
|
|
|
|
const TrackCardTile({
|
|
required this.trackId,
|
|
required this.title,
|
|
required this.artist,
|
|
required this.artUri,
|
|
required this.explicit,
|
|
required this.onTap,
|
|
this.onSecondary,
|
|
this.trailing,
|
|
this.width,
|
|
super.key,
|
|
});
|
|
|
|
factory TrackCardTile.fromTrack(
|
|
Track track, {
|
|
required VoidCallback onTap,
|
|
SecondaryTapCallback? onSecondary,
|
|
Widget? trailing,
|
|
double? width,
|
|
}) =>
|
|
TrackCardTile(
|
|
trackId: track.id,
|
|
title: track.title!,
|
|
artist: track.artistString,
|
|
artUri: track.albumArt!.thumb,
|
|
explicit: track.explicit ?? false,
|
|
onSecondary: onSecondary,
|
|
onTap: onTap,
|
|
width: width,
|
|
trailing: trailing,
|
|
);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox(
|
|
width: width ?? _kDefaultWidth,
|
|
height: 64.0,
|
|
child: Card(
|
|
clipBehavior: Clip.antiAlias,
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
onLongPress: normalizeSecondary(onSecondary),
|
|
onSecondaryTapUp: onSecondary,
|
|
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
|
CachedImage(url: artUri, rounded: false),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(title, overflow: TextOverflow.ellipsis, maxLines: 1),
|
|
Text(
|
|
artist,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.bodyMedium
|
|
?.copyWith(color: Theme.of(context).disabledColor),
|
|
)
|
|
]),
|
|
),
|
|
),
|
|
]),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class TrackTile extends StatelessWidget {
|
|
final VoidCallback? onTap;
|
|
|
|
/// Hold or Right Click
|
|
final SecondaryTapCallback? onSecondary;
|
|
final Widget? trailing;
|
|
final String trackId;
|
|
final String title;
|
|
final String artist;
|
|
final String artUri;
|
|
final bool explicit;
|
|
final String durationString;
|
|
|
|
/// Disable if not needed, makes app lag, and uses lots of resources
|
|
final bool checkTrackOffline;
|
|
|
|
const TrackTile({
|
|
required this.trackId,
|
|
required this.title,
|
|
required this.artist,
|
|
required this.artUri,
|
|
required this.explicit,
|
|
required this.durationString,
|
|
this.onTap,
|
|
this.onSecondary,
|
|
this.trailing,
|
|
this.checkTrackOffline = true,
|
|
super.key,
|
|
});
|
|
|
|
factory TrackTile.fromTrack(Track track,
|
|
{VoidCallback? onTap,
|
|
SecondaryTapCallback? onSecondary,
|
|
Widget? trailing,
|
|
bool checkTrackOffline = true}) =>
|
|
TrackTile(
|
|
trackId: track.id,
|
|
title: track.title!,
|
|
artist: track.artistString,
|
|
artUri: track.albumArt!.thumb,
|
|
explicit: track.explicit ?? false,
|
|
durationString: track.durationString,
|
|
onSecondary: onSecondary,
|
|
onTap: onTap,
|
|
trailing: trailing,
|
|
checkTrackOffline: checkTrackOffline,
|
|
);
|
|
|
|
factory TrackTile.fromMediaItem(MediaItem mediaItem,
|
|
{VoidCallback? onTap,
|
|
SecondaryTapCallback? onSecondary,
|
|
Widget? trailing,
|
|
bool checkTrackOffline = true}) =>
|
|
TrackTile(
|
|
trackId: mediaItem.id,
|
|
title: mediaItem.title,
|
|
artist: mediaItem.artist ?? '',
|
|
artUri: mediaItem.extras!['thumb'],
|
|
explicit: false,
|
|
durationString: Track.durationAsString(mediaItem.duration!),
|
|
onSecondary: onSecondary,
|
|
onTap: onTap,
|
|
trailing: trailing,
|
|
checkTrackOffline: checkTrackOffline,
|
|
);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onSecondaryTapUp: onSecondary,
|
|
child: ListTile(
|
|
title: StreamBuilder<MediaItem?>(
|
|
stream: audioHandler.mediaItem,
|
|
builder: (context, snapshot) {
|
|
final mediaItem = snapshot.data;
|
|
final bool isHighlighted = mediaItem?.id == trackId;
|
|
return Text(
|
|
title,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.clip,
|
|
style: TextStyle(
|
|
color: isHighlighted
|
|
? Theme.of(context).colorScheme.primary
|
|
: null),
|
|
);
|
|
}),
|
|
subtitle: Text(
|
|
artist,
|
|
maxLines: 1,
|
|
),
|
|
leading: StreamBuilder<MediaItem?>(
|
|
initialData: audioHandler.mediaItem.value,
|
|
stream: audioHandler.mediaItem,
|
|
builder: (context, snapshot) {
|
|
final child = CachedImage(
|
|
url: artUri,
|
|
width: 48.0,
|
|
height: 48.0,
|
|
);
|
|
|
|
if (snapshot.data?.id == trackId) {
|
|
return Stack(children: [
|
|
child,
|
|
Positioned.fill(
|
|
child: DecoratedBox(
|
|
decoration: const BoxDecoration(color: Colors.black26),
|
|
child: Center(
|
|
child: SizedBox(
|
|
width: 18.0,
|
|
height: 16.0,
|
|
child: StreamBuilder<bool>(
|
|
stream: playerHelper.playing,
|
|
builder: (context, snapshot) {
|
|
return MiniMusicVisualizer(
|
|
color: Colors.white70,
|
|
animate: snapshot.data ?? false);
|
|
})),
|
|
)),
|
|
),
|
|
]);
|
|
}
|
|
return child;
|
|
}),
|
|
onTap: onTap,
|
|
onLongPress: normalizeSecondary(onSecondary),
|
|
trailing: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (checkTrackOffline)
|
|
FutureBuilder<bool>(
|
|
future:
|
|
downloadManager.checkOffline(track: Track(id: trackId)),
|
|
builder: (context, snapshot) {
|
|
if (snapshot.data == true) {
|
|
return const Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 2.0),
|
|
child: Icon(
|
|
FreezerIcons.primitive_dot,
|
|
color: Colors.green,
|
|
size: 12.0,
|
|
),
|
|
);
|
|
}
|
|
return const SizedBox.shrink();
|
|
}),
|
|
if (explicit)
|
|
const Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 2.0),
|
|
child: Text(
|
|
'E',
|
|
style: TextStyle(color: Colors.red),
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 42.0,
|
|
child: Text(
|
|
durationString,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
if (trailing != null) trailing!
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AlbumTile extends StatelessWidget {
|
|
final Album? album;
|
|
final void Function()? onTap;
|
|
|
|
/// Hold or Right click
|
|
final SecondaryTapCallback? onSecondary;
|
|
final Widget? trailing;
|
|
|
|
const AlbumTile(this.album,
|
|
{super.key, this.onTap, this.onSecondary, this.trailing});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onSecondaryTapUp: onSecondary,
|
|
child: ListTile(
|
|
title: Text(
|
|
album!.title!,
|
|
maxLines: 1,
|
|
),
|
|
subtitle: Text(
|
|
album!.artistString,
|
|
maxLines: 1,
|
|
),
|
|
leading: CachedImage(
|
|
url: album!.art!.thumb,
|
|
width: 48,
|
|
),
|
|
onTap: onTap,
|
|
onLongPress: normalizeSecondary(onSecondary),
|
|
trailing: trailing,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ArtistTile extends StatelessWidget {
|
|
final Artist? artist;
|
|
final void Function()? onTap;
|
|
|
|
/// Hold or Right click
|
|
final SecondaryTapCallback? onSecondary;
|
|
|
|
const ArtistTile(this.artist, {super.key, this.onTap, this.onSecondary});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InkWell(
|
|
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
|
|
onTap: onTap,
|
|
onLongPress: normalizeSecondary(onSecondary),
|
|
onSecondaryTapUp: onSecondary,
|
|
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
|
const SizedBox(height: 4.0),
|
|
CachedImage(
|
|
url: artist!.picture!.thumb,
|
|
circular: true,
|
|
width: 100,
|
|
),
|
|
const SizedBox(height: 8.0),
|
|
Text(
|
|
artist!.name!,
|
|
maxLines: 1,
|
|
textAlign: TextAlign.center,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(fontSize: 14.0),
|
|
),
|
|
const SizedBox(height: 4),
|
|
]));
|
|
}
|
|
}
|
|
|
|
class PlaylistTile extends StatelessWidget {
|
|
final Playlist? playlist;
|
|
final void Function()? onTap;
|
|
final SecondaryTapCallback? onSecondary;
|
|
final Widget? trailing;
|
|
|
|
const PlaylistTile(this.playlist,
|
|
{super.key, this.onSecondary, this.onTap, this.trailing});
|
|
|
|
String? get subtitle {
|
|
if (playlist!.user == null ||
|
|
playlist!.user!.name == null ||
|
|
playlist!.user!.name == '' ||
|
|
playlist!.user!.id == DeezerAPI.instance.userId) {
|
|
if (playlist!.trackCount == null) return '';
|
|
return '${playlist!.trackCount} ${'Tracks'.i18n}';
|
|
}
|
|
return playlist!.user!.name;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onSecondaryTapUp: onSecondary,
|
|
child: ListTile(
|
|
title: Text(
|
|
playlist!.title!,
|
|
maxLines: 1,
|
|
),
|
|
subtitle: Text(
|
|
subtitle!,
|
|
maxLines: 1,
|
|
),
|
|
leading: CachedImage(
|
|
url: playlist!.image!.thumb,
|
|
width: 48,
|
|
rounded: true,
|
|
),
|
|
onTap: onTap,
|
|
onLongPress: normalizeSecondary(onSecondary),
|
|
trailing: trailing,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ArtistHorizontalTile extends StatelessWidget {
|
|
final Artist? artist;
|
|
final void Function()? onTap;
|
|
final void Function()? onHold;
|
|
final Widget? trailing;
|
|
|
|
const ArtistHorizontalTile(this.artist,
|
|
{super.key, this.onHold, this.onTap, this.trailing});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
|
child: ListTile(
|
|
title: Text(
|
|
artist!.name!,
|
|
maxLines: 1,
|
|
),
|
|
leading: CircleAvatar(
|
|
backgroundImage: CachedNetworkImageProvider(artist!.picture!.thumb,
|
|
cacheManager: cacheManager)),
|
|
onTap: onTap,
|
|
onLongPress: onHold,
|
|
trailing: trailing,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class PlaylistCardTile extends StatelessWidget {
|
|
final Playlist? playlist;
|
|
final VoidCallback? onTap;
|
|
final SecondaryTapCallback? onSecondary;
|
|
const PlaylistCardTile(this.playlist,
|
|
{super.key, this.onTap, this.onSecondary});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
onSecondaryTapUp: onSecondary,
|
|
child: SizedBox(
|
|
height: 180.0,
|
|
child: InkWell(
|
|
onTap: onTap,
|
|
onLongPress: normalizeSecondary(onSecondary),
|
|
child: Column(
|
|
children: <Widget>[
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Stack(
|
|
children: [
|
|
CachedImage(
|
|
url: playlist!.image!.thumb,
|
|
width: 128.0,
|
|
height: 128.0,
|
|
rounded: true,
|
|
),
|
|
Positioned(
|
|
bottom: 8.0,
|
|
left: 8.0,
|
|
child: PlayItemButton(
|
|
onTap: () async {
|
|
final Playlist fullPlaylist = await DeezerAPI
|
|
.instance
|
|
.fullPlaylist(playlist!.id);
|
|
await playerHelper.playFromPlaylist(fullPlaylist);
|
|
},
|
|
))
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 2.0),
|
|
SizedBox(
|
|
width: 144,
|
|
child: Text(
|
|
playlist!.title!,
|
|
maxLines: 1,
|
|
textAlign: TextAlign.center,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(fontSize: 14.0),
|
|
),
|
|
),
|
|
const SizedBox(
|
|
height: 4.0,
|
|
)
|
|
],
|
|
),
|
|
)),
|
|
);
|
|
}
|
|
}
|
|
|
|
class PlayItemButton extends StatelessWidget {
|
|
final FutureOr<void> Function() onTap;
|
|
final double size;
|
|
final bool filled;
|
|
const PlayItemButton({
|
|
required this.onTap,
|
|
this.size = 32.0,
|
|
this.filled = true,
|
|
super.key,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox.square(
|
|
dimension: size,
|
|
child: DecoratedBox(
|
|
decoration: filled
|
|
? const BoxDecoration(
|
|
shape: BoxShape.circle, color: Colors.white)
|
|
: const BoxDecoration(),
|
|
child: Center(
|
|
child: AwaitingButton(
|
|
onTap: onTap,
|
|
child: Icon(
|
|
Icons.play_arrow,
|
|
color: filled ? Colors.black : Colors.white,
|
|
shadows: filled
|
|
? null
|
|
: const [
|
|
Shadow(blurRadius: 2.0, color: Colors.black)
|
|
],
|
|
size: size / 1.5,
|
|
)))));
|
|
}
|
|
}
|
|
|
|
class AwaitingButton extends StatefulWidget {
|
|
final FutureOr<void> Function() onTap;
|
|
final double size;
|
|
final Widget child;
|
|
const AwaitingButton(
|
|
{required this.onTap, required this.child, this.size = 32.0, super.key});
|
|
|
|
@override
|
|
State<AwaitingButton> createState() => _AwaitingButtonState();
|
|
}
|
|
|
|
class _AwaitingButtonState extends State<AwaitingButton> {
|
|
final _isLoading = ValueNotifier(false);
|
|
void _onTap() {
|
|
final ret = widget.onTap();
|
|
if (ret is Future) {
|
|
_isLoading.value = true;
|
|
ret.whenComplete(() => _isLoading.value = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_isLoading.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ValueListenableBuilder<bool>(
|
|
valueListenable: _isLoading,
|
|
child: InkWell(
|
|
onTap: _onTap,
|
|
child: widget.child,
|
|
),
|
|
builder: (context, isLoading, child) => isLoading
|
|
? SizedBox.square(
|
|
dimension: widget.size / 2,
|
|
child: const CircularProgressIndicator(
|
|
strokeWidth: 2.0,
|
|
color: Colors.black,
|
|
),
|
|
)
|
|
: child!);
|
|
}
|
|
}
|
|
|
|
class SmartTrackListTile extends StatefulWidget {
|
|
final SmartTrackList? smartTrackList;
|
|
final FutureOr<void> Function()? onTap;
|
|
final void Function()? onHold;
|
|
final double size;
|
|
|
|
const SmartTrackListTile(this.smartTrackList,
|
|
{super.key, this.onHold, this.onTap, this.size = 128.0});
|
|
|
|
@override
|
|
State<SmartTrackListTile> createState() => _SmartTrackListTileState();
|
|
}
|
|
|
|
class _SmartTrackListTileState extends State<SmartTrackListTile> {
|
|
final _isLoading = ValueNotifier<bool>(false);
|
|
|
|
@override
|
|
void dispose() {
|
|
_isLoading.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onTap() {
|
|
final future = widget.onTap?.call();
|
|
if (future is Future) {
|
|
_isLoading.value = true;
|
|
future.whenComplete(() => _isLoading.value = false);
|
|
}
|
|
}
|
|
|
|
Widget buildTrackTileCover(List<DeezerImageDetails> covers) {
|
|
if (covers.length == 4) {
|
|
return ClipRRect(
|
|
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
|
|
child: SizedBox.square(
|
|
dimension: widget.size,
|
|
child: Column(
|
|
children: [
|
|
Expanded(
|
|
child: Row(children: [
|
|
...[covers[0], covers[1]].map((e) => CachedImage(
|
|
url: e.thumb,
|
|
))
|
|
]),
|
|
),
|
|
Expanded(
|
|
child: Row(children: [
|
|
...[covers[2], covers[3]].map((e) => CachedImage(
|
|
url: e.thumb,
|
|
))
|
|
]),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
// return GridView(
|
|
// gridDelegate:
|
|
// SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
|
|
// primary: false,
|
|
// physics: NeverScrollableScrollPhysics(),
|
|
// children: [...covers.map((e) => CachedImage(url: e.thumb))],
|
|
// );
|
|
}
|
|
|
|
if (widget.smartTrackList?.id == 'flow') {
|
|
return Material(
|
|
elevation: 2.0,
|
|
shape: const CircleBorder(),
|
|
color: Theme.of(context).colorScheme.onInverseSurface,
|
|
child: CachedImage(
|
|
width: widget.size,
|
|
height: widget.size,
|
|
url: covers[0].size(232, 232, id: 'none', num: 80, format: 'png'),
|
|
rounded: false,
|
|
circular: true,
|
|
),
|
|
);
|
|
}
|
|
return CachedImage(
|
|
width: widget.size,
|
|
height: widget.size,
|
|
url: covers[0].full,
|
|
rounded: true,
|
|
circular: false,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InkWell(
|
|
onTap: _onTap,
|
|
onLongPress: widget.onHold,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: SizedBox.square(
|
|
dimension: widget.size,
|
|
child: Stack(
|
|
children: [
|
|
buildTrackTileCover(widget.smartTrackList!.cover!),
|
|
if (widget.smartTrackList?.id != 'flow')
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8.0, vertical: 6.0),
|
|
child: Text(
|
|
widget.smartTrackList!.title!,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
fontSize: 16.0,
|
|
shadows: [
|
|
Shadow(
|
|
offset: Offset(1, 1),
|
|
blurRadius: 2,
|
|
color: Colors.black)
|
|
],
|
|
color: Colors.white),
|
|
),
|
|
),
|
|
if (widget.smartTrackList?.id != 'flow')
|
|
Center(
|
|
child: SizedBox.square(
|
|
dimension: 32.0,
|
|
child: DecoratedBox(
|
|
decoration: const BoxDecoration(
|
|
shape: BoxShape.circle, color: Colors.white),
|
|
child: Center(
|
|
child: ValueListenableBuilder<bool>(
|
|
valueListenable: _isLoading,
|
|
builder: (context, isLoading, _) {
|
|
if (isLoading) {
|
|
return const SizedBox.square(
|
|
dimension: 16.0,
|
|
child: CircularProgressIndicator(
|
|
color: Colors.black,
|
|
strokeWidth: 2.0,
|
|
));
|
|
}
|
|
return const Icon(
|
|
Icons.play_arrow,
|
|
color: Colors.black,
|
|
size: 24.0,
|
|
);
|
|
}),
|
|
),
|
|
))),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: widget.size,
|
|
child: Text(
|
|
widget.smartTrackList!.subtitle!,
|
|
maxLines: 3,
|
|
textAlign: TextAlign.center,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(fontSize: 14.0),
|
|
),
|
|
),
|
|
const SizedBox(height: 8.0)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AlbumCard extends StatelessWidget {
|
|
final Album album;
|
|
final void Function()? onTap;
|
|
final SecondaryTapCallback? onSecondary;
|
|
|
|
const AlbumCard(this.album, {super.key, this.onTap, this.onSecondary});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
onLongPress: normalizeSecondary(onSecondary),
|
|
onSecondaryTapUp: onSecondary,
|
|
child: Column(
|
|
children: <Widget>[
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Stack(
|
|
children: [
|
|
CachedImage(
|
|
width: 128.0,
|
|
height: 128.0,
|
|
url: album.art!.thumb,
|
|
rounded: true),
|
|
Positioned(
|
|
bottom: 8.0,
|
|
left: 8.0,
|
|
child: PlayItemButton(
|
|
onTap: () async {
|
|
final fullAlbum =
|
|
await DeezerAPI.instance.album(album.id);
|
|
await playerHelper.playFromAlbum(fullAlbum);
|
|
},
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 144.0,
|
|
child: Text(
|
|
album.title!,
|
|
maxLines: 1,
|
|
textAlign: TextAlign.center,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(fontSize: 14.0),
|
|
),
|
|
),
|
|
const SizedBox(height: 4.0),
|
|
SizedBox(
|
|
width: 144.0,
|
|
child: Text(
|
|
album.artistString,
|
|
maxLines: 1,
|
|
textAlign: TextAlign.center,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 12.0,
|
|
color: (Theme.of(context).brightness == Brightness.light)
|
|
? Colors.grey[800]
|
|
: Colors.white70),
|
|
),
|
|
),
|
|
const SizedBox(height: 8.0)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ChannelTile extends StatelessWidget {
|
|
final DeezerChannel channel;
|
|
final Function? onTap;
|
|
const ChannelTile(this.channel, {super.key, this.onTap});
|
|
|
|
Color _textColor() {
|
|
double luminance = channel.backgroundColor.computeLuminance();
|
|
return (luminance > 0.5) ? Colors.black : Colors.white;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final Widget child;
|
|
if (channel.logo != null) {
|
|
child = Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: CachedNetworkImage(
|
|
cacheKey: channel.logo!.md5,
|
|
cacheManager: cacheManager,
|
|
height: 52.0,
|
|
imageUrl:
|
|
channel.logo!.size(52, 0, num: 100, id: 'none', format: 'png')),
|
|
);
|
|
} else {
|
|
child = Text(
|
|
channel.title!,
|
|
textAlign: TextAlign.center,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 18.0,
|
|
fontWeight: FontWeight.bold,
|
|
color: channel.picture == null ? _textColor() : Colors.white),
|
|
);
|
|
}
|
|
return SizedBox(
|
|
width: 150,
|
|
height: 75,
|
|
child: DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
|
|
color: channel.picture == null ? channel.backgroundColor : null,
|
|
image: channel.picture == null
|
|
? null
|
|
: DecorationImage(
|
|
fit: BoxFit.cover,
|
|
image: CachedNetworkImageProvider(
|
|
channel.picture!.size(134, 264),
|
|
cacheManager: cacheManager,
|
|
cacheKey: channel.picture!.md5))),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
clipBehavior: Clip.hardEdge,
|
|
borderRadius: const BorderRadius.all(Radius.circular(4.0)),
|
|
child: InkWell(
|
|
focusColor: Colors.black45, // give better visibility
|
|
onTap: onTap as void Function()?,
|
|
child: Center(child: child)),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ShowCard extends StatelessWidget {
|
|
final Show? show;
|
|
final VoidCallback? onTap;
|
|
final VoidCallback? onHold;
|
|
|
|
const ShowCard(this.show, {super.key, this.onTap, this.onHold});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
onLongPress: onHold,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: CachedImage(
|
|
url: show!.art!.thumb,
|
|
width: 128.0,
|
|
height: 128.0,
|
|
rounded: true,
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 144.0,
|
|
child: Text(
|
|
show!.name!,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(fontSize: 14.0),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ShowTile extends StatelessWidget {
|
|
final Show show;
|
|
final Function? onTap;
|
|
final Function? onHold;
|
|
|
|
const ShowTile(this.show, {super.key, this.onTap, this.onHold});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListTile(
|
|
title: Text(
|
|
show.name!,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
subtitle: Text(
|
|
show.description!,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
onTap: onTap as void Function()?,
|
|
onLongPress: onHold as void Function()?,
|
|
leading: CachedImage(
|
|
url: show.art!.thumb,
|
|
width: 48,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class ShowEpisodeTile extends StatelessWidget {
|
|
final ShowEpisode episode;
|
|
final VoidCallback? onTap;
|
|
final SecondaryTapCallback? onSecondary;
|
|
final Widget? trailing;
|
|
|
|
const ShowEpisodeTile(this.episode,
|
|
{super.key, this.onTap, this.onSecondary, this.trailing});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
onLongPress: normalizeSecondary(onSecondary),
|
|
onSecondaryTapUp: onSecondary,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ListTile(
|
|
title: Text(episode.title!, maxLines: 2),
|
|
trailing: trailing,
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
child: Text(
|
|
episode.description!,
|
|
maxLines: 10,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
color: Theme.of(context)
|
|
.textTheme
|
|
.titleMedium!
|
|
.color!
|
|
.withOpacity(0.9)),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const 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
|
|
.titleMedium!
|
|
.color!
|
|
.withOpacity(0.6)),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Divider(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|