Pato05
f126ffef46
use get_url api by default, and fall back to old generation if get_url failed start to write a better cachemanager to implement in all systems write in more appropriate directories on windows and linux improve check for Connectivity by adding a fallback (needed for example on linux systems without NetworkManager) allow to dynamically change track quality without rebuilding the object
846 lines
25 KiB
Dart
846 lines
25 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/translations.i18n.dart';
|
|
|
|
import '../api/definitions.dart';
|
|
import 'cached_image.dart';
|
|
|
|
import 'dart:async';
|
|
|
|
typedef SecondaryTapCallback = void Function(TapDownDetails?);
|
|
|
|
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(
|
|
onSecondaryTapDown: 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(
|
|
onSecondaryTapDown: 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),
|
|
onSecondaryTapDown: 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(
|
|
onSecondaryTapDown: onSecondary,
|
|
child: ListTile(
|
|
title: Text(
|
|
playlist!.title!,
|
|
maxLines: 1,
|
|
),
|
|
subtitle: Text(
|
|
subtitle!,
|
|
maxLines: 1,
|
|
),
|
|
leading: CachedImage(
|
|
url: playlist!.image!.thumb,
|
|
width: 48,
|
|
),
|
|
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)),
|
|
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(
|
|
onSecondaryTapDown: 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))],
|
|
// );
|
|
}
|
|
return CachedImage(
|
|
width: widget.size,
|
|
height: widget.size,
|
|
url: covers[0].full,
|
|
rounded: widget.smartTrackList?.id != 'flow',
|
|
circular: widget.smartTrackList?.id == 'flow',
|
|
);
|
|
}
|
|
|
|
@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),
|
|
),
|
|
),
|
|
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,
|
|
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),
|
|
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),
|
|
onSecondaryTapDown: 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(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|