Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mark podcast episodes as played #1125

Merged
merged 1 commit into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading