866 lines
26 KiB
Dart
866 lines
26 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:fluttericon/octicons_icons.dart';
|
|
import 'package:freezer/api/deezer.dart';
|
|
import 'package:freezer/api/download.dart';
|
|
import 'package:freezer/api/player/audio_handler.dart';
|
|
import 'package:freezer/main.dart';
|
|
import 'package:freezer/translations.i18n.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 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,
|
|
Key? key,
|
|
}) : super(key: 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: CachedImage(
|
|
url: artUri,
|
|
width: 48.0,
|
|
height: 48.0,
|
|
),
|
|
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(
|
|
Octicons.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.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.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 StatefulWidget {
|
|
final FutureOr<void> Function() onTap;
|
|
final double size;
|
|
const PlayItemButton({required this.onTap, this.size = 32.0, Key? key})
|
|
: super(key: key);
|
|
|
|
@override
|
|
State<PlayItemButton> createState() => _PlayItemButtonState();
|
|
}
|
|
|
|
class _PlayItemButtonState extends State<PlayItemButton> {
|
|
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 SizedBox.square(
|
|
dimension: widget.size,
|
|
child: DecoratedBox(
|
|
decoration:
|
|
const BoxDecoration(shape: BoxShape.circle, color: Colors.white),
|
|
child: Center(
|
|
child: ValueListenableBuilder<bool>(
|
|
valueListenable: _isLoading,
|
|
child: InkWell(
|
|
onTap: _onTap,
|
|
child: Icon(
|
|
Icons.play_arrow,
|
|
color: Colors.black,
|
|
size: widget.size / 1.5,
|
|
),
|
|
),
|
|
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 void Function()? onHold;
|
|
|
|
const AlbumCard(this.album, {super.key, this.onTap, this.onHold});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
onLongPress: onHold,
|
|
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.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(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|