From 9dc109974222abbc345ae19efab2feb46fe7e67b Mon Sep 17 00:00:00 2001 From: Jagandeep Brar Date: Mon, 6 Jan 2020 17:30:17 -0600 Subject: [PATCH] [Logger] Implemented On-Device Logger (#44) * [Packages] Added f_logs * [Logger] Initialized, captures crashes * [State, Logger] Safe-guarded setState, added logger tab in settings * [Logs] Implemented logs page, log types page, stub log view page * [Logs] Added log types page, log view page, stub log details page * [Logger] Finished details view * [Logger] Removed info logging * [Lidarr] Added logging to API calls * [Radarr] Added logging to API calls * [ListTiles] Added arrows for ListTiles that have more text * [Sonarr] Added logging to API calls * [Sonarr] Safetied some calculations * [SABnzbd] Added logging to API calls * [Sonarr] Prevent dividing by zero Closes #38 --- ios/Podfile.lock | 6 + ios/Runner/Info.plist | 7 + lib/logic/automation/lidarr/api.dart | 121 ++++++++++- lib/logic/automation/radarr/api.dart | 109 +++++++++- lib/logic/automation/sonarr/api.dart | 145 ++++++++++++-- .../automation/sonarr/entry/episode.dart | 2 +- lib/logic/clients/sabnzbd/api.dart | 107 +++++++++- lib/main.dart | 9 +- lib/pages/lidarr/lidarr.dart | 8 +- .../subpages/catalogue/addartist/details.dart | 22 +- .../lidarr/subpages/catalogue/catalogue.dart | 40 ++-- lib/pages/lidarr/subpages/details/artist.dart | 24 ++- lib/pages/lidarr/subpages/details/edit.dart | 22 +- .../lidarr/subpages/details/release.dart | 4 + lib/pages/radarr/radarr.dart | 8 +- .../subpages/catalogue/addmovie/details.dart | 20 +- .../radarr/subpages/catalogue/catalogue.dart | 40 ++-- lib/pages/radarr/subpages/details/edit.dart | 22 +- lib/pages/radarr/subpages/details/movie.dart | 16 +- .../radarr/subpages/details/release.dart | 4 + lib/pages/sabnzbd/sabnzbd.dart | 8 +- .../subpages/history/details/details.dart | 8 + .../sabnzbd/subpages/history/history.dart | 8 +- lib/pages/sabnzbd/subpages/queue/queue.dart | 10 +- lib/pages/settings/settings.dart | 8 +- .../subpages/automation/tabs/lidarr.dart | 20 +- .../subpages/automation/tabs/radarr.dart | 20 +- .../subpages/automation/tabs/sonarr.dart | 20 +- .../subpages/clients/tabs/sabnzbd.dart | 20 +- .../settings/subpages/general/general.dart | 2 + .../settings/subpages/general/tabs/logs.dart | 106 ++++++++++ .../subpages/general/tabs/logs/details.dart | 153 ++++++++++++++ .../subpages/general/tabs/logs/type.dart | 109 ++++++++++ .../subpages/general/tabs/logs/view.dart | 189 ++++++++++++++++++ .../subpages/general/tabs/lunasea.dart | 10 +- .../subpages/general/tabs/profile.dart | 10 +- .../settings/subpages/general/tabs/tabs.dart | 1 + lib/pages/sonarr/sonarr.dart | 8 +- .../subpages/catalogue/addseries/details.dart | 22 +- .../sonarr/subpages/catalogue/catalogue.dart | 34 ++-- lib/pages/sonarr/subpages/details/edit.dart | 22 +- .../sonarr/subpages/details/release.dart | 4 + lib/pages/sonarr/subpages/details/season.dart | 8 +- lib/pages/sonarr/subpages/details/show.dart | 14 +- lib/system/logger.dart | 62 ++++++ lib/system/ui/dialogs/system.dart | 130 +++++++++++- pubspec.lock | 37 +++- pubspec.yaml | 2 + 48 files changed, 1569 insertions(+), 212 deletions(-) create mode 100644 lib/pages/settings/subpages/general/tabs/logs.dart create mode 100644 lib/pages/settings/subpages/general/tabs/logs/details.dart create mode 100644 lib/pages/settings/subpages/general/tabs/logs/type.dart create mode 100644 lib/pages/settings/subpages/general/tabs/logs/view.dart create mode 100644 lib/system/logger.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d5157cd9..ad897fe6 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -6,6 +6,8 @@ PODS: - Flutter - path_provider (0.0.1): - Flutter + - permission_handler (4.0.0): + - Flutter - shared_preferences (0.0.1): - Flutter - shared_preferences_macos (0.0.1): @@ -24,6 +26,7 @@ DEPENDENCIES: - Flutter (from `Flutter`) - package_info (from `.symlinks/plugins/package_info/ios`) - path_provider (from `.symlinks/plugins/path_provider/ios`) + - permission_handler (from `.symlinks/plugins/permission_handler/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) - shared_preferences_macos (from `.symlinks/plugins/shared_preferences_macos/ios`) - shared_preferences_web (from `.symlinks/plugins/shared_preferences_web/ios`) @@ -40,6 +43,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/package_info/ios" path_provider: :path: ".symlinks/plugins/path_provider/ios" + permission_handler: + :path: ".symlinks/plugins/permission_handler/ios" shared_preferences: :path: ".symlinks/plugins/shared_preferences/ios" shared_preferences_macos: @@ -58,6 +63,7 @@ SPEC CHECKSUMS: Flutter: 0e3d915762c693b495b44d77113d4970485de6ec package_info: 48b108e75b8802c2d5e126f208ef540561c98aef path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d + permission_handler: 0fb88b8a3c23e31e5de45b8f1bb8b7de28cf8941 shared_preferences: 430726339841afefe5142b9c1f50cb6bd7793e01 shared_preferences_macos: f3f29b71ccbb56bf40c9dd6396c9acf15e214087 shared_preferences_web: 141cce0c3ed1a1c5bf2a0e44f52d31eeb66e5ea9 diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 153addc6..d4aefc7c 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -59,5 +59,12 @@ UIViewControllerBasedStatusBarAppearance + + NSPhotoLibraryAddUsageDescription + LunaSea would like to save photos from the app to your gallery + NSPhotoLibraryUsageDescription + LunaSea would like to access your photo gallery for uploading images to the app diff --git a/lib/logic/automation/lidarr/api.dart b/lib/logic/automation/lidarr/api.dart index 1834a542..040f587b 100644 --- a/lib/logic/automation/lidarr/api.dart +++ b/lib/logic/automation/lidarr/api.dart @@ -2,9 +2,18 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:lunasea/configuration/values.dart'; import 'package:lunasea/logic/automation/lidarr.dart'; +import 'package:lunasea/system/logger.dart'; class LidarrAPI { LidarrAPI._(); + + static void logWarning(String methodName, String text) { + Logger.warning('package:lunasea/logic/automation/lidarr/api.dart', methodName, 'Lidarr: $text'); + } + + static void logError(String methodName, String text, Object error) { + Logger.error('package:lunasea/logic/automation/lidarr/api.dart', methodName, 'Lidarr: $text', error, StackTrace.current); + } static Future testConnection(List values) async { try { @@ -19,8 +28,10 @@ class LidarrAPI { } } } catch (e) { + logError('testConnection', 'Connection test failed', e); return false; } + logWarning('testConnection', 'Connection test failed'); return false; } @@ -37,10 +48,14 @@ class LidarrAPI { if(response.statusCode == 200) { List body = json.decode(response.body); return body.length ?? 0; + } else { + logError('getArtistCount', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getArtistCount', 'Failed to fetch artist count', e); return -1; } + logWarning('getArtistCount', 'Failed to fetch artist count'); return -1; } @@ -79,11 +94,15 @@ class LidarrAPI { )); } return entries; + } else { + logError('getAllArtists', ' HTTP Status Code (${response.statusCode})', null); } } } catch (e) { + logError('getAllArtists', 'Failed to fetch artists', e); return null; } + logWarning('getAllArtists', 'Failed to fetch artists'); return null; } @@ -109,10 +128,14 @@ class LidarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('refreshArtist', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('refreshArtist', 'Failed to refresh artist ($artistID)', e); return false; } + logWarning('refreshArtist', 'Failed to refresh artist ($artistID)'); return false; } @@ -147,11 +170,15 @@ class LidarrAPI { body['links'] ?? [], body['albumFolder'] ?? false, ); + } else { + logError('getArtist', ' HTTP Status Code (${response.statusCode})', null); } } } catch (e) { + logError('getArtist', 'Failed to fetch artist ($artistID)', e); return null; } + logWarning('getArtist', 'Failed to fetch artist ($artistID)'); return null; } @@ -170,10 +197,14 @@ class LidarrAPI { if(body.length == 0) { return true; } + } else { + logError('removeArtist', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('removeArtist', 'Failed to remove artist ($artistID)', e); return false; } + logWarning('removeArtist', 'Failed to remove artist ($artistID)'); return false; } @@ -205,11 +236,17 @@ class LidarrAPI { ); if(response.statusCode == 202) { return true; + } else { + logError('editArtist', ' HTTP Status Code (${response.statusCode})', null); } + } else { + logError('editArtist', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('editArtist', 'Failed to edit artist ($artistID)', e); return false; } + logWarning('editArtist', 'Failed to edit artist ($artistID)'); return false; } @@ -248,11 +285,15 @@ class LidarrAPI { return b.releaseDateObject.compareTo(a.releaseDateObject); }); return entries; + } else { + logError('getArtistAlbums', ' HTTP Status Code (${response.statusCode})', null); } } } catch (e) { + logError('getArtistAlbums', 'Failed to fetch albums ($artistID)', e); return null; } + logWarning('getArtistAlbums', 'Failed to fetch albums ($artistID)'); return null; } @@ -278,11 +319,15 @@ class LidarrAPI { body[0]['statistics'] == null ? 0 : body[0]['statistics']['percentOfTracks'] ?? 0, body[0]['releaseDate'] ?? '', ); + } else { + logError('getAlbum', ' HTTP Status Code (${response.statusCode})', null); } } } catch (e) { + logError('getAlbum', 'Failed to fetch album ($albumID)', e); return null; } + logWarning('getAlbum', 'Failed to fetch album ($albumID)'); return null; } @@ -310,10 +355,14 @@ class LidarrAPI { )); } return entries; + } else { + logError('getAlbumTracks', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getAlbumTracks', 'Failed to fetch album tracks ($albumID)', e); return null; } + logWarning('getAlbumTracks', 'Failed to fetch album tracks ($albumID)'); return null; } @@ -337,10 +386,14 @@ class LidarrAPI { ); } return _entries; + } else { + logError('getQualityProfiles', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getQualityProfiles', 'Failed to fetch quality profiles', e); return null; } + logWarning('getQualityProfiles', 'Failed to fetch quality profiles'); return null; } @@ -364,10 +417,14 @@ class LidarrAPI { ); } return _entries; + } else { + logError('getMetadataProfiles', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getMetadataProfiles', 'Failed to fetch metadata profiles', e); return null; } + logWarning('getMetadataProfiles', 'Failed to fetch metadata profiles'); return null; } @@ -457,10 +514,14 @@ class LidarrAPI { } } return _entries; + } else { + logError('getHistory', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getHistory', 'Failed to fetch history', e); return null; } + logWarning('getHistory', 'Failed to fetch history'); return null; } @@ -488,10 +549,14 @@ class LidarrAPI { )); } return entries; + } else { + logError('getMissing', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getMissing', 'Failed to fetch missing albums', e); return null; } + logWarning('getMissing', 'Failed to fetch missing albums'); return null; } @@ -517,10 +582,14 @@ class LidarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('searchAlbums', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('searchAlbums', 'Failed to search for albums (${albums.toString()})', e); return false; } + logWarning('searchAlbums', 'Failed to search for albums (${albums.toString()})'); return false; } @@ -547,10 +616,14 @@ class LidarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('searchAllMissing', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('searchAllMissing', 'Failed to search for all missing albums', e); return false; } + logWarning('searchAllMissing', 'Failed to search for all missing albums'); return false; } @@ -575,10 +648,14 @@ class LidarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('updateLibrary', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('updateLibrary', 'Failed to update library', e); return false; } + logWarning('updateLibrary', 'Failed to update library'); return false; } @@ -603,10 +680,14 @@ class LidarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('triggerRssSync', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('triggerRssSync', 'Failed to trigger RSS sync', e); return false; } + logWarning('triggerRssSync', 'Failed to trigger RSS sync'); return false; } @@ -631,10 +712,14 @@ class LidarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('triggerBackup', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('triggerBackup', 'Failed to backup database', e); return false; } + logWarning('triggerBackup', 'Failed to backup database'); return false; } @@ -661,11 +746,17 @@ class LidarrAPI { ); if(response.statusCode == 202) { return true; + } else { + logError('toggleArtistMonitored', ' HTTP Status Code (${response.statusCode})', null); } + } else { + logError('toggleArtistMonitored', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('toggleArtistMonitored', 'Failed to toggle artist monitored status ($artistID)', e); return false; } + logWarning('toggleArtistMonitored', 'Failed to toggle artist monitored status ($artistID)'); return false; } @@ -692,11 +783,17 @@ class LidarrAPI { ); if(response.statusCode == 202) { return true; + } else { + logError('downloadRelease', ' HTTP Status Code (${response.statusCode})', null); } + } else { + logError('downloadRelease', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('toggleAlbumMonitored', 'Failed to toggle album monitored status ($albumID)', e); return false; } + logWarning('toggleAlbumMonitored', 'Failed to toggle album monitored status ($albumID)'); return false; } @@ -728,10 +825,14 @@ class LidarrAPI { )); } return entries; + } else { + logError('searchArtists', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('searchArtists', 'Failed to search ($search)', e); return null; } + logWarning('searchArtists', 'Failed to search ($search)'); return null; } @@ -762,20 +863,24 @@ class LidarrAPI { ); if(response.statusCode == 201) { return true; + } else { + logError('addArtist', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('addArtist', 'Failed to add artist (${entry.title})', e); return false; } + logWarning('addArtist', 'Failed to add artist (${entry.title})'); return false; } - static Future> getReleases(int albumId) async { + static Future> getReleases(int albumID) async { List values = Values.lidarrValues; if(values[0] == false) { return null; } try { - String uri = '${values[1]}/api/v1/release?apikey=${values[2]}&albumId=$albumId'; + String uri = '${values[1]}/api/v1/release?apikey=${values[2]}&albumId=$albumID'; http.Response response = await http.get( Uri.encodeFull(uri), ); @@ -801,10 +906,14 @@ class LidarrAPI { )); } return entries; + } else { + logError('getReleases', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getReleases', 'Failed to fetch releases ($albumID)', e); return null; } + logWarning('getReleases', 'Failed to fetch releases ($albumID)'); return null; } @@ -827,10 +936,14 @@ class LidarrAPI { ); if(response.statusCode == 200) { return true; + } else { + logError('downloadRelease', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('downloadRelease', 'Failed to download release ($guid)', e); return false; } + logWarning('downloadRelease', 'Failed to download release ($guid)'); return false; } @@ -854,10 +967,14 @@ class LidarrAPI { )); } return _entries; + } else { + logError('getRootFolders', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getRootFolders', 'Failed to fetch root folders', e); return null; } + logWarning('getRootFolders', 'Failed to fetch root folders'); return null; } } diff --git a/lib/logic/automation/radarr/api.dart b/lib/logic/automation/radarr/api.dart index 25a7c9e7..51a0d9b5 100644 --- a/lib/logic/automation/radarr/api.dart +++ b/lib/logic/automation/radarr/api.dart @@ -2,9 +2,18 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:lunasea/configuration/values.dart'; import 'package:lunasea/logic/automation/radarr.dart'; +import 'package:lunasea/system/logger.dart'; class RadarrAPI { RadarrAPI._(); + + static void logWarning(String methodName, String text) { + Logger.warning('package:lunasea/logic/automation/radarr/api.dart', methodName, 'Radarr: $text'); + } + + static void logError(String methodName, String text, Object error) { + Logger.error('package:lunasea/logic/automation/radarr/api.dart', methodName, 'Radarr: $text', error, StackTrace.current); + } static Future testConnection(List values) async { try { @@ -19,8 +28,10 @@ class RadarrAPI { } } } catch (e) { + logError('testConnection', 'Connection test failed', e); return false; } + logWarning('testConnection', 'Connection test failed'); return false; } @@ -53,10 +64,14 @@ class RadarrAPI { ); if(response.statusCode == 201) { return true; + } else { + logError('addMovie', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('addMovie', 'Failed to add movie (${entry.title})', e); return false; } + logWarning('addMovie', 'Failed to add movie (${entry.title})'); return false; } @@ -82,10 +97,14 @@ class RadarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('refreshMovie', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('refreshMovie', 'Failed to refresh movie ($id)', e); return false; } + logWarning('refreshMovie', 'Failed to refresh movie ($id)'); return false; } @@ -110,10 +129,14 @@ class RadarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('updateLibrary', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('updateLibrary', 'Failed to update library', e); return false; } + logWarning('updateLibrary', 'Failed to update library'); return false; } @@ -138,10 +161,14 @@ class RadarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('triggerRssSync', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('triggerRssSync', 'Failed to trigger RSS sync', e); return false; } + logWarning('triggerRssSync', 'Failed to trigger RSS sync'); return false; } @@ -166,10 +193,14 @@ class RadarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('triggerBackup', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('triggerBackup', 'Failed to backup database', e); return false; } + logWarning('triggerBackup', 'Failed to backup database'); return false; } @@ -200,11 +231,17 @@ class RadarrAPI { ); if(response.statusCode == 202) { return true; + } else { + logError('editMovie', ' HTTP Status Code (${response.statusCode})', null); } + } else { + logError('editMovie', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('editMovie', 'Failed to edit movie ($movieID)', e); return false; } + logWarning('editMovie', 'Failed to edit movie ($movieID)'); return false; } @@ -223,10 +260,14 @@ class RadarrAPI { if(body.length == 0) { return true; } + } else { + logError('removeMovie', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('removeMovie', 'Failed to remove movie ($id)', e); return false; } + logWarning('removeMovie', 'Failed to remove movie ($id)'); return false; } @@ -243,10 +284,14 @@ class RadarrAPI { if(response.statusCode == 200) { List body = json.decode(response.body); return body.length ?? 0; + } else { + logError('getMovieCount', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getMovieCount', 'Failed to fetch movie count', e); return -1; } + logWarning('getMovieCount', 'Failed to fetch movie count'); return -1; } @@ -294,11 +339,15 @@ class RadarrAPI { ); } return entries; + } else { + logError('getAllMovies', ' HTTP Status Code (${response.statusCode})', null); } } } catch (e) { + logError('getAllMovies', 'Failed to fetch all movies', e); return null; } + logWarning('getAllMovies', 'Failed to fetch all movies'); return null; } @@ -340,11 +389,15 @@ class RadarrAPI { body['tmdbId'] ?? 0, body['pathState'] == 'static' ? true : false, ); + } else { + logError('getMovie', ' HTTP Status Code (${response.statusCode})', null); } } } catch (e) { + logError('getMovie', 'Failed to fetch movie ($id)', e); return null; } + logWarning('getMovie', 'Failed to fetch movie ($id)'); return null; } @@ -363,10 +416,14 @@ class RadarrAPI { if(body.length == 0) { return true; } + } else { + logError('removeMovieFile', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('removeMovieFile', 'Failed to remove movie file ($id)', e); return false; } + logWarning('removeMovieFile', 'Failed to remove movie file ($id)'); return false; } @@ -456,11 +513,15 @@ class RadarrAPI { return a.inCinemasObject.compareTo(b.inCinemasObject); }); return [_availableEntries, _cinemaEntries, _announcedEntries].expand((x) => x).toList(); + } else { + logError('getMissing', ' HTTP Status Code (${response.statusCode})', null); } } } catch (e) { + logError('getMissing', 'Failed to fetch missing movies', e); return null; } + logWarning('getMissing', 'Failed to fetch missing movies'); return null; } @@ -528,10 +589,14 @@ class RadarrAPI { } } return _entries; + } else { + logError('getHistory', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getHistory', 'Failed to fetch history', e); return null; } + logWarning('getHistory', 'Failed to fetch history'); return null; } @@ -557,10 +622,14 @@ class RadarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('searchMissingMovies', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('searchMissingMovies', 'Failed to search for missing movies (${movieIDs.toString()})', e); return false; } + logWarning('searchMissingMovies', 'Failed to search for missing movies (${movieIDs.toString()})'); return false; } @@ -587,10 +656,14 @@ class RadarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('searchAllMissing', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('searchAllMissing', 'Failed to search for all missing movies', e); return false; } + logWarning('searchAllMissing', 'Failed to search for all missing movies'); return false; } @@ -617,11 +690,17 @@ class RadarrAPI { ); if(response.statusCode == 202) { return true; + } else { + logError('toggleMovieMonitored', ' HTTP Status Code (${response.statusCode})', null); } + } else { + logError('toggleMovieMonitored', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('toggleMovieMonitored', 'Failed to toggle movie monitored status ($movieID)', e); return false; } + logWarning('toggleMovieMonitored', 'Failed to toggle movie monitored status ($movieID)'); return false; } @@ -645,20 +724,24 @@ class RadarrAPI { ); } return _entries; + } else { + logError('getQualityProfiles', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getQualityProfiles', 'Failed to fetch quality profiles', e); return null; } + logWarning('getQualityProfiles', 'Failed to fetch quality profiles'); return null; } - static Future> getReleases(int movieId) async { + static Future> getReleases(int movieID) async { List values = Values.radarrValues; if(values[0] == false) { return null; } try { - String uri = '${values[1]}/api/release?apikey=${values[2]}&movieId=$movieId'; + String uri = '${values[1]}/api/release?apikey=${values[2]}&movieId=$movieID'; http.Response response = await http.get( Uri.encodeFull(uri), ); @@ -684,10 +767,14 @@ class RadarrAPI { )); } return _entries; + } else { + logError('getReleases', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getReleases', 'Failed to get releases ($movieID)', e); return null; } + logWarning('getReleases', 'Failed to get releases ($movieID)'); return null; } @@ -710,10 +797,14 @@ class RadarrAPI { ); if(response.statusCode == 200) { return true; + } else { + logError('downloadRelease', ' HTTP Status Code (${response.statusCode})', null); } - } catch(e) { + } catch (e) { + logError('downloadRelease', 'Failed to download release ($guid)', e); return false; } + logWarning('downloadRelease', 'Failed to download release ($guid)'); return false; } @@ -744,11 +835,15 @@ class RadarrAPI { )); } return entries; + } else { + logError('searchMovies', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { - return null; + logError('searchMovies', 'Failed to search ($search)', e); + return null; } - return null; + logWarning('searchMovies', 'Failed to search ($search)'); + return null; } static Future> getRootFolders() async { @@ -771,10 +866,14 @@ class RadarrAPI { )); } return _entries; + } else { + logError('getRootFolders', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getRootFolders', 'Failed to fetch root folders', e); return null; } + logWarning('getRootFolders', 'Failed to fetch root folders'); return null; } } \ No newline at end of file diff --git a/lib/logic/automation/sonarr/api.dart b/lib/logic/automation/sonarr/api.dart index 19761478..4f058f69 100644 --- a/lib/logic/automation/sonarr/api.dart +++ b/lib/logic/automation/sonarr/api.dart @@ -3,9 +3,18 @@ import 'package:http/http.dart' as http; import 'package:lunasea/configuration/values.dart'; import 'package:lunasea/logic/automation/sonarr.dart'; import 'package:intl/intl.dart'; +import 'package:lunasea/system/logger.dart'; class SonarrAPI { SonarrAPI._(); + + static void logWarning(String methodName, String text) { + Logger.warning('package:lunasea/logic/automation/sonarr/api.dart', methodName, 'Sonarr: $text'); + } + + static void logError(String methodName, String text, Object error) { + Logger.error('package:lunasea/logic/automation/sonarr/api.dart', methodName, 'Sonarr: $text', error, StackTrace.current); + } static Future testConnection(List values) async { try { @@ -20,8 +29,10 @@ class SonarrAPI { } } } catch (e) { + logError('testConnection', 'Connection test failed', e); return false; } + logWarning('testConnection', 'Connection test failed'); return false; } @@ -32,20 +43,24 @@ class SonarrAPI { } try { String uri = '${values[1]}/api/series?apikey=${values[2]}'; - http.Response response = await http.get( - Uri.encodeFull(uri), - ); - if(response.statusCode == 200) { - List body = json.decode(response.body); - return body.length ?? 0; - } + http.Response response = await http.get( + Uri.encodeFull(uri), + ); + if(response.statusCode == 200) { + List body = json.decode(response.body); + return body.length ?? 0; + } else { + logError('getSeriesCount', ' HTTP Status Code (${response.statusCode})', null); + } } catch (e) { + logError('getSeriesCount', 'Failed to fetch series count', e); return -1; } + logWarning('getSeriesCount', 'Failed to fetch series count'); return -1; } - static Future refreshSeries(int id) async { + static Future refreshSeries(int seriesID) async { List values = Values.sonarrValues; if(values[0] == false) { return false; @@ -59,7 +74,7 @@ class SonarrAPI { }, body: json.encode({ 'name': 'RefreshSeries', - 'seriesId': id, + 'seriesId': seriesID, }), ); if(response.statusCode == 201) { @@ -67,20 +82,24 @@ class SonarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('refreshSeries', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('refreshSeries', 'Failed to refresh series ($seriesID)', e); return false; } + logWarning('refreshSeries', 'Failed to refresh series ($seriesID)'); return false; } - static Future removeSeries(int id) async { + static Future removeSeries(int seriesID) async { List values = Values.sonarrValues; if(values[0] == false) { return false; } try { - String uri = '${values[1]}/api/series/$id?apikey=${values[2]}'; + String uri = '${values[1]}/api/series/$seriesID?apikey=${values[2]}'; http.Response response = await http.delete( Uri.encodeFull(uri), ); @@ -89,10 +108,14 @@ class SonarrAPI { if(body.length == 0) { return true; } + } else { + logError('removeSeries', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('removeSeries', 'Failed to remove series ($seriesID)', e); return false; } + logWarning('removeSeries', 'Failed to remove series ($seriesID)'); return false; } @@ -126,10 +149,14 @@ class SonarrAPI { )); } return entries; + } else { + logError('searchSeries', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('searchSeries', 'Failed to search ($search)', e); return null; } + logWarning('searchSeries', 'Failed to search ($search)'); return null; } @@ -165,10 +192,14 @@ class SonarrAPI { ); if(response.statusCode == 201) { return true; + } else { + logError('addSeries', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('addSeries', 'Failed to add series (${entry.title})', e); return false; } + logWarning('addSeries', 'Failed to add series (${entry.title})'); return false; } @@ -199,15 +230,21 @@ class SonarrAPI { ); if(response.statusCode == 202) { return true; + } else { + logError('editSeries', ' HTTP Status Code (${response.statusCode})', null); } + } else { + logError('editSeries', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('editSeries', 'Failed to edit series ($seriesID)', e); return false; } + logWarning('editSeries', 'Failed to edit series ($seriesID)'); return false; } - static Future getSeries(int id) async { + static Future getSeries(int seriesID) async { List values = Values.sonarrValues; if(values[0] == false) { return null; @@ -215,7 +252,7 @@ class SonarrAPI { try { Map _qualities = await getQualityProfiles(); if(_qualities != null) { - String uri = '${values[1]}/api/series/$id?apikey=${values[2]}'; + String uri = '${values[1]}/api/series/$seriesID?apikey=${values[2]}'; http.Response response = await http.get( Uri.encodeFull(uri), ); @@ -248,11 +285,15 @@ class SonarrAPI { body['profileId'] != null ? _qualities[body['qualityProfileId']].name : '', ); return entry; + } else { + logError('getSeries', ' HTTP Status Code (${response.statusCode})', null); } } } catch (e) { + logError('getSeries', 'Failed to fetch series ($seriesID)', e); return null; } + logWarning('getSeries', 'Failed to fetch series ($seriesID)'); return null; } @@ -302,11 +343,15 @@ class SonarrAPI { ); } return entries; + } else { + logError('getAllSeries', ' HTTP Status Code (${response.statusCode})', null); } } } catch (e) { + logError('getAllSeries', 'Failed to fetch all series', e); return null; } + logWarning('getAllSeries', 'Failed to fetch all series'); return null; } @@ -355,10 +400,14 @@ class SonarrAPI { } } return entries; + } else { + logError('getUpcoming', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getUpcoming', 'Failed to fetch upcoming episodes', e); return null; } + logWarning('getUpcoming', 'Failed to fetch upcoming episodes'); return null; } @@ -450,10 +499,14 @@ class SonarrAPI { } } return _entries; + } else { + logError('getHistory', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getHistory', 'Failed to fetch history', e); return null; } + logWarning('getHistory', 'Failed to fetch history'); return null; } @@ -505,10 +558,14 @@ class SonarrAPI { } } return entries; + } else { + logError('getEpisodes', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getEpisodes', 'Failed to fetch episodes ($seriesID, $seasonNumber)', e); return null; } + logWarning('getEpisodes', 'Failed to fetch episodes ($seriesID, $seasonNumber)'); return null; } @@ -534,10 +591,14 @@ class SonarrAPI { ); } return entries; + } else { + logError('getQueue', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getQueue', 'Failed to fetch queue', e); return null; } + logWarning('getQueue', 'Failed to fetch queue'); return null; } @@ -566,10 +627,14 @@ class SonarrAPI { )); } return entries; + } else { + logError('getMissing', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getMissing', 'Failed to fetch missing episodes', e); return null; } + logWarning('getMissing', 'Failed to fetch missing episodes'); return null; } @@ -594,10 +659,14 @@ class SonarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('searchAllMissing', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('searchAllMissing', 'Failed to search for all missing episodes', e); return false; } + logWarning('searchAllMissing', 'Failed to search for all missing episodes'); return false; } @@ -622,10 +691,14 @@ class SonarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('updateLibrary', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('updateLibrary', 'Failed to update library', e); return false; } + logWarning('updateLibrary', 'Failed to update library'); return false; } @@ -650,10 +723,14 @@ class SonarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('triggerRssSync', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('triggerRssSync', 'Failed to trigger RSS sync', e); return false; } + logWarning('triggerRssSync', 'Failed to trigger RSS sync'); return false; } @@ -678,10 +755,14 @@ class SonarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('triggerBackup', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('triggerBackup', 'Failed to backup database', e); return false; } + logWarning('triggerBackup', 'Failed to backup database'); return false; } @@ -708,10 +789,14 @@ class SonarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('searchSeason', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('searchSeason', 'Failed to search for season ($seriesID, $season)', e); return false; } + logWarning('searchSeason', 'Failed to search for season ($seriesID, $season)'); return false; } @@ -737,10 +822,14 @@ class SonarrAPI { if(body.containsKey('status')) { return true; } + } else { + logError('searchEpisodes', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('searchEpisodes', 'Failed to search for episodes (${episodeIDs.toString()})', e); return false; } + logWarning('searchEpisodes', 'Failed to search for episodes (${episodeIDs.toString()})'); return false; } @@ -767,11 +856,17 @@ class SonarrAPI { ); if(response.statusCode == 202) { return true; + } else { + logError('toggleSeriesMonitored', ' HTTP Status Code (${response.statusCode})', null); } + } else { + logError('toggleSeriesMonitored', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('toggleSeriesMonitored', 'Failed to toggle series monitored ($seriesID)', e); return false; } + logWarning('toggleSeriesMonitored', 'Failed to toggle series monitored ($seriesID)'); return false; } @@ -802,11 +897,17 @@ class SonarrAPI { ); if(response.statusCode == 202) { return true; + } else { + logError('toggleSeasonMonitored', ' HTTP Status Code (${response.statusCode})', null); } + } else { + logError('toggleSeasonMonitored', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('toggleSeasonMonitored', 'Failed to toggle season monitored ($seriesID, $seasonID)', e); return false; } + logWarning('toggleSeasonMonitored', 'Failed to toggle season monitored ($seriesID, $seasonID)'); return false; } @@ -830,10 +931,14 @@ class SonarrAPI { )); } return _entries; + } else { + logError('getRootFolders', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getRootFolders', 'Failed to fetch root folders', e); return null; } + logWarning('getRootFolders', 'Failed to fetch root folders'); return null; } @@ -857,10 +962,14 @@ class SonarrAPI { ); } return _entries; + } else { + logError('getQualityProfiles', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getQualityProfiles', 'Failed to fetch quality profiles', e); return null; } + logWarning('getQualityProfiles', 'Failed to fetch quality profiles'); return null; } @@ -896,10 +1005,14 @@ class SonarrAPI { )); } return _entries; + } else { + logError('getReleases', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getReleases', 'Failed to fetch releases ($episodeId)', e); return null; } + logWarning('getReleases', 'Failed to fetch releases ($episodeId)'); return null; } @@ -922,10 +1035,14 @@ class SonarrAPI { ); if(response.statusCode == 200) { return true; + } else { + logError('downloadRelease', ' HTTP Status Code (${response.statusCode})', null); } - } catch(e) { + } catch (e) { + logError('downloadRelease', 'Failed to download release ($guid)', e); return false; } + logWarning('downloadRelease', 'Failed to download release ($guid)'); return false; } } diff --git a/lib/logic/automation/sonarr/entry/episode.dart b/lib/logic/automation/sonarr/entry/episode.dart index e36e65d1..344f442c 100644 --- a/lib/logic/automation/sonarr/entry/episode.dart +++ b/lib/logic/automation/sonarr/entry/episode.dart @@ -65,7 +65,7 @@ class SonarrEpisodeEntry { TextSpan get subtitle { if(queue != null) { return TextSpan( - text: '${queue.status} (${(100-(queue.sizeLeft/queue.size)*100).abs().toInt()}%)', + text: '${queue?.status ?? 'Unknown'} (${(100-((queue?.sizeLeft ?? 0)/(queue?.size ?? 1))*100).abs().toInt()}%)', style: TextStyle( color: Colors.blue, fontWeight: FontWeight.bold, diff --git a/lib/logic/clients/sabnzbd/api.dart b/lib/logic/clients/sabnzbd/api.dart index 4c9dbc1b..c89d4a43 100644 --- a/lib/logic/clients/sabnzbd/api.dart +++ b/lib/logic/clients/sabnzbd/api.dart @@ -2,9 +2,18 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:lunasea/configuration/values.dart'; import 'package:lunasea/logic/clients/sabnzbd/entry.dart'; +import 'package:lunasea/system/logger.dart'; class SABnzbdAPI { SABnzbdAPI._(); + + static void logWarning(String methodName, String text) { + Logger.warning('package:lunasea/logic/clients/sabnzbd/api.dart', methodName, 'SABnzbd: $text'); + } + + static void logError(String methodName, String text, Object error) { + Logger.error('package:lunasea/logic/clients/sabnzbd/api.dart', methodName, 'SABnzbd: $text', error, StackTrace.current); + } static Future testConnection(List values) async { try { @@ -19,8 +28,10 @@ class SABnzbdAPI { } } } catch (e) { + logError('testConnection', 'Connection test failed', e); return false; } + logWarning('testConnection', 'Connection test failed'); return false; } @@ -61,10 +72,14 @@ class SABnzbdAPI { statisticsBody['total'] ?? 0, ); } + } else { + logError('getStatistics', ' HTTP Status Code(s) (${status.statusCode}, ${statistics.statusCode})', null); } } catch (e) { + logError('getStatistics', 'Failed to fetch statistics', e); return null; } + logWarning('getStatistics', 'Failed to fetch statistics'); return null; } @@ -83,10 +98,14 @@ class SABnzbdAPI { if(body['status'] != null && body['status']) { return true; } + } else { + logError('pauseQueue', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('pauseQueue', 'Failed to pause queue', e); return false; } + logWarning('pauseQueue', 'Failed to pause queue'); return false; } @@ -105,10 +124,14 @@ class SABnzbdAPI { if(body['status'] != null && body['status']) { return true; } + } else { + logError('resumeQueue', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('resumeQueue', 'Failed to resume queue', e); return false; } + logWarning('resumeQueue', 'Failed to resume queue'); return false; } @@ -127,10 +150,14 @@ class SABnzbdAPI { if(body['status'] != null && body['status']) { return true; } + } else { + logError('pauseSingleJob', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('pauseSingleJob', 'Failed to pause job ($nzoId)', e); return false; } + logWarning('pauseSingleJob', 'Failed to pause job ($nzoId)'); return false; } @@ -149,10 +176,14 @@ class SABnzbdAPI { if(body['status'] != null && body['status']) { return true; } + } else { + logError('resumeSingleJob', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('resumeSingleJob', 'Failed to resume job ($nzoId)', e); return false; } + logWarning('resumeSingleJob', 'Failed to resume job ($nzoId)'); return false; } @@ -171,10 +202,14 @@ class SABnzbdAPI { if(body['status'] != null && body['status']) { return true; } + } else { + logError('deleteJob', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('deleteJob', 'Failed to delete job ($nzoId)', e); return false; } + logWarning('deleteJob', 'Failed to delete job ($nzoId)'); return false; } @@ -193,10 +228,14 @@ class SABnzbdAPI { if(body['status'] != null && body['status']) { return true; } + } else { + logError('renameJob', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('renameJob', 'Failed to rename job ($nzoId, $name)', e); return false; } + logWarning('renameJob', 'Failed to rename job ($nzoId, $name)'); return false; } @@ -215,10 +254,14 @@ class SABnzbdAPI { if(body['status'] != null && body['status']) { return true; } + } else { + logError('setJobPassword', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('setJobPassword', 'Failed to set job password ($nzoId, $password)', e); return false; } + logWarning('setJobPassword', 'Failed to set job password ($nzoId, $password)'); return false; } @@ -237,10 +280,14 @@ class SABnzbdAPI { if(body['status'] || body['position'] != null) { return true; } + } else { + logError('setJobPriority', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('setJobPriority', 'Failed to set job priority ($nzoId, $priority)', e); return false; } + logWarning('setJobPriority', 'Failed to set job priority ($nzoId, $priority)'); return false; } @@ -259,10 +306,14 @@ class SABnzbdAPI { if(body['status'] != null && body['status']) { return true; } + } else { + logError('deleteHistory', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('deleteHistory', 'Failed to delete history entry ($nzoId)', e); return false; } + logWarning('deleteHistory', 'Failed to delete history entry ($nzoId)'); return false; } @@ -303,10 +354,14 @@ class SABnzbdAPI { queue, ]; } + } else { + logError('getStatusAndQueue', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getStatusAndQueue', 'Failed to fetch status and queue', e); return null; } + logWarning('getStatusAndQueue', 'Failed to fetch status and queue'); return null; } @@ -341,10 +396,14 @@ class SABnzbdAPI { } return entries; } + } else { + logError('getHistory', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getHistory', 'Failed to fetch history', e); return null; } + logWarning('getHistory', 'Failed to fetch history'); return null; } @@ -365,10 +424,14 @@ class SABnzbdAPI { return true; } } + } else { + logError('moveQueue', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('moveQueue', 'Failed to move queue entry ($nzoId, $index)', e); return false; } + logWarning('moveQueue', 'Failed to move queue entry ($nzoId, $index)'); return false; } @@ -387,10 +450,14 @@ class SABnzbdAPI { if(body['status'] != null && body['status']) { return true; } + } else { + logError('sortQueue', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('sortQueue', 'Failed to sort queue ($sort, $dir)', e); return false; } + logWarning('sortQueue', 'Failed to sort queue ($sort, $dir)'); return false; } @@ -415,10 +482,14 @@ class SABnzbdAPI { } return entries; } + } else { + logError('getCategories', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('getCategories', 'Failed to fetch categories', e); return null; } + logWarning('getCategories', 'Failed to fetch categories'); return null; } @@ -437,10 +508,14 @@ class SABnzbdAPI { if(body['status'] != null && body['status']) { return true; } + } else { + logError('setCategory', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('setCategory', 'Failed to set category ($nzoId, $category)', e); return false; } + logWarning('setCategory', 'Failed to set category ($nzoId, $category)'); return false; } @@ -459,10 +534,14 @@ class SABnzbdAPI { if(body['status'] != null && body['status']) { return true; } + } else { + logError('setSpeedLimit', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('setSpeedLimit', 'Failed to set speed limit ($limit)', e); return false; } + logWarning('setSpeedLimit', 'Failed to set speed limit ($limit)'); return false; } @@ -482,8 +561,14 @@ class SABnzbdAPI { if(body['status'] != null && body['status']) { return true; } + } else { + logError('uploadURL', ' HTTP Status Code (${response.statusCode})', null); } - } catch(e) {} + } catch (e) { + logError('uploadURL', 'Failed to upload NZB by URL ($url)', e); + return false; + } + logWarning('uploadURL', 'Failed to upload NZB by URL ($url)'); return false; } @@ -501,10 +586,14 @@ class SABnzbdAPI { if(body['status'] != null && body['status']) { return true; } + } else { + logError('uploadFile', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('uploadFile', 'Failed to upload nzb file ($name)', e); return false; } + logWarning('uploadFile', 'Failed to upload nzb file ($name)'); return false; } @@ -523,10 +612,14 @@ class SABnzbdAPI { if(body['status'] != null && body['status']) { return true; } + } else { + logError('setOnCompleteAction', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('setOnCompleteAction', 'Failed to set on-complete action ($action)', e); return false; } + logWarning('setOnCompleteAction', 'Failed to set on-complete action ($action)'); return false; } @@ -545,10 +638,14 @@ class SABnzbdAPI { if(body['status'] != null && body['status']) { return true; } + } else { + logError('clearHistory', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('clearHistory', 'Failed to clear history ($action)', e); return false; } + logWarning('clearHistory', 'Failed to clear history ($action)'); return false; } @@ -567,10 +664,14 @@ class SABnzbdAPI { if(body['status'] != null && body['status']) { return true; } + } else { + logError('retryFailedJob', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('retryFailedJob', 'Failed to retry job ($nzoId)', e); return false; } + logWarning('retryFailedJob', 'Failed to retry job ($nzoId)'); return false; } @@ -589,10 +690,14 @@ class SABnzbdAPI { if(body['status'] != null && body['status']) { return true; } + } else { + logError('retryFailedJobPassword', ' HTTP Status Code (${response.statusCode})', null); } } catch (e) { + logError('retryFailedJobPassword', 'Failed to retry job with new password ($nzoId, $password)', e); return false; } + logWarning('retryFailedJobPassword', 'Failed to retry job with new password ($nzoId, $password)'); return false; } } diff --git a/lib/main.dart b/lib/main.dart index d8d700a0..1c6b891c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,19 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:lunasea/system/constants.dart'; import 'package:lunasea/pages/pages.dart'; import 'package:lunasea/configuration/configuration.dart'; +import 'package:lunasea/system/logger.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Configuration.pullAndSanitizeValues(); - runApp(_BIOS()); + Logger.initialize(); + runZoned>(() async { + runApp(_BIOS()); + }, onError: (Object error, StackTrace stack) { + Logger.fatal(error, stack); + }); } class _BIOS extends StatelessWidget { diff --git a/lib/pages/lidarr/lidarr.dart b/lib/pages/lidarr/lidarr.dart index 3fd935be..f25cb19e 100644 --- a/lib/pages/lidarr/lidarr.dart +++ b/lib/pages/lidarr/lidarr.dart @@ -49,9 +49,11 @@ class _LidarrState extends State { ]; void _navOnTap(int index) { - setState(() { - _currIndex = index; - }); + if(mounted) { + setState(() { + _currIndex = index; + }); + } } @override diff --git a/lib/pages/lidarr/subpages/catalogue/addartist/details.dart b/lib/pages/lidarr/subpages/catalogue/addartist/details.dart index 28b85704..234303ae 100644 --- a/lib/pages/lidarr/subpages/catalogue/addartist/details.dart +++ b/lib/pages/lidarr/subpages/catalogue/addartist/details.dart @@ -169,9 +169,11 @@ class _LidarrArtistSearchDetailsState extends State { trailing: Switch( value: _monitored, onChanged: (value) { - setState(() { - _monitored = value; - }); + if(mounted) { + setState(() { + _monitored = value; + }); + } }, ), ), @@ -186,9 +188,11 @@ class _LidarrArtistSearchDetailsState extends State { trailing: Switch( value: _albumFolders, onChanged: (value) { - setState(() { - _albumFolders = value; - }); + if(mounted) { + setState(() { + _albumFolders = value; + }); + } }, ), ), @@ -205,7 +209,7 @@ class _LidarrArtistSearchDetailsState extends State { ), onTap: () async { List _values = await LidarrDialogs.showEditRootFolderPrompt(context, _rootFolders); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _rootFolder = _values[1]; }); @@ -225,7 +229,7 @@ class _LidarrArtistSearchDetailsState extends State { ), onTap: () async { List _values = await LidarrDialogs.showEditQualityProfilePrompt(context, _qualityProfiles); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _qualityProfile = _values[1]; }); @@ -245,7 +249,7 @@ class _LidarrArtistSearchDetailsState extends State { ), onTap: () async { List _values = await LidarrDialogs.showEditMetadataProfilePrompt(context, _metadataProfiles); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _metadataProfile = _values[1]; }); diff --git a/lib/pages/lidarr/subpages/catalogue/catalogue.dart b/lib/pages/lidarr/subpages/catalogue/catalogue.dart index a6d83b22..ead5bf26 100644 --- a/lib/pages/lidarr/subpages/catalogue/catalogue.dart +++ b/lib/pages/lidarr/subpages/catalogue/catalogue.dart @@ -60,12 +60,14 @@ class _CatalogueState extends State with TickerProviderStateMixi _animationContoller = AnimationController(vsync: this, duration: kThemeAnimationDuration); _animationContoller?.forward(); _searchController.addListener(() { - setState(() { - searchFilter = _searchController.text; - _searchedEntries = _catalogueEntries.where( - (entry) => searchFilter == 'null' || searchFilter == '' ? entry != null : entry.title.toLowerCase().contains(searchFilter.toLowerCase()), - ).toList(); - }); + if(mounted) { + setState(() { + searchFilter = _searchController.text; + _searchedEntries = _catalogueEntries.where( + (entry) => searchFilter == 'null' || searchFilter == '' ? entry != null : entry.title.toLowerCase().contains(searchFilter.toLowerCase()), + ).toList(); + }); + } }); _scrollController.addListener(() { if(_scrollController?.position?.userScrollDirection == ScrollDirection.reverse) { @@ -124,9 +126,11 @@ class _CatalogueState extends State with TickerProviderStateMixi child: Elements.getIcon(_hideUnmonitored ? Icons.visibility_off : Icons.visibility), tooltip: 'Hide/Unhide Unmonitored Artists', onPressed: () { - setState(() { - _hideUnmonitored = !_hideUnmonitored; - }); + if(mounted) { + setState(() { + _hideUnmonitored = !_hideUnmonitored; + }); + } }, ), scale: _animationContoller, @@ -253,9 +257,11 @@ class _CatalogueState extends State with TickerProviderStateMixi tooltip: 'Toggle Monitored', onPressed: () async { if(await LidarrAPI.toggleArtistMonitored(entry.artistID, !entry.monitored)) { - setState(() { - entry.monitored = !entry.monitored; - }); + if(mounted) { + setState(() { + entry.monitored = !entry.monitored; + }); + } Notifications.showSnackBar( _scaffoldKey, entry.monitored ? 'Monitoring ${entry.title}' : 'No longer monitoring ${entry.title}', @@ -355,9 +361,11 @@ class _CatalogueState extends State with TickerProviderStateMixi if(result != null) { switch(result[0]) { case 'updated_artist': { - setState(() { - entry = result[1]; - }); + if(mounted) { + setState(() { + entry = result[1]; + }); + } Notifications.showSnackBar(_scaffoldKey, 'Updated ${entry.title}'); break; } @@ -369,7 +377,7 @@ class _CatalogueState extends State with TickerProviderStateMixi LidarrCatalogueEntry _entry = await LidarrAPI.getArtist(entry.artistID); _entry ??= _searchedEntries[index]; if(mounted) { - if(_searchedEntries[index]?.title == entry.title) { + if(_searchedEntries[index]?.title == entry.title && mounted) { setState(() { _searchedEntries[index] = _entry; }); diff --git a/lib/pages/lidarr/subpages/details/artist.dart b/lib/pages/lidarr/subpages/details/artist.dart index 16632201..cf1bdd40 100644 --- a/lib/pages/lidarr/subpages/details/artist.dart +++ b/lib/pages/lidarr/subpages/details/artist.dart @@ -129,9 +129,11 @@ class _LidarrArtistDetailsState extends State { IconButton( icon: Icon(_hideUnmonitored ? Icons.visibility_off : Icons.visibility), onPressed: () async { - setState(() { - _hideUnmonitored = !_hideUnmonitored; - }); + if(mounted) { + setState(() { + _hideUnmonitored = !_hideUnmonitored; + }); + } }, tooltip: 'Hide/Unhide Unmonitored Content', ), @@ -329,9 +331,11 @@ class _LidarrArtistDetailsState extends State { tooltip: 'Toggle Monitored', onPressed: () async { if(await LidarrAPI.toggleAlbumMonitored(entry.albumID, !entry.monitored)) { - setState(() { - entry.monitored = !entry.monitored; - }); + if(mounted) { + setState(() { + entry.monitored = !entry.monitored; + }); + } Notifications.showSnackBar( _scaffoldKey, entry.monitored ? 'Monitoring ${entry.title}' : 'No longer monitoring ${entry.title}', @@ -632,9 +636,11 @@ class _LidarrArtistDetailsState extends State { if(result != null) { switch(result[0]) { case 'updated_artist': { - setState(() { - entry = result[1]; - }); + if(mounted) { + setState(() { + entry = result[1]; + }); + } Notifications.showSnackBar(_scaffoldKey, 'Updated ${entry.title}'); break; } diff --git a/lib/pages/lidarr/subpages/details/edit.dart b/lib/pages/lidarr/subpages/details/edit.dart index addd1561..56e9b406 100644 --- a/lib/pages/lidarr/subpages/details/edit.dart +++ b/lib/pages/lidarr/subpages/details/edit.dart @@ -153,9 +153,11 @@ class _LidarrEditArtistState extends State { trailing: Switch( value: _monitored, onChanged: (value) { - setState(() { - _monitored = value; - }); + if(mounted) { + setState(() { + _monitored = value; + }); + } }, ), ), @@ -169,9 +171,11 @@ class _LidarrEditArtistState extends State { trailing: Switch( value: _albumFolders, onChanged: (value) { - setState(() { - _albumFolders = value; - }); + if(mounted) { + setState(() { + _albumFolders = value; + }); + } }, ), ), @@ -188,7 +192,7 @@ class _LidarrEditArtistState extends State { ), onTap: () async { List _values = await SystemDialogs.showEditTextPrompt(context, 'Artist Path', prefill: _path); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _path = _values[1]; }); @@ -208,7 +212,7 @@ class _LidarrEditArtistState extends State { ), onTap: () async { List _values = await LidarrDialogs.showEditQualityProfilePrompt(context, _qualityProfiles); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _qualityProfile = _values[1]; }); @@ -228,7 +232,7 @@ class _LidarrEditArtistState extends State { ), onTap: () async { List _values = await LidarrDialogs.showEditMetadataProfilePrompt(context, _metadataProfiles); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _metadataProfile = _values[1]; }); diff --git a/lib/pages/lidarr/subpages/details/release.dart b/lib/pages/lidarr/subpages/details/release.dart index 9afee074..c9b74b78 100644 --- a/lib/pages/lidarr/subpages/details/release.dart +++ b/lib/pages/lidarr/subpages/details/release.dart @@ -125,6 +125,10 @@ class _LidarrReleaseInfoState extends State { onTap: () async { SystemDialogs.showTextPreviewPrompt(context, 'Release Title', entry.title); }, + trailing: IconButton( + icon: Elements.getIcon(Icons.arrow_forward_ios), + onPressed: null, + ), ), margin: Elements.getCardMargin(), elevation: 4.0, diff --git a/lib/pages/radarr/radarr.dart b/lib/pages/radarr/radarr.dart index 6974fb13..0eb8e570 100644 --- a/lib/pages/radarr/radarr.dart +++ b/lib/pages/radarr/radarr.dart @@ -49,9 +49,11 @@ class _RadarrState extends State { ]; void _navOnTap(int index) { - setState(() { - _currIndex = index; - }); + if(mounted) { + setState(() { + _currIndex = index; + }); + } } @override diff --git a/lib/pages/radarr/subpages/catalogue/addmovie/details.dart b/lib/pages/radarr/subpages/catalogue/addmovie/details.dart index 6ab8ce93..e6b80381 100644 --- a/lib/pages/radarr/subpages/catalogue/addmovie/details.dart +++ b/lib/pages/radarr/subpages/catalogue/addmovie/details.dart @@ -163,9 +163,11 @@ class _RadarrMovieSearchDetailsState extends State { trailing: Switch( value: monitored, onChanged: (value) { - setState(() { - monitored = value; - }); + if(mounted) { + setState(() { + monitored = value; + }); + } }, ), ), @@ -183,9 +185,11 @@ class _RadarrMovieSearchDetailsState extends State { onTap: () async { List _values = await RadarrDialogs.showEditRootFolderPrompt(context, _rootFolders); if(_values[0]) { - _rootFolder = _values[1]; - setState(() { - }); + if(mounted) { + setState(() { + _rootFolder = _values[1]; + }); + } } }, ), @@ -202,7 +206,7 @@ class _RadarrMovieSearchDetailsState extends State { ), onTap: () async { List _values = await RadarrDialogs.showEditQualityProfilePrompt(context, _qualityProfiles); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _qualityProfile = _values[1]; }); @@ -222,7 +226,7 @@ class _RadarrMovieSearchDetailsState extends State { ), onTap: () async { List _values = await RadarrDialogs.showMinimumAvailabilityPrompt(context, Constants.radarrMinAvailability); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _minimumAvailability = _values[1]; }); diff --git a/lib/pages/radarr/subpages/catalogue/catalogue.dart b/lib/pages/radarr/subpages/catalogue/catalogue.dart index 0fbc2cc8..47d7a82f 100644 --- a/lib/pages/radarr/subpages/catalogue/catalogue.dart +++ b/lib/pages/radarr/subpages/catalogue/catalogue.dart @@ -61,12 +61,14 @@ class _CatalogueState extends State with TickerProviderStateMixi _animationContoller = AnimationController(vsync: this, duration: kThemeAnimationDuration); _animationContoller?.forward(); _searchController.addListener(() { - setState(() { - searchFilter = _searchController.text; - _searchedEntries = _catalogueEntries.where( - (entry) => searchFilter == 'null' || searchFilter == '' ? entry != null : entry.title.toLowerCase().contains(searchFilter.toLowerCase()), - ).toList(); - }); + if(mounted) { + setState(() { + searchFilter = _searchController.text; + _searchedEntries = _catalogueEntries.where( + (entry) => searchFilter == 'null' || searchFilter == '' ? entry != null : entry.title.toLowerCase().contains(searchFilter.toLowerCase()), + ).toList(); + }); + } }); _scrollController.addListener(() { if(_scrollController?.position?.userScrollDirection == ScrollDirection.reverse) { @@ -125,9 +127,11 @@ class _CatalogueState extends State with TickerProviderStateMixi child: Elements.getIcon(_hideUnmonitored ? Icons.visibility_off : Icons.visibility), tooltip: 'Hide/Unhide Unmonitored Movies', onPressed: () { - setState(() { - _hideUnmonitored = !_hideUnmonitored; - }); + if(mounted) { + setState(() { + _hideUnmonitored = !_hideUnmonitored; + }); + } }, ), scale: _animationContoller, @@ -299,9 +303,11 @@ class _CatalogueState extends State with TickerProviderStateMixi tooltip: 'Toggle Monitored', onPressed: () async { if(await RadarrAPI.toggleMovieMonitored(entry.movieID, !entry.monitored)) { - setState(() { - entry.monitored = !entry.monitored; - }); + if(mounted) { + setState(() { + entry.monitored = !entry.monitored; + }); + } Notifications.showSnackBar( _scaffoldKey, entry.monitored ? 'Monitoring ${entry.title}' : 'No longer monitoring ${entry.title}', @@ -404,9 +410,11 @@ class _CatalogueState extends State with TickerProviderStateMixi if(result != null) { switch(result[0]) { case 'updated_movie': { - setState(() { - entry = result[1]; - }); + if(mounted) { + setState(() { + entry = result[1]; + }); + } Notifications.showSnackBar(_scaffoldKey, 'Updated ${entry.title}'); break; } @@ -418,7 +426,7 @@ class _CatalogueState extends State with TickerProviderStateMixi RadarrCatalogueEntry _entry = await RadarrAPI.getMovie(entry.movieID); _entry ??= _searchedEntries[index]; if(mounted) { - if(_searchedEntries[index]?.title == entry.title) { + if(_searchedEntries[index]?.title == entry.title && mounted) { setState(() { _searchedEntries[index] = _entry; }); diff --git a/lib/pages/radarr/subpages/details/edit.dart b/lib/pages/radarr/subpages/details/edit.dart index 1de409d8..64d8c6b3 100644 --- a/lib/pages/radarr/subpages/details/edit.dart +++ b/lib/pages/radarr/subpages/details/edit.dart @@ -136,9 +136,11 @@ class _RadarrEditMovieState extends State { trailing: Switch( value: _monitored, onChanged: (value) { - setState(() { - _monitored = value; - }); + if(mounted) { + setState(() { + _monitored = value; + }); + } }, ), ), @@ -152,9 +154,11 @@ class _RadarrEditMovieState extends State { trailing: Switch( value: _static, onChanged: (value) { - setState(() { - _static = value; - }); + if(mounted) { + setState(() { + _static = value; + }); + } }, ), ), @@ -171,7 +175,7 @@ class _RadarrEditMovieState extends State { ), onTap: () async { List _values = await SystemDialogs.showEditTextPrompt(context, 'Movie Path', prefill: _path); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _path = _values[1]; }); @@ -191,7 +195,7 @@ class _RadarrEditMovieState extends State { ), onTap: () async { List _values = await RadarrDialogs.showEditQualityProfilePrompt(context, _qualityProfiles); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _qualityProfile = _values[1]; }); @@ -211,7 +215,7 @@ class _RadarrEditMovieState extends State { ), onTap: () async { List _values = await RadarrDialogs.showMinimumAvailabilityPrompt(context, Constants.radarrMinAvailability); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _minimumAvailability = _values[1]; }); diff --git a/lib/pages/radarr/subpages/details/movie.dart b/lib/pages/radarr/subpages/details/movie.dart index 48aa2bdf..56a8e5ca 100644 --- a/lib/pages/radarr/subpages/details/movie.dart +++ b/lib/pages/radarr/subpages/details/movie.dart @@ -88,9 +88,11 @@ class _RadarrMovieDetailsState extends State { } void _needFetch() { - setState(() { - _loading = true; - }); + if(mounted) { + setState(() { + _loading = true; + }); + } } Future _refreshData() async { @@ -219,9 +221,11 @@ class _RadarrMovieDetailsState extends State { if(result != null) { switch(result[0]) { case 'updated_movie': { - setState(() { - entry = result[1]; - }); + if(mounted) { + setState(() { + entry = result[1]; + }); + } Notifications.showSnackBar(_scaffoldKey, 'Updated ${entry.title}'); break; } diff --git a/lib/pages/radarr/subpages/details/release.dart b/lib/pages/radarr/subpages/details/release.dart index bf7ee377..ccc0232e 100644 --- a/lib/pages/radarr/subpages/details/release.dart +++ b/lib/pages/radarr/subpages/details/release.dart @@ -125,6 +125,10 @@ class _RadarrReleaseInfoState extends State { onTap: () async { SystemDialogs.showTextPreviewPrompt(context, 'Release Title', entry.title); }, + trailing: IconButton( + icon: Elements.getIcon(Icons.arrow_forward_ios), + onPressed: null, + ), ), margin: Elements.getCardMargin(), elevation: 4.0, diff --git a/lib/pages/sabnzbd/sabnzbd.dart b/lib/pages/sabnzbd/sabnzbd.dart index b82ada5e..6bf56d8c 100644 --- a/lib/pages/sabnzbd/sabnzbd.dart +++ b/lib/pages/sabnzbd/sabnzbd.dart @@ -52,9 +52,11 @@ class _SABnzbdState extends State { ]; void _navOnTap(int index) { - setState(() { - _currIndex = index; - }); + if(mounted) { + setState(() { + _currIndex = index; + }); + } } void _refreshStatus(SABnzbdStatusEntry entry) { diff --git a/lib/pages/sabnzbd/subpages/history/details/details.dart b/lib/pages/sabnzbd/subpages/history/details/details.dart index 71eb4f9e..b0d446c6 100644 --- a/lib/pages/sabnzbd/subpages/history/details/details.dart +++ b/lib/pages/sabnzbd/subpages/history/details/details.dart @@ -179,6 +179,10 @@ class _SABnzbdHistoryDetailsState extends State { onTap: () async { await SystemDialogs.showTextPreviewPrompt(context, 'Storage Location', entry.storageLocation); }, + trailing: IconButton( + icon: Elements.getIcon(Icons.arrow_forward_ios), + onPressed: null, + ), ), margin: Elements.getCardMargin(), elevation: 4.0, @@ -192,6 +196,10 @@ class _SABnzbdHistoryDetailsState extends State { String data = stage['actions'].join(',\n').replaceAll('
', '.\n'); await SystemDialogs.showTextPreviewPrompt(context, '${stage['name']} Stage', data); }, + trailing: IconButton( + icon: Elements.getIcon(Icons.arrow_forward_ios), + onPressed: null, + ), ), margin: Elements.getCardMargin(), elevation: 4.0, diff --git a/lib/pages/sabnzbd/subpages/history/history.dart b/lib/pages/sabnzbd/subpages/history/history.dart index 1affa62a..e0c25a6d 100644 --- a/lib/pages/sabnzbd/subpages/history/history.dart +++ b/lib/pages/sabnzbd/subpages/history/history.dart @@ -143,9 +143,11 @@ class _SABnzbdHistoryState extends State with TickerProviderStat ) ), onPressed: () async { - setState(() { - _hideCompleted = !_hideCompleted; - }); + if(mounted) { + setState(() { + _hideCompleted = !_hideCompleted; + }); + } }, ), scale: _animationContoller, diff --git a/lib/pages/sabnzbd/subpages/queue/queue.dart b/lib/pages/sabnzbd/subpages/queue/queue.dart index 9892a604..b35382f2 100644 --- a/lib/pages/sabnzbd/subpages/queue/queue.dart +++ b/lib/pages/sabnzbd/subpages/queue/queue.dart @@ -183,13 +183,13 @@ class _SABnzbdQueueState extends State { ), onPressed: () async { if(_paused) { - if(await SABnzbdAPI.resumeQueue()) { + if(await SABnzbdAPI.resumeQueue() && mounted) { setState(() { _paused = false; }); } } else { - if(await SABnzbdAPI.pauseQueue()) { + if(await SABnzbdAPI.pauseQueue() && mounted) { setState(() { _paused = true; }); @@ -240,7 +240,7 @@ class _SABnzbdQueueState extends State { switch(values[1]) { case 'status': { if(entry.isPaused) { - if(await SABnzbdAPI.resumeSingleJob(entry.nzoId)) { + if(await SABnzbdAPI.resumeSingleJob(entry.nzoId) && mounted) { setState(() { entry.status = 'Downloading'; }); @@ -250,7 +250,7 @@ class _SABnzbdQueueState extends State { Notifications.showSnackBar(scaffoldKey, 'Failed to resume job'); } } else { - if(await SABnzbdAPI.pauseSingleJob(entry.nzoId)) { + if(await SABnzbdAPI.pauseSingleJob(entry.nzoId) && mounted) { setState(() { entry.status = 'Paused'; }); @@ -302,7 +302,7 @@ class _SABnzbdQueueState extends State { case 'rename': { values = await SABnzbdDialogs.showRenameJobPrompt(context, entry.name); if(values[0]) { - if(await SABnzbdAPI.renameJob(entry.nzoId, values[1])) { + if(await SABnzbdAPI.renameJob(entry.nzoId, values[1]) && mounted) { setState(() { entry.name = values[1]; }); diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 6cb3c9aa..e882f5d2 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -42,9 +42,11 @@ class _SettingsState extends State { ]; void _navOnTap(int index) { - setState(() { - _currIndex = index; - }); + if(mounted) { + setState(() { + _currIndex = index; + }); + } } @override diff --git a/lib/pages/settings/subpages/automation/tabs/lidarr.dart b/lib/pages/settings/subpages/automation/tabs/lidarr.dart index 52e6730b..01c47bae 100644 --- a/lib/pages/settings/subpages/automation/tabs/lidarr.dart +++ b/lib/pages/settings/subpages/automation/tabs/lidarr.dart @@ -37,9 +37,11 @@ class _LidarrState extends State { } void _refreshData() { - setState(() { - _lidarrValues = List.from(Values.lidarrValues); - }); + if(mounted) { + setState(() { + _lidarrValues = List.from(Values.lidarrValues); + }); + } } Widget _buildFloatingActionButton() { @@ -64,9 +66,11 @@ class _LidarrState extends State { trailing: Switch( value: _lidarrValues[0], onChanged: (value) { - setState(() { - _lidarrValues[0] = value; - }); + if(mounted) { + setState(() { + _lidarrValues[0] = value; + }); + } }, ), ), @@ -83,7 +87,7 @@ class _LidarrState extends State { ), onTap: () async { List _values = await SystemDialogs.showEditTextPrompt(context, 'Lidarr Host', prefill: _lidarrValues[1], showHostHint: true); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _lidarrValues[1] = _values[1]; }); @@ -103,7 +107,7 @@ class _LidarrState extends State { ), onTap: () async { List _values = await SystemDialogs.showEditTextPrompt(context, 'Lidarr API Key', prefill: _lidarrValues[2]); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _lidarrValues[2] = _values[1]; }); diff --git a/lib/pages/settings/subpages/automation/tabs/radarr.dart b/lib/pages/settings/subpages/automation/tabs/radarr.dart index 338cf63e..3eca3897 100644 --- a/lib/pages/settings/subpages/automation/tabs/radarr.dart +++ b/lib/pages/settings/subpages/automation/tabs/radarr.dart @@ -37,9 +37,11 @@ class _RadarrState extends State { } void _refreshData() { - setState(() { - _radarrValues = List.from(Values.radarrValues); - }); + if(mounted) { + setState(() { + _radarrValues = List.from(Values.radarrValues); + }); + } } Widget _buildFloatingActionButton() { @@ -64,9 +66,11 @@ class _RadarrState extends State { trailing: Switch( value: _radarrValues[0], onChanged: (value) { - setState(() { - _radarrValues[0] = value; - }); + if(mounted) { + setState(() { + _radarrValues[0] = value; + }); + } }, ), ), @@ -83,7 +87,7 @@ class _RadarrState extends State { ), onTap: () async { List _values = await SystemDialogs.showEditTextPrompt(context, 'Radarr Host', prefill: _radarrValues[1], showHostHint: true); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _radarrValues[1] = _values[1]; }); @@ -103,7 +107,7 @@ class _RadarrState extends State { ), onTap: () async { List _values = await SystemDialogs.showEditTextPrompt(context, 'Radarr API Key', prefill: _radarrValues[2]); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _radarrValues[2] = _values[1]; }); diff --git a/lib/pages/settings/subpages/automation/tabs/sonarr.dart b/lib/pages/settings/subpages/automation/tabs/sonarr.dart index 730444b9..3b401e69 100644 --- a/lib/pages/settings/subpages/automation/tabs/sonarr.dart +++ b/lib/pages/settings/subpages/automation/tabs/sonarr.dart @@ -37,9 +37,11 @@ class _SonarrState extends State { } void _refreshData() { - setState(() { - _sonarrValues = List.from(Values.sonarrValues); - }); + if(mounted) { + setState(() { + _sonarrValues = List.from(Values.sonarrValues); + }); + } } Widget _buildFloatingActionButton() { @@ -64,9 +66,11 @@ class _SonarrState extends State { trailing: Switch( value: _sonarrValues[0], onChanged: (value) { - setState(() { - _sonarrValues[0] = value; - }); + if(mounted) { + setState(() { + _sonarrValues[0] = value; + }); + } }, ), ), @@ -83,7 +87,7 @@ class _SonarrState extends State { ), onTap: () async { List _values = await SystemDialogs.showEditTextPrompt(context, 'Sonarr Host', prefill: _sonarrValues[1], showHostHint: true); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _sonarrValues[1] = _values[1]; }); @@ -103,7 +107,7 @@ class _SonarrState extends State { ), onTap: () async { List _values = await SystemDialogs.showEditTextPrompt(context, 'Sonarr API Key', prefill: _sonarrValues[2]); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _sonarrValues[2] = _values[1]; }); diff --git a/lib/pages/settings/subpages/clients/tabs/sabnzbd.dart b/lib/pages/settings/subpages/clients/tabs/sabnzbd.dart index e6060744..8a19c103 100644 --- a/lib/pages/settings/subpages/clients/tabs/sabnzbd.dart +++ b/lib/pages/settings/subpages/clients/tabs/sabnzbd.dart @@ -37,9 +37,11 @@ class _SABnzbdState extends State { } void _refreshData() { - setState(() { - _sabnzbdValues = List.from(Values.sabnzbdValues); - }); + if(mounted) { + setState(() { + _sabnzbdValues = List.from(Values.sabnzbdValues); + }); + } } Widget _buildFloatingActionButton() { @@ -64,9 +66,11 @@ class _SABnzbdState extends State { trailing: Switch( value: _sabnzbdValues[0], onChanged: (value) { - setState(() { - _sabnzbdValues[0] = value; - }); + if(mounted) { + setState(() { + _sabnzbdValues[0] = value; + }); + } }, ), ), @@ -83,7 +87,7 @@ class _SABnzbdState extends State { ), onTap: () async { List _values = await SystemDialogs.showEditTextPrompt(context, 'SABnzbd Host', prefill: _sabnzbdValues[1], showHostHint: true); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _sabnzbdValues[1] = _values[1]; }); @@ -103,7 +107,7 @@ class _SABnzbdState extends State { ), onTap: () async { List _values = await SystemDialogs.showEditTextPrompt(context, 'SABnzbd API Key', prefill: _sabnzbdValues[2]); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _sabnzbdValues[2] = _values[1]; }); diff --git a/lib/pages/settings/subpages/general/general.dart b/lib/pages/settings/subpages/general/general.dart index 3c057a63..b8d136fc 100644 --- a/lib/pages/settings/subpages/general/general.dart +++ b/lib/pages/settings/subpages/general/general.dart @@ -20,6 +20,7 @@ class _GeneralState extends State { final List _titles = [ 'Profile', 'Configuration', + 'Logs', 'LunaSea', ]; @@ -33,6 +34,7 @@ class _GeneralState extends State { children: [ Profile(), BackupRestore(), + Logs(), LunaSea(), ], ), diff --git a/lib/pages/settings/subpages/general/tabs/logs.dart b/lib/pages/settings/subpages/general/tabs/logs.dart new file mode 100644 index 00000000..aaef52e8 --- /dev/null +++ b/lib/pages/settings/subpages/general/tabs/logs.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/pages/settings/subpages/general/tabs/logs/type.dart'; +import 'package:lunasea/system/logger.dart'; +import 'package:lunasea/system/ui.dart'; + +class Logs extends StatelessWidget { + @override + Widget build(BuildContext context) { + return _LogsWidget(); + } +} + +class _LogsWidget extends StatefulWidget { + @override + State createState() { + return _LogsState(); + } +} + +class _LogsState extends State { + final _scaffoldKey = GlobalKey(); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + body: _logSettings(), + ); + } + + Widget _logSettings() { + return Scrollbar( + child: ListView( + children: [ + Card( + child: ListTile( + title: Elements.getTitle('View Logs'), + subtitle: Elements.getSubtitle('View all recorded logs'), + trailing: IconButton( + icon: Elements.getIcon(Icons.developer_mode), + onPressed: null, + ), + onTap: () async { + await _viewLogs(); + }, + ), + margin: Elements.getCardMargin(), + elevation: 4.0, + ), + Card( + child: ListTile( + title: Elements.getTitle('Export Logs'), + subtitle: Elements.getSubtitle('Export all recorded logs'), + trailing: IconButton( + icon: Elements.getIcon(Icons.file_download), + onPressed: null, + ), + onTap: () async { + List _values = await SystemDialogs.showExportLogsPrompt(context); + if(_values[0]) { + Logger.exportLogs(); + Notifications.showSnackBar(_scaffoldKey, 'Exported recorded logs to the filesystem'); + } + }, + ), + margin: Elements.getCardMargin(), + elevation: 4.0, + ), + Card( + child: ListTile( + title: Elements.getTitle('Clear Logs'), + subtitle: Elements.getSubtitle('Clear all recorded logs'), + trailing: IconButton( + icon: Elements.getIcon(Icons.delete), + onPressed: null, + ), + onTap: () async { + List _values = await SystemDialogs.showClearLogsPrompt(context); + if(_values[0]) { + Logger.clearLogs(); + Notifications.showSnackBar(_scaffoldKey, 'All recorded logs have been cleared'); + } + }, + ), + margin: Elements.getCardMargin(), + elevation: 4.0, + ), + ], + padding: Elements.getListViewPadding(), + ), + ); + } + + Future _viewLogs() async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => TypeLogs(), + ), + ); + } +} diff --git a/lib/pages/settings/subpages/general/tabs/logs/details.dart b/lib/pages/settings/subpages/general/tabs/logs/details.dart new file mode 100644 index 00000000..b4aa6a8d --- /dev/null +++ b/lib/pages/settings/subpages/general/tabs/logs/details.dart @@ -0,0 +1,153 @@ +import 'package:f_logs/f_logs.dart' as FLog; +import 'package:flutter/material.dart'; +import 'package:lunasea/system/ui.dart'; + +class LogDetails extends StatelessWidget { + final FLog.Log log; + + LogDetails({ + Key key, + @required this.log, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return _LogDetailsWidget(log: log); + } +} + +class _LogDetailsWidget extends StatefulWidget { + final FLog.Log log; + + _LogDetailsWidget({ + Key key, + @required this.log, + }) : super(key: key); + + @override + State createState() { + return _LogDetailsState(log: log); + } +} + +class _LogDetailsState extends State { + final FLog.Log log; + final _scaffoldKey = GlobalKey(); + + _LogDetailsState({ + Key key, + @required this.log, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: Navigation.getAppBar('Log Details', context), + body: _buildDetails(), + ); + } + + Widget _buildDetails() { + return Scrollbar( + child: ListView( + children: [ + _buildPackage(), + _buildMethod(), + _buildTimestamp(), + _buildMessage(), + if(log.exception != 'null') _buildException(), + if(log.stacktrace != 'null') _buildStackTrace(), + ], + padding: Elements.getListViewPadding(extraBottom: true), + ), + ); + } + + Widget _buildPackage() { + return Card( + child: ListTile( + title: Elements.getTitle('Class'), + subtitle: Elements.getSubtitle(log.className, preventOverflow: true), + ), + margin: Elements.getCardMargin(), + elevation: 4.0, + ); + } + + Widget _buildMethod() { + return Card( + child: ListTile( + title: Elements.getTitle('Method'), + subtitle: Elements.getSubtitle(log.methodName, preventOverflow: true), + ), + margin: Elements.getCardMargin(), + elevation: 4.0, + ); + } + + Widget _buildTimestamp() { + return Card( + child: ListTile( + title: Elements.getTitle('Timestamp'), + subtitle: Elements.getSubtitle(log.timestamp, preventOverflow: true), + ), + margin: Elements.getCardMargin(), + elevation: 4.0, + ); + } + + Widget _buildMessage() { + return Card( + child: ListTile( + title: Elements.getTitle('Message'), + subtitle: Elements.getSubtitle(log.text, preventOverflow: true), + onTap: () async { + SystemDialogs.showTextPreviewPrompt(context, 'Message', log.text); + }, + trailing: IconButton( + icon: Elements.getIcon(Icons.arrow_forward_ios), + onPressed: null, + ), + ), + margin: Elements.getCardMargin(), + elevation: 4.0, + ); + } + + Widget _buildException() { + return Card( + child: ListTile( + title: Elements.getTitle('Exception'), + subtitle: Elements.getSubtitle(log.exception, preventOverflow: true), + onTap: () async { + SystemDialogs.showTextPreviewPrompt(context, 'Exception', log.exception, alignLeft: true); + }, + trailing: IconButton( + icon: Elements.getIcon(Icons.arrow_forward_ios), + onPressed: null, + ), + ), + margin: Elements.getCardMargin(), + elevation: 4.0, + ); + } + + Widget _buildStackTrace() { + return Card( + child: ListTile( + title: Elements.getTitle('Stack Trace'), + subtitle: Elements.getSubtitle(log.stacktrace, preventOverflow: true), + onTap: () async { + SystemDialogs.showTextPreviewPrompt(context, 'Stack Trace', log.stacktrace, alignLeft: true); + }, + trailing: IconButton( + icon: Elements.getIcon(Icons.arrow_forward_ios), + onPressed: null, + ), + ), + margin: Elements.getCardMargin(), + elevation: 4.0, + ); + } +} diff --git a/lib/pages/settings/subpages/general/tabs/logs/type.dart b/lib/pages/settings/subpages/general/tabs/logs/type.dart new file mode 100644 index 00000000..3e6c2b91 --- /dev/null +++ b/lib/pages/settings/subpages/general/tabs/logs/type.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/pages/settings/subpages/general/tabs/logs/view.dart'; +import 'package:lunasea/system/ui.dart'; + +class TypeLogs extends StatelessWidget { + @override + Widget build(BuildContext context) { + return _TypeLogsWidget(); + } +} + +class _TypeLogsWidget extends StatefulWidget { + @override + State createState() { + return _TypeLogsState(); + } +} + +class _TypeLogsState extends State { + final _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: Navigation.getAppBar('Log Types', context), + body: _buildList(), + ); + } + + Widget _buildList() { + return Scrollbar( + child: ListView( + children: [ + Card( + child: ListTile( + title: Elements.getTitle('All Logs'), + subtitle: Elements.getSubtitle('View logs of all types'), + trailing: IconButton( + icon: Elements.getIcon(Icons.developer_mode), + onPressed: null, + ), + onTap: () async { + await _viewLogs('All'); + }, + ), + elevation: 4.0, + margin: Elements.getCardMargin(), + ), + Elements.getDivider(), + Card( + child: ListTile( + title: Elements.getTitle('Warning'), + subtitle: Elements.getSubtitle('View warning logs'), + trailing: IconButton( + icon: Elements.getIcon(Icons.warning), + onPressed: null, + ), + onTap: () async { + await _viewLogs('Warning'); + }, + ), + elevation: 4.0, + margin: Elements.getCardMargin(), + ), + Card( + child: ListTile( + title: Elements.getTitle('Error'), + subtitle: Elements.getSubtitle('View error logs'), + trailing: IconButton( + icon: Elements.getIcon(Icons.report), + onPressed: null, + ), + onTap: () async { + await _viewLogs('Error'); + }, + ), + elevation: 4.0, + margin: Elements.getCardMargin(), + ), + Card( + child: ListTile( + title: Elements.getTitle('Fatal'), + subtitle: Elements.getSubtitle('View fatal logs'), + trailing: IconButton( + icon: Elements.getIcon(Icons.new_releases), + onPressed: null, + ), + onTap: () async { + await _viewLogs('Fatal'); + }, + ), + elevation: 4.0, + margin: Elements.getCardMargin(), + ), + ], + padding: Elements.getListViewPadding(extraBottom: true), + ), + ); + } + + Future _viewLogs(String type) async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ViewLogs(type: type), + ), + ); + } +} diff --git a/lib/pages/settings/subpages/general/tabs/logs/view.dart b/lib/pages/settings/subpages/general/tabs/logs/view.dart new file mode 100644 index 00000000..6c4736e6 --- /dev/null +++ b/lib/pages/settings/subpages/general/tabs/logs/view.dart @@ -0,0 +1,189 @@ +import 'package:f_logs/f_logs.dart' as FLog; +import 'package:flutter/material.dart'; +import 'package:lunasea/pages/settings/subpages/general/tabs/logs/details.dart'; +import 'package:lunasea/system/constants.dart'; +import 'package:lunasea/system/ui.dart'; + +class ViewLogs extends StatelessWidget { + final String type; + + ViewLogs({ + Key key, + @required this.type, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return _ViewLogsWidget(type: type); + } +} + +class _ViewLogsWidget extends StatefulWidget { + final String type; + + _ViewLogsWidget({ + Key key, + @required this.type, + }) : super(key: key); + + @override + State createState() { + return _ViewLogsState(type: type); + } +} + +class _ViewLogsState extends State { + final String type; + final _scaffoldKey = GlobalKey(); + bool _loading = true; + List _logs; + List levels; + + _ViewLogsState({ + Key key, + @required this.type, + }); + + @override + void initState() { + super.initState(); + _setLogLevel(); + _fetchLogs(); + } + + void _setLogLevel() { + switch(type) { + case 'All': { levels = []; break; } + case 'Warning': { levels = [FLog.LogLevel.WARNING.toString()]; break; } + case 'Error': { levels = [FLog.LogLevel.ERROR.toString()]; break; } + case 'Fatal': { levels = [FLog.LogLevel.FATAL.toString()]; break; } + default: { levels = null; break; } + } + } + + TextSpan _getLogLevel(FLog.LogLevel level) { + switch(level.toString()) { + case 'LogLevel.WARNING': { + return TextSpan( + text: '\nWARNING', + style: TextStyle( + color: Colors.orange, + fontWeight: FontWeight.bold, + ), + ); + } + case 'LogLevel.ERROR': { + return TextSpan( + text: '\nERROR', + style: TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + ), + ); + } + case 'LogLevel.FATAL': { + return TextSpan( + text: '\nFATAL', + style: TextStyle( + color: Color(Constants.ACCENT_COLOR), + fontWeight: FontWeight.bold, + ), + ); + } + default: { + return TextSpan( + text: '\nUNKNOWN', + style: TextStyle( + color: Colors.blueGrey, + fontWeight: FontWeight.bold, + ), + ); + } + } + } + + Future _fetchLogs() async { + if(mounted) { + setState(() { + _loading = true; + }); + } + _logs = await FLog.FLog.getAllLogsByFilter( + logLevels: levels, + ); + _logs.sort((a, b) => b.timeInMillis.compareTo(a.timeInMillis)); + if(mounted) { + setState(() { + _loading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: Navigation.getAppBar('$type Logs', context), + body: _loading ? + Notifications.centeredMessage('Loading...') : + _logs == null ? + Notifications.centeredMessage('An Error has Occurred') : + _logs.length == 0 ? + Notifications.centeredMessage('No Logs Found') : + _buildLogList(), + ); + } + + Widget _buildLogList() { + return Scrollbar( + child: ListView.builder( + itemCount: _logs.length, + itemBuilder: (context, index) { + return _buildEntry(_logs[index]); + }, + padding: Elements.getListViewPadding(extraBottom: true), + ), + ); + } + + Widget _buildEntry(FLog.Log log) { + return Card( + child: ListTile( + title: Elements.getTitle(log.text), + subtitle: RichText( + text: TextSpan( + style: TextStyle( + color: Colors.white70, + letterSpacing: Constants.LETTER_SPACING, + ), + children: [ + TextSpan( + text: '${log.timestamp}', + ), + _getLogLevel(log.logLevel), + ], + ), + + ), + trailing: IconButton( + icon: Elements.getIcon(Icons.arrow_forward_ios), + onPressed: null, + ), + onTap: () async { + await _logDetails(log); + }, + contentPadding: Elements.getContentPadding(), + ), + margin: Elements.getCardMargin(), + elevation: 4.0, + ); + } + + Future _logDetails(FLog.Log log) async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => LogDetails(log: log), + ), + ); + } +} diff --git a/lib/pages/settings/subpages/general/tabs/lunasea.dart b/lib/pages/settings/subpages/general/tabs/lunasea.dart index 1f30c62a..e8370ff0 100644 --- a/lib/pages/settings/subpages/general/tabs/lunasea.dart +++ b/lib/pages/settings/subpages/general/tabs/lunasea.dart @@ -39,10 +39,12 @@ class _LunaSeaState extends State { void _fetchVersion() async { PackageInfo info = await PackageInfo.fromPlatform(); - setState(() { - _version = info.version; - _buildNumber = info.buildNumber; - }); + if(mounted) { + setState(() { + _version = info.version; + _buildNumber = info.buildNumber; + }); + } } Widget _systemLinks() { diff --git a/lib/pages/settings/subpages/general/tabs/profile.dart b/lib/pages/settings/subpages/general/tabs/profile.dart index eebeb1ed..fccb76db 100644 --- a/lib/pages/settings/subpages/general/tabs/profile.dart +++ b/lib/pages/settings/subpages/general/tabs/profile.dart @@ -29,10 +29,12 @@ class _ProfileState extends State { } void _refreshData() { - setState(() { - _enabled = Profiles.enabledProfile; - _profiles = Profiles.profileList; - }); + if(mounted) { + setState(() { + _enabled = Profiles.enabledProfile; + _profiles = Profiles.profileList; + }); + } } @override diff --git a/lib/pages/settings/subpages/general/tabs/tabs.dart b/lib/pages/settings/subpages/general/tabs/tabs.dart index bee8eedd..47a30255 100644 --- a/lib/pages/settings/subpages/general/tabs/tabs.dart +++ b/lib/pages/settings/subpages/general/tabs/tabs.dart @@ -1,3 +1,4 @@ export 'package:lunasea/pages/settings/subpages/general/tabs/backup_restore.dart'; +export 'package:lunasea/pages/settings/subpages/general/tabs/logs.dart'; export 'package:lunasea/pages/settings/subpages/general/tabs/lunasea.dart'; export 'package:lunasea/pages/settings/subpages/general/tabs/profile.dart'; diff --git a/lib/pages/sonarr/sonarr.dart b/lib/pages/sonarr/sonarr.dart index 9690d3ba..8ab4f4b3 100644 --- a/lib/pages/sonarr/sonarr.dart +++ b/lib/pages/sonarr/sonarr.dart @@ -53,9 +53,11 @@ class _SonarrState extends State { ]; void _navOnTap(int index) { - setState(() { - _currIndex = index; - }); + if(mounted) { + setState(() { + _currIndex = index; + }); + } } @override diff --git a/lib/pages/sonarr/subpages/catalogue/addseries/details.dart b/lib/pages/sonarr/subpages/catalogue/addseries/details.dart index 7bff7e77..6be98a3f 100644 --- a/lib/pages/sonarr/subpages/catalogue/addseries/details.dart +++ b/lib/pages/sonarr/subpages/catalogue/addseries/details.dart @@ -165,9 +165,11 @@ class _SonarrSeriesSearchDetailsState extends State { trailing: Switch( value: monitored, onChanged: (value) { - setState(() { - monitored = value; - }); + if(mounted) { + setState(() { + monitored = value; + }); + } }, ), ), @@ -181,9 +183,11 @@ class _SonarrSeriesSearchDetailsState extends State { trailing: Switch( value: seasonFolders, onChanged: (value) { - setState(() { - seasonFolders = value; - }); + if(mounted) { + setState(() { + seasonFolders = value; + }); + } }, ), ), @@ -200,7 +204,7 @@ class _SonarrSeriesSearchDetailsState extends State { ), onTap: () async { List _values = await SonarrDialogs.showEditRootFolderPrompt(context, _rootFolders); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _rootFolder = _values[1]; }); @@ -220,7 +224,7 @@ class _SonarrSeriesSearchDetailsState extends State { ), onTap: () async { List _values = await SonarrDialogs.showEditQualityProfilePrompt(context, _qualityProfiles); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _qualityProfile = _values[1]; }); @@ -240,7 +244,7 @@ class _SonarrSeriesSearchDetailsState extends State { ), onTap: () async { List _values = await SonarrDialogs.showEditSeriesTypePrompt(context); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _seriesType = _values[1]; }); diff --git a/lib/pages/sonarr/subpages/catalogue/catalogue.dart b/lib/pages/sonarr/subpages/catalogue/catalogue.dart index b9e9b90e..c0666958 100644 --- a/lib/pages/sonarr/subpages/catalogue/catalogue.dart +++ b/lib/pages/sonarr/subpages/catalogue/catalogue.dart @@ -60,12 +60,14 @@ class _CatalogueState extends State with TickerProviderStateMixi _animationContoller = AnimationController(vsync: this, duration: kThemeAnimationDuration); _animationContoller?.forward(); _searchController.addListener(() { - setState(() { - searchFilter = _searchController.text; - _searchedEntries = _catalogueEntries.where( - (entry) => searchFilter == 'null' || searchFilter == '' ? entry != null : entry.title.toLowerCase().contains(searchFilter.toLowerCase()), - ).toList(); - }); + if(mounted) { + setState(() { + searchFilter = _searchController.text; + _searchedEntries = _catalogueEntries.where( + (entry) => searchFilter == 'null' || searchFilter == '' ? entry != null : entry.title.toLowerCase().contains(searchFilter.toLowerCase()), + ).toList(); + }); + } }); _scrollController.addListener(() { if(_scrollController?.position?.userScrollDirection == ScrollDirection.reverse) { @@ -124,9 +126,11 @@ class _CatalogueState extends State with TickerProviderStateMixi child: Elements.getIcon(_hideUnmonitored ? Icons.visibility_off : Icons.visibility), tooltip: 'Hide/Unhide Unmonitored Series', onPressed: () { - setState(() { - _hideUnmonitored = !_hideUnmonitored; - }); + if(mounted) { + setState(() { + _hideUnmonitored = !_hideUnmonitored; + }); + } }, ), scale: _animationContoller, @@ -252,7 +256,7 @@ class _CatalogueState extends State with TickerProviderStateMixi ), tooltip: 'Toggle Monitored', onPressed: () async { - if(await SonarrAPI.toggleSeriesMonitored(entry.seriesID, !entry.monitored)) { + if(await SonarrAPI.toggleSeriesMonitored(entry.seriesID, !entry.monitored) && mounted) { setState(() { entry.monitored = !entry.monitored; }); @@ -355,9 +359,11 @@ class _CatalogueState extends State with TickerProviderStateMixi if(result != null) { switch(result[0]) { case 'updated_series': { - setState(() { - entry = result[1]; - }); + if(mounted) { + setState(() { + entry = result[1]; + }); + } Notifications.showSnackBar(_scaffoldKey, 'Updated ${entry.title}'); break; } @@ -369,7 +375,7 @@ class _CatalogueState extends State with TickerProviderStateMixi SonarrCatalogueEntry _entry = await SonarrAPI.getSeries(entry.seriesID); _entry ??= _searchedEntries[index]; if(mounted) { - if(_searchedEntries[index]?.title == entry.title) { + if(_searchedEntries[index]?.title == entry.title && mounted) { setState(() { _searchedEntries[index] = _entry; }); diff --git a/lib/pages/sonarr/subpages/details/edit.dart b/lib/pages/sonarr/subpages/details/edit.dart index 9a258c97..510e562f 100644 --- a/lib/pages/sonarr/subpages/details/edit.dart +++ b/lib/pages/sonarr/subpages/details/edit.dart @@ -132,9 +132,11 @@ class _SonarrEditSeriesState extends State { trailing: Switch( value: _monitored, onChanged: (value) { - setState(() { - _monitored = value; - }); + if(mounted) { + setState(() { + _monitored = value; + }); + } }, ), ), @@ -148,9 +150,11 @@ class _SonarrEditSeriesState extends State { trailing: Switch( value: _seasonFolder, onChanged: (value) { - setState(() { - _seasonFolder = value; - }); + if(mounted) { + setState(() { + _seasonFolder = value; + }); + } }, ), ), @@ -167,7 +171,7 @@ class _SonarrEditSeriesState extends State { ), onTap: () async { List _values = await SystemDialogs.showEditTextPrompt(context, 'Series Path', prefill: _path); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _path = _values[1]; }); @@ -187,7 +191,7 @@ class _SonarrEditSeriesState extends State { ), onTap: () async { List _values = await SonarrDialogs.showEditQualityProfilePrompt(context, _qualityProfiles); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _qualityProfile = _values[1]; }); @@ -207,7 +211,7 @@ class _SonarrEditSeriesState extends State { ), onTap: () async { List _values = await SonarrDialogs.showEditSeriesTypePrompt(context); - if(_values[0]) { + if(_values[0] && mounted) { setState(() { _seriesType = _values[1]; }); diff --git a/lib/pages/sonarr/subpages/details/release.dart b/lib/pages/sonarr/subpages/details/release.dart index 7029422c..6f15f704 100644 --- a/lib/pages/sonarr/subpages/details/release.dart +++ b/lib/pages/sonarr/subpages/details/release.dart @@ -125,6 +125,10 @@ class _SonarrReleaseInfoState extends State { onTap: () async { SystemDialogs.showTextPreviewPrompt(context, 'Release Title', entry.title); }, + trailing: IconButton( + icon: Elements.getIcon(Icons.arrow_forward_ios), + onPressed: null, + ), ), margin: Elements.getCardMargin(), elevation: 4.0, diff --git a/lib/pages/sonarr/subpages/details/season.dart b/lib/pages/sonarr/subpages/details/season.dart index 0b13bafa..d962167a 100644 --- a/lib/pages/sonarr/subpages/details/season.dart +++ b/lib/pages/sonarr/subpages/details/season.dart @@ -138,7 +138,9 @@ class _SonarrSeasonDetailsState extends State { } void _selectedCallback() { - setState(() {}); + if(mounted) { + setState(() {}); + } } Future> _searchEpisodes() async { @@ -239,7 +241,9 @@ class _SonarrSeasonDetailsState extends State { } card.entry.isSelected = flag; } - setState(() {}); + if(mounted) { + setState(() {}); + } }, onLongPress: () async { List values = await SonarrDialogs.showSearchSeasonPrompt(context, season); diff --git a/lib/pages/sonarr/subpages/details/show.dart b/lib/pages/sonarr/subpages/details/show.dart index e863c87f..dd709fe0 100644 --- a/lib/pages/sonarr/subpages/details/show.dart +++ b/lib/pages/sonarr/subpages/details/show.dart @@ -415,14 +415,14 @@ class _SonarrShowDetailsState extends State { seasonNumber = -1; episodeCount = entry.episodeCount; availableEpisodeCount = entry.episodeFileCount; - percentage = (entry.episodeCount == 0 || entry.episodeFileCount == null || entry.episodeCount == null) ? 0 : ((entry.episodeFileCount/entry.episodeCount)*100).round(); + percentage = (episodeCount == null || availableEpisodeCount == null || episodeCount == 0) ? 0 : ((availableEpisodeCount/episodeCount)*100).round(); isMonitored = entry.monitored; } else { season = entry.seasonData[entry.seasonData.length-index]; seasonNumber = season['seasonNumber']; episodeCount = season['statistics']['totalEpisodeCount']; availableEpisodeCount = season['statistics']['episodeFileCount']; - percentage = (episodeCount == 0 || availableEpisodeCount == null || episodeCount == null) ? 0 : ((availableEpisodeCount/episodeCount)*100).round(); + percentage = (episodeCount == null || availableEpisodeCount == null || episodeCount == 0) ? 0 : ((availableEpisodeCount/episodeCount)*100).round(); isMonitored = season['monitored']; } return Card( @@ -468,7 +468,7 @@ class _SonarrShowDetailsState extends State { onPressed: seasonNumber >= 0 ? () async { String snackMsg = seasonNumber == 0 ? 'specials' : 'season $seasonNumber'; - if(await SonarrAPI.toggleSeasonMonitored(entry.seriesID, seasonNumber, !season['monitored'])) { + if(await SonarrAPI.toggleSeasonMonitored(entry.seriesID, seasonNumber, !season['monitored']) && mounted) { setState(() { season['monitored'] = !season['monitored']; }); @@ -626,9 +626,11 @@ class _SonarrShowDetailsState extends State { if(result != null) { switch(result[0]) { case 'updated_series': { - setState(() { - entry = result[1]; - }); + if(mounted) { + setState(() { + entry = result[1]; + }); + } Notifications.showSnackBar(_scaffoldKey, 'Updated ${entry.title}'); break; } diff --git a/lib/system/logger.dart b/lib/system/logger.dart new file mode 100644 index 00000000..0fbd44d3 --- /dev/null +++ b/lib/system/logger.dart @@ -0,0 +1,62 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:stack_trace/stack_trace.dart'; +import 'package:f_logs/f_logs.dart'; + +class Logger { + Logger._(); + + static void initialize() { + LogsConfig config = FLog.getDefaultConfigurations() + ..formatType = FormatType.FORMAT_SQUARE + ..timestampFormat = 'MMMM dd, y - hh:mm:ss a'; + FLog.applyConfigurations(config); + FlutterError.onError = (FlutterErrorDetails details) async { + bool inDebugMode = false; + assert(inDebugMode = true); + if (inDebugMode) { + FlutterError.dumpErrorToConsole(details); + } + Zone.current.handleUncaughtError(details.exception, details.stack); + }; + } + + static void warning(String className, String methodName, String text, {DataLogType type = DataLogType.DEFAULT}) { + FLog.warning( + className: className, + methodName: methodName, + text: text, + dataLogType: type.toString(), + ); + } + + static void error(String className, String methodName, String text, Object error, StackTrace trace, {DataLogType type = DataLogType.DEFAULT}) { + FLog.error( + className: className, + methodName: methodName, + text: text, + exception: Exception(error.toString()), + stacktrace: trace, + dataLogType: type.toString(), + ); + } + + static void fatal(Object error, StackTrace trace, {DataLogType type = DataLogType.DEFAULT}) { + FLog.fatal( + className: Trace.from(trace).frames[1].uri.toString() ?? 'Unknown', + methodName: Trace.from(trace).frames[1].member.toString() ?? 'Unknown', + text: error.toString(), + exception: Exception(error.toString()), + stacktrace: trace, + dataLogType: type.toString(), + ); + } + + static void exportLogs() { + FLog.exportLogs(); + } + + static void clearLogs() { + FLog.clearLogs(); + } +} diff --git a/lib/system/ui/dialogs/system.dart b/lib/system/ui/dialogs/system.dart index 2e3f115d..8a6d1b88 100644 --- a/lib/system/ui/dialogs/system.dart +++ b/lib/system/ui/dialogs/system.dart @@ -827,7 +827,7 @@ class SystemDialogs { return [flag, textController.text]; } - static Future showTextPreviewPrompt(BuildContext context, String title, String text) async { + static Future showTextPreviewPrompt(BuildContext context, String title, String text, {bool alignLeft = false}) async { await showDialog( context: context, builder: (BuildContext context) { @@ -860,7 +860,7 @@ class SystemDialogs { style: TextStyle( color: Colors.white, ), - textAlign: TextAlign.center, + textAlign: alignLeft ? TextAlign.left : TextAlign.center, ), ], ), @@ -870,4 +870,130 @@ class SystemDialogs { } ); } + + static Future> showClearLogsPrompt(BuildContext context) async { + bool flag = false; + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + 'Clear Logs', + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + actions: [ + FlatButton( + child: Text( + 'Cancel', + style: TextStyle( + color: Colors.white, + ), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + FlatButton( + child: Text( + 'Clear', + style: TextStyle( + color: Colors.red, + ), + ), + onPressed: () { + flag = true; + Navigator.of(context).pop(); + }, + ), + ], + content: SingleChildScrollView( + child: Text( + 'Are you sure you want to clear all recorded logs?\n\nLogs can be useful for bug reports and debugging.', + style: TextStyle( + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + padding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0), + ), + ); + } + ); + return [flag]; + } + + static Future> showExportLogsPrompt(BuildContext context) async { + bool flag = false; + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + 'Export Logs', + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + actions: [ + FlatButton( + child: Text( + 'Cancel', + style: TextStyle( + color: Colors.white, + ), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + FlatButton( + child: Text( + 'Export', + style: TextStyle( + color: Color(Constants.ACCENT_COLOR), + ), + ), + onPressed: () { + flag = true; + Navigator.of(context).pop(); + }, + ), + ], + content: SingleChildScrollView( + child: RichText( + text: TextSpan( + style: TextStyle( + color: Colors.white, + letterSpacing: Constants.LETTER_SPACING, + fontSize: 16.0, + ), + children: [ + TextSpan( + text: 'Are you sure you want to export all recorded logs to the filesystem?\n\nThe exported logs can be found in\n', + ), + TextSpan( + text: 'On My /LunaSea/FLogs', + style: TextStyle( + color: Color(Constants.ACCENT_COLOR), + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: '.', + ), + ] + ), + textAlign: TextAlign.center, + ), + padding: EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0), + ), + ); + } + ); + return [flag]; + } } diff --git a/pubspec.lock b/pubspec.lock index b2b5b822..4f377f5f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -99,6 +99,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.0" + f_logs: + dependency: "direct main" + description: + name: f_logs + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.2" file_picker: dependency: "direct main" description: @@ -247,6 +254,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1+1" + permission_handler: + dependency: transitive + description: + name: permission_handler + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" petitparser: dependency: transitive description: @@ -282,6 +296,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.5" + sembast: + dependency: transitive + description: + name: sembast + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2+2" shared_preferences: dependency: "direct main" description: @@ -323,7 +344,7 @@ packages: source: hosted version: "1.5.5" stack_trace: - dependency: transitive + dependency: "direct main" description: name: stack_trace url: "https://pub.dartlang.org" @@ -343,6 +364,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.5" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" term_glyph: dependency: transitive description: @@ -413,6 +441,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.5.0" + xxtea: + dependency: transitive + description: + name: xxtea + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8542033f..d7439637 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,12 +13,14 @@ dependencies: sdk: flutter flutter_advanced_networkimage: ^0.6.2 flutter_sticky_header: ^0.4.0 + f_logs: ^1.2.2 http: ^0.12.0+2 intl: ^0.16.0 package_info: ^0.4.0+13 path_provider: ^1.5.1 percent_indicator: ^2.1.1+1 shared_preferences: ^0.5.6 + stack_trace: ^1.9.3 url_launcher: ^5.4.1 validators: ^2.0.0+1