added cargo files
This commit is contained in:
957
PinePods-0.8.2/mobile/lib/ui/widgets/episode_tile.dart
Normal file
957
PinePods-0.8.2/mobile/lib/ui/widgets/episode_tile.dart
Normal file
@@ -0,0 +1,957 @@
|
||||
// Copyright 2020 Ben Hills and the project contributors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:pinepods_mobile/bloc/podcast/audio_bloc.dart';
|
||||
import 'package:pinepods_mobile/bloc/podcast/episode_bloc.dart';
|
||||
import 'package:pinepods_mobile/bloc/podcast/podcast_bloc.dart';
|
||||
import 'package:pinepods_mobile/bloc/podcast/queue_bloc.dart';
|
||||
import 'package:pinepods_mobile/bloc/settings/settings_bloc.dart';
|
||||
import 'package:pinepods_mobile/entities/downloadable.dart';
|
||||
import 'package:pinepods_mobile/entities/episode.dart';
|
||||
import 'package:pinepods_mobile/l10n/L.dart';
|
||||
import 'package:pinepods_mobile/services/audio/audio_player_service.dart';
|
||||
import 'package:pinepods_mobile/state/queue_event_state.dart';
|
||||
import 'package:pinepods_mobile/ui/podcast/episode_details.dart';
|
||||
import 'package:pinepods_mobile/ui/podcast/transport_controls.dart';
|
||||
import 'package:pinepods_mobile/ui/widgets/action_text.dart';
|
||||
import 'package:pinepods_mobile/ui/widgets/tile_image.dart';
|
||||
import 'package:pinepods_mobile/ui/utils/player_utils.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dialogs/flutter_dialogs.dart';
|
||||
import 'package:intl/intl.dart' show DateFormat;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
/// This class builds a tile for each episode in the podcast feed.
|
||||
class EpisodeTile extends StatelessWidget {
|
||||
final Episode episode;
|
||||
final bool download;
|
||||
final bool play;
|
||||
final bool playing;
|
||||
final bool queued;
|
||||
|
||||
const EpisodeTile({
|
||||
super.key,
|
||||
required this.episode,
|
||||
required this.download,
|
||||
required this.play,
|
||||
this.playing = false,
|
||||
this.queued = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mediaQueryData = MediaQuery.of(context);
|
||||
|
||||
if (mediaQueryData.accessibleNavigation) {
|
||||
if (defaultTargetPlatform == TargetPlatform.iOS) {
|
||||
return _CupertinoAccessibleEpisodeTile(
|
||||
episode: episode,
|
||||
download: download,
|
||||
play: play,
|
||||
playing: playing,
|
||||
queued: queued,
|
||||
);
|
||||
} else {
|
||||
return _AccessibleEpisodeTile(
|
||||
episode: episode,
|
||||
download: download,
|
||||
play: play,
|
||||
playing: playing,
|
||||
queued: queued,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return ExpandableEpisodeTile(
|
||||
episode: episode,
|
||||
download: download,
|
||||
play: play,
|
||||
playing: playing,
|
||||
queued: queued,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An EpisodeTitle is built with an [ExpansionTile] widget and displays the episode's
|
||||
/// basic details, thumbnail and play button.
|
||||
///
|
||||
/// It can then be expanded to present addition information about the episode and further
|
||||
/// controls.
|
||||
///
|
||||
/// TODO: Replace [Opacity] with [Container] with a transparent colour.
|
||||
class ExpandableEpisodeTile extends StatefulWidget {
|
||||
final Episode episode;
|
||||
final bool download;
|
||||
final bool play;
|
||||
final bool playing;
|
||||
final bool queued;
|
||||
|
||||
const ExpandableEpisodeTile({
|
||||
super.key,
|
||||
required this.episode,
|
||||
required this.download,
|
||||
required this.play,
|
||||
this.playing = false,
|
||||
this.queued = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ExpandableEpisodeTile> createState() => _ExpandableEpisodeTileState();
|
||||
}
|
||||
|
||||
class _ExpandableEpisodeTileState extends State<ExpandableEpisodeTile> {
|
||||
bool expanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final episodeBloc = Provider.of<EpisodeBloc>(context);
|
||||
final queueBloc = Provider.of<QueueBloc>(context);
|
||||
|
||||
return ExpansionTile(
|
||||
tilePadding: const EdgeInsets.fromLTRB(16.0, 0.0, 8.0, 0.0),
|
||||
key: Key('PT${widget.episode.guid}'),
|
||||
onExpansionChanged: (isExpanded) {
|
||||
setState(() {
|
||||
expanded = isExpanded;
|
||||
});
|
||||
},
|
||||
trailing: Opacity(
|
||||
opacity: widget.episode.played ? 0.5 : 1.0,
|
||||
child: EpisodeTransportControls(
|
||||
episode: widget.episode,
|
||||
download: widget.download,
|
||||
play: widget.play,
|
||||
),
|
||||
),
|
||||
leading: ExcludeSemantics(
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomLeft,
|
||||
fit: StackFit.passthrough,
|
||||
children: <Widget>[
|
||||
Opacity(
|
||||
opacity: widget.episode.played ? 0.5 : 1.0,
|
||||
child: TileImage(
|
||||
url: widget.episode.thumbImageUrl ?? widget.episode.imageUrl!,
|
||||
size: 56.0,
|
||||
highlight: widget.episode.highlight,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 5.0,
|
||||
width: 56.0 * (widget.episode.percentagePlayed / 100),
|
||||
child: Container(
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
subtitle: Opacity(
|
||||
opacity: widget.episode.played ? 0.5 : 1.0,
|
||||
child: EpisodeSubtitle(widget.episode),
|
||||
),
|
||||
title: Opacity(
|
||||
opacity: widget.episode.played ? 0.5 : 1.0,
|
||||
child: Text(
|
||||
widget.episode.title!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
softWrap: false,
|
||||
style: textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
children: <Widget>[
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: Text(
|
||||
widget.episode.descriptionText!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
maxLines: 5,
|
||||
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 4.0, 0.0, 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)),
|
||||
),
|
||||
onPressed: widget.episode.downloaded
|
||||
? () {
|
||||
showPlatformDialog<void>(
|
||||
context: context,
|
||||
useRootNavigator: false,
|
||||
builder: (_) => BasicDialogAlert(
|
||||
title: Text(
|
||||
L.of(context)!.delete_episode_title,
|
||||
),
|
||||
content: Text(L.of(context)!.delete_episode_confirmation),
|
||||
actions: <Widget>[
|
||||
BasicDialogAction(
|
||||
title: ActionText(
|
||||
L.of(context)!.cancel_button_label,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
BasicDialogAction(
|
||||
title: ActionText(
|
||||
L.of(context)!.delete_button_label,
|
||||
),
|
||||
iosIsDefaultAction: true,
|
||||
iosIsDestructiveAction: true,
|
||||
onPressed: () {
|
||||
episodeBloc.deleteDownload(widget.episode);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.delete_outline,
|
||||
semanticLabel: L.of(context)!.delete_episode_button_label,
|
||||
size: 22,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 2.0),
|
||||
),
|
||||
ExcludeSemantics(
|
||||
child: Text(
|
||||
L.of(context)!.delete_label,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0)),
|
||||
),
|
||||
onPressed: widget.playing
|
||||
? null
|
||||
: () {
|
||||
if (widget.queued) {
|
||||
queueBloc.queueEvent(QueueRemoveEvent(episode: widget.episode));
|
||||
} else {
|
||||
queueBloc.queueEvent(QueueAddEvent(episode: widget.episode));
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
widget.queued ? Icons.playlist_add_check_outlined : Icons.playlist_add_outlined,
|
||||
semanticLabel: widget.queued
|
||||
? L.of(context)!.semantics_remove_from_queue
|
||||
: L.of(context)!.semantics_add_to_queue,
|
||||
size: 22,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 2.0),
|
||||
),
|
||||
ExcludeSemantics(
|
||||
child: Text(
|
||||
widget.queued ? 'Remove' : 'Add',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0)),
|
||||
),
|
||||
onPressed: () {
|
||||
episodeBloc.togglePlayed(widget.episode);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
widget.episode.played ? Icons.unpublished_outlined : Icons.check_circle_outline,
|
||||
size: 22,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 2.0),
|
||||
),
|
||||
Text(
|
||||
widget.episode.played ? L.of(context)!.mark_unplayed_label : L.of(context)!.mark_played_label,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0.0)),
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet<void>(
|
||||
barrierLabel: L.of(context)!.scrim_episode_details_selector,
|
||||
context: context,
|
||||
backgroundColor: theme.bottomAppBarTheme.color,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(10.0),
|
||||
topRight: Radius.circular(10.0),
|
||||
),
|
||||
),
|
||||
builder: (context) {
|
||||
return EpisodeDetails(
|
||||
episode: widget.episode,
|
||||
);
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
Icons.unfold_more_outlined,
|
||||
size: 22,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 2.0),
|
||||
),
|
||||
Text(
|
||||
L.of(context)!.more_label,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// This is an accessible version of the episode tile that uses Apple theming.
|
||||
/// When the tile is tapped, an iOS menu will appear with the relevant options.
|
||||
class _CupertinoAccessibleEpisodeTile extends StatefulWidget {
|
||||
final Episode episode;
|
||||
final bool download;
|
||||
final bool play;
|
||||
final bool playing;
|
||||
final bool queued;
|
||||
|
||||
const _CupertinoAccessibleEpisodeTile({
|
||||
required this.episode,
|
||||
required this.download,
|
||||
required this.play,
|
||||
this.playing = false,
|
||||
this.queued = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_CupertinoAccessibleEpisodeTile> createState() => _CupertinoAccessibleEpisodeTileState();
|
||||
}
|
||||
|
||||
class _CupertinoAccessibleEpisodeTileState extends State<_CupertinoAccessibleEpisodeTile> {
|
||||
bool expanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final audioBloc = Provider.of<AudioBloc>(context, listen: false);
|
||||
final episodeBloc = Provider.of<EpisodeBloc>(context);
|
||||
final podcastBloc = Provider.of<PodcastBloc>(context);
|
||||
final queueBloc = Provider.of<QueueBloc>(context);
|
||||
|
||||
return StreamBuilder<_PlayerControlState>(
|
||||
stream: Rx.combineLatest2(audioBloc.playingState!, audioBloc.nowPlaying!,
|
||||
(AudioState audioState, Episode? episode) => _PlayerControlState(audioState, episode)),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
final audioState = snapshot.data!.audioState;
|
||||
final nowPlaying = snapshot.data!.episode;
|
||||
final currentlyPlaying = nowPlaying?.guid == widget.episode.guid && audioState == AudioState.playing;
|
||||
final currentlyPaused = nowPlaying?.guid == widget.episode.guid && audioState == AudioState.pausing;
|
||||
|
||||
return Semantics(
|
||||
button: true,
|
||||
child: ListTile(
|
||||
key: Key('PT${widget.episode.guid}'),
|
||||
leading: ExcludeSemantics(
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomLeft,
|
||||
fit: StackFit.passthrough,
|
||||
children: <Widget>[
|
||||
Opacity(
|
||||
opacity: widget.episode.played ? 0.5 : 1.0,
|
||||
child: TileImage(
|
||||
url: widget.episode.thumbImageUrl ?? widget.episode.imageUrl!,
|
||||
size: 56.0,
|
||||
highlight: widget.episode.highlight,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 5.0,
|
||||
width: 56.0 * (widget.episode.percentagePlayed / 100),
|
||||
child: Container(
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
subtitle: Opacity(
|
||||
opacity: widget.episode.played ? 0.5 : 1.0,
|
||||
child: EpisodeSubtitle(widget.episode),
|
||||
),
|
||||
title: Opacity(
|
||||
opacity: widget.episode.played ? 0.5 : 1.0,
|
||||
child: Text(
|
||||
widget.episode.title!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
softWrap: false,
|
||||
style: textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
actions: <Widget>[
|
||||
if (currentlyPlaying)
|
||||
CupertinoActionSheetAction(
|
||||
isDefaultAction: true,
|
||||
onPressed: () {
|
||||
audioBloc.transitionState(TransitionState.pause);
|
||||
Navigator.pop(context, 'Cancel');
|
||||
},
|
||||
child: Text(L.of(context)!.pause_button_label),
|
||||
),
|
||||
if (currentlyPaused)
|
||||
CupertinoActionSheetAction(
|
||||
isDefaultAction: true,
|
||||
onPressed: () {
|
||||
audioBloc.transitionState(TransitionState.play);
|
||||
Navigator.pop(context, 'Cancel');
|
||||
},
|
||||
child: Text(L.of(context)!.resume_button_label),
|
||||
),
|
||||
if (!currentlyPlaying && !currentlyPaused)
|
||||
CupertinoActionSheetAction(
|
||||
isDefaultAction: true,
|
||||
onPressed: () {
|
||||
final settings = Provider.of<SettingsBloc>(context, listen: false).currentSettings;
|
||||
audioBloc.play(widget.episode);
|
||||
optionalShowNowPlaying(context, settings);
|
||||
Navigator.pop(context, 'Cancel');
|
||||
},
|
||||
child: widget.episode.downloaded
|
||||
? Text(L.of(context)!.play_download_button_label)
|
||||
: Text(L.of(context)!.play_button_label),
|
||||
),
|
||||
if (widget.episode.downloadState == DownloadState.queued ||
|
||||
widget.episode.downloadState == DownloadState.downloading)
|
||||
CupertinoActionSheetAction(
|
||||
isDefaultAction: false,
|
||||
onPressed: () {
|
||||
episodeBloc.deleteDownload(widget.episode);
|
||||
Navigator.pop(context, 'Cancel');
|
||||
},
|
||||
child: Text(L.of(context)!.cancel_download_button_label),
|
||||
),
|
||||
if (widget.episode.downloadState != DownloadState.downloading)
|
||||
CupertinoActionSheetAction(
|
||||
isDefaultAction: false,
|
||||
onPressed: () {
|
||||
if (widget.episode.downloaded) {
|
||||
episodeBloc.deleteDownload(widget.episode);
|
||||
Navigator.pop(context, 'Cancel');
|
||||
} else {
|
||||
podcastBloc.downloadEpisode(widget.episode);
|
||||
Navigator.pop(context, 'Cancel');
|
||||
}
|
||||
},
|
||||
child: widget.episode.downloaded
|
||||
? Text(L.of(context)!.delete_episode_button_label)
|
||||
: Text(L.of(context)!.download_episode_button_label),
|
||||
),
|
||||
if (!currentlyPlaying && !widget.queued)
|
||||
CupertinoActionSheetAction(
|
||||
isDefaultAction: false,
|
||||
onPressed: () {
|
||||
queueBloc.queueEvent(QueueAddEvent(episode: widget.episode));
|
||||
Navigator.pop(context, 'Cancel');
|
||||
},
|
||||
child: Text(L.of(context)!.semantics_add_to_queue),
|
||||
),
|
||||
if (!currentlyPlaying && widget.queued)
|
||||
CupertinoActionSheetAction(
|
||||
isDefaultAction: false,
|
||||
onPressed: () {
|
||||
queueBloc.queueEvent(QueueRemoveEvent(episode: widget.episode));
|
||||
Navigator.pop(context, 'Cancel');
|
||||
},
|
||||
child: Text(L.of(context)!.semantics_remove_from_queue),
|
||||
),
|
||||
if (widget.episode.played)
|
||||
CupertinoActionSheetAction(
|
||||
isDefaultAction: false,
|
||||
onPressed: () {
|
||||
episodeBloc.togglePlayed(widget.episode);
|
||||
Navigator.pop(context, 'Cancel');
|
||||
},
|
||||
child: Text(L.of(context)!.semantics_mark_episode_unplayed),
|
||||
),
|
||||
if (!widget.episode.played)
|
||||
CupertinoActionSheetAction(
|
||||
isDefaultAction: false,
|
||||
onPressed: () {
|
||||
episodeBloc.togglePlayed(widget.episode);
|
||||
Navigator.pop(context, 'Cancel');
|
||||
},
|
||||
child: Text(L.of(context)!.semantics_mark_episode_played),
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
isDefaultAction: false,
|
||||
onPressed: () {
|
||||
Navigator.pop(context, 'Cancel');
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
barrierLabel: L.of(context)!.scrim_episode_details_selector,
|
||||
backgroundColor: theme.bottomAppBarTheme.color,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(10.0),
|
||||
topRight: Radius.circular(10.0),
|
||||
),
|
||||
),
|
||||
builder: (context) {
|
||||
return EpisodeDetails(
|
||||
episode: widget.episode,
|
||||
);
|
||||
});
|
||||
},
|
||||
child: Text(L.of(context)!.episode_details_button_label),
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
isDefaultAction: false,
|
||||
onPressed: () {
|
||||
Navigator.pop(context, 'Close');
|
||||
},
|
||||
child: Text(L.of(context)!.close_button_label),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// This is an accessible version of the episode tile that uses Android theming.
|
||||
/// When the tile is tapped, an Android dialog menu will appear with the relevant
|
||||
/// options.
|
||||
class _AccessibleEpisodeTile extends StatefulWidget {
|
||||
final Episode episode;
|
||||
final bool download;
|
||||
final bool play;
|
||||
final bool playing;
|
||||
final bool queued;
|
||||
|
||||
const _AccessibleEpisodeTile({
|
||||
required this.episode,
|
||||
required this.download,
|
||||
required this.play,
|
||||
this.playing = false,
|
||||
this.queued = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_AccessibleEpisodeTile> createState() => _AccessibleEpisodeTileState();
|
||||
}
|
||||
|
||||
class _AccessibleEpisodeTileState extends State<_AccessibleEpisodeTile> {
|
||||
bool expanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final audioBloc = Provider.of<AudioBloc>(context, listen: false);
|
||||
final episodeBloc = Provider.of<EpisodeBloc>(context);
|
||||
final podcastBloc = Provider.of<PodcastBloc>(context);
|
||||
final queueBloc = Provider.of<QueueBloc>(context);
|
||||
|
||||
return StreamBuilder<_PlayerControlState>(
|
||||
stream: Rx.combineLatest2(audioBloc.playingState!, audioBloc.nowPlaying!,
|
||||
(AudioState audioState, Episode? episode) => _PlayerControlState(audioState, episode)),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
final audioState = snapshot.data!.audioState;
|
||||
final nowPlaying = snapshot.data!.episode;
|
||||
final currentlyPlaying = nowPlaying?.guid == widget.episode.guid && audioState == AudioState.playing;
|
||||
final currentlyPaused = nowPlaying?.guid == widget.episode.guid && audioState == AudioState.pausing;
|
||||
|
||||
return ListTile(
|
||||
key: Key('PT${widget.episode.guid}'),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Semantics(
|
||||
header: true,
|
||||
child: SimpleDialog(
|
||||
//TODO: Fix this - should not be hardcoded text
|
||||
title: const Text('Episode Actions'),
|
||||
children: <Widget>[
|
||||
if (currentlyPlaying)
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
audioBloc.transitionState(TransitionState.pause);
|
||||
Navigator.pop(context, '');
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||
child: Text(L.of(context)!.pause_button_label),
|
||||
),
|
||||
if (currentlyPaused)
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
audioBloc.transitionState(TransitionState.play);
|
||||
Navigator.pop(context, '');
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||
child: Text(L.of(context)!.resume_button_label),
|
||||
),
|
||||
if (!currentlyPlaying && !currentlyPaused && widget.episode.downloaded)
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
final settings = Provider.of<SettingsBloc>(context, listen: false).currentSettings;
|
||||
audioBloc.play(widget.episode);
|
||||
optionalShowNowPlaying(context, settings);
|
||||
Navigator.pop(context, '');
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||
child: Text(L.of(context)!.play_download_button_label),
|
||||
),
|
||||
if (!currentlyPlaying && !currentlyPaused && !widget.episode.downloaded)
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
final settings = Provider.of<SettingsBloc>(context, listen: false).currentSettings;
|
||||
audioBloc.play(widget.episode);
|
||||
optionalShowNowPlaying(context, settings);
|
||||
Navigator.pop(context, '');
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||
child: Text(L.of(context)!.play_button_label),
|
||||
),
|
||||
if (widget.episode.downloadState == DownloadState.queued ||
|
||||
widget.episode.downloadState == DownloadState.downloading)
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
episodeBloc.deleteDownload(widget.episode);
|
||||
Navigator.pop(context, '');
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||
child: Text(L.of(context)!.cancel_download_button_label),
|
||||
),
|
||||
if (widget.episode.downloadState != DownloadState.downloading && widget.episode.downloaded)
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
episodeBloc.deleteDownload(widget.episode);
|
||||
Navigator.pop(context, '');
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||
child: Text(L.of(context)!.delete_episode_button_label),
|
||||
),
|
||||
if (widget.episode.downloadState != DownloadState.downloading && !widget.episode.downloaded)
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
podcastBloc.downloadEpisode(widget.episode);
|
||||
Navigator.pop(context, '');
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||
child: Text(L.of(context)!.download_episode_button_label),
|
||||
),
|
||||
if (!currentlyPlaying && !widget.queued)
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
queueBloc.queueEvent(QueueAddEvent(episode: widget.episode));
|
||||
Navigator.pop(context, '');
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||
child: Text(L.of(context)!.semantics_add_to_queue),
|
||||
),
|
||||
if (!currentlyPlaying && widget.queued)
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
queueBloc.queueEvent(QueueRemoveEvent(episode: widget.episode));
|
||||
Navigator.pop(context, '');
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||
child: Text(L.of(context)!.semantics_remove_from_queue),
|
||||
),
|
||||
if (widget.episode.played)
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
episodeBloc.togglePlayed(widget.episode);
|
||||
Navigator.pop(context, '');
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||
child: Text(L.of(context)!.semantics_mark_episode_unplayed),
|
||||
),
|
||||
if (!widget.episode.played)
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
episodeBloc.togglePlayed(widget.episode);
|
||||
Navigator.pop(context, '');
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||
child: Text(L.of(context)!.semantics_mark_episode_played),
|
||||
),
|
||||
SimpleDialogOption(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, '');
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
barrierLabel: L.of(context)!.scrim_episode_details_selector,
|
||||
backgroundColor: theme.bottomAppBarTheme.color,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(10.0),
|
||||
topRight: Radius.circular(10.0),
|
||||
),
|
||||
),
|
||||
builder: (context) {
|
||||
return EpisodeDetails(
|
||||
episode: widget.episode,
|
||||
);
|
||||
});
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||
child: Text(L.of(context)!.episode_details_button_label),
|
||||
),
|
||||
SimpleDialogOption(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||
// child: Text(L.of(context)!.close_button_label),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
child: ActionText(L.of(context)!.close_button_label),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, '');
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
leading: ExcludeSemantics(
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomLeft,
|
||||
fit: StackFit.passthrough,
|
||||
children: <Widget>[
|
||||
Opacity(
|
||||
opacity: widget.episode.played ? 0.5 : 1.0,
|
||||
child: TileImage(
|
||||
url: widget.episode.thumbImageUrl ?? widget.episode.imageUrl!,
|
||||
size: 56.0,
|
||||
highlight: widget.episode.highlight,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 5.0,
|
||||
width: 56.0 * (widget.episode.percentagePlayed / 100),
|
||||
child: Container(
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
subtitle: Opacity(
|
||||
opacity: widget.episode.played ? 0.5 : 1.0,
|
||||
child: EpisodeSubtitle(widget.episode),
|
||||
),
|
||||
title: Opacity(
|
||||
opacity: widget.episode.played ? 0.5 : 1.0,
|
||||
child: Text(
|
||||
widget.episode.title!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 2,
|
||||
softWrap: false,
|
||||
style: textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class EpisodeTransportControls extends StatelessWidget {
|
||||
final Episode episode;
|
||||
final bool download;
|
||||
final bool play;
|
||||
|
||||
const EpisodeTransportControls({
|
||||
super.key,
|
||||
required this.episode,
|
||||
required this.download,
|
||||
required this.play,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final buttons = <Widget>[];
|
||||
|
||||
if (download) {
|
||||
buttons.add(Semantics(
|
||||
container: true,
|
||||
child: DownloadControl(
|
||||
episode: episode,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if (play) {
|
||||
buttons.add(Semantics(
|
||||
container: true,
|
||||
child: PlayControl(
|
||||
episode: episode,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: (buttons.length * 48.0),
|
||||
child: Row(
|
||||
children: <Widget>[...buttons],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EpisodeSubtitle extends StatelessWidget {
|
||||
final Episode episode;
|
||||
final String date;
|
||||
final Duration length;
|
||||
|
||||
EpisodeSubtitle(this.episode, {super.key})
|
||||
: date = episode.publicationDate == null
|
||||
? ''
|
||||
: DateFormat(episode.publicationDate!.year == DateTime.now().year ? 'd MMM' : 'd MMM yyyy')
|
||||
.format(episode.publicationDate!),
|
||||
length = Duration(seconds: episode.duration);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
var timeRemaining = episode.timeRemaining;
|
||||
|
||||
String title;
|
||||
|
||||
if (length.inSeconds > 0) {
|
||||
if (length.inSeconds < 60) {
|
||||
title = '$date • ${length.inSeconds} sec';
|
||||
} else {
|
||||
title = '$date • ${length.inMinutes} min';
|
||||
}
|
||||
} else {
|
||||
title = date;
|
||||
}
|
||||
|
||||
if (timeRemaining.inSeconds > 0) {
|
||||
if (timeRemaining.inSeconds < 60) {
|
||||
title = '$title / ${timeRemaining.inSeconds} sec left';
|
||||
} else {
|
||||
title = '$title / ${timeRemaining.inMinutes} min left';
|
||||
}
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: Text(
|
||||
title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
style: textTheme.bodySmall,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// This class acts as a wrapper between the current audio state and
|
||||
/// downloadables. Saves all that nesting of StreamBuilders.
|
||||
class _PlayerControlState {
|
||||
final AudioState audioState;
|
||||
final Episode? episode;
|
||||
|
||||
_PlayerControlState(this.audioState, this.episode);
|
||||
}
|
||||
Reference in New Issue
Block a user