Skip to content

Commit

Permalink
feat: mark podcast episodes as played (#1125)
Browse files Browse the repository at this point in the history
Fixes #1117
  • Loading branch information
Feichtmeier authored Jan 20, 2025
1 parent ad7aa2d commit 3b80a25
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 18 deletions.
5 changes: 5 additions & 0 deletions lib/common/view/icons.dart
Original file line number Diff line number Diff line change
Expand Up @@ -462,4 +462,9 @@ class Iconz {
: appleStyled
? CupertinoIcons.moon
: Icons.mode_night_rounded;
static IconData get markAllRead => yaruStyled
? YaruIcons.ok_filled
: appleStyled
? CupertinoIcons.check_mark_circled_solid
: Icons.check_circle;
}
1 change: 1 addition & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
}
}
},
"markAllEpisodesAsDone": "Mark all episodes as done",
"downloadsOnly": "Downloads only",
"downloadsDirectory": "Location of your downloads",
"downloadsDirectoryDescription": "Make sure MusicPod can access this directory!",
Expand Down
67 changes: 64 additions & 3 deletions lib/persistence_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,33 @@ Future<void> writeCustomSetting(
final file = File(p.join(workingDir, filename));

if (!file.existsSync()) {
file.create();
file.createSync();
}

await file.writeAsString(jsonStr);
}

Future<void> writeCustomSettings({
required List<MapEntry<String, dynamic>> entries,
required String filename,
}) async {
if (entries.isEmpty) return;
final oldSettings = await getCustomSettings(filename);

for (var entry in entries) {
if (oldSettings.containsKey(entry.key)) {
oldSettings.update(entry.key, (v) => entry.value);
} else {
oldSettings.putIfAbsent(entry.key, () => entry.value);
}
}

final jsonStr = jsonEncode(oldSettings);
final workingDir = await getWorkingDir();
final file = File(p.join(workingDir, filename));

if (!file.existsSync()) {
await file.create();
}

await file.writeAsString(jsonStr);
Expand All @@ -103,12 +129,33 @@ Future<void> removeCustomSetting(
final file = File(p.join(workingDir, filename));

if (!file.existsSync()) {
file.create();
await file.create();
}
await file.writeAsString(jsonStr);
}
}

Future<void> removeCustomSettings(
List<String> keys, [
String filename = kSettingsFileName,
]) async {
final oldSettings = await getCustomSettings(filename);
for (var key in keys) {
if (oldSettings.containsKey(key)) {
oldSettings.remove(key);
}
}

final jsonStr = jsonEncode(oldSettings);
final workingDir = await getWorkingDir();
final file = File(p.join(workingDir, filename));

if (!file.existsSync()) {
await file.create();
}
await file.writeAsString(jsonStr);
}

Future<dynamic> readCustomSetting(
dynamic key, [
String filename = kSettingsFileName,
Expand Down Expand Up @@ -143,14 +190,28 @@ Future<Map<String, String>> getCustomSettings([
}
}

Future<void> wipeCustomSettings([
String filename = kSettingsFileName,
]) async {
final workingDir = await getWorkingDir();

final file = File(p.join(workingDir, filename));

if (!file.existsSync()) {
file.createSync();
}

await file.writeAsString('{}');
}

Future<void> writeStringIterable({
required Iterable<String> iterable,
required String filename,
}) async {
final workingDir = await getWorkingDir();
final file = File('$workingDir/$filename');
if (!file.existsSync()) {
file.create();
await file.create();
}
await file.writeAsString(iterable.join('\n'));
}
Expand Down
4 changes: 4 additions & 0 deletions lib/player/player_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ class PlayerModel extends SafeChangeNotifier {
Map<String, Duration>? get lastPositions => _playerService.lastPositions;
Duration? getLastPosition(String? url) => _playerService.getLastPosition(url);
Future<void> safeLastPosition() => _playerService.safeLastPosition();

Future<void> safeAllLastPositions(List<Audio> audios) =>
_playerService.safeAllLastPositions(audios);

Future<void> removeLastPosition(String key) =>
_playerService.removeLastPosition(key);
Future<void> removeLastPositions(List<Audio> audios) =>
Expand Down
43 changes: 33 additions & 10 deletions lib/player/player_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -441,9 +441,7 @@ class PlayerService {
Future<void> _setPlayerState() async {
final playerState = await _readPlayerState();

_lastPositions = (await getCustomSettings(kLastPositionsFileName)).map(
(key, value) => MapEntry(key, value.parsedDuration ?? Duration.zero),
);
await _loadLastPositions();

if (playerState?.audio != null) {
_setAudio(playerState!.audio!);
Expand Down Expand Up @@ -475,6 +473,12 @@ class PlayerService {
}
}

Future<void> _loadLastPositions() async {
_lastPositions = (await getCustomSettings(kLastPositionsFileName)).map(
(key, value) => MapEntry(key, value.parsedDuration ?? Duration.zero),
);
}

//
// Last Positions used when the app re-opens and for podcasts
//
Expand All @@ -497,20 +501,39 @@ class PlayerService {
_propertiesChangedController.add(true);
}

Future<void> safeAllLastPositions(List<Audio> audios) async {
await writeCustomSettings(
entries: audios
.where((e) => e.url != null && e.durationMs != null)
.map(
(e) => MapEntry(
e.url!,
Duration(milliseconds: e.durationMs!.toInt()).toString(),
),
)
.toList(),
filename: kLastPositionsFileName,
);
await _loadLastPositions();

_propertiesChangedController.add(true);
}

Future<void> removeLastPosition(String key) async {
await removeCustomSetting(key, kLastPositionsFileName);
_lastPositions.remove(key);
_propertiesChangedController.add(true);
}

Future<void> removeLastPositions(List<Audio> audios) async {
for (var e in audios) {
if (e.url != null) {
await removeCustomSetting(e.url!, kLastPositionsFileName);
_lastPositions.remove(e.url!);
_propertiesChangedController.add(true);
}
}
await removeCustomSettings(
audios.where((e) => e.url != null).map((e) => e.url!).toList(),
kLastPositionsFileName,
);

await _loadLastPositions();

_propertiesChangedController.add(true);
}

Duration? getLastPosition(String? url) => _lastPositions[url];
Expand Down
2 changes: 2 additions & 0 deletions lib/player/view/bottom_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import 'bottom_player_image.dart';
import 'play_button.dart';
import 'playback_rate_button.dart';
import 'player_main_controls.dart';
import 'player_pause_timer_button.dart';
import 'player_title_and_artist.dart';
import 'player_track.dart';
import 'player_view.dart';
Expand Down Expand Up @@ -103,6 +104,7 @@ class BottomPlayer extends StatelessWidget with WatchItMixin {
if (audio?.audioType == AudioType.podcast)
PlaybackRateButton(active: active),
if (!isMobilePlatform) const VolumeSliderPopup(),
const PlayerPauseTimerButton(),
const QueueButton(
isSelected: false,
),
Expand Down
4 changes: 4 additions & 0 deletions lib/player/view/full_height_player_top_controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import '../../library/library_model.dart';
import '../../player/player_model.dart';
import '../../search/search_model.dart';
import 'playback_rate_button.dart';
import 'player_pause_timer_button.dart';
import 'player_view.dart';
import 'queue/queue_button.dart';
import 'volume_popup.dart';
Expand Down Expand Up @@ -88,6 +89,9 @@ class FullHeightPlayerTopControls extends StatelessWidget with WatchItMixin {
_ => const SizedBox.shrink(),
},
if (showQueueButton) QueueButton(color: iconColor),
PlayerPauseTimerButton(
iconColor: iconColor,
),
ShareButton(
audio: audio,
active: active,
Expand Down
8 changes: 5 additions & 3 deletions lib/player/view/player_main_controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import '../../extensions/theme_data_x.dart';
import '../../l10n/l10n.dart';
import '../player_model.dart';
import 'play_button.dart';
import 'player_pause_timer_button.dart';
import 'repeat_button.dart';
import 'seek_button.dart';
import 'shuffle_button.dart';
Expand Down Expand Up @@ -117,8 +116,11 @@ class PlayerMainControls extends StatelessWidget with WatchItMixin {
active: active,
iconColor: defaultColor,
),
AudioType.radio => PlayerPauseTimerButton(
iconColor: defaultColor,
AudioType.radio => SizedBox.square(
dimension: context.theme.iconButtonTheme.style?.shape
?.resolve({})
?.dimensions
.horizontal,
),
_ => const SizedBox.shrink(),
},
Expand Down
12 changes: 10 additions & 2 deletions lib/podcasts/view/podcast_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import '../../extensions/build_context_x.dart';
import '../../l10n/l10n.dart';
import '../../library/library_model.dart';
import '../../player/player_model.dart';
import '../../player/view/player_pause_timer_button.dart';
import '../../search/search_model.dart';
import '../../search/search_type.dart';
import '../../settings/settings_model.dart';
Expand Down Expand Up @@ -168,7 +167,16 @@ class _PodcastPageState extends State<PodcastPage> {
children: [
if (!isMobilePlatform)
PodcastReplayButton(audios: episodesWithDownloads),
const PlayerPauseTimerButton(),
IconButton(
tooltip: l10n.markAllEpisodesAsDone,
onPressed: () {
di<PlayerModel>()
.safeAllLastPositions(episodesWithDownloads);
di<LibraryModel>()
.removePodcastUpdate(widget.feedUrl);
},
icon: Icon(Iconz.markAllRead),
),
PodcastSubButton(
audios: episodesWithDownloads,
pageId: widget.feedUrl,
Expand Down
Loading

0 comments on commit 3b80a25

Please sign in to comment.