Skip to content

Commit

Permalink
feat(sonarr): re-implementation of multi-select episode actions
Browse files Browse the repository at this point in the history
  • Loading branch information
JagandeepBrar committed May 1, 2023
1 parent 5adaf05 commit 79d1388
Show file tree
Hide file tree
Showing 18 changed files with 374 additions and 42 deletions.
8 changes: 8 additions & 0 deletions assets/localization/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -703,14 +703,17 @@
"sonarr.Episode": "Episode",
"sonarr.Episodes": "Episodes",
"sonarr.EpisodesAvailable": "Episodes Available",
"sonarr.EpisodesCount": "{} Episodes",
"sonarr.EpisodeFileDeleted": "Episode File Deleted",
"sonarr.EpisodeFileRenamed": "Episode File Renamed",
"sonarr.EpisodeFilesDeleted": "Episode Files Deleted",
"sonarr.EpisodeImported": "Episode Imported ({})",
"sonarr.EpisodeNumber": "Episode {}",
"sonarr.FailedToAddSeries": "Failed to Add Series",
"sonarr.FailedToAddTag": "Failed to Add Tag",
"sonarr.FailedToBackupDatabase": "Failed to Backup Database",
"sonarr.FailedToDeleteEpisodeFile": "Failed to Delete Episode File",
"sonarr.FailedToDeleteEpisodeFiles": "Failed to Delete Episode Files",
"sonarr.FailedToDownloadRelease": "Failed to Download Release",
"sonarr.FailedToMonitorEpisode": "Failed to Monitor Episode",
"sonarr.FailedToMonitorSeason": "Failed to Monitor Season",
Expand All @@ -719,6 +722,7 @@
"sonarr.FailedToRemoveFromQueue": "Failed to Remove From Queue",
"sonarr.FailedToRemoveSeries": "Failed to Remove Series",
"sonarr.FailedToRunRSSSync": "Failed to Run RSS Sync",
"sonarr.FailedToSearchForEpisodes": "Failed to Search for Episodes",
"sonarr.FailedToSearchForMonitoredEpisodes": "Failed to Search for Monitored Episodes",
"sonarr.FailedToSeasonSearch": "Failed to Season Search",
"sonarr.FailedToSearch": "Failed to Search",
Expand Down Expand Up @@ -763,6 +767,8 @@
"sonarr.More": "More",
"sonarr.Name": "Name",
"sonarr.NoEpisodesFound": "No Episodes Found",
"sonarr.NoEpisodeFilesFound": "No Episode Files Found",
"sonarr.NoEpisodeFilesFoundDeleteMessage": "No selected episodes have files to delete",
"sonarr.NoHistoryFound": "No History Found",
"sonarr.NoLongerMonitoring": "No Longer Monitoring",
"sonarr.NoMessagesFound": "No Messages Found",
Expand All @@ -772,6 +778,7 @@
"sonarr.NoSeriesFound": "No Series Found",
"sonarr.NoSummaryAvailable": "No Summary Available",
"sonarr.NoTagsFound": "No Tags Found",
"sonarr.OneEpisode": "1 Episode",
"sonarr.OneSeason": "1 Season",
"sonarr.Other": "Other",
"sonarr.Overview": "Overview",
Expand Down Expand Up @@ -808,6 +815,7 @@
"sonarr.Search": "Search",
"sonarr.Searching": "Searching{}",
"sonarr.SearchingForEpisode": "Searching for Episode…",
"sonarr.SearchingForEpisodes": "Searching for Episodes…",
"sonarr.SearchingForSeason": "Searching for Season…",
"sonarr.SearchingForMonitoredEpisodes": "Searching for Monitored Episodes…",
"sonarr.SearchingDescription": "Searching for all missing episodes",
Expand Down
1 change: 1 addition & 0 deletions lib/api/sonarr/controllers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ part 'controllers/command/series_search.dart';
// Episode File
part 'controllers/episode_file.dart';
part 'controllers/episode_file/delete_episode_file.dart';
part 'controllers/episode_file/delete_episode_files.dart';
part 'controllers/episode_file/get_episode_file.dart';
part 'controllers/episode_file/get_series_episode_files.dart';

Expand Down
6 changes: 6 additions & 0 deletions lib/api/sonarr/controllers/episode_file.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ class SonarrControllerEpisodeFile {
}) async =>
_commandDeleteEpisodeFile(_client, episodeFileId: episodeFileId);

/// Delete the given episode files.
Future<void> deleteBulk({
required List<int> episodeFileIds,
}) async =>
_commandDeleteEpisodeFiles(_client, episodeFileIds: episodeFileIds);

/// Handler for [episodefile/{id}](https://github.com/Sonarr/Sonarr/wiki/EpisodeFile#get).
///
/// Returns the episode file with the matching episode ID.
Expand Down
10 changes: 10 additions & 0 deletions lib/api/sonarr/controllers/episode_file/delete_episode_files.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
part of sonarr_commands;

Future<void> _commandDeleteEpisodeFiles(
Dio client, {
required List<int> episodeFileIds,
}) async {
await client.delete('episodefile/bulk', data: {
'episodeFileIds': episodeFileIds,
});
}
88 changes: 88 additions & 0 deletions lib/modules/sonarr/core/api_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,54 @@ class SonarrAPIController {
return false;
}

Future<bool> deleteEpisodes({
required BuildContext context,
required List<int> episodeFileIds,
bool showSnackbar = true,
}) async {
if (episodeFileIds.isEmpty) {
showLunaInfoSnackBar(
title: 'sonarr.NoEpisodeFilesFound'.tr(),
message: 'sonarr.NoEpisodeFilesFoundDeleteMessage'.tr(),
);
return true;
}

if (context.read<SonarrState>().enabled) {
return context
.read<SonarrState>()
.api!
.episodeFile
.deleteBulk(episodeFileIds: episodeFileIds)
.then((response) {
if (showSnackbar) {
showLunaSuccessSnackBar(
title: 'sonarr.EpisodeFilesDeleted'.tr(),
message: episodeFileIds.length > 1
? 'sonarr.EpisodesCount'
.tr(args: [episodeFileIds.length.toString()])
: 'sonarr.OneEpisode'.tr(),
);
}
return true;
}).catchError((error, stack) {
LunaLogger().error(
'Failed to delete episodes (${episodeFileIds.join(',')})',
error,
stack,
);
if (showSnackbar) {
showLunaErrorSnackBar(
title: 'sonarr.FailedToDeleteEpisodeFiles'.tr(),
error: error,
);
}
return false;
});
}
return false;
}

Future<bool> episodeSearch({
required BuildContext context,
required SonarrEpisode episode,
Expand Down Expand Up @@ -163,6 +211,46 @@ class SonarrAPIController {
return false;
}

Future<bool> multiEpisodeSearch({
required BuildContext context,
required List<int> episodeIds,
bool showSnackbar = true,
}) async {
if (context.read<SonarrState>().enabled) {
return context
.read<SonarrState>()
.api!
.command
.episodeSearch(episodeIds: episodeIds)
.then((response) {
if (showSnackbar) {
showLunaSuccessSnackBar(
title: 'sonarr.SearchingForEpisodes'.tr(),
message: episodeIds.length > 1
? 'sonarr.EpisodesCount'
.tr(args: [episodeIds.length.toString()])
: 'sonarr.OneEpisode'.tr(),
);
}
return true;
}).catchError((error, stack) {
LunaLogger().error(
'Failed to search for episode: ${episodeIds.join(',')}',
error,
stack,
);
if (showSnackbar) {
showLunaErrorSnackBar(
title: 'sonarr.FailedToSearchForEpisodes'.tr(),
error: error,
);
}
return false;
});
}
return false;
}

Future<bool> toggleSeasonMonitored({
required BuildContext context,
required SonarrSeriesSeason season,
Expand Down
34 changes: 34 additions & 0 deletions lib/modules/sonarr/core/dialogs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,40 @@ class SonarrDialogs {
return Tuple2(_flag, _value);
}

Future<Tuple2<bool, SonarrEpisodeMultiSettingsType?>> episodeMultiSettings(
BuildContext context,
int episodes,
) async {
bool _flag = false;
SonarrEpisodeMultiSettingsType? _value;

void _setValues(bool flag, SonarrEpisodeMultiSettingsType value) {
_flag = flag;
_value = value;
Navigator.of(context, rootNavigator: true).pop();
}

await LunaDialog.dialog(
context: context,
title: episodes > 1
? 'sonarr.EpisodesCount'.tr(args: [episodes.toString()])
: 'sonarr.OneEpisode'.tr(),
content: List.generate(
SonarrEpisodeMultiSettingsType.values.length,
(idx) => LunaDialog.tile(
text: SonarrEpisodeMultiSettingsType.values[idx].name,
icon: SonarrEpisodeMultiSettingsType.values[idx].icon,
iconColor: LunaColours().byListIndex(idx),
onTap: () {
_setValues(true, SonarrEpisodeMultiSettingsType.values[idx]);
},
),
),
contentPadding: LunaDialog.listDialogContentPadding(),
);
return Tuple2(_flag, _value);
}

static Future<List<dynamic>> setDefaultPage(
BuildContext context, {
required List<String> titles,
Expand Down
1 change: 1 addition & 0 deletions lib/modules/sonarr/core/types.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export 'types/filter_releases.dart';
export 'types/filter_series.dart';
export 'types/monitor_status.dart';
export 'types/settings_episode_multi.dart';
export 'types/settings_episode.dart';
export 'types/settings_global.dart';
export 'types/settings_season.dart';
Expand Down
54 changes: 54 additions & 0 deletions lib/modules/sonarr/core/types/settings_episode_multi.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:lunasea/modules/sonarr.dart';
import 'package:lunasea/vendor.dart';
import 'package:lunasea/widgets/ui.dart';

enum SonarrEpisodeMultiSettingsType {
AUTOMATIC_SEARCH,
DELETE_FILES;

IconData get icon {
switch (this) {
case SonarrEpisodeMultiSettingsType.AUTOMATIC_SEARCH:
return LunaIcons.SEARCH;
case SonarrEpisodeMultiSettingsType.DELETE_FILES:
return LunaIcons.DELETE;
}
}

String get name {
switch (this) {
case SonarrEpisodeMultiSettingsType.AUTOMATIC_SEARCH:
return 'sonarr.AutomaticSearch'.tr();
case SonarrEpisodeMultiSettingsType.DELETE_FILES:
return 'sonarr.DeleteFiles'.tr();
}
}

Future<void> execute(
BuildContext context,
List<SonarrEpisode> episodes,
) async {
switch (this) {
case SonarrEpisodeMultiSettingsType.AUTOMATIC_SEARCH:
final episodeIds = episodes.map((ep) => ep.id!).toList();
await SonarrAPIController().multiEpisodeSearch(
context: context,
episodeIds: episodeIds,
);
break;
case SonarrEpisodeMultiSettingsType.DELETE_FILES:
final episodeIds = episodes
.filter((ep) => ep.episodeFileId != null && ep.episodeFileId != 0)
.map((ep) => ep.episodeFileId!)
.toList();
await SonarrAPIController().deleteEpisodes(
context: context,
episodeFileIds: episodeIds,
);
break;
}

context.read<SonarrSeasonDetailsState>().fetchState(context);
}
}
34 changes: 34 additions & 0 deletions lib/modules/sonarr/routes/season_details/state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,38 @@ class SonarrSeasonDetailsState extends ChangeNotifier {
}
notifyListeners();
}

final Set<int> selectedEpisodes = {};

void toggleSelectedEpisode(SonarrEpisode episode) {
final id = episode.id!;
if (selectedEpisodes.contains(id)) {
selectedEpisodes.remove(id);
} else {
selectedEpisodes.add(id);
}

notifyListeners();
}

void clearSelectedEpisodes() {
selectedEpisodes.clear();
notifyListeners();
}

Future<void> toggleSeasonEpisodes(int seasonNumber) async {
final eps = (await episodes)!
.filter((ep) => ep.value.seasonNumber == seasonNumber)
.map((ep) => ep.value.id!)
.toList();
final allSelected = eps.every(selectedEpisodes.contains);

if (allSelected) {
selectedEpisodes.removeAll(eps);
} else {
selectedEpisodes.addAll(eps);
}

notifyListeners();
}
}
11 changes: 11 additions & 0 deletions lib/modules/sonarr/routes/season_details/widgets/episode_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ class _State extends State<SonarrEpisodeTile> {
trailing: _trailing(),
onTap: _onTap,
onLongPress: _onLongPress,
backgroundColor: context
.read<SonarrSeasonDetailsState>()
.selectedEpisodes
.contains(widget.episode.id)
? LunaColours.accent.selected()
: null,
);
}

Expand Down Expand Up @@ -83,6 +89,11 @@ class _State extends State<SonarrEpisodeTile> {
return LunaIconButton(
text: widget.episode.episodeNumber.toString(),
textSize: LunaUI.FONT_SIZE_H4,
onPressed: () {
context
.read<SonarrSeasonDetailsState>()
.toggleSelectedEpisode(widget.episode);
},
);
}

Expand Down
Loading

0 comments on commit 79d1388

Please sign in to comment.