From db4dfdaf9ca1976d1a6e4b0ebc451b2e0f08b19e Mon Sep 17 00:00:00 2001 From: Jagandeep Brar Date: Sun, 20 Feb 2022 12:37:37 -0500 Subject: [PATCH] feat(overseerr): issues (de)serialization & list integration --- assets/localization/en.json | 12 +- build.yaml | 7 ++ lib/core/models/logs/log_type.dart | 2 +- lib/core/ui/icons/icon.dart | 4 + lib/core/utilities.dart | 1 + lib/core/utilities/parser.dart | 13 ++ lib/modules/overseerr/api/models.dart | 3 + .../overseerr/api/models/issues/issue.dart | 71 +++++++++++ .../api/models/issues/issue_comment.dart | 47 ++++++++ .../api/models/issues/issue_page.dart | 29 +++++ .../overseerr/api/models/media/media.dart | 16 +-- .../overseerr/api/models/media/season.dart | 8 +- .../api/models/requests/request.dart | 8 +- .../overseerr/api/models/users/user.dart | 9 +- lib/modules/overseerr/api/overseerr.dart | 8 ++ lib/modules/overseerr/api/types.dart | 4 + .../api/types/issue_filter_type.dart | 28 +++++ .../overseerr/api/types/issue_sort_type.dart | 23 ++++ .../overseerr/api/types/issue_status.dart | 42 +++++++ .../overseerr/api/types/issue_type.dart | 46 +++++++ lib/modules/overseerr/api/utilities.dart | 32 +++-- lib/modules/overseerr/core/extensions.dart | 1 + .../core/extensions/overseerr_issue.dart | 21 ++++ lib/modules/overseerr/routes.dart | 1 + lib/modules/overseerr/routes/issues.dart | 2 + .../overseerr/routes/issues/route.dart | 31 +++++ .../overseerr/routes/issues/widgets.dart | 2 + .../issues/widgets/issue_list_view.dart | 68 +++++++++++ .../routes/issues/widgets/issue_tile.dart | 112 ++++++++++++++++++ .../overseerr/routes/overseerr/route.dart | 3 +- .../overseerr/widgets/navigation_bar.dart | 10 +- .../requests/widgets/request_list_view.dart | 2 +- lib/modules/sonarr/core/dialogs.dart | 2 +- localization/overseerr/en.json | 14 ++- 34 files changed, 638 insertions(+), 44 deletions(-) create mode 100644 build.yaml create mode 100644 lib/core/utilities/parser.dart create mode 100644 lib/modules/overseerr/api/models/issues/issue.dart create mode 100644 lib/modules/overseerr/api/models/issues/issue_comment.dart create mode 100644 lib/modules/overseerr/api/models/issues/issue_page.dart create mode 100644 lib/modules/overseerr/api/types/issue_filter_type.dart create mode 100644 lib/modules/overseerr/api/types/issue_sort_type.dart create mode 100644 lib/modules/overseerr/api/types/issue_status.dart create mode 100644 lib/modules/overseerr/api/types/issue_type.dart create mode 100644 lib/modules/overseerr/core/extensions/overseerr_issue.dart create mode 100644 lib/modules/overseerr/routes/issues.dart create mode 100644 lib/modules/overseerr/routes/issues/route.dart create mode 100644 lib/modules/overseerr/routes/issues/widgets.dart create mode 100644 lib/modules/overseerr/routes/issues/widgets/issue_list_view.dart create mode 100644 lib/modules/overseerr/routes/issues/widgets/issue_tile.dart diff --git a/assets/localization/en.json b/assets/localization/en.json index 671f6632..a985aa28 100644 --- a/assets/localization/en.json +++ b/assets/localization/en.json @@ -509,22 +509,32 @@ "tautulli.Year": "Year", "lidarr.StartSearchFor": "Start Search For…", "lidarr.StartSearchForMissingAlbums": "Start search for missing albums", + "overseerr.Audio": "Audio", "overseerr.Approved": "Approved", "overseerr.Available": "Available", "overseerr.Declined": "Declined", + "overseerr.Issues": "Issues", + "overseerr.NoIssues": "No Issues", + "overseerr.NoIssuesFound": "No Issues Found", "overseerr.NoRequests": "No Requests", + "overseerr.NoRequestsFound": "No Requests Found", "overseerr.OneRequest": "1 Request", + "overseerr.Open": "Open", + "overseerr.OpenedBy": "Opened by {}", + "overseerr.Other": "Other", "overseerr.PartiallyAvailable": "Partially Available", "overseerr.Pending": "Pending", "overseerr.Processing": "Processing", "overseerr.SomeRequests": "{} Requests", + "overseerr.Resolved": "Resolved", "overseerr.Requested": "Requested", "overseerr.RequestedBy": "Requested by {}", "overseerr.Requests": "Requests", - "overseerr.NoRequestsFound": "No Requests Found", + "overseerr.Subtitle": "Subtitle", "overseerr.NoUsersFound": "No Users Found", "overseerr.UnknownUser": "Unknown User", "overseerr.Users": "Users", + "overseerr.Video": "Video", "sonarr.AddedOn": "Added On", "sonarr.AddedSeries": "Added Series", "sonarr.AddedTag": "Added Tag", diff --git a/build.yaml b/build.yaml new file mode 100644 index 00000000..c30e6f37 --- /dev/null +++ b/build.yaml @@ -0,0 +1,7 @@ +targets: + $default: + builders: + json_serializable: + options: + explicit_to_json: true + include_if_null: false diff --git a/lib/core/models/logs/log_type.dart b/lib/core/models/logs/log_type.dart index eb122df6..fcbd5f6e 100644 --- a/lib/core/models/logs/log_type.dart +++ b/lib/core/models/logs/log_type.dart @@ -74,7 +74,7 @@ extension LunaLogTypeExtension on LunaLogType { IconData get icon { switch (this) { case LunaLogType.WARNING: - return Icons.warning_rounded; + return LunaIcons.WARNING; case LunaLogType.ERROR: return Icons.report_rounded; case LunaLogType.CRITICAL: diff --git a/lib/core/ui/icons/icon.dart b/lib/core/ui/icons/icon.dart index a0749da1..1aa44ccd 100644 --- a/lib/core/ui/icons/icon.dart +++ b/lib/core/ui/icons/icon.dart @@ -13,6 +13,7 @@ class LunaIcons { static const IconData DOWNLOAD = Icons.file_download_rounded; static const IconData ERROR = Icons.error_rounded; static const IconData FILTER = Icons.filter_list_rounded; + static const IconData HISTORY = Icons.history_rounded; static const IconData HOME = Icons.home_rounded; static const IconData MONITOR_OFF = Icons.turned_in_not_rounded; static const IconData MONITOR_ON = Icons.turned_in_rounded; @@ -21,13 +22,16 @@ class LunaIcons { static const IconData PROFILES = Icons.switch_account_rounded; static const IconData REFRESH = Icons.refresh_rounded; static const IconData RENAME = Icons.text_format_rounded; + static const IconData REQUESTS = Icons.access_time_rounded; static const IconData SEARCH = Icons.search_rounded; static const IconData SETTINGS = Icons.settings_rounded; static const IconData SHUFFLE = Icons.shuffle_rounded; static const IconData SORT = Icons.sort_rounded; static const IconData TRANSLATE = Icons.translate_rounded; static const IconData USER = Icons.person_rounded; + static const IconData USERS = Icons.people_rounded; static const IconData VIDEO_CAM = Icons.videocam_rounded; static const IconData VIEW = Icons.grid_view_rounded; + static const IconData WARNING = Icons.warning_amber_rounded; static const IconData WATCHED = Icons.visibility_rounded; } diff --git a/lib/core/utilities.dart b/lib/core/utilities.dart index 34776652..97129ef5 100644 --- a/lib/core/utilities.dart +++ b/lib/core/utilities.dart @@ -2,5 +2,6 @@ export 'utilities/changelog.dart'; export 'utilities/encryption.dart'; export 'utilities/helpers.dart'; export 'utilities/logger.dart'; +export 'utilities/parser.dart'; export 'utilities/uuid.dart'; export 'utilities/validator.dart'; diff --git a/lib/core/utilities/parser.dart b/lib/core/utilities/parser.dart new file mode 100644 index 00000000..b5b74722 --- /dev/null +++ b/lib/core/utilities/parser.dart @@ -0,0 +1,13 @@ +abstract class LunaParser { + /// Parse any [String] to a [DateTime] object if possible. + static DateTime? dateTimeFromString(String? date) { + return DateTime.tryParse(date ?? ''); + } + + /// Take a [DateTime] object and convert it to an ISO 8601 [String] representation. + /// + /// The format is `yyyy-MM-ddTHH:mm:ss.mmmuuuZ`. + static String? dateTimeToISO8601(DateTime? date) { + return date?.toIso8601String(); + } +} diff --git a/lib/modules/overseerr/api/models.dart b/lib/modules/overseerr/api/models.dart index 095640d9..d1c2c164 100644 --- a/lib/modules/overseerr/api/models.dart +++ b/lib/modules/overseerr/api/models.dart @@ -16,6 +16,9 @@ export 'models/content/season.dart'; export 'models/content/series.dart'; export 'models/content/spoken_language.dart'; export 'models/content/watch_provider.dart'; +export 'models/issues/issue_comment.dart'; +export 'models/issues/issue_page.dart'; +export 'models/issues/issue.dart'; export 'models/media/media.dart'; export 'models/media/season.dart'; export 'models/requests/request_count.dart'; diff --git a/lib/modules/overseerr/api/models/issues/issue.dart b/lib/modules/overseerr/api/models/issues/issue.dart new file mode 100644 index 00000000..63baac70 --- /dev/null +++ b/lib/modules/overseerr/api/models/issues/issue.dart @@ -0,0 +1,71 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/overseerr.dart'; + +part 'issue.g.dart'; + +@JsonSerializable() +class OverseerrIssue { + @JsonKey() + int id; + + @JsonKey() + OverseerrIssueType? issueType; + + @JsonKey() + OverseerrIssueStatus? status; + + @JsonKey() + int? problemSeason; + + @JsonKey() + int? problemEpisode; + + @JsonKey( + fromJson: LunaParser.dateTimeFromString, + toJson: LunaParser.dateTimeToISO8601, + ) + DateTime? createdAt; + + @JsonKey( + fromJson: LunaParser.dateTimeFromString, + toJson: LunaParser.dateTimeToISO8601, + ) + DateTime? updatedAt; + + @JsonKey() + OverseerrMedia? media; + + @JsonKey() + OverseerrUser? createdBy; + + @JsonKey() + OverseerrUser? modifiedBy; + + @JsonKey() + List? comments; + + OverseerrIssue({ + required this.id, + this.issueType, + this.status, + this.problemSeason, + this.problemEpisode, + this.createdAt, + this.updatedAt, + this.media, + this.createdBy, + this.modifiedBy, + this.comments, + }); + + /// Returns a JSON-encoded string version of this object. + @override + String toString() => json.encode(this.toJson()); + + /// Deserialize a JSON map to a [OverseerrIssue] object. + factory OverseerrIssue.fromJson(Map json) => + _$OverseerrIssueFromJson(json); + + /// Serialize a [OverseerrIssue] object to a JSON map. + Map toJson() => _$OverseerrIssueToJson(this); +} diff --git a/lib/modules/overseerr/api/models/issues/issue_comment.dart b/lib/modules/overseerr/api/models/issues/issue_comment.dart new file mode 100644 index 00000000..f8f16807 --- /dev/null +++ b/lib/modules/overseerr/api/models/issues/issue_comment.dart @@ -0,0 +1,47 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/overseerr.dart'; + +part 'issue_comment.g.dart'; + +@JsonSerializable() +class OverseerrIssueComment { + @JsonKey() + int id; + + @JsonKey() + String message; + + @JsonKey( + fromJson: LunaParser.dateTimeFromString, + toJson: LunaParser.dateTimeToISO8601, + ) + DateTime? createdAt; + + @JsonKey( + fromJson: LunaParser.dateTimeFromString, + toJson: LunaParser.dateTimeToISO8601, + ) + DateTime? updatedAt; + + @JsonKey() + OverseerrUser? user; + + OverseerrIssueComment({ + required this.id, + required this.message, + this.createdAt, + this.updatedAt, + this.user, + }); + + /// Returns a JSON-encoded string version of this object. + @override + String toString() => json.encode(this.toJson()); + + /// Deserialize a JSON map to a [OverseerrIssueComment] object. + factory OverseerrIssueComment.fromJson(Map json) => + _$OverseerrIssueCommentFromJson(json); + + /// Serialize a [OverseerrIssueComment] object to a JSON map. + Map toJson() => _$OverseerrIssueCommentToJson(this); +} diff --git a/lib/modules/overseerr/api/models/issues/issue_page.dart b/lib/modules/overseerr/api/models/issues/issue_page.dart new file mode 100644 index 00000000..b04c66f7 --- /dev/null +++ b/lib/modules/overseerr/api/models/issues/issue_page.dart @@ -0,0 +1,29 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/overseerr.dart'; + +part 'issue_page.g.dart'; + +@JsonSerializable() +class OverseerrIssuePage { + @JsonKey() + OverseerrPageInfo? pageInfo; + + @JsonKey() + List? results; + + OverseerrIssuePage({ + this.pageInfo, + this.results, + }); + + /// Returns a JSON-encoded string version of this object. + @override + String toString() => json.encode(this.toJson()); + + /// Deserialize a JSON map to a [OverseerrIssuePage] object. + factory OverseerrIssuePage.fromJson(Map json) => + _$OverseerrIssuePageFromJson(json); + + /// Serialize a [OverseerrIssuePage] object to a JSON map. + Map toJson() => _$OverseerrIssuePageToJson(this); +} diff --git a/lib/modules/overseerr/api/models/media/media.dart b/lib/modules/overseerr/api/models/media/media.dart index 0af19ae0..0a168faf 100644 --- a/lib/modules/overseerr/api/models/media/media.dart +++ b/lib/modules/overseerr/api/models/media/media.dart @@ -36,26 +36,26 @@ class OverseerrMedia { OverseerrMediaStatus? status4k; @JsonKey( - fromJson: OverseerrUtilities.dateTimeFromJson, - toJson: OverseerrUtilities.dateTimeToJson, + fromJson: LunaParser.dateTimeFromString, + toJson: LunaParser.dateTimeToISO8601, ) DateTime? createdAt; @JsonKey( - fromJson: OverseerrUtilities.dateTimeFromJson, - toJson: OverseerrUtilities.dateTimeToJson, + fromJson: LunaParser.dateTimeFromString, + toJson: LunaParser.dateTimeToISO8601, ) DateTime? updatedAt; @JsonKey( - fromJson: OverseerrUtilities.dateTimeFromJson, - toJson: OverseerrUtilities.dateTimeToJson, + fromJson: LunaParser.dateTimeFromString, + toJson: LunaParser.dateTimeToISO8601, ) DateTime? lastSeasonChange; @JsonKey( - fromJson: OverseerrUtilities.dateTimeFromJson, - toJson: OverseerrUtilities.dateTimeToJson, + fromJson: LunaParser.dateTimeFromString, + toJson: LunaParser.dateTimeToISO8601, ) DateTime? mediaAddedAt; diff --git a/lib/modules/overseerr/api/models/media/season.dart b/lib/modules/overseerr/api/models/media/season.dart index 04878eab..83b692dc 100644 --- a/lib/modules/overseerr/api/models/media/season.dart +++ b/lib/modules/overseerr/api/models/media/season.dart @@ -27,15 +27,15 @@ class OverseerrMediaSeason { @JsonKey( name: 'createdAt', - fromJson: OverseerrUtilities.dateTimeFromJson, - toJson: OverseerrUtilities.dateTimeToJson, + fromJson: LunaParser.dateTimeFromString, + toJson: LunaParser.dateTimeToISO8601, ) DateTime? createdAt; @JsonKey( name: 'updatedAt', - fromJson: OverseerrUtilities.dateTimeFromJson, - toJson: OverseerrUtilities.dateTimeToJson, + fromJson: LunaParser.dateTimeFromString, + toJson: LunaParser.dateTimeToISO8601, ) DateTime? updatedAt; diff --git a/lib/modules/overseerr/api/models/requests/request.dart b/lib/modules/overseerr/api/models/requests/request.dart index 0561634a..a20a695f 100644 --- a/lib/modules/overseerr/api/models/requests/request.dart +++ b/lib/modules/overseerr/api/models/requests/request.dart @@ -17,15 +17,15 @@ class OverseerrRequest { @JsonKey( name: 'createdAt', - fromJson: OverseerrUtilities.dateTimeFromJson, - toJson: OverseerrUtilities.dateTimeToJson, + fromJson: LunaParser.dateTimeFromString, + toJson: LunaParser.dateTimeToISO8601, ) DateTime? createdAt; @JsonKey( name: 'updatedAt', - fromJson: OverseerrUtilities.dateTimeFromJson, - toJson: OverseerrUtilities.dateTimeToJson, + fromJson: LunaParser.dateTimeFromString, + toJson: LunaParser.dateTimeToISO8601, ) DateTime? updatedAt; diff --git a/lib/modules/overseerr/api/models/users/user.dart b/lib/modules/overseerr/api/models/users/user.dart index b4205c4c..3eb3a221 100644 --- a/lib/modules/overseerr/api/models/users/user.dart +++ b/lib/modules/overseerr/api/models/users/user.dart @@ -1,5 +1,4 @@ import 'package:lunasea/core.dart'; -import 'package:lunasea/modules/overseerr.dart'; part 'user.g.dart'; @@ -46,15 +45,15 @@ class OverseerrUser { @JsonKey( name: 'createdAt', - fromJson: OverseerrUtilities.dateTimeFromJson, - toJson: OverseerrUtilities.dateTimeToJson, + fromJson: LunaParser.dateTimeFromString, + toJson: LunaParser.dateTimeToISO8601, ) DateTime? createdAt; @JsonKey( name: 'updatedAt', - fromJson: OverseerrUtilities.dateTimeFromJson, - toJson: OverseerrUtilities.dateTimeToJson, + fromJson: LunaParser.dateTimeFromString, + toJson: LunaParser.dateTimeToISO8601, ) DateTime? updatedAt; diff --git a/lib/modules/overseerr/api/overseerr.dart b/lib/modules/overseerr/api/overseerr.dart index 337e48fb..4c43d453 100644 --- a/lib/modules/overseerr/api/overseerr.dart +++ b/lib/modules/overseerr/api/overseerr.dart @@ -25,6 +25,14 @@ abstract class Overseerr { return _Overseerr(_dio, baseUrl: _baseUrl); } + @GET('issue') + Future getIssues({ + @Query('take') int? take, + @Query('skip') int? skip, + @Query('filter') String? filter, + @Query('sort') String? sort, + }); + @GET('movie/{id}') Future getMovie({ @Path('id') required int id, diff --git a/lib/modules/overseerr/api/types.dart b/lib/modules/overseerr/api/types.dart index d25f991d..e4d10883 100644 --- a/lib/modules/overseerr/api/types.dart +++ b/lib/modules/overseerr/api/types.dart @@ -1,3 +1,7 @@ +export 'types/issue_filter_type.dart'; +export 'types/issue_sort_type.dart'; +export 'types/issue_status.dart'; +export 'types/issue_type.dart'; export 'types/media_status.dart'; export 'types/media_type.dart'; export 'types/request_filter_type.dart'; diff --git a/lib/modules/overseerr/api/types/issue_filter_type.dart b/lib/modules/overseerr/api/types/issue_filter_type.dart new file mode 100644 index 00000000..ec7849ae --- /dev/null +++ b/lib/modules/overseerr/api/types/issue_filter_type.dart @@ -0,0 +1,28 @@ +import 'package:lunasea/core.dart'; + +const _ALL = 'all'; +const _OPEN = 'open'; +const _RESOLVED = 'resolved'; + +@JsonEnum() +enum OverseerrIssueFilterType { + @JsonValue(_ALL) + ALL, + @JsonValue(_OPEN) + OPEN, + @JsonValue(_RESOLVED) + RESOLVED, +} + +extension OverseerrIssueFilterTypeExtension on OverseerrIssueFilterType { + String get key { + switch (this) { + case OverseerrIssueFilterType.ALL: + return _ALL; + case OverseerrIssueFilterType.OPEN: + return _OPEN; + case OverseerrIssueFilterType.RESOLVED: + return _RESOLVED; + } + } +} diff --git a/lib/modules/overseerr/api/types/issue_sort_type.dart b/lib/modules/overseerr/api/types/issue_sort_type.dart new file mode 100644 index 00000000..d3d622c1 --- /dev/null +++ b/lib/modules/overseerr/api/types/issue_sort_type.dart @@ -0,0 +1,23 @@ +import 'package:lunasea/core.dart'; + +const _MOST_RECENT = 'added'; +const _LAST_MODIFIED = 'modified'; + +@JsonEnum() +enum OverseerrIssueSortType { + @JsonValue(_MOST_RECENT) + MOST_RECENT, + @JsonValue(_LAST_MODIFIED) + LAST_MODIFIED, +} + +extension OverseerrIssueSortTypeExtension on OverseerrIssueSortType { + String get key { + switch (this) { + case OverseerrIssueSortType.MOST_RECENT: + return _MOST_RECENT; + case OverseerrIssueSortType.LAST_MODIFIED: + return _LAST_MODIFIED; + } + } +} diff --git a/lib/modules/overseerr/api/types/issue_status.dart b/lib/modules/overseerr/api/types/issue_status.dart new file mode 100644 index 00000000..e9b8301e --- /dev/null +++ b/lib/modules/overseerr/api/types/issue_status.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +const _OPEN = 1; +const _RESOLVED = 2; + +@JsonEnum() +enum OverseerrIssueStatus { + @JsonValue(_OPEN) + OPEN, + @JsonValue(_RESOLVED) + RESOLVED, +} + +extension OverseerrIssueStatusExtension on OverseerrIssueStatus { + int get key { + switch (this) { + case OverseerrIssueStatus.OPEN: + return _OPEN; + case OverseerrIssueStatus.RESOLVED: + return _RESOLVED; + } + } + + String get name { + switch (this) { + case OverseerrIssueStatus.OPEN: + return 'overseerr.Open'.tr(); + case OverseerrIssueStatus.RESOLVED: + return 'overseerr.Resolved'.tr(); + } + } + + Color get color { + switch (this) { + case OverseerrIssueStatus.OPEN: + return LunaColours.orange; + case OverseerrIssueStatus.RESOLVED: + return LunaColours.accent; + } + } +} diff --git a/lib/modules/overseerr/api/types/issue_type.dart b/lib/modules/overseerr/api/types/issue_type.dart new file mode 100644 index 00000000..432722a6 --- /dev/null +++ b/lib/modules/overseerr/api/types/issue_type.dart @@ -0,0 +1,46 @@ +import 'package:lunasea/core.dart'; + +const _VIDEO = 1; +const _AUDIO = 2; +const _SUBTITLE = 3; +const _OTHER = 4; + +@JsonEnum() +enum OverseerrIssueType { + @JsonValue(_VIDEO) + VIDEO, + @JsonValue(_AUDIO) + AUDIO, + @JsonValue(_SUBTITLE) + SUBTITLE, + @JsonValue(_OTHER) + OTHER, +} + +extension OverseerrIssueTypeExtension on OverseerrIssueType { + int get key { + switch (this) { + case OverseerrIssueType.VIDEO: + return _VIDEO; + case OverseerrIssueType.AUDIO: + return _AUDIO; + case OverseerrIssueType.SUBTITLE: + return _SUBTITLE; + case OverseerrIssueType.OTHER: + return _OTHER; + } + } + + String get name { + switch (this) { + case OverseerrIssueType.VIDEO: + return 'overseerr.Video'.tr(); + case OverseerrIssueType.AUDIO: + return 'overseerr.Audio'.tr(); + case OverseerrIssueType.SUBTITLE: + return 'overseerr.Subtitle'.tr(); + case OverseerrIssueType.OTHER: + return 'overseerr.Other'.tr(); + } + } +} diff --git a/lib/modules/overseerr/api/utilities.dart b/lib/modules/overseerr/api/utilities.dart index 9200f167..6393e5d6 100644 --- a/lib/modules/overseerr/api/utilities.dart +++ b/lib/modules/overseerr/api/utilities.dart @@ -1,19 +1,27 @@ import 'types.dart'; abstract class OverseerrUtilities { - static DateTime? dateTimeFromJson(String? date) => - DateTime.tryParse(date ?? ''); - static String? dateTimeToJson(DateTime? date) => date?.toIso8601String(); + static OverseerrMediaType? mediaTypeFromJson(String? type) { + return OverseerrMediaType.TV.from(type); + } - static OverseerrMediaType? mediaTypeFromJson(String? type) => - OverseerrMediaType.TV.from(type); - static String? mediaTypeToJson(OverseerrMediaType? type) => type?.value; + static String? mediaTypeToJson(OverseerrMediaType? type) { + return type?.value; + } - static OverseerrMediaStatus? mediaStatusFromJson(int? type) => - OverseerrMediaStatus.UNKNOWN.from(type); - static int? mediaStatusToJson(OverseerrMediaStatus? type) => type?.value; + static OverseerrMediaStatus? mediaStatusFromJson(int? type) { + return OverseerrMediaStatus.UNKNOWN.from(type); + } - static OverseerrRequestStatus? requestStatusFromJson(int? type) => - OverseerrRequestStatus.APPROVED.from(type); - static int? requestStatusToJson(OverseerrRequestStatus? type) => type?.value; + static int? mediaStatusToJson(OverseerrMediaStatus? type) { + return type?.value; + } + + static OverseerrRequestStatus? requestStatusFromJson(int? type) { + return OverseerrRequestStatus.APPROVED.from(type); + } + + static int? requestStatusToJson(OverseerrRequestStatus? type) { + return type?.value; + } } diff --git a/lib/modules/overseerr/core/extensions.dart b/lib/modules/overseerr/core/extensions.dart index 62db68fb..dbda7593 100644 --- a/lib/modules/overseerr/core/extensions.dart +++ b/lib/modules/overseerr/core/extensions.dart @@ -1,3 +1,4 @@ +export 'extensions/overseerr_issue.dart'; export 'extensions/overseerr_movie.dart'; export 'extensions/overseerr_request_status.dart'; export 'extensions/overseerr_request.dart'; diff --git a/lib/modules/overseerr/core/extensions/overseerr_issue.dart b/lib/modules/overseerr/core/extensions/overseerr_issue.dart new file mode 100644 index 00000000..bc68ab58 --- /dev/null +++ b/lib/modules/overseerr/core/extensions/overseerr_issue.dart @@ -0,0 +1,21 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/overseerr.dart'; + +extension LunaOverseerrIssueExtension on OverseerrIssue? { + String lunaIssueStatus() { + if (this?.status == null) return LunaUI.TEXT_EMDASH; + return this!.status!.name; + } + + String lunaOpenedBy() { + return 'overseerr.OpenedBy'.tr(args: [ + this?.createdBy.lunaDisplayName() ?? 'overseerr.UnknownUser'.tr(), + ]); + } + + String lunaTypeAndStatus() { + String type = this?.issueType?.name ?? LunaUI.TEXT_EMDASH; + String status = this?.status?.name ?? LunaUI.TEXT_EMDASH; + return '$status ($type)'; + } +} diff --git a/lib/modules/overseerr/routes.dart b/lib/modules/overseerr/routes.dart index 7001ee4f..ec577d38 100644 --- a/lib/modules/overseerr/routes.dart +++ b/lib/modules/overseerr/routes.dart @@ -1,3 +1,4 @@ +export 'routes/issues.dart'; export 'routes/overseerr.dart'; export 'routes/requests.dart'; export 'routes/users.dart'; diff --git a/lib/modules/overseerr/routes/issues.dart b/lib/modules/overseerr/routes/issues.dart new file mode 100644 index 00000000..4f36d64c --- /dev/null +++ b/lib/modules/overseerr/routes/issues.dart @@ -0,0 +1,2 @@ +export 'issues/route.dart'; +export 'issues/widgets.dart'; diff --git a/lib/modules/overseerr/routes/issues/route.dart b/lib/modules/overseerr/routes/issues/route.dart new file mode 100644 index 00000000..116c8dee --- /dev/null +++ b/lib/modules/overseerr/routes/issues/route.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/overseerr.dart'; + +class OverseerrIssuesRoute extends StatefulWidget { + const OverseerrIssuesRoute({ + Key? key, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State + with AutomaticKeepAliveClientMixin { + final _scaffoldKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return LunaScaffold( + scaffoldKey: _scaffoldKey, + module: LunaModule.OVERSEERR, + hideDrawer: true, + body: const OverseerrIssuesListView(), + ); + } +} diff --git a/lib/modules/overseerr/routes/issues/widgets.dart b/lib/modules/overseerr/routes/issues/widgets.dart new file mode 100644 index 00000000..01d7f4e8 --- /dev/null +++ b/lib/modules/overseerr/routes/issues/widgets.dart @@ -0,0 +1,2 @@ +export 'widgets/issue_list_view.dart'; +export 'widgets/issue_tile.dart'; diff --git a/lib/modules/overseerr/routes/issues/widgets/issue_list_view.dart b/lib/modules/overseerr/routes/issues/widgets/issue_list_view.dart new file mode 100644 index 00000000..53179f08 --- /dev/null +++ b/lib/modules/overseerr/routes/issues/widgets/issue_list_view.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/overseerr.dart'; + +class OverseerrIssuesListView extends StatefulWidget { + const OverseerrIssuesListView({ + Key? key, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + final GlobalKey _refreshKey = + GlobalKey(); + final PagingController _pagingController = + PagingController(firstPageKey: 0); + + @override + void dispose() { + _pagingController.dispose(); + super.dispose(); + } + + Future _fetchPage(int pageKey) async { + final int pageSize = OverseerrDatabaseValue.CONTENT_PAGE_SIZE.data; + await context + .read() + .api + ?.getIssues( + take: OverseerrDatabaseValue.CONTENT_PAGE_SIZE.data, + skip: pageKey * pageSize, + filter: OverseerrIssueFilterType.ALL.key, + sort: OverseerrIssueSortType.MOST_RECENT.key, + ) + .then((data) => _processPage(data, pageKey, pageSize)) + .catchError((error, stack) { + LunaLogger().error( + 'Unable to fetch Overseerr issues page / take: $pageSize / skip: ${pageKey * pageSize}', + error, + stack, + ); + _pagingController.error = error; + }); + } + + void _processPage(OverseerrIssuePage page, int key, int size) { + List _issues = page.results ?? []; + if (_issues.length < size) { + return _pagingController.appendLastPage(_issues); + } + return _pagingController.appendPage(_issues, key + 1); + } + + @override + Widget build(BuildContext context) { + return LunaPagedListView( + refreshKey: _refreshKey, + pagingController: _pagingController, + scrollController: OverseerrNavigationBar.scrollControllers[0], + listener: _fetchPage, + noItemsFoundMessage: 'overseerr.NoRequestsFound'.tr(), + // itemExtent: LunaBlock.calculateItemExtent(3), + itemBuilder: (context, issue, _) => OverseerrIssueTile(issue: issue), + ); + } +} diff --git a/lib/modules/overseerr/routes/issues/widgets/issue_tile.dart b/lib/modules/overseerr/routes/issues/widgets/issue_tile.dart new file mode 100644 index 00000000..4d6848b3 --- /dev/null +++ b/lib/modules/overseerr/routes/issues/widgets/issue_tile.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/overseerr.dart'; + +class OverseerrIssueTile extends StatefulWidget { + final OverseerrIssue issue; + + const OverseerrIssueTile({ + Key? key, + required this.issue, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State with LunaLoadCallbackMixin { + bool _loaded = false; + + @override + Future loadCallback() async { + int id = widget.issue.media!.tmdbId!; + switch (widget.issue.media!.mediaType!) { + case OverseerrMediaType.MOVIE: + if (mounted) await context.read().fetchMovie(id); + break; + case OverseerrMediaType.TV: + if (mounted) await context.read().fetchSeries(id); + break; + } + if (mounted) setState(() => _loaded = true); + } + + @override + Widget build(BuildContext context) { + if (!_loaded) return _loadingBlock(); + + return Selector, Future>>( + selector: (_, state) => Tuple2( + state.getMovie(widget.issue.media!.tmdbId!), + state.getSeries(widget.issue.media!.tmdbId!), + ), + builder: (context, data, _) => FutureBuilder( + future: Future.wait([data.item1, data.item2]), + builder: (context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + switch (widget.issue.media!.mediaType!) { + case OverseerrMediaType.MOVIE: + if (snapshot.data?[0] != null) + return _movieBlock(context, snapshot.data![0]); + break; + case OverseerrMediaType.TV: + if (snapshot.data?[1] != null) + return _seriesBlock(context, snapshot.data![1]); + break; + } + } + return _loadingBlock(); + }, + ), + ); + } + + Widget _loadingBlock() { + return const LunaBlock(skeletonEnabled: true, skeletonSubtitles: 3); + } + + Widget _movieBlock(BuildContext context, OverseerrMovie movie) { + return LunaBlock( + title: movie.lunaTitle(), + body: [ + TextSpan(text: movie.lunaYear()), + TextSpan(text: widget.issue.lunaOpenedBy()), + TextSpan( + text: widget.issue.lunaTypeAndStatus(), + style: TextStyle( + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + color: widget.issue.status!.color, + ), + ), + ], + posterHeaders: context.read().headers, + posterUrl: TheMovieDB.getPosterURL(movie.posterPath), + posterPlaceholderIcon: LunaIcons.VIDEO_CAM, + backgroundHeaders: context.read().headers, + backgroundUrl: TheMovieDB.getBackdropURL(movie.backdropPath), + ); + } + + Widget _seriesBlock(BuildContext context, OverseerrSeries series) { + return LunaBlock( + title: series.lunaTitle(), + body: [ + TextSpan(text: series.lunaYear()), + TextSpan(text: widget.issue.lunaOpenedBy()), + TextSpan( + text: widget.issue.lunaTypeAndStatus(), + style: TextStyle( + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + color: widget.issue.status!.color, + ), + ), + ], + posterHeaders: context.read().headers, + posterUrl: TheMovieDB.getPosterURL(series.posterPath), + posterPlaceholderIcon: LunaIcons.VIDEO_CAM, + backgroundHeaders: context.read().headers, + backgroundUrl: TheMovieDB.getBackdropURL(series.backdropPath), + ); + } +} diff --git a/lib/modules/overseerr/routes/overseerr/route.dart b/lib/modules/overseerr/routes/overseerr/route.dart index 5e5424e0..d5ef1dfb 100644 --- a/lib/modules/overseerr/routes/overseerr/route.dart +++ b/lib/modules/overseerr/routes/overseerr/route.dart @@ -83,13 +83,14 @@ class _State extends State<_Widget> { if (!enabled!) { return LunaMessage.moduleNotEnabled( context: context, - module: 'Overseerr', + module: LunaModule.OVERSEERR.name, ); } return LunaPageView( controller: _pageController, children: const [ OverseerrRequestsRoute(), + OverseerrIssuesRoute(), OverseerrUserRoute(), ], ); diff --git a/lib/modules/overseerr/routes/overseerr/widgets/navigation_bar.dart b/lib/modules/overseerr/routes/overseerr/widgets/navigation_bar.dart index 44a01642..da9aaa59 100644 --- a/lib/modules/overseerr/routes/overseerr/widgets/navigation_bar.dart +++ b/lib/modules/overseerr/routes/overseerr/widgets/navigation_bar.dart @@ -9,13 +9,15 @@ class OverseerrNavigationBar extends StatelessWidget { ); static const List icons = [ - Icons.movie_rounded, - Icons.person_rounded, + LunaIcons.REQUESTS, + LunaIcons.WARNING, + LunaIcons.USERS, ]; static List get titles => [ - 'Requests', - 'Users', + 'overseerr.Requests'.tr(), + 'overseerr.Issues'.tr(), + 'overseerr.Users'.tr(), ]; const OverseerrNavigationBar({ diff --git a/lib/modules/overseerr/routes/requests/widgets/request_list_view.dart b/lib/modules/overseerr/routes/requests/widgets/request_list_view.dart index 8c01d5fc..12260fac 100644 --- a/lib/modules/overseerr/routes/requests/widgets/request_list_view.dart +++ b/lib/modules/overseerr/routes/requests/widgets/request_list_view.dart @@ -58,7 +58,7 @@ class _State extends State { pagingController: _pagingController, scrollController: OverseerrNavigationBar.scrollControllers[0], listener: _fetchPage, - noItemsFoundMessage: 'overseerr.NoRequestsFound'.tr(), + noItemsFoundMessage: 'overseerr.NoIssuesFound'.tr(), // itemExtent: LunaBlock.calculateItemExtent(3), itemBuilder: (context, request, _) => OverseerrRequestTile( request: request, diff --git a/lib/modules/sonarr/core/dialogs.dart b/lib/modules/sonarr/core/dialogs.dart index 4966abfb..85cc1625 100644 --- a/lib/modules/sonarr/core/dialogs.dart +++ b/lib/modules/sonarr/core/dialogs.dart @@ -738,7 +738,7 @@ class SonarrDialogs { const Padding( padding: EdgeInsets.only(right: 32.0), child: Icon( - Icons.warning_amber_rounded, + LunaIcons.WARNING, color: LunaColours.orange, size: 24.0, ), diff --git a/localization/overseerr/en.json b/localization/overseerr/en.json index d5caba77..e05a61f1 100644 --- a/localization/overseerr/en.json +++ b/localization/overseerr/en.json @@ -1,18 +1,28 @@ { + "overseerr.Audio": "Audio", "overseerr.Approved": "Approved", "overseerr.Available": "Available", "overseerr.Declined": "Declined", + "overseerr.Issues": "Issues", + "overseerr.NoIssues": "No Issues", + "overseerr.NoIssuesFound": "No Issues Found", "overseerr.NoRequests": "No Requests", + "overseerr.NoRequestsFound": "No Requests Found", "overseerr.OneRequest": "1 Request", + "overseerr.Open": "Open", + "overseerr.OpenedBy": "Opened by {}", + "overseerr.Other": "Other", "overseerr.PartiallyAvailable": "Partially Available", "overseerr.Pending": "Pending", "overseerr.Processing": "Processing", "overseerr.SomeRequests": "{} Requests", + "overseerr.Resolved": "Resolved", "overseerr.Requested": "Requested", "overseerr.RequestedBy": "Requested by {}", "overseerr.Requests": "Requests", - "overseerr.NoRequestsFound": "No Requests Found", + "overseerr.Subtitle": "Subtitle", "overseerr.NoUsersFound": "No Users Found", "overseerr.UnknownUser": "Unknown User", - "overseerr.Users": "Users" + "overseerr.Users": "Users", + "overseerr.Video": "Video" } \ No newline at end of file