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.dart'; import 'package:freezer/translations.i18n.dart'; import '../api/definitions.dart'; import 'cached_image.dart'; import 'dart:async'; class TrackTile extends StatelessWidget { final Track track; final void Function()? onTap; final void Function()? onHold; final Widget? trailing; TrackTile(this.track, {this.onTap, this.onHold, this.trailing, Key? key}) : super(key: key); @override Widget build(BuildContext context) { return ListTile( title: StreamBuilder( stream: audioHandler.mediaItem, builder: (context, snapshot) { final bool isHighlighted; final mediaItem = snapshot.data; if (!snapshot.hasData || snapshot.data == null) isHighlighted = false; else isHighlighted = mediaItem!.id == track.id; return Text( track.title!, maxLines: 1, overflow: TextOverflow.clip, style: TextStyle( color: isHighlighted ? Theme.of(context).colorScheme.primary : null), ); }), subtitle: Text( track.artistString, maxLines: 1, ), leading: CachedImage( url: track.albumArt!.thumb, width: 48.0, height: 48.0, ), onTap: onTap, onLongPress: onHold, trailing: Row( mainAxisSize: MainAxisSize.min, children: [ FutureBuilder( future: downloadManager.checkOffline(track: track), 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 (track.explicit ?? false) const Padding( padding: EdgeInsets.symmetric(horizontal: 2.0), child: Text( 'E', style: TextStyle(color: Colors.red), ), ), SizedBox( width: 42.0, child: Text( track.durationString, textAlign: TextAlign.center, ), ), if (trailing != null) trailing! ], ), ); } } class AlbumTile extends StatelessWidget { final Album? album; final void Function()? onTap; final void Function()? onHold; final Widget? trailing; AlbumTile(this.album, {this.onTap, this.onHold, this.trailing}); @override Widget build(BuildContext context) { return ListTile( title: Text( album!.title!, maxLines: 1, ), subtitle: Text( album!.artistString, maxLines: 1, ), leading: CachedImage( url: album!.art!.thumb, width: 48, ), onTap: onTap, onLongPress: onHold, trailing: trailing, ); } } class ArtistTile extends StatelessWidget { final Artist? artist; final void Function()? onTap; final void Function()? onHold; ArtistTile(this.artist, {this.onTap, this.onHold}); @override Widget build(BuildContext context) { return InkWell( borderRadius: BorderRadius.all(Radius.circular(4.0)), onTap: onTap, onLongPress: onHold, child: Column(mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 4), CachedImage( url: artist!.picture!.thumb, circular: true, width: 100, ), const SizedBox(height: 8), 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 void Function()? onHold; final Widget? trailing; PlaylistTile(this.playlist, {this.onHold, 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 ListTile( title: Text( playlist!.title!, maxLines: 1, ), subtitle: Text( subtitle!, maxLines: 1, ), leading: CachedImage( url: playlist!.image!.thumb, width: 48, ), onTap: onTap, onLongPress: onHold, trailing: trailing, ); } } class ArtistHorizontalTile extends StatelessWidget { final Artist? artist; final void Function()? onTap; final void Function()? onHold; final Widget? trailing; ArtistHorizontalTile(this.artist, {this.onHold, this.onTap, this.trailing}); @override Widget build(BuildContext context) { return Padding( padding: 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 Function? onTap; final Function? onHold; PlaylistCardTile(this.playlist, {this.onTap, this.onHold}); @override Widget build(BuildContext context) { return SizedBox( height: 180.0, child: InkWell( onTap: onTap as void Function()?, onLongPress: onHold as void Function()?, child: Column( children: [ Padding( padding: 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: TextStyle(fontSize: 14.0), ), ), const SizedBox( height: 4.0, ) ], ), )); } } class PlayItemButton extends StatefulWidget { final FutureOr Function() onTap; final double size; const PlayItemButton({required this.onTap, this.size = 32.0, Key? key}) : super(key: key); @override State createState() => _PlayItemButtonState(); } class _PlayItemButtonState extends State { 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: BoxDecoration(shape: BoxShape.circle, color: Colors.white), child: Center( child: ValueListenableBuilder( 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: CircularProgressIndicator( strokeWidth: 2.0, color: Colors.black, ), ) : child!), ), ), ); } } class SmartTrackListTile extends StatefulWidget { final SmartTrackList? smartTrackList; final FutureOr Function()? onTap; final void Function()? onHold; final double size; const SmartTrackListTile(this.smartTrackList, {this.onHold, this.onTap, this.size = 128.0}); @override State createState() => _SmartTrackListTileState(); } class _SmartTrackListTileState extends State { final _isLoading = ValueNotifier(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 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: [ Padding( padding: EdgeInsets.all(8.0), child: SizedBox.square( dimension: widget.size, child: Stack( children: [ buildTrackTileCover(widget.smartTrackList!.cover!), if (widget.smartTrackList?.id != 'flow') Padding( padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), child: Text( widget.smartTrackList!.title!, maxLines: 1, overflow: TextOverflow.ellipsis, style: 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: BoxDecoration( shape: BoxShape.circle, color: Colors.white), child: Center( child: ValueListenableBuilder( 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: TextStyle(fontSize: 14.0), ), ), const SizedBox(height: 8.0) ], ), ); } } class AlbumCard extends StatelessWidget { final Album album; final void Function()? onTap; final void Function()? onHold; AlbumCard(this.album, {this.onTap, this.onHold}); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, onLongPress: onHold, child: Column( children: [ Padding( padding: const EdgeInsets.all(8.0), child: Stack( children: [ CachedImage( width: 128.0, height: 128.0, url: album!.art!.thumb, rounded: true), Positioned( child: PlayItemButton( onTap: () async { final fullAlbum = await deezerAPI.album(album.id); await playerHelper.playFromAlbum(fullAlbum); }, ), bottom: 8.0, left: 8.0, ) ], ), ), SizedBox( width: 144.0, child: Text( album!.title!, maxLines: 1, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, style: 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; ChannelTile(this.channel, {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( onTap: this.onTap as void Function()?, child: Center(child: child)), ), ), ); } } class ShowCard extends StatelessWidget { final Show? show; final Function? onTap; final Function? onHold; ShowCard(this.show, {this.onTap, this.onHold}); @override Widget build(BuildContext context) { return Container( child: InkWell( onTap: onTap as void Function()?, onLongPress: onHold as void Function()?, child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: EdgeInsets.all(8.0), child: CachedImage( url: show!.art!.thumb, width: 128.0, height: 128.0, rounded: true, ), ), Container( width: 144.0, child: Text( show!.name!, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: TextStyle(fontSize: 14.0), ), ), ], ), ), ); } } class ShowTile extends StatelessWidget { final Show show; final Function? onTap; final Function? onHold; ShowTile(this.show, {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 Function? onTap; final Function? onHold; final Widget? trailing; ShowEpisodeTile(this.episode, {this.onTap, this.onHold, this.trailing}); @override Widget build(BuildContext context) { return InkWell( onLongPress: onHold as void Function()?, onTap: onTap as void Function()?, child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( title: Text(episode.title!, maxLines: 2), trailing: trailing, ), Padding( padding: EdgeInsets.symmetric(horizontal: 16.0), child: Text( episode.description!, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( color: Theme.of(context) .textTheme .titleMedium! .color! .withOpacity(0.9)), ), ), Padding( padding: 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)), ), ], ), ), Divider(), ], ), ); } }