diff --git a/assets/LunaBrandIcons.ttf b/assets/LunaBrandIcons.ttf index ec33402df8..31a170af45 100644 Binary files a/assets/LunaBrandIcons.ttf and b/assets/LunaBrandIcons.ttf differ diff --git a/assets/images/brands/config.json b/assets/images/brands/config.json index f30a788763..dafee7b3c7 100644 --- a/assets/images/brands/config.json +++ b/assets/images/brands/config.json @@ -229,6 +229,20 @@ "search": [ "tautulli" ] + }, + { + "uid": "6f8328fd95cff8996f124ec0ef82a3b3", + "css": "readarr", + "code": 59408, + "src": "custom_icons", + "selected": true, + "svg": { + "path": "M171.6 665.3C171.6 665.3 166.6 317.6 166 317 166.6 265.1 183.1 270.3 195.8 268.7 389.7 279.3 460.3 338.3 464.7 340.5 477.7 345.4 485.1 371.5 484.5 370.9 485.1 371.5 488.8 781.6 488.2 781 491.3 802.9 475.2 801.5 474.6 800.9 352.5 705 184 680.9 183.4 680.2 171.6 679.5 171.6 665.3 171.6 665.3ZM826 666.1C826 666.1 830.9 317.6 831.5 316.9 830.9 264.9 814.5 270.1 801.8 268.6 607.9 279.2 537.2 338.3 532.9 340.5 519.9 345.4 512.4 371.6 513.1 371 512.4 371.6 508.7 782.7 509.3 782.1 506.2 804.1 522.3 802.6 523 802 645 705.9 813.6 681.7 814.2 681.1 826 680.3 826 666.1 826 666.1ZM169.1 721.3C169.1 721.3 164 373.6 163.4 373 164 321.1 180.8 326.2 193.6 324.7 390.7 335.3 462.5 394.3 466.9 396.5 480.1 401.4 487.6 427.5 487 426.9 487.6 427.5 491.4 837.6 490.8 837 493.9 858.9 477.6 857.5 476.9 856.8 352.9 761 181.7 736.9 181 736.2 169.1 735.5 169.1 721.3 169.1 721.3ZM828.5 721.7C828.5 721.7 833.5 373.2 834.1 372.6 833.5 320.6 816.8 325.8 803.9 324.2 606.8 334.8 535.1 394 530.7 396.2 517.4 401.1 509.9 427.3 510.5 426.6 509.9 427.3 506.1 838.4 506.7 837.7 503.6 859.7 520 858.3 520.6 857.7 644.6 761.6 815.9 737.4 816.5 736.7 828.5 736 828.5 721.7 828.5 721.7Z", + "width": 1000 + }, + "search": [ + "readarr" + ] } ] } \ No newline at end of file diff --git a/assets/images/brands/readarr.svg b/assets/images/brands/readarr.svg new file mode 100644 index 0000000000..b8ec609ba9 --- /dev/null +++ b/assets/images/brands/readarr.svg @@ -0,0 +1,23 @@ + + + + + background + + + + + + + Layer 1 + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/service_goodreads.png b/assets/images/service_goodreads.png new file mode 100644 index 0000000000..d6fa994a5c Binary files /dev/null and b/assets/images/service_goodreads.png differ diff --git a/assets/localization/de.json b/assets/localization/de.json index ddbc2cde08..9a767b1402 100644 --- a/assets/localization/de.json +++ b/assets/localization/de.json @@ -1,205 +1,25 @@ { - "settings.EncryptionKey": "Sicherungsphrase", - "settings.EnabledProfile": "Aktives Profil", - "settings.DismissBannersHint2": "Die Kurzinfo-Banner werden dir Tipps und Hinweise zu verfügbaren Funktionen in LunaSea geben.", - "settings.DismissBannersHint1": "Bist du sicher dass du alle Kurzinfo-Banner ausblenden möchtest?", - "settings.DeleteHeader": "Lösche Kopfdaten", - "settings.CustomHeaders": "Benutzerdefinierte Kopfdaten", - "settings.CustomHeader": "Benutzerdefinierte Kopfdaten", - "settings.AddHeader": "Kopfdaten hinzufügen", - "settings.DismissBanners": "Banner ausblenden", - "settings.DeleteProfile": "Profil löschen", - "settings.DeleteIndexerHint1": "Bist du sicher dass du diesen Indexer löschen möchtest?", - "settings.DeleteIndexer": "Indexer löschen", - "settings.DeleteHeaderHint1": "Bist du sicher dass du diese Kopfdaten löschen möchtest?", - "settings.DecryptBackupHint1": "Bitte gib die Sicherungsphrase für diese Sicherung ein.", - "settings.DecryptBackup": "Sicherung entschlüsseln", - "settings.Custom": "Benutzerdefiniert…", - "settings.ClearLogsHint1": "Bist du sicher dass du alle erfassten Protokolle entfernen möchtest?\n\nProtokolle können für Fehlerreports und Fehlersuche nützlich sein.", - "settings.ClearLogs": "Protokolle löschen", - "settings.ClearConfigurationHint3": "Du wirst abgemeldet wenn du bei deinem LunaSea Konto angemeldet bist.", - "settings.ClearConfigurationHint2": "Du wirst ganz von Vorne beginnen. Bitte stelle sicher dass du deine jetzige Konfiguration gesichert hast!", - "settings.ClearConfigurationHint1": "Bist du sicher das du die Konfiguration entfernen möchtest?", - "settings.ClearConfiguration": "Entferne Konfiguration", - "settings.BroadcastAddressValidation": "Ungültige Broadcast Adresse", - "settings.BroadcastAddressHint3": "Bei einer Beispiel IP-Adresse eines Rechners von 192.168.1.111 ist die daraus resultierende Broadcast Adresse die 192.168.1.255", - "settings.BroadcastAddressHint2": "Typischerweise ist dies die IP Adresse deines Rechners mit dem letzten Oktett als 255 ersetzt", - "settings.BroadcastAddressHint1": "Dies ist die Broadcast Adresse für dein lokales Netzwerk", - "settings.BroadcastAddress": "Broadcast-Adresse", - "settings.BasicAuthenticationHint3": "Der Benutzername und das Passwort werden automatisch in Base64-Kodierung umgewandelt", - "settings.BasicAuthenticationHint2": "Das Passwort darf einen Doppelpunkt enthalten", - "settings.BasicAuthenticationHint1": "Der Benutzername darf keinen Doppelpunkt enthalten", - "settings.BasicAuthentication": "Basic Authentifizierung", - "settings.BannersNotificationModuleSupportBody": "Notifikationen basierend auf Webhooks werden zur Zeit nur von den unten aufgelisteten Modulen unterstützt.\n\nZusätzliche Modulunterstützung ist für die Zukunft geplant!", - "settings.BannersNotificationModuleSupportHeader": "Unterstützte Module", - "settings.BackupList": "Liste der Sicherungen", - "settings.BackupConfigurationHint2": "Die Sicherungsphrase muss mindestens 8 Zeichen enthalten", - "settings.BackupConfigurationHint1": "Alle Sicherungen werden vor dem Exportieren verschlüsselt", - "settings.BackupConfiguration": "Konfiguration sichern", - "settings.AddProfile": "Profil hinzufügen", - "settings.AccountHelpHint1": "LunaSea bietet ein kostenloses Konto an um deine Konfiguration in der Cloud zu speichern. Zusätzliche Funktionen folgen in der Zukunft!", - "settings.AccountHelp": "LunaSea-Konto", - "settings.MustBeValueBetween": "Muss ein Wert zwischen {} und {} sein", - "settings.UsernameValidation": "Benutzername erforderlich", - "settings.Username": "Benutzername", - "settings.StartingType": "Starttyp", - "settings.StartingSize": "Startgröße", - "settings.StartingDay": "Starttag", - "settings.ShowCalendarEntries": "Zeige {} Kalendereinträge", - "settings.SignOutHint1": "Bist du sicher dass du dich von deinem LunaSea Konto abmelden möchtest?", - "settings.SignOut": "Abmelden", - "settings.RenameProfile": "Profil umbenennen", - "settings.ProfileNameRequired": "Profilname erforderlich", - "settings.ProfileName": "Profilname", - "settings.ProfileAlreadyExists": "Profil existiert bereits", - "settings.PasswordValidation": "Passwort erforderlich", - "settings.Password": "Passwort", - "settings.OpenLinksIn": "Öffne Links in…", - "settings.NoHeadersAdded": "Keine Kopfdaten hinzugefügt", - "settings.NoBackupsFound": "Keine Sicherungen gefunden", - "settings.MinimumCharacters": "Minimum von {} Zeichen", - "settings.MACAddressValidation": "Ungültige MAC Adresse", - "settings.MACAddressHint4": "Jedes Hexadezimaloktett sollte durch einen Doppelpunkt getrennt sein", - "settings.MACAddressHint3": "Hexadezimalzahlen rangieren von 0-9 und A-F", - "settings.MACAddressHint2": "MAC Adressen bestehen aus sechs zweistelligen Hexadezimalhäppchen (ein Oktett)", - "settings.MACAddressHint1": "Dies ist die MAC Adresse des Rechners den du aufwecken möchtest", - "settings.MACAddress": "MAC Adresse", - "settings.Language": "Sprache", - "settings.ImageBackgroundOpacityHint2": "Um die Verwendung von Hintergrundbildern komplett auszuschalten, setze den Wert auf 0.", - "settings.ImageBackgroundOpacityHint1": "Wähle die Deckkraft für Hintergrundbilder.", - "settings.ImageBackgroundOpacity": "Deckkraft des Hintergrundbilds", - "settings.HostValidation": "Host muss http:// oder https:// mit angeben", - "settings.HostHint5": "Um Basic Authentifizierung hinzuzufügen, benutze bitte die Funktion für benutzerdefinierte Kopfdaten", - "settings.HostHint4": "Bitte gebe den Port mit an wenn kein Reverse Proxy verwendet wird", - "settings.HostHint3": "Verwende nicht localhost oder 127.0.0.1", - "settings.HostHint2": "Du musst entweder http:// oder https:// mit angeben", - "settings.HostHint1": "Dies ist die URL mit der du die Weboberfläche des Service aufrufst", - "settings.Host": "Host", - "settings.HeaderValueValidation": "Kopfdaten Wert erforderlich", - "settings.HeaderValue": "Kopfdaten Wert", - "settings.HeaderKeyValidation": "Kopfdaten Schlüssel erforderlich", - "settings.HeaderKey": "Kopfdaten Schlüssel", - "settings.HeaderDeleted": "Kopfdaten gelöscht", - "settings.HeaderAdded": "Kopfdaten hinzugefügt", - "settings.SystemDescription": "System-Werkzeuge und Info", - "settings.System": "System", - "settings.ResourcesDescription": "Nützliche Ressourcen und Links", - "settings.Resources": "Ressourcen", - "settings.ProfilesDescription": "Verwalte deine Profile", - "settings.Profiles": "Profile", - "settings.NotificationsDescription": "Richte Webhooks für Push-Benachrichtigungen ein", - "settings.Notifications": "Benachrichtigungen", - "settings.DonationsDescription": "Spende an den Entwickler", - "settings.Donations": "Spenden", - "settings.DebugMenuDescription": "Fehlersuche- und Entwicklungswerkzeuge", - "settings.DebugMenu": "Fehlersuchemenü", - "settings.ConfigurationDescription": "Konfiguriere und richte LunaSea ein", - "settings.Configuration": "Konfiguration", - "settings.AccountDescription": "Dein LunaSea-Konto", - "settings.Account": "Konto", - "settings.TestConnection": "Verbindung testen", - "settings.SignIn": "Anmelden", - "settings.SignedOutSuccessMessage": "Von deinem LunaSea Konto abgemeldet", - "settings.SignedOutSuccess": "Abgemeldet", - "settings.SignedOutFailure": "Abmelden fehlgeschlagen", - "settings.SignedInSuccess": "Anmelden erfolgreich", - "settings.SignedInFailure": "Anmelden fehlgeschlagen", - "settings.RestoreFromCloudSuccessMessage": "Deine Konfiguration wurde wiederhergestellt", - "settings.RestoreFromCloudSuccess": "Wiederherstellen erfolgreich", - "settings.RestoreFromCloudFailure": "Wiederherstellen fehlgeschlagen", - "settings.RestoreFromCloudDescription": "Konfigurationsdaten wiederherstellen", - "settings.RestoreFromCloud": "Sicherung aus der Cloud wiederherstellen", - "settings.ResetPassword": "Passwort zurücksetzen", - "settings.RegisteredSuccess": "Registriert", - "settings.RegisteredFailure": "Registrieren fehlgeschlagen", - "settings.Register": "Registrieren", - "settings.QuickActionsDescription": "Schnellaktionen auf dem Home-Bildschirm", - "settings.QuickActions": "Schnellaktionen", - "settings.LocalizationDescription": "Passe die Sprache an deine Region an", - "settings.Localization": "Übersetzung", - "settings.InvalidPasswordMessage": "Das Passwort ist ungültig", - "settings.InvalidPassword": "Ungültiges Passwort", - "settings.InvalidEmailMessage": "Die E-Mail-Adresse ist ungültig", - "settings.InvalidEmail": "Ungültige E-Mail Addresse", - "settings.HostRequiredMessage": "Host ist erforderlich um sich mit {} zu verbinden", - "settings.HostRequired": "Host erforderlich", - "settings.ForgotYourPassword": "Passwort vergessen?", - "settings.EmailSentSuccessMessage": "Eine E-Mail zum Zurücksetzen deines Passworts wurde gesendet!", - "settings.EmailSentSuccess": "E-Mail gesendet", - "settings.EmailSentFailure": "Passwort zurücksetzen fehlgeschlagen", - "settings.Email": "E-Mail", - "settings.DrawerDescription": "Passe das Seitenmenü an", - "settings.Drawer": "Seitenmenü", - "settings.DeleteCloudBackupSuccess": "Gelöscht", - "settings.DeleteCloudBackupFailure": "Löschen fehlgeschlagen", - "settings.DeleteCloudBackupDescription": "Lösche eine Konfigurationsdatei", - "settings.DeleteCloudBackup": "Lösche Cloudsicherung", - "settings.DefaultPage": "Standardseite", - "settings.CustomHeadersDescription": "Füge benutzerdefinierte Kopfdaten zu Requests hinzu", - "settings.ConnectionTestFailed": "Verbindungstest fehlgeschlagen", - "settings.ConnectionDetailsDescription": "Verbindungsdetails für {}", - "settings.ConnectionDetails": "Verbindungsdetails", - "settings.ConnectedSuccessfullyMessage": "{} ist bereit für die Verwendung mit LunaSea!", - "settings.ConnectedSuccessfully": "Verbunden", - "settings.ConfigureModule": "Konfiguriere {}", - "settings.BackupToCloudSuccess": "Sicherung erfolgreich", - "settings.BackupToCloudFailure": "Sicherung fehlgeschlagen", - "settings.BackupToCloudDescription": "Sicherung der Konfigurationsdaten", - "settings.BackupToCloud": "Cloud-Sicherung", - "settings.BackgroundImageOpacity": "Deckkraft des Hintergrundbilds", - "settings.AutomaticallyManageOrderDescription": "Liste Module alphabetisch auf", - "settings.AutomaticallyManageOrder": "Automatische Sortierung", - "settings.AppearanceDescription": "Passe das Aussehen an", - "settings.Appearance": "Aussehen", - "settings.ApiKeyRequiredMessage": "API Schlüssel ist erforderlich um sich mit {} zu verbinden", - "settings.ApiKeyRequired": "API Schlüssel erforderlich", - "settings.ApiKey": "API Schlüssel", - "settings.AmoledThemeDescription": "Pures schwarzes dunkles Design", - "settings.AmoledThemeBordersDescription": "Fügt subtile Ränder zur UI hinzu", - "settings.AmoledThemeBorders": "AMOLED Design Ränder", - "settings.AmoledTheme": "AMOLED Design", - "settings.NoExternalModulesFound": "Keine externen Module gefunden", - "settings.ModuleNotFound": "Modul nicht gefunden", - "settings.EditModule": "Modul editieren", - "settings.DisplayName": "Anzeigename", - "settings.DeleteModuleSuccess": "Modul gelöscht", - "settings.DeleteModuleHint1": "Bist du sicher dass du dieses externe Modul löschen möchtest?", - "settings.DeleteModule": "Modul löschen", - "settings.AllFieldsAreRequired": "Alle Felder sind erforderlich", - "settings.AddModuleSuccess": "Modul hinzugefügt", - "settings.AddModuleFailed": "Module hinzufügen fehlgeschlagen", - "settings.AddModule": "Modul hinzufügen", - "settings.AccountDeleted": "Konto gelöscht", - "settings.AccountDeletedMessage": "LunaSea Konto gelöscht", - "settings.AccountSettings": "Konto-Einstellungen", - "settings.DeleteAccountHint1": "Bist du sicher dass du dein LunaSea-Konto löschen möchtest?", - "settings.DeleteAccountHint2": "Dies wird ebenfalls sämtliche mit diesem Konto verknüpften Daten sowie Cloudsicherungen löschen.", - "settings.StartingView": "Startansicht", - "settings.Add": "Hinzufügen", - "settings.AddProfileDescription": "Neues Profil hinzufügen", - "settings.DefaultPages": "Standardseiten", - "settings.DefaultPagesDescription": "Standardseiten festlegen", - "settings.RenameProfileDescription": "Existierendes Profil umbenennen", - "settings.ClearImageCacheHint2": "Bilder für eine große Mediathek erneut herunterzuladen kann eine große Menge Daten verbrauchen.", - "settings.DefaultSortingAndFiltering": "Standardsortierung & -filterung", - "settings.DefaultSortingAndFilteringDescription": "Standardsortierungs & -filterungsmethoden festlegen", - "settings.DeleteAccount": "Konto löschen", - "settings.DeleteAccountDescription": "Konto permanent löschen", - "settings.DeleteAccountWarning1": "Dies ist nicht rückgängig zu machen", - "settings.FailedToDeleteAccount": "Löschen des Kontos fehlgeschlagen", - "settings.ClearImageCache": "Entferne Bild-Zwischenspeicher", - "settings.ClearImageCacheHint1": "Bist du sicher dass du alle Bilder aus dem Zwischenspeicher entfernen möchtest?", - "settings.DeleteProfileDescription": "Lösche ein existierendes Profil", - "settings.DefaultOptions": "Standardeinstellungen", - "settings.DefaultOptionsDescription": "Sortierung, Filterung und Ansichtsoptionen bearbeiten", - "settings.Network": "Netzwerk", - "settings.NetworkDescription": "Netzwerkeinstellungen vornehmen", - "settings.TLSCertificateValidation": "SSL-Zertifikate überprüfen", - "settings.TLSCertificateValidationDescription": "Einstellen, ob bei SSL-Verbindungen die Zertifikate überprüft werden", - "settings.FilterCategory": "Kategorie filtern", - "settings.SortCategory": "Kategorie sortieren", - "settings.SortDirection": "Sortierrichtung", - "settings.ViewRecentChanges": "Letzte Änderungen anzeigen", + "dashboard.Wednesday": "Mittwoch", + "dashboard.TwoWeeks": "Zwei Wochen", + "dashboard.Tuesday": "Dienstag", + "dashboard.Thursday": "Donnerstag", + "dashboard.Sunday": "Sonntag", + "dashboard.Schedule": "Übersicht", + "dashboard.Saturday": "Samstag", + "dashboard.PastDaysDescription": "Wähle die Anzahl an Tagen in der Vergangenheit für die Kalendereinträge abgerufen werden sollen.", + "dashboard.PastDays": "Vergangene Tage", + "dashboard.OneWeek": "Eine Woche", + "dashboard.OneMonth": "Ein Monat", + "dashboard.NoNewContent": "Keine neuen Inhalte", + "dashboard.Monday": "Montag", + "dashboard.MinimumOfOneDay": "Minimum von 1 Tag", + "dashboard.FutureDaysDescription": "Wähle die Anzahl an Tagen in der Zukunft für die Kalendereinträge abgerufen werden sollen.", + "dashboard.FutureDays": "Zukünftige Tage", + "dashboard.Friday": "Freitag", + "dashboard.Calendar": "Kalender", + "dashboard.Modules": "Module", + "lidarr.StartSearchFor": "Beginne Suche nach…", + "lidarr.StartSearchForMissingAlbums": "Beginne Suche nach fehlenden Alben", "lunasea.Add": "Hinzufügen", "lunasea.Alpha": "Alpha", "lunasea.AnErrorHasOccurred": "Ein Fehler ist aufgetreten", @@ -264,6 +84,14 @@ "lunasea.Update": "Aktualisierung", "lunasea.View": "Ansicht", "lunasea.Website": "Internetseite", + "overseerr.Users": "Benutzer", + "overseerr.UnknownUser": "Unbekannter Benutzer", + "overseerr.NoUsersFound": "Keine Benutzer gefunden", + "overseerr.NoRequestsFound": "Keine Anfragen gefunden", + "overseerr.Requests": "Anfragen", + "overseerr.NoRequests": "Keine Anfragen", + "overseerr.OneRequest": "1 Anfrage", + "overseerr.SomeRequests": "{} Anfragen", "radarr.AddMovie": "Film hinzufügen", "radarr.AddMovieAndSearch": "Hinzufügen + Suchen", "radarr.AddedTag": "Schlagwort hinzugefügt", @@ -389,103 +217,207 @@ "search.Results": "Ergebnisse", "search.NoSubcategoriesFound": "Keine Unterkategorien gefunden", "search.NoResultsFound": "Keine Ergebnisse gefunden", - "dashboard.Wednesday": "Mittwoch", - "dashboard.TwoWeeks": "Zwei Wochen", - "dashboard.Tuesday": "Dienstag", - "dashboard.Thursday": "Donnerstag", - "dashboard.Sunday": "Sonntag", - "dashboard.Schedule": "Übersicht", - "dashboard.Saturday": "Samstag", - "dashboard.PastDaysDescription": "Wähle die Anzahl an Tagen in der Vergangenheit für die Kalendereinträge abgerufen werden sollen.", - "dashboard.PastDays": "Vergangene Tage", - "dashboard.OneWeek": "Eine Woche", - "dashboard.OneMonth": "Ein Monat", - "dashboard.NoNewContent": "Keine neuen Inhalte", - "dashboard.Monday": "Montag", - "dashboard.MinimumOfOneDay": "Minimum von 1 Tag", - "dashboard.FutureDaysDescription": "Wähle die Anzahl an Tagen in der Zukunft für die Kalendereinträge abgerufen werden sollen.", - "dashboard.FutureDays": "Zukünftige Tage", - "dashboard.Friday": "Freitag", - "dashboard.Calendar": "Kalender", - "dashboard.Modules": "Module", - "tautulli.TerminateSessionFailed": "Abbrechen der Sitzung fehlgeschlagen", - "tautulli.TerminateSession": "Sitzung abbrechen", - "tautulli.TerminatedSession": "Abgebrochene Sitzung", - "tautulli.TerminationConfirmMessage": "Möchtest du diese Sitzung abbrechen?", - "tautulli.Terminate": "Abbrechen", - "tautulli.Episode": "Folge {}", - "tautulli.Year": "Jahr", - "tautulli.ViewWebGUI": "Weboberfläche ansehen", - "tautulli.Video": "Video", - "tautulli.Users": "Benutzer", - "tautulli.User": "Benutzer", - "tautulli.Transcodes": "Umwandlungen", - "tautulli.Transcode": "Umwandeln", - "tautulli.Title": "Titel", - "tautulli.Throttled": "Gebremst", - "tautulli.TerminationMessage": "Abbruch-Begründung", - "tautulli.TerminationAttachMessage": "Du kannst optional eine Abbruch-Begründung angeben.", - "tautulli.Subtitle": "Untertitel", - "tautulli.Stream": "Stream", - "tautulli.SessionsMany": "{} Sitzungen", - "tautulli.SessionsOne": "1 Sitzung", - "tautulli.Sessions": "Sitzungen", - "tautulli.SessionEnded": "Sitzung beendet", - "tautulli.Season": "Staffel {}", - "tautulli.Quality": "Qualität", - "tautulli.Product": "Produkt", - "tautulli.Player": "Wiedergabegerät", - "tautulli.Platform": "Plattform", - "tautulli.NoActiveStreams": "Keine aktiven Streams", - "tautulli.None": "Nichts", - "tautulli.More": "Mehr", - "tautulli.Metadata": "Metadaten", - "tautulli.Location": "Ort", - "tautulli.Library": "Bibliothek", - "tautulli.History": "Verlauf", - "tautulli.ETA": "Restzeit", - "tautulli.Duration": "Dauer", - "tautulli.DirectStreams": "Direkt-Streams", - "tautulli.DirectStream": "Direkt-Stream", - "tautulli.DirectPlays": "Direktwiedergaben", - "tautulli.DirectPlay": "Direktwiedergabe", - "tautulli.DeletingTemporarySessionsFailed": "Löschen der temporären Sitzungen fehlgeschlagen", - "tautulli.DeletingTemporarySessionsDescription": "Temporäre Sitzungen werden gelöscht", - "tautulli.DeletingTemporarySessions": "Lösche temporäre Sitzungen …", - "tautulli.DeletingImageCacheFailed": "Löschen des Bild-Zwischenspeicher fehlgeschlagen", - "tautulli.DeletingImageCacheDescription": "Tautulli Bild-Zwischenspeicher wird gelöscht", - "tautulli.DeletingImageCache": "Lösche Bild-Zwischenspeicher …", - "tautulli.DeletingCacheFailed": "Löschen des Zwischenspeichers fehlgeschlagen", - "tautulli.DeletingCacheDescription": "Tautulli Zwischenspeicher wird gelöscht", - "tautulli.DeletingCache": "Lösche Zwischenspeicher …", - "tautulli.DeleteTemporarySessions": "Lösche temporäre Sitzungen", - "tautulli.DeleteImageCache": "Bild-Zwischenspeicher löschen", - "tautulli.DeleteCache": "Zwischenspeicher löschen", - "tautulli.Copy": "Kopieren", - "tautulli.Container": "Medien-Container", - "tautulli.Burn": "Einbrennen", - "tautulli.Bandwidth": "Bandbreite", - "tautulli.BackupDatabase": "Datenbank sichern", - "tautulli.BackupConfiguration": "Konfiguration sichern", - "tautulli.BackingUpDatabaseFailed": "Sichern der Datenbank fehlgeschlagen", - "tautulli.BackingUpDatabaseDescription": "Deine Datenbank wird im Hintergrund gesichert", - "tautulli.BackingUpDatabase": "Datenbank wird gesichert …", - "tautulli.BackingUpConfigurationFailed": "Sichern der Konfiguration fehlgeschlagen", - "tautulli.BackingUpConfigurationDescription": "Deine Konfiguration wird im Hintergrund gesichert", - "tautulli.BackingUpConfiguration": "Konfiguration wird gesichert …", - "tautulli.Audio": "Audio", - "tautulli.ActivityDetails": "Aktivitätsdetails", - "tautulli.Activity": "Aktivität", - "lidarr.StartSearchFor": "Beginne Suche nach…", - "lidarr.StartSearchForMissingAlbums": "Beginne Suche nach fehlenden Alben", - "overseerr.Users": "Benutzer", - "overseerr.UnknownUser": "Unbekannter Benutzer", - "overseerr.NoUsersFound": "Keine Benutzer gefunden", - "overseerr.NoRequestsFound": "Keine Anfragen gefunden", - "overseerr.Requests": "Anfragen", - "overseerr.NoRequests": "Keine Anfragen", - "overseerr.OneRequest": "1 Anfrage", - "overseerr.SomeRequests": "{} Anfragen", + "settings.EncryptionKey": "Sicherungsphrase", + "settings.EnabledProfile": "Aktives Profil", + "settings.DismissBannersHint2": "Die Kurzinfo-Banner werden dir Tipps und Hinweise zu verfügbaren Funktionen in LunaSea geben.", + "settings.DismissBannersHint1": "Bist du sicher dass du alle Kurzinfo-Banner ausblenden möchtest?", + "settings.DeleteHeader": "Lösche Kopfdaten", + "settings.CustomHeaders": "Benutzerdefinierte Kopfdaten", + "settings.CustomHeader": "Benutzerdefinierte Kopfdaten", + "settings.AddHeader": "Kopfdaten hinzufügen", + "settings.DismissBanners": "Banner ausblenden", + "settings.DeleteProfile": "Profil löschen", + "settings.DeleteIndexerHint1": "Bist du sicher dass du diesen Indexer löschen möchtest?", + "settings.DeleteIndexer": "Indexer löschen", + "settings.DeleteHeaderHint1": "Bist du sicher dass du diese Kopfdaten löschen möchtest?", + "settings.DecryptBackupHint1": "Bitte gib die Sicherungsphrase für diese Sicherung ein.", + "settings.DecryptBackup": "Sicherung entschlüsseln", + "settings.Custom": "Benutzerdefiniert…", + "settings.ClearLogsHint1": "Bist du sicher dass du alle erfassten Protokolle entfernen möchtest?\n\nProtokolle können für Fehlerreports und Fehlersuche nützlich sein.", + "settings.ClearLogs": "Protokolle löschen", + "settings.ClearConfigurationHint3": "Du wirst abgemeldet wenn du bei deinem LunaSea Konto angemeldet bist.", + "settings.ClearConfigurationHint2": "Du wirst ganz von Vorne beginnen. Bitte stelle sicher dass du deine jetzige Konfiguration gesichert hast!", + "settings.ClearConfigurationHint1": "Bist du sicher das du die Konfiguration entfernen möchtest?", + "settings.ClearConfiguration": "Entferne Konfiguration", + "settings.BroadcastAddressValidation": "Ungültige Broadcast Adresse", + "settings.BroadcastAddressHint3": "Bei einer Beispiel IP-Adresse eines Rechners von 192.168.1.111 ist die daraus resultierende Broadcast Adresse die 192.168.1.255", + "settings.BroadcastAddressHint2": "Typischerweise ist dies die IP Adresse deines Rechners mit dem letzten Oktett als 255 ersetzt", + "settings.BroadcastAddressHint1": "Dies ist die Broadcast Adresse für dein lokales Netzwerk", + "settings.BroadcastAddress": "Broadcast-Adresse", + "settings.BasicAuthenticationHint3": "Der Benutzername und das Passwort werden automatisch in Base64-Kodierung umgewandelt", + "settings.BasicAuthenticationHint2": "Das Passwort darf einen Doppelpunkt enthalten", + "settings.BasicAuthenticationHint1": "Der Benutzername darf keinen Doppelpunkt enthalten", + "settings.BasicAuthentication": "Basic Authentifizierung", + "settings.BannersNotificationModuleSupportBody": "Notifikationen basierend auf Webhooks werden zur Zeit nur von den unten aufgelisteten Modulen unterstützt.\n\nZusätzliche Modulunterstützung ist für die Zukunft geplant!", + "settings.BannersNotificationModuleSupportHeader": "Unterstützte Module", + "settings.BackupList": "Liste der Sicherungen", + "settings.BackupConfigurationHint2": "Die Sicherungsphrase muss mindestens 8 Zeichen enthalten", + "settings.BackupConfigurationHint1": "Alle Sicherungen werden vor dem Exportieren verschlüsselt", + "settings.BackupConfiguration": "Konfiguration sichern", + "settings.AddProfile": "Profil hinzufügen", + "settings.AccountHelpHint1": "LunaSea bietet ein kostenloses Konto an um deine Konfiguration in der Cloud zu speichern. Zusätzliche Funktionen folgen in der Zukunft!", + "settings.AccountHelp": "LunaSea-Konto", + "settings.MustBeValueBetween": "Muss ein Wert zwischen {} und {} sein", + "settings.UsernameValidation": "Benutzername erforderlich", + "settings.Username": "Benutzername", + "settings.StartingType": "Starttyp", + "settings.StartingSize": "Startgröße", + "settings.StartingDay": "Starttag", + "settings.ShowCalendarEntries": "Zeige {} Kalendereinträge", + "settings.SignOutHint1": "Bist du sicher dass du dich von deinem LunaSea Konto abmelden möchtest?", + "settings.SignOut": "Abmelden", + "settings.RenameProfile": "Profil umbenennen", + "settings.ProfileNameRequired": "Profilname erforderlich", + "settings.ProfileName": "Profilname", + "settings.ProfileAlreadyExists": "Profil existiert bereits", + "settings.PasswordValidation": "Passwort erforderlich", + "settings.Password": "Passwort", + "settings.OpenLinksIn": "Öffne Links in…", + "settings.NoHeadersAdded": "Keine Kopfdaten hinzugefügt", + "settings.NoBackupsFound": "Keine Sicherungen gefunden", + "settings.MinimumCharacters": "Minimum von {} Zeichen", + "settings.MACAddressValidation": "Ungültige MAC Adresse", + "settings.MACAddressHint4": "Jedes Hexadezimaloktett sollte durch einen Doppelpunkt getrennt sein", + "settings.MACAddressHint3": "Hexadezimalzahlen rangieren von 0-9 und A-F", + "settings.MACAddressHint2": "MAC Adressen bestehen aus sechs zweistelligen Hexadezimalhäppchen (ein Oktett)", + "settings.MACAddressHint1": "Dies ist die MAC Adresse des Rechners den du aufwecken möchtest", + "settings.MACAddress": "MAC Adresse", + "settings.Language": "Sprache", + "settings.ImageBackgroundOpacityHint2": "Um die Verwendung von Hintergrundbildern komplett auszuschalten, setze den Wert auf 0.", + "settings.ImageBackgroundOpacityHint1": "Wähle die Deckkraft für Hintergrundbilder.", + "settings.ImageBackgroundOpacity": "Deckkraft des Hintergrundbilds", + "settings.HostValidation": "Host muss http:// oder https:// mit angeben", + "settings.HostHint5": "Um Basic Authentifizierung hinzuzufügen, benutze bitte die Funktion für benutzerdefinierte Kopfdaten", + "settings.HostHint4": "Bitte gebe den Port mit an wenn kein Reverse Proxy verwendet wird", + "settings.HostHint3": "Verwende nicht localhost oder 127.0.0.1", + "settings.HostHint2": "Du musst entweder http:// oder https:// mit angeben", + "settings.HostHint1": "Dies ist die URL mit der du die Weboberfläche des Service aufrufst", + "settings.Host": "Host", + "settings.HeaderValueValidation": "Kopfdaten Wert erforderlich", + "settings.HeaderValue": "Kopfdaten Wert", + "settings.HeaderKeyValidation": "Kopfdaten Schlüssel erforderlich", + "settings.HeaderKey": "Kopfdaten Schlüssel", + "settings.HeaderDeleted": "Kopfdaten gelöscht", + "settings.HeaderAdded": "Kopfdaten hinzugefügt", + "settings.SystemDescription": "System-Werkzeuge und Info", + "settings.System": "System", + "settings.ResourcesDescription": "Nützliche Ressourcen und Links", + "settings.Resources": "Ressourcen", + "settings.ProfilesDescription": "Verwalte deine Profile", + "settings.Profiles": "Profile", + "settings.NotificationsDescription": "Richte Webhooks für Push-Benachrichtigungen ein", + "settings.Notifications": "Benachrichtigungen", + "settings.DonationsDescription": "Spende an den Entwickler", + "settings.Donations": "Spenden", + "settings.DebugMenuDescription": "Fehlersuche- und Entwicklungswerkzeuge", + "settings.DebugMenu": "Fehlersuchemenü", + "settings.ConfigurationDescription": "Konfiguriere und richte LunaSea ein", + "settings.Configuration": "Konfiguration", + "settings.AccountDescription": "Dein LunaSea-Konto", + "settings.Account": "Konto", + "settings.TestConnection": "Verbindung testen", + "settings.SignIn": "Anmelden", + "settings.SignedOutSuccessMessage": "Von deinem LunaSea Konto abgemeldet", + "settings.SignedOutSuccess": "Abgemeldet", + "settings.SignedOutFailure": "Abmelden fehlgeschlagen", + "settings.SignedInSuccess": "Anmelden erfolgreich", + "settings.SignedInFailure": "Anmelden fehlgeschlagen", + "settings.RestoreFromCloudSuccessMessage": "Deine Konfiguration wurde wiederhergestellt", + "settings.RestoreFromCloudSuccess": "Wiederherstellen erfolgreich", + "settings.RestoreFromCloudFailure": "Wiederherstellen fehlgeschlagen", + "settings.RestoreFromCloudDescription": "Konfigurationsdaten wiederherstellen", + "settings.RestoreFromCloud": "Sicherung aus der Cloud wiederherstellen", + "settings.ResetPassword": "Passwort zurücksetzen", + "settings.RegisteredSuccess": "Registriert", + "settings.RegisteredFailure": "Registrieren fehlgeschlagen", + "settings.Register": "Registrieren", + "settings.QuickActionsDescription": "Schnellaktionen auf dem Home-Bildschirm", + "settings.QuickActions": "Schnellaktionen", + "settings.LocalizationDescription": "Passe die Sprache an deine Region an", + "settings.Localization": "Übersetzung", + "settings.InvalidPasswordMessage": "Das Passwort ist ungültig", + "settings.InvalidPassword": "Ungültiges Passwort", + "settings.InvalidEmailMessage": "Die E-Mail-Adresse ist ungültig", + "settings.InvalidEmail": "Ungültige E-Mail Addresse", + "settings.HostRequiredMessage": "Host ist erforderlich um sich mit {} zu verbinden", + "settings.HostRequired": "Host erforderlich", + "settings.ForgotYourPassword": "Passwort vergessen?", + "settings.EmailSentSuccessMessage": "Eine E-Mail zum Zurücksetzen deines Passworts wurde gesendet!", + "settings.EmailSentSuccess": "E-Mail gesendet", + "settings.EmailSentFailure": "Passwort zurücksetzen fehlgeschlagen", + "settings.Email": "E-Mail", + "settings.DrawerDescription": "Passe das Seitenmenü an", + "settings.Drawer": "Seitenmenü", + "settings.DeleteCloudBackupSuccess": "Gelöscht", + "settings.DeleteCloudBackupFailure": "Löschen fehlgeschlagen", + "settings.DeleteCloudBackupDescription": "Lösche eine Konfigurationsdatei", + "settings.DeleteCloudBackup": "Lösche Cloudsicherung", + "settings.DefaultPage": "Standardseite", + "settings.CustomHeadersDescription": "Füge benutzerdefinierte Kopfdaten zu Requests hinzu", + "settings.ConnectionTestFailed": "Verbindungstest fehlgeschlagen", + "settings.ConnectionDetailsDescription": "Verbindungsdetails für {}", + "settings.ConnectionDetails": "Verbindungsdetails", + "settings.ConnectedSuccessfullyMessage": "{} ist bereit für die Verwendung mit LunaSea!", + "settings.ConnectedSuccessfully": "Verbunden", + "settings.ConfigureModule": "Konfiguriere {}", + "settings.BackupToCloudSuccess": "Sicherung erfolgreich", + "settings.BackupToCloudFailure": "Sicherung fehlgeschlagen", + "settings.BackupToCloudDescription": "Sicherung der Konfigurationsdaten", + "settings.BackupToCloud": "Cloud-Sicherung", + "settings.BackgroundImageOpacity": "Deckkraft des Hintergrundbilds", + "settings.AutomaticallyManageOrderDescription": "Liste Module alphabetisch auf", + "settings.AutomaticallyManageOrder": "Automatische Sortierung", + "settings.AppearanceDescription": "Passe das Aussehen an", + "settings.Appearance": "Aussehen", + "settings.ApiKeyRequiredMessage": "API Schlüssel ist erforderlich um sich mit {} zu verbinden", + "settings.ApiKeyRequired": "API Schlüssel erforderlich", + "settings.ApiKey": "API Schlüssel", + "settings.AmoledThemeDescription": "Pures schwarzes dunkles Design", + "settings.AmoledThemeBordersDescription": "Fügt subtile Ränder zur UI hinzu", + "settings.AmoledThemeBorders": "AMOLED Design Ränder", + "settings.AmoledTheme": "AMOLED Design", + "settings.NoExternalModulesFound": "Keine externen Module gefunden", + "settings.ModuleNotFound": "Modul nicht gefunden", + "settings.EditModule": "Modul editieren", + "settings.DisplayName": "Anzeigename", + "settings.DeleteModuleSuccess": "Modul gelöscht", + "settings.DeleteModuleHint1": "Bist du sicher dass du dieses externe Modul löschen möchtest?", + "settings.DeleteModule": "Modul löschen", + "settings.AllFieldsAreRequired": "Alle Felder sind erforderlich", + "settings.AddModuleSuccess": "Modul hinzugefügt", + "settings.AddModuleFailed": "Module hinzufügen fehlgeschlagen", + "settings.AddModule": "Modul hinzufügen", + "settings.AccountDeleted": "Konto gelöscht", + "settings.AccountDeletedMessage": "LunaSea Konto gelöscht", + "settings.AccountSettings": "Konto-Einstellungen", + "settings.DeleteAccountHint1": "Bist du sicher dass du dein LunaSea-Konto löschen möchtest?", + "settings.DeleteAccountHint2": "Dies wird ebenfalls sämtliche mit diesem Konto verknüpften Daten sowie Cloudsicherungen löschen.", + "settings.StartingView": "Startansicht", + "settings.Add": "Hinzufügen", + "settings.AddProfileDescription": "Neues Profil hinzufügen", + "settings.DefaultPages": "Standardseiten", + "settings.DefaultPagesDescription": "Standardseiten festlegen", + "settings.RenameProfileDescription": "Existierendes Profil umbenennen", + "settings.ClearImageCacheHint2": "Bilder für eine große Mediathek erneut herunterzuladen kann eine große Menge Daten verbrauchen.", + "settings.DefaultSortingAndFiltering": "Standardsortierung & -filterung", + "settings.DefaultSortingAndFilteringDescription": "Standardsortierungs & -filterungsmethoden festlegen", + "settings.DeleteAccount": "Konto löschen", + "settings.DeleteAccountDescription": "Konto permanent löschen", + "settings.DeleteAccountWarning1": "Dies ist nicht rückgängig zu machen", + "settings.FailedToDeleteAccount": "Löschen des Kontos fehlgeschlagen", + "settings.ClearImageCache": "Entferne Bild-Zwischenspeicher", + "settings.ClearImageCacheHint1": "Bist du sicher dass du alle Bilder aus dem Zwischenspeicher entfernen möchtest?", + "settings.DeleteProfileDescription": "Lösche ein existierendes Profil", + "settings.DefaultOptions": "Standardeinstellungen", + "settings.DefaultOptionsDescription": "Sortierung, Filterung und Ansichtsoptionen bearbeiten", + "settings.Network": "Netzwerk", + "settings.NetworkDescription": "Netzwerkeinstellungen vornehmen", + "settings.TLSCertificateValidation": "SSL-Zertifikate überprüfen", + "settings.TLSCertificateValidationDescription": "Einstellen, ob bei SSL-Verbindungen die Zertifikate überprüft werden", + "settings.FilterCategory": "Kategorie filtern", + "settings.SortCategory": "Kategorie sortieren", + "settings.SortDirection": "Sortierrichtung", + "settings.ViewRecentChanges": "Letzte Änderungen anzeigen", "sonarr.AddSeries": "Serie hinzufügen", "sonarr.AddToExclusionList": "Zur Ignorierliste hinzufügen", "sonarr.AllSeasons": "Alle Staffeln", @@ -693,5 +625,73 @@ "sonarr.RemoveFromQueue": "Aus Warteschlange entfernen", "sonarr.Queued": "Eingereiht", "sonarr.Usenet": "Usenet", - "sonarr.Title": "Titel" + "sonarr.Title": "Titel", + "tautulli.TerminateSessionFailed": "Abbrechen der Sitzung fehlgeschlagen", + "tautulli.TerminateSession": "Sitzung abbrechen", + "tautulli.TerminatedSession": "Abgebrochene Sitzung", + "tautulli.TerminationConfirmMessage": "Möchtest du diese Sitzung abbrechen?", + "tautulli.Terminate": "Abbrechen", + "tautulli.Episode": "Folge {}", + "tautulli.Year": "Jahr", + "tautulli.ViewWebGUI": "Weboberfläche ansehen", + "tautulli.Video": "Video", + "tautulli.Users": "Benutzer", + "tautulli.User": "Benutzer", + "tautulli.Transcodes": "Umwandlungen", + "tautulli.Transcode": "Umwandeln", + "tautulli.Title": "Titel", + "tautulli.Throttled": "Gebremst", + "tautulli.TerminationMessage": "Abbruch-Begründung", + "tautulli.TerminationAttachMessage": "Du kannst optional eine Abbruch-Begründung angeben.", + "tautulli.Subtitle": "Untertitel", + "tautulli.Stream": "Stream", + "tautulli.SessionsMany": "{} Sitzungen", + "tautulli.SessionsOne": "1 Sitzung", + "tautulli.Sessions": "Sitzungen", + "tautulli.SessionEnded": "Sitzung beendet", + "tautulli.Season": "Staffel {}", + "tautulli.Quality": "Qualität", + "tautulli.Product": "Produkt", + "tautulli.Player": "Wiedergabegerät", + "tautulli.Platform": "Plattform", + "tautulli.NoActiveStreams": "Keine aktiven Streams", + "tautulli.None": "Nichts", + "tautulli.More": "Mehr", + "tautulli.Metadata": "Metadaten", + "tautulli.Location": "Ort", + "tautulli.Library": "Bibliothek", + "tautulli.History": "Verlauf", + "tautulli.ETA": "Restzeit", + "tautulli.Duration": "Dauer", + "tautulli.DirectStreams": "Direkt-Streams", + "tautulli.DirectStream": "Direkt-Stream", + "tautulli.DirectPlays": "Direktwiedergaben", + "tautulli.DirectPlay": "Direktwiedergabe", + "tautulli.DeletingTemporarySessionsFailed": "Löschen der temporären Sitzungen fehlgeschlagen", + "tautulli.DeletingTemporarySessionsDescription": "Temporäre Sitzungen werden gelöscht", + "tautulli.DeletingTemporarySessions": "Lösche temporäre Sitzungen …", + "tautulli.DeletingImageCacheFailed": "Löschen des Bild-Zwischenspeicher fehlgeschlagen", + "tautulli.DeletingImageCacheDescription": "Tautulli Bild-Zwischenspeicher wird gelöscht", + "tautulli.DeletingImageCache": "Lösche Bild-Zwischenspeicher …", + "tautulli.DeletingCacheFailed": "Löschen des Zwischenspeichers fehlgeschlagen", + "tautulli.DeletingCacheDescription": "Tautulli Zwischenspeicher wird gelöscht", + "tautulli.DeletingCache": "Lösche Zwischenspeicher …", + "tautulli.DeleteTemporarySessions": "Lösche temporäre Sitzungen", + "tautulli.DeleteImageCache": "Bild-Zwischenspeicher löschen", + "tautulli.DeleteCache": "Zwischenspeicher löschen", + "tautulli.Copy": "Kopieren", + "tautulli.Container": "Medien-Container", + "tautulli.Burn": "Einbrennen", + "tautulli.Bandwidth": "Bandbreite", + "tautulli.BackupDatabase": "Datenbank sichern", + "tautulli.BackupConfiguration": "Konfiguration sichern", + "tautulli.BackingUpDatabaseFailed": "Sichern der Datenbank fehlgeschlagen", + "tautulli.BackingUpDatabaseDescription": "Deine Datenbank wird im Hintergrund gesichert", + "tautulli.BackingUpDatabase": "Datenbank wird gesichert …", + "tautulli.BackingUpConfigurationFailed": "Sichern der Konfiguration fehlgeschlagen", + "tautulli.BackingUpConfigurationDescription": "Deine Konfiguration wird im Hintergrund gesichert", + "tautulli.BackingUpConfiguration": "Konfiguration wird gesichert …", + "tautulli.Audio": "Audio", + "tautulli.ActivityDetails": "Aktivitätsdetails", + "tautulli.Activity": "Aktivität" } \ No newline at end of file diff --git a/assets/localization/en.json b/assets/localization/en.json index cad95db9f0..8437dde43f 100644 --- a/assets/localization/en.json +++ b/assets/localization/en.json @@ -1,4 +1,448 @@ { + "dashboard.Calendar": "Calendar", + "dashboard.Friday": "Friday", + "dashboard.FutureDays": "Future Days", + "dashboard.FutureDaysDescription": "Set the number of days in the future to fetch calendar entries for.", + "dashboard.MinimumOfOneDay": "Minimum of 1 Day", + "dashboard.Modules": "Modules", + "dashboard.Monday": "Monday", + "dashboard.NoNewContent": "No New Content", + "dashboard.OneMonth": "One Month", + "dashboard.OneWeek": "One Week", + "dashboard.PastDays": "Past Days", + "dashboard.PastDaysDescription": "Set the number of days in the past to fetch calendar entries for.", + "dashboard.Saturday": "Saturday", + "dashboard.Schedule": "Schedule", + "dashboard.Sunday": "Sunday", + "dashboard.Thursday": "Thursday", + "dashboard.Tuesday": "Tuesday", + "dashboard.TwoWeeks": "Two Weeks", + "dashboard.Wednesday": "Wednesday", + "lidarr.StartSearchFor": "Start Search For…", + "lidarr.StartSearchForMissingAlbums": "Start search for missing albums", + "lunasea.Ascending": "Ascending", + "lunasea.Add": "Add", + "lunasea.Alpha": "Alpha", + "lunasea.AnErrorHasOccurred": "An Error Has Occurred", + "lunasea.BackUp": "Back Up", + "lunasea.Beta": "Beta", + "lunasea.BlockView": "Block View", + "lunasea.BuyMeABeer": "Buy Me A Beer", + "lunasea.BuyMeABurger": "Buy Me A Burger", + "lunasea.BuyMeACoffee": "Buy Me A Coffee", + "lunasea.BuyMeASoda": "Buy Me A Soda", + "lunasea.Cancel": "Cancel", + "lunasea.ChangeProfiles": "Change Profiles", + "lunasea.CheckLogsMessage": "Check the logs for more details", + "lunasea.Clear": "Clear", + "lunasea.Close": "Close", + "lunasea.ComingSoon": "Coming Soon", + "lunasea.Dashboard": "Dashboard", + "lunasea.DaysAgo": "{} Days Ago", + "lunasea.Delete": "Delete", + "lunasea.Descending": "Descending", + "lunasea.Develop": "Develop", + "lunasea.Disable": "Disable", + "lunasea.Disabled": "Disabled", + "lunasea.Dismiss": "Dismiss", + "lunasea.ExternalModules": "External Modules", + "lunasea.Fixes": "Fixes", + "lunasea.FullChangelog": "Full Changelog", + "lunasea.GoBack": "Go Back", + "lunasea.GoToSettings": "Go to Settings", + "lunasea.GridView": "Grid View", + "lunasea.Home": "Home", + "lunasea.HoursAgo": "{} Hours Ago", + "lunasea.IncorrectEncryptionKey": "Incorrect encryption key", + "lunasea.Internal": "Internal", + "lunasea.JustNow": "Just Now", + "lunasea.Module": "Module", + "lunasea.ModuleIsNotEnabled": "{} Is Not Enabled", + "lunasea.New": "New", + "lunasea.NotSet": "Not Set", + "lunasea.NoModulesEnabled": "No Modules Enabled", + "lunasea.OneHourAgo": "1 Hour Ago", + "lunasea.Options": "Options", + "lunasea.Page": "Page", + "lunasea.PlatformSpecific": "Platform-Specific", + "lunasea.Production": "Production", + "lunasea.Refresh": "Refresh", + "lunasea.Refreshing": "Refreshing…", + "lunasea.Remove": "Remove", + "lunasea.Rename": "Rename", + "lunasea.Restore": "Restore", + "lunasea.ReturnToDashboard": "Return to Dashboard", + "lunasea.SearchTextBar": "Search…", + "lunasea.Set": "Set", + "lunasea.Settings": "Settings", + "lunasea.StartingView": "Starting View", + "lunasea.TryAgain": "Try Again", + "lunasea.Tweaks": "Tweaks", + "lunasea.Unknown": "Unknown", + "lunasea.UnknownDate": "Unknown Date", + "lunasea.UnknownError": "Unknown Error", + "lunasea.UnknownModule": "Unknown Module", + "lunasea.Update": "Update", + "lunasea.View": "View", + "lunasea.Website": "Website", + "overseerr.Approved": "Approved", + "overseerr.Available": "Available", + "overseerr.Declined": "Declined", + "overseerr.NoRequests": "No Requests", + "overseerr.OneRequest": "1 Request", + "overseerr.PartiallyAvailable": "Partially Available", + "overseerr.Pending": "Pending", + "overseerr.Processing": "Processing", + "overseerr.SomeRequests": "{} Requests", + "overseerr.Requested": "Requested", + "overseerr.RequestedBy": "Requested by {}", + "overseerr.Requests": "Requests", + "overseerr.NoRequestsFound": "No Requests Found", + "overseerr.NoUsersFound": "No Users Found", + "overseerr.UnknownUser": "Unknown User", + "overseerr.Users": "Users", + "radarr.Age": "Age", + "radarr.All": "All", + "radarr.AddedTag": "Added Tag", + "radarr.AddMovie": "Add Movie", + "radarr.AddMovieAndSearch": "Add + Search", + "radarr.Alphabetical": "Alphabetical", + "radarr.Approved": "Approved", + "radarr.Automatic": "Automatic", + "radarr.AvailabilityUnknown": "Availability Unknown", + "radarr.AvailableIn": "Available in {}", + "radarr.AvailableToday": "Available Today", + "radarr.BackupDatabase": "Backup Database", + "radarr.CinemaDateUnknown": "Cinema Date Unknown", + "radarr.Configure": "Configure", + "radarr.Copy": "Copy", + "radarr.CopyFull": "Hardlink/Copy Files", + "radarr.CutoffUnmet": "Cutoff Unmet", + "radarr.DateAdded": "Date Added", + "radarr.DigitalRelease": "Digital Release", + "radarr.DirectoryNotFound": "Directory Not Found", + "radarr.Discover": "Discover", + "radarr.DownloadFailed": "Download Failed", + "radarr.DownloadIgnored": "Download Ignored", + "radarr.EditMovie": "Edit Movie", + "radarr.FailedToAddTag": "Failed to Add Tag", + "radarr.FileBrowser": "File Browser…", + "radarr.FilterCatalogue": "Filter Catalogue", + "radarr.GrabbedFrom": "Grabbed from {}", + "radarr.History": "History", + "radarr.HistoryDescription": "View Recent Activity", + "radarr.Import": "Import", + "radarr.ImportMode": "Import Mode", + "radarr.InCinemas": "In Cinemas", + "radarr.InCinemasIn": "In Cinemas in {}", + "radarr.InCinemasToday": "In Cinemas Today", + "radarr.Interactive": "Interactive", + "radarr.Language": "Language", + "radarr.Languages": "Languages", + "radarr.ManualImport": "Manual Import", + "radarr.ManualImportDescription": "Import Movies from the Filesystem", + "radarr.MinimumAvailability": "Minimum Availability", + "radarr.Missing": "Missing", + "radarr.Monitor": "Monitor", + "radarr.Monitored": "Monitored", + "radarr.MonitoredDescription": "Monitor for new releases", + "radarr.MonitorMovie": "Monitor Movie", + "radarr.More": "More", + "radarr.Move": "Move", + "radarr.MoveFull": "Move Files", + "radarr.Movie": "Movie", + "radarr.Movies": "Movies", + "radarr.MovieFileDeleted": "Movie File Deleted", + "radarr.MovieFileRenamed": "Movie File Renamed", + "radarr.MovieImported": "Movie Imported ({})", + "radarr.MovieNotFound": "Movie Not Found", + "radarr.MoviePath": "Movie Path", + "radarr.NoFilesFound": "No Files Found", + "radarr.NoHistoryFound": "No History Found", + "radarr.NoTagsFound": "No Tags Found", + "radarr.NoMoviesFound": "No Movies Found", + "radarr.NoResultsFound": "No Results Found", + "radarr.NoSubdirectoriesFound": "No Subdirectories Found", + "radarr.NoSummaryIsAvailable": "No summary is available.", + "radarr.ParentDirectory": "Parent Directory", + "radarr.PhysicalRelease": "Physical Release", + "radarr.Quality": "Quality", + "radarr.QualityProfile": "Quality Profile", + "radarr.Queue": "Queue", + "radarr.QueueDescription": "View Active & Queued Content", + "radarr.Quick": "Quick", + "radarr.RefreshMovie": "Refresh Movie", + "radarr.Rejected": "Rejected", + "radarr.Releases": "Releases", + "radarr.Released": "Released", + "radarr.ReleasedToday": "Released Today", + "radarr.RemoveMovie": "Remove Movie", + "radarr.RootFolder": "Root Folder", + "radarr.Runtime": "Runtime", + "radarr.RunRSSSync": "Run RSS Sync", + "radarr.Search": "Search", + "radarr.SearchAllMissing": "Search All Missing", + "radarr.SearchFor": "Search for {}", + "radarr.Seeders": "Seeders", + "radarr.SelectLanguage": "Select Languages", + "radarr.SelectMovie": "Select Movie", + "radarr.SelectQuality": "Select Quality", + "radarr.Size": "Size", + "radarr.SortCatalogue": "Sort Catalogue", + "radarr.StartSearchFor": "Start Search For…", + "radarr.StartSearchForMissingMovie": "Start search for missing movie", + "radarr.Studio": "Studio", + "radarr.SystemStatus": "System Status", + "radarr.SystemStatusDescription": "System Status & Disk Space", + "radarr.Tags": "Tags", + "radarr.TagsDescription": "Manage Your Tags", + "radarr.Type": "Type", + "radarr.Unmonitored": "Unmonitored", + "radarr.UnmonitorMovie": "Unmonitor Movie", + "radarr.Upcoming": "Upcoming", + "radarr.UpdateLibrary": "Update Library", + "radarr.UpdateMovie": "Update Movie", + "radarr.ViewWebGUI": "View Web GUI", + "radarr.Wanted": "Wanted", + "radarr.Weight": "Weight", + "radarr.Year": "Year", + "readarr.AddAuthor": "Add Author", + "readarr.AddReleaseToBlocklist": "Add Release to Blocklist", + "readarr.AddToExclusionList": "Add to Exclusion List", + "readarr.AddedAuthor": "Added Author", + "readarr.AddedOn": "Added On", + "readarr.AddedTag": "Added Tag", + "readarr.Age": "Age", + "readarr.All": "All", + "readarr.AllSeasons": "All Seasons", + "readarr.Approved": "Approved", + "readarr.Audio": "Audio", + "readarr.Author": "Author", + "readarr.AuthorDetails": "Author Details", + "readarr.AuthorEnded": "Author Ended", + "readarr.AuthorFolderImported": "Episode Imported from Author Folder", + "readarr.AuthorNotFound": "Author Not Found", + "readarr.AuthorPath": "Author Path", + "readarr.AuthorType": "Author Type", + "readarr.Authors": "Authors", + "readarr.Automatic": "Automatic", + "readarr.AutomaticSearch": "Automatic Search", + "readarr.BackingUpDatabase": "Backing Up Database{}", + "readarr.BackingUpDatabaseDescription": "Backing up the database in the background", + "readarr.BackupDatabase": "Backup Database", + "readarr.BitDepth": "Bit Depth", + "readarr.Bitrate": "Bitrate", + "readarr.Books": "Books", + "readarr.BookDetails": "Book Details", + "readarr.BookFileDeleted": "Book File Deleted", + "readarr.BookFileImported": "Book File Imported", + "readarr.BookFileRenamed": "Book File Renamed", + "readarr.BookImportIncomplete": "Files downloaded but could not be imported", + "readarr.Channels": "Channels", + "readarr.CheckDownloadClient": "Check download client for more details", + "readarr.Client": "Client", + "readarr.Codec": "Codec", + "readarr.Continuing": "Continuing", + "readarr.DeleteBookFile": "Delete Book File", + "readarr.DeleteBookFileHint1": "Are you sure you want to delete this book file?", + "readarr.DeleteFile": "Delete File", + "readarr.DeleteFiles": "Delete Files", + "readarr.DeleteReasonManual": "File was deleted by via UI", + "readarr.DeleteReasonMissingFromDisk": "readarr was unable to find the file on disk so it was removed", + "readarr.DeleteReasonUpgrade": "File was deleted to import an upgrade", + "readarr.Destination": "Destination", + "readarr.DestinationRelative": "Destination Relative", + "readarr.Download": "Download", + "readarr.DownloadClientUnavailable": "Download Client is Unavailable", + "readarr.DownloadFailed": "Download Failed", + "readarr.DownloadID": "Download ID", + "readarr.DownloadIgnored": "Download Ignored", + "readarr.DownloadWarning": "Download Warning", + "readarr.DownloadWarningWithMessage": "Download Warning: {}", + "readarr.Downloaded": "Downloaded", + "readarr.DownloadedImporting": "Downloaded: Importing", + "readarr.DownloadedWaitingToImport": "Downloaded: Waiting to Import", + "readarr.DownloadedWaitingToProcess": "Downloaded: Waiting to Process", + "readarr.Downloading": "Downloading", + "readarr.DownloadingRelease": "Downloading Release…", + "readarr.DownloadingWithMessage": "Downloading: {}", + "readarr.EditAuthor": "Edit Author", + "readarr.EmptyQueue": "Empty Queue", + "readarr.Ended": "Ended", + "readarr.Episode": "Episode", + "readarr.EpisodeImported": "Episode Imported ({})", + "readarr.EpisodeNumber": "Episode {}", + "readarr.Episodes": "Episodes", + "readarr.EpisodesAvailable": "Episodes Available", + "readarr.FPS": "FPS", + "readarr.FailedToAddAuthor": "Failed to Add Author", + "readarr.FailedToAddTag": "Failed to Add Tag", + "readarr.FailedToBackupDatabase": "Failed to Backup Database", + "readarr.FailedToDeleteBookFile": "Failed to Delete Book File", + "readarr.FailedToDownloadRelease": "Failed to Download Release", + "readarr.FailedToMonitorAuthor": "Failed to Monitor Author", + "readarr.FailedToMonitorEpisode": "Failed to Monitor Episode", + "readarr.FailedToMonitorSeason": "Failed to Monitor Season", + "readarr.FailedToRefresh": "Failed to Refresh", + "readarr.FailedToRemoveAuthor": "Failed to Remove Author", + "readarr.FailedToRemoveBook": "Failed to Remove Book", + "readarr.FailedToRemoveFromQueue": "Failed to Remove From Queue", + "readarr.FailedToRunRSSSync": "Failed to Run RSS Sync", + "readarr.FailedToSearch": "Failed to Search", + "readarr.FailedToSeasonSearch": "Failed to Season Search", + "readarr.FailedToUnmonitorAuthor": "Failed to Unmonitor Author", + "readarr.FailedToUnmonitorBook": "Failed to Unmonitor Book", + "readarr.FailedToUpdateAuthor": "Failed to Update Author", + "readarr.FailedToUpdateLibrary": "Failed to Update Library", + "readarr.Files": "Files", + "readarr.FilterCatalogue": "Filter Catalogue", + "readarr.FilterReleases": "Filter Releases", + "readarr.GrabbedFrom": "Grabbed from {}", + "readarr.History": "History", + "readarr.HistoryDescription": "View Recent Activity", + "readarr.Import": "Import", + "readarr.ImportFailed": "Imported Failed", + "readarr.ImportedTo": "Imported To", + "readarr.Indexer": "Indexer", + "readarr.InfoURL": "Info URL", + "readarr.Interactive": "Interactive", + "readarr.InteractiveSearch": "Interactive Search", + "readarr.Language": "Language", + "readarr.LanguageProfile": "Language Profile", + "readarr.Languages": "Languages", + "readarr.Leechers": "Leechers", + "readarr.ManualImport": "Manual Import", + "readarr.ManualImportDescription": "Import Content from the Filesystem", + "readarr.ManySeasons": "{} Seasons", + "readarr.MediaInfo": "Media Info", + "readarr.Message": "Message", + "readarr.Messages": "Messages", + "readarr.MetadataProfile": "Metadata Profile", + "readarr.Missing": "Missing", + "readarr.MissingEpisodes": "Missing Episodes", + "readarr.MissingEpisodesHint1": "Are you sure you want to search for all missing episodes?", + "readarr.Monitor": "Monitor", + "readarr.MonitorAuthor": "Monitor Author", + "readarr.MonitorEpisode": "Monitor Episode", + "readarr.Monitored": "Monitored", + "readarr.MonitoredDescription": "Download monitored episodes in this Author", + "readarr.Monitoring": "Monitoring", + "readarr.More": "More", + "readarr.Name": "Name", + "readarr.NoAuthorFound": "No Author Found", + "readarr.NoEpisodesFound": "No Books Found", + "readarr.NoHistoryFound": "No History Found", + "readarr.NoLongerMonitoring": "No Longer Monitoring", + "readarr.NoMessagesFound": "No Messages Found", + "readarr.NoReleasesFound": "No Releases Found", + "readarr.NoResultsFound": "No Results Found", + "readarr.NoSeasonsFound": "No Seasons Found", + "readarr.NoSummaryAvailable": "No Summary Available", + "readarr.NoTagsFound": "No Tags Found", + "readarr.OneSeason": "1 Season", + "readarr.Other": "Other", + "readarr.Overview": "Overview", + "readarr.Pages": "Pages", + "readarr.Path": "Path", + "readarr.Paused": "Paused", + "readarr.Pending": "Pending", + "readarr.PendingWithMessage": "Pending: {}", + "readarr.PublishedDate": "Published Date", + "readarr.Quality": "Quality", + "readarr.QualityProfile": "Quality Profile", + "readarr.Queue": "Queue", + "readarr.QueueDescription": "View Active & Queued Content", + "readarr.Queued": "Queued", + "readarr.Reason": "Reason", + "readarr.RefreshAuthor": "Refresh Author", + "readarr.Rejected": "Rejected", + "readarr.RelativePath": "Relative Path", + "readarr.ReleaseGroup": "Release Group", + "readarr.Releases": "Releases", + "readarr.RemoveAuthor": "Remove Author", + "readarr.RemoveFromDownloadClient": "Remove From Download Client", + "readarr.RemoveFromQueue": "Remove From Queue", + "readarr.RemovedAuthor": "Removed Author", + "readarr.RemovedAuthorWithFiles": "Removed Author (With Files)", + "readarr.RemovedBook": "Removed Book", + "readarr.RemovedBookWithFiles": "Removed Book (With Files)", + "readarr.RemovedFromQueue": "Removed From Queue", + "readarr.Resolution": "Resolution", + "readarr.RootFolder": "Root Folder", + "readarr.RunRSSSync": "Run RSS Sync", + "readarr.RunningRSSSync": "Running RSS Sync{}", + "readarr.RunningRSSSyncDescription": "Running RSS sync in the background", + "readarr.Runtime": "Runtime", + "readarr.ScanType": "Scan Type", + "readarr.Search": "Search", + "readarr.SearchAllMissing": "Search All Missing", + "readarr.SearchFor": "Search for {}", + "readarr.Searching": "Searching{}", + "readarr.SearchingDescription": "Searching for all missing episodes", + "readarr.SearchingForEpisode": "Searching for Episode…", + "readarr.SearchingForSeason": "Searching for Season…", + "readarr.Season": "Season", + "readarr.SeasonDetails": "Season Details", + "readarr.SeasonFolders": "Season Folders", + "readarr.SeasonFoldersDescription": "Sort episodes into season folders", + "readarr.SeasonNumber": "Season {}", + "readarr.Seeders": "Seeders", + "readarr.Size": "Size", + "readarr.SortCatalogue": "Sort Catalogue", + "readarr.SortReleases": "Sort Releases", + "readarr.Source": "Source", + "readarr.SourceRelative": "Source Relative", + "readarr.SourceTitle": "Source Title", + "readarr.Specials": "Specials", + "readarr.StartSearchFor": "Start Search For…", + "readarr.StartSearchForCutoffUnmetEpisodes": "Start search for cutoff unmet episodes", + "readarr.StartSearchForMissingEpisodes": "Start search for missing episodes", + "readarr.Streams": "Streams", + "readarr.Subtitles": "Subtitles", + "readarr.SystemStatus": "System Status", + "readarr.SystemStatusDescription": "System Status & Disk Space", + "readarr.Tags": "Tags", + "readarr.TagsDescription": "Manage Your Tags", + "readarr.TimeLeft": "Time Left", + "readarr.Title": "Title", + "readarr.Torrent": "Torrent", + "readarr.Unaired": "Unaired", + "readarr.UnmonitorAuthor": "Unmonitor Author", + "readarr.UnmonitorEpisode": "Unmonitor Episode", + "readarr.Unmonitored": "Unmonitored", + "readarr.Upcoming": "Upcoming", + "readarr.UpdateAuthor": "Update Author", + "readarr.UpdateLibrary": "Update Library", + "readarr.UpdatedAuthor": "Updated Author", + "readarr.UpdatingLibrary": "Updating Library", + "readarr.UpdatingLibraryDescription": "Updating library in the background", + "readarr.UseSeasonFolders": "Use Season Folders", + "readarr.UseSeasonFoldersDescription": "Sort episodes into season folders", + "readarr.Usenet": "Usenet", + "readarr.Video": "Video", + "readarr.ViewWebGUI": "View Web GUI", + "readarr.WordScore": "Word Score", + "search.Age": "Age", + "search.AllSubcategories": "All Subcategories", + "search.Alphabetical": "Alphabetical", + "search.Category": "Category", + "search.Categories": "Categories", + "search.Comments": "Comments", + "search.Download": "Download", + "search.Downloading": "Downloading…", + "search.DownloadingNZBToDevice": "Downloading NZB to your device", + "search.DownloadToDevice": "Download to Device", + "search.FailedToDownloadNZB": "Failed to Download NZB", + "search.FailedToSend": "Failed to Send", + "search.NoCategoriesFound": "No Categories Found", + "search.NoResultsFound": "No Results Found", + "search.NoSubcategoriesFound": "No Subcategories Found", + "search.Results": "Results", + "search.Search": "Search", + "search.SentNZBData": "Sent NZB Data", + "search.SentTo": "Sent to {}", + "search.Size": "Size", + "search.Subcategories": "Subcategories", "settings.Account": "Account", "settings.AccountDeleted": "Account Deleted", "settings.AccountDeletedMessage": "Deleted your LunaSea account", @@ -214,302 +658,6 @@ "settings.WeblateDescription": "Help Localize LunaSea", "settings.Website": "Website", "settings.WebsiteDescription": "Visit LunaSea's Website", - "lunasea.Ascending": "Ascending", - "lunasea.Add": "Add", - "lunasea.Alpha": "Alpha", - "lunasea.AnErrorHasOccurred": "An Error Has Occurred", - "lunasea.BackUp": "Back Up", - "lunasea.Beta": "Beta", - "lunasea.BlockView": "Block View", - "lunasea.BuyMeABeer": "Buy Me A Beer", - "lunasea.BuyMeABurger": "Buy Me A Burger", - "lunasea.BuyMeACoffee": "Buy Me A Coffee", - "lunasea.BuyMeASoda": "Buy Me A Soda", - "lunasea.Cancel": "Cancel", - "lunasea.ChangeProfiles": "Change Profiles", - "lunasea.CheckLogsMessage": "Check the logs for more details", - "lunasea.Clear": "Clear", - "lunasea.Close": "Close", - "lunasea.ComingSoon": "Coming Soon", - "lunasea.Dashboard": "Dashboard", - "lunasea.DaysAgo": "{} Days Ago", - "lunasea.Delete": "Delete", - "lunasea.Descending": "Descending", - "lunasea.Develop": "Develop", - "lunasea.Disable": "Disable", - "lunasea.Disabled": "Disabled", - "lunasea.Dismiss": "Dismiss", - "lunasea.ExternalModules": "External Modules", - "lunasea.Fixes": "Fixes", - "lunasea.FullChangelog": "Full Changelog", - "lunasea.GoBack": "Go Back", - "lunasea.GoToSettings": "Go to Settings", - "lunasea.GridView": "Grid View", - "lunasea.Home": "Home", - "lunasea.HoursAgo": "{} Hours Ago", - "lunasea.IncorrectEncryptionKey": "Incorrect encryption key", - "lunasea.Internal": "Internal", - "lunasea.JustNow": "Just Now", - "lunasea.Module": "Module", - "lunasea.ModuleIsNotEnabled": "{} Is Not Enabled", - "lunasea.New": "New", - "lunasea.NotSet": "Not Set", - "lunasea.NoModulesEnabled": "No Modules Enabled", - "lunasea.OneHourAgo": "1 Hour Ago", - "lunasea.Options": "Options", - "lunasea.Page": "Page", - "lunasea.PlatformSpecific": "Platform-Specific", - "lunasea.Production": "Production", - "lunasea.Refresh": "Refresh", - "lunasea.Refreshing": "Refreshing…", - "lunasea.Remove": "Remove", - "lunasea.Rename": "Rename", - "lunasea.Restore": "Restore", - "lunasea.ReturnToDashboard": "Return to Dashboard", - "lunasea.SearchTextBar": "Search…", - "lunasea.Set": "Set", - "lunasea.Settings": "Settings", - "lunasea.StartingView": "Starting View", - "lunasea.TryAgain": "Try Again", - "lunasea.Tweaks": "Tweaks", - "lunasea.Unknown": "Unknown", - "lunasea.UnknownDate": "Unknown Date", - "lunasea.UnknownError": "Unknown Error", - "lunasea.UnknownModule": "Unknown Module", - "lunasea.Update": "Update", - "lunasea.View": "View", - "lunasea.Website": "Website", - "radarr.Age": "Age", - "radarr.All": "All", - "radarr.AddedTag": "Added Tag", - "radarr.AddMovie": "Add Movie", - "radarr.AddMovieAndSearch": "Add + Search", - "radarr.Alphabetical": "Alphabetical", - "radarr.Approved": "Approved", - "radarr.Automatic": "Automatic", - "radarr.AvailabilityUnknown": "Availability Unknown", - "radarr.AvailableIn": "Available in {}", - "radarr.AvailableToday": "Available Today", - "radarr.BackupDatabase": "Backup Database", - "radarr.CinemaDateUnknown": "Cinema Date Unknown", - "radarr.Configure": "Configure", - "radarr.Copy": "Copy", - "radarr.CopyFull": "Hardlink/Copy Files", - "radarr.CutoffUnmet": "Cutoff Unmet", - "radarr.DateAdded": "Date Added", - "radarr.DigitalRelease": "Digital Release", - "radarr.DirectoryNotFound": "Directory Not Found", - "radarr.Discover": "Discover", - "radarr.DownloadFailed": "Download Failed", - "radarr.DownloadIgnored": "Download Ignored", - "radarr.EditMovie": "Edit Movie", - "radarr.FailedToAddTag": "Failed to Add Tag", - "radarr.FileBrowser": "File Browser…", - "radarr.FilterCatalogue": "Filter Catalogue", - "radarr.GrabbedFrom": "Grabbed from {}", - "radarr.History": "History", - "radarr.HistoryDescription": "View Recent Activity", - "radarr.Import": "Import", - "radarr.ImportMode": "Import Mode", - "radarr.InCinemas": "In Cinemas", - "radarr.InCinemasIn": "In Cinemas in {}", - "radarr.InCinemasToday": "In Cinemas Today", - "radarr.Interactive": "Interactive", - "radarr.Language": "Language", - "radarr.Languages": "Languages", - "radarr.ManualImport": "Manual Import", - "radarr.ManualImportDescription": "Import Movies from the Filesystem", - "radarr.MinimumAvailability": "Minimum Availability", - "radarr.Missing": "Missing", - "radarr.Monitor": "Monitor", - "radarr.Monitored": "Monitored", - "radarr.MonitoredDescription": "Monitor for new releases", - "radarr.MonitorMovie": "Monitor Movie", - "radarr.More": "More", - "radarr.Move": "Move", - "radarr.MoveFull": "Move Files", - "radarr.Movie": "Movie", - "radarr.Movies": "Movies", - "radarr.MovieFileDeleted": "Movie File Deleted", - "radarr.MovieFileRenamed": "Movie File Renamed", - "radarr.MovieImported": "Movie Imported ({})", - "radarr.MovieNotFound": "Movie Not Found", - "radarr.MoviePath": "Movie Path", - "radarr.NoFilesFound": "No Files Found", - "radarr.NoHistoryFound": "No History Found", - "radarr.NoTagsFound": "No Tags Found", - "radarr.NoMoviesFound": "No Movies Found", - "radarr.NoResultsFound": "No Results Found", - "radarr.NoSubdirectoriesFound": "No Subdirectories Found", - "radarr.NoSummaryIsAvailable": "No summary is available.", - "radarr.ParentDirectory": "Parent Directory", - "radarr.PhysicalRelease": "Physical Release", - "radarr.Quality": "Quality", - "radarr.QualityProfile": "Quality Profile", - "radarr.Queue": "Queue", - "radarr.QueueDescription": "View Active & Queued Content", - "radarr.Quick": "Quick", - "radarr.RefreshMovie": "Refresh Movie", - "radarr.Rejected": "Rejected", - "radarr.Releases": "Releases", - "radarr.Released": "Released", - "radarr.ReleasedToday": "Released Today", - "radarr.RemoveMovie": "Remove Movie", - "radarr.RootFolder": "Root Folder", - "radarr.Runtime": "Runtime", - "radarr.RunRSSSync": "Run RSS Sync", - "radarr.Search": "Search", - "radarr.SearchAllMissing": "Search All Missing", - "radarr.SearchFor": "Search for {}", - "radarr.Seeders": "Seeders", - "radarr.SelectLanguage": "Select Languages", - "radarr.SelectMovie": "Select Movie", - "radarr.SelectQuality": "Select Quality", - "radarr.Size": "Size", - "radarr.SortCatalogue": "Sort Catalogue", - "radarr.StartSearchFor": "Start Search For…", - "radarr.StartSearchForMissingMovie": "Start search for missing movie", - "radarr.Studio": "Studio", - "radarr.SystemStatus": "System Status", - "radarr.SystemStatusDescription": "System Status & Disk Space", - "radarr.Tags": "Tags", - "radarr.TagsDescription": "Manage Your Tags", - "radarr.Type": "Type", - "radarr.Unmonitored": "Unmonitored", - "radarr.UnmonitorMovie": "Unmonitor Movie", - "radarr.Upcoming": "Upcoming", - "radarr.UpdateLibrary": "Update Library", - "radarr.UpdateMovie": "Update Movie", - "radarr.ViewWebGUI": "View Web GUI", - "radarr.Wanted": "Wanted", - "radarr.Weight": "Weight", - "radarr.Year": "Year", - "search.Age": "Age", - "search.AllSubcategories": "All Subcategories", - "search.Alphabetical": "Alphabetical", - "search.Category": "Category", - "search.Categories": "Categories", - "search.Comments": "Comments", - "search.Download": "Download", - "search.Downloading": "Downloading…", - "search.DownloadingNZBToDevice": "Downloading NZB to your device", - "search.DownloadToDevice": "Download to Device", - "search.FailedToDownloadNZB": "Failed to Download NZB", - "search.FailedToSend": "Failed to Send", - "search.NoCategoriesFound": "No Categories Found", - "search.NoResultsFound": "No Results Found", - "search.NoSubcategoriesFound": "No Subcategories Found", - "search.Results": "Results", - "search.Search": "Search", - "search.SentNZBData": "Sent NZB Data", - "search.SentTo": "Sent to {}", - "search.Size": "Size", - "search.Subcategories": "Subcategories", - "dashboard.Calendar": "Calendar", - "dashboard.Friday": "Friday", - "dashboard.FutureDays": "Future Days", - "dashboard.FutureDaysDescription": "Set the number of days in the future to fetch calendar entries for.", - "dashboard.MinimumOfOneDay": "Minimum of 1 Day", - "dashboard.Modules": "Modules", - "dashboard.Monday": "Monday", - "dashboard.NoNewContent": "No New Content", - "dashboard.OneMonth": "One Month", - "dashboard.OneWeek": "One Week", - "dashboard.PastDays": "Past Days", - "dashboard.PastDaysDescription": "Set the number of days in the past to fetch calendar entries for.", - "dashboard.Saturday": "Saturday", - "dashboard.Schedule": "Schedule", - "dashboard.Sunday": "Sunday", - "dashboard.Thursday": "Thursday", - "dashboard.Tuesday": "Tuesday", - "dashboard.TwoWeeks": "Two Weeks", - "dashboard.Wednesday": "Wednesday", - "tautulli.Activity": "Activity", - "tautulli.ActivityDetails": "Activity Details", - "tautulli.Audio": "Audio", - "tautulli.BackingUpConfiguration": "Backing Up Configuration…", - "tautulli.BackingUpConfigurationDescription": "Backing up your configuration in the background", - "tautulli.BackingUpConfigurationFailed": "Failed to Backup Configuration", - "tautulli.BackingUpDatabase": "Backing Up Database…", - "tautulli.BackingUpDatabaseDescription": "Backing up your database in the background", - "tautulli.BackingUpDatabaseFailed": "Failed to Backup Database", - "tautulli.BackupConfiguration": "Backup Configuration", - "tautulli.BackupDatabase": "Backup Database", - "tautulli.Bandwidth": "Bandwidth", - "tautulli.Burn": "Burn", - "tautulli.Container": "Container", - "tautulli.Copy": "Copy", - "tautulli.DeleteCache": "Delete Cache", - "tautulli.DeleteImageCache": "Delete Image Cache", - "tautulli.DeleteTemporarySessions": "Delete Temporary Sessions", - "tautulli.DeletingCache": "Deleting Cache…", - "tautulli.DeletingCacheDescription": "Tautulli cache is being deleted", - "tautulli.DeletingCacheFailed": "Failed to Delete Cache", - "tautulli.DeletingImageCache": "Deleting Image Cache…", - "tautulli.DeletingImageCacheDescription": "Tautulli image cache is being deleted", - "tautulli.DeletingImageCacheFailed": "Failed to Delete Image Cache", - "tautulli.DeletingTemporarySessions": "Deleting Temporary Sessions…", - "tautulli.DeletingTemporarySessionsDescription": "Temporary sessions are being deleted", - "tautulli.DeletingTemporarySessionsFailed": "Failed to Delete Temporary Sessions", - "tautulli.DirectPlay": "Direct Play", - "tautulli.DirectPlays": "Direct Plays", - "tautulli.DirectStream": "Direct Stream", - "tautulli.DirectStreams": "Direct Streams", - "tautulli.Duration": "Duration", - "tautulli.Episode": "Episode {}", - "tautulli.ETA": "ETA", - "tautulli.History": "History", - "tautulli.Library": "Library", - "tautulli.Location": "Location", - "tautulli.Metadata": "Metadata", - "tautulli.More": "More", - "tautulli.None": "None", - "tautulli.NoActiveStreams": "No Active Streams", - "tautulli.Platform": "Platform", - "tautulli.Player": "Player", - "tautulli.Product": "Product", - "tautulli.Quality": "Quality", - "tautulli.Season": "Season {}", - "tautulli.SessionEnded": "Session Ended", - "tautulli.Sessions": "Sessions", - "tautulli.SessionsOne": "1 Session", - "tautulli.SessionsMany": "{} Sessions", - "tautulli.Stream": "Stream", - "tautulli.Subtitle": "Subtitle", - "tautulli.Terminate": "Terminate", - "tautulli.TerminationAttachMessage": "You can optionally attach a termination message below.", - "tautulli.TerminationConfirmMessage": "Do you want to terminate this session?", - "tautulli.TerminationMessage": "Termination Message", - "tautulli.TerminatedSession": "Terminated Session", - "tautulli.TerminateSession": "Terminate Session", - "tautulli.TerminateSessionFailed": "Failed to Terminate Session", - "tautulli.Throttled": "Throttled", - "tautulli.Title": "Title", - "tautulli.Transcode": "Transcode", - "tautulli.Transcodes": "Transcodes", - "tautulli.User": "User", - "tautulli.Users": "Users", - "tautulli.Video": "Video", - "tautulli.ViewWebGUI": "View Web GUI", - "tautulli.Year": "Year", - "lidarr.StartSearchFor": "Start Search For…", - "lidarr.StartSearchForMissingAlbums": "Start search for missing albums", - "overseerr.Approved": "Approved", - "overseerr.Available": "Available", - "overseerr.Declined": "Declined", - "overseerr.NoRequests": "No Requests", - "overseerr.OneRequest": "1 Request", - "overseerr.PartiallyAvailable": "Partially Available", - "overseerr.Pending": "Pending", - "overseerr.Processing": "Processing", - "overseerr.SomeRequests": "{} Requests", - "overseerr.Requested": "Requested", - "overseerr.RequestedBy": "Requested by {}", - "overseerr.Requests": "Requests", - "overseerr.NoRequestsFound": "No Requests Found", - "overseerr.NoUsersFound": "No Users Found", - "overseerr.UnknownUser": "Unknown User", - "overseerr.Users": "Users", "sonarr.AddedOn": "Added On", "sonarr.AddedSeries": "Added Series", "sonarr.AddedTag": "Added Tag", @@ -717,5 +865,73 @@ "sonarr.UseSeasonFoldersDescription": "Sort episodes into season folders", "sonarr.Video": "Video", "sonarr.ViewWebGUI": "View Web GUI", - "sonarr.WordScore": "Word Score" + "sonarr.WordScore": "Word Score", + "tautulli.Activity": "Activity", + "tautulli.ActivityDetails": "Activity Details", + "tautulli.Audio": "Audio", + "tautulli.BackingUpConfiguration": "Backing Up Configuration…", + "tautulli.BackingUpConfigurationDescription": "Backing up your configuration in the background", + "tautulli.BackingUpConfigurationFailed": "Failed to Backup Configuration", + "tautulli.BackingUpDatabase": "Backing Up Database…", + "tautulli.BackingUpDatabaseDescription": "Backing up your database in the background", + "tautulli.BackingUpDatabaseFailed": "Failed to Backup Database", + "tautulli.BackupConfiguration": "Backup Configuration", + "tautulli.BackupDatabase": "Backup Database", + "tautulli.Bandwidth": "Bandwidth", + "tautulli.Burn": "Burn", + "tautulli.Container": "Container", + "tautulli.Copy": "Copy", + "tautulli.DeleteCache": "Delete Cache", + "tautulli.DeleteImageCache": "Delete Image Cache", + "tautulli.DeleteTemporarySessions": "Delete Temporary Sessions", + "tautulli.DeletingCache": "Deleting Cache…", + "tautulli.DeletingCacheDescription": "Tautulli cache is being deleted", + "tautulli.DeletingCacheFailed": "Failed to Delete Cache", + "tautulli.DeletingImageCache": "Deleting Image Cache…", + "tautulli.DeletingImageCacheDescription": "Tautulli image cache is being deleted", + "tautulli.DeletingImageCacheFailed": "Failed to Delete Image Cache", + "tautulli.DeletingTemporarySessions": "Deleting Temporary Sessions…", + "tautulli.DeletingTemporarySessionsDescription": "Temporary sessions are being deleted", + "tautulli.DeletingTemporarySessionsFailed": "Failed to Delete Temporary Sessions", + "tautulli.DirectPlay": "Direct Play", + "tautulli.DirectPlays": "Direct Plays", + "tautulli.DirectStream": "Direct Stream", + "tautulli.DirectStreams": "Direct Streams", + "tautulli.Duration": "Duration", + "tautulli.Episode": "Episode {}", + "tautulli.ETA": "ETA", + "tautulli.History": "History", + "tautulli.Library": "Library", + "tautulli.Location": "Location", + "tautulli.Metadata": "Metadata", + "tautulli.More": "More", + "tautulli.None": "None", + "tautulli.NoActiveStreams": "No Active Streams", + "tautulli.Platform": "Platform", + "tautulli.Player": "Player", + "tautulli.Product": "Product", + "tautulli.Quality": "Quality", + "tautulli.Season": "Season {}", + "tautulli.SessionEnded": "Session Ended", + "tautulli.Sessions": "Sessions", + "tautulli.SessionsOne": "1 Session", + "tautulli.SessionsMany": "{} Sessions", + "tautulli.Stream": "Stream", + "tautulli.Subtitle": "Subtitle", + "tautulli.Terminate": "Terminate", + "tautulli.TerminationAttachMessage": "You can optionally attach a termination message below.", + "tautulli.TerminationConfirmMessage": "Do you want to terminate this session?", + "tautulli.TerminationMessage": "Termination Message", + "tautulli.TerminatedSession": "Terminated Session", + "tautulli.TerminateSession": "Terminate Session", + "tautulli.TerminateSessionFailed": "Failed to Terminate Session", + "tautulli.Throttled": "Throttled", + "tautulli.Title": "Title", + "tautulli.Transcode": "Transcode", + "tautulli.Transcodes": "Transcodes", + "tautulli.User": "User", + "tautulli.Users": "Users", + "tautulli.Video": "Video", + "tautulli.ViewWebGUI": "View Web GUI", + "tautulli.Year": "Year" } \ No newline at end of file diff --git a/assets/localization/es.json b/assets/localization/es.json index 7e37f6f665..5e74795d00 100644 --- a/assets/localization/es.json +++ b/assets/localization/es.json @@ -1,4 +1,184 @@ { + "dashboard.PastDays": "Días pasados", + "dashboard.PastDaysDescription": "Ajustar el número de días en el pasado para buscar entradas en el calendario.", + "dashboard.OneWeek": "Una semana", + "dashboard.Calendar": "Calendario", + "dashboard.Friday": "Viernes", + "dashboard.FutureDays": "Días futuros", + "dashboard.MinimumOfOneDay": "Mínimo de un día", + "dashboard.Saturday": "Sábado", + "dashboard.Schedule": "Horario", + "dashboard.Sunday": "Domingo", + "dashboard.Thursday": "Jueves", + "dashboard.Tuesday": "Martes", + "dashboard.TwoWeeks": "Dos semanas", + "dashboard.Wednesday": "Miércoles", + "dashboard.FutureDaysDescription": "Ajustar el número de días en el futuro para buscar entradas en el calendario.", + "dashboard.Modules": "Modulos", + "dashboard.Monday": "Lunes", + "dashboard.NoNewContent": "No contenido nuevo", + "dashboard.OneMonth": "Un mes", + "lunasea.Add": "Añadir", + "lunasea.AnErrorHasOccurred": "Se ha producido un error", + "lunasea.BackUp": "Respaldar", + "lunasea.Cancel": "Cancelar", + "lunasea.ChangeProfiles": "Cambiar perfiles", + "lunasea.CheckLogsMessage": "Consulte los registros para más detalles", + "lunasea.Clear": "Borrar", + "lunasea.ComingSoon": "Próximamente", + "lunasea.Dashboard": "Tablero", + "lunasea.Delete": "Eliminar", + "lunasea.Disable": "Desactivar", + "lunasea.Disabled": "Desactivado", + "lunasea.Dismiss": "Descartar", + "lunasea.ExternalModules": "Módulos externos", + "lunasea.GoBack": "Regresar", + "lunasea.GoToSettings": "Ve a Configuración", + "lunasea.IncorrectEncryptionKey": "Clave de cifrado incorrecta", + "lunasea.Module": "Módulo", + "lunasea.ModuleIsNotEnabled": "{} No está habilitado", + "lunasea.NoModulesEnabled": "Sin módulos habilitados", + "lunasea.NotSet": "No establecido", + "lunasea.Page": "Página", + "lunasea.Refresh": "Actualizar", + "lunasea.Rename": "Renombrar", + "lunasea.Restore": "Restaurar", + "lunasea.ReturnToDashboard": "Regresar al Tablero", + "lunasea.SearchTextBar": "Buscar…", + "lunasea.Set": "Establecer", + "lunasea.Settings": "Configuración", + "lunasea.TryAgain": "Volver a intentar", + "lunasea.Unknown": "Desconocido", + "lunasea.UnknownError": "Error desconocido", + "lunasea.UnknownModule": "Módulo desconocido", + "lunasea.Website": "Sitio web", + "overseerr.NoRequests": "Sin solicitudes", + "overseerr.OneRequest": "1 solicitud", + "overseerr.SomeRequests": "{} solicitudes", + "overseerr.Requests": "Solicitudes", + "overseerr.NoRequestsFound": "No se encontraron solicitudes", + "overseerr.UnknownUser": "Usuario desconocido", + "overseerr.NoUsersFound": "No se encontraron usuarios", + "overseerr.Users": "Usuarios", + "radarr.AddMovie": "Añadir película", + "radarr.AddMovieAndSearch": "Añadir y Buscar", + "radarr.Age": "Edad", + "radarr.All": "Todo", + "radarr.Alphabetical": "Alfabético", + "radarr.Approved": "Aprobado", + "radarr.Automatic": "Automático", + "radarr.AvailabilityUnknown": "Disponibilidad desconocida", + "radarr.AvailableIn": "Disponible en {}", + "radarr.AvailableToday": "Disponible hoy", + "radarr.BackupDatabase": "Respaldar base de datos", + "radarr.CinemaDateUnknown": "Fecha de cine desconocida", + "radarr.Configure": "Configurar", + "radarr.Copy": "Copiar", + "radarr.CopyFull": "Enlazar/Copiar archivos", + "radarr.CutoffUnmet": "Límite no alcanzado", + "radarr.DateAdded": "Fecha añadida", + "radarr.DigitalRelease": "Publicación digital", + "radarr.DirectoryNotFound": "Directorio no encontrado", + "radarr.Discover": "Descubra", + "radarr.DownloadFailed": "Descarga fracasó", + "radarr.DownloadIgnored": "Descarga ignorada", + "radarr.EditMovie": "Editar película", + "radarr.FileBrowser": "Explorador de archivos…", + "radarr.FilterCatalogue": "Catálogo de filtros", + "radarr.GrabbedFrom": "Tomada de {}", + "radarr.History": "Historia", + "radarr.HistoryDescription": "Ver actividad reciente", + "radarr.Import": "Importar", + "radarr.ImportMode": "Modo de importación", + "radarr.InCinemas": "En cines", + "radarr.InCinemasIn": "En cines en {}", + "radarr.InCinemasToday": "En cines hoy", + "radarr.Interactive": "Interactivo", + "radarr.Language": "Idioma", + "radarr.Languages": "Idiomas", + "radarr.ManualImport": "Importación manual", + "radarr.ManualImportDescription": "Importar películas desde el sistema de archivos", + "radarr.MinimumAvailability": "Disponibilidad mínima", + "radarr.Missing": "Falta", + "radarr.MonitorMovie": "Monitorear película", + "radarr.Monitored": "Monitoreada", + "radarr.MonitoredDescription": "Monitorear nuevos lanzamientos", + "radarr.More": "Más", + "radarr.Move": "Mover", + "radarr.MoveFull": "Mover archivos", + "radarr.Movie": "Película", + "radarr.MovieFileDeleted": "Archivo de película eliminado", + "radarr.MovieFileRenamed": "Archivo de película renombrado", + "radarr.MovieImported": "Película importada ({})", + "radarr.MovieNotFound": "Película no encontrada", + "radarr.MoviePath": "Ruta de la película", + "radarr.Movies": "Películas", + "radarr.NoFilesFound": "No se encontraron archivos", + "radarr.NoHistoryFound": "No se encontró historial", + "radarr.NoMoviesFound": "No se encontraron películas", + "radarr.NoResultsFound": "No se encontraron resultados", + "radarr.NoSubdirectoriesFound": "No se encontraron subdirectorios", + "radarr.NoSummaryIsAvailable": "No hay resumen disponible.", + "radarr.ParentDirectory": "Directorio principal", + "radarr.PhysicalRelease": "Estreno Físico", + "radarr.Quality": "Calidad", + "radarr.QualityProfile": "Perfil de calidad", + "radarr.Queue": "Cola", + "radarr.QueueDescription": "Ver contenido activo y en cola", + "radarr.Quick": "Rápidamente", + "radarr.RefreshMovie": "Actualizar película", + "radarr.Rejected": "Rechazado", + "radarr.Released": "Estrenada", + "radarr.ReleasedToday": "Estrena hoy", + "radarr.RemoveMovie": "Eliminar película", + "radarr.RootFolder": "Carpeta raíz", + "radarr.RunRSSSync": "Ejecutar sincronización RSS", + "radarr.Runtime": "Duración", + "radarr.Search": "Buscar", + "radarr.SearchAllMissing": "Búsqueda que falta", + "radarr.SearchFor": "Buscar {}", + "radarr.Seeders": "Seeders", + "radarr.SelectLanguage": "Seleccionar idiomas", + "radarr.SelectMovie": "Seleccionar película", + "radarr.SelectQuality": "Seleccionar calidad", + "radarr.Size": "Tamaño", + "radarr.SortCatalogue": "Ordenar catálogo", + "radarr.Studio": "Estudio", + "radarr.SystemStatus": "Estado del sistema", + "radarr.SystemStatusDescription": "Estado del sistema y tamaño de disco", + "radarr.Tags": "Etiquetas", + "radarr.TagsDescription": "Administre sus etiquetas", + "radarr.Type": "Tipo", + "radarr.UnmonitorMovie": "No monitorear", + "radarr.Unmonitored": "No monitoreadas", + "radarr.Upcoming": "Próximamente", + "radarr.UpdateLibrary": "Actualizar librería", + "radarr.UpdateMovie": "Actualizar película", + "radarr.ViewWebGUI": "Ver GUI web", + "radarr.Wanted": "Se busca", + "radarr.Weight": "Peso", + "radarr.Year": "Año", + "search.Alphabetical": "Alfabética", + "search.Category": "Categoria", + "search.Comments": "Comentarios", + "search.Download": "Descarga", + "search.Downloading": "Descargando…", + "search.DownloadingNZBToDevice": "Descargando NZB a su dispositivo", + "search.DownloadToDevice": "Descargar al dispositivo", + "search.FailedToDownloadNZB": "Descarga NZB fallida", + "search.FailedToSend": "Fallo al enviar", + "search.NoCategoriesFound": "Categorias no encontradas", + "search.NoResultsFound": "Resultados no encontrados", + "search.Search": "Búsqueda", + "search.SentNZBData": "Data NZB enviada", + "search.SentTo": "Enviada a {}", + "search.Size": "Tamaño", + "search.Subcategories": "Subcategorías", + "search.AllSubcategories": "Todas las subcategorías", + "search.Age": "Edad", + "search.Categories": "Categorias", + "search.NoSubcategoriesFound": "Subcategorías no encontradas", + "search.Results": "Resultados", "settings.BroadcastAddressHint1": "Esta es la dirección de transmisión de su red local", "settings.AccountHelp": "Cuenta LunaSea", "settings.AddHeader": "Añadir cabecera", @@ -169,178 +349,6 @@ "settings.RestoreFromCloudDescription": "Restaurar datos de configuración", "settings.RestoreFromCloudFailure": "Falló la restauración", "settings.BannersNotificationModuleSupportBody": "Actualmente, las notificaciones basadas en webhook solo se admiten en los módulos que se enumeran a continuación.\n\n¡El soporte para módulos adicionales vendrá en el futuro!", - "lunasea.Add": "Añadir", - "lunasea.AnErrorHasOccurred": "Se ha producido un error", - "lunasea.BackUp": "Respaldar", - "lunasea.Cancel": "Cancelar", - "lunasea.ChangeProfiles": "Cambiar perfiles", - "lunasea.CheckLogsMessage": "Consulte los registros para más detalles", - "lunasea.Clear": "Borrar", - "lunasea.ComingSoon": "Próximamente", - "lunasea.Dashboard": "Tablero", - "lunasea.Delete": "Eliminar", - "lunasea.Disable": "Desactivar", - "lunasea.Disabled": "Desactivado", - "lunasea.Dismiss": "Descartar", - "lunasea.ExternalModules": "Módulos externos", - "lunasea.GoBack": "Regresar", - "lunasea.GoToSettings": "Ve a Configuración", - "lunasea.IncorrectEncryptionKey": "Clave de cifrado incorrecta", - "lunasea.Module": "Módulo", - "lunasea.ModuleIsNotEnabled": "{} No está habilitado", - "lunasea.NoModulesEnabled": "Sin módulos habilitados", - "lunasea.NotSet": "No establecido", - "lunasea.Page": "Página", - "lunasea.Refresh": "Actualizar", - "lunasea.Rename": "Renombrar", - "lunasea.Restore": "Restaurar", - "lunasea.ReturnToDashboard": "Regresar al Tablero", - "lunasea.SearchTextBar": "Buscar…", - "lunasea.Set": "Establecer", - "lunasea.Settings": "Configuración", - "lunasea.TryAgain": "Volver a intentar", - "lunasea.Unknown": "Desconocido", - "lunasea.UnknownError": "Error desconocido", - "lunasea.UnknownModule": "Módulo desconocido", - "lunasea.Website": "Sitio web", - "radarr.AddMovie": "Añadir película", - "radarr.AddMovieAndSearch": "Añadir y Buscar", - "radarr.Age": "Edad", - "radarr.All": "Todo", - "radarr.Alphabetical": "Alfabético", - "radarr.Approved": "Aprobado", - "radarr.Automatic": "Automático", - "radarr.AvailabilityUnknown": "Disponibilidad desconocida", - "radarr.AvailableIn": "Disponible en {}", - "radarr.AvailableToday": "Disponible hoy", - "radarr.BackupDatabase": "Respaldar base de datos", - "radarr.CinemaDateUnknown": "Fecha de cine desconocida", - "radarr.Configure": "Configurar", - "radarr.Copy": "Copiar", - "radarr.CopyFull": "Enlazar/Copiar archivos", - "radarr.CutoffUnmet": "Límite no alcanzado", - "radarr.DateAdded": "Fecha añadida", - "radarr.DigitalRelease": "Publicación digital", - "radarr.DirectoryNotFound": "Directorio no encontrado", - "radarr.Discover": "Descubra", - "radarr.DownloadFailed": "Descarga fracasó", - "radarr.DownloadIgnored": "Descarga ignorada", - "radarr.EditMovie": "Editar película", - "radarr.FileBrowser": "Explorador de archivos…", - "radarr.FilterCatalogue": "Catálogo de filtros", - "radarr.GrabbedFrom": "Tomada de {}", - "radarr.History": "Historia", - "radarr.HistoryDescription": "Ver actividad reciente", - "radarr.Import": "Importar", - "radarr.ImportMode": "Modo de importación", - "radarr.InCinemas": "En cines", - "radarr.InCinemasIn": "En cines en {}", - "radarr.InCinemasToday": "En cines hoy", - "radarr.Interactive": "Interactivo", - "radarr.Language": "Idioma", - "radarr.Languages": "Idiomas", - "radarr.ManualImport": "Importación manual", - "radarr.ManualImportDescription": "Importar películas desde el sistema de archivos", - "radarr.MinimumAvailability": "Disponibilidad mínima", - "radarr.Missing": "Falta", - "radarr.MonitorMovie": "Monitorear película", - "radarr.Monitored": "Monitoreada", - "radarr.MonitoredDescription": "Monitorear nuevos lanzamientos", - "radarr.More": "Más", - "radarr.Move": "Mover", - "radarr.MoveFull": "Mover archivos", - "radarr.Movie": "Película", - "radarr.MovieFileDeleted": "Archivo de película eliminado", - "radarr.MovieFileRenamed": "Archivo de película renombrado", - "radarr.MovieImported": "Película importada ({})", - "radarr.MovieNotFound": "Película no encontrada", - "radarr.MoviePath": "Ruta de la película", - "radarr.Movies": "Películas", - "radarr.NoFilesFound": "No se encontraron archivos", - "radarr.NoHistoryFound": "No se encontró historial", - "radarr.NoMoviesFound": "No se encontraron películas", - "radarr.NoResultsFound": "No se encontraron resultados", - "radarr.NoSubdirectoriesFound": "No se encontraron subdirectorios", - "radarr.NoSummaryIsAvailable": "No hay resumen disponible.", - "radarr.ParentDirectory": "Directorio principal", - "radarr.PhysicalRelease": "Estreno Físico", - "radarr.Quality": "Calidad", - "radarr.QualityProfile": "Perfil de calidad", - "radarr.Queue": "Cola", - "radarr.QueueDescription": "Ver contenido activo y en cola", - "radarr.Quick": "Rápidamente", - "radarr.RefreshMovie": "Actualizar película", - "radarr.Rejected": "Rechazado", - "radarr.Released": "Estrenada", - "radarr.ReleasedToday": "Estrena hoy", - "radarr.RemoveMovie": "Eliminar película", - "radarr.RootFolder": "Carpeta raíz", - "radarr.RunRSSSync": "Ejecutar sincronización RSS", - "radarr.Runtime": "Duración", - "radarr.Search": "Buscar", - "radarr.SearchAllMissing": "Búsqueda que falta", - "radarr.SearchFor": "Buscar {}", - "radarr.Seeders": "Seeders", - "radarr.SelectLanguage": "Seleccionar idiomas", - "radarr.SelectMovie": "Seleccionar película", - "radarr.SelectQuality": "Seleccionar calidad", - "radarr.Size": "Tamaño", - "radarr.SortCatalogue": "Ordenar catálogo", - "radarr.Studio": "Estudio", - "radarr.SystemStatus": "Estado del sistema", - "radarr.SystemStatusDescription": "Estado del sistema y tamaño de disco", - "radarr.Tags": "Etiquetas", - "radarr.TagsDescription": "Administre sus etiquetas", - "radarr.Type": "Tipo", - "radarr.UnmonitorMovie": "No monitorear", - "radarr.Unmonitored": "No monitoreadas", - "radarr.Upcoming": "Próximamente", - "radarr.UpdateLibrary": "Actualizar librería", - "radarr.UpdateMovie": "Actualizar película", - "radarr.ViewWebGUI": "Ver GUI web", - "radarr.Wanted": "Se busca", - "radarr.Weight": "Peso", - "radarr.Year": "Año", - "search.Alphabetical": "Alfabética", - "search.Category": "Categoria", - "search.Comments": "Comentarios", - "search.Download": "Descarga", - "search.Downloading": "Descargando…", - "search.DownloadingNZBToDevice": "Descargando NZB a su dispositivo", - "search.DownloadToDevice": "Descargar al dispositivo", - "search.FailedToDownloadNZB": "Descarga NZB fallida", - "search.FailedToSend": "Fallo al enviar", - "search.NoCategoriesFound": "Categorias no encontradas", - "search.NoResultsFound": "Resultados no encontrados", - "search.Search": "Búsqueda", - "search.SentNZBData": "Data NZB enviada", - "search.SentTo": "Enviada a {}", - "search.Size": "Tamaño", - "search.Subcategories": "Subcategorías", - "search.AllSubcategories": "Todas las subcategorías", - "search.Age": "Edad", - "search.Categories": "Categorias", - "search.NoSubcategoriesFound": "Subcategorías no encontradas", - "search.Results": "Resultados", - "dashboard.PastDays": "Días pasados", - "dashboard.PastDaysDescription": "Ajustar el número de días en el pasado para buscar entradas en el calendario.", - "dashboard.OneWeek": "Una semana", - "dashboard.Calendar": "Calendario", - "dashboard.Friday": "Viernes", - "dashboard.FutureDays": "Días futuros", - "dashboard.MinimumOfOneDay": "Mínimo de un día", - "dashboard.Saturday": "Sábado", - "dashboard.Schedule": "Horario", - "dashboard.Sunday": "Domingo", - "dashboard.Thursday": "Jueves", - "dashboard.Tuesday": "Martes", - "dashboard.TwoWeeks": "Dos semanas", - "dashboard.Wednesday": "Miércoles", - "dashboard.FutureDaysDescription": "Ajustar el número de días en el futuro para buscar entradas en el calendario.", - "dashboard.Modules": "Modulos", - "dashboard.Monday": "Lunes", - "dashboard.NoNewContent": "No contenido nuevo", - "dashboard.OneMonth": "Un mes", "tautulli.Quality": "Calidad", "tautulli.None": "Ninguno", "tautulli.Terminate": "Terminar", @@ -408,13 +416,5 @@ "tautulli.Throttled": "Acelerado", "tautulli.Transcode": "Transcodificar", "tautulli.Transcodes": "Transcodificaciones", - "tautulli.ETA": "Tiempo Restante", - "overseerr.NoRequests": "Sin solicitudes", - "overseerr.OneRequest": "1 solicitud", - "overseerr.SomeRequests": "{} solicitudes", - "overseerr.Requests": "Solicitudes", - "overseerr.NoRequestsFound": "No se encontraron solicitudes", - "overseerr.UnknownUser": "Usuario desconocido", - "overseerr.NoUsersFound": "No se encontraron usuarios", - "overseerr.Users": "Usuarios" + "tautulli.ETA": "Tiempo Restante" } \ No newline at end of file diff --git a/assets/localization/fr.json b/assets/localization/fr.json index 9e537acbd8..64a9ace075 100644 --- a/assets/localization/fr.json +++ b/assets/localization/fr.json @@ -1,205 +1,25 @@ { - "settings.MustBeValueBetween": "Doit être une valeur entre {} et {}", - "settings.UsernameValidation": "Nom d'utilisateur requis", - "settings.Username": "Nom d'utilisateur", - "settings.StartingType": "Type de démarrage", - "settings.StartingSize": "Taille initiale", - "settings.StartingDay": "Jour de début", - "settings.ShowCalendarEntries": "Afficher {} entrées du calendrier", - "settings.SignOutHint1": "Etes-vous sûr de vouloir vous déconnecter de votre compte LunaSea ?", - "settings.SignOut": "Se déconnecter", - "settings.RenameProfile": "Renommer le profil", - "settings.ProfileNameRequired": "Nom du profil requis", - "settings.ProfileName": "Nom du profil", - "settings.ProfileAlreadyExists": "Le profil existe déjà", - "settings.PasswordValidation": "Mot de passe requis", - "settings.Password": "Mot de passe", - "settings.OpenLinksIn": "Ouvrir les liens avec…", - "settings.NoHeadersAdded": "Aucun en-tête ajouté", - "settings.NoBackupsFound": "Aucune sauvegarde trouvée", - "settings.MinimumCharacters": "Minimum {} caractères", - "settings.MACAddressValidation": "Adresse MAC non valide", - "settings.MACAddressHint4": "Chaque octet hexadécimal doit être séparé par deux points (:)", - "settings.MACAddressHint3": "Les chiffres hexadécimaux vont de 0 à 9 et de A à F", - "settings.MACAddressHint2": "Les adresses MAC contiennent six nibbles hexidécimaux à deux chiffres (un octet)", - "settings.MACAddressHint1": "Il s'agit de l'adresse MAC de l'ordinateur que vous souhaitez réveiller", - "settings.MACAddress": "Adresse MAC", - "settings.Language": "Langue", - "settings.ImageBackgroundOpacityHint2": "Pour désactiver complètement la récupération des images d'arrière-plan, définissez la valeur à 0.", - "settings.ImageBackgroundOpacityHint1": "Définissez l'opacité de l'arrière-plan.", - "settings.ImageBackgroundOpacity": "Opacité de l'arrière-plan", - "settings.HostValidation": "L'hôte doit inclure http:// ou https://", - "settings.HostHint5": "Pour ajouter l'authentification basique, veuillez utiliser la fonction d'en-têtes personnalisés", - "settings.HostHint4": "Si vous n'utilisez pas de proxy inverse, veuillez inclure le port", - "settings.HostHint3": "N'utilisez pas localhost ou 127.0.0.1", - "settings.HostHint2": "Vous devez inclure soit http:// soit https://", - "settings.HostHint1": "Il s'agit de l'URL à partir de laquelle vous accédez à l'interface graphique web du service", - "settings.Host": "Hôte", - "settings.HeaderValueValidation": "Valeur d'en-tête requise", - "settings.HeaderValue": "Valeur de l'en-tête", - "settings.HeaderKeyValidation": "Clé d'en-tête requise", - "settings.HeaderKey": "Clé d'en-tête", - "settings.HeaderDeleted": "En-tête supprimé", - "settings.HeaderAdded": "En-tête ajouté", - "settings.EncryptionKey": "Clé de chiffrement", - "settings.EnabledProfile": "Profil activé", - "settings.DismissBanners": "Cacher les bannières", - "settings.DismissBannersHint1": "Êtes-vous sûr(e) de vouloir cacher toutes les infobulles ?", - "settings.DismissBannersHint2": "Les d'infobulles vous donneront des conseils et des astuces pour les fonctionnalités disponibles dans LunaSea.", - "settings.DeleteProfile": "Supprimer le profil", - "settings.DeleteIndexerHint1": "Êtes-vous sûr(e) de vouloir supprimer cet indexeur ?", - "settings.DeleteIndexer": "Supprimer l'indexeur", - "settings.DeleteHeaderHint1": "Êtes-vous sûr(e) de vouloir supprimer cet en-tête ?", - "settings.DeleteHeader": "Supprimer l'en-tête", - "settings.DecryptBackupHint1": "Veuillez entrer la clé de chiffrage pour cette sauvegarde.", - "settings.DecryptBackup": "Déchiffrer la sauvegarde", - "settings.CustomHeaders": "En-têtes personnalisés", - "settings.CustomHeader": "En-tête personnalisé", - "settings.Custom": "Personnaliser…", - "settings.ClearLogs": "Effacer les journaux", - "settings.ClearLogsHint1": "Êtes-vous sûr(e) de vouloir effacer tous les journaux enregistrés ?\n\nLes journaux peuvent être utiles pour les rapports de bogues et le débogage.", - "settings.ClearConfigurationHint3": "Si vous êtes connecté(e) à un compte LunaSea, vous serez déconnecté(e).", - "settings.ClearConfigurationHint2": "Vous allez repartir de zéro, assurez-vous d'avoir sauvegardé votre configuration actuelle !", - "settings.ClearConfigurationHint1": "Êtes-vous sûr(e) de vouloir effacer votre configuration ?", - "settings.ClearConfiguration": "Effacer la configuration", - "settings.BroadcastAddressValidation": "Adresse de diffusion non valide", - "settings.BroadcastAddressHint3": "Si l'on prend comme exemple l'adresse IP de la machine 192.168.1.111, l'adresse IP de diffusion résultante est 192.168.1.255", - "settings.BroadcastAddressHint2": "En général, il s'agit de l'adresse IP de votre machine, le dernier octet étant fixé à 255", - "settings.BroadcastAddressHint1": "Ceci est l'adresse de diffusion de votre réseau local", - "settings.BroadcastAddress": "Adresse de diffusion", - "settings.BasicAuthenticationHint3": "Le nom d'utilisateur et le mot de passe sont automatiquement convertis en encodage base64", - "settings.BasicAuthenticationHint2": "Le mot de passe peut contenir le caractère deux-points", - "settings.BasicAuthenticationHint1": "Le nom d'utilisateur ne doit pas contenir de deux-points", - "settings.BasicAuthentication": "Authentification basique", - "settings.BannersNotificationModuleSupportBody": "Les notifications basées sur les Webhooks ne sont actuellement supportées que dans les modules listés ci-dessous.\n\nD'autres modules seront pris en charge à l'avenir !", - "settings.BannersNotificationModuleSupportHeader": "Modules pris en charge", - "settings.BackupList": "Liste des sauvegardes", - "settings.BackupConfigurationHint2": "La clé de chiffrement doit comporter au moins 8 caractères", - "settings.BackupConfigurationHint1": "Toutes les sauvegardes sont cryptées avant d'être exportées", - "settings.BackupConfiguration": "Sauvegarder la configuration", - "settings.AddProfile": "Ajouter un profil", - "settings.AddHeader": "Ajouter un en-tête", - "settings.AccountHelpHint1": "LunaSea propose un compte gratuit pour sauvegarder votre configuration sur le cloud, avec des fonctionnalités supplémentaires à venir !", - "settings.AccountHelp": "Compte LunaSea", - "settings.TestConnection": "Tester la connexion", - "settings.SystemDescription": "Utilitaires système et informations", - "settings.System": "Système", - "settings.SignIn": "Se connecter", - "settings.SignedOutSuccessMessage": "Déconnecté de votre compte LunaSea", - "settings.SignedOutSuccess": "Se déconnecter", - "settings.SignedOutFailure": "Échec lors de la déconnexion", - "settings.SignedInSuccess": "Connexion réussie", - "settings.SignedInFailure": "Échec de la connexion", - "settings.RestoreFromCloudSuccessMessage": "Votre configuration a été restaurée", - "settings.RestoreFromCloudSuccess": "Restauré avec succès", - "settings.RestoreFromCloudFailure": "Échec de la restauration", - "settings.RestoreFromCloudDescription": "Restaurer les données de configuration", - "settings.RestoreFromCloud": "Restaurer à partir du cloud", - "settings.ResourcesDescription": "Ressources et liens utiles", - "settings.Resources": "Ressources", - "settings.ResetPassword": "Réinitialiser le mot de passe", - "settings.RegisteredSuccess": "Inscrit", - "settings.RegisteredFailure": "Échec de l'inscription", - "settings.Register": "S'inscrire", - "settings.QuickActionsDescription": "Actions rapides sur l'écran d'accueil", - "settings.QuickActions": "Actions rapides", - "settings.Profiles": "Profils", - "settings.ProfilesDescription": "Gérez vos profils", - "settings.NotificationsDescription": "Configurer les crochets pour les notifications push", - "settings.Notifications": "Notifications", - "settings.LocalizationDescription": "Personnalisez selon vos paramètres régionaux", - "settings.Localization": "Localisation", - "settings.InvalidPasswordMessage": "Le mot de passe est invalide", - "settings.InvalidPassword": "Mot de passe incorrect", - "settings.InvalidEmailMessage": "L'adresse électronique est invalide", - "settings.InvalidEmail": "Email invalide", - "settings.HostRequiredMessage": "L'hôte est requis pour se connecter à {}", - "settings.HostRequired": "Hôte requis", - "settings.ForgotYourPassword": "Mot de passe oublié ?", - "settings.EmailSentSuccessMessage": "Un courriel pour réinitialiser votre mot de passe a été envoyé !", - "settings.EmailSentSuccess": "Courriel envoyé", - "settings.EmailSentFailure": "Échec de la réinitialisation du mot de passe", - "settings.Email": "Courriel", - "settings.DrawerDescription": "Personnalisez le menu latéral", - "settings.Drawer": "Menu latéral", - "settings.DonationsDescription": "Faire un don au développeur", - "settings.Donations": "Dons", - "settings.DeleteCloudBackupSuccess": "Supprimé", - "settings.DeleteCloudBackupFailure": "Échec de la suppression", - "settings.DeleteCloudBackupDescription": "Supprimer un fichier de configuration", - "settings.DeleteCloudBackup": "Supprimer la sauvegarde dans le cloud", - "settings.DebugMenuDescription": "Utilitaires de débogage et de développement", - "settings.DebugMenu": "Menu de débogage", - "settings.DefaultPage": "Page par défaut", - "settings.CustomHeadersDescription": "Ajouter des en-têtes personnalisés aux demandes", - "settings.ConnectionTestFailed": "Le test de connexion a échoué", - "settings.ConnectionDetailsDescription": "Détails de connexion pour {}", - "settings.ConnectionDetails": "Détails de connexion", - "settings.ConnectedSuccessfullyMessage": "{} est prêt à être utilisé avec LunaSea !", - "settings.ConnectedSuccessfully": "Connecté", - "settings.ConfigurationDescription": "Configurer et paramétrer LunaSea", - "settings.Configuration": "Configuration", - "settings.ConfigureModule": "Configurer {}", - "settings.BackupToCloudSuccess": "Sauvegardé avec succès", - "settings.BackupToCloudFailure": "Échec de la sauvegarde", - "settings.BackupToCloudDescription": "Sauvegarde des données de configuration", - "settings.BackupToCloud": "Sauvegarde dans le cloud", - "settings.BackgroundImageOpacity": "Opacité de l'image d'arrière-plan", - "settings.AutomaticallyManageOrderDescription": "Liste des modules par ordre alphabétique", - "settings.AutomaticallyManageOrder": "Tri automatique", - "settings.AppearanceDescription": "Personnalisez l'apparence", - "settings.Appearance": "Apparence", - "settings.ApiKeyRequiredMessage": "La clé API est requise pour se connecter à {}", - "settings.ApiKeyRequired": "Clé API requise", - "settings.ApiKey": "clé API", - "settings.AmoledThemeDescription": "Thème noir pur", - "settings.AmoledThemeBordersDescription": "Ajouter des bordures subtiles dans toute l'interface", - "settings.AmoledThemeBorders": "Bordures du thème AMOLED", - "settings.AmoledTheme": "Thème AMOLED", - "settings.AccountDescription": "Votre compte LunaSea", - "settings.Account": "Compte", - "settings.NoExternalModulesFound": "Aucun module externe trouvé", - "settings.ModuleNotFound": "Module introuvable", - "settings.EditModule": "Modifier le module", - "settings.DisplayName": "Nom affiché", - "settings.DeleteModuleSuccess": "Module supprimé", - "settings.DeleteModuleHint1": "Êtes-vous sûr(e) de vouloir supprimer ce module externe ?", - "settings.DeleteModule": "Supprimer le module", - "settings.AllFieldsAreRequired": "Tous les champs sont requis", - "settings.AddModuleSuccess": "Module ajouté", - "settings.AddModuleFailed": "Échec lors de l'ajout du module", - "settings.AddModule": "Ajouter un module", - "settings.AccountDeletedMessage": "Votre compte LunaSea a été supprimé", - "settings.DeleteAccountHint2": "Ce processus supprimera également toutes les sauvegardes stockées dans le nuage et les données liées à ce compte.", - "settings.DeleteAccountWarning1": "Ce processus est irréversible", - "settings.FailedToDeleteAccount": "Échec de la suppression du compte", - "settings.AccountSettings": "Paramètres du compte", - "settings.DeleteAccount": "Supprimer le compte", - "settings.AccountDeleted": "Compte supprimé", - "settings.DeleteAccountDescription": "Supprimer définitivement votre compte", - "settings.DeleteAccountHint1": "Voulez-vous vraiment supprimer votre compte LunaSea ?", - "settings.StartingView": "Vue d'ensemble", - "settings.AddProfileDescription": "Ajouter un nouveau profil", - "settings.DefaultPages": "Pages par défaut", - "settings.DefaultPagesDescription": "Définir les pages de destination par défaut", - "settings.DefaultSortingAndFiltering": "Tri et filtrage par défaut", - "settings.DefaultSortingAndFilteringDescription": "Définir les méthodes de tri et de filtrage par défaut", - "settings.DeleteProfileDescription": "Supprimer un profil existant", - "settings.RenameProfileDescription": "Renommer un profil existant", - "settings.Add": "Ajouter", - "settings.SortCategory": "Catégorie de tri", - "settings.DefaultOptions": "Options par défaut", - "settings.FilterCategory": "Catégorie de filtre", - "settings.DefaultOptionsDescription": "Définir des options de tri, de filtrage et d'affichage", - "settings.SortDirection": "Direction du tri", - "settings.ClearImageCache": "Effacer le cache d'image", - "settings.ClearImageCacheHint1": "Êtes-vous sûr(e) de vouloir effacer toutes les images du cache ?", - "settings.ClearImageCacheHint2": "Le retéléchargement d'images pour une grande bibliothèque peut consommer une grande quantité de données.", - "settings.Network": "Réseau", - "settings.NetworkDescription": "Personnaliser les fonctionnalités du réseau", - "settings.TLSCertificateValidation": "Validation du certificat TLS", - "settings.TLSCertificateValidationDescription": "Valider les certificats dans les connexions TLS", - "settings.ViewRecentChanges": "Afficher les changements récents", + "dashboard.Wednesday": "Mercredi", + "dashboard.TwoWeeks": "Deux semaines", + "dashboard.Tuesday": "Mardi", + "dashboard.Thursday": "Jeudi", + "dashboard.Sunday": "Dimanche", + "dashboard.Schedule": "Programme", + "dashboard.Saturday": "Samedi", + "dashboard.PastDaysDescription": "Définissez le nombre de jours passés pour lesquels vous souhaitez récupérer les données du calendrier.", + "dashboard.PastDays": "Les jours précédents", + "dashboard.OneWeek": "Une semaine", + "dashboard.OneMonth": "Un mois", + "dashboard.NoNewContent": "Aucun nouveau contenu", + "dashboard.Monday": "Lundi", + "dashboard.MinimumOfOneDay": "Minimum 1 jour", + "dashboard.FutureDaysDescription": "Définissez le nombre de jours pour lesquels vous souhaitez récupérer les données du calendrier.", + "dashboard.FutureDays": "Jours à venir", + "dashboard.Friday": "Vendredi", + "dashboard.Calendar": "Calendrier", + "dashboard.Modules": "Modules", + "lidarr.StartSearchFor": "Démarrer la recherche de…", + "lidarr.StartSearchForMissingAlbums": "Lancez la recherche des albums manquants", "lunasea.Add": "Ajouter", "lunasea.Alpha": "Alpha", "lunasea.AnErrorHasOccurred": "Une erreur s'est produite", @@ -265,6 +85,14 @@ "lunasea.Update": "Mettre à jour", "lunasea.View": "Vue", "lunasea.Website": "Site web", + "overseerr.Users": "Utilisateurs", + "overseerr.UnknownUser": "Utilisateur inconnu", + "overseerr.NoUsersFound": "Aucun utilisateur trouvé", + "overseerr.Requests": "Requêtes", + "overseerr.NoRequestsFound": "Aucune demande trouvée", + "overseerr.NoRequests": "Aucune demande", + "overseerr.OneRequest": "1 demande", + "overseerr.SomeRequests": "{} demandes", "radarr.AddMovie": "Ajouter un film", "radarr.AddMovieAndSearch": "Ajouter + Rechercher", "radarr.AddedTag": "Étiquette ajoutée", @@ -391,103 +219,207 @@ "search.Alphabetical": "Alphabétique", "search.AllSubcategories": "Toutes les sous-catégories", "search.Age": "Âge", - "dashboard.Wednesday": "Mercredi", - "dashboard.TwoWeeks": "Deux semaines", - "dashboard.Tuesday": "Mardi", - "dashboard.Thursday": "Jeudi", - "dashboard.Sunday": "Dimanche", - "dashboard.Schedule": "Programme", - "dashboard.Saturday": "Samedi", - "dashboard.PastDaysDescription": "Définissez le nombre de jours passés pour lesquels vous souhaitez récupérer les données du calendrier.", - "dashboard.PastDays": "Les jours précédents", - "dashboard.OneWeek": "Une semaine", - "dashboard.OneMonth": "Un mois", - "dashboard.NoNewContent": "Aucun nouveau contenu", - "dashboard.Monday": "Lundi", - "dashboard.MinimumOfOneDay": "Minimum 1 jour", - "dashboard.FutureDaysDescription": "Définissez le nombre de jours pour lesquels vous souhaitez récupérer les données du calendrier.", - "dashboard.FutureDays": "Jours à venir", - "dashboard.Friday": "Vendredi", - "dashboard.Calendar": "Calendrier", - "dashboard.Modules": "Modules", - "tautulli.Burn": "Graver", - "tautulli.Year": "Année", - "tautulli.ViewWebGUI": "Afficher l'interface Web", - "tautulli.Video": "Vidéo", - "tautulli.Users": "Utilisateurs", - "tautulli.User": "Utilisateur", - "tautulli.Transcodes": "Transcodes", - "tautulli.Transcode": "Transcode", - "tautulli.Title": "Titre", - "tautulli.Throttled": "Throttled", - "tautulli.TerminateSessionFailed": "Échec de la fin de la session", - "tautulli.TerminateSession": "Mettre fin à la session", - "tautulli.TerminatedSession": "Session terminée", - "tautulli.TerminationMessage": "Message de fin de session", - "tautulli.TerminationConfirmMessage": "Voulez-vous mettre fin à cette session ?", - "tautulli.TerminationAttachMessage": "Vous pouvez éventuellement joindre un message ci-dessous.", - "tautulli.Terminate": "Terminer", - "tautulli.Subtitle": "Sous-titre", - "tautulli.Stream": "Flux", - "tautulli.SessionsMany": "{} Sessions", - "tautulli.SessionsOne": "1 session", - "tautulli.Sessions": "Sessions", - "tautulli.SessionEnded": "Session terminée", - "tautulli.Season": "Saison {}", - "tautulli.Quality": "Qualité", - "tautulli.Product": "Produit", - "tautulli.Player": "Lecteur", - "tautulli.Platform": "Plateforme", - "tautulli.NoActiveStreams": "Aucun flux en cours", - "tautulli.None": "Aucun", - "tautulli.More": "Plus", - "tautulli.Metadata": "Métadonnées", - "tautulli.Location": "Localisation", - "tautulli.Library": "Bibliothèque", - "tautulli.History": "Historique", - "tautulli.ETA": "Temps restant", - "tautulli.Episode": "Épisode {}", - "tautulli.Duration": "Durée", - "tautulli.DirectStreams": "Flux directs", - "tautulli.DirectStream": "Flux direct", - "tautulli.DirectPlays": "Lectures directe", - "tautulli.DirectPlay": "Lecture directe", - "tautulli.DeletingTemporarySessionsFailed": "Échec de la suppression des sessions temporaires", - "tautulli.DeletingTemporarySessionsDescription": "Les sessions temporaires ont été supprimés", - "tautulli.DeletingImageCacheDescription": "Le cache des images de Tautulli a été supprimé", - "tautulli.DeletingTemporarySessions": "Suppression des sessions temporaires…", - "tautulli.DeletingImageCacheFailed": "Échec de la suppression du cache d'image", - "tautulli.DeletingImageCache": "Suppression du cache des images…", - "tautulli.DeletingCacheFailed": "Échec de la suppression du cache", - "tautulli.DeletingCacheDescription": "Le cache de Tautulli est en cours de suppression", - "tautulli.DeletingCache": "Suppression du cache en cours…", - "tautulli.DeleteTemporarySessions": "Supprimer les sessions temporaires", - "tautulli.DeleteImageCache": "Supprimer le cache des images", - "tautulli.DeleteCache": "Supprimer le cache", - "tautulli.Copy": "Copier", - "tautulli.Container": "Conteneur", - "tautulli.Bandwidth": "Bande passante", - "tautulli.BackupDatabase": "Sauvegarder la base de donnée", - "tautulli.BackupConfiguration": "Sauvegarder la configuration", - "tautulli.BackingUpDatabaseFailed": "Échec de la sauvegarde de la base de données", - "tautulli.BackingUpDatabaseDescription": "Sauvegarde de votre base de données en arrière-plan", - "tautulli.BackingUpDatabase": "Sauvegarde de la base de données…", - "tautulli.BackingUpConfigurationFailed": "Échec de la sauvegarde de la configuration", - "tautulli.BackingUpConfigurationDescription": "Sauvegarde de votre configuration en arrière-plan", - "tautulli.BackingUpConfiguration": "Sauvegarde de la configuration…", - "tautulli.Audio": "Audio", - "tautulli.ActivityDetails": "Détails de l'activité", - "tautulli.Activity": "Activité", - "lidarr.StartSearchFor": "Démarrer la recherche de…", - "lidarr.StartSearchForMissingAlbums": "Lancez la recherche des albums manquants", - "overseerr.Users": "Utilisateurs", - "overseerr.UnknownUser": "Utilisateur inconnu", - "overseerr.NoUsersFound": "Aucun utilisateur trouvé", - "overseerr.Requests": "Requêtes", - "overseerr.NoRequestsFound": "Aucune demande trouvée", - "overseerr.NoRequests": "Aucune demande", - "overseerr.OneRequest": "1 demande", - "overseerr.SomeRequests": "{} demandes", + "settings.MustBeValueBetween": "Doit être une valeur entre {} et {}", + "settings.UsernameValidation": "Nom d'utilisateur requis", + "settings.Username": "Nom d'utilisateur", + "settings.StartingType": "Type de démarrage", + "settings.StartingSize": "Taille initiale", + "settings.StartingDay": "Jour de début", + "settings.ShowCalendarEntries": "Afficher {} entrées du calendrier", + "settings.SignOutHint1": "Etes-vous sûr de vouloir vous déconnecter de votre compte LunaSea ?", + "settings.SignOut": "Se déconnecter", + "settings.RenameProfile": "Renommer le profil", + "settings.ProfileNameRequired": "Nom du profil requis", + "settings.ProfileName": "Nom du profil", + "settings.ProfileAlreadyExists": "Le profil existe déjà", + "settings.PasswordValidation": "Mot de passe requis", + "settings.Password": "Mot de passe", + "settings.OpenLinksIn": "Ouvrir les liens avec…", + "settings.NoHeadersAdded": "Aucun en-tête ajouté", + "settings.NoBackupsFound": "Aucune sauvegarde trouvée", + "settings.MinimumCharacters": "Minimum {} caractères", + "settings.MACAddressValidation": "Adresse MAC non valide", + "settings.MACAddressHint4": "Chaque octet hexadécimal doit être séparé par deux points (:)", + "settings.MACAddressHint3": "Les chiffres hexadécimaux vont de 0 à 9 et de A à F", + "settings.MACAddressHint2": "Les adresses MAC contiennent six nibbles hexidécimaux à deux chiffres (un octet)", + "settings.MACAddressHint1": "Il s'agit de l'adresse MAC de l'ordinateur que vous souhaitez réveiller", + "settings.MACAddress": "Adresse MAC", + "settings.Language": "Langue", + "settings.ImageBackgroundOpacityHint2": "Pour désactiver complètement la récupération des images d'arrière-plan, définissez la valeur à 0.", + "settings.ImageBackgroundOpacityHint1": "Définissez l'opacité de l'arrière-plan.", + "settings.ImageBackgroundOpacity": "Opacité de l'arrière-plan", + "settings.HostValidation": "L'hôte doit inclure http:// ou https://", + "settings.HostHint5": "Pour ajouter l'authentification basique, veuillez utiliser la fonction d'en-têtes personnalisés", + "settings.HostHint4": "Si vous n'utilisez pas de proxy inverse, veuillez inclure le port", + "settings.HostHint3": "N'utilisez pas localhost ou 127.0.0.1", + "settings.HostHint2": "Vous devez inclure soit http:// soit https://", + "settings.HostHint1": "Il s'agit de l'URL à partir de laquelle vous accédez à l'interface graphique web du service", + "settings.Host": "Hôte", + "settings.HeaderValueValidation": "Valeur d'en-tête requise", + "settings.HeaderValue": "Valeur de l'en-tête", + "settings.HeaderKeyValidation": "Clé d'en-tête requise", + "settings.HeaderKey": "Clé d'en-tête", + "settings.HeaderDeleted": "En-tête supprimé", + "settings.HeaderAdded": "En-tête ajouté", + "settings.EncryptionKey": "Clé de chiffrement", + "settings.EnabledProfile": "Profil activé", + "settings.DismissBanners": "Cacher les bannières", + "settings.DismissBannersHint1": "Êtes-vous sûr(e) de vouloir cacher toutes les infobulles ?", + "settings.DismissBannersHint2": "Les d'infobulles vous donneront des conseils et des astuces pour les fonctionnalités disponibles dans LunaSea.", + "settings.DeleteProfile": "Supprimer le profil", + "settings.DeleteIndexerHint1": "Êtes-vous sûr(e) de vouloir supprimer cet indexeur ?", + "settings.DeleteIndexer": "Supprimer l'indexeur", + "settings.DeleteHeaderHint1": "Êtes-vous sûr(e) de vouloir supprimer cet en-tête ?", + "settings.DeleteHeader": "Supprimer l'en-tête", + "settings.DecryptBackupHint1": "Veuillez entrer la clé de chiffrage pour cette sauvegarde.", + "settings.DecryptBackup": "Déchiffrer la sauvegarde", + "settings.CustomHeaders": "En-têtes personnalisés", + "settings.CustomHeader": "En-tête personnalisé", + "settings.Custom": "Personnaliser…", + "settings.ClearLogs": "Effacer les journaux", + "settings.ClearLogsHint1": "Êtes-vous sûr(e) de vouloir effacer tous les journaux enregistrés ?\n\nLes journaux peuvent être utiles pour les rapports de bogues et le débogage.", + "settings.ClearConfigurationHint3": "Si vous êtes connecté(e) à un compte LunaSea, vous serez déconnecté(e).", + "settings.ClearConfigurationHint2": "Vous allez repartir de zéro, assurez-vous d'avoir sauvegardé votre configuration actuelle !", + "settings.ClearConfigurationHint1": "Êtes-vous sûr(e) de vouloir effacer votre configuration ?", + "settings.ClearConfiguration": "Effacer la configuration", + "settings.BroadcastAddressValidation": "Adresse de diffusion non valide", + "settings.BroadcastAddressHint3": "Si l'on prend comme exemple l'adresse IP de la machine 192.168.1.111, l'adresse IP de diffusion résultante est 192.168.1.255", + "settings.BroadcastAddressHint2": "En général, il s'agit de l'adresse IP de votre machine, le dernier octet étant fixé à 255", + "settings.BroadcastAddressHint1": "Ceci est l'adresse de diffusion de votre réseau local", + "settings.BroadcastAddress": "Adresse de diffusion", + "settings.BasicAuthenticationHint3": "Le nom d'utilisateur et le mot de passe sont automatiquement convertis en encodage base64", + "settings.BasicAuthenticationHint2": "Le mot de passe peut contenir le caractère deux-points", + "settings.BasicAuthenticationHint1": "Le nom d'utilisateur ne doit pas contenir de deux-points", + "settings.BasicAuthentication": "Authentification basique", + "settings.BannersNotificationModuleSupportBody": "Les notifications basées sur les Webhooks ne sont actuellement supportées que dans les modules listés ci-dessous.\n\nD'autres modules seront pris en charge à l'avenir !", + "settings.BannersNotificationModuleSupportHeader": "Modules pris en charge", + "settings.BackupList": "Liste des sauvegardes", + "settings.BackupConfigurationHint2": "La clé de chiffrement doit comporter au moins 8 caractères", + "settings.BackupConfigurationHint1": "Toutes les sauvegardes sont cryptées avant d'être exportées", + "settings.BackupConfiguration": "Sauvegarder la configuration", + "settings.AddProfile": "Ajouter un profil", + "settings.AddHeader": "Ajouter un en-tête", + "settings.AccountHelpHint1": "LunaSea propose un compte gratuit pour sauvegarder votre configuration sur le cloud, avec des fonctionnalités supplémentaires à venir !", + "settings.AccountHelp": "Compte LunaSea", + "settings.TestConnection": "Tester la connexion", + "settings.SystemDescription": "Utilitaires système et informations", + "settings.System": "Système", + "settings.SignIn": "Se connecter", + "settings.SignedOutSuccessMessage": "Déconnecté de votre compte LunaSea", + "settings.SignedOutSuccess": "Se déconnecter", + "settings.SignedOutFailure": "Échec lors de la déconnexion", + "settings.SignedInSuccess": "Connexion réussie", + "settings.SignedInFailure": "Échec de la connexion", + "settings.RestoreFromCloudSuccessMessage": "Votre configuration a été restaurée", + "settings.RestoreFromCloudSuccess": "Restauré avec succès", + "settings.RestoreFromCloudFailure": "Échec de la restauration", + "settings.RestoreFromCloudDescription": "Restaurer les données de configuration", + "settings.RestoreFromCloud": "Restaurer à partir du cloud", + "settings.ResourcesDescription": "Ressources et liens utiles", + "settings.Resources": "Ressources", + "settings.ResetPassword": "Réinitialiser le mot de passe", + "settings.RegisteredSuccess": "Inscrit", + "settings.RegisteredFailure": "Échec de l'inscription", + "settings.Register": "S'inscrire", + "settings.QuickActionsDescription": "Actions rapides sur l'écran d'accueil", + "settings.QuickActions": "Actions rapides", + "settings.Profiles": "Profils", + "settings.ProfilesDescription": "Gérez vos profils", + "settings.NotificationsDescription": "Configurer les crochets pour les notifications push", + "settings.Notifications": "Notifications", + "settings.LocalizationDescription": "Personnalisez selon vos paramètres régionaux", + "settings.Localization": "Localisation", + "settings.InvalidPasswordMessage": "Le mot de passe est invalide", + "settings.InvalidPassword": "Mot de passe incorrect", + "settings.InvalidEmailMessage": "L'adresse électronique est invalide", + "settings.InvalidEmail": "Email invalide", + "settings.HostRequiredMessage": "L'hôte est requis pour se connecter à {}", + "settings.HostRequired": "Hôte requis", + "settings.ForgotYourPassword": "Mot de passe oublié ?", + "settings.EmailSentSuccessMessage": "Un courriel pour réinitialiser votre mot de passe a été envoyé !", + "settings.EmailSentSuccess": "Courriel envoyé", + "settings.EmailSentFailure": "Échec de la réinitialisation du mot de passe", + "settings.Email": "Courriel", + "settings.DrawerDescription": "Personnalisez le menu latéral", + "settings.Drawer": "Menu latéral", + "settings.DonationsDescription": "Faire un don au développeur", + "settings.Donations": "Dons", + "settings.DeleteCloudBackupSuccess": "Supprimé", + "settings.DeleteCloudBackupFailure": "Échec de la suppression", + "settings.DeleteCloudBackupDescription": "Supprimer un fichier de configuration", + "settings.DeleteCloudBackup": "Supprimer la sauvegarde dans le cloud", + "settings.DebugMenuDescription": "Utilitaires de débogage et de développement", + "settings.DebugMenu": "Menu de débogage", + "settings.DefaultPage": "Page par défaut", + "settings.CustomHeadersDescription": "Ajouter des en-têtes personnalisés aux demandes", + "settings.ConnectionTestFailed": "Le test de connexion a échoué", + "settings.ConnectionDetailsDescription": "Détails de connexion pour {}", + "settings.ConnectionDetails": "Détails de connexion", + "settings.ConnectedSuccessfullyMessage": "{} est prêt à être utilisé avec LunaSea !", + "settings.ConnectedSuccessfully": "Connecté", + "settings.ConfigurationDescription": "Configurer et paramétrer LunaSea", + "settings.Configuration": "Configuration", + "settings.ConfigureModule": "Configurer {}", + "settings.BackupToCloudSuccess": "Sauvegardé avec succès", + "settings.BackupToCloudFailure": "Échec de la sauvegarde", + "settings.BackupToCloudDescription": "Sauvegarde des données de configuration", + "settings.BackupToCloud": "Sauvegarde dans le cloud", + "settings.BackgroundImageOpacity": "Opacité de l'image d'arrière-plan", + "settings.AutomaticallyManageOrderDescription": "Liste des modules par ordre alphabétique", + "settings.AutomaticallyManageOrder": "Tri automatique", + "settings.AppearanceDescription": "Personnalisez l'apparence", + "settings.Appearance": "Apparence", + "settings.ApiKeyRequiredMessage": "La clé API est requise pour se connecter à {}", + "settings.ApiKeyRequired": "Clé API requise", + "settings.ApiKey": "clé API", + "settings.AmoledThemeDescription": "Thème noir pur", + "settings.AmoledThemeBordersDescription": "Ajouter des bordures subtiles dans toute l'interface", + "settings.AmoledThemeBorders": "Bordures du thème AMOLED", + "settings.AmoledTheme": "Thème AMOLED", + "settings.AccountDescription": "Votre compte LunaSea", + "settings.Account": "Compte", + "settings.NoExternalModulesFound": "Aucun module externe trouvé", + "settings.ModuleNotFound": "Module introuvable", + "settings.EditModule": "Modifier le module", + "settings.DisplayName": "Nom affiché", + "settings.DeleteModuleSuccess": "Module supprimé", + "settings.DeleteModuleHint1": "Êtes-vous sûr(e) de vouloir supprimer ce module externe ?", + "settings.DeleteModule": "Supprimer le module", + "settings.AllFieldsAreRequired": "Tous les champs sont requis", + "settings.AddModuleSuccess": "Module ajouté", + "settings.AddModuleFailed": "Échec lors de l'ajout du module", + "settings.AddModule": "Ajouter un module", + "settings.AccountDeletedMessage": "Votre compte LunaSea a été supprimé", + "settings.DeleteAccountHint2": "Ce processus supprimera également toutes les sauvegardes stockées dans le nuage et les données liées à ce compte.", + "settings.DeleteAccountWarning1": "Ce processus est irréversible", + "settings.FailedToDeleteAccount": "Échec de la suppression du compte", + "settings.AccountSettings": "Paramètres du compte", + "settings.DeleteAccount": "Supprimer le compte", + "settings.AccountDeleted": "Compte supprimé", + "settings.DeleteAccountDescription": "Supprimer définitivement votre compte", + "settings.DeleteAccountHint1": "Voulez-vous vraiment supprimer votre compte LunaSea ?", + "settings.StartingView": "Vue d'ensemble", + "settings.AddProfileDescription": "Ajouter un nouveau profil", + "settings.DefaultPages": "Pages par défaut", + "settings.DefaultPagesDescription": "Définir les pages de destination par défaut", + "settings.DefaultSortingAndFiltering": "Tri et filtrage par défaut", + "settings.DefaultSortingAndFilteringDescription": "Définir les méthodes de tri et de filtrage par défaut", + "settings.DeleteProfileDescription": "Supprimer un profil existant", + "settings.RenameProfileDescription": "Renommer un profil existant", + "settings.Add": "Ajouter", + "settings.SortCategory": "Catégorie de tri", + "settings.DefaultOptions": "Options par défaut", + "settings.FilterCategory": "Catégorie de filtre", + "settings.DefaultOptionsDescription": "Définir des options de tri, de filtrage et d'affichage", + "settings.SortDirection": "Direction du tri", + "settings.ClearImageCache": "Effacer le cache d'image", + "settings.ClearImageCacheHint1": "Êtes-vous sûr(e) de vouloir effacer toutes les images du cache ?", + "settings.ClearImageCacheHint2": "Le retéléchargement d'images pour une grande bibliothèque peut consommer une grande quantité de données.", + "settings.Network": "Réseau", + "settings.NetworkDescription": "Personnaliser les fonctionnalités du réseau", + "settings.TLSCertificateValidation": "Validation du certificat TLS", + "settings.TLSCertificateValidationDescription": "Valider les certificats dans les connexions TLS", + "settings.ViewRecentChanges": "Afficher les changements récents", "sonarr.AddedOn": "Ajouté le", "sonarr.AddedSeries": "Série ajoutée", "sonarr.AddedTag": "Étiquette ajoutée", @@ -695,5 +627,73 @@ "sonarr.Leechers": "Receveurs", "sonarr.Seeders": "Envoyeurs", "sonarr.NoLongerMonitoring": "Plus de suivi", - "sonarr.StartSearchForCutoffUnmetEpisodes": "Démarrer la recherche d'épisodes ne correspondant pas" + "sonarr.StartSearchForCutoffUnmetEpisodes": "Démarrer la recherche d'épisodes ne correspondant pas", + "tautulli.Burn": "Graver", + "tautulli.Year": "Année", + "tautulli.ViewWebGUI": "Afficher l'interface Web", + "tautulli.Video": "Vidéo", + "tautulli.Users": "Utilisateurs", + "tautulli.User": "Utilisateur", + "tautulli.Transcodes": "Transcodes", + "tautulli.Transcode": "Transcode", + "tautulli.Title": "Titre", + "tautulli.Throttled": "Throttled", + "tautulli.TerminateSessionFailed": "Échec de la fin de la session", + "tautulli.TerminateSession": "Mettre fin à la session", + "tautulli.TerminatedSession": "Session terminée", + "tautulli.TerminationMessage": "Message de fin de session", + "tautulli.TerminationConfirmMessage": "Voulez-vous mettre fin à cette session ?", + "tautulli.TerminationAttachMessage": "Vous pouvez éventuellement joindre un message ci-dessous.", + "tautulli.Terminate": "Terminer", + "tautulli.Subtitle": "Sous-titre", + "tautulli.Stream": "Flux", + "tautulli.SessionsMany": "{} Sessions", + "tautulli.SessionsOne": "1 session", + "tautulli.Sessions": "Sessions", + "tautulli.SessionEnded": "Session terminée", + "tautulli.Season": "Saison {}", + "tautulli.Quality": "Qualité", + "tautulli.Product": "Produit", + "tautulli.Player": "Lecteur", + "tautulli.Platform": "Plateforme", + "tautulli.NoActiveStreams": "Aucun flux en cours", + "tautulli.None": "Aucun", + "tautulli.More": "Plus", + "tautulli.Metadata": "Métadonnées", + "tautulli.Location": "Localisation", + "tautulli.Library": "Bibliothèque", + "tautulli.History": "Historique", + "tautulli.ETA": "Temps restant", + "tautulli.Episode": "Épisode {}", + "tautulli.Duration": "Durée", + "tautulli.DirectStreams": "Flux directs", + "tautulli.DirectStream": "Flux direct", + "tautulli.DirectPlays": "Lectures directe", + "tautulli.DirectPlay": "Lecture directe", + "tautulli.DeletingTemporarySessionsFailed": "Échec de la suppression des sessions temporaires", + "tautulli.DeletingTemporarySessionsDescription": "Les sessions temporaires ont été supprimés", + "tautulli.DeletingImageCacheDescription": "Le cache des images de Tautulli a été supprimé", + "tautulli.DeletingTemporarySessions": "Suppression des sessions temporaires…", + "tautulli.DeletingImageCacheFailed": "Échec de la suppression du cache d'image", + "tautulli.DeletingImageCache": "Suppression du cache des images…", + "tautulli.DeletingCacheFailed": "Échec de la suppression du cache", + "tautulli.DeletingCacheDescription": "Le cache de Tautulli est en cours de suppression", + "tautulli.DeletingCache": "Suppression du cache en cours…", + "tautulli.DeleteTemporarySessions": "Supprimer les sessions temporaires", + "tautulli.DeleteImageCache": "Supprimer le cache des images", + "tautulli.DeleteCache": "Supprimer le cache", + "tautulli.Copy": "Copier", + "tautulli.Container": "Conteneur", + "tautulli.Bandwidth": "Bande passante", + "tautulli.BackupDatabase": "Sauvegarder la base de donnée", + "tautulli.BackupConfiguration": "Sauvegarder la configuration", + "tautulli.BackingUpDatabaseFailed": "Échec de la sauvegarde de la base de données", + "tautulli.BackingUpDatabaseDescription": "Sauvegarde de votre base de données en arrière-plan", + "tautulli.BackingUpDatabase": "Sauvegarde de la base de données…", + "tautulli.BackingUpConfigurationFailed": "Échec de la sauvegarde de la configuration", + "tautulli.BackingUpConfigurationDescription": "Sauvegarde de votre configuration en arrière-plan", + "tautulli.BackingUpConfiguration": "Sauvegarde de la configuration…", + "tautulli.Audio": "Audio", + "tautulli.ActivityDetails": "Détails de l'activité", + "tautulli.Activity": "Activité" } \ No newline at end of file diff --git a/assets/localization/hu.json b/assets/localization/hu.json index 6ec5b8dbc0..55a7ee3afa 100644 --- a/assets/localization/hu.json +++ b/assets/localization/hu.json @@ -1,4 +1,176 @@ { + "dashboard.Wednesday": "Szerda", + "dashboard.TwoWeeks": "Két hét", + "dashboard.Tuesday": "Kedd", + "dashboard.Thursday": "Csütörtök", + "dashboard.Sunday": "Vasárnap", + "dashboard.Schedule": "Ütemezve", + "dashboard.Saturday": "Szombat", + "dashboard.PastDaysDescription": "Állítsa be a \"Nemrégiben\" rész napok számát a, amelyekre be szeretné tölteni a naptárbejegyzéseket.", + "dashboard.PastDays": "Nemrégiben", + "dashboard.OneWeek": "Egy hét", + "dashboard.OneMonth": "Egy hónap", + "dashboard.NoNewContent": "Nincs új tartalom", + "dashboard.Monday": "Hétfő", + "dashboard.Modules": "Modul", + "dashboard.MinimumOfOneDay": "Minimum 1 nap", + "dashboard.FutureDaysDescription": "Állítsa be a \"hamarosan\" rész napjainak számát, amelyekre be szeretné tölteni a naptárbejegyzéseket.", + "dashboard.FutureDays": "Hamarosan", + "dashboard.Friday": "Péntek", + "dashboard.Calendar": "Naptár", + "lunasea.Add": "Hozzáadás", + "lunasea.AnErrorHasOccurred": "Hiba történt", + "lunasea.BackUp": "Biztonsági mentés", + "lunasea.Cancel": "Mégsem", + "lunasea.ChangeProfiles": "Profil váltása", + "lunasea.CheckLogsMessage": "További részletekért ellenőrizze a naplót", + "lunasea.Clear": "Törlés", + "lunasea.ComingSoon": "Hamarosan", + "lunasea.Dashboard": "Irányítópult", + "lunasea.Delete": "Törlés", + "lunasea.Disable": "Letiltás", + "lunasea.Disabled": "Letiltva", + "lunasea.Dismiss": "Elvetés", + "lunasea.ExternalModules": "Külső modulok", + "lunasea.GoBack": "Vissza", + "lunasea.GoToSettings": "Beállítások", + "lunasea.IncorrectEncryptionKey": "Helytelen titkosítási kulcs", + "lunasea.Module": "Modul", + "lunasea.ModuleIsNotEnabled": "{} nincs engedélyezve", + "lunasea.NoModulesEnabled": "Nincs engedélyezett modul", + "lunasea.NotSet": "Nincs beállítva", + "lunasea.Page": "Oldal", + "lunasea.Refresh": "Frissítés", + "lunasea.Rename": "Átnevezés", + "lunasea.Restore": "Visszaállítás", + "lunasea.ReturnToDashboard": "Vissza az Irányítópultra", + "lunasea.SearchTextBar": "Keresés…", + "lunasea.Set": "Beállítva", + "lunasea.Settings": "Beállítások", + "lunasea.TryAgain": "Próbálja újra", + "lunasea.Unknown": "Ismeretlen", + "lunasea.UnknownError": "Ismeretlen hiba", + "lunasea.UnknownModule": "Ismeretlen modul", + "lunasea.Website": "Weboldal", + "radarr.AddMovie": "Film hozzáadása", + "radarr.AddMovieAndSearch": "Hozzáadás és keresés", + "radarr.Age": "Kor", + "radarr.All": "Összes", + "radarr.Alphabetical": "ABC", + "radarr.Approved": "Jóváhagyott", + "radarr.Automatic": "Automatikus", + "radarr.AvailabilityUnknown": "Elérhetőség ismeretlen", + "radarr.AvailableIn": "Elérhető: {}", + "radarr.AvailableToday": "Ma elérhető", + "radarr.BackupDatabase": "Adatbázis mentése", + "radarr.CinemaDateUnknown": "A vetítés dátuma ismeretlen", + "radarr.Configure": "Konfigurálás", + "radarr.Copy": "Másolás", + "radarr.CopyFull": "Hardlink/Fájlok másolása", + "radarr.CutoffUnmet": "Teljesítetlen levágása", + "radarr.DateAdded": "Hozzáadás dátuma", + "radarr.DigitalRelease": "Digitális kiadvány", + "radarr.DirectoryNotFound": "Mappa nem található", + "radarr.Discover": "Felfedezés", + "radarr.DownloadFailed": "Letöltés sikertelen", + "radarr.DownloadIgnored": "Letöltés figyelmen kívül hagyva", + "radarr.EditMovie": "Film szerkesztése", + "radarr.FileBrowser": "Fájl böngésző…", + "radarr.FilterCatalogue": "Katalógus szűrése", + "radarr.GrabbedFrom": "Elkapta {}", + "radarr.History": "Előzmények", + "radarr.HistoryDescription": "Legutóbbi tevékenység megtekintése", + "radarr.Import": "Importálás", + "radarr.ImportMode": "Importálási mód", + "radarr.InCinemas": "A mozikban", + "radarr.InCinemasIn": "Moziban: {}", + "radarr.InCinemasToday": "Moziban mától", + "radarr.Interactive": "Interaktív", + "radarr.Language": "Nyelv", + "radarr.Languages": "Nyelvek", + "radarr.ManualImport": "Manuális importálás", + "radarr.ManualImportDescription": "Filmek importálása a fájlrendszerből", + "radarr.MinimumAvailability": "Minimum elérhetőség", + "radarr.Missing": "Hiányzik", + "radarr.MonitorMovie": "Filmek monitorozása", + "radarr.Monitored": "Monitorozva", + "radarr.MonitoredDescription": "Új megjelenések monitorozása", + "radarr.More": "Továbbiak", + "radarr.Move": "Mozgatás", + "radarr.MoveFull": "Fájlok mozgatása", + "radarr.Movie": "Film", + "radarr.MovieFileDeleted": "Film fájl törölve", + "radarr.MovieFileRenamed": "Film fájl átnevezése", + "radarr.MovieImported": "Film importálva ({})", + "radarr.MovieNotFound": "Film nem található", + "radarr.MoviePath": "Film elérési útja", + "radarr.Movies": "Filmek", + "radarr.NoFilesFound": "Nem találtam fájlokat", + "radarr.NoHistoryFound": "Nincsenek előzmények", + "radarr.NoMoviesFound": "Nem található film", + "radarr.NoResultsFound": "Nincs találat", + "radarr.NoSubdirectoriesFound": "Nincsenek almappák", + "radarr.NoSummaryIsAvailable": "Összefoglaló nem áll rendelkezésre.", + "radarr.ParentDirectory": "Szülőkönyvtár", + "radarr.PhysicalRelease": "Fizikai megjelenés", + "radarr.Quality": "Minőség", + "radarr.QualityProfile": "Minőségi profil", + "radarr.Queue": "Sor", + "radarr.QueueDescription": "Aktív és várakozási sorban lévő tartalom megtekintése", + "radarr.Quick": "Gyors", + "radarr.RefreshMovie": "Film frissítése", + "radarr.Rejected": "Elutasítva", + "radarr.Released": "Megjelent", + "radarr.ReleasedToday": "Megjelenik ma", + "radarr.RemoveMovie": "Film eltávolítása", + "radarr.RootFolder": "Gyökérkönyvtár", + "radarr.RunRSSSync": "RSS szinkronizálás futtatása", + "radarr.Runtime": "Futásidő", + "radarr.Search": "Keresés", + "radarr.SearchAllMissing": "Összes hiányzó keresése", + "radarr.SearchFor": "Keresés: {}", + "radarr.Seeders": "Seederek", + "radarr.SelectLanguage": "Nyelv kiválasztása", + "radarr.SelectMovie": "Film kiválasztása", + "radarr.SelectQuality": "Minőség kiválasztása", + "radarr.Size": "Méret", + "radarr.SortCatalogue": "Katalógus rendezése", + "radarr.Studio": "Stúdió", + "radarr.SystemStatus": "Rendszer státusza", + "radarr.SystemStatusDescription": "Rendszer státusza és szabad terület", + "radarr.Tags": "Címkék", + "radarr.TagsDescription": "Cimkék rendezése", + "radarr.Type": "Típus", + "radarr.UnmonitorMovie": "Film monitorozásának visszavonása", + "radarr.Unmonitored": "Nincs monitorozva", + "radarr.Upcoming": "Hamarosan", + "radarr.UpdateLibrary": "Könyvtár frissítése", + "radarr.UpdateMovie": "Film frissítése", + "radarr.ViewWebGUI": "Webes kezelőfelület megtekintése", + "radarr.Wanted": "Kívánatos", + "radarr.Weight": "Súly", + "radarr.Year": "Év", + "search.Subcategories": "Alkategóriák", + "search.Size": "Méret", + "search.SentTo": "Elküldve {}", + "search.SentNZBData": "Elküldött NZB adatok", + "search.Search": "Keresés", + "search.Results": "Eredmények", + "search.NoSubcategoriesFound": "Nem található alkategória", + "search.NoResultsFound": "Nincs találat", + "search.NoCategoriesFound": "Nincsenek kategóriák", + "search.FailedToSend": "Nem sikerült elküldeni", + "search.FailedToDownloadNZB": "Nem sikerült letölteni az NZB-t", + "search.DownloadToDevice": "Letöltés az eszközre", + "search.DownloadingNZBToDevice": "NZB letöltése az eszközre", + "search.Downloading": "Letöltés…", + "search.Download": "Letöltés", + "search.Comments": "Hozzászólások", + "search.Categories": "Kategóriák", + "search.Category": "Kategória", + "search.Alphabetical": "ABC sorrend", + "search.AllSubcategories": "Minden alkategória", + "search.Age": "Kor", "settings.ApiKeyRequiredMessage": "API-kulcs szükséges a {} való csatlakozáshoz", "settings.MustBeValueBetween": "Az értéknek {} és {} között kell lennie", "settings.UsernameValidation": "Felhasználónév kötelező", @@ -169,178 +341,6 @@ "settings.AccountHelp": "LunaSea fiók", "settings.AccountDescription": "Saját LunaSea fiók", "settings.Account": "Fiók", - "lunasea.Add": "Hozzáadás", - "lunasea.AnErrorHasOccurred": "Hiba történt", - "lunasea.BackUp": "Biztonsági mentés", - "lunasea.Cancel": "Mégsem", - "lunasea.ChangeProfiles": "Profil váltása", - "lunasea.CheckLogsMessage": "További részletekért ellenőrizze a naplót", - "lunasea.Clear": "Törlés", - "lunasea.ComingSoon": "Hamarosan", - "lunasea.Dashboard": "Irányítópult", - "lunasea.Delete": "Törlés", - "lunasea.Disable": "Letiltás", - "lunasea.Disabled": "Letiltva", - "lunasea.Dismiss": "Elvetés", - "lunasea.ExternalModules": "Külső modulok", - "lunasea.GoBack": "Vissza", - "lunasea.GoToSettings": "Beállítások", - "lunasea.IncorrectEncryptionKey": "Helytelen titkosítási kulcs", - "lunasea.Module": "Modul", - "lunasea.ModuleIsNotEnabled": "{} nincs engedélyezve", - "lunasea.NoModulesEnabled": "Nincs engedélyezett modul", - "lunasea.NotSet": "Nincs beállítva", - "lunasea.Page": "Oldal", - "lunasea.Refresh": "Frissítés", - "lunasea.Rename": "Átnevezés", - "lunasea.Restore": "Visszaállítás", - "lunasea.ReturnToDashboard": "Vissza az Irányítópultra", - "lunasea.SearchTextBar": "Keresés…", - "lunasea.Set": "Beállítva", - "lunasea.Settings": "Beállítások", - "lunasea.TryAgain": "Próbálja újra", - "lunasea.Unknown": "Ismeretlen", - "lunasea.UnknownError": "Ismeretlen hiba", - "lunasea.UnknownModule": "Ismeretlen modul", - "lunasea.Website": "Weboldal", - "radarr.AddMovie": "Film hozzáadása", - "radarr.AddMovieAndSearch": "Hozzáadás és keresés", - "radarr.Age": "Kor", - "radarr.All": "Összes", - "radarr.Alphabetical": "ABC", - "radarr.Approved": "Jóváhagyott", - "radarr.Automatic": "Automatikus", - "radarr.AvailabilityUnknown": "Elérhetőség ismeretlen", - "radarr.AvailableIn": "Elérhető: {}", - "radarr.AvailableToday": "Ma elérhető", - "radarr.BackupDatabase": "Adatbázis mentése", - "radarr.CinemaDateUnknown": "A vetítés dátuma ismeretlen", - "radarr.Configure": "Konfigurálás", - "radarr.Copy": "Másolás", - "radarr.CopyFull": "Hardlink/Fájlok másolása", - "radarr.CutoffUnmet": "Teljesítetlen levágása", - "radarr.DateAdded": "Hozzáadás dátuma", - "radarr.DigitalRelease": "Digitális kiadvány", - "radarr.DirectoryNotFound": "Mappa nem található", - "radarr.Discover": "Felfedezés", - "radarr.DownloadFailed": "Letöltés sikertelen", - "radarr.DownloadIgnored": "Letöltés figyelmen kívül hagyva", - "radarr.EditMovie": "Film szerkesztése", - "radarr.FileBrowser": "Fájl böngésző…", - "radarr.FilterCatalogue": "Katalógus szűrése", - "radarr.GrabbedFrom": "Elkapta {}", - "radarr.History": "Előzmények", - "radarr.HistoryDescription": "Legutóbbi tevékenység megtekintése", - "radarr.Import": "Importálás", - "radarr.ImportMode": "Importálási mód", - "radarr.InCinemas": "A mozikban", - "radarr.InCinemasIn": "Moziban: {}", - "radarr.InCinemasToday": "Moziban mától", - "radarr.Interactive": "Interaktív", - "radarr.Language": "Nyelv", - "radarr.Languages": "Nyelvek", - "radarr.ManualImport": "Manuális importálás", - "radarr.ManualImportDescription": "Filmek importálása a fájlrendszerből", - "radarr.MinimumAvailability": "Minimum elérhetőség", - "radarr.Missing": "Hiányzik", - "radarr.MonitorMovie": "Filmek monitorozása", - "radarr.Monitored": "Monitorozva", - "radarr.MonitoredDescription": "Új megjelenések monitorozása", - "radarr.More": "Továbbiak", - "radarr.Move": "Mozgatás", - "radarr.MoveFull": "Fájlok mozgatása", - "radarr.Movie": "Film", - "radarr.MovieFileDeleted": "Film fájl törölve", - "radarr.MovieFileRenamed": "Film fájl átnevezése", - "radarr.MovieImported": "Film importálva ({})", - "radarr.MovieNotFound": "Film nem található", - "radarr.MoviePath": "Film elérési útja", - "radarr.Movies": "Filmek", - "radarr.NoFilesFound": "Nem találtam fájlokat", - "radarr.NoHistoryFound": "Nincsenek előzmények", - "radarr.NoMoviesFound": "Nem található film", - "radarr.NoResultsFound": "Nincs találat", - "radarr.NoSubdirectoriesFound": "Nincsenek almappák", - "radarr.NoSummaryIsAvailable": "Összefoglaló nem áll rendelkezésre.", - "radarr.ParentDirectory": "Szülőkönyvtár", - "radarr.PhysicalRelease": "Fizikai megjelenés", - "radarr.Quality": "Minőség", - "radarr.QualityProfile": "Minőségi profil", - "radarr.Queue": "Sor", - "radarr.QueueDescription": "Aktív és várakozási sorban lévő tartalom megtekintése", - "radarr.Quick": "Gyors", - "radarr.RefreshMovie": "Film frissítése", - "radarr.Rejected": "Elutasítva", - "radarr.Released": "Megjelent", - "radarr.ReleasedToday": "Megjelenik ma", - "radarr.RemoveMovie": "Film eltávolítása", - "radarr.RootFolder": "Gyökérkönyvtár", - "radarr.RunRSSSync": "RSS szinkronizálás futtatása", - "radarr.Runtime": "Futásidő", - "radarr.Search": "Keresés", - "radarr.SearchAllMissing": "Összes hiányzó keresése", - "radarr.SearchFor": "Keresés: {}", - "radarr.Seeders": "Seederek", - "radarr.SelectLanguage": "Nyelv kiválasztása", - "radarr.SelectMovie": "Film kiválasztása", - "radarr.SelectQuality": "Minőség kiválasztása", - "radarr.Size": "Méret", - "radarr.SortCatalogue": "Katalógus rendezése", - "radarr.Studio": "Stúdió", - "radarr.SystemStatus": "Rendszer státusza", - "radarr.SystemStatusDescription": "Rendszer státusza és szabad terület", - "radarr.Tags": "Címkék", - "radarr.TagsDescription": "Cimkék rendezése", - "radarr.Type": "Típus", - "radarr.UnmonitorMovie": "Film monitorozásának visszavonása", - "radarr.Unmonitored": "Nincs monitorozva", - "radarr.Upcoming": "Hamarosan", - "radarr.UpdateLibrary": "Könyvtár frissítése", - "radarr.UpdateMovie": "Film frissítése", - "radarr.ViewWebGUI": "Webes kezelőfelület megtekintése", - "radarr.Wanted": "Kívánatos", - "radarr.Weight": "Súly", - "radarr.Year": "Év", - "search.Subcategories": "Alkategóriák", - "search.Size": "Méret", - "search.SentTo": "Elküldve {}", - "search.SentNZBData": "Elküldött NZB adatok", - "search.Search": "Keresés", - "search.Results": "Eredmények", - "search.NoSubcategoriesFound": "Nem található alkategória", - "search.NoResultsFound": "Nincs találat", - "search.NoCategoriesFound": "Nincsenek kategóriák", - "search.FailedToSend": "Nem sikerült elküldeni", - "search.FailedToDownloadNZB": "Nem sikerült letölteni az NZB-t", - "search.DownloadToDevice": "Letöltés az eszközre", - "search.DownloadingNZBToDevice": "NZB letöltése az eszközre", - "search.Downloading": "Letöltés…", - "search.Download": "Letöltés", - "search.Comments": "Hozzászólások", - "search.Categories": "Kategóriák", - "search.Category": "Kategória", - "search.Alphabetical": "ABC sorrend", - "search.AllSubcategories": "Minden alkategória", - "search.Age": "Kor", - "dashboard.Wednesday": "Szerda", - "dashboard.TwoWeeks": "Két hét", - "dashboard.Tuesday": "Kedd", - "dashboard.Thursday": "Csütörtök", - "dashboard.Sunday": "Vasárnap", - "dashboard.Schedule": "Ütemezve", - "dashboard.Saturday": "Szombat", - "dashboard.PastDaysDescription": "Állítsa be a \"Nemrégiben\" rész napok számát a, amelyekre be szeretné tölteni a naptárbejegyzéseket.", - "dashboard.PastDays": "Nemrégiben", - "dashboard.OneWeek": "Egy hét", - "dashboard.OneMonth": "Egy hónap", - "dashboard.NoNewContent": "Nincs új tartalom", - "dashboard.Monday": "Hétfő", - "dashboard.Modules": "Modul", - "dashboard.MinimumOfOneDay": "Minimum 1 nap", - "dashboard.FutureDaysDescription": "Állítsa be a \"hamarosan\" rész napjainak számát, amelyekre be szeretné tölteni a naptárbejegyzéseket.", - "dashboard.FutureDays": "Hamarosan", - "dashboard.Friday": "Péntek", - "dashboard.Calendar": "Naptár", "tautulli.Throttled": "Throttled", "tautulli.Year": "Év", "tautulli.ViewWebGUI": "Webes kezelőfelület megjelenítése", diff --git a/assets/localization/it.json b/assets/localization/it.json index 28dd7a65f0..6a767025a2 100644 --- a/assets/localization/it.json +++ b/assets/localization/it.json @@ -1,85 +1,23 @@ { - "settings.AccountDescription": "Il tuo account LunaSea", - "settings.Account": "Account", - "settings.EditModule": "Modifica modulo", - "settings.DonationsDescription": "Dona allo sviluppatore", - "settings.Donations": "Donazioni", - "settings.DisplayName": "Nome da mostrare", - "settings.DismissBannersHint1": "Sei sicuro/a di voler eliminare tutti i banner tooltip?", - "settings.DeleteProfile": "Elimina il profilo", - "settings.DeleteModuleSuccess": "Modulo eliminato", - "settings.DeleteModuleHint1": "Sei sicuro/a di voler eliminare questo modulo esterno?", - "settings.DeleteModule": "Elimina il modulo", - "settings.DeleteIndexerHint1": "Sei sicuro/a di voler eliminare questo indicizzatore?", - "settings.DeleteIndexer": "Elimina l'indicizzatore", - "settings.DeleteHeaderHint1": "Sei sicuro/a di voler eliminare questa intestazione?", - "settings.DeleteHeader": "Elimina intestazione", - "settings.DeleteCloudBackupSuccess": "Eliminato con successo", - "settings.DeleteCloudBackupFailure": "Eliminazione fallita", - "settings.DebugMenuDescription": "Utilità di debug e sviluppo", - "settings.DebugMenu": "Menu di debug", - "settings.DecryptBackupHint1": "Inserisci la chiave di crittografia per questo backup.", - "settings.DecryptBackup": "Decifra il backup", - "settings.DefaultPage": "Pagina predefinita", - "settings.CustomHeadersDescription": "Aggiungi intestazioni personalizzate alle richieste", - "settings.CustomHeaders": "Intestazioni personalizzate", - "settings.CustomHeader": "Intestazione personalizzata", - "settings.Custom": "Personalizza…", - "settings.ConnectionTestFailed": "Test di connessione fallito", - "settings.ConnectionDetailsDescription": "Dettagli di connessione per {}", - "settings.ConnectionDetails": "Dettagli di connessione", - "settings.ConnectedSuccessfullyMessage": "{} è pronto all'uso con LunaSea!", - "settings.ConnectedSuccessfully": "Collegato con successo", - "settings.ConfigurationDescription": "Configurare e impostare LunaSea", - "settings.Configuration": "Configurazione", - "settings.ConfigureModule": "Configura {}", - "settings.ClearLogsHint1": "Sei sicuro/a di voler cancellare tutti i registri registrati?\n\nI registri possono essere utili per le segnalazioni di errore e il debug.", - "settings.ClearLogs": "Cancella i registri", - "settings.ClearConfigurationHint3": "Se sei iscritto/a a un account LunaSea, sarai disconnesso/a.", - "settings.ClearConfigurationHint2": "Partirai da una tabula rasa, assicurati di fare prima un backup della tua configurazione attuale!", - "settings.ClearConfigurationHint1": "Sei sicuro/a di voler cancellare la tua configurazione?", - "settings.ClearConfiguration": "Cancella configurazione", - "settings.BroadcastAddressValidation": "Indirizzo di trasmissione non valido", - "settings.BroadcastAddressHint3": "Dato un indirizzo IP macchina di esempio di 192.168.1.111, l'indirizzo IP broadcast risultante è 192.168.1.255", - "settings.BroadcastAddressHint2": "Di solito questo è l'indirizzo IP della tua macchina con l'ultimo ottetto impostato su 255", - "settings.BroadcastAddressHint1": "Questo è l'indirizzo di trasmissione della vostra rete locale", - "settings.BroadcastAddress": "Indirizzo di trasmissione", - "settings.BasicAuthenticationHint3": "Il nome utente e la password sono automaticamente convertiti in codifica base64", - "settings.BasicAuthenticationHint2": "La password può contenere due punti", - "settings.BasicAuthenticationHint1": "Il nome utente non deve contenere due punti", - "settings.BasicAuthentication": "Autenticazione di base", - "settings.BackupToCloudDescription": "Backup dei dati di configurazione", - "settings.AmoledThemeDescription": "Tema scuro nero puro", - "settings.InvalidEmailMessage": "L'indirizzo e-mail non è valido", - "settings.InvalidEmail": "E-mail non valida", - "settings.EmailSentSuccessMessage": "Un'e-mail per reimpostare la tua password è stata inviata!", - "settings.EmailSentSuccess": "E-mail inviata", - "settings.EmailSentFailure": "Impossibile reimpostare la password", - "settings.Email": "E-mail", - "settings.BannersNotificationModuleSupportHeader": "Moduli supportati", - "settings.BackupToCloudSuccess": "Backup effettuato con successo", - "settings.BackupToCloudFailure": "Impossibile eseguire il backup", - "settings.BackupList": "Elenco di backup", - "settings.BackupConfigurationHint2": "La chiave di crittografia deve essere di almeno 8 caratteri", - "settings.BackupConfigurationHint1": "Tutti i backup sono criptati prima di essere esportati", - "settings.BackupConfiguration": "Configurazione di backup", - "settings.BackgroundImageOpacity": "Opacità dell'immagine di sfondo", - "settings.AutomaticallyManageOrderDescription": "Elenca i moduli in ordine alfabetico", - "settings.AutomaticallyManageOrder": "Gestisci automaticamente l'ordine", - "settings.AppearanceDescription": "Personalizza l'aspetto", - "settings.Appearance": "Aspetto", - "settings.ApiKeyRequiredMessage": "La chiave API è necessaria per connettersi a {}", - "settings.ApiKeyRequired": "Chiave API richiesta", - "settings.ApiKey": "Chiave API", - "settings.AmoledTheme": "Tema AMOLED", - "settings.AllFieldsAreRequired": "Tutti i campi sono obbligatori", - "settings.AddProfile": "Aggiungi profilo", - "settings.AddModuleSuccess": "Modulo aggiunto", - "settings.AddModuleFailed": "Impossibile aggiungere il modulo", - "settings.AddModule": "Aggiungi modulo", - "settings.AddHeader": "Aggiungi intestazione", - "settings.AccountHelpHint1": "LunaSea offre un account gratuito per il backup della tua configurazione nel cloud, con ulteriori funzionalità in arrivo in futuro!", - "settings.AccountHelp": "Account LunaSea", + "dashboard.Modules": "Moduli", + "dashboard.MinimumOfOneDay": "Minimo 1 giorno", + "dashboard.FutureDaysDescription": "Imposta il numero di giorni nel futuro per cui recuperare le voci del calendario.", + "dashboard.Wednesday": "Mercoledì", + "dashboard.TwoWeeks": "Due settimane", + "dashboard.Tuesday": "Martedì", + "dashboard.Thursday": "Giovedì", + "dashboard.Sunday": "Domenica", + "dashboard.Schedule": "Programma", + "dashboard.Saturday": "Sabato", + "dashboard.PastDaysDescription": "Imposta il numero di giorni passati per i quali recuperare le voci del calendario.", + "dashboard.PastDays": "Giorni passati", + "dashboard.OneWeek": "Una settimana", + "dashboard.OneMonth": "Un mese", + "dashboard.NoNewContent": "Nessun nuovo contenuto", + "dashboard.Monday": "Lunedì", + "dashboard.Friday": "Venerdì", + "dashboard.FutureDays": "Giorni futuri", + "dashboard.Calendar": "Calendario", "lunasea.AnErrorHasOccurred": "C'è stato un errore", "lunasea.Cancel": "Annulla", "lunasea.ChangeProfiles": "Cambia profili", @@ -214,25 +152,87 @@ "search.Alphabetical": "Alfabetico", "search.AllSubcategories": "Tutte Le Sottocategorie", "search.Age": "Età", - "dashboard.Modules": "Moduli", - "dashboard.MinimumOfOneDay": "Minimo 1 giorno", - "dashboard.FutureDaysDescription": "Imposta il numero di giorni nel futuro per cui recuperare le voci del calendario.", - "dashboard.Wednesday": "Mercoledì", - "dashboard.TwoWeeks": "Due settimane", - "dashboard.Tuesday": "Martedì", - "dashboard.Thursday": "Giovedì", - "dashboard.Sunday": "Domenica", - "dashboard.Schedule": "Programma", - "dashboard.Saturday": "Sabato", - "dashboard.PastDaysDescription": "Imposta il numero di giorni passati per i quali recuperare le voci del calendario.", - "dashboard.PastDays": "Giorni passati", - "dashboard.OneWeek": "Una settimana", - "dashboard.OneMonth": "Un mese", - "dashboard.NoNewContent": "Nessun nuovo contenuto", - "dashboard.Monday": "Lunedì", - "dashboard.Friday": "Venerdì", - "dashboard.FutureDays": "Giorni futuri", - "dashboard.Calendar": "Calendario", + "settings.AccountDescription": "Il tuo account LunaSea", + "settings.Account": "Account", + "settings.EditModule": "Modifica modulo", + "settings.DonationsDescription": "Dona allo sviluppatore", + "settings.Donations": "Donazioni", + "settings.DisplayName": "Nome da mostrare", + "settings.DismissBannersHint1": "Sei sicuro/a di voler eliminare tutti i banner tooltip?", + "settings.DeleteProfile": "Elimina il profilo", + "settings.DeleteModuleSuccess": "Modulo eliminato", + "settings.DeleteModuleHint1": "Sei sicuro/a di voler eliminare questo modulo esterno?", + "settings.DeleteModule": "Elimina il modulo", + "settings.DeleteIndexerHint1": "Sei sicuro/a di voler eliminare questo indicizzatore?", + "settings.DeleteIndexer": "Elimina l'indicizzatore", + "settings.DeleteHeaderHint1": "Sei sicuro/a di voler eliminare questa intestazione?", + "settings.DeleteHeader": "Elimina intestazione", + "settings.DeleteCloudBackupSuccess": "Eliminato con successo", + "settings.DeleteCloudBackupFailure": "Eliminazione fallita", + "settings.DebugMenuDescription": "Utilità di debug e sviluppo", + "settings.DebugMenu": "Menu di debug", + "settings.DecryptBackupHint1": "Inserisci la chiave di crittografia per questo backup.", + "settings.DecryptBackup": "Decifra il backup", + "settings.DefaultPage": "Pagina predefinita", + "settings.CustomHeadersDescription": "Aggiungi intestazioni personalizzate alle richieste", + "settings.CustomHeaders": "Intestazioni personalizzate", + "settings.CustomHeader": "Intestazione personalizzata", + "settings.Custom": "Personalizza…", + "settings.ConnectionTestFailed": "Test di connessione fallito", + "settings.ConnectionDetailsDescription": "Dettagli di connessione per {}", + "settings.ConnectionDetails": "Dettagli di connessione", + "settings.ConnectedSuccessfullyMessage": "{} è pronto all'uso con LunaSea!", + "settings.ConnectedSuccessfully": "Collegato con successo", + "settings.ConfigurationDescription": "Configurare e impostare LunaSea", + "settings.Configuration": "Configurazione", + "settings.ConfigureModule": "Configura {}", + "settings.ClearLogsHint1": "Sei sicuro/a di voler cancellare tutti i registri registrati?\n\nI registri possono essere utili per le segnalazioni di errore e il debug.", + "settings.ClearLogs": "Cancella i registri", + "settings.ClearConfigurationHint3": "Se sei iscritto/a a un account LunaSea, sarai disconnesso/a.", + "settings.ClearConfigurationHint2": "Partirai da una tabula rasa, assicurati di fare prima un backup della tua configurazione attuale!", + "settings.ClearConfigurationHint1": "Sei sicuro/a di voler cancellare la tua configurazione?", + "settings.ClearConfiguration": "Cancella configurazione", + "settings.BroadcastAddressValidation": "Indirizzo di trasmissione non valido", + "settings.BroadcastAddressHint3": "Dato un indirizzo IP macchina di esempio di 192.168.1.111, l'indirizzo IP broadcast risultante è 192.168.1.255", + "settings.BroadcastAddressHint2": "Di solito questo è l'indirizzo IP della tua macchina con l'ultimo ottetto impostato su 255", + "settings.BroadcastAddressHint1": "Questo è l'indirizzo di trasmissione della vostra rete locale", + "settings.BroadcastAddress": "Indirizzo di trasmissione", + "settings.BasicAuthenticationHint3": "Il nome utente e la password sono automaticamente convertiti in codifica base64", + "settings.BasicAuthenticationHint2": "La password può contenere due punti", + "settings.BasicAuthenticationHint1": "Il nome utente non deve contenere due punti", + "settings.BasicAuthentication": "Autenticazione di base", + "settings.BackupToCloudDescription": "Backup dei dati di configurazione", + "settings.AmoledThemeDescription": "Tema scuro nero puro", + "settings.InvalidEmailMessage": "L'indirizzo e-mail non è valido", + "settings.InvalidEmail": "E-mail non valida", + "settings.EmailSentSuccessMessage": "Un'e-mail per reimpostare la tua password è stata inviata!", + "settings.EmailSentSuccess": "E-mail inviata", + "settings.EmailSentFailure": "Impossibile reimpostare la password", + "settings.Email": "E-mail", + "settings.BannersNotificationModuleSupportHeader": "Moduli supportati", + "settings.BackupToCloudSuccess": "Backup effettuato con successo", + "settings.BackupToCloudFailure": "Impossibile eseguire il backup", + "settings.BackupList": "Elenco di backup", + "settings.BackupConfigurationHint2": "La chiave di crittografia deve essere di almeno 8 caratteri", + "settings.BackupConfigurationHint1": "Tutti i backup sono criptati prima di essere esportati", + "settings.BackupConfiguration": "Configurazione di backup", + "settings.BackgroundImageOpacity": "Opacità dell'immagine di sfondo", + "settings.AutomaticallyManageOrderDescription": "Elenca i moduli in ordine alfabetico", + "settings.AutomaticallyManageOrder": "Gestisci automaticamente l'ordine", + "settings.AppearanceDescription": "Personalizza l'aspetto", + "settings.Appearance": "Aspetto", + "settings.ApiKeyRequiredMessage": "La chiave API è necessaria per connettersi a {}", + "settings.ApiKeyRequired": "Chiave API richiesta", + "settings.ApiKey": "Chiave API", + "settings.AmoledTheme": "Tema AMOLED", + "settings.AllFieldsAreRequired": "Tutti i campi sono obbligatori", + "settings.AddProfile": "Aggiungi profilo", + "settings.AddModuleSuccess": "Modulo aggiunto", + "settings.AddModuleFailed": "Impossibile aggiungere il modulo", + "settings.AddModule": "Aggiungi modulo", + "settings.AddHeader": "Aggiungi intestazione", + "settings.AccountHelpHint1": "LunaSea offre un account gratuito per il backup della tua configurazione nel cloud, con ulteriori funzionalità in arrivo in futuro!", + "settings.AccountHelp": "Account LunaSea", "tautulli.Bandwidth": "Larghezza di banda", "tautulli.BackingUpDatabase": "Backup della banca dati…", "tautulli.BackingUpConfiguration": "Backup della configurazione…", diff --git a/assets/localization/nb-NO.json b/assets/localization/nb-NO.json index 5ef08ad6a0..7047dfb6d9 100644 --- a/assets/localization/nb-NO.json +++ b/assets/localization/nb-NO.json @@ -1,105 +1,23 @@ { - "settings.AccountDeleted": "Konto slettet", - "settings.AccountDeletedMessage": "Slettet din LunaSea-konto", - "settings.AccountSettings": "Kontoinnstillinger", - "settings.Username": "Brukernavn", - "settings.DeleteAccountDescription": "Slett kontoen din for godt", - "settings.DeleteAccount": "Slett konto", - "settings.DeleteAccountWarning1": "Denne handlingen kan ikke angres", - "settings.DeleteCloudBackupFailure": "Klarte ikke å utføre sletting", - "settings.DeleteHeader": "Slett hode", - "settings.DeleteIndexer": "Slett indeksator", - "settings.DeleteModule": "Slett modul", - "settings.DeleteModuleHint1": "Slett denne eksterne modulen?", - "settings.DeleteModuleSuccess": "Modul slettet", - "settings.DeleteProfileDescription": "Slett en eksisterende profil", - "settings.DisplayName": "Visningsnavn", - "settings.DonationsDescription": "Send penger til utvikleren", - "settings.DrawerDescription": "Tilpass skuffen", - "settings.EnabledProfile": "Påskrudd profil", - "settings.ForgotYourPassword": "Glemt passordet?", - "settings.HeaderValueValidation": "Hode-verdi påkrevd", - "settings.HostRequired": "Vert påkrevd", - "settings.InvalidEmail": "Ugyldig e-postadresse", - "settings.InvalidEmailMessage": "E-postadressen er ugyldig", - "settings.InvalidPassword": "Ugyldig passord", - "settings.InvalidPasswordMessage": "Passordet er ugyldig", - "settings.Language": "Språk", - "settings.Localization": "Lokalisering", - "settings.MACAddress": "MAC-adresse", - "settings.MACAddressValidation": "Ugyldig MAC-adresse", - "settings.MinimumCharacters": "Minst {} tegn", - "settings.ModuleNotFound": "Fant ikke modulen", - "settings.ResetPassword": "Tilbakestill passord", - "settings.MustBeValueBetween": "Må være en verdi mellom {} og {}", - "settings.UsernameValidation": "Brukernavn påkrevd", - "settings.AccountHelp": "LunaSea-konto", - "settings.BasicAuthenticationHint2": "Passordet kan inneholde kolon", - "settings.DeleteProfile": "Slett profil", - "settings.DeleteIndexerHint1": "Slett denne indeksatoren?", - "settings.Host": "Vert", - "settings.Network": "Nettverk", - "settings.NetworkDescription": "Tilpass nettverksfunksjoner", - "settings.RegisteredFailure": "Klarte ikke å registrere", - "settings.Register": "Registrer", - "settings.RegisteredSuccess": "Registrert", - "settings.RenameProfile": "Bytt navn på profil", - "settings.RenameProfileDescription": "Endre navn på eksisterende profil", - "settings.StartingDay": "Startdato", - "settings.StartingType": "Starttype", - "settings.StartingView": "Startvisning", - "settings.DeleteCloudBackup": "Slett sky-sikkerhetskopi", - "settings.DeleteCloudBackupDescription": "Slett en oppsettsfil", - "settings.EmailSentFailure": "Klarte ikke å tilbakestille passord", - "settings.EmailSentSuccess": "E-post sendt", - "settings.EncryptionKey": "Krypteringsnøkkel", - "settings.HostHint3": "Ikke bruk localhost eller 127.0.0.1", - "settings.Notifications": "Merknader", - "settings.BasicAuthenticationHint1": "Brukernavnet kan ikke inneholde kolon", - "settings.Account": "Konto", - "settings.AccountDescription": "Din LunaSea-konto", - "settings.BroadcastAddress": "Kringkastingsadresse", - "settings.Configuration": "Oppsett", - "settings.HeaderDeleted": "Hode slettet", - "settings.HeaderValue": "Hode-verdi", - "settings.QuickActions": "Hurtighandlinger", - "settings.SignIn": "Logg inn", - "settings.SignOut": "Logg ut", - "settings.SignOutHint1": "Logg ut av din LunaSea-konto?", - "settings.ShowCalendarEntries": "Vis {} kalenderoppføringer", - "settings.StartingSize": "Startstørrelse", - "settings.System": "System", - "settings.TestConnection": "Test tilkobling", - "settings.TLSCertificateValidation": "Bekreftelse av TLS-sertifikat", - "settings.SystemDescription": "Systemverktøy og info", - "settings.ViewRecentChanges": "Vis nylige endringer", - "settings.Add": "Legg til", - "settings.AddModule": "Legg til modul", - "settings.AddModuleFailed": "Klarte ikke å legge til modul", - "settings.AddModuleSuccess": "Modul tillagt", - "settings.AddProfile": "Legg til profil", - "settings.AddProfileDescription": "Legg til en ny profil", - "settings.AllFieldsAreRequired": "Alle felter må fylles ut", - "settings.AmoledTheme": "AMOLED-drakt", - "settings.AmoledThemeBorders": "AMOLED-draktkanter", - "settings.BroadcastAddressHint1": "Dette er kringkastingsadressen for ditt lokalnettverk", - "settings.BroadcastAddressValidation": "Ugyldig kringkastingsadresse", - "settings.ClearConfiguration": "Tøm oppsett", - "settings.ClearLogs": "Tøm loggføring", - "settings.ConfigureModule": "Sett opp {}", - "settings.DeleteHeaderHint1": "Slett dette hodet?", - "settings.Drawer": "Skuff", - "settings.Resources": "Ressurser", - "settings.ResourcesDescription": "Nyttige ressurser og lenker", - "settings.Donations": "Donasjoner", - "settings.ConfigurationDescription": "Sett opp LunaSea", - "settings.FailedToDeleteAccount": "Klarte ikke å slette konto", - "settings.HeaderAdded": "Hode lagt til", - "settings.ConnectedSuccessfully": "Tilkoblet", - "settings.EditModule": "Rediger modul", - "settings.HeaderKey": "Hode-nøkkel", - "settings.HeaderKeyValidation": "Hodenøkkel påkrevd", - "settings.Email": "E-post", + "dashboard.NoNewContent": "Inget nytt innhold", + "dashboard.OneMonth": "Én måned", + "dashboard.OneWeek": "Én uke", + "dashboard.PastDays": "Tidligere dager", + "dashboard.PastDaysDescription": "Sett antall dager i fortiden å hente kalenderoppføringer for.", + "dashboard.Saturday": "Lørdag", + "dashboard.Sunday": "Søndag", + "dashboard.Schedule": "Program", + "dashboard.Tuesday": "Tirsdag", + "dashboard.TwoWeeks": "To uker", + "dashboard.Calendar": "Kalender", + "dashboard.Friday": "Fredag", + "dashboard.FutureDays": "Fremtidige dager", + "dashboard.MinimumOfOneDay": "Maks. én dag", + "dashboard.FutureDaysDescription": "Sett antall dager i fremtiden å hente kalenderoppføringer for.", + "dashboard.Modules": "Moduler", + "dashboard.Monday": "Mandag", + "dashboard.Wednesday": "Onsdag", + "dashboard.Thursday": "Torsdag", "lunasea.Add": "Legg til", "lunasea.Alpha": "Alfa", "lunasea.AnErrorHasOccurred": "En feil har inntruffet", @@ -257,25 +175,107 @@ "search.Alphabetical": "Alfabetisk", "search.AllSubcategories": "Alle underkategorier", "search.Age": "Alder", - "dashboard.NoNewContent": "Inget nytt innhold", - "dashboard.OneMonth": "Én måned", - "dashboard.OneWeek": "Én uke", - "dashboard.PastDays": "Tidligere dager", - "dashboard.PastDaysDescription": "Sett antall dager i fortiden å hente kalenderoppføringer for.", - "dashboard.Saturday": "Lørdag", - "dashboard.Sunday": "Søndag", - "dashboard.Schedule": "Program", - "dashboard.Tuesday": "Tirsdag", - "dashboard.TwoWeeks": "To uker", - "dashboard.Calendar": "Kalender", - "dashboard.Friday": "Fredag", - "dashboard.FutureDays": "Fremtidige dager", - "dashboard.MinimumOfOneDay": "Maks. én dag", - "dashboard.FutureDaysDescription": "Sett antall dager i fremtiden å hente kalenderoppføringer for.", - "dashboard.Modules": "Moduler", - "dashboard.Monday": "Mandag", - "dashboard.Wednesday": "Onsdag", - "dashboard.Thursday": "Torsdag", + "settings.AccountDeleted": "Konto slettet", + "settings.AccountDeletedMessage": "Slettet din LunaSea-konto", + "settings.AccountSettings": "Kontoinnstillinger", + "settings.Username": "Brukernavn", + "settings.DeleteAccountDescription": "Slett kontoen din for godt", + "settings.DeleteAccount": "Slett konto", + "settings.DeleteAccountWarning1": "Denne handlingen kan ikke angres", + "settings.DeleteCloudBackupFailure": "Klarte ikke å utføre sletting", + "settings.DeleteHeader": "Slett hode", + "settings.DeleteIndexer": "Slett indeksator", + "settings.DeleteModule": "Slett modul", + "settings.DeleteModuleHint1": "Slett denne eksterne modulen?", + "settings.DeleteModuleSuccess": "Modul slettet", + "settings.DeleteProfileDescription": "Slett en eksisterende profil", + "settings.DisplayName": "Visningsnavn", + "settings.DonationsDescription": "Send penger til utvikleren", + "settings.DrawerDescription": "Tilpass skuffen", + "settings.EnabledProfile": "Påskrudd profil", + "settings.ForgotYourPassword": "Glemt passordet?", + "settings.HeaderValueValidation": "Hode-verdi påkrevd", + "settings.HostRequired": "Vert påkrevd", + "settings.InvalidEmail": "Ugyldig e-postadresse", + "settings.InvalidEmailMessage": "E-postadressen er ugyldig", + "settings.InvalidPassword": "Ugyldig passord", + "settings.InvalidPasswordMessage": "Passordet er ugyldig", + "settings.Language": "Språk", + "settings.Localization": "Lokalisering", + "settings.MACAddress": "MAC-adresse", + "settings.MACAddressValidation": "Ugyldig MAC-adresse", + "settings.MinimumCharacters": "Minst {} tegn", + "settings.ModuleNotFound": "Fant ikke modulen", + "settings.ResetPassword": "Tilbakestill passord", + "settings.MustBeValueBetween": "Må være en verdi mellom {} og {}", + "settings.UsernameValidation": "Brukernavn påkrevd", + "settings.AccountHelp": "LunaSea-konto", + "settings.BasicAuthenticationHint2": "Passordet kan inneholde kolon", + "settings.DeleteProfile": "Slett profil", + "settings.DeleteIndexerHint1": "Slett denne indeksatoren?", + "settings.Host": "Vert", + "settings.Network": "Nettverk", + "settings.NetworkDescription": "Tilpass nettverksfunksjoner", + "settings.RegisteredFailure": "Klarte ikke å registrere", + "settings.Register": "Registrer", + "settings.RegisteredSuccess": "Registrert", + "settings.RenameProfile": "Bytt navn på profil", + "settings.RenameProfileDescription": "Endre navn på eksisterende profil", + "settings.StartingDay": "Startdato", + "settings.StartingType": "Starttype", + "settings.StartingView": "Startvisning", + "settings.DeleteCloudBackup": "Slett sky-sikkerhetskopi", + "settings.DeleteCloudBackupDescription": "Slett en oppsettsfil", + "settings.EmailSentFailure": "Klarte ikke å tilbakestille passord", + "settings.EmailSentSuccess": "E-post sendt", + "settings.EncryptionKey": "Krypteringsnøkkel", + "settings.HostHint3": "Ikke bruk localhost eller 127.0.0.1", + "settings.Notifications": "Merknader", + "settings.BasicAuthenticationHint1": "Brukernavnet kan ikke inneholde kolon", + "settings.Account": "Konto", + "settings.AccountDescription": "Din LunaSea-konto", + "settings.BroadcastAddress": "Kringkastingsadresse", + "settings.Configuration": "Oppsett", + "settings.HeaderDeleted": "Hode slettet", + "settings.HeaderValue": "Hode-verdi", + "settings.QuickActions": "Hurtighandlinger", + "settings.SignIn": "Logg inn", + "settings.SignOut": "Logg ut", + "settings.SignOutHint1": "Logg ut av din LunaSea-konto?", + "settings.ShowCalendarEntries": "Vis {} kalenderoppføringer", + "settings.StartingSize": "Startstørrelse", + "settings.System": "System", + "settings.TestConnection": "Test tilkobling", + "settings.TLSCertificateValidation": "Bekreftelse av TLS-sertifikat", + "settings.SystemDescription": "Systemverktøy og info", + "settings.ViewRecentChanges": "Vis nylige endringer", + "settings.Add": "Legg til", + "settings.AddModule": "Legg til modul", + "settings.AddModuleFailed": "Klarte ikke å legge til modul", + "settings.AddModuleSuccess": "Modul tillagt", + "settings.AddProfile": "Legg til profil", + "settings.AddProfileDescription": "Legg til en ny profil", + "settings.AllFieldsAreRequired": "Alle felter må fylles ut", + "settings.AmoledTheme": "AMOLED-drakt", + "settings.AmoledThemeBorders": "AMOLED-draktkanter", + "settings.BroadcastAddressHint1": "Dette er kringkastingsadressen for ditt lokalnettverk", + "settings.BroadcastAddressValidation": "Ugyldig kringkastingsadresse", + "settings.ClearConfiguration": "Tøm oppsett", + "settings.ClearLogs": "Tøm loggføring", + "settings.ConfigureModule": "Sett opp {}", + "settings.DeleteHeaderHint1": "Slett dette hodet?", + "settings.Drawer": "Skuff", + "settings.Resources": "Ressurser", + "settings.ResourcesDescription": "Nyttige ressurser og lenker", + "settings.Donations": "Donasjoner", + "settings.ConfigurationDescription": "Sett opp LunaSea", + "settings.FailedToDeleteAccount": "Klarte ikke å slette konto", + "settings.HeaderAdded": "Hode lagt til", + "settings.ConnectedSuccessfully": "Tilkoblet", + "settings.EditModule": "Rediger modul", + "settings.HeaderKey": "Hode-nøkkel", + "settings.HeaderKeyValidation": "Hodenøkkel påkrevd", + "settings.Email": "E-post", "tautulli.ETA": "Estimert tid til ankomst", "tautulli.Throttled": "Strupet", "tautulli.TerminationAttachMessage": "Du kan alternativt legge ved en avsluttingsmelding nedenfor.", diff --git a/assets/localization/nl.json b/assets/localization/nl.json index a46ddbc816..0259c49f77 100644 --- a/assets/localization/nl.json +++ b/assets/localization/nl.json @@ -1,44 +1,23 @@ { - "settings.AddModuleFailed": "Module Toevoegen Gefaald", - "settings.AccountDescription": "Uw LunaSea Account", - "settings.AccountHelp": "LunaSea Account", - "settings.AddModule": "Module Toevoegen", - "settings.AddModuleSuccess": "Module Toegevoegd", - "settings.AddProfile": "Profiel Toevoegen", - "settings.Account": "Account", - "settings.AccountHelpHint1": "LunaSea biedt een gratis account aan om van uw instellingen een back-up te maken in de Cloud, met extra functies komend in de toekomst!", - "settings.AddHeader": "Header Toevoegen", - "settings.AllFieldsAreRequired": "Alle velden zijn vereist", - "settings.AmoledTheme": "AMOLED Thema", - "settings.AmoledThemeBorders": "AMOLED Thema Randen", - "settings.BackupConfigurationHint2": "De encryptie sleutel moet minimaal 8 karakters zijn", - "settings.AmoledThemeBordersDescription": "Subtiele Randen Aan Het UI Toevoegen", - "settings.AmoledThemeDescription": "Puur Zwart Donker Thema", - "settings.ApiKey": "API-sleutel", - "settings.ApiKeyRequired": "API-sleutel Vereist", - "settings.ApiKeyRequiredMessage": "API-sleutel is vereist om te verbinden met {}", - "settings.Appearance": "Uiterlijk", - "settings.AppearanceDescription": "Look & Feel Aanpassen", - "settings.AutomaticallyManageOrderDescription": "Modulen Alfabetisch Weergeven", - "settings.AutomaticallyManageOrder": "Automatisch Sorteren", - "settings.BackgroundImageOpacity": "Doorzichtigheid Achtergrondafbeelding", - "settings.BackupConfiguration": "Back-up Configuratie", - "settings.BackupConfigurationHint1": "Alle back-ups zijn versleuteld voordat ze worden geëxporteerd", - "settings.BackupList": "Back-up Lijst", - "settings.BackupToCloud": "Back-up naar Cloud", - "settings.AccountDeletedMessage": "Uw LunaSea account is met succes verwijderd", - "settings.AccountSettings": "Account instellingen", - "settings.AccountDeleted": "Account verwijderd", - "settings.ClearConfigurationHint1": "Weet je zeker dat je de configuratie wilt verwijderen?", - "settings.ClearConfiguration": "Configuratie wissen", - "settings.BackupToCloudDescription": "Configuratie data opslaan", - "settings.BackupToCloudFailure": "Backup mislukt", - "settings.BackupToCloudSuccess": "Backup geslaagd", - "settings.BannersNotificationModuleSupportHeader": "Ondersteunde modules", - "settings.BannersNotificationModuleSupportBody": "Notificaties gebaseerd op webhooks worden alleen ondersteund door onderstaande modules\n\nOndersteuning voor meer modules volgen in de toekomst!", - "settings.BasicAuthenticationHint1": "Gebruikersnaam mag geen dubbele punt bevatten", - "settings.BasicAuthenticationHint2": "Wachtwoord mag wel een dubbele punt bevatten", - "settings.BasicAuthenticationHint3": "Gebruikersnaam en wachtwoord worden automatisch geconverteerd naar base64 codering", + "dashboard.FutureDays": "Toekomstige Dagen", + "dashboard.FutureDaysDescription": "Stel het aantal dagen in de toekomst in om kalendergegevens voor op te halen.", + "dashboard.MinimumOfOneDay": "Minimum van 1 Dag", + "dashboard.Modules": "Modulen", + "dashboard.Monday": "Maandag", + "dashboard.NoNewContent": "Geen Nieuwe Inhoud", + "dashboard.PastDays": "Afgelopen Dagen", + "dashboard.PastDaysDescription": "Stel het aantal dagen in het verleden in om kalendergegevens voor op te halen.", + "dashboard.Saturday": "Zaterdag", + "dashboard.Schedule": "Planning", + "dashboard.Sunday": "Zondag", + "dashboard.Thursday": "Donderdag", + "dashboard.Tuesday": "Dinsdag", + "dashboard.TwoWeeks": "Twee Weken", + "dashboard.Wednesday": "Woensdag", + "dashboard.Calendar": "Kalender", + "dashboard.Friday": "Vrijdag", + "dashboard.OneMonth": "Een Maand", + "dashboard.OneWeek": "Een Week", "lunasea.Add": "Voeg Toe", "lunasea.AnErrorHasOccurred": "Een Fout Is Opgetreden", "lunasea.BackUp": "Back-up", @@ -79,6 +58,14 @@ "lunasea.UnknownModule": "Onbekende Module", "lunasea.Update": "Update", "lunasea.Website": "Website", + "overseerr.NoRequests": "Geen Verzoeken", + "overseerr.OneRequest": "1 Verzoek", + "overseerr.Requests": "Verzoeken", + "overseerr.NoRequestsFound": "Geen Verzoeken Gevonden", + "overseerr.NoUsersFound": "Geen Gebruikers Gevonden", + "overseerr.UnknownUser": "Onbekende Gebruiker", + "overseerr.Users": "Gebruikers", + "overseerr.SomeRequests": "{} Verzoeken", "radarr.AddMovie": "Film Toevoegen", "radarr.AddMovieAndSearch": "Voeg Toe + Zoek", "radarr.AddedTag": "Label Toegevoegd", @@ -205,45 +192,46 @@ "search.SentTo": "Verzonden naar {}", "search.Size": "Grootte", "search.Subcategories": "Subcategorieën", - "dashboard.FutureDays": "Toekomstige Dagen", - "dashboard.FutureDaysDescription": "Stel het aantal dagen in de toekomst in om kalendergegevens voor op te halen.", - "dashboard.MinimumOfOneDay": "Minimum van 1 Dag", - "dashboard.Modules": "Modulen", - "dashboard.Monday": "Maandag", - "dashboard.NoNewContent": "Geen Nieuwe Inhoud", - "dashboard.PastDays": "Afgelopen Dagen", - "dashboard.PastDaysDescription": "Stel het aantal dagen in het verleden in om kalendergegevens voor op te halen.", - "dashboard.Saturday": "Zaterdag", - "dashboard.Schedule": "Planning", - "dashboard.Sunday": "Zondag", - "dashboard.Thursday": "Donderdag", - "dashboard.Tuesday": "Dinsdag", - "dashboard.TwoWeeks": "Twee Weken", - "dashboard.Wednesday": "Woensdag", - "dashboard.Calendar": "Kalender", - "dashboard.Friday": "Vrijdag", - "dashboard.OneMonth": "Een Maand", - "dashboard.OneWeek": "Een Week", - "tautulli.Activity": "Activiteit", - "tautulli.ActivityDetails": "Activiteit Details", - "tautulli.Audio": "Geluid", - "tautulli.BackingUpDatabase": "Back-up maken van Database…", - "tautulli.BackingUpDatabaseDescription": "Back-up maken van database op de achtergrond", - "tautulli.BackingUpDatabaseFailed": "Back-up Database Gefaald", - "tautulli.BackupConfiguration": "Back-up Configuratie", - "tautulli.BackingUpConfiguration": "Back-up maken van Configuratie…", - "tautulli.BackingUpConfigurationDescription": "Back-up maken van configuratie op de achtergrond", - "tautulli.BackingUpConfigurationFailed": "Back-up Configuratie Gefaald", - "tautulli.BackupDatabase": "Back-up Database", - "tautulli.Bandwidth": "Bandbreedte", - "overseerr.NoRequests": "Geen Verzoeken", - "overseerr.OneRequest": "1 Verzoek", - "overseerr.Requests": "Verzoeken", - "overseerr.NoRequestsFound": "Geen Verzoeken Gevonden", - "overseerr.NoUsersFound": "Geen Gebruikers Gevonden", - "overseerr.UnknownUser": "Onbekende Gebruiker", - "overseerr.Users": "Gebruikers", - "overseerr.SomeRequests": "{} Verzoeken", + "settings.AddModuleFailed": "Module Toevoegen Gefaald", + "settings.AccountDescription": "Uw LunaSea Account", + "settings.AccountHelp": "LunaSea Account", + "settings.AddModule": "Module Toevoegen", + "settings.AddModuleSuccess": "Module Toegevoegd", + "settings.AddProfile": "Profiel Toevoegen", + "settings.Account": "Account", + "settings.AccountHelpHint1": "LunaSea biedt een gratis account aan om van uw instellingen een back-up te maken in de Cloud, met extra functies komend in de toekomst!", + "settings.AddHeader": "Header Toevoegen", + "settings.AllFieldsAreRequired": "Alle velden zijn vereist", + "settings.AmoledTheme": "AMOLED Thema", + "settings.AmoledThemeBorders": "AMOLED Thema Randen", + "settings.BackupConfigurationHint2": "De encryptie sleutel moet minimaal 8 karakters zijn", + "settings.AmoledThemeBordersDescription": "Subtiele Randen Aan Het UI Toevoegen", + "settings.AmoledThemeDescription": "Puur Zwart Donker Thema", + "settings.ApiKey": "API-sleutel", + "settings.ApiKeyRequired": "API-sleutel Vereist", + "settings.ApiKeyRequiredMessage": "API-sleutel is vereist om te verbinden met {}", + "settings.Appearance": "Uiterlijk", + "settings.AppearanceDescription": "Look & Feel Aanpassen", + "settings.AutomaticallyManageOrderDescription": "Modulen Alfabetisch Weergeven", + "settings.AutomaticallyManageOrder": "Automatisch Sorteren", + "settings.BackgroundImageOpacity": "Doorzichtigheid Achtergrondafbeelding", + "settings.BackupConfiguration": "Back-up Configuratie", + "settings.BackupConfigurationHint1": "Alle back-ups zijn versleuteld voordat ze worden geëxporteerd", + "settings.BackupList": "Back-up Lijst", + "settings.BackupToCloud": "Back-up naar Cloud", + "settings.AccountDeletedMessage": "Uw LunaSea account is met succes verwijderd", + "settings.AccountSettings": "Account instellingen", + "settings.AccountDeleted": "Account verwijderd", + "settings.ClearConfigurationHint1": "Weet je zeker dat je de configuratie wilt verwijderen?", + "settings.ClearConfiguration": "Configuratie wissen", + "settings.BackupToCloudDescription": "Configuratie data opslaan", + "settings.BackupToCloudFailure": "Backup mislukt", + "settings.BackupToCloudSuccess": "Backup geslaagd", + "settings.BannersNotificationModuleSupportHeader": "Ondersteunde modules", + "settings.BannersNotificationModuleSupportBody": "Notificaties gebaseerd op webhooks worden alleen ondersteund door onderstaande modules\n\nOndersteuning voor meer modules volgen in de toekomst!", + "settings.BasicAuthenticationHint1": "Gebruikersnaam mag geen dubbele punt bevatten", + "settings.BasicAuthenticationHint2": "Wachtwoord mag wel een dubbele punt bevatten", + "settings.BasicAuthenticationHint3": "Gebruikersnaam en wachtwoord worden automatisch geconverteerd naar base64 codering", "sonarr.AddedSeries": "Series toegevoegd", "sonarr.AddedOn": "Toegevoegd Op", "sonarr.AddedTag": "Label toegevoegd", @@ -451,5 +439,17 @@ "sonarr.UpdatedSeries": "Series ververst", "sonarr.Usenet": "Usenet", "sonarr.UseSeasonFolders": "Gebruik seizoen mappen", - "sonarr.WordScore": "Woord score" + "sonarr.WordScore": "Woord score", + "tautulli.Activity": "Activiteit", + "tautulli.ActivityDetails": "Activiteit Details", + "tautulli.Audio": "Geluid", + "tautulli.BackingUpDatabase": "Back-up maken van Database…", + "tautulli.BackingUpDatabaseDescription": "Back-up maken van database op de achtergrond", + "tautulli.BackingUpDatabaseFailed": "Back-up Database Gefaald", + "tautulli.BackupConfiguration": "Back-up Configuratie", + "tautulli.BackingUpConfiguration": "Back-up maken van Configuratie…", + "tautulli.BackingUpConfigurationDescription": "Back-up maken van configuratie op de achtergrond", + "tautulli.BackingUpConfigurationFailed": "Back-up Configuratie Gefaald", + "tautulli.BackupDatabase": "Back-up Database", + "tautulli.Bandwidth": "Bandbreedte" } \ No newline at end of file diff --git a/assets/localization/pt.json b/assets/localization/pt.json index 2669ddcddc..6e199656b4 100644 --- a/assets/localization/pt.json +++ b/assets/localization/pt.json @@ -1,4 +1,105 @@ { + "dashboard.PastDaysDescription": "Defina o número de dias passados para buscar registros do calendário.", + "dashboard.MinimumOfOneDay": "Mínimo de 1 dia", + "dashboard.FutureDaysDescription": "Defina o número de dias a frente para buscar registros do calendário.", + "dashboard.FutureDays": "Próximos Dias", + "dashboard.Wednesday": "Quarta-feira", + "dashboard.TwoWeeks": "Duas Semanas", + "dashboard.Tuesday": "Terça-feira", + "dashboard.Thursday": "Quinta-feira", + "dashboard.Sunday": "Domingo", + "dashboard.Schedule": "Cronograma", + "dashboard.Saturday": "Sábado", + "dashboard.PastDays": "Dias Anteriores", + "dashboard.OneWeek": "Uma Semana", + "dashboard.OneMonth": "Um Mês", + "dashboard.NoNewContent": "Sem Conteúdo Novo", + "dashboard.Monday": "Segunda-feira", + "dashboard.Modules": "Módulos", + "dashboard.Friday": "Sexta-feira", + "dashboard.Calendar": "Calendário", + "lunasea.Add": "Adicionar", + "lunasea.AnErrorHasOccurred": "Ocorreu um erro", + "lunasea.BackUp": "Back Up", + "lunasea.Cancel": "Cancelar", + "lunasea.ChangeProfiles": "Mudar de Perfil", + "lunasea.CheckLogsMessage": "Verifique os registos para mais detalhes", + "lunasea.Clear": "Limpar", + "lunasea.Close": "Fechar", + "lunasea.ComingSoon": "Em Breve", + "lunasea.Dashboard": "Painel de Controle", + "lunasea.Delete": "Eliminar", + "lunasea.Disable": "Desativar", + "lunasea.Disabled": "Desativado", + "lunasea.Dismiss": "Dispensar", + "lunasea.ExternalModules": "Módulos Externos", + "lunasea.GoBack": "Voltar", + "lunasea.GoToSettings": "Ir para as Definições", + "lunasea.IncorrectEncryptionKey": "Chave de encriptação incorreta", + "lunasea.Module": "Módulo", + "lunasea.ModuleIsNotEnabled": "{} Não Está Ativo", + "lunasea.NoModulesEnabled": "Nenhum Módulo Ativado", + "lunasea.NotSet": "Não Definido", + "lunasea.Options": "Opções", + "lunasea.Page": "Página", + "lunasea.Refresh": "Atualizar", + "lunasea.Refreshing": "Atualizando…", + "lunasea.Remove": "Remover", + "lunasea.Rename": "Renomear", + "lunasea.Restore": "Restaurar", + "lunasea.ReturnToDashboard": "Voltar ao painel de aplicações", + "lunasea.SearchTextBar": "Procurar…", + "lunasea.Set": "Definir", + "lunasea.Settings": "Definições", + "lunasea.TryAgain": "Tente novamente", + "lunasea.Unknown": "Desconhecido", + "lunasea.UnknownDate": "Data Desconhecida", + "lunasea.UnknownError": "Erro Desconhecido", + "lunasea.UnknownModule": "Módulo Desconhecido", + "lunasea.Update": "Atualizar", + "lunasea.Website": "Website", + "overseerr.NoRequestsFound": "Nenhum Pedido Encontrado", + "overseerr.Requests": "Pedidos", + "overseerr.NoUsersFound": "Nenhum Usuário Encontrado", + "overseerr.NoRequests": "Sem Pedidos", + "overseerr.OneRequest": "1 Pedido", + "overseerr.SomeRequests": "{} Pedidos", + "overseerr.UnknownUser": "Usuário Desconhecido", + "overseerr.Users": "Usuários", + "radarr.AddMovie": "Adicionar Filme", + "radarr.Age": "Idade", + "radarr.All": "Todos", + "radarr.Alphabetical": "Alfabética", + "radarr.Approved": "Aprovado", + "radarr.Automatic": "Automático", + "radarr.AvailabilityUnknown": "Disponibilidade Desconhecida", + "radarr.AvailableIn": "Disponível em {}", + "radarr.AvailableToday": "Disponível Hoje", + "radarr.CinemaDateUnknown": "Data nos Cinemas Desconhecida", + "radarr.Configure": "Configurar", + "radarr.Copy": "Copiar", + "radarr.CopyFull": "Cópia Física/Copiar Arquivos", + "search.Subcategories": "Subcategorias", + "search.Size": "Tamanho", + "search.SentTo": "Enviar para {}", + "search.SentNZBData": "Enviar dados NZB", + "search.Search": "Pesquisar", + "search.Results": "Resultados", + "search.NoSubcategoriesFound": "Nenhuma Subcategoria Encontrada", + "search.NoResultsFound": "Nenhum Resultado Encontrado", + "search.NoCategoriesFound": "Nenhuma Categoria Encontrada", + "search.FailedToSend": "Falha ao Enviar", + "search.FailedToDownloadNZB": "Falha ao Baixar NZB", + "search.DownloadToDevice": "Baixar para o Dispositivo", + "search.DownloadingNZBToDevice": "Baixar NZB para o seu dispositivo", + "search.Downloading": "Baixando…", + "search.Download": "Baixar", + "search.Comments": "Comentários", + "search.Categories": "Categorias", + "search.Category": "Categoria", + "search.Alphabetical": "Alfabético", + "search.AllSubcategories": "Todas Subcategorias", + "search.Age": "Idade", "settings.ClearConfigurationHint1": "Tem a certeza que quer limpar a sua configuração?", "settings.ClearConfiguration": "Limpar Configuração", "settings.BroadcastAddressValidation": "Endereço de Difusão Inválido", @@ -168,99 +269,53 @@ "settings.AmoledThemeDescription": "Tema escuro puramente preto", "settings.AmoledThemeBordersDescription": "Adicionar bordas sutis na interface", "settings.AmoledThemeBorders": "Bordas no Tema AMOLED", - "lunasea.Add": "Adicionar", - "lunasea.AnErrorHasOccurred": "Ocorreu um erro", - "lunasea.BackUp": "Back Up", - "lunasea.Cancel": "Cancelar", - "lunasea.ChangeProfiles": "Mudar de Perfil", - "lunasea.CheckLogsMessage": "Verifique os registos para mais detalhes", - "lunasea.Clear": "Limpar", - "lunasea.Close": "Fechar", - "lunasea.ComingSoon": "Em Breve", - "lunasea.Dashboard": "Painel de Controle", - "lunasea.Delete": "Eliminar", - "lunasea.Disable": "Desativar", - "lunasea.Disabled": "Desativado", - "lunasea.Dismiss": "Dispensar", - "lunasea.ExternalModules": "Módulos Externos", - "lunasea.GoBack": "Voltar", - "lunasea.GoToSettings": "Ir para as Definições", - "lunasea.IncorrectEncryptionKey": "Chave de encriptação incorreta", - "lunasea.Module": "Módulo", - "lunasea.ModuleIsNotEnabled": "{} Não Está Ativo", - "lunasea.NoModulesEnabled": "Nenhum Módulo Ativado", - "lunasea.NotSet": "Não Definido", - "lunasea.Options": "Opções", - "lunasea.Page": "Página", - "lunasea.Refresh": "Atualizar", - "lunasea.Refreshing": "Atualizando…", - "lunasea.Remove": "Remover", - "lunasea.Rename": "Renomear", - "lunasea.Restore": "Restaurar", - "lunasea.ReturnToDashboard": "Voltar ao painel de aplicações", - "lunasea.SearchTextBar": "Procurar…", - "lunasea.Set": "Definir", - "lunasea.Settings": "Definições", - "lunasea.TryAgain": "Tente novamente", - "lunasea.Unknown": "Desconhecido", - "lunasea.UnknownDate": "Data Desconhecida", - "lunasea.UnknownError": "Erro Desconhecido", - "lunasea.UnknownModule": "Módulo Desconhecido", - "lunasea.Update": "Atualizar", - "lunasea.Website": "Website", - "radarr.AddMovie": "Adicionar Filme", - "radarr.Age": "Idade", - "radarr.All": "Todos", - "radarr.Alphabetical": "Alfabética", - "radarr.Approved": "Aprovado", - "radarr.Automatic": "Automático", - "radarr.AvailabilityUnknown": "Disponibilidade Desconhecida", - "radarr.AvailableIn": "Disponível em {}", - "radarr.AvailableToday": "Disponível Hoje", - "radarr.CinemaDateUnknown": "Data nos Cinemas Desconhecida", - "radarr.Configure": "Configurar", - "radarr.Copy": "Copiar", - "radarr.CopyFull": "Cópia Física/Copiar Arquivos", - "search.Subcategories": "Subcategorias", - "search.Size": "Tamanho", - "search.SentTo": "Enviar para {}", - "search.SentNZBData": "Enviar dados NZB", - "search.Search": "Pesquisar", - "search.Results": "Resultados", - "search.NoSubcategoriesFound": "Nenhuma Subcategoria Encontrada", - "search.NoResultsFound": "Nenhum Resultado Encontrado", - "search.NoCategoriesFound": "Nenhuma Categoria Encontrada", - "search.FailedToSend": "Falha ao Enviar", - "search.FailedToDownloadNZB": "Falha ao Baixar NZB", - "search.DownloadToDevice": "Baixar para o Dispositivo", - "search.DownloadingNZBToDevice": "Baixar NZB para o seu dispositivo", - "search.Downloading": "Baixando…", - "search.Download": "Baixar", - "search.Comments": "Comentários", - "search.Categories": "Categorias", - "search.Category": "Categoria", - "search.Alphabetical": "Alfabético", - "search.AllSubcategories": "Todas Subcategorias", - "search.Age": "Idade", - "dashboard.PastDaysDescription": "Defina o número de dias passados para buscar registros do calendário.", - "dashboard.MinimumOfOneDay": "Mínimo de 1 dia", - "dashboard.FutureDaysDescription": "Defina o número de dias a frente para buscar registros do calendário.", - "dashboard.FutureDays": "Próximos Dias", - "dashboard.Wednesday": "Quarta-feira", - "dashboard.TwoWeeks": "Duas Semanas", - "dashboard.Tuesday": "Terça-feira", - "dashboard.Thursday": "Quinta-feira", - "dashboard.Sunday": "Domingo", - "dashboard.Schedule": "Cronograma", - "dashboard.Saturday": "Sábado", - "dashboard.PastDays": "Dias Anteriores", - "dashboard.OneWeek": "Uma Semana", - "dashboard.OneMonth": "Um Mês", - "dashboard.NoNewContent": "Sem Conteúdo Novo", - "dashboard.Monday": "Segunda-feira", - "dashboard.Modules": "Módulos", - "dashboard.Friday": "Sexta-feira", - "dashboard.Calendar": "Calendário", + "sonarr.AddedTag": "Tag Adicionada", + "sonarr.AddSeries": "Adicionar Série", + "sonarr.DeleteFile": "Deletar Arquivo", + "sonarr.EpisodeFileDeleted": "Arquivo do Episódio Deletado", + "sonarr.EpisodeFileRenamed": "Arquivo do Episódio Renomeado", + "sonarr.EpisodeNumber": "Episódio {}", + "sonarr.FailedToAddSeries": "Falhou ao Adicionar Séries", + "sonarr.FailedToAddTag": "Falhou ao adicionar Tag", + "sonarr.HistoryDescription": "Ver Atividade Recente", + "sonarr.ImportedTo": "Importado para", + "sonarr.ManualImport": "Importação Manual", + "sonarr.ManySeasons": "{} Temporadas", + "sonarr.MediaInfo": "Info da Mídia", + "sonarr.Message": "Mensagem", + "sonarr.MissingEpisodes": "Episódios Faltando", + "sonarr.Monitor": "Monitorar", + "sonarr.Ended": "Finalizado", + "sonarr.Episodes": "Episódios", + "sonarr.AddToExclusionList": "Adicionar a Lista de Exclusão", + "sonarr.Age": "Idade", + "sonarr.DownloadIgnored": "Download Ignorado", + "sonarr.Missing": "Faltando", + "sonarr.Monitoring": "Monitorando", + "sonarr.AllSeasons": "Todas as Temporadas", + "sonarr.AutomaticSearch": "Busca Automática", + "sonarr.DeleteEpisodeFile": "Deletar Arquivo do Episódio", + "sonarr.Language": "Língua", + "sonarr.Audio": "Áudio", + "sonarr.DeleteEpisodeFileHint1": "Tem certeza que deseja apagar o arquivo desse episódio?", + "sonarr.DeleteFiles": "Deletar Arquivos", + "sonarr.Episode": "Episódio", + "sonarr.EpisodesAvailable": "Episódios Disponíveis", + "sonarr.EpisodeImported": "Episódio Importado ({})", + "sonarr.FilterCatalogue": "Filtrar Catálogo", + "sonarr.FilterReleases": "Filtrar Lançamentos", + "sonarr.Languages": "Línguas", + "sonarr.ManualImportDescription": "Importar Conteúdo do Sistema de Arquivos", + "sonarr.Monitored": "Monitorado", + "sonarr.MissingEpisodesHint1": "Tem certeza que deseja procurar por todos os episódios faltantes?", + "sonarr.MonitorEpisode": "Monitorar Episódio", + "sonarr.MonitorSeries": "Monitorar Série", + "sonarr.MonitoredDescription": "Monitorar série por novos lançamentos", + "sonarr.More": "Mais", + "sonarr.Name": "Nome", + "sonarr.NoEpisodesFound": "Nenhum Episódio Encontrado", + "sonarr.AddedOn": "Adicionado Em", + "sonarr.AddedSeries": "Séries Adicionadas", "tautulli.Terminate": "Terminar", "tautulli.Subtitle": "Legenda", "tautulli.Stream": "Transmissão", @@ -318,60 +373,5 @@ "tautulli.Users": "Utilizadores", "tautulli.Video": "Vídeo", "tautulli.ViewWebGUI": "Ver WEB GUI", - "tautulli.Year": "Ano", - "overseerr.NoRequestsFound": "Nenhum Pedido Encontrado", - "overseerr.Requests": "Pedidos", - "overseerr.NoUsersFound": "Nenhum Usuário Encontrado", - "overseerr.NoRequests": "Sem Pedidos", - "overseerr.OneRequest": "1 Pedido", - "overseerr.SomeRequests": "{} Pedidos", - "overseerr.UnknownUser": "Usuário Desconhecido", - "overseerr.Users": "Usuários", - "sonarr.AddedTag": "Tag Adicionada", - "sonarr.AddSeries": "Adicionar Série", - "sonarr.DeleteFile": "Deletar Arquivo", - "sonarr.EpisodeFileDeleted": "Arquivo do Episódio Deletado", - "sonarr.EpisodeFileRenamed": "Arquivo do Episódio Renomeado", - "sonarr.EpisodeNumber": "Episódio {}", - "sonarr.FailedToAddSeries": "Falhou ao Adicionar Séries", - "sonarr.FailedToAddTag": "Falhou ao adicionar Tag", - "sonarr.HistoryDescription": "Ver Atividade Recente", - "sonarr.ImportedTo": "Importado para", - "sonarr.ManualImport": "Importação Manual", - "sonarr.ManySeasons": "{} Temporadas", - "sonarr.MediaInfo": "Info da Mídia", - "sonarr.Message": "Mensagem", - "sonarr.MissingEpisodes": "Episódios Faltando", - "sonarr.Monitor": "Monitorar", - "sonarr.Ended": "Finalizado", - "sonarr.Episodes": "Episódios", - "sonarr.AddToExclusionList": "Adicionar a Lista de Exclusão", - "sonarr.Age": "Idade", - "sonarr.DownloadIgnored": "Download Ignorado", - "sonarr.Missing": "Faltando", - "sonarr.Monitoring": "Monitorando", - "sonarr.AllSeasons": "Todas as Temporadas", - "sonarr.AutomaticSearch": "Busca Automática", - "sonarr.DeleteEpisodeFile": "Deletar Arquivo do Episódio", - "sonarr.Language": "Língua", - "sonarr.Audio": "Áudio", - "sonarr.DeleteEpisodeFileHint1": "Tem certeza que deseja apagar o arquivo desse episódio?", - "sonarr.DeleteFiles": "Deletar Arquivos", - "sonarr.Episode": "Episódio", - "sonarr.EpisodesAvailable": "Episódios Disponíveis", - "sonarr.EpisodeImported": "Episódio Importado ({})", - "sonarr.FilterCatalogue": "Filtrar Catálogo", - "sonarr.FilterReleases": "Filtrar Lançamentos", - "sonarr.Languages": "Línguas", - "sonarr.ManualImportDescription": "Importar Conteúdo do Sistema de Arquivos", - "sonarr.Monitored": "Monitorado", - "sonarr.MissingEpisodesHint1": "Tem certeza que deseja procurar por todos os episódios faltantes?", - "sonarr.MonitorEpisode": "Monitorar Episódio", - "sonarr.MonitorSeries": "Monitorar Série", - "sonarr.MonitoredDescription": "Monitorar série por novos lançamentos", - "sonarr.More": "Mais", - "sonarr.Name": "Nome", - "sonarr.NoEpisodesFound": "Nenhum Episódio Encontrado", - "sonarr.AddedOn": "Adicionado Em", - "sonarr.AddedSeries": "Séries Adicionadas" + "tautulli.Year": "Ano" } \ No newline at end of file diff --git a/assets/localization/ru.json b/assets/localization/ru.json index 7b9401ffec..39e9cafe06 100644 --- a/assets/localization/ru.json +++ b/assets/localization/ru.json @@ -1,63 +1,21 @@ { - "settings.Username": "Имя пользователя", - "settings.System": "Система", - "settings.SignOut": "Выйти", - "settings.SignIn": "Войти", - "settings.ResourcesDescription": "Полезные ресурсы и ссылки", - "settings.Resources": "Ресурсы", - "settings.ResetPassword": "Сброс пароля", - "settings.RenameProfile": "Переименовать профиль", - "settings.Register": "Регистрация", - "settings.QuickActions": "Быстрые действия", - "settings.ProfileName": "Название профиля", - "settings.ProfileAlreadyExists": "Профиль уже существует", - "settings.Profiles": "Профили", - "settings.PasswordValidation": "Требуется пароль", - "settings.Password": "Пароль", - "settings.OpenLinksIn": "Открыть ссылки в…", - "settings.NoHeadersAdded": "Заголовки не добавлены", - "settings.NoExternalModulesFound": "Внешние модули не найдены", - "settings.NoBackupsFound": "Резервные копии не найдены", - "settings.Notifications": "Уведомления", - "settings.ModuleNotFound": "Модуль не найден", - "settings.MACAddressValidation": "Неверный MAC-адрес", - "settings.MACAddress": "MAC-адрес", - "settings.Localization": "Локализация", - "settings.Language": "Язык", - "settings.InvalidPassword": "Неверный пароль", - "settings.Host": "Хост", - "settings.HeaderKey": "Ключ заголовка", - "settings.HeaderDeleted": "Заголовок удален", - "settings.ForgotYourPassword": "Забыли пароль?", - "settings.EncryptionKey": "Ключ шифрования", - "settings.EmailSentFailure": "Не удалось сбросить пароль", - "settings.Donations": "Пожертвования", - "settings.DisplayName": "Отображаемое имя", - "settings.DeleteProfile": "Удалить профиль", - "settings.DeleteModuleSuccess": "Модуль удален", - "settings.DeleteModule": "Удалить модуль", - "settings.DeleteHeader": "Удалить заголовок", - "settings.DeleteCloudBackupFailure": "Не удалось удалить", - "settings.DeleteCloudBackupDescription": "Удалить файл конфигурации", - "settings.DeleteCloudBackup": "Удалить облачную резервную копию", - "settings.DebugMenu": "Меню отладки", - "settings.DefaultPage": "Страница по умолчанию", - "settings.Configuration": "Конфигурация", - "settings.ClearConfiguration": "Очистить конфигурацию", - "settings.BannersNotificationModuleSupportHeader": "Поддерживаемые модули", - "settings.BackupConfiguration": "Конфигурация резервного копирования", - "settings.BackgroundImageOpacity": "Непрозрачность фонового изображения", - "settings.ApiKeyRequired": "Требуется ключ API", - "settings.ApiKey": "Ключ API", - "settings.AmoledTheme": "Тема AMOLED", - "settings.AddProfile": "Добавить профиль", - "settings.AddModuleSuccess": "Модуль добавлен", - "settings.AddModuleFailed": "Не удалось добавить модуль", - "settings.AddModule": "Добавить модуль", - "settings.AddHeader": "Добавить заголовок", - "settings.AccountHelp": "Аккаунт LunaSea", - "settings.AccountDescription": "Ваш аккаунт в LunaSea", - "settings.Account": "Аккаунт", + "dashboard.Calendar": "Календарь", + "dashboard.PastDays": "Прошедшие дни", + "dashboard.NoNewContent": "Нет нового контента", + "dashboard.FutureDays": "Будущие дни", + "dashboard.Wednesday": "Среда", + "dashboard.TwoWeeks": "Две недели", + "dashboard.Tuesday": "Вторник", + "dashboard.Thursday": "Четверг", + "dashboard.Sunday": "Воскресенье", + "dashboard.Schedule": "Расписание", + "dashboard.Saturday": "Суббота", + "dashboard.OneWeek": "Одна неделя", + "dashboard.OneMonth": "Один месяц", + "dashboard.Monday": "Понедельник", + "dashboard.Modules": "Модули", + "dashboard.MinimumOfOneDay": "Минимум 1 день", + "dashboard.Friday": "Пятница", "lunasea.AnErrorHasOccurred": "Произошла ошибка", "lunasea.Cancel": "Отмена", "lunasea.ChangeProfiles": "Изменить профили", @@ -71,6 +29,14 @@ "lunasea.Settings": "Настройки", "lunasea.TryAgain": "Попробуйте снова", "lunasea.Unknown": "Неизвестно", + "overseerr.Users": "Пользователи", + "overseerr.UnknownUser": "Неизвестный пользователь", + "overseerr.NoUsersFound": "Пользователи не найдены", + "overseerr.NoRequestsFound": "Запросы не найдены", + "overseerr.Requests": "Запросы", + "overseerr.NoRequests": "Нет запросов", + "overseerr.OneRequest": "1 запрос", + "overseerr.SomeRequests": "{} запросов", "radarr.AddMovie": "Добавить фильм", "radarr.AddMovieAndSearch": "Добавить + поиск", "radarr.Age": "Возраст", @@ -128,23 +94,65 @@ "search.Comments": "Комментарии", "search.Categories": "Категории", "search.Category": "Категория", - "dashboard.Calendar": "Календарь", - "dashboard.PastDays": "Прошедшие дни", - "dashboard.NoNewContent": "Нет нового контента", - "dashboard.FutureDays": "Будущие дни", - "dashboard.Wednesday": "Среда", - "dashboard.TwoWeeks": "Две недели", - "dashboard.Tuesday": "Вторник", - "dashboard.Thursday": "Четверг", - "dashboard.Sunday": "Воскресенье", - "dashboard.Schedule": "Расписание", - "dashboard.Saturday": "Суббота", - "dashboard.OneWeek": "Одна неделя", - "dashboard.OneMonth": "Один месяц", - "dashboard.Monday": "Понедельник", - "dashboard.Modules": "Модули", - "dashboard.MinimumOfOneDay": "Минимум 1 день", - "dashboard.Friday": "Пятница", + "settings.Username": "Имя пользователя", + "settings.System": "Система", + "settings.SignOut": "Выйти", + "settings.SignIn": "Войти", + "settings.ResourcesDescription": "Полезные ресурсы и ссылки", + "settings.Resources": "Ресурсы", + "settings.ResetPassword": "Сброс пароля", + "settings.RenameProfile": "Переименовать профиль", + "settings.Register": "Регистрация", + "settings.QuickActions": "Быстрые действия", + "settings.ProfileName": "Название профиля", + "settings.ProfileAlreadyExists": "Профиль уже существует", + "settings.Profiles": "Профили", + "settings.PasswordValidation": "Требуется пароль", + "settings.Password": "Пароль", + "settings.OpenLinksIn": "Открыть ссылки в…", + "settings.NoHeadersAdded": "Заголовки не добавлены", + "settings.NoExternalModulesFound": "Внешние модули не найдены", + "settings.NoBackupsFound": "Резервные копии не найдены", + "settings.Notifications": "Уведомления", + "settings.ModuleNotFound": "Модуль не найден", + "settings.MACAddressValidation": "Неверный MAC-адрес", + "settings.MACAddress": "MAC-адрес", + "settings.Localization": "Локализация", + "settings.Language": "Язык", + "settings.InvalidPassword": "Неверный пароль", + "settings.Host": "Хост", + "settings.HeaderKey": "Ключ заголовка", + "settings.HeaderDeleted": "Заголовок удален", + "settings.ForgotYourPassword": "Забыли пароль?", + "settings.EncryptionKey": "Ключ шифрования", + "settings.EmailSentFailure": "Не удалось сбросить пароль", + "settings.Donations": "Пожертвования", + "settings.DisplayName": "Отображаемое имя", + "settings.DeleteProfile": "Удалить профиль", + "settings.DeleteModuleSuccess": "Модуль удален", + "settings.DeleteModule": "Удалить модуль", + "settings.DeleteHeader": "Удалить заголовок", + "settings.DeleteCloudBackupFailure": "Не удалось удалить", + "settings.DeleteCloudBackupDescription": "Удалить файл конфигурации", + "settings.DeleteCloudBackup": "Удалить облачную резервную копию", + "settings.DebugMenu": "Меню отладки", + "settings.DefaultPage": "Страница по умолчанию", + "settings.Configuration": "Конфигурация", + "settings.ClearConfiguration": "Очистить конфигурацию", + "settings.BannersNotificationModuleSupportHeader": "Поддерживаемые модули", + "settings.BackupConfiguration": "Конфигурация резервного копирования", + "settings.BackgroundImageOpacity": "Непрозрачность фонового изображения", + "settings.ApiKeyRequired": "Требуется ключ API", + "settings.ApiKey": "Ключ API", + "settings.AmoledTheme": "Тема AMOLED", + "settings.AddProfile": "Добавить профиль", + "settings.AddModuleSuccess": "Модуль добавлен", + "settings.AddModuleFailed": "Не удалось добавить модуль", + "settings.AddModule": "Добавить модуль", + "settings.AddHeader": "Добавить заголовок", + "settings.AccountHelp": "Аккаунт LunaSea", + "settings.AccountDescription": "Ваш аккаунт в LunaSea", + "settings.Account": "Аккаунт", "tautulli.Year": "Год", "tautulli.Video": "Видео", "tautulli.Users": "Пользователи", @@ -183,13 +191,5 @@ "tautulli.BackingUpConfigurationDescription": "Резервное копирование конфигурации в фоновом режиме", "tautulli.BackingUpConfiguration": "Резервное копирование конфигурации…", "tautulli.Audio": "Аудио", - "tautulli.Activity": "Активность", - "overseerr.Users": "Пользователи", - "overseerr.UnknownUser": "Неизвестный пользователь", - "overseerr.NoUsersFound": "Пользователи не найдены", - "overseerr.NoRequestsFound": "Запросы не найдены", - "overseerr.Requests": "Запросы", - "overseerr.NoRequests": "Нет запросов", - "overseerr.OneRequest": "1 запрос", - "overseerr.SomeRequests": "{} запросов" + "tautulli.Activity": "Активность" } \ No newline at end of file diff --git a/assets/localization/sv.json b/assets/localization/sv.json index 5c755fbb15..9e0ca9f4c6 100644 --- a/assets/localization/sv.json +++ b/assets/localization/sv.json @@ -1,120 +1,23 @@ { - "settings.BackupConfigurationHint1": "Alla backups krypteras innan de exporteras", - "settings.BackupConfiguration": "Backup Konfiguration", - "settings.AddProfile": "Lägg Till Profil", - "settings.AddHeader": "Lägg Till Header", - "settings.AccountHelpHint1": "LunaSea erbjuder ett gratiskonto för backup av konfigurationen till molnet, med ytterligare funktioner som kommer i framtiden!", - "settings.AccountHelp": "LunaSea Konto", - "settings.MustBeValueBetween": "Måste vara ett värde mellan {} och {}", - "settings.UsernameValidation": "Användarnamn krävs", - "settings.Username": "Användarnamn", - "settings.TestConnection": "Testa anslutning", - "settings.SystemDescription": "Systemverktyg och information", - "settings.System": "System", - "settings.StartingType": "Starttyp", - "settings.StartingSize": "Startstorlek", - "settings.StartingDay": "Startdag", - "settings.ShowCalendarEntries": "Visa {} Kalenderposter", - "settings.SignOutHint1": "Är du säker på att du vill logga ut från ditt LunaSea-konto?", - "settings.SignOut": "Logga ut", - "settings.SignIn": "Logga in", - "settings.SignedOutSuccessMessage": "Loggade ut från ditt LunaSea-konto", - "settings.SignedOutFailure": "Det gick inte att logga ut", - "settings.SignedInFailure": "Det gick inte att logga in", - "settings.RestoreFromCloudSuccessMessage": "Din konfiguration har återställts", - "settings.RestoreFromCloudFailure": "Det gick inte att återställa", - "settings.RestoreFromCloudDescription": "Återställ konfigurationsdata", - "settings.RestoreFromCloud": "Återställ från moln", - "settings.ResourcesDescription": "Användbara resurser och länkar", - "settings.Resources": "Resurser", - "settings.ResetPassword": "Återställ Lösenord", - "settings.RenameProfile": "Byt namn på profil", - "settings.RegisteredFailure": "Det gick inte att registrera", - "settings.Register": "Registrera", - "settings.QuickActions": "Snabb åtgärder", - "settings.ProfileNameRequired": "Profilnamn krävs", - "settings.ProfileName": "Profilnamn", - "settings.ProfileAlreadyExists": "Profilen finns redan", - "settings.ProfilesDescription": "Hantera dina profiler", - "settings.Profiles": "Profiler", - "settings.PasswordValidation": "Lösenord krävs", - "settings.Password": "Lösenord", - "settings.OpenLinksIn": "Öppna länkar i…", - "settings.NoExternalModulesFound": "Inga externa moduler hittades", - "settings.NoBackupsFound": "Inga säkerhetskopior hittades", - "settings.ModuleNotFound": "Modul hittades inte", - "settings.MinimumCharacters": "Minst {} tecken", - "settings.MACAddressValidation": "Ogiltig MAC-adress", - "settings.MACAddress": "MAC-adress", - "settings.LocalizationDescription": "Anpassa till din lokal", - "settings.Localization": "Lokalisering", - "settings.Language": "Språk", - "settings.InvalidPasswordMessage": "Lösenordet är ogiltigt", - "settings.InvalidPassword": "Felaktigt lösenord", - "settings.InvalidEmailMessage": "E -mailadressen är ogiltig", - "settings.InvalidEmail": "Ogiltig Email", - "settings.HostValidation": "Värd måste inkludera http:// eller https://", - "settings.HostRequiredMessage": "Värd krävs för att ansluta till {}", - "settings.HostRequired": "Värd krävs", - "settings.HostHint3": "Använd inte localhost eller 127.0.0.1", - "settings.HostHint2": "Du måste inkludera antingen http:// eller https://", - "settings.HostHint1": "Detta är webbadressen där du får åtkomst till webbgränssnittet för tjänsten", - "settings.Host": "Värd", - "settings.ForgotYourPassword": "Glömt ditt lösenord?", - "settings.EncryptionKey": "Krypteringsnyckel", - "settings.EnabledProfile": "Aktiverad profil", - "settings.EmailSentSuccessMessage": "Ett email för att återställa ditt lösenord har skickats!", - "settings.EmailSentSuccess": "Email skickat", - "settings.EmailSentFailure": "Det gick inte att återställa lösenordet", - "settings.Email": "Email", - "settings.EditModule": "Redigera modul", - "settings.DonationsDescription": "Donera till utvecklaren", - "settings.Donations": "Donationer", - "settings.DisplayName": "Visningsnamn", - "settings.DeleteProfile": "Ta bort profil", - "settings.DeleteModuleSuccess": "Modul borttagen", - "settings.DeleteModuleHint1": "Är du säker på att du vill ta bort denna externa modul?", - "settings.DeleteModule": "Ta bort modul", - "settings.DeleteCloudBackupFailure": "Det gick inte att ta bort", - "settings.DeleteCloudBackupDescription": "Ta bort en konfigurationsfil", - "settings.DeleteCloudBackup": "Ta bort Molnbackup", - "settings.DebugMenuDescription": "Debug- och utvecklingsverktyg", - "settings.DebugMenu": "Debugmeny", - "settings.DecryptBackup": "Dekryptera säkerhetskopiering", - "settings.DefaultPage": "Standardsida", - "settings.Custom": "Anpassad…", - "settings.ConnectionTestFailed": "Anslutningstest misslyckades", - "settings.ConnectionDetailsDescription": "Anslutningsinformation för {}", - "settings.ConnectionDetails": "Anslutningsinformation", - "settings.ConnectedSuccessfullyMessage": "{} är redo att användas med LunaSea!", - "settings.ConnectedSuccessfully": "Ansluten framgångsrikt", - "settings.ConfigurationDescription": "Konfigurera & Ställ in LunaSea", - "settings.Configuration": "Konfiguration", - "settings.ConfigureModule": "Konfigurera {}", - "settings.ClearConfigurationHint1": "Är du säker på att du vill rensa din konfiguration?", - "settings.ClearConfiguration": "Rensa Konfiguration", - "settings.BroadcastAddressHint2": "Vanligtvis är detta maskinens IP-adress med den sista oktetten inställd på 255", - "settings.BasicAuthenticationHint2": "Lösenordet kan innehålla ett kolon", - "settings.BasicAuthenticationHint1": "Användarnamnet får inte innehålla ett kolon", - "settings.BasicAuthentication": "Grundläggande autentisering", - "settings.BackupToCloudFailure": "Misslyckades att säkerhetskopiera", - "settings.BackupToCloudDescription": "Säkerhetskopiera konfigurationsdata", - "settings.BackupToCloud": "Säkerhetskopiera till moln", - "settings.BackupList": "Säkerhetskopieringslista", - "settings.BackupConfigurationHint2": "Krypteringsnyckeln måste innehålla minst 8 tecken", - "settings.AutomaticallyManageOrderDescription": "Lista moduler alfabetiskt", - "settings.AppearanceDescription": "Anpassa utseendet", - "settings.Appearance": "Utseende", - "settings.ApiKeyRequiredMessage": "API -nyckel krävs för att ansluta till {}", - "settings.ApiKeyRequired": "API -nyckel krävs", - "settings.ApiKey": "API-nyckel", - "settings.AmoledTheme": "AMOLED tema", - "settings.AllFieldsAreRequired": "Alla fält är obligatoriska", - "settings.AddModuleSuccess": "Modul tillagd", - "settings.AddModuleFailed": "Misslyckades med att lägga till modul", - "settings.AddModule": "Lägg till modul", - "settings.AccountDescription": "Ditt LunaSea Konto", - "settings.Account": "Konto", + "dashboard.Wednesday": "Onsdag", + "dashboard.TwoWeeks": "Två Veckor", + "dashboard.Tuesday": "Tisdag", + "dashboard.Thursday": "Torsdag", + "dashboard.Sunday": "Söndag", + "dashboard.Schedule": "Schema", + "dashboard.Saturday": "Lördag", + "dashboard.PastDaysDescription": "Sätt antal dagar i det förflutna som kalendern skall hämta poster för.", + "dashboard.PastDays": "Förflutna Dagar", + "dashboard.OneWeek": "En Vecka", + "dashboard.OneMonth": "En Månad", + "dashboard.NoNewContent": "Inget Nytt Innehåll", + "dashboard.Monday": "Måndag", + "dashboard.Modules": "Moduler", + "dashboard.MinimumOfOneDay": "Minimum av 1 Dag", + "dashboard.FutureDaysDescription": "Sätt antal dagar i framtiden som kalendern skall hämta poster för.", + "dashboard.FutureDays": "Framtida Dagar", + "dashboard.Friday": "Fredag", + "dashboard.Calendar": "Kalender", "lunasea.Add": "Lägg Till", "lunasea.AnErrorHasOccurred": "Ett fel har inträffat", "lunasea.BackUp": "Back Up", @@ -149,6 +52,9 @@ "lunasea.UnknownError": "Okänt fel", "lunasea.UnknownModule": "Okänd modul", "lunasea.Website": "Webbsida", + "overseerr.Users": "Användare", + "overseerr.UnknownUser": "Okänd användare", + "overseerr.NoUsersFound": "Inga användare hittades", "radarr.AddMovie": "Lägg till Film", "radarr.AddMovieAndSearch": "Lägg till + Sök", "radarr.Age": "Ålder", @@ -267,25 +173,122 @@ "search.Categories": "Kategorier", "search.Category": "Kategori", "search.AllSubcategories": "Alla Underkategorier", - "dashboard.Wednesday": "Onsdag", - "dashboard.TwoWeeks": "Två Veckor", - "dashboard.Tuesday": "Tisdag", - "dashboard.Thursday": "Torsdag", - "dashboard.Sunday": "Söndag", - "dashboard.Schedule": "Schema", - "dashboard.Saturday": "Lördag", - "dashboard.PastDaysDescription": "Sätt antal dagar i det förflutna som kalendern skall hämta poster för.", - "dashboard.PastDays": "Förflutna Dagar", - "dashboard.OneWeek": "En Vecka", - "dashboard.OneMonth": "En Månad", - "dashboard.NoNewContent": "Inget Nytt Innehåll", - "dashboard.Monday": "Måndag", - "dashboard.Modules": "Moduler", - "dashboard.MinimumOfOneDay": "Minimum av 1 Dag", - "dashboard.FutureDaysDescription": "Sätt antal dagar i framtiden som kalendern skall hämta poster för.", - "dashboard.FutureDays": "Framtida Dagar", - "dashboard.Friday": "Fredag", - "dashboard.Calendar": "Kalender", + "settings.BackupConfigurationHint1": "Alla backups krypteras innan de exporteras", + "settings.BackupConfiguration": "Backup Konfiguration", + "settings.AddProfile": "Lägg Till Profil", + "settings.AddHeader": "Lägg Till Header", + "settings.AccountHelpHint1": "LunaSea erbjuder ett gratiskonto för backup av konfigurationen till molnet, med ytterligare funktioner som kommer i framtiden!", + "settings.AccountHelp": "LunaSea Konto", + "settings.MustBeValueBetween": "Måste vara ett värde mellan {} och {}", + "settings.UsernameValidation": "Användarnamn krävs", + "settings.Username": "Användarnamn", + "settings.TestConnection": "Testa anslutning", + "settings.SystemDescription": "Systemverktyg och information", + "settings.System": "System", + "settings.StartingType": "Starttyp", + "settings.StartingSize": "Startstorlek", + "settings.StartingDay": "Startdag", + "settings.ShowCalendarEntries": "Visa {} Kalenderposter", + "settings.SignOutHint1": "Är du säker på att du vill logga ut från ditt LunaSea-konto?", + "settings.SignOut": "Logga ut", + "settings.SignIn": "Logga in", + "settings.SignedOutSuccessMessage": "Loggade ut från ditt LunaSea-konto", + "settings.SignedOutFailure": "Det gick inte att logga ut", + "settings.SignedInFailure": "Det gick inte att logga in", + "settings.RestoreFromCloudSuccessMessage": "Din konfiguration har återställts", + "settings.RestoreFromCloudFailure": "Det gick inte att återställa", + "settings.RestoreFromCloudDescription": "Återställ konfigurationsdata", + "settings.RestoreFromCloud": "Återställ från moln", + "settings.ResourcesDescription": "Användbara resurser och länkar", + "settings.Resources": "Resurser", + "settings.ResetPassword": "Återställ Lösenord", + "settings.RenameProfile": "Byt namn på profil", + "settings.RegisteredFailure": "Det gick inte att registrera", + "settings.Register": "Registrera", + "settings.QuickActions": "Snabb åtgärder", + "settings.ProfileNameRequired": "Profilnamn krävs", + "settings.ProfileName": "Profilnamn", + "settings.ProfileAlreadyExists": "Profilen finns redan", + "settings.ProfilesDescription": "Hantera dina profiler", + "settings.Profiles": "Profiler", + "settings.PasswordValidation": "Lösenord krävs", + "settings.Password": "Lösenord", + "settings.OpenLinksIn": "Öppna länkar i…", + "settings.NoExternalModulesFound": "Inga externa moduler hittades", + "settings.NoBackupsFound": "Inga säkerhetskopior hittades", + "settings.ModuleNotFound": "Modul hittades inte", + "settings.MinimumCharacters": "Minst {} tecken", + "settings.MACAddressValidation": "Ogiltig MAC-adress", + "settings.MACAddress": "MAC-adress", + "settings.LocalizationDescription": "Anpassa till din lokal", + "settings.Localization": "Lokalisering", + "settings.Language": "Språk", + "settings.InvalidPasswordMessage": "Lösenordet är ogiltigt", + "settings.InvalidPassword": "Felaktigt lösenord", + "settings.InvalidEmailMessage": "E -mailadressen är ogiltig", + "settings.InvalidEmail": "Ogiltig Email", + "settings.HostValidation": "Värd måste inkludera http:// eller https://", + "settings.HostRequiredMessage": "Värd krävs för att ansluta till {}", + "settings.HostRequired": "Värd krävs", + "settings.HostHint3": "Använd inte localhost eller 127.0.0.1", + "settings.HostHint2": "Du måste inkludera antingen http:// eller https://", + "settings.HostHint1": "Detta är webbadressen där du får åtkomst till webbgränssnittet för tjänsten", + "settings.Host": "Värd", + "settings.ForgotYourPassword": "Glömt ditt lösenord?", + "settings.EncryptionKey": "Krypteringsnyckel", + "settings.EnabledProfile": "Aktiverad profil", + "settings.EmailSentSuccessMessage": "Ett email för att återställa ditt lösenord har skickats!", + "settings.EmailSentSuccess": "Email skickat", + "settings.EmailSentFailure": "Det gick inte att återställa lösenordet", + "settings.Email": "Email", + "settings.EditModule": "Redigera modul", + "settings.DonationsDescription": "Donera till utvecklaren", + "settings.Donations": "Donationer", + "settings.DisplayName": "Visningsnamn", + "settings.DeleteProfile": "Ta bort profil", + "settings.DeleteModuleSuccess": "Modul borttagen", + "settings.DeleteModuleHint1": "Är du säker på att du vill ta bort denna externa modul?", + "settings.DeleteModule": "Ta bort modul", + "settings.DeleteCloudBackupFailure": "Det gick inte att ta bort", + "settings.DeleteCloudBackupDescription": "Ta bort en konfigurationsfil", + "settings.DeleteCloudBackup": "Ta bort Molnbackup", + "settings.DebugMenuDescription": "Debug- och utvecklingsverktyg", + "settings.DebugMenu": "Debugmeny", + "settings.DecryptBackup": "Dekryptera säkerhetskopiering", + "settings.DefaultPage": "Standardsida", + "settings.Custom": "Anpassad…", + "settings.ConnectionTestFailed": "Anslutningstest misslyckades", + "settings.ConnectionDetailsDescription": "Anslutningsinformation för {}", + "settings.ConnectionDetails": "Anslutningsinformation", + "settings.ConnectedSuccessfullyMessage": "{} är redo att användas med LunaSea!", + "settings.ConnectedSuccessfully": "Ansluten framgångsrikt", + "settings.ConfigurationDescription": "Konfigurera & Ställ in LunaSea", + "settings.Configuration": "Konfiguration", + "settings.ConfigureModule": "Konfigurera {}", + "settings.ClearConfigurationHint1": "Är du säker på att du vill rensa din konfiguration?", + "settings.ClearConfiguration": "Rensa Konfiguration", + "settings.BroadcastAddressHint2": "Vanligtvis är detta maskinens IP-adress med den sista oktetten inställd på 255", + "settings.BasicAuthenticationHint2": "Lösenordet kan innehålla ett kolon", + "settings.BasicAuthenticationHint1": "Användarnamnet får inte innehålla ett kolon", + "settings.BasicAuthentication": "Grundläggande autentisering", + "settings.BackupToCloudFailure": "Misslyckades att säkerhetskopiera", + "settings.BackupToCloudDescription": "Säkerhetskopiera konfigurationsdata", + "settings.BackupToCloud": "Säkerhetskopiera till moln", + "settings.BackupList": "Säkerhetskopieringslista", + "settings.BackupConfigurationHint2": "Krypteringsnyckeln måste innehålla minst 8 tecken", + "settings.AutomaticallyManageOrderDescription": "Lista moduler alfabetiskt", + "settings.AppearanceDescription": "Anpassa utseendet", + "settings.Appearance": "Utseende", + "settings.ApiKeyRequiredMessage": "API -nyckel krävs för att ansluta till {}", + "settings.ApiKeyRequired": "API -nyckel krävs", + "settings.ApiKey": "API-nyckel", + "settings.AmoledTheme": "AMOLED tema", + "settings.AllFieldsAreRequired": "Alla fält är obligatoriska", + "settings.AddModuleSuccess": "Modul tillagd", + "settings.AddModuleFailed": "Misslyckades med att lägga till modul", + "settings.AddModule": "Lägg till modul", + "settings.AccountDescription": "Ditt LunaSea Konto", + "settings.Account": "Konto", "tautulli.Year": "År", "tautulli.ViewWebGUI": "Visa Web GUI", "tautulli.Video": "Video", @@ -353,8 +356,5 @@ "tautulli.BackingUpConfiguration": "Säkerhetskopiera Konfiguration…", "tautulli.Audio": "Ljud", "tautulli.ActivityDetails": "Aktivitetsdetaljer", - "tautulli.Activity": "Aktivitet", - "overseerr.Users": "Användare", - "overseerr.UnknownUser": "Okänd användare", - "overseerr.NoUsersFound": "Inga användare hittades" + "tautulli.Activity": "Aktivitet" } \ No newline at end of file diff --git a/assets/localization/tr.json b/assets/localization/tr.json index debaf98455..5ad9b2fe99 100644 --- a/assets/localization/tr.json +++ b/assets/localization/tr.json @@ -1,205 +1,25 @@ { - "settings.Account": "Hesap", - "settings.HostHint1": "Bu, hizmet için web grafiksel kullanıcı arayüzüne eriştiğiniz URL'dir", - "settings.MustBeValueBetween": "{} ile {} arasında bir değer olmalıdır", - "settings.UsernameValidation": "Kullanıcı Adı Gerekli", - "settings.Username": "Kullanıcı Adı", - "settings.TestConnection": "Bağlantıyı Test Et", - "settings.SystemDescription": "Sistem Yardımcı Programları ve Bilgileri", - "settings.System": "Sistem", - "settings.StartingType": "Başlangıç Türü", - "settings.StartingSize": "Başlangıç Boyutu", - "settings.StartingDay": "Başlangıç Günü", - "settings.ShowCalendarEntries": "{} Takvim Girdisini Göster", - "settings.SignOutHint1": "LunaSea hesabınızdan çıkmak istediğinize emin misiniz?", - "settings.SignOut": "Oturumu Kapat", - "settings.SignIn": "Oturum Aç", - "settings.SignedOutSuccessMessage": "LunaSea hesabınızdan çıkış yaptınız", - "settings.SignedOutSuccess": "Oturum Kapatıldı", - "settings.SignedOutFailure": "Oturum Kapatılamadı", - "settings.SignedInSuccess": "Başarıyla Oturum Açıldı", - "settings.SignedInFailure": "Oturum Açılamadı", - "settings.RestoreFromCloudSuccessMessage": "Yapılandırmanız geri yüklendi", - "settings.RestoreFromCloudSuccess": "Başarıyla Geri Yüklendi", - "settings.RestoreFromCloudFailure": "Geri Yüklenemedi", - "settings.RestoreFromCloudDescription": "Yapılandırma Verilerini Geri Yükle", - "settings.RestoreFromCloud": "Buluttan Geri Yükle", - "settings.ResourcesDescription": "Faydalı Kaynaklar ve Bağlantılar", - "settings.Resources": "Kaynaklar", - "settings.ResetPassword": "Parolayı Sıfırla", - "settings.RenameProfile": "Profili Yeniden Adlandır", - "settings.RegisteredSuccess": "Kaydolundu", - "settings.RegisteredFailure": "Kaydolunamadı", - "settings.Register": "Kaydol", - "settings.QuickActionsDescription": "Ana Ekranda Hızlı Eylemler", - "settings.QuickActions": "Hızlı Eylemler", - "settings.ProfileNameRequired": "Profil Adı Gerekli", - "settings.ProfileName": "Profil Adı", - "settings.ProfileAlreadyExists": "Profil Zaten Var", - "settings.ProfilesDescription": "Profillerinizi Yönetin", - "settings.Profiles": "Profiller", - "settings.PasswordValidation": "Parola Gerekli", - "settings.Password": "Parola", - "settings.OpenLinksIn": "Bağlantıları Şununla Aç…", - "settings.NoHeadersAdded": "Başlık Eklenmedi", - "settings.NoExternalModulesFound": "Harici Modül Bulunamadı", - "settings.NoBackupsFound": "Yedekleme Bulunamadı", - "settings.NotificationsDescription": "Anlık Bildirimler için Web Kancaları Ayarlayın", - "settings.Notifications": "Bildirimler", - "settings.ModuleNotFound": "Modül Bulunamadı", - "settings.MinimumCharacters": "En az {} karakter", - "settings.MACAddressValidation": "Geçersiz MAC Adresi", - "settings.MACAddressHint4": "Her onaltılık sekizli iki nokta üst üste ile ayrılmalıdır", - "settings.MACAddressHint3": "Onaltılık basamaklar 0-9 ve A-F arasındadır", - "settings.MACAddressHint2": "MAC adresleri altı adet onaltılık basamak ikilisi (yani sekizli, octet) içerir", - "settings.MACAddressHint1": "Bu, uyandırmak istediğiniz makinenin MAC adresidir", - "settings.MACAddress": "MAC Adresi", - "settings.LocalizationDescription": "Yerel Ayarlarınıza Göre Özelleştirin", - "settings.Localization": "Yerelleştirme", - "settings.Language": "Dil", - "settings.InvalidPasswordMessage": "Parola geçersiz", - "settings.InvalidPassword": "Geçersiz Parola", - "settings.InvalidEmailMessage": "E-posta adresi geçersiz", - "settings.InvalidEmail": "Geçersiz E-posta Adresi", - "settings.ImageBackgroundOpacityHint2": "Arka plan resimlerini getirmeyi tamamen devre dışı bırakmak için değeri 0 olarak ayarlayın.", - "settings.ImageBackgroundOpacityHint1": "Arka plan resimlerinin opaklığını ayarlayın.", - "settings.ImageBackgroundOpacity": "Resim Arka Plan Opaklığı", - "settings.HostValidation": "Ana makine http:// veya https:// içermelidir", - "settings.HostRequiredMessage": "{}'e bağlanmak için ana makine gereklidir", - "settings.HostRequired": "Ana Makine Gerekli", - "settings.HostHint5": "Temel kimlik doğrulaması eklemek için lütfen özel başlıklar özelliğini kullanın", - "settings.HostHint4": "Ters vekil sunucu kullanmadığınızda, lütfen bağlantı noktasını ekleyin", - "settings.HostHint3": "localhost veya 127.0.0.1 kullanmayın", - "settings.HostHint2": "http:// veya https:// eklemelisiniz", - "settings.Host": "Ana Makine", - "settings.HeaderValueValidation": "Başlık Değeri Gerekli", - "settings.HeaderValue": "Başlık Değeri", - "settings.HeaderKeyValidation": "Başlık Anahtarı Gerekli", - "settings.HeaderKey": "Başlık Anahtarı", - "settings.HeaderDeleted": "Başlık Silindi", - "settings.HeaderAdded": "Başlık Eklendi", - "settings.ForgotYourPassword": "Parolanızı mı Unuttunuz?", - "settings.EncryptionKey": "Şifreleme Anahtarı", - "settings.EnabledProfile": "Etkin Profil", - "settings.EmailSentSuccessMessage": "Parolanızı sıfırlamak için bir e-posta gönderildi!", - "settings.EmailSentSuccess": "E-posta Gönderildi", - "settings.EmailSentFailure": "Parola Sıfırlanamadı", - "settings.Email": "E-posta", - "settings.EditModule": "Modülü Düzenle", - "settings.DrawerDescription": "Çekmeceyi Özelleştirin", - "settings.Drawer": "Çekmece", - "settings.DonationsDescription": "Geliştiriciye Bağış Yapın", - "settings.Donations": "Bağışlar", - "settings.DisplayName": "Görünen Ad", - "settings.DismissBannersHint2": "Araç ipucu afişleri, LunaSea'de bulunan özellikler için size ipuçları ve püf noktaları verecektir.", - "settings.DismissBannersHint1": "Tüm araç ipucu afişlerini kapatmak istediğinizden emin misiniz?", - "settings.DismissBanners": "Afişleri Kapat", - "settings.DeleteProfile": "Profili Sil", - "settings.DeleteModuleSuccess": "Modül Silindi", - "settings.DeleteModuleHint1": "Bu harici modülü silmek istediğinizden emin misiniz?", - "settings.DeleteModule": "Modülü Sil", - "settings.DeleteIndexerHint1": "Bu dizinleyiciyi silmek istediğinizden emin misiniz?", - "settings.DeleteIndexer": "Dizinleyiciyi Sil", - "settings.DeleteHeaderHint1": "Bu başlığı silmek istediğinizden emin misiniz?", - "settings.DeleteHeader": "Başlığı Sil", - "settings.DeleteCloudBackupSuccess": "Silindi", - "settings.DeleteCloudBackupFailure": "Silinemedi", - "settings.DeleteCloudBackupDescription": "Bir Yapılandırma Dosyasını Sil", - "settings.DeleteCloudBackup": "Bulut Yedeklemeyi Sil", - "settings.DebugMenuDescription": "Hata Ayıklama ve Geliştirme Yardımcı Programları", - "settings.DebugMenu": "Hata Ayıklama Menüsü", - "settings.DecryptBackupHint1": "Lütfen bu yedekleme için şifreleme anahtarını girin.", - "settings.DecryptBackup": "Yedeklemenin Şifresini Çöz", - "settings.DefaultPage": "Öntanımlı Sayfa", - "settings.CustomHeadersDescription": "İsteklere Özel Başlıklar Ekleyin", - "settings.CustomHeaders": "Özel Başlıklar", - "settings.CustomHeader": "Özel Başlık", - "settings.Custom": "Özel…", - "settings.ConnectionTestFailed": "Bağlantı Testi Başarısız Oldu", - "settings.ConnectionDetailsDescription": "{} için Bağlantı Ayrıntıları", - "settings.ConnectionDetails": "Bağlantı Ayrıntıları", - "settings.ConnectedSuccessfullyMessage": "{}, LunaSea ile kullanıma hazır!", - "settings.ConnectedSuccessfully": "Bağlandı", - "settings.ConfigurationDescription": "LunaSea'yi Kurun ve Yapılandırın", - "settings.Configuration": "Yapılandırma", - "settings.ConfigureModule": "{} Yapılandır", - "settings.ClearLogsHint1": "Tüm kayıtlı günlükleri silmek istediğinizden emin misiniz?\n\nGünlükler, hata bildirme ve hata ayıklama için faydalı olabilir.", - "settings.ClearLogs": "Günlük Kayıtlarını Temizle", - "settings.ClearConfigurationHint3": "Bir LunaSea hesabında oturum açtıysanız, oturumunuz kapatılacak.", - "settings.ClearConfigurationHint2": "Temiz bir sayfadan başlayacaksınız, lütfen önce geçerli yapılandırmanızı yedeklediğinizden emin olun!", - "settings.ClearConfigurationHint1": "Yapılandırmanızı temizlemek istediğinizden emin misiniz?", - "settings.ClearConfiguration": "Yapılandırmayı Temizle", - "settings.BroadcastAddressValidation": "Geçersiz Genel Yayın Adresi", - "settings.BroadcastAddressHint3": "Örneğin, makinenin IP adresi 192.168.1.111 ise, elde edilen genel yayın IP adresi 192.168.1.255 olur", - "settings.BroadcastAddressHint2": "Genellikle bu, makinenizin IP adresinin son kısmının 255 olarak değiştirilmiş halidir", - "settings.BroadcastAddressHint1": "Bu, yerel ağınızın genel yayın adresidir", - "settings.BroadcastAddress": "Genel Yayın Adresi", - "settings.BasicAuthenticationHint3": "Kullanıcı adı ve parola otomatik olarak base64 kodlamasına dönüştürülür", - "settings.BasicAuthenticationHint2": "Parola iki nokta üst üste içerebilir", - "settings.BasicAuthenticationHint1": "Kullanıcı adı iki nokta üst üste içermemelidir", - "settings.BasicAuthentication": "Temel Kimlik Doğrulaması", - "settings.BannersNotificationModuleSupportBody": "Web kancası tabanlı bildirimler şu anda yalnızca aşağıda listelenen modüllerde desteklenmektedir.\n\nEk modül desteği ileride gelecek!", - "settings.BannersNotificationModuleSupportHeader": "Desteklenen Modüller", - "settings.BackupToCloudSuccess": "Başarıyla Yedeklendi", - "settings.BackupToCloudFailure": "Yedeklenemedi", - "settings.BackupToCloudDescription": "Yapılandırma Verilerini Yedekle", - "settings.BackupToCloud": "Buluta Yedekle", - "settings.BackupList": "Yedekleme Listesi", - "settings.BackupConfigurationHint2": "Şifreleme anahtarı en az 8 karakter olmalıdır", - "settings.BackupConfigurationHint1": "Tüm yedeklemeler dışa aktarılmadan önce şifrelenmektedir", - "settings.BackupConfiguration": "Yapılandırmayı Yedekle", - "settings.BackgroundImageOpacity": "Arka Plan Resmi Opaklığı", - "settings.AutomaticallyManageOrderDescription": "Modülleri Alfabetik Olarak Listeleyin", - "settings.AutomaticallyManageOrder": "Sıralamayı Otomatik Olarak Yönet", - "settings.AppearanceDescription": "Görünümü Özelleştirin", - "settings.Appearance": "Görünüm", - "settings.ApiKeyRequiredMessage": "{}'e bağlanmak için API anahtarı gereklidir", - "settings.ApiKeyRequired": "API Anahtarı Gerekli", - "settings.ApiKey": "API Anahtarı", - "settings.AmoledThemeDescription": "Saf Siyah Koyu Tema", - "settings.AmoledThemeBordersDescription": "Kullanıcı Arayüzü Boyunca İnce Kenarlıklar Ekleyin", - "settings.AmoledThemeBorders": "AMOLED Tema Kenarlıkları", - "settings.AmoledTheme": "AMOLED Tema", - "settings.AllFieldsAreRequired": "Tüm alanlar gereklidir", - "settings.AddProfile": "Profil Ekle", - "settings.AddModuleSuccess": "Modül Eklendi", - "settings.AddModuleFailed": "Modül Eklenemedi", - "settings.AddModule": "Modül Ekle", - "settings.AddHeader": "Başlık Ekle", - "settings.AccountHelpHint1": "LunaSea, gelecekte ek özelliklerle birlikte yapılandırmanızı buluta yedeklemek için ücretsiz bir hesap sunmaktadır!", - "settings.AccountHelp": "LunaSea Hesabı", - "settings.AccountDescription": "LunaSea Hesabınız", - "settings.AccountDeletedMessage": "LunaSea hesabınız silindi", - "settings.DeleteAccountWarning1": "Bu işlem geri döndürülemez", - "settings.DeleteAccountHint2": "Bu işlem, depolanan tüm bulut yedeklerini ve bu hesaba bağlı verileri de silecektir.", - "settings.AccountSettings": "Hesap Ayarları", - "settings.DeleteAccountHint1": "LunaSea hesabınızı silmek istediğinizden emin misiniz?", - "settings.DeleteAccount": "Hesabı Sil", - "settings.AccountDeleted": "Hesap Silindi", - "settings.FailedToDeleteAccount": "Hesap Silinemedi", - "settings.DeleteAccountDescription": "Hesabınızı Kalıcı Olarak Silin", - "settings.StartingView": "Başlangıç Görünümü", - "settings.DefaultSortingAndFiltering": "Öntanımlı Sıralama ve Süzme", - "settings.DeleteProfileDescription": "Mevcut Bir Profili Silin", - "settings.RenameProfileDescription": "Mevcut Bir Profili Yeniden Adlandırın", - "settings.DefaultSortingAndFilteringDescription": "Öntanımlı Sıralama ve Süzme Yöntemlerini Ayarlayın", - "settings.Add": "Ekle", - "settings.AddProfileDescription": "Yeni Bir Profil Ekleyin", - "settings.DefaultPages": "Öntanımlı Sayfalar", - "settings.DefaultPagesDescription": "Öntanımlı Açılış Sayfalarını Ayarlayın", - "settings.ClearImageCache": "Resim Önbelleğini Temizle", - "settings.ClearImageCacheHint1": "Tüm resimleri önbellekten temizlemek istediğinizden emin misiniz?", - "settings.ClearImageCacheHint2": "Büyük bir kütüphane için resimleri yeniden indirmek büyük miktarda veri tüketebilir.", - "settings.DefaultOptionsDescription": "Sıralama, Süzme ve Görüntüleme Seçeneklerini Ayarlayın", - "settings.DefaultOptions": "Öntanımlı Seçenekler", - "settings.SortDirection": "Sıralama Yönü", - "settings.FilterCategory": "Süzme Kategorisi", - "settings.SortCategory": "Sıralama Kategorisi", - "settings.Network": "Ağ", - "settings.TLSCertificateValidation": "TLS Sertifika Doğrulaması", - "settings.NetworkDescription": "Ağ Özelliklerini Özelleştirin", - "settings.TLSCertificateValidationDescription": "TLS Bağlantılarında Sertifikaları Doğrulayın", - "settings.ViewRecentChanges": "Son Değişiklikleri Görüntüle", + "dashboard.Calendar": "Takvim", + "dashboard.Wednesday": "Çarşamba", + "dashboard.TwoWeeks": "İki Hafta", + "dashboard.Tuesday": "Salı", + "dashboard.Thursday": "Perşembe", + "dashboard.Sunday": "Pazar", + "dashboard.Schedule": "Plan", + "dashboard.Saturday": "Cumartesi", + "dashboard.PastDaysDescription": "Takvim girdilerinin getirileceği geçmişteki gün sayısını ayarlayın.", + "dashboard.PastDays": "Geçmiş Günler", + "dashboard.OneWeek": "Bir Hafta", + "dashboard.OneMonth": "Bir Ay", + "dashboard.NoNewContent": "Yeni İçerik Yok", + "dashboard.Monday": "Pazartesi", + "dashboard.Modules": "Modüller", + "dashboard.MinimumOfOneDay": "En Az 1 Gün", + "dashboard.FutureDaysDescription": "Takvim girdilerinin getirileceği gelecekteki gün sayısını ayarlayın.", + "dashboard.FutureDays": "Gelecek Günler", + "dashboard.Friday": "Cuma", + "lidarr.StartSearchFor": "Aramaya Başla…", + "lidarr.StartSearchForMissingAlbums": "Eksik albümleri aramaya başla", "lunasea.Add": "Ekle", "lunasea.Alpha": "Alfa", "lunasea.AnErrorHasOccurred": "Bir Hata Oluştu", @@ -265,6 +85,14 @@ "lunasea.Update": "Güncelle", "lunasea.View": "Görünüm", "lunasea.Website": "Web Sitesi", + "overseerr.Users": "Kullanıcılar", + "overseerr.UnknownUser": "Bilinmeyen Kullanıcı", + "overseerr.NoUsersFound": "Kullanıcı Bulunamadı", + "overseerr.NoRequestsFound": "İstek Bulunamadı", + "overseerr.Requests": "İstekler", + "overseerr.OneRequest": "1 İstek", + "overseerr.SomeRequests": "{} İstek", + "overseerr.NoRequests": "İstek Yok", "radarr.AddMovie": "Film Ekle", "radarr.AddMovieAndSearch": "Ekle + Ara", "radarr.AddedTag": "Etiket Eklendi", @@ -391,103 +219,207 @@ "search.Alphabetical": "Alfabetik", "search.AllSubcategories": "Tüm Alt Kategoriler", "search.Age": "Yaş", - "dashboard.Calendar": "Takvim", - "dashboard.Wednesday": "Çarşamba", - "dashboard.TwoWeeks": "İki Hafta", - "dashboard.Tuesday": "Salı", - "dashboard.Thursday": "Perşembe", - "dashboard.Sunday": "Pazar", - "dashboard.Schedule": "Plan", - "dashboard.Saturday": "Cumartesi", - "dashboard.PastDaysDescription": "Takvim girdilerinin getirileceği geçmişteki gün sayısını ayarlayın.", - "dashboard.PastDays": "Geçmiş Günler", - "dashboard.OneWeek": "Bir Hafta", - "dashboard.OneMonth": "Bir Ay", - "dashboard.NoNewContent": "Yeni İçerik Yok", - "dashboard.Monday": "Pazartesi", - "dashboard.Modules": "Modüller", - "dashboard.MinimumOfOneDay": "En Az 1 Gün", - "dashboard.FutureDaysDescription": "Takvim girdilerinin getirileceği gelecekteki gün sayısını ayarlayın.", - "dashboard.FutureDays": "Gelecek Günler", - "dashboard.Friday": "Cuma", - "tautulli.Activity": "Etkinlik", - "tautulli.Year": "Yıl", - "tautulli.ViewWebGUI": "Web Grafiksel Kullanıcı Arayüzünü Görüntüle", - "tautulli.Video": "Video", - "tautulli.Users": "Kullanıcılar", - "tautulli.User": "Kullanıcı", - "tautulli.Transcodes": "Kod Dönüştürmeler", - "tautulli.Transcode": "Kod Dönüştürme", - "tautulli.Title": "Başlık", - "tautulli.Throttled": "Kısıldı", - "tautulli.TerminateSessionFailed": "Oturum Sonlandırılamadı", - "tautulli.TerminateSession": "Oturumu Sonlandır", - "tautulli.TerminatedSession": "Sonlandırılan Oturum", - "tautulli.TerminationMessage": "Sonlandırma Mesajı", - "tautulli.TerminationConfirmMessage": "Bu oturumu sonlandırmak istiyor musunuz?", - "tautulli.TerminationAttachMessage": "İsteğe bağlı olarak aşağıya bir sonlandırma mesajı ekleyebilirsiniz.", - "tautulli.Terminate": "Sonlandır", - "tautulli.Subtitle": "Alt Yazı", - "tautulli.Stream": "Akış", - "tautulli.SessionsMany": "{} Oturum", - "tautulli.SessionsOne": "1 Oturum", - "tautulli.Sessions": "Oturumlar", - "tautulli.SessionEnded": "Oturum Sona Erdi", - "tautulli.Season": "Sezon {}", - "tautulli.Quality": "Kalite", - "tautulli.Product": "Ürün", - "tautulli.Player": "Oynatıcı", - "tautulli.Platform": "Platform", - "tautulli.NoActiveStreams": "Etkin Akış Yok", - "tautulli.None": "Yok", - "tautulli.More": "Daha Fazla", - "tautulli.Metadata": "Üst Veri", - "tautulli.Location": "Konum", - "tautulli.Library": "Kütüphane", - "tautulli.History": "Geçmiş", - "tautulli.ETA": "Kalan Süre", - "tautulli.Episode": "Bölüm {}", - "tautulli.Duration": "Süre", - "tautulli.DirectStreams": "Doğrudan Akışlar", - "tautulli.DirectStream": "Doğrudan Akış", - "tautulli.DirectPlays": "Doğrudan Oynatmalar", - "tautulli.DirectPlay": "Doğrudan Oynatma", - "tautulli.DeletingTemporarySessionsFailed": "Geçici Oturumlar Silinemedi", - "tautulli.DeletingTemporarySessionsDescription": "Geçici oturumlar siliniyor", - "tautulli.DeletingTemporarySessions": "Geçici Oturumlar Siliniyor…", - "tautulli.DeletingImageCacheFailed": "Resim Önbelleği Silinemedi", - "tautulli.DeletingImageCacheDescription": "Tautulli resim önbelleği siliniyor", - "tautulli.DeletingImageCache": "Resim Önbelleği Siliniyor…", - "tautulli.DeletingCacheFailed": "Önbellek Silinemedi", - "tautulli.DeletingCacheDescription": "Tautulli önbelleği siliniyor", - "tautulli.DeletingCache": "Önbellek Siliniyor…", - "tautulli.DeleteTemporarySessions": "Geçici Oturumları Sil", - "tautulli.DeleteImageCache": "Resim Önbelleğini Sil", - "tautulli.DeleteCache": "Önbelleği Sil", - "tautulli.Copy": "Kopyala", - "tautulli.Container": "Konteyner", - "tautulli.Burn": "Yaz", - "tautulli.Bandwidth": "Bant Genişliği", - "tautulli.BackupDatabase": "Veri Tabanını Yedekle", - "tautulli.BackupConfiguration": "Yapılandırmayı Yedekle", - "tautulli.BackingUpDatabaseFailed": "Veri Tabanı Yedeklenemedi", - "tautulli.BackingUpDatabaseDescription": "Veri tabanınız arka planda yedekleniyor", - "tautulli.BackingUpDatabase": "Veri Tabanı Yedekleniyor…", - "tautulli.BackingUpConfigurationFailed": "Yapılandırma Yedeklenemedi", - "tautulli.BackingUpConfigurationDescription": "Yapılandırmanız arka planda yedekleniyor", - "tautulli.BackingUpConfiguration": "Yapılandırma Yedekleniyor…", - "tautulli.Audio": "Ses", - "tautulli.ActivityDetails": "Etkinlik Ayrıntıları", - "lidarr.StartSearchFor": "Aramaya Başla…", - "lidarr.StartSearchForMissingAlbums": "Eksik albümleri aramaya başla", - "overseerr.Users": "Kullanıcılar", - "overseerr.UnknownUser": "Bilinmeyen Kullanıcı", - "overseerr.NoUsersFound": "Kullanıcı Bulunamadı", - "overseerr.NoRequestsFound": "İstek Bulunamadı", - "overseerr.Requests": "İstekler", - "overseerr.OneRequest": "1 İstek", - "overseerr.SomeRequests": "{} İstek", - "overseerr.NoRequests": "İstek Yok", + "settings.Account": "Hesap", + "settings.HostHint1": "Bu, hizmet için web grafiksel kullanıcı arayüzüne eriştiğiniz URL'dir", + "settings.MustBeValueBetween": "{} ile {} arasında bir değer olmalıdır", + "settings.UsernameValidation": "Kullanıcı Adı Gerekli", + "settings.Username": "Kullanıcı Adı", + "settings.TestConnection": "Bağlantıyı Test Et", + "settings.SystemDescription": "Sistem Yardımcı Programları ve Bilgileri", + "settings.System": "Sistem", + "settings.StartingType": "Başlangıç Türü", + "settings.StartingSize": "Başlangıç Boyutu", + "settings.StartingDay": "Başlangıç Günü", + "settings.ShowCalendarEntries": "{} Takvim Girdisini Göster", + "settings.SignOutHint1": "LunaSea hesabınızdan çıkmak istediğinize emin misiniz?", + "settings.SignOut": "Oturumu Kapat", + "settings.SignIn": "Oturum Aç", + "settings.SignedOutSuccessMessage": "LunaSea hesabınızdan çıkış yaptınız", + "settings.SignedOutSuccess": "Oturum Kapatıldı", + "settings.SignedOutFailure": "Oturum Kapatılamadı", + "settings.SignedInSuccess": "Başarıyla Oturum Açıldı", + "settings.SignedInFailure": "Oturum Açılamadı", + "settings.RestoreFromCloudSuccessMessage": "Yapılandırmanız geri yüklendi", + "settings.RestoreFromCloudSuccess": "Başarıyla Geri Yüklendi", + "settings.RestoreFromCloudFailure": "Geri Yüklenemedi", + "settings.RestoreFromCloudDescription": "Yapılandırma Verilerini Geri Yükle", + "settings.RestoreFromCloud": "Buluttan Geri Yükle", + "settings.ResourcesDescription": "Faydalı Kaynaklar ve Bağlantılar", + "settings.Resources": "Kaynaklar", + "settings.ResetPassword": "Parolayı Sıfırla", + "settings.RenameProfile": "Profili Yeniden Adlandır", + "settings.RegisteredSuccess": "Kaydolundu", + "settings.RegisteredFailure": "Kaydolunamadı", + "settings.Register": "Kaydol", + "settings.QuickActionsDescription": "Ana Ekranda Hızlı Eylemler", + "settings.QuickActions": "Hızlı Eylemler", + "settings.ProfileNameRequired": "Profil Adı Gerekli", + "settings.ProfileName": "Profil Adı", + "settings.ProfileAlreadyExists": "Profil Zaten Var", + "settings.ProfilesDescription": "Profillerinizi Yönetin", + "settings.Profiles": "Profiller", + "settings.PasswordValidation": "Parola Gerekli", + "settings.Password": "Parola", + "settings.OpenLinksIn": "Bağlantıları Şununla Aç…", + "settings.NoHeadersAdded": "Başlık Eklenmedi", + "settings.NoExternalModulesFound": "Harici Modül Bulunamadı", + "settings.NoBackupsFound": "Yedekleme Bulunamadı", + "settings.NotificationsDescription": "Anlık Bildirimler için Web Kancaları Ayarlayın", + "settings.Notifications": "Bildirimler", + "settings.ModuleNotFound": "Modül Bulunamadı", + "settings.MinimumCharacters": "En az {} karakter", + "settings.MACAddressValidation": "Geçersiz MAC Adresi", + "settings.MACAddressHint4": "Her onaltılık sekizli iki nokta üst üste ile ayrılmalıdır", + "settings.MACAddressHint3": "Onaltılık basamaklar 0-9 ve A-F arasındadır", + "settings.MACAddressHint2": "MAC adresleri altı adet onaltılık basamak ikilisi (yani sekizli, octet) içerir", + "settings.MACAddressHint1": "Bu, uyandırmak istediğiniz makinenin MAC adresidir", + "settings.MACAddress": "MAC Adresi", + "settings.LocalizationDescription": "Yerel Ayarlarınıza Göre Özelleştirin", + "settings.Localization": "Yerelleştirme", + "settings.Language": "Dil", + "settings.InvalidPasswordMessage": "Parola geçersiz", + "settings.InvalidPassword": "Geçersiz Parola", + "settings.InvalidEmailMessage": "E-posta adresi geçersiz", + "settings.InvalidEmail": "Geçersiz E-posta Adresi", + "settings.ImageBackgroundOpacityHint2": "Arka plan resimlerini getirmeyi tamamen devre dışı bırakmak için değeri 0 olarak ayarlayın.", + "settings.ImageBackgroundOpacityHint1": "Arka plan resimlerinin opaklığını ayarlayın.", + "settings.ImageBackgroundOpacity": "Resim Arka Plan Opaklığı", + "settings.HostValidation": "Ana makine http:// veya https:// içermelidir", + "settings.HostRequiredMessage": "{}'e bağlanmak için ana makine gereklidir", + "settings.HostRequired": "Ana Makine Gerekli", + "settings.HostHint5": "Temel kimlik doğrulaması eklemek için lütfen özel başlıklar özelliğini kullanın", + "settings.HostHint4": "Ters vekil sunucu kullanmadığınızda, lütfen bağlantı noktasını ekleyin", + "settings.HostHint3": "localhost veya 127.0.0.1 kullanmayın", + "settings.HostHint2": "http:// veya https:// eklemelisiniz", + "settings.Host": "Ana Makine", + "settings.HeaderValueValidation": "Başlık Değeri Gerekli", + "settings.HeaderValue": "Başlık Değeri", + "settings.HeaderKeyValidation": "Başlık Anahtarı Gerekli", + "settings.HeaderKey": "Başlık Anahtarı", + "settings.HeaderDeleted": "Başlık Silindi", + "settings.HeaderAdded": "Başlık Eklendi", + "settings.ForgotYourPassword": "Parolanızı mı Unuttunuz?", + "settings.EncryptionKey": "Şifreleme Anahtarı", + "settings.EnabledProfile": "Etkin Profil", + "settings.EmailSentSuccessMessage": "Parolanızı sıfırlamak için bir e-posta gönderildi!", + "settings.EmailSentSuccess": "E-posta Gönderildi", + "settings.EmailSentFailure": "Parola Sıfırlanamadı", + "settings.Email": "E-posta", + "settings.EditModule": "Modülü Düzenle", + "settings.DrawerDescription": "Çekmeceyi Özelleştirin", + "settings.Drawer": "Çekmece", + "settings.DonationsDescription": "Geliştiriciye Bağış Yapın", + "settings.Donations": "Bağışlar", + "settings.DisplayName": "Görünen Ad", + "settings.DismissBannersHint2": "Araç ipucu afişleri, LunaSea'de bulunan özellikler için size ipuçları ve püf noktaları verecektir.", + "settings.DismissBannersHint1": "Tüm araç ipucu afişlerini kapatmak istediğinizden emin misiniz?", + "settings.DismissBanners": "Afişleri Kapat", + "settings.DeleteProfile": "Profili Sil", + "settings.DeleteModuleSuccess": "Modül Silindi", + "settings.DeleteModuleHint1": "Bu harici modülü silmek istediğinizden emin misiniz?", + "settings.DeleteModule": "Modülü Sil", + "settings.DeleteIndexerHint1": "Bu dizinleyiciyi silmek istediğinizden emin misiniz?", + "settings.DeleteIndexer": "Dizinleyiciyi Sil", + "settings.DeleteHeaderHint1": "Bu başlığı silmek istediğinizden emin misiniz?", + "settings.DeleteHeader": "Başlığı Sil", + "settings.DeleteCloudBackupSuccess": "Silindi", + "settings.DeleteCloudBackupFailure": "Silinemedi", + "settings.DeleteCloudBackupDescription": "Bir Yapılandırma Dosyasını Sil", + "settings.DeleteCloudBackup": "Bulut Yedeklemeyi Sil", + "settings.DebugMenuDescription": "Hata Ayıklama ve Geliştirme Yardımcı Programları", + "settings.DebugMenu": "Hata Ayıklama Menüsü", + "settings.DecryptBackupHint1": "Lütfen bu yedekleme için şifreleme anahtarını girin.", + "settings.DecryptBackup": "Yedeklemenin Şifresini Çöz", + "settings.DefaultPage": "Öntanımlı Sayfa", + "settings.CustomHeadersDescription": "İsteklere Özel Başlıklar Ekleyin", + "settings.CustomHeaders": "Özel Başlıklar", + "settings.CustomHeader": "Özel Başlık", + "settings.Custom": "Özel…", + "settings.ConnectionTestFailed": "Bağlantı Testi Başarısız Oldu", + "settings.ConnectionDetailsDescription": "{} için Bağlantı Ayrıntıları", + "settings.ConnectionDetails": "Bağlantı Ayrıntıları", + "settings.ConnectedSuccessfullyMessage": "{}, LunaSea ile kullanıma hazır!", + "settings.ConnectedSuccessfully": "Bağlandı", + "settings.ConfigurationDescription": "LunaSea'yi Kurun ve Yapılandırın", + "settings.Configuration": "Yapılandırma", + "settings.ConfigureModule": "{} Yapılandır", + "settings.ClearLogsHint1": "Tüm kayıtlı günlükleri silmek istediğinizden emin misiniz?\n\nGünlükler, hata bildirme ve hata ayıklama için faydalı olabilir.", + "settings.ClearLogs": "Günlük Kayıtlarını Temizle", + "settings.ClearConfigurationHint3": "Bir LunaSea hesabında oturum açtıysanız, oturumunuz kapatılacak.", + "settings.ClearConfigurationHint2": "Temiz bir sayfadan başlayacaksınız, lütfen önce geçerli yapılandırmanızı yedeklediğinizden emin olun!", + "settings.ClearConfigurationHint1": "Yapılandırmanızı temizlemek istediğinizden emin misiniz?", + "settings.ClearConfiguration": "Yapılandırmayı Temizle", + "settings.BroadcastAddressValidation": "Geçersiz Genel Yayın Adresi", + "settings.BroadcastAddressHint3": "Örneğin, makinenin IP adresi 192.168.1.111 ise, elde edilen genel yayın IP adresi 192.168.1.255 olur", + "settings.BroadcastAddressHint2": "Genellikle bu, makinenizin IP adresinin son kısmının 255 olarak değiştirilmiş halidir", + "settings.BroadcastAddressHint1": "Bu, yerel ağınızın genel yayın adresidir", + "settings.BroadcastAddress": "Genel Yayın Adresi", + "settings.BasicAuthenticationHint3": "Kullanıcı adı ve parola otomatik olarak base64 kodlamasına dönüştürülür", + "settings.BasicAuthenticationHint2": "Parola iki nokta üst üste içerebilir", + "settings.BasicAuthenticationHint1": "Kullanıcı adı iki nokta üst üste içermemelidir", + "settings.BasicAuthentication": "Temel Kimlik Doğrulaması", + "settings.BannersNotificationModuleSupportBody": "Web kancası tabanlı bildirimler şu anda yalnızca aşağıda listelenen modüllerde desteklenmektedir.\n\nEk modül desteği ileride gelecek!", + "settings.BannersNotificationModuleSupportHeader": "Desteklenen Modüller", + "settings.BackupToCloudSuccess": "Başarıyla Yedeklendi", + "settings.BackupToCloudFailure": "Yedeklenemedi", + "settings.BackupToCloudDescription": "Yapılandırma Verilerini Yedekle", + "settings.BackupToCloud": "Buluta Yedekle", + "settings.BackupList": "Yedekleme Listesi", + "settings.BackupConfigurationHint2": "Şifreleme anahtarı en az 8 karakter olmalıdır", + "settings.BackupConfigurationHint1": "Tüm yedeklemeler dışa aktarılmadan önce şifrelenmektedir", + "settings.BackupConfiguration": "Yapılandırmayı Yedekle", + "settings.BackgroundImageOpacity": "Arka Plan Resmi Opaklığı", + "settings.AutomaticallyManageOrderDescription": "Modülleri Alfabetik Olarak Listeleyin", + "settings.AutomaticallyManageOrder": "Sıralamayı Otomatik Olarak Yönet", + "settings.AppearanceDescription": "Görünümü Özelleştirin", + "settings.Appearance": "Görünüm", + "settings.ApiKeyRequiredMessage": "{}'e bağlanmak için API anahtarı gereklidir", + "settings.ApiKeyRequired": "API Anahtarı Gerekli", + "settings.ApiKey": "API Anahtarı", + "settings.AmoledThemeDescription": "Saf Siyah Koyu Tema", + "settings.AmoledThemeBordersDescription": "Kullanıcı Arayüzü Boyunca İnce Kenarlıklar Ekleyin", + "settings.AmoledThemeBorders": "AMOLED Tema Kenarlıkları", + "settings.AmoledTheme": "AMOLED Tema", + "settings.AllFieldsAreRequired": "Tüm alanlar gereklidir", + "settings.AddProfile": "Profil Ekle", + "settings.AddModuleSuccess": "Modül Eklendi", + "settings.AddModuleFailed": "Modül Eklenemedi", + "settings.AddModule": "Modül Ekle", + "settings.AddHeader": "Başlık Ekle", + "settings.AccountHelpHint1": "LunaSea, gelecekte ek özelliklerle birlikte yapılandırmanızı buluta yedeklemek için ücretsiz bir hesap sunmaktadır!", + "settings.AccountHelp": "LunaSea Hesabı", + "settings.AccountDescription": "LunaSea Hesabınız", + "settings.AccountDeletedMessage": "LunaSea hesabınız silindi", + "settings.DeleteAccountWarning1": "Bu işlem geri döndürülemez", + "settings.DeleteAccountHint2": "Bu işlem, depolanan tüm bulut yedeklerini ve bu hesaba bağlı verileri de silecektir.", + "settings.AccountSettings": "Hesap Ayarları", + "settings.DeleteAccountHint1": "LunaSea hesabınızı silmek istediğinizden emin misiniz?", + "settings.DeleteAccount": "Hesabı Sil", + "settings.AccountDeleted": "Hesap Silindi", + "settings.FailedToDeleteAccount": "Hesap Silinemedi", + "settings.DeleteAccountDescription": "Hesabınızı Kalıcı Olarak Silin", + "settings.StartingView": "Başlangıç Görünümü", + "settings.DefaultSortingAndFiltering": "Öntanımlı Sıralama ve Süzme", + "settings.DeleteProfileDescription": "Mevcut Bir Profili Silin", + "settings.RenameProfileDescription": "Mevcut Bir Profili Yeniden Adlandırın", + "settings.DefaultSortingAndFilteringDescription": "Öntanımlı Sıralama ve Süzme Yöntemlerini Ayarlayın", + "settings.Add": "Ekle", + "settings.AddProfileDescription": "Yeni Bir Profil Ekleyin", + "settings.DefaultPages": "Öntanımlı Sayfalar", + "settings.DefaultPagesDescription": "Öntanımlı Açılış Sayfalarını Ayarlayın", + "settings.ClearImageCache": "Resim Önbelleğini Temizle", + "settings.ClearImageCacheHint1": "Tüm resimleri önbellekten temizlemek istediğinizden emin misiniz?", + "settings.ClearImageCacheHint2": "Büyük bir kütüphane için resimleri yeniden indirmek büyük miktarda veri tüketebilir.", + "settings.DefaultOptionsDescription": "Sıralama, Süzme ve Görüntüleme Seçeneklerini Ayarlayın", + "settings.DefaultOptions": "Öntanımlı Seçenekler", + "settings.SortDirection": "Sıralama Yönü", + "settings.FilterCategory": "Süzme Kategorisi", + "settings.SortCategory": "Sıralama Kategorisi", + "settings.Network": "Ağ", + "settings.TLSCertificateValidation": "TLS Sertifika Doğrulaması", + "settings.NetworkDescription": "Ağ Özelliklerini Özelleştirin", + "settings.TLSCertificateValidationDescription": "TLS Bağlantılarında Sertifikaları Doğrulayın", + "settings.ViewRecentChanges": "Son Değişiklikleri Görüntüle", "sonarr.AddedSeries": "Eklenen Diziler", "sonarr.Age": "Yıl", "sonarr.All": "Her Şey", @@ -695,5 +627,73 @@ "sonarr.UpdatingLibraryDescription": "Arka planda kitaplığı güncelleme", "sonarr.UpdatedSeries": "Güncellenen Diziler", "sonarr.UseSeasonFolders": "Sezon Klasörlerini Kullan", - "sonarr.ViewWebGUI": "Web Arayüzü Görüntüle" + "sonarr.ViewWebGUI": "Web Arayüzü Görüntüle", + "tautulli.Activity": "Etkinlik", + "tautulli.Year": "Yıl", + "tautulli.ViewWebGUI": "Web Grafiksel Kullanıcı Arayüzünü Görüntüle", + "tautulli.Video": "Video", + "tautulli.Users": "Kullanıcılar", + "tautulli.User": "Kullanıcı", + "tautulli.Transcodes": "Kod Dönüştürmeler", + "tautulli.Transcode": "Kod Dönüştürme", + "tautulli.Title": "Başlık", + "tautulli.Throttled": "Kısıldı", + "tautulli.TerminateSessionFailed": "Oturum Sonlandırılamadı", + "tautulli.TerminateSession": "Oturumu Sonlandır", + "tautulli.TerminatedSession": "Sonlandırılan Oturum", + "tautulli.TerminationMessage": "Sonlandırma Mesajı", + "tautulli.TerminationConfirmMessage": "Bu oturumu sonlandırmak istiyor musunuz?", + "tautulli.TerminationAttachMessage": "İsteğe bağlı olarak aşağıya bir sonlandırma mesajı ekleyebilirsiniz.", + "tautulli.Terminate": "Sonlandır", + "tautulli.Subtitle": "Alt Yazı", + "tautulli.Stream": "Akış", + "tautulli.SessionsMany": "{} Oturum", + "tautulli.SessionsOne": "1 Oturum", + "tautulli.Sessions": "Oturumlar", + "tautulli.SessionEnded": "Oturum Sona Erdi", + "tautulli.Season": "Sezon {}", + "tautulli.Quality": "Kalite", + "tautulli.Product": "Ürün", + "tautulli.Player": "Oynatıcı", + "tautulli.Platform": "Platform", + "tautulli.NoActiveStreams": "Etkin Akış Yok", + "tautulli.None": "Yok", + "tautulli.More": "Daha Fazla", + "tautulli.Metadata": "Üst Veri", + "tautulli.Location": "Konum", + "tautulli.Library": "Kütüphane", + "tautulli.History": "Geçmiş", + "tautulli.ETA": "Kalan Süre", + "tautulli.Episode": "Bölüm {}", + "tautulli.Duration": "Süre", + "tautulli.DirectStreams": "Doğrudan Akışlar", + "tautulli.DirectStream": "Doğrudan Akış", + "tautulli.DirectPlays": "Doğrudan Oynatmalar", + "tautulli.DirectPlay": "Doğrudan Oynatma", + "tautulli.DeletingTemporarySessionsFailed": "Geçici Oturumlar Silinemedi", + "tautulli.DeletingTemporarySessionsDescription": "Geçici oturumlar siliniyor", + "tautulli.DeletingTemporarySessions": "Geçici Oturumlar Siliniyor…", + "tautulli.DeletingImageCacheFailed": "Resim Önbelleği Silinemedi", + "tautulli.DeletingImageCacheDescription": "Tautulli resim önbelleği siliniyor", + "tautulli.DeletingImageCache": "Resim Önbelleği Siliniyor…", + "tautulli.DeletingCacheFailed": "Önbellek Silinemedi", + "tautulli.DeletingCacheDescription": "Tautulli önbelleği siliniyor", + "tautulli.DeletingCache": "Önbellek Siliniyor…", + "tautulli.DeleteTemporarySessions": "Geçici Oturumları Sil", + "tautulli.DeleteImageCache": "Resim Önbelleğini Sil", + "tautulli.DeleteCache": "Önbelleği Sil", + "tautulli.Copy": "Kopyala", + "tautulli.Container": "Konteyner", + "tautulli.Burn": "Yaz", + "tautulli.Bandwidth": "Bant Genişliği", + "tautulli.BackupDatabase": "Veri Tabanını Yedekle", + "tautulli.BackupConfiguration": "Yapılandırmayı Yedekle", + "tautulli.BackingUpDatabaseFailed": "Veri Tabanı Yedeklenemedi", + "tautulli.BackingUpDatabaseDescription": "Veri tabanınız arka planda yedekleniyor", + "tautulli.BackingUpDatabase": "Veri Tabanı Yedekleniyor…", + "tautulli.BackingUpConfigurationFailed": "Yapılandırma Yedeklenemedi", + "tautulli.BackingUpConfigurationDescription": "Yapılandırmanız arka planda yedekleniyor", + "tautulli.BackingUpConfiguration": "Yapılandırma Yedekleniyor…", + "tautulli.Audio": "Ses", + "tautulli.ActivityDetails": "Etkinlik Ayrıntıları" } \ No newline at end of file diff --git a/assets/localization/zh-Hans.json b/assets/localization/zh-Hans.json index 19bc7c422a..2d1015c51a 100644 --- a/assets/localization/zh-Hans.json +++ b/assets/localization/zh-Hans.json @@ -1,205 +1,25 @@ { - "settings.AccountDescription": "账户描述", - "settings.AccountHelp": "账户帮助", - "settings.AddModule": "添加模块", - "settings.AddModuleFailed": "添加模块失败", - "settings.AddModuleSuccess": "增加模块", - "settings.AddProfile": "添加配置文件", - "settings.AllFieldsAreRequired": "所有资料必需填写", - "settings.AmoledTheme": "AMOLED主题", - "settings.AmoledThemeBorders": "AMOLED主题边框", - "settings.AmoledThemeBordersDescription": "在UI中添加细微的边框", - "settings.AmoledThemeDescription": "纯黑暗主题", - "settings.AddHeader": "添加标题", - "settings.ApiKeyRequiredMessage": "需要API密钥才能连接到 {}", - "settings.Appearance": "外观", - "settings.AppearanceDescription": "外观描述", - "settings.AutomaticallyManageOrder": "自动排序", - "settings.AutomaticallyManageOrderDescription": "按字母顺序列出模块", - "settings.BackupConfiguration": "备份配置", - "settings.BackupConfigurationHint1": "所有备份在导出之前加密", - "settings.BackupConfigurationHint2": "加密密钥必须至少为8个字符", - "settings.BackupList": "备份列表", - "settings.BackupToCloud": "备份至云端", - "settings.BackupToCloudDescription": "备份配置数据", - "settings.BackupToCloudFailure": "备份失败", - "settings.BackupToCloudSuccess": "备份成功", - "settings.BasicAuthentication": "基础认证", - "settings.BasicAuthenticationHint1": "用户名不能包含冒号", - "settings.BasicAuthenticationHint2": "密码可以包含冒号", - "settings.BasicAuthenticationHint3": "用户名和密码将自动转换为base64编码", - "settings.BroadcastAddress": "广播地址", - "settings.BroadcastAddressHint1": "这是本地网络的广播地址", - "settings.BroadcastAddressHint2": "通常这是您机器的 IP 地址,最后一个八位字节设置为 255", - "settings.BroadcastAddressHint3": "给定一个示例机器 IP 地址 192.168.1.111,得到的广播 IP 地址是 192.168.1.255", - "settings.BroadcastAddressValidation": "无效的广播地址", - "settings.ClearConfiguration": "清除配置", - "settings.ClearConfigurationHint1": "确定要清除您的配置吗?", - "settings.ClearConfigurationHint3": "如果您登录到LunaSea帐户,您将被注销。", - "settings.ClearLogs": "清除日志", - "settings.ClearLogsHint1": "是否确定要清除所有记录的日志?\n\n日志对于错误报告和调试非常有用。", - "settings.ConfigureModule": "设定 {}", - "settings.Configuration": "配置", - "settings.ConfigurationDescription": "配置和设置 LunaSea", - "settings.ConnectedSuccessfully": "连接的", - "settings.ConnectedSuccessfullyMessage": "{} 已准备好与 LunaSea 一起使用!", - "settings.ConnectionDetails": "连接详情", - "settings.ConnectionDetailsDescription": "{} 的连接详情", - "settings.ConnectionTestFailed": "连接测试失败", - "settings.Custom": "自定义…", - "settings.CustomHeader": "自定义请求头", - "settings.CustomHeaders": "自定义消息头", - "settings.CustomHeadersDescription": "向请求添加自定义消息头", - "settings.DefaultPage": "默认页", - "settings.DecryptBackup": "解密备份", - "settings.DebugMenu": "调试菜单", - "settings.DebugMenuDescription": "调试和开发实用程序", - "settings.DeleteCloudBackup": "删除云端备份", - "settings.DeleteCloudBackupFailure": "删除失败", - "settings.DeleteCloudBackupSuccess": "已删除", - "settings.DeleteIndexer": "删除索引器", - "settings.DeleteIndexerHint1": "您确定要删除此索引器吗?", - "settings.DeleteModule": "删除模块", - "settings.DeleteModuleHint1": "您确定要删除此外部模块吗?", - "settings.DeleteModuleSuccess": "模块已删除", - "settings.DeleteProfile": "删除配置文件", - "settings.DismissBannersHint1": "您确定要关闭所有工具提示横幅吗?", - "settings.DisplayName": "显示名称", - "settings.Donations": "捐款", - "settings.DonationsDescription": "捐赠给开发商", - "settings.EditModule": "编辑模块", - "settings.Email": "电子邮件", - "settings.EmailSentFailure": "重置密码失败", - "settings.EmailSentSuccess": "邮件已发送", - "settings.EmailSentSuccessMessage": "重置密码的电子邮件已发送!", - "settings.EnabledProfile": "启用配置文件", - "settings.EncryptionKey": "加密密钥", - "settings.Drawer": "抽屉", - "settings.DrawerDescription": "自定义抽屉", - "settings.DeleteHeaderHint1": "您确定要删除此标题吗?", - "settings.DeleteHeader": "删除标题", - "settings.HostHint1": "这是您访问服务的 Web GUI 的 URL", - "settings.HostHint2": "您必须包含 http:// 或 https://", - "settings.HostHint4": "不使用反向代理时,请包括端口", - "settings.HostHint5": "要添加基本身份验证,请使用自定义标题功能", - "settings.HostRequired": "需要主机", - "settings.ImageBackgroundOpacity": "图像背景不透明度", - "settings.ImageBackgroundOpacityHint1": "设置背景图像的不透明度。", - "settings.ImageBackgroundOpacityHint2": "要完全禁用获取背景图像,请将值设置为 0。", - "settings.InvalidEmail": "无效的邮件地址", - "settings.InvalidEmailMessage": "电子邮件地址无效", - "settings.InvalidPasswordMessage": "密码无效", - "settings.Language": "语言", - "settings.Localization": "本地化", - "settings.LocalizationDescription": "自定义您的语言环境", - "settings.MACAddress": "MAC地址", - "settings.MACAddressHint1": "这是你要唤醒的机器的MAC地址", - "settings.MACAddressHint2": "MAC 地址包含六个两位数的十六进制半字节(一个八位字节)", - "settings.MACAddressValidation": "无效的 MAC 地址", - "settings.MinimumCharacters": "最少 {}个字符", - "settings.ModuleNotFound": "未找到模块", - "settings.Notifications": "通知", - "settings.NotificationsDescription": "为推送通知设置 Webhook", - "settings.NoBackupsFound": "未找到备份", - "settings.NoExternalModulesFound": "未找到外部模块", - "settings.NoHeadersAdded": "未添加响应头", - "settings.OpenLinksIn": "打开链接…", - "settings.Password": "密码", - "settings.PasswordValidation": "需要密码", - "settings.Profiles": "简介", - "settings.ProfilesDescription": "管理您的个人资料", - "settings.ProfileAlreadyExists": "配置文件已存在", - "settings.ProfileName": "个人资料名称", - "settings.HeaderAdded": "标题已添加", - "settings.HeaderDeleted": "标题已删除", - "settings.HeaderKey": "标题键", - "settings.HeaderValueValidation": "需要标题值", - "settings.Host": "主机", - "settings.ProfileNameRequired": "需要配置文件名称", - "settings.Register": "注册", - "settings.RegisteredFailure": "注册失败", - "settings.RegisteredSuccess": "注册", - "settings.RenameProfile": "重命名配置文件", - "settings.ResetPassword": "重置密码", - "settings.Resources": "资源", - "settings.ResourcesDescription": "有用的资源和链接", - "settings.RestoreFromCloudDescription": "恢复配置数据", - "settings.MustBeValueBetween": "必须是 {} 和 {} 之间的值", - "settings.RestoreFromCloudFailure": "无法恢复", - "settings.RestoreFromCloudSuccess": "恢复成功", - "settings.SignedInFailure": "登录失败", - "settings.SignedInSuccess": "登录成功", - "settings.SignedOutFailure": "退出失败", - "settings.SignedOutSuccess": "退出", - "settings.SignedOutSuccessMessage": "退出您的 LunaSea 帐户", - "settings.SignIn": "登入", - "settings.SignOut": "退出", - "settings.SignOutHint1": "您确定要退出您的 LunaSea 帐户吗?", - "settings.StartingDay": "开始日", - "settings.StartingSize": "起始大小", - "settings.StartingType": "起始类型", - "settings.System": "系统", - "settings.SystemDescription": "系统实用程序和信息", - "settings.TestConnection": "测试连接", - "settings.Username": "用户名", - "settings.UsernameValidation": "需要用户名", - "settings.Account": "账户", - "settings.AccountHelpHint1": "LunaSea 提供了一个免费帐户来将您的配置备份到云端,未来还会有更多功能!", - "settings.ApiKey": "API密钥", - "settings.ApiKeyRequired": "需要API密钥", - "settings.DismissBannersHint2": "工具提示横幅将为您提供有关 LunaSea 中可用功能的提示和提示。", - "settings.BackgroundImageOpacity": "背景图像不透明度", - "settings.BannersNotificationModuleSupportHeader": "支持的模块", - "settings.BannersNotificationModuleSupportBody": "当前仅在下面列出的模块中支持基于 Webhook 的通知。\n\n将来会提供额外的模块支持!", - "settings.ClearConfigurationHint2": "您将从头开始,请确保备份当前配置!", - "settings.DismissBanners": "关闭横幅", - "settings.DecryptBackupHint1": "请输入此备份的加密密钥。", - "settings.DeleteCloudBackupDescription": "删除配置文件", - "settings.ForgotYourPassword": "忘记密码了?", - "settings.HostHint3": "不要使用本地主机或 127.0.0.1", - "settings.InvalidPassword": "无效的密码", - "settings.HostRequiredMessage": "主机需要连接到 {}", - "settings.HostValidation": "主机必须包含 http:// 或 https://", - "settings.MACAddressHint3": "十六进制数字范围从 0-9 和 A-F", - "settings.ShowCalendarEntries": "显示 {} 日历条目", - "settings.QuickActionsDescription": "主屏幕上的快速操作", - "settings.MACAddressHint4": "每个十六进制八位字节应由冒号分隔", - "settings.QuickActions": "快速操作", - "settings.RestoreFromCloudSuccessMessage": "您的配置已恢复", - "settings.RestoreFromCloud": "从云端恢复", - "settings.HeaderKeyValidation": "需要标题键", - "settings.HeaderValue": "标题值", - "settings.AccountSettings": "帐户设置", - "settings.DeleteAccountHint2": "此过程还将删除所有存储的云备份和附加到此帐户的数据。", - "settings.DeleteAccountHint1": "您确定要删除您的 LunaSea 帐户吗?", - "settings.AccountDeleted": "帐户已删除", - "settings.AccountDeletedMessage": "删除您的 LunaSea 帐户", - "settings.DeleteAccount": "删除帐户", - "settings.DeleteAccountDescription": "永久删除您的帐户", - "settings.DeleteAccountWarning1": "这个过程不可逆", - "settings.FailedToDeleteAccount": "删除账户失败", - "settings.StartingView": "开始视图", - "settings.DefaultPages": "默认页", - "settings.DefaultPagesDescription": "设置默认登录页", - "settings.DefaultSortingAndFiltering": "默认排序和过滤", - "settings.DefaultSortingAndFilteringDescription": "设置默认排序和过滤方法", - "settings.DeleteProfileDescription": "删除现有配置文件", - "settings.RenameProfileDescription": "重命名现有配置文件", - "settings.Add": "添加", - "settings.AddProfileDescription": "添加新配置文件", - "settings.ClearImageCacheHint2": "为大型库重新下载图像可能会消耗大量数据。", - "settings.ClearImageCache": "清除图像缓存", - "settings.ClearImageCacheHint1": "您确定要清除缓存中的所有图像吗?", - "settings.DefaultOptions": "默认选项", - "settings.SortCategory": "排序类别", - "settings.DefaultOptionsDescription": "设置排序、过滤和查看选项", - "settings.SortDirection": "排序方向", - "settings.FilterCategory": "过滤器类别", - "settings.Network": "网络", - "settings.TLSCertificateValidationDescription": "验证 TLS 连接中的证书", - "settings.NetworkDescription": "自定义网络功能", - "settings.TLSCertificateValidation": "TLS 证书验证", - "settings.ViewRecentChanges": "查看最近的更改", + "dashboard.Calendar": "日历", + "dashboard.Friday": "星期五", + "dashboard.FutureDays": "未来的日子", + "dashboard.FutureDaysDescription": "设置未来要获取日历条目的天数。", + "dashboard.MinimumOfOneDay": "最少 1 天", + "dashboard.Modules": "模块", + "dashboard.Monday": "星期一", + "dashboard.NoNewContent": "没有新内容", + "dashboard.OneMonth": "一个月", + "dashboard.OneWeek": "一周", + "dashboard.PastDays": "过去的日子", + "dashboard.Saturday": "星期六", + "dashboard.Schedule": "星期六", + "dashboard.Sunday": "星期天", + "dashboard.Thursday": "星期四", + "dashboard.TwoWeeks": "两周", + "dashboard.Wednesday": "星期三", + "dashboard.PastDaysDescription": "设置过去的天数以获取日历条目。", + "dashboard.Tuesday": "星期二", + "lidarr.StartSearchFor": "开始搜索…", + "lidarr.StartSearchForMissingAlbums": "开始搜索丢失的专辑", "lunasea.Add": "添加", "lunasea.Alpha": "预览版", "lunasea.AnErrorHasOccurred": "发生了错误", @@ -265,6 +85,14 @@ "lunasea.Update": "更新", "lunasea.View": "视图", "lunasea.Website": "网站", + "overseerr.NoRequests": "没有请求", + "overseerr.OneRequest": "1个请求", + "overseerr.SomeRequests": "{} 请求", + "overseerr.Requests": "请求", + "overseerr.NoRequestsFound": "未找到请求", + "overseerr.NoUsersFound": "未找到相应的用户", + "overseerr.UnknownUser": "未知用户", + "overseerr.Users": "用户", "radarr.AddMovie": "添加电影", "radarr.AddMovieAndSearch": "添加搜索", "radarr.AddedTag": "添加标签", @@ -391,103 +219,207 @@ "search.Subcategories": "子类别", "search.Age": "年龄", "search.Download": "下载", - "dashboard.Calendar": "日历", - "dashboard.Friday": "星期五", - "dashboard.FutureDays": "未来的日子", - "dashboard.FutureDaysDescription": "设置未来要获取日历条目的天数。", - "dashboard.MinimumOfOneDay": "最少 1 天", - "dashboard.Modules": "模块", - "dashboard.Monday": "星期一", - "dashboard.NoNewContent": "没有新内容", - "dashboard.OneMonth": "一个月", - "dashboard.OneWeek": "一周", - "dashboard.PastDays": "过去的日子", - "dashboard.Saturday": "星期六", - "dashboard.Schedule": "星期六", - "dashboard.Sunday": "星期天", - "dashboard.Thursday": "星期四", - "dashboard.TwoWeeks": "两周", - "dashboard.Wednesday": "星期三", - "dashboard.PastDaysDescription": "设置过去的天数以获取日历条目。", - "dashboard.Tuesday": "星期二", - "tautulli.Activity": "活动", - "tautulli.Audio": "音频", - "tautulli.BackingUpConfiguration": "正在备份配置…", - "tautulli.BackingUpConfigurationDescription": "在后台备份您的配置", - "tautulli.BackingUpConfigurationFailed": "备份配置失败", - "tautulli.BackingUpDatabase": "正在备份数据库…", - "tautulli.BackingUpDatabaseDescription": "在后台备份您的数据库", - "tautulli.BackingUpDatabaseFailed": "备份数据库失败", - "tautulli.BackupConfiguration": "备份配置", - "tautulli.BackupDatabase": "备份数据库", - "tautulli.Bandwidth": "带宽", - "tautulli.Burn": "燃烧", - "tautulli.Container": "容器", - "tautulli.Copy": "复制", - "tautulli.DeleteCache": "删除缓存", - "tautulli.DeleteImageCache": "删除图像缓存", - "tautulli.DeleteTemporarySessions": "删除临时会话", - "tautulli.DeletingCache": "正在删除缓存…", - "tautulli.DeletingCacheDescription": "正在删除 Tautulli 缓存", - "tautulli.DeletingCacheFailed": "删除缓存失败", - "tautulli.DeletingImageCacheDescription": "正在删除 Tautulli 图片缓存", - "tautulli.ActivityDetails": "活动详情", - "tautulli.DeletingImageCacheFailed": "删除图片缓存失败", - "tautulli.DeletingTemporarySessionsDescription": "正在删除临时会话", - "tautulli.DeletingTemporarySessionsFailed": "删除临时会话失败", - "tautulli.DirectPlay": "直接播放", - "tautulli.DirectPlays": "直接播放", - "tautulli.DirectStream": "直播", - "tautulli.DirectStreams": "直接流", - "tautulli.Duration": "持续时间", - "tautulli.Episode": "剧集{}", - "tautulli.ETA": "预计到达时间", - "tautulli.History": "历史", - "tautulli.Library": "图书馆", - "tautulli.Location": "定位", - "tautulli.Metadata": "元数据", - "tautulli.More": "更多", - "tautulli.None": "没有", - "tautulli.Platform": "平台", - "tautulli.Player": "播放器", - "tautulli.Product": "产品", - "tautulli.Quality": "画质", - "tautulli.SessionEnded": "会话结束", - "tautulli.Sessions": "会话", - "tautulli.SessionsOne": "1 节", - "tautulli.SessionsMany": "{} 会话", - "tautulli.Stream": "流", - "tautulli.Terminate": "终止", - "tautulli.TerminationAttachMessage": "您可以选择在下方附加终止消息。", - "tautulli.TerminationConfirmMessage": "您要终止此会话吗?", - "tautulli.TerminationMessage": "终止消息", - "tautulli.TerminatedSession": "终止的会话", - "tautulli.TerminateSession": "终止会话", - "tautulli.Throttled": "节流", - "tautulli.Title": "标题", - "tautulli.Transcode": "转码", - "tautulli.Transcodes": "转码", - "tautulli.User": "用户", - "tautulli.Users": "使用者", - "tautulli.Video": "视频", - "tautulli.ViewWebGUI": "网页图形用户界面", - "tautulli.Year": "年", - "tautulli.Subtitle": "副标题", - "tautulli.DeletingImageCache": "正在删除图像缓存…", - "tautulli.DeletingTemporarySessions": "正在删除临时会话…", - "tautulli.NoActiveStreams": "没有活动流", - "tautulli.Season": "季 {}", - "tautulli.TerminateSessionFailed": "终止会话失败", - "lidarr.StartSearchFor": "开始搜索…", - "lidarr.StartSearchForMissingAlbums": "开始搜索丢失的专辑", - "overseerr.NoRequests": "没有请求", - "overseerr.OneRequest": "1个请求", - "overseerr.SomeRequests": "{} 请求", - "overseerr.Requests": "请求", - "overseerr.NoRequestsFound": "未找到请求", - "overseerr.NoUsersFound": "未找到相应的用户", - "overseerr.UnknownUser": "未知用户", - "overseerr.Users": "用户", + "settings.AccountDescription": "账户描述", + "settings.AccountHelp": "账户帮助", + "settings.AddModule": "添加模块", + "settings.AddModuleFailed": "添加模块失败", + "settings.AddModuleSuccess": "增加模块", + "settings.AddProfile": "添加配置文件", + "settings.AllFieldsAreRequired": "所有资料必需填写", + "settings.AmoledTheme": "AMOLED主题", + "settings.AmoledThemeBorders": "AMOLED主题边框", + "settings.AmoledThemeBordersDescription": "在UI中添加细微的边框", + "settings.AmoledThemeDescription": "纯黑暗主题", + "settings.AddHeader": "添加标题", + "settings.ApiKeyRequiredMessage": "需要API密钥才能连接到 {}", + "settings.Appearance": "外观", + "settings.AppearanceDescription": "外观描述", + "settings.AutomaticallyManageOrder": "自动排序", + "settings.AutomaticallyManageOrderDescription": "按字母顺序列出模块", + "settings.BackupConfiguration": "备份配置", + "settings.BackupConfigurationHint1": "所有备份在导出之前加密", + "settings.BackupConfigurationHint2": "加密密钥必须至少为8个字符", + "settings.BackupList": "备份列表", + "settings.BackupToCloud": "备份至云端", + "settings.BackupToCloudDescription": "备份配置数据", + "settings.BackupToCloudFailure": "备份失败", + "settings.BackupToCloudSuccess": "备份成功", + "settings.BasicAuthentication": "基础认证", + "settings.BasicAuthenticationHint1": "用户名不能包含冒号", + "settings.BasicAuthenticationHint2": "密码可以包含冒号", + "settings.BasicAuthenticationHint3": "用户名和密码将自动转换为base64编码", + "settings.BroadcastAddress": "广播地址", + "settings.BroadcastAddressHint1": "这是本地网络的广播地址", + "settings.BroadcastAddressHint2": "通常这是您机器的 IP 地址,最后一个八位字节设置为 255", + "settings.BroadcastAddressHint3": "给定一个示例机器 IP 地址 192.168.1.111,得到的广播 IP 地址是 192.168.1.255", + "settings.BroadcastAddressValidation": "无效的广播地址", + "settings.ClearConfiguration": "清除配置", + "settings.ClearConfigurationHint1": "确定要清除您的配置吗?", + "settings.ClearConfigurationHint3": "如果您登录到LunaSea帐户,您将被注销。", + "settings.ClearLogs": "清除日志", + "settings.ClearLogsHint1": "是否确定要清除所有记录的日志?\n\n日志对于错误报告和调试非常有用。", + "settings.ConfigureModule": "设定 {}", + "settings.Configuration": "配置", + "settings.ConfigurationDescription": "配置和设置 LunaSea", + "settings.ConnectedSuccessfully": "连接的", + "settings.ConnectedSuccessfullyMessage": "{} 已准备好与 LunaSea 一起使用!", + "settings.ConnectionDetails": "连接详情", + "settings.ConnectionDetailsDescription": "{} 的连接详情", + "settings.ConnectionTestFailed": "连接测试失败", + "settings.Custom": "自定义…", + "settings.CustomHeader": "自定义请求头", + "settings.CustomHeaders": "自定义消息头", + "settings.CustomHeadersDescription": "向请求添加自定义消息头", + "settings.DefaultPage": "默认页", + "settings.DecryptBackup": "解密备份", + "settings.DebugMenu": "调试菜单", + "settings.DebugMenuDescription": "调试和开发实用程序", + "settings.DeleteCloudBackup": "删除云端备份", + "settings.DeleteCloudBackupFailure": "删除失败", + "settings.DeleteCloudBackupSuccess": "已删除", + "settings.DeleteIndexer": "删除索引器", + "settings.DeleteIndexerHint1": "您确定要删除此索引器吗?", + "settings.DeleteModule": "删除模块", + "settings.DeleteModuleHint1": "您确定要删除此外部模块吗?", + "settings.DeleteModuleSuccess": "模块已删除", + "settings.DeleteProfile": "删除配置文件", + "settings.DismissBannersHint1": "您确定要关闭所有工具提示横幅吗?", + "settings.DisplayName": "显示名称", + "settings.Donations": "捐款", + "settings.DonationsDescription": "捐赠给开发商", + "settings.EditModule": "编辑模块", + "settings.Email": "电子邮件", + "settings.EmailSentFailure": "重置密码失败", + "settings.EmailSentSuccess": "邮件已发送", + "settings.EmailSentSuccessMessage": "重置密码的电子邮件已发送!", + "settings.EnabledProfile": "启用配置文件", + "settings.EncryptionKey": "加密密钥", + "settings.Drawer": "抽屉", + "settings.DrawerDescription": "自定义抽屉", + "settings.DeleteHeaderHint1": "您确定要删除此标题吗?", + "settings.DeleteHeader": "删除标题", + "settings.HostHint1": "这是您访问服务的 Web GUI 的 URL", + "settings.HostHint2": "您必须包含 http:// 或 https://", + "settings.HostHint4": "不使用反向代理时,请包括端口", + "settings.HostHint5": "要添加基本身份验证,请使用自定义标题功能", + "settings.HostRequired": "需要主机", + "settings.ImageBackgroundOpacity": "图像背景不透明度", + "settings.ImageBackgroundOpacityHint1": "设置背景图像的不透明度。", + "settings.ImageBackgroundOpacityHint2": "要完全禁用获取背景图像,请将值设置为 0。", + "settings.InvalidEmail": "无效的邮件地址", + "settings.InvalidEmailMessage": "电子邮件地址无效", + "settings.InvalidPasswordMessage": "密码无效", + "settings.Language": "语言", + "settings.Localization": "本地化", + "settings.LocalizationDescription": "自定义您的语言环境", + "settings.MACAddress": "MAC地址", + "settings.MACAddressHint1": "这是你要唤醒的机器的MAC地址", + "settings.MACAddressHint2": "MAC 地址包含六个两位数的十六进制半字节(一个八位字节)", + "settings.MACAddressValidation": "无效的 MAC 地址", + "settings.MinimumCharacters": "最少 {}个字符", + "settings.ModuleNotFound": "未找到模块", + "settings.Notifications": "通知", + "settings.NotificationsDescription": "为推送通知设置 Webhook", + "settings.NoBackupsFound": "未找到备份", + "settings.NoExternalModulesFound": "未找到外部模块", + "settings.NoHeadersAdded": "未添加响应头", + "settings.OpenLinksIn": "打开链接…", + "settings.Password": "密码", + "settings.PasswordValidation": "需要密码", + "settings.Profiles": "简介", + "settings.ProfilesDescription": "管理您的个人资料", + "settings.ProfileAlreadyExists": "配置文件已存在", + "settings.ProfileName": "个人资料名称", + "settings.HeaderAdded": "标题已添加", + "settings.HeaderDeleted": "标题已删除", + "settings.HeaderKey": "标题键", + "settings.HeaderValueValidation": "需要标题值", + "settings.Host": "主机", + "settings.ProfileNameRequired": "需要配置文件名称", + "settings.Register": "注册", + "settings.RegisteredFailure": "注册失败", + "settings.RegisteredSuccess": "注册", + "settings.RenameProfile": "重命名配置文件", + "settings.ResetPassword": "重置密码", + "settings.Resources": "资源", + "settings.ResourcesDescription": "有用的资源和链接", + "settings.RestoreFromCloudDescription": "恢复配置数据", + "settings.MustBeValueBetween": "必须是 {} 和 {} 之间的值", + "settings.RestoreFromCloudFailure": "无法恢复", + "settings.RestoreFromCloudSuccess": "恢复成功", + "settings.SignedInFailure": "登录失败", + "settings.SignedInSuccess": "登录成功", + "settings.SignedOutFailure": "退出失败", + "settings.SignedOutSuccess": "退出", + "settings.SignedOutSuccessMessage": "退出您的 LunaSea 帐户", + "settings.SignIn": "登入", + "settings.SignOut": "退出", + "settings.SignOutHint1": "您确定要退出您的 LunaSea 帐户吗?", + "settings.StartingDay": "开始日", + "settings.StartingSize": "起始大小", + "settings.StartingType": "起始类型", + "settings.System": "系统", + "settings.SystemDescription": "系统实用程序和信息", + "settings.TestConnection": "测试连接", + "settings.Username": "用户名", + "settings.UsernameValidation": "需要用户名", + "settings.Account": "账户", + "settings.AccountHelpHint1": "LunaSea 提供了一个免费帐户来将您的配置备份到云端,未来还会有更多功能!", + "settings.ApiKey": "API密钥", + "settings.ApiKeyRequired": "需要API密钥", + "settings.DismissBannersHint2": "工具提示横幅将为您提供有关 LunaSea 中可用功能的提示和提示。", + "settings.BackgroundImageOpacity": "背景图像不透明度", + "settings.BannersNotificationModuleSupportHeader": "支持的模块", + "settings.BannersNotificationModuleSupportBody": "当前仅在下面列出的模块中支持基于 Webhook 的通知。\n\n将来会提供额外的模块支持!", + "settings.ClearConfigurationHint2": "您将从头开始,请确保备份当前配置!", + "settings.DismissBanners": "关闭横幅", + "settings.DecryptBackupHint1": "请输入此备份的加密密钥。", + "settings.DeleteCloudBackupDescription": "删除配置文件", + "settings.ForgotYourPassword": "忘记密码了?", + "settings.HostHint3": "不要使用本地主机或 127.0.0.1", + "settings.InvalidPassword": "无效的密码", + "settings.HostRequiredMessage": "主机需要连接到 {}", + "settings.HostValidation": "主机必须包含 http:// 或 https://", + "settings.MACAddressHint3": "十六进制数字范围从 0-9 和 A-F", + "settings.ShowCalendarEntries": "显示 {} 日历条目", + "settings.QuickActionsDescription": "主屏幕上的快速操作", + "settings.MACAddressHint4": "每个十六进制八位字节应由冒号分隔", + "settings.QuickActions": "快速操作", + "settings.RestoreFromCloudSuccessMessage": "您的配置已恢复", + "settings.RestoreFromCloud": "从云端恢复", + "settings.HeaderKeyValidation": "需要标题键", + "settings.HeaderValue": "标题值", + "settings.AccountSettings": "帐户设置", + "settings.DeleteAccountHint2": "此过程还将删除所有存储的云备份和附加到此帐户的数据。", + "settings.DeleteAccountHint1": "您确定要删除您的 LunaSea 帐户吗?", + "settings.AccountDeleted": "帐户已删除", + "settings.AccountDeletedMessage": "删除您的 LunaSea 帐户", + "settings.DeleteAccount": "删除帐户", + "settings.DeleteAccountDescription": "永久删除您的帐户", + "settings.DeleteAccountWarning1": "这个过程不可逆", + "settings.FailedToDeleteAccount": "删除账户失败", + "settings.StartingView": "开始视图", + "settings.DefaultPages": "默认页", + "settings.DefaultPagesDescription": "设置默认登录页", + "settings.DefaultSortingAndFiltering": "默认排序和过滤", + "settings.DefaultSortingAndFilteringDescription": "设置默认排序和过滤方法", + "settings.DeleteProfileDescription": "删除现有配置文件", + "settings.RenameProfileDescription": "重命名现有配置文件", + "settings.Add": "添加", + "settings.AddProfileDescription": "添加新配置文件", + "settings.ClearImageCacheHint2": "为大型库重新下载图像可能会消耗大量数据。", + "settings.ClearImageCache": "清除图像缓存", + "settings.ClearImageCacheHint1": "您确定要清除缓存中的所有图像吗?", + "settings.DefaultOptions": "默认选项", + "settings.SortCategory": "排序类别", + "settings.DefaultOptionsDescription": "设置排序、过滤和查看选项", + "settings.SortDirection": "排序方向", + "settings.FilterCategory": "过滤器类别", + "settings.Network": "网络", + "settings.TLSCertificateValidationDescription": "验证 TLS 连接中的证书", + "settings.NetworkDescription": "自定义网络功能", + "settings.TLSCertificateValidation": "TLS 证书验证", + "settings.ViewRecentChanges": "查看最近的更改", "sonarr.AddSeries": "添加系列", "sonarr.AddedOn": "添加于", "sonarr.Age": "年龄", @@ -695,5 +627,73 @@ "sonarr.TimeLeft": "剩余时间", "sonarr.Title": "标题", "sonarr.Torrent": "种子", - "sonarr.Usenet": "用户网络" + "sonarr.Usenet": "用户网络", + "tautulli.Activity": "活动", + "tautulli.Audio": "音频", + "tautulli.BackingUpConfiguration": "正在备份配置…", + "tautulli.BackingUpConfigurationDescription": "在后台备份您的配置", + "tautulli.BackingUpConfigurationFailed": "备份配置失败", + "tautulli.BackingUpDatabase": "正在备份数据库…", + "tautulli.BackingUpDatabaseDescription": "在后台备份您的数据库", + "tautulli.BackingUpDatabaseFailed": "备份数据库失败", + "tautulli.BackupConfiguration": "备份配置", + "tautulli.BackupDatabase": "备份数据库", + "tautulli.Bandwidth": "带宽", + "tautulli.Burn": "燃烧", + "tautulli.Container": "容器", + "tautulli.Copy": "复制", + "tautulli.DeleteCache": "删除缓存", + "tautulli.DeleteImageCache": "删除图像缓存", + "tautulli.DeleteTemporarySessions": "删除临时会话", + "tautulli.DeletingCache": "正在删除缓存…", + "tautulli.DeletingCacheDescription": "正在删除 Tautulli 缓存", + "tautulli.DeletingCacheFailed": "删除缓存失败", + "tautulli.DeletingImageCacheDescription": "正在删除 Tautulli 图片缓存", + "tautulli.ActivityDetails": "活动详情", + "tautulli.DeletingImageCacheFailed": "删除图片缓存失败", + "tautulli.DeletingTemporarySessionsDescription": "正在删除临时会话", + "tautulli.DeletingTemporarySessionsFailed": "删除临时会话失败", + "tautulli.DirectPlay": "直接播放", + "tautulli.DirectPlays": "直接播放", + "tautulli.DirectStream": "直播", + "tautulli.DirectStreams": "直接流", + "tautulli.Duration": "持续时间", + "tautulli.Episode": "剧集{}", + "tautulli.ETA": "预计到达时间", + "tautulli.History": "历史", + "tautulli.Library": "图书馆", + "tautulli.Location": "定位", + "tautulli.Metadata": "元数据", + "tautulli.More": "更多", + "tautulli.None": "没有", + "tautulli.Platform": "平台", + "tautulli.Player": "播放器", + "tautulli.Product": "产品", + "tautulli.Quality": "画质", + "tautulli.SessionEnded": "会话结束", + "tautulli.Sessions": "会话", + "tautulli.SessionsOne": "1 节", + "tautulli.SessionsMany": "{} 会话", + "tautulli.Stream": "流", + "tautulli.Terminate": "终止", + "tautulli.TerminationAttachMessage": "您可以选择在下方附加终止消息。", + "tautulli.TerminationConfirmMessage": "您要终止此会话吗?", + "tautulli.TerminationMessage": "终止消息", + "tautulli.TerminatedSession": "终止的会话", + "tautulli.TerminateSession": "终止会话", + "tautulli.Throttled": "节流", + "tautulli.Title": "标题", + "tautulli.Transcode": "转码", + "tautulli.Transcodes": "转码", + "tautulli.User": "用户", + "tautulli.Users": "使用者", + "tautulli.Video": "视频", + "tautulli.ViewWebGUI": "网页图形用户界面", + "tautulli.Year": "年", + "tautulli.Subtitle": "副标题", + "tautulli.DeletingImageCache": "正在删除图像缓存…", + "tautulli.DeletingTemporarySessions": "正在删除临时会话…", + "tautulli.NoActiveStreams": "没有活动流", + "tautulli.Season": "季 {}", + "tautulli.TerminateSessionFailed": "终止会话失败" } \ No newline at end of file diff --git a/lib/core/assets.dart b/lib/core/assets.dart index ad141e8e9a..3d6571d7cb 100644 --- a/lib/core/assets.dart +++ b/lib/core/assets.dart @@ -14,4 +14,5 @@ class LunaAssets { static const String serviceLastfm = 'assets/images/service_lastfm.png'; static const String serviceYoutube = 'assets/images/service_youtube.png'; static const String serviceImdb = 'assets/images/service_imdb.png'; + static const String serviceGoodreads = 'assets/images/service_goodreads.png'; } diff --git a/lib/core/extensions/string.dart b/lib/core/extensions/string.dart index defbdfaa78..b5a16a5a52 100644 --- a/lib/core/extensions/string.dart +++ b/lib/core/extensions/string.dart @@ -91,6 +91,14 @@ extension StringLinksExtension on String { Future lunaOpenTVMaze() async => await _openLink('https://www.tvmaze.com/shows/$this'); + /// Attach this string as a author ID to Goodreads and attempt to launch it as a URL. + Future lunaOpenGoodreadsAuthor() async => + await _openLink('https://www.goodreads.com/author/show/$this'); + + /// Attach this string as a author ID to Goodreads and attempt to launch it as a URL. + Future lunaOpenGoodreadsBook() async => + await _openLink('https://www.goodreads.com/book/show/$this'); + Future copyToClipboard({ bool showSnackBar = true, }) async { diff --git a/lib/core/models/configuration/profile.dart b/lib/core/models/configuration/profile.dart index a3c588eb19..6aaee1118a 100644 --- a/lib/core/models/configuration/profile.dart +++ b/lib/core/models/configuration/profile.dart @@ -52,6 +52,11 @@ class ProfileHiveObject extends HiveObject { overseerrHost: '', overseerrKey: '', overseerrHeaders: {}, + //Readarr + readarrEnabled: false, + readarrHost: '', + readarrKey: '', + readarrHeaders: {}, ); /// Create a new [ProfileHiveObject] from another [ProfileHiveObject] (deep-copy). @@ -102,6 +107,11 @@ class ProfileHiveObject extends HiveObject { overseerrHost: profile.overseerrHost, overseerrKey: profile.overseerrKey, overseerrHeaders: profile.overseerrHeaders, + //Readarr + readarrEnabled: profile.readarrEnabled, + readarrHost: profile.readarrHost, + readarrKey: profile.readarrKey, + readarrHeaders: profile.readarrHeaders, ); /// Create a new [ProfileHiveObject] from a map where the keys map 1-to-1. @@ -154,6 +164,11 @@ class ProfileHiveObject extends HiveObject { overseerrHost: profile['overseerrHost'] ?? '', overseerrKey: profile['overseerrKey'] ?? '', overseerrHeaders: profile['overseerrHeaders'] ?? {}, + //Readarr + readarrEnabled: profile['readarrEnabled'] ?? false, + readarrHost: profile['readarrHost'] ?? '', + readarrKey: profile['readarrKey'] ?? '', + readarrHeaders: profile['readarrHeaders'] ?? {}, ); ProfileHiveObject({ @@ -202,6 +217,11 @@ class ProfileHiveObject extends HiveObject { required this.overseerrHost, required this.overseerrKey, required this.overseerrHeaders, + //Readarr + required this.readarrEnabled, + required this.readarrHost, + required this.readarrKey, + required this.readarrHeaders, }); @override @@ -254,6 +274,11 @@ class ProfileHiveObject extends HiveObject { "overseerrHost": overseerrHost, "overseerrKey": overseerrKey, "overseerrHeaders": overseerrHeaders, + //Readarr + "readarrEnabled": readarrEnabled, + "readarrHost": readarrHost, + "readarrKey": readarrKey, + "readarrHeaders": readarrHeaders, }; //Lidarr @@ -413,6 +438,23 @@ class ProfileHiveObject extends HiveObject { 'headers': overseerrHeaders ?? {}, }; + //Readarr + @HiveField(44) + bool? readarrEnabled; + @HiveField(45) + String? readarrHost; + @HiveField(46) + String? readarrKey; + @HiveField(47) + Map? readarrHeaders; + + Map getReadarr() => { + 'enabled': readarrEnabled ?? false, + 'host': readarrHost ?? '', + 'key': readarrKey ?? '', + 'headers': readarrHeaders ?? {}, + }; + bool get anythingEnabled { for (LunaModule module in LunaModule.DASHBOARD.allModules()) { if (module.isEnabled) return true; diff --git a/lib/core/modules.dart b/lib/core/modules.dart index abe8677054..a2e0b16496 100644 --- a/lib/core/modules.dart +++ b/lib/core/modules.dart @@ -11,6 +11,7 @@ const _LIDARR_KEY = 'lidarr'; const _NZBGET_KEY = 'nzbget'; const _OVERSEERR_KEY = 'overseerr'; const _RADARR_KEY = 'radarr'; +const _READARR_KEY = 'readarr'; const _SABNZBD_KEY = 'sabnzbd'; const _SEARCH_KEY = 'search'; const _SETTINGS_KEY = 'settings'; @@ -22,7 +23,7 @@ const _WAKE_ON_LAN_KEY = 'wake_on_lan'; enum LunaModule { @HiveField(0) DASHBOARD, - @HiveField(11) + @HiveField(12) EXTERNAL_MODULES, @HiveField(1) LIDARR, @@ -33,6 +34,8 @@ enum LunaModule { @HiveField(4) RADARR, @HiveField(5) + READARR, + @HiveField(13) SABNZBD, @HiveField(6) SEARCH, @@ -62,6 +65,8 @@ extension LunaModuleExtension on LunaModule { return LunaFlavor().isLowerOrEqualTo(LunaEnvironment.DEVELOP); case LunaModule.RADARR: return true; + case LunaModule.READARR: + return true; case LunaModule.SABNZBD: return true; case LunaModule.SEARCH: @@ -105,6 +110,8 @@ extension LunaModuleExtension on LunaModule { return LunaModule.NZBGET; case _RADARR_KEY: return LunaModule.RADARR; + case _READARR_KEY: + return LunaModule.READARR; case _SABNZBD_KEY: return LunaModule.SABNZBD; case _SEARCH_KEY: @@ -136,6 +143,8 @@ extension LunaModuleExtension on LunaModule { return _NZBGET_KEY; case LunaModule.RADARR: return _RADARR_KEY; + case LunaModule.READARR: + return _READARR_KEY; case LunaModule.SABNZBD: return _SABNZBD_KEY; case LunaModule.SEARCH: @@ -170,6 +179,8 @@ extension LunaModuleExtension on LunaModule { return LunaProfile.current.overseerrEnabled ?? false; case LunaModule.RADARR: return LunaProfile.current.radarrEnabled ?? false; + case LunaModule.READARR: + return LunaProfile.current.readarrEnabled ?? false; case LunaModule.SABNZBD: return LunaProfile.current.sabnzbdEnabled ?? false; case LunaModule.SEARCH: @@ -200,6 +211,8 @@ extension LunaModuleExtension on LunaModule { return LunaProfile.current.getOverseerr(); case LunaModule.RADARR: return LunaProfile.current.getRadarr(); + case LunaModule.READARR: + return LunaProfile.current.getReadarr(); case LunaModule.SABNZBD: return LunaProfile.current.getSABnzbd(); case LunaModule.SEARCH: @@ -232,6 +245,8 @@ extension LunaModuleExtension on LunaModule { return 'NZBGet'; case LunaModule.RADARR: return 'Radarr'; + case LunaModule.READARR: + return 'Readarr'; case LunaModule.SABNZBD: return 'SABnzbd'; case LunaModule.SEARCH: @@ -262,6 +277,8 @@ extension LunaModuleExtension on LunaModule { return NZBGet.ROUTE_NAME; case LunaModule.RADARR: return RadarrHomeRouter().route(); + case LunaModule.READARR: + return ReadarrHomeRouter().route(); case LunaModule.SABNZBD: return SABnzbd.ROUTE_NAME; case LunaModule.SEARCH: @@ -292,6 +309,8 @@ extension LunaModuleExtension on LunaModule { return LunaBrandIcons.nzbget; case LunaModule.RADARR: return LunaBrandIcons.radarr; + case LunaModule.READARR: + return LunaBrandIcons.readarr; case LunaModule.SABNZBD: return LunaBrandIcons.sabnzbd; case LunaModule.SEARCH: @@ -326,6 +345,8 @@ extension LunaModuleExtension on LunaModule { return LidarrDatabase(); case LunaModule.RADARR: return RadarrDatabase(); + case LunaModule.READARR: + return ReadarrDatabase(); case LunaModule.SONARR: return SonarrDatabase(); case LunaModule.NZBGET: @@ -356,6 +377,8 @@ extension LunaModuleExtension on LunaModule { return context.read(); case LunaModule.RADARR: return context.read(); + case LunaModule.READARR: + return context.read(); case LunaModule.SONARR: return context.read(); case LunaModule.NZBGET: @@ -382,6 +405,8 @@ extension LunaModuleExtension on LunaModule { return const Color(0xFF42D535); case LunaModule.RADARR: return const Color(0xFFFEC333); + case LunaModule.READARR: + return const Color(0x00CA302D); case LunaModule.SABNZBD: return const Color(0xFFFECC2B); case LunaModule.SEARCH: @@ -412,6 +437,8 @@ extension LunaModuleExtension on LunaModule { return 'https://nzbget.net'; case LunaModule.RADARR: return 'https://radarr.video'; + case LunaModule.READARR: + return 'https://readarr.com'; case LunaModule.SABNZBD: return 'https://sabnzbd.org'; case LunaModule.SEARCH: @@ -442,6 +469,8 @@ extension LunaModuleExtension on LunaModule { return 'https://github.com/nzbget/nzbget'; case LunaModule.RADARR: return 'https://github.com/Radarr/Radarr'; + case LunaModule.READARR: + return 'https://github.com/Readarr/Readarr'; case LunaModule.SABNZBD: return 'https://github.com/sabnzbd/sabnzbd'; case LunaModule.SEARCH: @@ -472,6 +501,8 @@ extension LunaModuleExtension on LunaModule { return 'Manage Usenet Downloads'; case LunaModule.RADARR: return 'Manage Movies'; + case LunaModule.READARR: + return 'Manage Books'; case LunaModule.SABNZBD: return 'Manage Usenet Downloads'; case LunaModule.SEARCH: @@ -504,6 +535,8 @@ extension LunaModuleExtension on LunaModule { return 'NZBGet is a binary downloader, which downloads files from Usenet based on information given in nzb-files.'; case LunaModule.RADARR: return 'Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available.'; + case LunaModule.READARR: + return 'Readarr is an ebook and audiobook collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new books from your favorite authors and will grab, sort, and rename them.'; case LunaModule.SABNZBD: return 'SABnzbd is a multi-platform binary newsgroup downloader. The program works in the background and simplifies the downloading verifying and extracting of files from Usenet.'; case LunaModule.SEARCH: @@ -534,6 +567,8 @@ extension LunaModuleExtension on LunaModule { return ShortcutItem(type: key, localizedTitle: name); case LunaModule.RADARR: return ShortcutItem(type: key, localizedTitle: name); + case LunaModule.READARR: + return ShortcutItem(type: key, localizedTitle: name); case LunaModule.SABNZBD: return ShortcutItem(type: key, localizedTitle: name); case LunaModule.SEARCH: @@ -564,6 +599,8 @@ extension LunaModuleExtension on LunaModule { return false; case LunaModule.RADARR: return true; + case LunaModule.READARR: + return true; case LunaModule.SABNZBD: return false; case LunaModule.SEARCH: @@ -629,6 +666,8 @@ extension LunaModuleExtension on LunaModule { return; case LunaModule.RADARR: return; + case LunaModule.READARR: + return; case LunaModule.SABNZBD: return; case LunaModule.SEARCH: @@ -659,6 +698,8 @@ extension LunaModuleExtension on LunaModule { return; case LunaModule.RADARR: return RadarrWebhooks().handle(data); + case LunaModule.READARR: + return ReadarrWebhooks().handle(data); case LunaModule.SABNZBD: return; case LunaModule.SEARCH: @@ -691,6 +732,8 @@ extension LunaModuleExtension on LunaModule { return SettingsConfigurationOverseerrRouter(); case LunaModule.RADARR: return SettingsConfigurationRadarrRouter(); + case LunaModule.READARR: + return SettingsConfigurationReadarrRouter(); case LunaModule.SABNZBD: return SettingsConfigurationSABnzbdRouter(); case LunaModule.SEARCH: diff --git a/lib/core/router/router.dart b/lib/core/router/router.dart index aa8a375100..2222f54b0e 100644 --- a/lib/core/router/router.dart +++ b/lib/core/router/router.dart @@ -27,6 +27,7 @@ class LunaRouter { ExternalModulesRouter().defineAllRoutes(router); OverseerrRouter().defineAllRoutes(router); RadarrRouter().defineAllRoutes(router); + ReadarrRouter().defineAllRoutes(router); SearchRouter().defineAllRoutes(router); SettingsRouter().defineAllRoutes(router); SonarrRouter().defineAllRoutes(router); diff --git a/lib/core/state/state.dart b/lib/core/state/state.dart index 406d99e727..f9463eae09 100644 --- a/lib/core/state/state.dart +++ b/lib/core/state/state.dart @@ -24,6 +24,7 @@ class LunaState { ChangeNotifierProvider(create: (_) => SearchState()), ChangeNotifierProvider(create: (_) => LidarrState()), ChangeNotifierProvider(create: (_) => RadarrState()), + ChangeNotifierProvider(create: (_) => ReadarrState()), ChangeNotifierProvider(create: (_) => SonarrState()), ChangeNotifierProvider(create: (_) => NZBGetState()), ChangeNotifierProvider(create: (_) => SABnzbdState()), diff --git a/lib/core/ui/icons/icon.dart b/lib/core/ui/icons/icon.dart index a0749da18c..5cc8655de2 100644 --- a/lib/core/ui/icons/icon.dart +++ b/lib/core/ui/icons/icon.dart @@ -4,6 +4,7 @@ class LunaIcons { static const IconData ADD = Icons.add_rounded; static const IconData ARROW_RIGHT = Icons.arrow_forward_ios_rounded; static const IconData ARROW_DROPDOWN = Icons.arrow_drop_down_rounded; + static const IconData BOOK = Icons.menu_book_rounded; static const IconData CLOUD_DELETE = Icons.cloud_off_rounded; static const IconData CLOUD_DOWNLOAD = Icons.cloud_download_rounded; static const IconData CLOUD_UPLOAD = Icons.cloud_upload_rounded; diff --git a/lib/core/ui/icons/icon_brands.dart b/lib/core/ui/icons/icon_brands.dart index 4b5135f71e..b036729e8d 100644 --- a/lib/core/ui/icons/icon_brands.dart +++ b/lib/core/ui/icons/icon_brands.dart @@ -1,3 +1,18 @@ +/// Flutter icons LunaBrandIcons +/// Copyright (C) 2022 by original authors @ fluttericon.com, fontello.com +/// This font was generated by FlutterIcon.com, which is derived from Fontello. +/// +/// To use this font, place it in your fonts/ directory and include the +/// following in your pubspec.yaml +/// +/// flutter: +/// fonts: +/// - family: LunaBrandIcons +/// fonts: +/// - asset: fonts/LunaBrandIcons.ttf +/// +/// +/// import 'package:flutter/widgets.dart'; class LunaBrandIcons { @@ -6,36 +21,21 @@ class LunaBrandIcons { static const _kFontFam = 'LunaBrandIcons'; static const String? _kFontPkg = null; - static const IconData bravebrowser = - IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData chrome = - IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData discord = - IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData firefox = - IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData github = - IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData lidarr = - IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData microsoftedge = - IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData nzbget = - IconData(0xe807, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData overseerr = - IconData(0xe808, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData plex = - IconData(0xe809, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData radarr = - IconData(0xe80a, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData reddit = - IconData(0xe80b, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData sabnzbd = - IconData(0xe80c, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData safari = - IconData(0xe80d, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData sonarr = - IconData(0xe80e, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData tautulli = - IconData(0xe80f, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData bravebrowser = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData chrome = IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData discord = IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData firefox = IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData github = IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData lidarr = IconData(0xe805, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData microsoftedge = IconData(0xe806, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData nzbget = IconData(0xe807, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData overseerr = IconData(0xe808, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData plex = IconData(0xe809, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData radarr = IconData(0xe80a, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData reddit = IconData(0xe80b, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData sabnzbd = IconData(0xe80c, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData safari = IconData(0xe80d, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData sonarr = IconData(0xe80e, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData tautulli = IconData(0xe80f, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static const IconData readarr = IconData(0xe810, fontFamily: _kFontFam, fontPackage: _kFontPkg); } diff --git a/lib/modules.dart b/lib/modules.dart index 5016669126..add92f8f9d 100644 --- a/lib/modules.dart +++ b/lib/modules.dart @@ -4,6 +4,7 @@ export 'modules/lidarr.dart'; export 'modules/nzbget.dart'; export 'modules/overseerr.dart'; export 'modules/radarr.dart'; +export 'modules/readarr.dart'; export 'modules/sabnzbd.dart'; export 'modules/search.dart'; export 'modules/settings.dart'; diff --git a/lib/modules/readarr.dart b/lib/modules/readarr.dart new file mode 100644 index 0000000000..e5042686a3 --- /dev/null +++ b/lib/modules/readarr.dart @@ -0,0 +1,3 @@ +export 'readarr/api.dart'; +export 'readarr/core.dart'; +export 'readarr/routes.dart'; diff --git a/lib/modules/readarr/api.dart b/lib/modules/readarr/api.dart new file mode 100644 index 0000000000..53c618d092 --- /dev/null +++ b/lib/modules/readarr/api.dart @@ -0,0 +1,5 @@ +export 'api/readarr.dart'; +export 'api/controllers.dart'; +export 'api/models.dart'; +export 'api/types.dart'; +export 'api/utilities.dart'; diff --git a/lib/modules/readarr/api/controllers.dart b/lib/modules/readarr/api/controllers.dart new file mode 100644 index 0000000000..6a5120e8fb --- /dev/null +++ b/lib/modules/readarr/api/controllers.dart @@ -0,0 +1,96 @@ +/// Library containing all logic and accessors to make calls to Readarr's API. +library readarr_commands; + +import './types.dart'; +import 'package:dio/dio.dart'; +import 'package:intl/intl.dart'; +import 'models.dart'; + +// Author +part 'src/controllers/author.dart'; +part 'src/controllers/author/add_author.dart'; +part 'src/controllers/author/delete_author.dart'; +part 'src/controllers/author/get_all_authors.dart'; +part 'src/controllers/author/get_author.dart'; +part 'src/controllers/author/update_author.dart'; + +// Author Lookup +part 'src/controllers/author_lookup.dart'; +part 'src/controllers/author_lookup/lookup.dart'; + +// Book +part 'src/controllers/book.dart'; +part 'src/controllers/book/get_all_books.dart'; +part 'src/controllers/book/set_monitored.dart'; +part 'src/controllers/book/update_book.dart'; +part 'src/controllers/book/delete_book.dart'; + +// Book File +part 'src/controllers/book_file.dart'; +part 'src/controllers/book_file/delete_book_file.dart'; +part 'src/controllers/book_file/get_book_file.dart'; +part 'src/controllers/book_file/get_author_book_files.dart'; + +// Calendar +part 'src/controllers/calendar.dart'; +part 'src/controllers/calendar/get_calendar.dart'; + +// Command +part 'src/controllers/command.dart'; +part 'src/controllers/command/author_search.dart'; +part 'src/controllers/command/backup.dart'; +part 'src/controllers/command/book_search.dart'; +part 'src/controllers/command/missing_episode_search.dart'; +part 'src/controllers/command/queue.dart'; +part 'src/controllers/command/refresh_monitored_downloads.dart'; +part 'src/controllers/command/refresh_author.dart'; +part 'src/controllers/command/refresh_book.dart'; +part 'src/controllers/command/rescan_author.dart'; +part 'src/controllers/command/rss_sync.dart'; +part 'src/controllers/command/season_search.dart'; + +// History +part 'src/controllers/history.dart'; +part 'src/controllers/history/get_history.dart'; +part 'src/controllers/history/get_history_by_author.dart'; + +// Import List +part 'src/controllers/import_list.dart'; +part 'src/controllers/import_list/get_exclusion_list.dart'; + +// Profile +part 'src/controllers/profile.dart'; +part 'src/controllers/profile/get_metadata_profiles.dart'; +part 'src/controllers/profile/get_quality_profiles.dart'; + +// Queue +part 'src/controllers/queue.dart'; +part 'src/controllers/queue/delete_queue.dart'; +part 'src/controllers/queue/get_queue.dart'; +part 'src/controllers/queue/get_queue_details.dart'; + +// Releases +part 'src/controllers/release.dart'; +part 'src/controllers/release/add_release.dart'; +part 'src/controllers/release/get_release.dart'; +part 'src/controllers/release/get_season_release.dart'; + +// Root Folder +part 'src/controllers/root_folder.dart'; +part 'src/controllers/root_folder/get_root_folders.dart'; + +// System +part 'src/controllers/system.dart'; +part 'src/controllers/system/get_status.dart'; + +// Tags +part 'src/controllers/tag.dart'; +part 'src/controllers/tag/add_tag.dart'; +part 'src/controllers/tag/delete_tag.dart'; +part 'src/controllers/tag/get_all_tags.dart'; +part 'src/controllers/tag/get_tag.dart'; +part 'src/controllers/tag/update_tag.dart'; + +// Wanted/Missing +part 'src/controllers/wanted.dart'; +part 'src/controllers/wanted/get_missing.dart'; diff --git a/lib/modules/readarr/api/models.dart b/lib/modules/readarr/api/models.dart new file mode 100644 index 0000000000..477ab3e09e --- /dev/null +++ b/lib/modules/readarr/api/models.dart @@ -0,0 +1,62 @@ +/// Library containing all model definitions for Readarr data. +library readarr_models; + +/// Author +export 'src/models/author/image.dart'; +export 'src/models/author/rating.dart'; +export 'src/models/author/season_statistics.dart'; +export 'src/models/author/season.dart'; +export 'src/models/author/author.dart'; +export 'src/models/author/author_statistics.dart'; + +/// Book +export 'src/models/book/book.dart'; + +/// Book File +export 'src/models/book_file/book_file.dart'; +export 'src/models/book_file/book_file_quality.dart'; +export 'src/models/book_file/book_file_quality_quality.dart'; +export 'src/models/book_file/book_file_quality_revision.dart'; + +/// Edition +export 'src/models/edition/edition.dart'; + +/// Command +export 'src/models/command/command.dart'; +export 'src/models/command/command_body.dart'; + +/// History +export 'src/models/history/history.dart'; +export 'src/models/history/history_record.dart'; + +/// Import List +export 'src/models/import_list/exclusion.dart'; + +/// Profile +export 'src/models/profile/metadata_profile.dart'; +export 'src/models/profile/quality_profile_cutoff.dart'; +export 'src/models/profile/quality_profile_item_quality.dart'; +export 'src/models/profile/quality_profile_item.dart'; +export 'src/models/profile/quality_profile.dart'; + +/// Queue +export 'src/models/queue/queue.dart'; +export 'src/models/queue/queue_record.dart'; +export 'src/models/queue/queue_status_message.dart'; + +/// Release +export 'src/models/release/release.dart'; +export 'src/models/release/added_release.dart'; + +/// Root Folder +export 'src/models/root_folder/root_folder.dart'; +export 'src/models/root_folder/unmapped_folder.dart'; + +/// System +export 'src/models/system/status.dart'; + +/// Tags +export 'src/models/tag/tag.dart'; + +/// Wanted/Missing +export 'src/models/wanted_missing/missing.dart'; diff --git a/lib/modules/readarr/api/readarr.dart b/lib/modules/readarr/api/readarr.dart new file mode 100644 index 0000000000..e756fd6606 --- /dev/null +++ b/lib/modules/readarr/api/readarr.dart @@ -0,0 +1,104 @@ +library readarr; + +import 'package:dio/dio.dart'; +import 'controllers.dart'; + +class Readarr { + Readarr._internal({ + required this.httpClient, + required this.author, + required this.authorLookup, + required this.book, + required this.bookFile, + required this.calendar, + required this.command, + required this.history, + required this.importList, + required this.profile, + required this.queue, + required this.release, + required this.rootFolder, + required this.system, + required this.tag, + required this.wanted, + }); + + factory Readarr({ + required String host, + required String apiKey, + Map? headers, + bool followRedirects = true, + int maxRedirects = 5, + }) { + Dio _dio = Dio( + BaseOptions( + baseUrl: host.endsWith('/') ? '${host}api/v1/' : '$host/api/v1/', + queryParameters: { + 'apikey': apiKey, + }, + headers: headers, + followRedirects: followRedirects, + maxRedirects: maxRedirects, + ), + ); + return Readarr._internal( + httpClient: _dio, + author: ReadarrControllerAuthor(_dio), + authorLookup: ReadarrControllerAuthorLookup(_dio), + book: ReadarrControllerBook(_dio), + bookFile: ReadarrControllerBookFile(_dio), + calendar: ReadarrControllerCalendar(_dio), + command: ReadarrControllerCommand(_dio), + history: ReadarrControllerHistory(_dio), + importList: ReadarrControllerImportList(_dio), + profile: ReadarrControllerProfile(_dio), + queue: ReadarrControllerQueue(_dio), + release: ReadarrControllerRelease(_dio), + rootFolder: ReadarrControllerRootFolder(_dio), + system: ReadarrControllerSystem(_dio), + tag: ReadarrControllerTag(_dio), + wanted: ReadarrControllerWanted(_dio), + ); + } + + factory Readarr.from({ + required Dio client, + }) { + return Readarr._internal( + httpClient: client, + author: ReadarrControllerAuthor(client), + authorLookup: ReadarrControllerAuthorLookup(client), + bookFile: ReadarrControllerBookFile(client), + book: ReadarrControllerBook(client), + calendar: ReadarrControllerCalendar(client), + command: ReadarrControllerCommand(client), + history: ReadarrControllerHistory(client), + importList: ReadarrControllerImportList(client), + profile: ReadarrControllerProfile(client), + queue: ReadarrControllerQueue(client), + release: ReadarrControllerRelease(client), + rootFolder: ReadarrControllerRootFolder(client), + system: ReadarrControllerSystem(client), + tag: ReadarrControllerTag(client), + wanted: ReadarrControllerWanted(client), + ); + } + + final Dio httpClient; + + final ReadarrControllerAuthor author; + final ReadarrControllerAuthorLookup authorLookup; + final ReadarrControllerBook book; + final ReadarrControllerBookFile bookFile; + final ReadarrControllerCalendar calendar; + final ReadarrControllerCommand command; + final ReadarrControllerHistory history; + final ReadarrControllerImportList importList; + final ReadarrControllerProfile profile; + final ReadarrControllerQueue queue; + final ReadarrControllerRelease release; + final ReadarrControllerRootFolder rootFolder; + final ReadarrControllerSystem system; + final ReadarrControllerTag tag; + final ReadarrControllerWanted wanted; +} diff --git a/lib/modules/readarr/api/src/controllers/author.dart b/lib/modules/readarr/api/src/controllers/author.dart new file mode 100644 index 0000000000..981e7ed23d --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/author.dart @@ -0,0 +1,78 @@ +part of readarr_commands; + +/// Facilitates, encapsulates, and manages individual calls related to series within Readarr. +/// +/// [ReadarrControllerAuthor] internally handles routing the HTTP client to the API calls. +class ReadarrControllerAuthor { + final Dio _client; + + /// Create a series command handler using an initialized [Dio] client. + ReadarrControllerAuthor(this._client); + + /// Handler for [series](https://github.com/Readarr/Readarr/wiki/Series#post). + /// + /// Adds a new series to your collection. + Future create({ + required ReadarrAuthor author, + required ReadarrQualityProfile qualityProfile, + required ReadarrMetadataProfile metadataProfile, + required ReadarrRootFolder rootFolder, + required ReadarrAuthorMonitorType monitorType, + List tags = const [], + bool searchForMissingEpisodes = false, + bool searchForCutoffUnmetEpisodes = false, + bool includeSeasonImages = false, + }) async => + _commandAddAuthor( + _client, + author: author, + qualityProfile: qualityProfile, + metadataProfile: metadataProfile, + rootFolder: rootFolder, + monitorType: monitorType, + tags: tags, + searchForMissingEpisodes: searchForMissingEpisodes, + searchForCutoffUnmetEpisodes: searchForCutoffUnmetEpisodes, + ); + + /// Handler for [series/{id}](https://github.com/Readarr/Readarr/wiki/Series#deleteid). + /// + /// Delete the series with the given series ID. + Future delete({ + required int authorId, + bool deleteFiles = false, + bool addImportListExclusion = false, + }) async => + _commandDeleteAuthor( + _client, + authorId: authorId, + deleteFiles: deleteFiles, + addImportListExclusion: addImportListExclusion, + ); + + /// Handler for [series/{id}](https://github.com/Readarr/Readarr/wiki/Series#getid). + /// + /// Returns the series with the matching ID. + Future get({required int authorId}) async => _commandGetAuthor( + _client, + authorId: authorId, + ); + + /// Handler for [series](https://github.com/Readarr/Readarr/wiki/Series#get). + /// + /// Returns a list of all series. + Future> getAll() async => _commandGetAllAuthors( + _client, + ); + + /// Handler for [author]https://github.com/Readarr/Readarr/wiki/Series#put). + /// + /// Update an existing series. + Future update({ + required ReadarrAuthor author, + }) async => + _commandUpdateAuthor( + _client, + author: author, + ); +} diff --git a/lib/modules/readarr/api/src/controllers/author/add_author.dart b/lib/modules/readarr/api/src/controllers/author/add_author.dart new file mode 100644 index 0000000000..e8ce823067 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/author/add_author.dart @@ -0,0 +1,29 @@ +part of readarr_commands; + +Future _commandAddAuthor( + Dio client, { + required ReadarrAuthor author, + required ReadarrQualityProfile qualityProfile, + required ReadarrMetadataProfile metadataProfile, + required ReadarrRootFolder rootFolder, + required ReadarrAuthorMonitorType monitorType, + List tags = const [], + bool searchForMissingEpisodes = false, + bool searchForCutoffUnmetEpisodes = false, +}) async { + Map _payload = author.toJson(); + _payload.addAll({ + 'monitored': true, + 'qualityProfileId': qualityProfile.id, + 'metadataProfileId': metadataProfile.id, + 'tags': tags.map((tag) => tag.id).toList(), + 'rootFolderPath': rootFolder.path, + 'addOptions': { + 'monitor': monitorType.value, + 'searchForMissingEpisodes': searchForMissingEpisodes, + 'searchForCutoffUnmetEpisodes': searchForCutoffUnmetEpisodes, + }, + }); + Response response = await client.post('author', data: _payload); + return ReadarrAuthor.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/author/delete_author.dart b/lib/modules/readarr/api/src/controllers/author/delete_author.dart new file mode 100644 index 0000000000..438d9461cc --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/author/delete_author.dart @@ -0,0 +1,13 @@ +part of readarr_commands; + +Future _commandDeleteAuthor( + Dio client, { + required int authorId, + bool deleteFiles = false, + bool addImportListExclusion = false, +}) async { + await client.delete('author/$authorId', queryParameters: { + 'deleteFiles': deleteFiles, + 'addImportListExclusion': addImportListExclusion, + }); +} diff --git a/lib/modules/readarr/api/src/controllers/author/get_all_authors.dart b/lib/modules/readarr/api/src/controllers/author/get_all_authors.dart new file mode 100644 index 0000000000..d2c82622fd --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/author/get_all_authors.dart @@ -0,0 +1,8 @@ +part of readarr_commands; + +Future> _commandGetAllAuthors(Dio client) async { + Response response = await client.get('author'); + return (response.data as List) + .map((series) => ReadarrAuthor.fromJson(series)) + .toList(); +} diff --git a/lib/modules/readarr/api/src/controllers/author/get_author.dart b/lib/modules/readarr/api/src/controllers/author/get_author.dart new file mode 100644 index 0000000000..f32eb3502a --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/author/get_author.dart @@ -0,0 +1,9 @@ +part of readarr_commands; + +Future _commandGetAuthor( + Dio client, { + required int authorId, +}) async { + Response response = await client.get('author/$authorId'); + return ReadarrAuthor.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/author/update_author.dart b/lib/modules/readarr/api/src/controllers/author/update_author.dart new file mode 100644 index 0000000000..72cbf3d9f1 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/author/update_author.dart @@ -0,0 +1,9 @@ +part of readarr_commands; + +Future _commandUpdateAuthor( + Dio client, { + required ReadarrAuthor author, +}) async { + Response response = await client.put('author', data: author.toJson()); + return ReadarrAuthor.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/author_lookup.dart b/lib/modules/readarr/api/src/controllers/author_lookup.dart new file mode 100644 index 0000000000..50f51b9d92 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/author_lookup.dart @@ -0,0 +1,22 @@ +part of readarr_commands; + +/// Facilitates, encapsulates, and manages individual calls related to series lookup within Readarr. +/// +/// [ReadarrControllerAuthorLookup] internally handles routing the HTTP client to the API calls. +class ReadarrControllerAuthorLookup { + final Dio _client; + + /// Create a series command handler using an initialized [Dio] client. + ReadarrControllerAuthorLookup(this._client); + + /// Handler for [series/lookup](https://github.com/Readarr/Readarr/wiki/Series-Lookup#get). + /// + /// Searches for new shows on TheTVDB.com utilizing readarr.tv's caching and augmentation proxy. + /// + /// Required Parameters: + /// - `term`: Term/words to search for + Future> get({ + required String term, + }) async => + _commandGetSeriesLookup(_client, term: term); +} diff --git a/lib/modules/readarr/api/src/controllers/author_lookup/lookup.dart b/lib/modules/readarr/api/src/controllers/author_lookup/lookup.dart new file mode 100644 index 0000000000..829c4f19d5 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/author_lookup/lookup.dart @@ -0,0 +1,13 @@ +part of readarr_commands; + +Future> _commandGetSeriesLookup( + Dio client, { + required String term, +}) async { + Response response = await client.get('author/lookup', queryParameters: { + 'term': term, + }); + return (response.data as List) + .map((series) => ReadarrAuthor.fromJson(series)) + .toList(); +} diff --git a/lib/modules/readarr/api/src/controllers/book.dart b/lib/modules/readarr/api/src/controllers/book.dart new file mode 100644 index 0000000000..b589a0f589 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/book.dart @@ -0,0 +1,94 @@ +part of readarr_commands; + +/// Facilitates, encapsulates, and manages individual calls related to series within Readarr. +/// +/// [ReadarrControllerBook] internally handles routing the HTTP client to the API calls. +class ReadarrControllerBook { + final Dio _client; + + /// Create a series command handler using an initialized [Dio] client. + ReadarrControllerBook(this._client); + +/* + /// Handler for [series](https://github.com/Readarr/Readarr/wiki/Series#post). + /// + /// Adds a new series to your collection. + Future create({ + required ReadarrAuthor author, + required ReadarrQualityProfile qualityProfile, + required ReadarrMetadataProfile metadataProfile, + required ReadarrRootFolder rootFolder, + required ReadarrAuthorMonitorType monitorType, + List tags = const [], + bool searchForMissingEpisodes = false, + bool searchForCutoffUnmetEpisodes = false, + bool includeSeasonImages = false, + }) async => + _commandAddAuthor( + _client, + author: author, + qualityProfile: qualityProfile, + metadataProfile: metadataProfile, + rootFolder: rootFolder, + monitorType: monitorType, + tags: tags, + searchForMissingEpisodes: searchForMissingEpisodes, + searchForCutoffUnmetEpisodes: searchForCutoffUnmetEpisodes, + );*/ + + /// Handler for [series/{id}](https://github.com/Readarr/Readarr/wiki/Series#deleteid). + /// + /// Delete the series with the given series ID. + Future delete({ + required int bookId, + bool deleteFiles = false, + bool addImportListExclusion = false, + }) async => + _commandDeleteBook( + _client, + bookId: bookId, + deleteFiles: deleteFiles, + addImportListExclusion: addImportListExclusion, + ); + +/* + /// Handler for [series/{id}](https://github.com/Readarr/Readarr/wiki/Series#getid). + /// + /// Returns the series with the matching ID. + Future get({required int authorId}) async => _commandGetAuthor( + _client, + authorId: authorId, + ); +*/ + /// Handler for [series](https://github.com/Readarr/Readarr/wiki/Series#get). + /// + /// Returns a list of all series. + Future> getAll() async => _commandGetAllBooks( + _client, + ); + +/* + /// Handler for [book]https://github.com/Readarr/Readarr/wiki/Series#put). + /// + /// Update an existing series. + Future update({ + required ReadarrBook author, + }) async => + _commandUpdateAuthor( + _client, + author: author, + );*/ + + /// Handler for `book/monitor`. + /// + /// Sets the monitored state for the given episode IDs. + Future> setMonitored({ + required List bookIds, + required bool monitored, + }) async => + _commandBookSetMonitored( + _client, + bookIds: bookIds, + monitored: monitored, + ); +} diff --git a/lib/modules/readarr/api/src/controllers/book/delete_book.dart b/lib/modules/readarr/api/src/controllers/book/delete_book.dart new file mode 100644 index 0000000000..a1d04f99fb --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/book/delete_book.dart @@ -0,0 +1,13 @@ +part of readarr_commands; + +Future _commandDeleteBook( + Dio client, { + required int bookId, + bool deleteFiles = false, + bool addImportListExclusion = false, +}) async { + await client.delete('book/$bookId', queryParameters: { + 'deleteFiles': deleteFiles, + 'addImportListExclusion': addImportListExclusion, + }); +} diff --git a/lib/modules/readarr/api/src/controllers/book/get_all_books.dart b/lib/modules/readarr/api/src/controllers/book/get_all_books.dart new file mode 100644 index 0000000000..fb351c5612 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/book/get_all_books.dart @@ -0,0 +1,8 @@ +part of readarr_commands; + +Future> _commandGetAllBooks(Dio client) async { + Response response = await client.get('book'); + return (response.data as List) + .map((book) => ReadarrBook.fromJson(book)) + .toList(); +} diff --git a/lib/modules/readarr/api/src/controllers/book/set_monitored.dart b/lib/modules/readarr/api/src/controllers/book/set_monitored.dart new file mode 100644 index 0000000000..d4d402bea0 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/book/set_monitored.dart @@ -0,0 +1,15 @@ +part of readarr_commands; + +Future> _commandBookSetMonitored( + Dio client, { + required List bookIds, + required bool monitored, +}) async { + Response response = await client.put('book/monitor', data: { + 'bookIds': bookIds, + 'monitored': monitored, + }); + return (response.data as List) + .map((episode) => ReadarrBook.fromJson(episode)) + .toList(); +} diff --git a/lib/modules/readarr/api/src/controllers/book/update_book.dart b/lib/modules/readarr/api/src/controllers/book/update_book.dart new file mode 100644 index 0000000000..47347350cb --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/book/update_book.dart @@ -0,0 +1,9 @@ +part of readarr_commands; + +Future _commandUpdateBook( + Dio client, { + required ReadarrBook book, +}) async { + Response response = await client.put('book', data: book.toJson()); + return ReadarrBook.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/book_file.dart b/lib/modules/readarr/api/src/controllers/book_file.dart new file mode 100644 index 0000000000..51a9e5340b --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/book_file.dart @@ -0,0 +1,44 @@ +part of readarr_commands; + +/// Facilitates, encapsulates, and manages individual calls related to episode files within Readarr. +/// +/// [ReadarrControllerBookFile] internally handles routing the HTTP client to the API calls. +class ReadarrControllerBookFile { + final Dio _client; + + /// Create a series command handler using an initialized [Dio] client. + ReadarrControllerBookFile(this._client); + + /// Handler for [episodefile/{id}](https://github.com/Readarr/Readarr/wiki/EpisodeFile#delete). + /// + /// Delete the given episode file. + /// + /// Required Parameters: + /// - `episodeFileId`: Episode ID for the episode to fetch + Future delete({ + required int bookFileId, + }) async => + _commandDeleteBookFile(_client, bookFileId: bookFileId); + + /// Handler for [episodefile/{id}](https://github.com/Readarr/Readarr/wiki/EpisodeFile#get). + /// + /// Returns the episode file with the matching episode ID. + /// + /// Required Parameters: + /// - `bookId`: Episode ID for the episode to fetch + Future> get({ + required int bookId, + }) async => + _commandGetBookFile(_client, bookId: bookId); + + /// Handler for [episodefile?seriesid={id}](https://github.com/Readarr/Readarr/wiki/EpisodeFile#getid). + /// + /// Returns all episode files for the given series. + /// + /// Required Parameters: + /// - `authorId`: Series ID for which to fetch episodes for + Future> getAuthor({ + required int authorId, + }) async => + _commandGetAuthorBookFiles(_client, authorId: authorId); +} diff --git a/lib/modules/readarr/api/src/controllers/book_file/delete_book_file.dart b/lib/modules/readarr/api/src/controllers/book_file/delete_book_file.dart new file mode 100644 index 0000000000..cfd73b993c --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/book_file/delete_book_file.dart @@ -0,0 +1,8 @@ +part of readarr_commands; + +Future _commandDeleteBookFile( + Dio client, { + required int bookFileId, +}) async { + await client.delete('bookFile/$bookFileId'); +} diff --git a/lib/modules/readarr/api/src/controllers/book_file/get_author_book_files.dart b/lib/modules/readarr/api/src/controllers/book_file/get_author_book_files.dart new file mode 100644 index 0000000000..6ee84799af --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/book_file/get_author_book_files.dart @@ -0,0 +1,13 @@ +part of readarr_commands; + +Future> _commandGetAuthorBookFiles( + Dio client, { + required int authorId, +}) async { + Response response = await client.get('bookFile', queryParameters: { + 'authorId': authorId, + }); + return (response.data as List) + .map((episode) => ReadarrBookFile.fromJson(episode)) + .toList(); +} diff --git a/lib/modules/readarr/api/src/controllers/book_file/get_book_file.dart b/lib/modules/readarr/api/src/controllers/book_file/get_book_file.dart new file mode 100644 index 0000000000..9055a28f97 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/book_file/get_book_file.dart @@ -0,0 +1,13 @@ +part of readarr_commands; + +Future> _commandGetBookFile( + Dio client, { + required int bookId, +}) async { + Response response = await client.get('bookFile', queryParameters: { + 'bookId': bookId, + }); + return (response.data as List) + .map((episode) => ReadarrBookFile.fromJson(episode)) + .toList(); +} diff --git a/lib/modules/readarr/api/src/controllers/calendar.dart b/lib/modules/readarr/api/src/controllers/calendar.dart new file mode 100644 index 0000000000..cd74ffed32 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/calendar.dart @@ -0,0 +1,33 @@ +part of readarr_commands; + +/// Facilitates, encapsulates, and manages individual calls related to calendar within Readarr. +/// +/// [ReadarrControllerCalendar] internally handles routing the HTTP client to the API calls. +class ReadarrControllerCalendar { + final Dio _client; + + /// Create a series command handler using an initialized [Dio] client. + ReadarrControllerCalendar(this._client); + + /// Handler for [calendar](https://github.com/Readarr/Readarr/wiki/Calendar#get). + /// + /// Gets upcoming episodes. + /// If start/end are not supplied episodes airing today and tomorrow will be returned. + Future> get({ + DateTime? start, + DateTime? end, + bool? unmonitored, + bool? includeSeries, + bool? includeEpisodeFile, + bool? includeEpisodeImages, + }) async => + _commandGetCalendar( + _client, + start: start, + end: end, + unmonitored: unmonitored, + includeAuthor: includeSeries, + includeEpisodeFile: includeEpisodeFile, + includeBookImages: includeEpisodeImages, + ); +} diff --git a/lib/modules/readarr/api/src/controllers/calendar/get_calendar.dart b/lib/modules/readarr/api/src/controllers/calendar/get_calendar.dart new file mode 100644 index 0000000000..c37de5606c --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/calendar/get_calendar.dart @@ -0,0 +1,23 @@ +part of readarr_commands; + +Future> _commandGetCalendar( + Dio client, { + DateTime? start, + DateTime? end, + bool? unmonitored, + bool? includeAuthor, + bool? includeEpisodeFile, + bool? includeBookImages, +}) async { + Response response = await client.get('calendar', queryParameters: { + if (start != null) 'start': DateFormat('y-MM-dd').format(start), + if (end != null) 'end': DateFormat('y-MM-dd').format(end), + if (unmonitored != null) 'unmonitored': unmonitored, + if (includeAuthor != null) 'includeAuthor': includeAuthor, + if (includeEpisodeFile != null) 'includeEpisodeFile': includeEpisodeFile, + if (includeBookImages != null) 'includeBookImages': includeBookImages, + }); + return (response.data as List) + .map((calendar) => ReadarrBook.fromJson(calendar)) + .toList(); +} diff --git a/lib/modules/readarr/api/src/controllers/command.dart b/lib/modules/readarr/api/src/controllers/command.dart new file mode 100644 index 0000000000..ca7261a504 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/command.dart @@ -0,0 +1,110 @@ +part of readarr_commands; + +/// Facilitates, encapsulates, and manages individual calls related to commands within Readarr. +/// +/// [ReadarrControllerCommand] internally handles routing the HTTP client to the API calls. +class ReadarrControllerCommand { + final Dio _client; + + /// Create a series command handler using an initialized [Dio] client. + ReadarrControllerCommand(this._client); + + /// Handler for [command (Backup)](https://github.com/Readarr/Readarr/wiki/Command#backup). + /// + /// Instruct Readarr to perform a backup of its database and config file (nzbdrone.db and config.xml). + Future backup() async => _commandBackup(_client); + + /// Handler for [command (AuthorSearch)](https://github.com/Readarr/Readarr/wiki/Command#episodesearch). + /// + /// Search for an author. + /// + /// Required Parameters: + /// - `authorId`: Author ID to search for + Future authorSearch({ + required int authorId, + }) async => + _commandAuthorSearch(_client, authorId: authorId); + + /// Handler for [command (EpisodeSearch)](https://github.com/Readarr/Readarr/wiki/Command#episodesearch). + /// + /// Search for one or more episodes. + /// + /// Required Parameters: + /// - `bookIds`: List of episode identifiers to search for + Future bookSearch({ + required List bookIds, + }) async => + _commandBookSearch(_client, bookIds: bookIds); + + /// Handler for [command](https://github.com/Readarr/Readarr/wiki/Command#get). + /// + /// Queries the status of a previously started command, or all currently started commands. + Future> queue() async => _commandCommandQueue(_client); + + /// Handler for [command (MissingEpisodeSearch)](https://github.com/Readarr/Readarr/wiki/Command#missingepisodesearch). + /// + /// Instruct Readarr to perform a backlog search of missing episodes (Similar functionality to Sickbeard). + Future missingBooksSearch() async => + _commandMissingBooksSearch(_client); + + /// Handler for [command (RefreshMonitoredDownloads)](https://github.com/Readarr/Readarr/wiki/Command). + /// + /// Refresh the actively monitored downloads in the queue. + Future refreshMonitoredDownloads() async => + _commandRefreshMonitoredDownloads(_client); + + /// Handler for [command (RefreshSeries)](https://github.com/Readarr/Readarr/wiki/Command#refreshseries). + /// + /// Refresh series information from trakt and rescan disk. + /// If no `authorId` is supplied, all series are refreshed. + /// + /// Optional Parameters: + /// - `authorId`: Series ID for the series to refresh + Future refreshAuthor({ + int? authorId, + }) async => + _commandRefreshAuthor(_client, authorId: authorId); + + /// Handler for [command (RefreshBook)](https://github.com/Readarr/Readarr/wiki/Command#refreshseries). + /// + /// Refresh book information from trakt and rescan disk. + /// If no `bookId` is supplied, all series are refreshed. + /// + /// Optional Parameters: + /// - `authorId`: Series ID for the series to refresh + Future refreshBook({ + int? bookId, + }) async => + _commandRefreshBook(_client, bookId: bookId); + + /// Handler for [command (RescanSeries)](https://github.com/Readarr/Readarr/wiki/Command#rescanseries). + /// + /// Refresh rescan disk for a single series. + /// If no `authorId` is supplied, all series are rescanned. + /// + /// Optional Parameters: + /// - `authorId`: Series ID for the series to refresh + Future rescanAuthor({ + int? authorId, + }) async => + _commandRescanSeries(_client, authorId: authorId); + + /// Handler for [command (RssSync)](https://github.com/Readarr/Readarr/wiki/Command#rsssync). + /// + /// Instruct Readarr to perform an RSS sync with all enabled indexers. + Future rssSync() async => _commandRSSSync(_client); + + /// Handler for [command (EpisodeSearch)](https://github.com/Readarr/Readarr/wiki/Command#episodesearch). + /// + /// Search for all episodes of a particular season. + /// + /// Required Parameters: + /// - `authorId`: Series identifier + /// - `seasonNumber`: Season number to search for + Future seasonSearch({ + required int authorId, + required int seasonNumber, + }) async => + _commandSeasonSearch(_client, + authorId: authorId, seasonNumber: seasonNumber); +} diff --git a/lib/modules/readarr/api/src/controllers/command/author_search.dart b/lib/modules/readarr/api/src/controllers/command/author_search.dart new file mode 100644 index 0000000000..edee820874 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/command/author_search.dart @@ -0,0 +1,12 @@ +part of readarr_commands; + +Future _commandAuthorSearch( + Dio client, { + required int authorId, +}) async { + Response response = await client.post('command', data: { + 'name': 'AuthorSearch', + 'authorId': authorId, + }); + return ReadarrCommand.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/command/backup.dart b/lib/modules/readarr/api/src/controllers/command/backup.dart new file mode 100644 index 0000000000..7a5ea17f6b --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/command/backup.dart @@ -0,0 +1,8 @@ +part of readarr_commands; + +Future _commandBackup(Dio client) async { + Response response = await client.post('command', data: { + 'name': 'Backup', + }); + return ReadarrCommand.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/command/book_search.dart b/lib/modules/readarr/api/src/controllers/command/book_search.dart new file mode 100644 index 0000000000..67cad4482a --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/command/book_search.dart @@ -0,0 +1,12 @@ +part of readarr_commands; + +Future _commandBookSearch( + Dio client, { + required List bookIds, +}) async { + Response response = await client.post('command', data: { + 'name': 'BookSearch', + 'bookIds': bookIds, + }); + return ReadarrCommand.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/command/missing_episode_search.dart b/lib/modules/readarr/api/src/controllers/command/missing_episode_search.dart new file mode 100644 index 0000000000..220d91c303 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/command/missing_episode_search.dart @@ -0,0 +1,8 @@ +part of readarr_commands; + +Future _commandMissingBooksSearch(Dio client) async { + Response response = await client.post('command', data: { + 'name': 'missingBookSearch', + }); + return ReadarrCommand.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/command/queue.dart b/lib/modules/readarr/api/src/controllers/command/queue.dart new file mode 100644 index 0000000000..d5b6fb5287 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/command/queue.dart @@ -0,0 +1,8 @@ +part of readarr_commands; + +Future> _commandCommandQueue(Dio client) async { + Response response = await client.get('command'); + return (response.data as List) + .map((command) => ReadarrCommand.fromJson(command)) + .toList(); +} diff --git a/lib/modules/readarr/api/src/controllers/command/refresh_author.dart b/lib/modules/readarr/api/src/controllers/command/refresh_author.dart new file mode 100644 index 0000000000..5fb01e3152 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/command/refresh_author.dart @@ -0,0 +1,12 @@ +part of readarr_commands; + +Future _commandRefreshAuthor( + Dio client, { + int? authorId, +}) async { + Response response = await client.post('command', data: { + 'name': 'RefreshAuthor', + if (authorId != null) 'authorId': authorId, + }); + return ReadarrCommand.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/command/refresh_book.dart b/lib/modules/readarr/api/src/controllers/command/refresh_book.dart new file mode 100644 index 0000000000..58e26d1937 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/command/refresh_book.dart @@ -0,0 +1,12 @@ +part of readarr_commands; + +Future _commandRefreshBook( + Dio client, { + int? bookId, +}) async { + Response response = await client.post('command', data: { + 'name': 'RefreshBook', + if (bookId != null) 'bookId': bookId, + }); + return ReadarrCommand.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/command/refresh_monitored_downloads.dart b/lib/modules/readarr/api/src/controllers/command/refresh_monitored_downloads.dart new file mode 100644 index 0000000000..a17e592ff0 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/command/refresh_monitored_downloads.dart @@ -0,0 +1,8 @@ +part of readarr_commands; + +Future _commandRefreshMonitoredDownloads(Dio client) async { + Response response = await client.post('command', data: { + 'name': 'RefreshMonitoredDownloads', + }); + return ReadarrCommand.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/command/rescan_author.dart b/lib/modules/readarr/api/src/controllers/command/rescan_author.dart new file mode 100644 index 0000000000..81ba0a07a8 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/command/rescan_author.dart @@ -0,0 +1,12 @@ +part of readarr_commands; + +Future _commandRescanSeries( + Dio client, { + int? authorId, +}) async { + Response response = await client.post('command', data: { + 'name': 'RescanSeries', + if (authorId != null) 'authorId': authorId, + }); + return ReadarrCommand.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/command/rss_sync.dart b/lib/modules/readarr/api/src/controllers/command/rss_sync.dart new file mode 100644 index 0000000000..b3ba52e291 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/command/rss_sync.dart @@ -0,0 +1,8 @@ +part of readarr_commands; + +Future _commandRSSSync(Dio client) async { + Response response = await client.post('command', data: { + 'name': 'RssSync', + }); + return ReadarrCommand.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/command/season_search.dart b/lib/modules/readarr/api/src/controllers/command/season_search.dart new file mode 100644 index 0000000000..8892eaaa61 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/command/season_search.dart @@ -0,0 +1,14 @@ +part of readarr_commands; + +Future _commandSeasonSearch( + Dio client, { + required int authorId, + required int seasonNumber, +}) async { + Response response = await client.post('command', data: { + 'name': 'SeasonSearch', + 'authorId': authorId, + 'seasonNumber': seasonNumber, + }); + return ReadarrCommand.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/history.dart b/lib/modules/readarr/api/src/controllers/history.dart new file mode 100644 index 0000000000..f810b9ec64 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/history.dart @@ -0,0 +1,58 @@ +part of readarr_commands; + +/// Facilitates, encapsulates, and manages individual calls related to history within Readarr. +/// +/// [ReadarrControllerHistory] internally handles routing the HTTP client to the API calls. +class ReadarrControllerHistory { + final Dio _client; + + /// Create a series command handler using an initialized [Dio] client. + ReadarrControllerHistory(this._client); + + /// Handler for [history](https://github.com/Readarr/Readarr/wiki/History#get). + /// + /// Gets history (grabs/failures/completed). + /// + /// Optional Parameters: + /// - `page`: The page of history to fetch (Default: 1) + /// - `pageSize`: The amount of items per page to fetch + /// - `sortKey`: [ReadarrHistorySortKey] object containing the sorting key + /// - `sortDirection`: [ReadarrSortDirection] object containing the sorting direction + /// - `bookId`: The episode ID to filter results for + /// - `downloadId`: The download ID to filter results for + Future get({ + int? page, + int? pageSize, + ReadarrHistorySortKey? sortKey, + ReadarrSortDirection? sortDirection, + int? bookId, + String? downloadId, + bool? includeAuthor, + bool? includeBook, + }) async => + _commandGetHistory( + _client, + sortKey: sortKey, + page: page, + pageSize: pageSize, + sortDirection: sortDirection, + bookId: bookId, + downloadId: downloadId, + includeBook: includeBook, + includeAuthor: includeAuthor, + ); + + Future> getByAuthor({ + required int authorId, + int? bookId, + bool? includeAuthor, + bool? includeBook, + }) async => + _commandGetHistoryByAuthor( + _client, + authorId: authorId, + bookId: bookId, + includeBook: includeBook, + includeAuthor: includeAuthor, + ); +} diff --git a/lib/modules/readarr/api/src/controllers/history/get_history.dart b/lib/modules/readarr/api/src/controllers/history/get_history.dart new file mode 100644 index 0000000000..ef294e92e7 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/history/get_history.dart @@ -0,0 +1,25 @@ +part of readarr_commands; + +Future _commandGetHistory( + Dio client, { + ReadarrHistorySortKey? sortKey, + int? page, + int? pageSize, + ReadarrSortDirection? sortDirection, + int? bookId, + String? downloadId, + bool? includeAuthor, + bool? includeBook, +}) async { + Response response = await client.get('history', queryParameters: { + if (sortKey != null) 'sortKey': sortKey.value, + if (page != null) 'page': page, + if (pageSize != null) 'pageSize': pageSize, + if (sortDirection != null) 'sortDirection': sortDirection.value, + if (bookId != null) 'bookId': bookId, + if (downloadId != null) 'downloadId': downloadId, + if (includeAuthor != null) 'includeAuthor': includeAuthor, + if (includeBook != null) 'includeBook': includeBook, + }); + return ReadarrHistory.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/history/get_history_by_author.dart b/lib/modules/readarr/api/src/controllers/history/get_history_by_author.dart new file mode 100644 index 0000000000..c41104105a --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/history/get_history_by_author.dart @@ -0,0 +1,19 @@ +part of readarr_commands; + +Future> _commandGetHistoryByAuthor( + Dio client, { + required int authorId, + int? bookId, + bool? includeAuthor, + bool? includeBook, +}) async { + Response response = await client.get('history/author', queryParameters: { + 'authorId': authorId, + if (bookId != null) 'bookId': bookId, + if (includeAuthor != null) 'includeAuthor': includeAuthor, + if (includeBook != null) 'includeBook': includeBook, + }); + return (response.data as List) + .map((series) => ReadarrHistoryRecord.fromJson(series)) + .toList(); +} diff --git a/lib/modules/readarr/api/src/controllers/import_list.dart b/lib/modules/readarr/api/src/controllers/import_list.dart new file mode 100644 index 0000000000..2d9934b51a --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/import_list.dart @@ -0,0 +1,14 @@ +part of readarr_commands; + +/// Facilitates, encapsulates, and manages individual calls related to import lists within Readarr. +/// +/// [ReadarrControllerImportList] internally handles routing the HTTP client to the API calls. +class ReadarrControllerImportList { + final Dio _client; + + /// Create an import list command handler using an initialized [Dio] client. + ReadarrControllerImportList(this._client); + + Future> get() async => + _commandGetExclusionList(_client); +} diff --git a/lib/modules/readarr/api/src/controllers/import_list/get_exclusion_list.dart b/lib/modules/readarr/api/src/controllers/import_list/get_exclusion_list.dart new file mode 100644 index 0000000000..caa5b6e24d --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/import_list/get_exclusion_list.dart @@ -0,0 +1,10 @@ +part of readarr_commands; + +Future> _commandGetExclusionList( + Dio client, +) async { + Response response = await client.get('importlistexclusion'); + return (response.data as List) + .map((series) => ReadarrExclusion.fromJson(series)) + .toList(); +} diff --git a/lib/modules/readarr/api/src/controllers/profile.dart b/lib/modules/readarr/api/src/controllers/profile.dart new file mode 100644 index 0000000000..961872b045 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/profile.dart @@ -0,0 +1,23 @@ +part of readarr_commands; + +/// Facilitates, encapsulates, and manages individual calls related to profiles within Readarr. +/// +/// [ReadarrControllerProfile] internally handles routing the HTTP client to the API calls. +class ReadarrControllerProfile { + final Dio _client; + + /// Create a series command handler using an initialized [Dio] client. + ReadarrControllerProfile(this._client); + + /// Handler for [profile](https://github.com/Readarr/Readarr/wiki/Profile#get). + /// + /// Returns a list of all quality profiles. + Future> getQualityProfiles() async => + _commandGetQualityProfiles(_client); + + /// Handler for [language profile](https://github.com/Readarr/Readarr/wiki/Profile#get). + /// + /// Returns a list of all language profiles. + Future> getMetadataProfiles() async => + _commandGetMetadataProfiles(_client); +} diff --git a/lib/modules/readarr/api/src/controllers/profile/get_metadata_profiles.dart b/lib/modules/readarr/api/src/controllers/profile/get_metadata_profiles.dart new file mode 100644 index 0000000000..0c00f8f484 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/profile/get_metadata_profiles.dart @@ -0,0 +1,10 @@ +part of readarr_commands; + +Future> _commandGetMetadataProfiles( + Dio client, +) async { + Response response = await client.get('metadataprofile'); + return (response.data as List) + .map((profile) => ReadarrMetadataProfile.fromJson(profile)) + .toList(); +} diff --git a/lib/modules/readarr/api/src/controllers/profile/get_quality_profiles.dart b/lib/modules/readarr/api/src/controllers/profile/get_quality_profiles.dart new file mode 100644 index 0000000000..c0c381a839 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/profile/get_quality_profiles.dart @@ -0,0 +1,10 @@ +part of readarr_commands; + +Future> _commandGetQualityProfiles( + Dio client, +) async { + Response response = await client.get('qualityprofile'); + return (response.data as List) + .map((profile) => ReadarrQualityProfile.fromJson(profile)) + .toList(); +} diff --git a/lib/modules/readarr/api/src/controllers/queue.dart b/lib/modules/readarr/api/src/controllers/queue.dart new file mode 100644 index 0000000000..4bdf4c36d1 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/queue.dart @@ -0,0 +1,66 @@ +part of readarr_commands; + +/// Facilitates, encapsulates, and manages individual calls related to queue within Readarr. +/// +/// [ReadarrControllerQueue] internally handles routing the HTTP client to the API calls. +class ReadarrControllerQueue { + final Dio _client; + + /// Create a series command handler using an initialized [Dio] client. + ReadarrControllerQueue(this._client); + + /// Handler for [queue](https://github.com/Readarr/Readarr/wiki/Queue#get). + /// + /// Gets currently downloading (queue) information. + Future get({ + bool? includeUnknownAuthorItems, + bool? includeAuthor, + bool? includeBook, + ReadarrSortDirection? sortDirection, + ReadarrQueueSortKey? sortKey, + int? page, + int? pageSize, + }) async => + _commandGetQueue( + _client, + includeUnknownAuthorItems: includeUnknownAuthorItems, + includeBook: includeBook, + includeAuthor: includeAuthor, + sortDirection: sortDirection, + sortKey: sortKey, + page: page, + pageSize: pageSize, + ); + + /// Handler for [queue/details](https://github.com/Readarr/Readarr/wiki/Queue#get). + /// + /// Gets currently downloading (queue) information. + Future> getDetails({ + int? authorId, + List? bookIds, + bool? includeAuthor, + bool? includeBook, + }) async => + _commandGetQueueDetails( + _client, + authorId: authorId, + bookIds: bookIds, + includeAuthor: includeAuthor, + includeBook: includeBook, + ); + + /// Handler for [queue](https://github.com/Readarr/Readarr/wiki/Queue#delete). + /// + /// Deletes an item from the queue and download client.. + Future delete({ + required int id, + bool? removeFromClient, + bool? blocklist, + }) async => + _commandDeleteQueue( + _client, + id: id, + blocklist: blocklist, + removeFromClient: removeFromClient, + ); +} diff --git a/lib/modules/readarr/api/src/controllers/queue/delete_queue.dart b/lib/modules/readarr/api/src/controllers/queue/delete_queue.dart new file mode 100644 index 0000000000..0cb51c02b4 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/queue/delete_queue.dart @@ -0,0 +1,13 @@ +part of readarr_commands; + +Future _commandDeleteQueue( + Dio client, { + required int id, + bool? removeFromClient, + bool? blocklist, +}) async { + await client.delete('queue/$id', queryParameters: { + if (removeFromClient != null) 'removeFromClient': removeFromClient, + if (blocklist != null) 'blocklist': blocklist, + }); +} diff --git a/lib/modules/readarr/api/src/controllers/queue/get_queue.dart b/lib/modules/readarr/api/src/controllers/queue/get_queue.dart new file mode 100644 index 0000000000..731a7067d7 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/queue/get_queue.dart @@ -0,0 +1,24 @@ +part of readarr_commands; + +Future _commandGetQueue( + Dio client, { + bool? includeUnknownAuthorItems, + bool? includeAuthor, + bool? includeBook, + ReadarrSortDirection? sortDirection, + ReadarrQueueSortKey? sortKey, + int? page, + int? pageSize, +}) async { + Response response = await client.get('queue', queryParameters: { + if (includeUnknownAuthorItems != null) + 'includeUnknownAuthorItems': includeUnknownAuthorItems, + if (includeAuthor != null) 'includeAuthor': includeAuthor, + if (includeBook != null) 'includeBook': includeBook, + if (sortDirection != null) 'sortDirection': sortDirection.value, + if (sortKey != null) 'sortKey': sortKey.value, + if (page != null) 'page': pageSize, + if (pageSize != null) 'pageSize': pageSize, + }); + return ReadarrQueue.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/queue/get_queue_details.dart b/lib/modules/readarr/api/src/controllers/queue/get_queue_details.dart new file mode 100644 index 0000000000..75d7f4a015 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/queue/get_queue_details.dart @@ -0,0 +1,19 @@ +part of readarr_commands; + +Future> _commandGetQueueDetails( + Dio client, { + int? authorId, + List? bookIds, + bool? includeAuthor, + bool? includeBook, +}) async { + Response response = await client.get('queue/details', queryParameters: { + if (bookIds != null) 'bookIds': bookIds, + if (authorId != null) 'authorId': authorId, + if (includeAuthor != null) 'includeAuthor': includeAuthor, + if (includeBook != null) 'includeBook': includeBook, + }); + return (response.data as List) + .map((series) => ReadarrQueueRecord.fromJson(series)) + .toList(); +} diff --git a/lib/modules/readarr/api/src/controllers/release.dart b/lib/modules/readarr/api/src/controllers/release.dart new file mode 100644 index 0000000000..48f3760c7a --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/release.dart @@ -0,0 +1,37 @@ +part of readarr_commands; + +/// Facilitates, encapsulates, and manages individual calls related to releases within Readarr. +/// +/// [ReadarrControllerRelease] internally handles routing the HTTP client to the API calls. +class ReadarrControllerRelease { + final Dio _client; + + /// Create a series command handler using an initialized [Dio] client. + ReadarrControllerRelease(this._client); + + /// Handler for [release](https://github.com/Readarr/Readarr/wiki/Release#get). + /// + /// Returns the a list of releases for the episode. + Future> get({ + required int bookId, + }) async => + _commandGetReleases(_client, bookId: bookId); + + /// Handler for [release](https://github.com/Readarr/Readarr/wiki/Release#get). + /// + /// Returns the a list of releases for the season. + Future> getAuthorPack({ + required int authorId, + }) async => + _commandGetAuthorReleases(_client, authorId: authorId); + + /// Handler for [release](https://github.com/Readarr/Readarr/wiki/Release#post). + /// + /// Adds a previously searched release to the download client, if the release is still in Readarr's search cache (30 minute cache). + /// If the release is not found in the cache Readarr will return a 404. + Future add({ + required String guid, + required int indexerId, + }) async => + _commandAddRelease(_client, guid: guid, indexerId: indexerId); +} diff --git a/lib/modules/readarr/api/src/controllers/release/add_release.dart b/lib/modules/readarr/api/src/controllers/release/add_release.dart new file mode 100644 index 0000000000..cbfea46e7f --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/release/add_release.dart @@ -0,0 +1,16 @@ +part of readarr_commands; + +Future _commandAddRelease( + Dio client, { + required String guid, + required int indexerId, +}) async { + Response response = await client.post( + 'release', + data: { + 'guid': guid, + 'indexerId': indexerId, + }, + ); + return ReadarrAddedRelease.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/release/get_release.dart b/lib/modules/readarr/api/src/controllers/release/get_release.dart new file mode 100644 index 0000000000..5ada145f68 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/release/get_release.dart @@ -0,0 +1,13 @@ +part of readarr_commands; + +Future> _commandGetReleases( + Dio client, { + required int bookId, +}) async { + Response response = await client.get('release', queryParameters: { + 'bookId': bookId, + }); + return (response.data as List) + .map((series) => ReadarrRelease.fromJson(series)) + .toList(); +} diff --git a/lib/modules/readarr/api/src/controllers/release/get_season_release.dart b/lib/modules/readarr/api/src/controllers/release/get_season_release.dart new file mode 100644 index 0000000000..bbd630bec3 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/release/get_season_release.dart @@ -0,0 +1,13 @@ +part of readarr_commands; + +Future> _commandGetAuthorReleases( + Dio client, { + required int authorId, +}) async { + Response response = await client.get('release', queryParameters: { + 'authorId': authorId, + }); + return (response.data as List) + .map((series) => ReadarrRelease.fromJson(series)) + .toList(); +} diff --git a/lib/modules/readarr/api/src/controllers/root_folder.dart b/lib/modules/readarr/api/src/controllers/root_folder.dart new file mode 100644 index 0000000000..23408ac820 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/root_folder.dart @@ -0,0 +1,16 @@ +part of readarr_commands; + +/// Facilitates, encapsulates, and manages individual calls related to root folders within Readarr. +/// +/// [ReadarrControllerRootFolder] internally handles routing the HTTP client to the API calls. +class ReadarrControllerRootFolder { + final Dio _client; + + /// Create a series command handler using an initialized [Dio] client. + ReadarrControllerRootFolder(this._client); + + /// Handler for [rootfolder](https://github.com/Readarr/Readarr/wiki/Rootfolder#get). + /// + /// Returns a list of root folders. + Future> get() async => _commandGetRootFolders(_client); +} diff --git a/lib/modules/readarr/api/src/controllers/root_folder/get_root_folders.dart b/lib/modules/readarr/api/src/controllers/root_folder/get_root_folders.dart new file mode 100644 index 0000000000..7db0d8b7b2 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/root_folder/get_root_folders.dart @@ -0,0 +1,8 @@ +part of readarr_commands; + +Future> _commandGetRootFolders(Dio client) async { + Response response = await client.get('rootfolder'); + return (response.data as List) + .map((folder) => ReadarrRootFolder.fromJson(folder)) + .toList(); +} diff --git a/lib/modules/readarr/api/src/controllers/system.dart b/lib/modules/readarr/api/src/controllers/system.dart new file mode 100644 index 0000000000..7cc40c5b7e --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/system.dart @@ -0,0 +1,16 @@ +part of readarr_commands; + +/// Facilitates, encapsulates, and manages individual calls related to system within Readarr. +/// +/// [ReadarrControllerSystem] internally handles routing the HTTP client to the API calls. +class ReadarrControllerSystem { + final Dio _client; + + /// Create a series command handler using an initialized [Dio] client. + ReadarrControllerSystem(this._client); + + /// Handler for [system/status](https://github.com/Readarr/Readarr/wiki/System-Status#get). + /// + /// Returns system status information. + Future getStatus() async => _commandGetStatus(_client); +} diff --git a/lib/modules/readarr/api/src/controllers/system/get_status.dart b/lib/modules/readarr/api/src/controllers/system/get_status.dart new file mode 100644 index 0000000000..345f36d23c --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/system/get_status.dart @@ -0,0 +1,6 @@ +part of readarr_commands; + +Future _commandGetStatus(Dio client) async { + Response response = await client.get('system/status'); + return ReadarrStatus.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/tag.dart b/lib/modules/readarr/api/src/controllers/tag.dart new file mode 100644 index 0000000000..0a1df14b3b --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/tag.dart @@ -0,0 +1,48 @@ +part of readarr_commands; + +/// Facilitates, encapsulates, and manages individual calls related to tags within Readarr. +/// +/// [ReadarrControllerTag] internally handles routing the HTTP client to the API calls. +class ReadarrControllerTag { + final Dio _client; + + /// Create a series command handler using an initialized [Dio] client. + ReadarrControllerTag(this._client); + + /// Handler for [tag](https://github.com/Readarr/Readarr/wiki/Tag#post). + /// + /// Adds a new tag. + Future create({ + required String label, + }) async => + _commandAddTag(_client, label: label); + + /// Handler for [tag/{id}](https://github.com/Readarr/Readarr/wiki/Tag#deleteid). + /// + /// Delete the tag with the given ID. + Future delete({ + required int id, + }) async => + _commandDeleteTag(_client, id: id); + + /// Handler for [tag/{id}](https://github.com/Readarr/Readarr/wiki/Tag#getid). + /// + /// Returns the tag with the matching ID. + Future get({ + required int id, + }) async => + _commandGetTag(_client, id: id); + + /// Handler for [tag](https://github.com/Readarr/Readarr/wiki/Tag#get). + /// + /// Returns a list of all tags. + Future> getAll() async => _commandGetAllTags(_client); + + /// Handler for [tag](https://github.com/Readarr/Readarr/wiki/Tag#put). + /// + /// Update an existing tag. + Future update({ + required ReadarrTag tag, + }) async => + _commandUpdateTag(_client, tag: tag); +} diff --git a/lib/modules/readarr/api/src/controllers/tag/add_tag.dart b/lib/modules/readarr/api/src/controllers/tag/add_tag.dart new file mode 100644 index 0000000000..abe57a2edc --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/tag/add_tag.dart @@ -0,0 +1,11 @@ +part of readarr_commands; + +Future _commandAddTag( + Dio client, { + required String label, +}) async { + Response response = await client.post('tag', data: { + 'label': label, + }); + return ReadarrTag.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/tag/delete_tag.dart b/lib/modules/readarr/api/src/controllers/tag/delete_tag.dart new file mode 100644 index 0000000000..238ea9ec52 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/tag/delete_tag.dart @@ -0,0 +1,9 @@ +part of readarr_commands; + +Future _commandDeleteTag( + Dio client, { + required int id, +}) async { + await client.delete('tag/$id'); + return; +} diff --git a/lib/modules/readarr/api/src/controllers/tag/get_all_tags.dart b/lib/modules/readarr/api/src/controllers/tag/get_all_tags.dart new file mode 100644 index 0000000000..89eb7398c5 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/tag/get_all_tags.dart @@ -0,0 +1,6 @@ +part of readarr_commands; + +Future> _commandGetAllTags(Dio client) async { + Response response = await client.get('tag'); + return (response.data as List).map((tag) => ReadarrTag.fromJson(tag)).toList(); +} diff --git a/lib/modules/readarr/api/src/controllers/tag/get_tag.dart b/lib/modules/readarr/api/src/controllers/tag/get_tag.dart new file mode 100644 index 0000000000..b47b44a3f2 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/tag/get_tag.dart @@ -0,0 +1,9 @@ +part of readarr_commands; + +Future _commandGetTag( + Dio client, { + required int id, +}) async { + Response response = await client.get('tag/$id'); + return ReadarrTag.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/tag/update_tag.dart b/lib/modules/readarr/api/src/controllers/tag/update_tag.dart new file mode 100644 index 0000000000..a4aaecc420 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/tag/update_tag.dart @@ -0,0 +1,9 @@ +part of readarr_commands; + +Future _commandUpdateTag( + Dio client, { + required ReadarrTag tag, +}) async { + Response response = await client.put('tag', data: tag.toJson()); + return ReadarrTag.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/controllers/wanted.dart b/lib/modules/readarr/api/src/controllers/wanted.dart new file mode 100644 index 0000000000..1b7e2bac87 --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/wanted.dart @@ -0,0 +1,38 @@ +part of readarr_commands; + +/// Facilitates, encapsulates, and manages individual calls related to wanted/missing episodes within Readarr. +/// +/// [ReadarrControllerWanted] internally handles routing the HTTP client to the API calls. +class ReadarrControllerWanted { + final Dio _client; + + /// Create a series command handler using an initialized [Dio] client. + ReadarrControllerWanted(this._client); + + /// Handler for [wanted/missing](https://github.com/Readarr/Readarr/wiki/Wanted-Missing#get). + /// + /// Returns a list of missing episode (episodes without files). + /// + /// Optional Parameters: + /// - `sortDir`: Sorting direction of the results + /// - `sortKey`: The key used for sorting the results + /// - `page`: Page of results to fetch + /// - `pageSize`: Size of the page to fetch + Future getMissing({ + ReadarrSortDirection? sortDir, + ReadarrWantedMissingSortKey? sortKey, + int? page, + int? pageSize, + bool? includeSeries, + bool? includeImages, + }) async => + _commandGetMissing( + _client, + sortDir: sortDir, + sortKey: sortKey, + page: page, + pageSize: pageSize, + includeSeries: includeSeries, + includeImages: includeImages, + ); +} diff --git a/lib/modules/readarr/api/src/controllers/wanted/get_missing.dart b/lib/modules/readarr/api/src/controllers/wanted/get_missing.dart new file mode 100644 index 0000000000..75a4f9d25c --- /dev/null +++ b/lib/modules/readarr/api/src/controllers/wanted/get_missing.dart @@ -0,0 +1,21 @@ +part of readarr_commands; + +Future _commandGetMissing( + Dio client, { + ReadarrSortDirection? sortDir, + ReadarrWantedMissingSortKey? sortKey, + int? page, + int? pageSize, + bool? includeSeries, + bool? includeImages, +}) async { + Response response = await client.get('wanted/missing', queryParameters: { + if (sortDir != null) 'sortDir': sortDir.value, + if (sortKey != null) 'sortKey': sortKey.value, + if (page != null) 'page': page, + if (pageSize != null) 'pageSize': pageSize, + if (includeSeries != null) 'includeSeries': includeSeries, + if (includeImages != null) 'includeImages': includeImages, + }); + return ReadarrMissing.fromJson(response.data); +} diff --git a/lib/modules/readarr/api/src/models/author/author.dart b/lib/modules/readarr/api/src/models/author/author.dart new file mode 100644 index 0000000000..095e0a3190 --- /dev/null +++ b/lib/modules/readarr/api/src/models/author/author.dart @@ -0,0 +1,112 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'author.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrAuthor { + @JsonKey(name: 'authorName') + String? title; + + @JsonKey(name: 'sortName') + String? sortTitle; + + @JsonKey(name: 'status') + String? status; + + @JsonKey(name: 'ended') + bool? ended; + + @JsonKey(name: 'overview') + String? overview; + + @JsonKey(name: 'images') + List? images; + + @JsonKey(name: 'remotePoster') + String? remotePoster; + + @JsonKey(name: 'path') + String? path; + + @JsonKey(name: 'metadataProfileId') + int? metadataProfileId; + + @JsonKey(name: 'qualityProfileId') + int? qualityProfileId; + + @JsonKey(name: 'monitored') + bool? monitored; + + @JsonKey(name: 'monitorNewItems') + String? monitorNewItems; + + @JsonKey(name: 'foreignAuthorId') + String? foreignAuthorId; + + @JsonKey(name: 'cleanName') + String? cleanTitle; + + @JsonKey(name: 'titleSlug') + String? titleSlug; + + @JsonKey(name: 'folder') + String? folder; + + @JsonKey(name: 'rootFolderPath') + String? rootFolderPath; + + @JsonKey(name: 'genres') + List? genres; + + @JsonKey(name: 'tags') + List? tags; + + @JsonKey( + name: 'added', + toJson: ReadarrUtilities.dateTimeToJson, + fromJson: ReadarrUtilities.dateTimeFromJson, + ) + DateTime? added; + + @JsonKey(name: 'ratings') + ReadarrAuthorRating? ratings; + + @JsonKey(name: 'statistics') + ReadarrAuthorStatistics? statistics; + + @JsonKey(name: 'id') + int? id; + + ReadarrAuthor({ + this.title, + this.sortTitle, + this.status, + this.ended, + this.overview, + this.images, + this.remotePoster, + this.path, + this.qualityProfileId, + this.monitored, + this.cleanTitle, + this.titleSlug, + this.folder, + this.rootFolderPath, + this.genres, + this.tags, + this.added, + this.ratings, + this.statistics, + this.id, + }); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrAuthor.fromJson(Map json) => + _$ReadarrAuthorFromJson(json); + + Map toJson() => _$ReadarrAuthorToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/author/author_statistics.dart b/lib/modules/readarr/api/src/models/author/author_statistics.dart new file mode 100644 index 0000000000..5ed649a4a9 --- /dev/null +++ b/lib/modules/readarr/api/src/models/author/author_statistics.dart @@ -0,0 +1,40 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; + +part 'author_statistics.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrAuthorStatistics { + @JsonKey(name: 'seasonCount') + int? seasonCount; + + @JsonKey(name: 'bookFileCount') + int? episodeFileCount; + + @JsonKey(name: 'bookCount') + int? episodeCount; + + @JsonKey(name: 'totalBookCount') + int? totalEpisodeCount; + + @JsonKey(name: 'sizeOnDisk') + int? sizeOnDisk; + + @JsonKey(name: 'percentOfBooks') + double? percentOfEpisodes; + + ReadarrAuthorStatistics({ + this.episodeFileCount, + this.episodeCount, + this.totalEpisodeCount, + this.sizeOnDisk, + this.percentOfEpisodes, + }); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrAuthorStatistics.fromJson(Map json) => + _$ReadarrAuthorStatisticsFromJson(json); + Map toJson() => _$ReadarrAuthorStatisticsToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/author/image.dart b/lib/modules/readarr/api/src/models/author/image.dart new file mode 100644 index 0000000000..18edc950a7 --- /dev/null +++ b/lib/modules/readarr/api/src/models/author/image.dart @@ -0,0 +1,30 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; + +part 'image.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrImage { + @JsonKey(name: 'coverType') + String? coverType; + + @JsonKey(name: 'url') + String? url; + + @JsonKey(name: 'remoteUrl') + String? remoteUrl; + + ReadarrImage({ + this.coverType, + this.url, + this.remoteUrl, + }); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrImage.fromJson(Map json) => + _$ReadarrImageFromJson(json); + + Map toJson() => _$ReadarrImageToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/author/rating.dart b/lib/modules/readarr/api/src/models/author/rating.dart new file mode 100644 index 0000000000..cbadeffcd1 --- /dev/null +++ b/lib/modules/readarr/api/src/models/author/rating.dart @@ -0,0 +1,32 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; + +part 'rating.g.dart'; + +/// Model for series' rating values. +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrAuthorRating { + /// Number of votes for the rating score + @JsonKey(name: 'votes') + int? votes; + + /// Final score/value of the rating + @JsonKey(name: 'value') + double? value; + + ReadarrAuthorRating({ + this.votes, + this.value, + }); + + /// Returns a JSON-encoded string version of this object. + @override + String toString() => json.encode(this.toJson()); + + /// Deserialize a JSON map to a [ReadarrAuthorRating] object. + factory ReadarrAuthorRating.fromJson(Map json) => + _$ReadarrAuthorRatingFromJson(json); + + /// Serialize a [ReadarrAuthorRating] object to a JSON map. + Map toJson() => _$ReadarrAuthorRatingToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/author/season.dart b/lib/modules/readarr/api/src/models/author/season.dart new file mode 100644 index 0000000000..9d88ddc64c --- /dev/null +++ b/lib/modules/readarr/api/src/models/author/season.dart @@ -0,0 +1,34 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'season.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrAuthorSeason { + @JsonKey(name: 'seasonNumber') + int? seasonNumber; + + @JsonKey(name: 'monitored') + bool? monitored; + + @JsonKey(name: 'statistics') + ReadarrAuthorSeasonStatistics? statistics; + + @JsonKey(name: 'images') + List? images; + + ReadarrAuthorSeason({ + this.seasonNumber, + this.monitored, + this.statistics, + this.images, + }); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrAuthorSeason.fromJson(Map json) => + _$ReadarrAuthorSeasonFromJson(json); + Map toJson() => _$ReadarrAuthorSeasonToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/author/season_statistics.dart b/lib/modules/readarr/api/src/models/author/season_statistics.dart new file mode 100644 index 0000000000..ae4ea03e50 --- /dev/null +++ b/lib/modules/readarr/api/src/models/author/season_statistics.dart @@ -0,0 +1,54 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'season_statistics.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrAuthorSeasonStatistics { + @JsonKey( + name: 'previousAiring', + toJson: ReadarrUtilities.dateTimeToJson, + fromJson: ReadarrUtilities.dateTimeFromJson, + ) + DateTime? previousAiring; + + @JsonKey( + name: 'nextAiring', + toJson: ReadarrUtilities.dateTimeToJson, + fromJson: ReadarrUtilities.dateTimeFromJson, + ) + DateTime? nextAiring; + + @JsonKey(name: 'episodeFileCount') + int? episodeFileCount; + + @JsonKey(name: 'episodeCount') + int? episodeCount; + + @JsonKey(name: 'totalEpisodeCount') + int? totalEpisodeCount; + + @JsonKey(name: 'sizeOnDisk') + int? sizeOnDisk; + + @JsonKey(name: 'percentOfEpisodes') + double? percentOfEpisodes; + + ReadarrAuthorSeasonStatistics({ + this.previousAiring, + this.nextAiring, + this.episodeFileCount, + this.episodeCount, + this.totalEpisodeCount, + this.sizeOnDisk, + this.percentOfEpisodes, + }); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrAuthorSeasonStatistics.fromJson(Map json) => + _$ReadarrAuthorSeasonStatisticsFromJson(json); + Map toJson() => _$ReadarrAuthorSeasonStatisticsToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/book/book.dart b/lib/modules/readarr/api/src/models/book/book.dart new file mode 100644 index 0000000000..374910fb76 --- /dev/null +++ b/lib/modules/readarr/api/src/models/book/book.dart @@ -0,0 +1,104 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'book.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrBook { + @JsonKey(name: 'id') + int? id; + + @JsonKey(name: 'title') + String? title; + + @JsonKey(name: 'authorTitle') + String? authorTitle; + + @JsonKey(name: 'seriesTitle') + String? seriesTitle; + + @JsonKey(name: 'disambiguation') + String? disambiguation; + + @JsonKey(name: 'overview') + String? overview; + + @JsonKey(name: 'authorId') + int? authorId; + + @JsonKey(name: 'foreignBookId') + String? foreignBookId; + + @JsonKey(name: 'titleSlug') + String? titleSlug; + + @JsonKey(name: 'monitored') + bool? monitored; + + @JsonKey(name: 'anyEditionOk') + bool? anyEditionOk; + + @JsonKey(name: 'ratings') + ReadarrAuthorRating? ratings; + + @JsonKey( + name: 'releaseDate', + toJson: ReadarrUtilities.dateTimeToJson, + fromJson: ReadarrUtilities.dateTimeFromJson, + ) + DateTime? releaseDate; + + @JsonKey(name: 'pageCount') + int? pageCount; + + @JsonKey(name: 'genres') + List? genres; + + @JsonKey(name: 'author') + ReadarrAuthor? series; + + @JsonKey(name: 'images') + List? images; + + @JsonKey(name: 'statistics') + ReadarrAuthorStatistics? statistics; + + @JsonKey( + name: 'added', + toJson: ReadarrUtilities.dateTimeToJson, + fromJson: ReadarrUtilities.dateTimeFromJson, + ) + DateTime? added; + + @JsonKey(name: 'remoteCover') + String? remoteCover; + + ReadarrBook( + {this.title, + this.authorTitle, + this.seriesTitle, + this.disambiguation, + this.overview, + this.authorId, + this.foreignBookId, + this.titleSlug, + this.monitored, + this.anyEditionOk, + this.ratings, + this.releaseDate, + this.pageCount, + this.genres, + this.series, + this.images, + this.statistics, + this.added, + this.remoteCover}); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrBook.fromJson(Map json) => + _$ReadarrBookFromJson(json); + Map toJson() => _$ReadarrBookToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/book_file/book_file.dart b/lib/modules/readarr/api/src/models/book_file/book_file.dart new file mode 100644 index 0000000000..1562ab5025 --- /dev/null +++ b/lib/modules/readarr/api/src/models/book_file/book_file.dart @@ -0,0 +1,59 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'book_file.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrBookFile { + @JsonKey(name: 'authorId') + int? authorId; + + @JsonKey(name: 'bookId') + int? bookId; + + @JsonKey(name: 'path') + String? path; + + @JsonKey(name: 'size') + int? size; + + @JsonKey( + name: 'dateAdded', + fromJson: ReadarrUtilities.dateTimeFromJson, + toJson: ReadarrUtilities.dateTimeToJson, + ) + DateTime? dateAdded; + + @JsonKey(name: 'quality') + ReadarrBookFileQuality? quality; + + @JsonKey(name: 'qualityWeight') + int? qualityWeight; + + @JsonKey(name: 'qualityCutoffNotMet') + bool? qualityCutoffNotMet; + + @JsonKey(name: 'id') + int? id; + + ReadarrBookFile({ + this.authorId, + this.bookId, + this.path, + this.size, + this.dateAdded, + this.quality, + this.qualityWeight, + this.qualityCutoffNotMet, + this.id, + }); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrBookFile.fromJson(Map json) => + _$ReadarrBookFileFromJson(json); + + Map toJson() => _$ReadarrBookFileToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/book_file/book_file_quality.dart b/lib/modules/readarr/api/src/models/book_file/book_file_quality.dart new file mode 100644 index 0000000000..decc94d7e3 --- /dev/null +++ b/lib/modules/readarr/api/src/models/book_file/book_file_quality.dart @@ -0,0 +1,26 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'book_file_quality.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrBookFileQuality { + @JsonKey(name: 'quality') + ReadarrBookFileQualityQuality? quality; + + @JsonKey(name: 'revision') + ReadarrBookFileQualityRevision? revision; + + ReadarrBookFileQuality({ + this.quality, + this.revision, + }); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrBookFileQuality.fromJson(Map json) => + _$ReadarrBookFileQualityFromJson(json); + Map toJson() => _$ReadarrBookFileQualityToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/book_file/book_file_quality_quality.dart b/lib/modules/readarr/api/src/models/book_file/book_file_quality_quality.dart new file mode 100644 index 0000000000..d5a87219e6 --- /dev/null +++ b/lib/modules/readarr/api/src/models/book_file/book_file_quality_quality.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; + +part 'book_file_quality_quality.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrBookFileQualityQuality { + @JsonKey(name: 'id') + int? id; + + @JsonKey(name: 'name') + String? name; + + @JsonKey(name: 'source') + String? source; + + @JsonKey(name: 'resolution') + int? resolution; + + ReadarrBookFileQualityQuality({ + this.id, + this.name, + this.source, + this.resolution, + }); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrBookFileQualityQuality.fromJson(Map json) => + _$ReadarrBookFileQualityQualityFromJson(json); + Map toJson() => _$ReadarrBookFileQualityQualityToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/book_file/book_file_quality_revision.dart b/lib/modules/readarr/api/src/models/book_file/book_file_quality_revision.dart new file mode 100644 index 0000000000..c761d5b125 --- /dev/null +++ b/lib/modules/readarr/api/src/models/book_file/book_file_quality_revision.dart @@ -0,0 +1,29 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; + +part 'book_file_quality_revision.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrBookFileQualityRevision { + @JsonKey(name: 'version') + int? version; + + @JsonKey(name: 'real') + int? real; + + @JsonKey(name: 'isRepack') + bool? isRepack; + + ReadarrBookFileQualityRevision({ + this.version, + this.real, + this.isRepack, + }); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrBookFileQualityRevision.fromJson(Map json) => + _$ReadarrBookFileQualityRevisionFromJson(json); + Map toJson() => _$ReadarrBookFileQualityRevisionToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/command/command.dart b/lib/modules/readarr/api/src/models/command/command.dart new file mode 100644 index 0000000000..8e7fc90a24 --- /dev/null +++ b/lib/modules/readarr/api/src/models/command/command.dart @@ -0,0 +1,106 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'command.g.dart'; + +/// Model for the response for executing a command in Readarr. +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrCommand { + /// Name of the command + @JsonKey(name: 'name') + String? name; + + /// Current message of the command + @JsonKey(name: 'message') + String? message; + + @JsonKey(name: 'body') + ReadarrCommandBody? body; + + /// Priority of the command + @JsonKey(name: 'priority') + String? priority; + + /// Current status of the command + @JsonKey(name: 'status') + String? status; + + /// [DateTime] that the command was queued + @JsonKey( + name: 'queued', + toJson: ReadarrUtilities.dateTimeToJson, + fromJson: ReadarrUtilities.dateTimeFromJson) + DateTime? queued; + + /// [DateTime] that the command was started + @JsonKey( + name: 'started', + toJson: ReadarrUtilities.dateTimeToJson, + fromJson: ReadarrUtilities.dateTimeFromJson) + DateTime? started; + + /// Method that triggered the command + @JsonKey(name: 'trigger') + String? trigger; + + /// Current state of the command + @JsonKey(name: 'state') + String? state; + + /// Was this command manually executed? + @JsonKey(name: 'manual') + bool? manual; + + /// [DateTime] that the command was started on + @JsonKey( + name: 'startedOn', + toJson: ReadarrUtilities.dateTimeToJson, + fromJson: ReadarrUtilities.dateTimeFromJson) + DateTime? startedOn; + + /// [DateTime] that the command state was changed at + @JsonKey( + name: 'stateChangeTime', + toJson: ReadarrUtilities.dateTimeToJson, + fromJson: ReadarrUtilities.dateTimeFromJson) + DateTime? stateChangeTime; + + /// Will updates be sent to the client for this command? + @JsonKey(name: 'sendUpdatesToClient') + bool? sendUpdatesToClient; + + /// Will this command update the scheduled tasks? + @JsonKey(name: 'updateScheduledTask') + bool? updateScheduledTask; + + /// Identifier of command instance + @JsonKey(name: 'id') + int? id; + + ReadarrCommand({ + this.name, + this.body, + this.priority, + this.status, + this.queued, + this.trigger, + this.state, + this.manual, + this.startedOn, + this.sendUpdatesToClient, + this.updateScheduledTask, + this.id, + }); + + /// Returns a JSON-encoded string version of this object. + @override + String toString() => json.encode(this.toJson()); + + /// Deserialize a JSON map to a [ReadarrCommand] object. + factory ReadarrCommand.fromJson(Map json) => + _$ReadarrCommandFromJson(json); + + /// Serialize a [ReadarrCommand] object to a JSON map. + Map toJson() => _$ReadarrCommandToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/command/command_body.dart b/lib/modules/readarr/api/src/models/command/command_body.dart new file mode 100644 index 0000000000..7410a7c598 --- /dev/null +++ b/lib/modules/readarr/api/src/models/command/command_body.dart @@ -0,0 +1,76 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; + +part 'command_body.g.dart'; + +/// Model for the body for executing a command in Readarr. +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrCommandBody { + /// Series ID attached to the command (if applicable, else null). + @JsonKey(name: 'authorId') + int? authorId; + + /// Is this command handling a new series? + @JsonKey(name: 'isNewSeries') + bool? isNewSeries; + + /// Type of the command + @JsonKey(name: 'type') + String? type; + + /// Will updates be sent to the client for this command? + @JsonKey(name: 'sendUpdatesToClient') + bool? sendUpdatesToClient; + + /// Will this command update the scheduled tasks? + @JsonKey(name: 'updateScheduledTask') + bool? updateScheduledTask; + + /// Message for completion + @JsonKey(name: 'completionMessage') + String? completionMessage; + + /// Does this command require disk access? + @JsonKey(name: 'requiresDiskAccess') + bool? requiresDiskAccess; + + /// Does this command need to execute exclusively? + @JsonKey(name: 'isExclusive') + bool? isExclusive; + + /// Name of the command + @JsonKey(name: 'name') + String? name; + + /// Method that triggered the command + @JsonKey(name: 'trigger') + String? trigger; + + /// Will messages for this command be suppressed? + @JsonKey(name: 'suppressMessages') + bool? suppressMessages; + + ReadarrCommandBody({ + this.authorId, + this.isNewSeries, + this.sendUpdatesToClient, + this.updateScheduledTask, + this.completionMessage, + this.requiresDiskAccess, + this.isExclusive, + this.name, + this.trigger, + this.suppressMessages, + }); + + /// Returns a JSON-encoded string version of this object. + @override + String toString() => json.encode(this.toJson()); + + /// Deserialize a JSON map to a [ReadarrCommandBody] object. + factory ReadarrCommandBody.fromJson(Map json) => + _$ReadarrCommandBodyFromJson(json); + + /// Serialize a [ReadarrCommandBody] object to a JSON map. + Map toJson() => _$ReadarrCommandBodyToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/edition/edition.dart b/lib/modules/readarr/api/src/models/edition/edition.dart new file mode 100644 index 0000000000..3065a4c7c1 --- /dev/null +++ b/lib/modules/readarr/api/src/models/edition/edition.dart @@ -0,0 +1,101 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'edition.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrEdition { + @JsonKey(name: 'id') + int? id; + + @JsonKey(name: 'bookId') + int? bookId; + + @JsonKey(name: 'foreignEditionId') + String? foreignEditionId; + + @JsonKey(name: 'titleSlug') + String? titleSlug; + + @JsonKey(name: 'isbn13') + String? isbn13; + + @JsonKey(name: 'asin') + String? asin; + + @JsonKey(name: 'title') + String? title; + + @JsonKey(name: 'language') + String? language; + + @JsonKey(name: 'overview') + String? overview; + + @JsonKey(name: 'format') + String? format; + + @JsonKey(name: 'isEbook') + bool? isEbook; + + @JsonKey(name: 'disambiguation') + String? disambiguation; + + @JsonKey(name: 'publisher') + String? publisher; + + @JsonKey(name: 'pageCount') + int? pageCount; + + @JsonKey( + name: 'releaseDate', + toJson: ReadarrUtilities.dateTimeToJson, + fromJson: ReadarrUtilities.dateTimeFromJson, + ) + DateTime? releaseDate; + + @JsonKey(name: 'images') + List? images; + + @JsonKey(name: 'ratings') + ReadarrAuthorRating? ratings; + + @JsonKey(name: 'monitored') + bool? monitored; + + @JsonKey(name: 'manualAdd') + bool? manualAdd; + + @JsonKey(name: 'remoteCover') + String? remoteCover; + + ReadarrEdition( + {this.id, + this.bookId, + this.foreignEditionId, + this.titleSlug, + this.isbn13, + this.asin, + this.title, + this.language, + this.overview, + this.format, + this.isEbook, + this.disambiguation, + this.publisher, + this.pageCount, + this.releaseDate, + this.images, + this.ratings, + this.monitored, + this.manualAdd, + this.remoteCover}); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrEdition.fromJson(Map json) => + _$ReadarrEditionFromJson(json); + Map toJson() => _$ReadarrEditionToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/history/history.dart b/lib/modules/readarr/api/src/models/history/history.dart new file mode 100644 index 0000000000..28957c179b --- /dev/null +++ b/lib/modules/readarr/api/src/models/history/history.dart @@ -0,0 +1,45 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'history.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrHistory { + @JsonKey(name: 'page') + int? page; + + @JsonKey(name: 'pageSize') + int? pageSize; + + @JsonKey( + name: 'sortKey', + fromJson: ReadarrUtilities.historySortKeyFromJson, + toJson: ReadarrUtilities.historySortKeyToJson, + ) + ReadarrHistorySortKey? sortKey; + + @JsonKey(name: 'sortDirection') + String? sortDirection; + + @JsonKey(name: 'totalRecords') + int? totalRecords; + + @JsonKey(name: 'records') + List? records; + + ReadarrHistory({ + this.page, + this.pageSize, + this.sortKey, + this.sortDirection, + this.totalRecords, + }); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrHistory.fromJson(Map json) => + _$ReadarrHistoryFromJson(json); + Map toJson() => _$ReadarrHistoryToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/history/history_record.dart b/lib/modules/readarr/api/src/models/history/history_record.dart new file mode 100644 index 0000000000..5c20675eba --- /dev/null +++ b/lib/modules/readarr/api/src/models/history/history_record.dart @@ -0,0 +1,75 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'history_record.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrHistoryRecord { + @JsonKey(name: 'bookId') + int? bookId; + + @JsonKey(name: 'authorId') + int? authorId; + + @JsonKey(name: 'sourceTitle') + String? sourceTitle; + + @JsonKey(name: 'quality') + ReadarrBookFileQuality? quality; + + @JsonKey(name: 'qualityCutoffNotMet') + bool? qualityCutoffNotMet; + + @JsonKey( + name: 'date', + toJson: ReadarrUtilities.dateTimeToJson, + fromJson: ReadarrUtilities.dateTimeFromJson, + ) + DateTime? date; + + @JsonKey(name: 'downloadId') + String? downloadId; + + @JsonKey( + name: 'eventType', + toJson: ReadarrUtilities.eventTypeToJson, + fromJson: ReadarrUtilities.eventTypeFromJson, + ) + ReadarrEventType? eventType; + + @JsonKey(name: 'data') + Map? data; + + @JsonKey(name: 'book') + ReadarrBook? episode; + + @JsonKey(name: 'author') + ReadarrAuthor? series; + + @JsonKey(name: 'id') + int? id; + + ReadarrHistoryRecord({ + this.bookId, + this.authorId, + this.sourceTitle, + this.quality, + this.qualityCutoffNotMet, + this.date, + this.downloadId, + this.eventType, + this.data, + this.episode, + this.series, + this.id, + }); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrHistoryRecord.fromJson(Map json) => + _$ReadarrHistoryRecordFromJson(json); + + Map toJson() => _$ReadarrHistoryRecordToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/import_list/exclusion.dart b/lib/modules/readarr/api/src/models/import_list/exclusion.dart new file mode 100644 index 0000000000..fb5cea41c6 --- /dev/null +++ b/lib/modules/readarr/api/src/models/import_list/exclusion.dart @@ -0,0 +1,29 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; + +part 'exclusion.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrExclusion { + @JsonKey(name: 'id') + int? id; + + @JsonKey(name: 'foreignId') + String? foreignId; + + @JsonKey(name: 'authorName') + String? authorName; + + ReadarrExclusion({ + this.id, + this.foreignId, + this.authorName, + }); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrExclusion.fromJson(Map json) => + _$ReadarrExclusionFromJson(json); + Map toJson() => _$ReadarrExclusionToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/profile/metadata_profile.dart b/lib/modules/readarr/api/src/models/profile/metadata_profile.dart new file mode 100644 index 0000000000..7d038f50e2 --- /dev/null +++ b/lib/modules/readarr/api/src/models/profile/metadata_profile.dart @@ -0,0 +1,64 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; + +part 'metadata_profile.g.dart'; + +/// Model for a language profile from Readarr. +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrMetadataProfile { + /// Name of the profile + @JsonKey(name: 'name') + String? name; + + @JsonKey(name: 'allowedLanguages') + String? allowedLanguages; + + @JsonKey(name: 'minPages') + int? minPages; + + @JsonKey(name: 'minPopularity') + int? minPopularity; + + @JsonKey(name: 'skipMissingDate') + bool? skipMissingDate; + + @JsonKey(name: 'skipMissingIsbn') + bool? skipMissingIsbn; + + @JsonKey(name: 'skipPartsAndSets') + bool? skipPartsAndSets; + + @JsonKey(name: 'skipSeriesSecondary') + bool? skipSeriesSecondary; + + @JsonKey(name: 'ignored') + String? ignored; + + /// Identifier of the language profile + @JsonKey(name: 'id') + int? id; + + ReadarrMetadataProfile({ + this.name, + this.allowedLanguages, + this.minPages, + this.minPopularity, + this.skipMissingDate, + this.skipMissingIsbn, + this.skipPartsAndSets, + this.skipSeriesSecondary, + this.ignored, + this.id, + }); + + /// Returns a JSON-encoded string version of this object. + @override + String toString() => json.encode(this.toJson()); + + /// Deserialize a JSON map to a [ReadarrMetadataProfile] object. + factory ReadarrMetadataProfile.fromJson(Map json) => + _$ReadarrMetadataProfileFromJson(json); + + /// Serialize a [ReadarrMetadataProfile] object to a JSON map. + Map toJson() => _$ReadarrMetadataProfileToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/profile/quality_profile.dart b/lib/modules/readarr/api/src/models/profile/quality_profile.dart new file mode 100644 index 0000000000..2b0aa2478d --- /dev/null +++ b/lib/modules/readarr/api/src/models/profile/quality_profile.dart @@ -0,0 +1,39 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'quality_profile_item.dart'; + +part 'quality_profile.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrQualityProfile { + @JsonKey(name: 'name') + String? name; + + @JsonKey(name: 'upgradeAllowed') + bool? upgradeAllowed; + + @JsonKey(name: 'cutoff') + int? cutoff; + + @JsonKey(name: 'items') + List? items; + + @JsonKey(name: 'id') + int? id; + + ReadarrQualityProfile({ + this.name, + this.upgradeAllowed, + this.cutoff, + this.items, + this.id, + }); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrQualityProfile.fromJson(Map json) => + _$ReadarrQualityProfileFromJson(json); + + Map toJson() => _$ReadarrQualityProfileToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/profile/quality_profile_cutoff.dart b/lib/modules/readarr/api/src/models/profile/quality_profile_cutoff.dart new file mode 100644 index 0000000000..da1b5ec65c --- /dev/null +++ b/lib/modules/readarr/api/src/models/profile/quality_profile_cutoff.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; + +part 'quality_profile_cutoff.g.dart'; + +/// Model for a quality profile cutoffs from Readarr. +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrQualityProfileCutoff { + /// Identifier of the cutoff profile + @JsonKey(name: 'id') + int? id; + + /// Name of the cutoff profile + @JsonKey(name: 'name') + String? name; + + /// Typical source medium of the cutoff profile + @JsonKey(name: 'source') + String? source; + + /// Resolution of the cutoff profile + @JsonKey(name: 'resolution') + int? resolution; + + ReadarrQualityProfileCutoff({ + this.id, + this.name, + this.source, + this.resolution, + }); + + /// Returns a JSON-encoded string version of this object. + @override + String toString() => json.encode(this.toJson()); + + /// Deserialize a JSON map to a [ReadarrQualityProfileCutoff] object. + factory ReadarrQualityProfileCutoff.fromJson(Map json) => + _$ReadarrQualityProfileCutoffFromJson(json); + + /// Serialize a [ReadarrQualityProfileCutoff] object to a JSON map. + Map toJson() => _$ReadarrQualityProfileCutoffToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/profile/quality_profile_item.dart b/lib/modules/readarr/api/src/models/profile/quality_profile_item.dart new file mode 100644 index 0000000000..4d9cd2730b --- /dev/null +++ b/lib/modules/readarr/api/src/models/profile/quality_profile_item.dart @@ -0,0 +1,27 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'quality_profile_item_quality.dart'; + +part 'quality_profile_item.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrQualityProfileItem { + @JsonKey(name: 'allowed') + bool? allowed; + + @JsonKey(name: 'quality') + ReadarrQualityProfileItemQuality? quality; + + ReadarrQualityProfileItem({ + this.allowed, + this.quality, + }); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrQualityProfileItem.fromJson(Map json) => + _$ReadarrQualityProfileItemFromJson(json); + + Map toJson() => _$ReadarrQualityProfileItemToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/profile/quality_profile_item_quality.dart b/lib/modules/readarr/api/src/models/profile/quality_profile_item_quality.dart new file mode 100644 index 0000000000..d6f80b0978 --- /dev/null +++ b/lib/modules/readarr/api/src/models/profile/quality_profile_item_quality.dart @@ -0,0 +1,43 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; + +part 'quality_profile_item_quality.g.dart'; + +/// Model for a quality profile's nested item's quality from Readarr. +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrQualityProfileItemQuality { + /// Identifier of the cutoff profile + @JsonKey(name: 'id') + int? id; + + /// Name of the cutoff profile + @JsonKey(name: 'name') + String? name; + + /// Typical source medium of the cutoff profile + @JsonKey(name: 'source') + String? source; + + /// Resolution of the cutoff profile + @JsonKey(name: 'resolution') + int? resolution; + + ReadarrQualityProfileItemQuality({ + this.id, + this.name, + this.source, + this.resolution, + }); + + /// Returns a JSON-encoded string version of this object. + @override + String toString() => json.encode(this.toJson()); + + /// Deserialize a JSON map to a [ReadarrQualityProfileItemQuality] object. + factory ReadarrQualityProfileItemQuality.fromJson(Map json) => + _$ReadarrQualityProfileItemQualityFromJson(json); + + /// Serialize a [ReadarrQualityProfileItemQuality] object to a JSON map. + Map toJson() => + _$ReadarrQualityProfileItemQualityToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/queue/queue.dart b/lib/modules/readarr/api/src/models/queue/queue.dart new file mode 100644 index 0000000000..910aea1086 --- /dev/null +++ b/lib/modules/readarr/api/src/models/queue/queue.dart @@ -0,0 +1,43 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'queue.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrQueue { + @JsonKey(name: 'page') + int? page; + + @JsonKey(name: 'pageSize') + int? pageSize; + + @JsonKey(name: 'sortKey') + String? sortKey; + + @JsonKey(name: 'sortDirection') + String? sortDirection; + + @JsonKey(name: 'totalRecords') + int? totalRecords; + + @JsonKey(name: 'records') + List? records; + + ReadarrQueue({ + this.page, + this.pageSize, + this.sortKey, + this.sortDirection, + this.totalRecords, + this.records, + }); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrQueue.fromJson(Map json) => + _$ReadarrQueueFromJson(json); + + Map toJson() => _$ReadarrQueueToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/queue/queue_record.dart b/lib/modules/readarr/api/src/models/queue/queue_record.dart new file mode 100644 index 0000000000..f0d2a5b254 --- /dev/null +++ b/lib/modules/readarr/api/src/models/queue/queue_record.dart @@ -0,0 +1,123 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'queue_record.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrQueueRecord { + @JsonKey(name: 'authorId') + int? authorId; + + @JsonKey(name: 'bookId') + int? bookId; + + @JsonKey(name: 'author') + ReadarrAuthor? series; + + @JsonKey(name: 'book') + ReadarrBook? episode; + + @JsonKey(name: 'quality') + ReadarrBookFileQuality? quality; + + @JsonKey(name: 'size') + double? size; + + @JsonKey(name: 'title') + String? title; + + @JsonKey(name: 'sizeleft') + double? sizeleft; + + @JsonKey(name: 'timeleft') + String? timeleft; + + @JsonKey( + name: 'estimatedCompletionTime', + toJson: ReadarrUtilities.dateTimeToJson, + fromJson: ReadarrUtilities.dateTimeFromJson, + ) + DateTime? estimatedCompletionTime; + + @JsonKey( + name: 'status', + toJson: ReadarrUtilities.queueStatusToJson, + fromJson: ReadarrUtilities.queueStatusFromJson, + ) + ReadarrQueueStatus? status; + + @JsonKey( + name: 'trackedDownloadStatus', + toJson: ReadarrUtilities.queueTrackedDownloadStatusToJson, + fromJson: ReadarrUtilities.queueTrackedDownloadStatusFromJson, + ) + ReadarrTrackedDownloadStatus? trackedDownloadStatus; + + @JsonKey( + name: 'trackedDownloadState', + toJson: ReadarrUtilities.queueTrackedDownloadStateToJson, + fromJson: ReadarrUtilities.queueTrackedDownloadStateFromJson, + ) + ReadarrTrackedDownloadState? trackedDownloadState; + + @JsonKey(name: 'statusMessages') + List? statusMessages; + + @JsonKey(name: 'errorMessage') + String? errorMessage; + + @JsonKey(name: 'downloadId') + String? downloadId; + + @JsonKey( + name: 'protocol', + toJson: ReadarrUtilities.protocolToJson, + fromJson: ReadarrUtilities.protocolFromJson, + ) + ReadarrProtocol? protocol; + + @JsonKey(name: 'downloadClient') + String? downloadClient; + + @JsonKey(name: 'indexer') + String? indexer; + + @JsonKey(name: 'outputPath') + String? outputPath; + + @JsonKey(name: 'id') + int? id; + + ReadarrQueueRecord({ + this.authorId, + this.bookId, + this.series, + this.episode, + this.quality, + this.size, + this.title, + this.sizeleft, + this.timeleft, + this.estimatedCompletionTime, + this.status, + this.trackedDownloadStatus, + this.trackedDownloadState, + this.statusMessages, + this.errorMessage, + this.downloadId, + this.protocol, + this.downloadClient, + this.indexer, + this.outputPath, + this.id, + }); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrQueueRecord.fromJson(Map json) => + _$ReadarrQueueRecordFromJson(json); + + Map toJson() => _$ReadarrQueueRecordToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/queue/queue_status_message.dart b/lib/modules/readarr/api/src/models/queue/queue_status_message.dart new file mode 100644 index 0000000000..049411b141 --- /dev/null +++ b/lib/modules/readarr/api/src/models/queue/queue_status_message.dart @@ -0,0 +1,29 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; + +part 'queue_status_message.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrQueueStatusMessage { + @JsonKey(name: 'title') + String? title; + + @JsonKey(name: 'messages') + List? messages; + + ReadarrQueueStatusMessage({ + this.title, + this.messages, + }); + + /// Returns a JSON-encoded string version of this object. + @override + String toString() => json.encode(this.toJson()); + + /// Deserialize a JSON map to a [ReadarrQueueStatusMessage] object. + factory ReadarrQueueStatusMessage.fromJson(Map json) => + _$ReadarrQueueStatusMessageFromJson(json); + + /// Serialize a [ReadarrQueueStatusMessage] object to a JSON map. + Map toJson() => _$ReadarrQueueStatusMessageToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/release/added_release.dart b/lib/modules/readarr/api/src/models/release/added_release.dart new file mode 100644 index 0000000000..29e299c1be --- /dev/null +++ b/lib/modules/readarr/api/src/models/release/added_release.dart @@ -0,0 +1,136 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'added_release.g.dart'; + +/// Model for an episode release that was added to Readarr. +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrAddedRelease { + /// GUID of the release + @JsonKey(name: 'guid') + String? guid; + + /// Quality weight + @JsonKey(name: 'qualityWeight') + int? qualityWeight; + + /// Release age (in days) + @JsonKey(name: 'age') + int? age; + + /// Release age (in hours) + @JsonKey(name: 'ageHours') + double? ageHours; + + /// Release age (in minutes) + @JsonKey(name: 'ageMinutes') + double? ageMinutes; + + /// File size + @JsonKey(name: 'size') + int? size; + + /// Indexer identifier + @JsonKey(name: 'indexerId') + int? indexerId; + + /// Is this the full season? + @JsonKey(name: 'fullSeason') + bool? fullSeason; + + /// Season of the release + @JsonKey(name: 'seasonNumber') + int? seasonNumber; + + /// Is this release approved? + @JsonKey(name: 'approved') + bool? approved; + + /// Is this release temporarily rejected? + @JsonKey(name: 'temporarilyRejected') + bool? temporarilyRejected; + + /// Is this release rejected? + @JsonKey(name: 'rejected') + bool? rejected; + + /// TVDB identifier + @JsonKey(name: 'tvdbId') + int? tvdbId; + + /// TVRage identifier + @JsonKey(name: 'tvRageId') + int? tvRageId; + + /// [DateTime] object for when the release was published + @JsonKey( + name: 'publishDate', + fromJson: ReadarrUtilities.dateTimeFromJson, + toJson: ReadarrUtilities.dateTimeToJson) + DateTime? publishDate; + + /// Is the download allowed? + @JsonKey(name: 'downloadAllowed') + bool? downloadAllowed; + + /// Weight of the release + @JsonKey(name: 'releaseWeight') + int? releaseWeight; + + /// The protocol used to download the release + @JsonKey(name: 'protocol') + String? protocol; + + /// Is the release formatted as a daily episode? + @JsonKey(name: 'isDaily') + bool? isDaily; + + /// Is this release absolute numbered? + @JsonKey(name: 'isAbsoluteNumbering') + bool? isAbsoluteNumbering; + + /// Is this *possibly* a special episode? + @JsonKey(name: 'isPossibleSpecialEpisode') + bool? isPossibleSpecialEpisode; + + /// Is this a specials episode? + @JsonKey(name: 'special') + bool? special; + + ReadarrAddedRelease({ + this.guid, + this.qualityWeight, + this.age, + this.ageHours, + this.ageMinutes, + this.size, + this.indexerId, + this.fullSeason, + this.seasonNumber, + this.approved, + this.temporarilyRejected, + this.rejected, + this.tvdbId, + this.tvRageId, + this.publishDate, + this.downloadAllowed, + this.releaseWeight, + this.protocol, + this.isDaily, + this.isAbsoluteNumbering, + this.isPossibleSpecialEpisode, + this.special, + }); + + /// Returns a JSON-encoded string version of this object. + @override + String toString() => json.encode(this.toJson()); + + /// Deserialize a JSON map to a [ReadarrAddedRelease] object. + factory ReadarrAddedRelease.fromJson(Map json) => + _$ReadarrAddedReleaseFromJson(json); + + /// Serialize a [ReadarrAddedRelease] object to a JSON map. + Map toJson() => _$ReadarrAddedReleaseToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/release/release.dart b/lib/modules/readarr/api/src/models/release/release.dart new file mode 100644 index 0000000000..cf79e0efb2 --- /dev/null +++ b/lib/modules/readarr/api/src/models/release/release.dart @@ -0,0 +1,195 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'release.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrRelease { + @JsonKey(name: 'guid') + String? guid; + + @JsonKey(name: 'quality') + ReadarrBookFileQuality? quality; + + @JsonKey(name: 'qualityWeight') + int? qualityWeight; + + @JsonKey(name: 'age') + int? age; + + @JsonKey(name: 'ageHours') + double? ageHours; + + @JsonKey(name: 'ageMinutes') + double? ageMinutes; + + @JsonKey(name: 'size') + int? size; + + @JsonKey(name: 'indexerId') + int? indexerId; + + @JsonKey(name: 'indexer') + String? indexer; + + @JsonKey(name: 'releaseGroup') + String? releaseGroup; + + @JsonKey(name: 'releaseHash') + String? releaseHash; + + @JsonKey(name: 'title') + String? title; + + @JsonKey(name: 'fullSeason') + bool? fullSeason; + + @JsonKey(name: 'sceneSource') + bool? sceneSource; + + @JsonKey(name: 'seasonNumber') + int? seasonNumber; + + @JsonKey(name: 'seriesTitle') + String? seriesTitle; + + @JsonKey(name: 'episodeNumbers') + List? episodeNumbers; + + @JsonKey(name: 'absoluteEpisodeNumbers') + List? absoluteEpisodeNumbers; + + @JsonKey(name: 'mappedSeasonNumber') + int? mappedSeasonNumber; + + @JsonKey(name: 'mappedEpisodeNumbers') + List? mappedEpisodeNumbers; + + @JsonKey(name: 'mappedAbsoluteEpisodeNumbers') + List? mappedAbsoluteEpisodeNumbers; + + @JsonKey(name: 'approved') + bool? approved; + + @JsonKey(name: 'temporarilyRejected') + bool? temporarilyRejected; + + @JsonKey(name: 'rejected') + bool? rejected; + + @JsonKey(name: 'tvdbId') + int? tvdbId; + + @JsonKey(name: 'tvRageId') + int? tvRageId; + + @JsonKey(name: 'rejections') + List? rejections; + + @JsonKey( + name: 'publishDate', + fromJson: ReadarrUtilities.dateTimeFromJson, + toJson: ReadarrUtilities.dateTimeToJson, + ) + DateTime? publishDate; + + @JsonKey(name: 'commentUrl') + String? commentUrl; + + @JsonKey(name: 'downloadUrl') + String? downloadUrl; + + @JsonKey(name: 'infoUrl') + String? infoUrl; + + @JsonKey(name: 'episodeRequested') + bool? episodeRequested; + + @JsonKey(name: 'downloadAllowed') + bool? downloadAllowed; + + @JsonKey(name: 'releaseWeight') + int? releaseWeight; + + @JsonKey(name: 'preferredWordScore') + int? preferredWordScore; + + @JsonKey( + name: 'protocol', + toJson: ReadarrUtilities.protocolToJson, + fromJson: ReadarrUtilities.protocolFromJson, + ) + ReadarrProtocol? protocol; + + @JsonKey(name: 'isDaily') + bool? isDaily; + + @JsonKey(name: 'isAbsoluteNumbering') + bool? isAbsoluteNumbering; + + @JsonKey(name: 'isPossibleSpecialEpisode') + bool? isPossibleSpecialEpisode; + + @JsonKey(name: 'special') + bool? special; + + @JsonKey(name: 'seeders') + int? seeders; + + @JsonKey(name: 'leechers') + int? leechers; + + ReadarrRelease({ + this.guid, + this.quality, + this.qualityWeight, + this.age, + this.ageHours, + this.ageMinutes, + this.size, + this.indexerId, + this.indexer, + this.releaseGroup, + this.releaseHash, + this.title, + this.fullSeason, + this.sceneSource, + this.seasonNumber, + this.seriesTitle, + this.episodeNumbers, + this.absoluteEpisodeNumbers, + this.mappedSeasonNumber, + this.mappedEpisodeNumbers, + this.mappedAbsoluteEpisodeNumbers, + this.approved, + this.temporarilyRejected, + this.rejected, + this.tvdbId, + this.tvRageId, + this.rejections, + this.publishDate, + this.commentUrl, + this.downloadUrl, + this.infoUrl, + this.episodeRequested, + this.downloadAllowed, + this.releaseWeight, + this.preferredWordScore, + this.protocol, + this.isDaily, + this.isAbsoluteNumbering, + this.isPossibleSpecialEpisode, + this.special, + this.leechers, + this.seeders, + }); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrRelease.fromJson(Map json) => + _$ReadarrReleaseFromJson(json); + + Map toJson() => _$ReadarrReleaseToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/root_folder/root_folder.dart b/lib/modules/readarr/api/src/models/root_folder/root_folder.dart new file mode 100644 index 0000000000..e035f6dce1 --- /dev/null +++ b/lib/modules/readarr/api/src/models/root_folder/root_folder.dart @@ -0,0 +1,48 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'root_folder.g.dart'; + +/// Model for root folders from Readarr. +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrRootFolder { + /// Root folder's path + @JsonKey(name: 'path') + String? path; + + /// Free space at the root folder + @JsonKey(name: 'freeSpace') + int? freeSpace; + + /// Total space at the root folder + @JsonKey(name: 'totalSpace') + int? totalSpace; + + /// List of unmapped folders within this root folder + @JsonKey(name: 'unmappedFolders') + List? unmappedFolders; + + /// Identifier of the root folder + @JsonKey(name: 'id') + int? id; + + ReadarrRootFolder({ + this.path, + this.freeSpace, + this.totalSpace, + this.unmappedFolders, + this.id, + }); + + /// Returns a JSON-encoded string version of this object. + @override + String toString() => json.encode(this.toJson()); + + /// Deserialize a JSON map to a [ReadarrRootFolder] object. + factory ReadarrRootFolder.fromJson(Map json) => + _$ReadarrRootFolderFromJson(json); + + /// Serialize a [ReadarrRootFolder] object to a JSON map. + Map toJson() => _$ReadarrRootFolderToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/root_folder/unmapped_folder.dart b/lib/modules/readarr/api/src/models/root_folder/unmapped_folder.dart new file mode 100644 index 0000000000..6c46c54539 --- /dev/null +++ b/lib/modules/readarr/api/src/models/root_folder/unmapped_folder.dart @@ -0,0 +1,32 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; + +part 'unmapped_folder.g.dart'; + +/// Model for unmapped folders within a root folder from Readarr. +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrUnmappedFolder { + /// Folder name + @JsonKey(name: 'name') + String? name; + + /// Root folder's path + @JsonKey(name: 'path') + String? path; + + ReadarrUnmappedFolder({ + this.name, + this.path, + }); + + /// Returns a JSON-encoded string version of this object. + @override + String toString() => json.encode(this.toJson()); + + /// Deserialize a JSON map to a [ReadarrUnmappedFolder] object. + factory ReadarrUnmappedFolder.fromJson(Map json) => + _$ReadarrUnmappedFolderFromJson(json); + + /// Serialize a [ReadarrUnmappedFolder] object to a JSON map. + Map toJson() => _$ReadarrUnmappedFolderToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/system/status.dart b/lib/modules/readarr/api/src/models/system/status.dart new file mode 100644 index 0000000000..ad35796bb7 --- /dev/null +++ b/lib/modules/readarr/api/src/models/system/status.dart @@ -0,0 +1,131 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'status.g.dart'; + +/// Model for the system status from Readarr. +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrStatus { + /// Readarr version + @JsonKey(name: 'version') + String? version; + + /// [DateTime] object representing the build time + @JsonKey( + name: 'buildTime', + fromJson: ReadarrUtilities.dateTimeFromJson, + toJson: ReadarrUtilities.dateTimeToJson) + DateTime? buildTime; + + /// Is debug version? + @JsonKey(name: 'isDebug') + bool? isDebug; + + /// Is production version? + @JsonKey(name: 'isProduction') + bool? isProduction; + + /// Is admin? + @JsonKey(name: 'isAdmin') + bool? isAdmin; + + /// Is user interactive? + @JsonKey(name: 'isUserInteractive') + bool? isUserInteractive; + + /// Startup path on system + @JsonKey(name: 'startupPath') + String? startupPath; + + /// App data location + @JsonKey(name: 'appData') + String? appData; + + /// Name of the operating system + @JsonKey(name: 'osName') + String? osName; + + /// Version of the operating system + @JsonKey(name: 'osVersion') + String? osVersion; + + /// Is Readarr using the mono runtime? + @JsonKey(name: 'isMonoRuntime') + bool? isMonoRuntime; + + /// Is Readarr using mono? + @JsonKey(name: 'isMono') + bool? isMono; + + /// Is Readarr running on Linux? + @JsonKey(name: 'isLinux') + bool? isLinux; + + /// Is Readarr running on MacOS? + @JsonKey(name: 'isOsx') + bool? isOsx; + + /// Is Readarr running on Windows? + @JsonKey(name: 'isWindows') + bool? isWindows; + + /// GitHub branch of the version + @JsonKey(name: 'branch') + String? branch; + + /// Type of authentication enabled + @JsonKey(name: 'authentication') + String? authentication; + + /// Version of SQLite + @JsonKey(name: 'sqliteVersion') + String? sqliteVersion; + + /// URL-base of the instance + @JsonKey(name: 'urlBase') + String? urlBase; + + /// Version of the runtime + @JsonKey(name: 'runtimeVersion') + String? runtimeVersion; + + /// Name of the runtime + @JsonKey(name: 'runtimeName') + String? runtimeName; + + ReadarrStatus({ + this.version, + this.buildTime, + this.isDebug, + this.isProduction, + this.isAdmin, + this.isUserInteractive, + this.startupPath, + this.appData, + this.osName, + this.osVersion, + this.isMonoRuntime, + this.isMono, + this.isLinux, + this.isOsx, + this.isWindows, + this.branch, + this.authentication, + this.sqliteVersion, + this.urlBase, + this.runtimeVersion, + this.runtimeName, + }); + + /// Returns a JSON-encoded string version of this object. + @override + String toString() => json.encode(this.toJson()); + + /// Deserialize a JSON map to a [ReadarrStatus] object. + factory ReadarrStatus.fromJson(Map json) => + _$ReadarrStatusFromJson(json); + + /// Serialize a [ReadarrStatus] object to a JSON map. + Map toJson() => _$ReadarrStatusToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/tag/tag.dart b/lib/modules/readarr/api/src/models/tag/tag.dart new file mode 100644 index 0000000000..17df6990a9 --- /dev/null +++ b/lib/modules/readarr/api/src/models/tag/tag.dart @@ -0,0 +1,25 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; + +part 'tag.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrTag { + @JsonKey(name: 'id') + int? id; + + @JsonKey(name: 'label') + String? label; + + ReadarrTag({ + this.id, + this.label, + }); + + @override + String toString() => json.encode(this.toJson()); + + factory ReadarrTag.fromJson(Map json) => + _$ReadarrTagFromJson(json); + Map toJson() => _$ReadarrTagToJson(this); +} diff --git a/lib/modules/readarr/api/src/models/wanted_missing/missing.dart b/lib/modules/readarr/api/src/models/wanted_missing/missing.dart new file mode 100644 index 0000000000..47721801d9 --- /dev/null +++ b/lib/modules/readarr/api/src/models/wanted_missing/missing.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'missing.g.dart'; + +/// Model for missing episode records from Readarr. +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class ReadarrMissing { + /// Page of the list of missing episodes + @JsonKey(name: 'page') + int? page; + + /// Amount of records returned on this page + @JsonKey(name: 'pageSize') + int? pageSize; + + /// Key used to sort the results + @JsonKey( + name: 'sortKey', + toJson: ReadarrUtilities.wantedMissingSortKeyToJson, + fromJson: ReadarrUtilities.wantedMissingSortKeyFromJson) + ReadarrWantedMissingSortKey? sortKey; + + /// Direction that the results were sorted + @JsonKey(name: 'sortDirection') + String? sortDirection; + + /// Total amount of records available + @JsonKey(name: 'totalRecords') + int? totalRecords; + + /// Missing episode records, each being an [ReadarrMissingRecord] object. + @JsonKey(name: 'records') + List? records; + + ReadarrMissing({ + this.page, + this.pageSize, + this.sortKey, + this.sortDirection, + this.totalRecords, + this.records, + }); + + /// Returns a JSON-encoded string version of this object. + @override + String toString() => json.encode(this.toJson()); + + /// Deserialize a JSON map to a [ReadarrMissing] object. + factory ReadarrMissing.fromJson(Map json) => + _$ReadarrMissingFromJson(json); + + /// Serialize a [ReadarrMissing] object to a JSON map. + Map toJson() => _$ReadarrMissingToJson(this); +} diff --git a/lib/modules/readarr/api/src/types/author_monitor_type.dart b/lib/modules/readarr/api/src/types/author_monitor_type.dart new file mode 100644 index 0000000000..9dba6a0133 --- /dev/null +++ b/lib/modules/readarr/api/src/types/author_monitor_type.dart @@ -0,0 +1,60 @@ +part of readarr_types; + +enum ReadarrAuthorMonitorType { + ALL, + FUTURE, + MISSING, + EXISTING, + PILOT, + FIRST_SEASON, + LATEST_SEASON, + NONE, +} + +extension ReadarrAuthorMonitorTypeExtension on ReadarrAuthorMonitorType { + ReadarrAuthorMonitorType? from(String? type) { + switch (type) { + case 'all': + return ReadarrAuthorMonitorType.ALL; + case 'future': + return ReadarrAuthorMonitorType.FUTURE; + case 'missing': + return ReadarrAuthorMonitorType.MISSING; + case 'existing': + return ReadarrAuthorMonitorType.EXISTING; + case 'pilot': + return ReadarrAuthorMonitorType.PILOT; + case 'firstSeason': + return ReadarrAuthorMonitorType.FIRST_SEASON; + case 'latestSeason': + return ReadarrAuthorMonitorType.LATEST_SEASON; + case 'none': + return ReadarrAuthorMonitorType.NONE; + default: + return null; + } + } + + String? get value { + switch (this) { + case ReadarrAuthorMonitorType.ALL: + return 'all'; + case ReadarrAuthorMonitorType.FUTURE: + return 'future'; + case ReadarrAuthorMonitorType.MISSING: + return 'missing'; + case ReadarrAuthorMonitorType.EXISTING: + return 'existing'; + case ReadarrAuthorMonitorType.PILOT: + return 'pilot'; + case ReadarrAuthorMonitorType.FIRST_SEASON: + return 'firstSeason'; + case ReadarrAuthorMonitorType.LATEST_SEASON: + return 'latestSeason'; + case ReadarrAuthorMonitorType.NONE: + return 'none'; + default: + return null; + } + } +} diff --git a/lib/modules/readarr/api/src/types/event_type.dart b/lib/modules/readarr/api/src/types/event_type.dart new file mode 100644 index 0000000000..1ac22f23cf --- /dev/null +++ b/lib/modules/readarr/api/src/types/event_type.dart @@ -0,0 +1,93 @@ +part of readarr_types; + +enum ReadarrEventType { + GRABBED, + BOOK_FILE_IMPORTED, + DOWNLOAD_FAILED, + BOOK_FILE_DELETED, + BOOK_FILE_RENAMED, + BOOK_IMPORT_INCOMPLETE, + DOWNLOAD_IMPORTED, + BOOK_FILE_RETAGGED, + DOWNLOAD_IGNORED +} + +/// Extension on [ReadarrEventType] to implement extended functionality. +extension ReadarrEventTypeExtension on ReadarrEventType { + /// Given a String, will return the correct `ReadarrEventType` object. + ReadarrEventType? from(String? type) { + switch (type) { + case 'grabbed': + return ReadarrEventType.GRABBED; + case 'bookFileImported': + return ReadarrEventType.BOOK_FILE_IMPORTED; + case 'downloadFailed': + return ReadarrEventType.DOWNLOAD_FAILED; + case 'bookFileDeleted': + return ReadarrEventType.BOOK_FILE_DELETED; + case 'bookFileRenamed': + return ReadarrEventType.BOOK_FILE_RENAMED; + case 'bookImportIncomplete': + return ReadarrEventType.BOOK_IMPORT_INCOMPLETE; + case 'downloadImported': + return ReadarrEventType.DOWNLOAD_IMPORTED; + case 'bookFileRetagged': + return ReadarrEventType.BOOK_FILE_RETAGGED; + case 'downloadIgnored': + return ReadarrEventType.DOWNLOAD_IGNORED; + default: + return null; + } + } + + /// The actual value/key for media types used in Readarr. + String? get value { + switch (this) { + case ReadarrEventType.GRABBED: + return 'grabbed'; + case ReadarrEventType.BOOK_FILE_IMPORTED: + return 'bookFileImported'; + case ReadarrEventType.DOWNLOAD_FAILED: + return 'downloadFailed'; + case ReadarrEventType.BOOK_FILE_DELETED: + return 'bookFileDeleted'; + case ReadarrEventType.BOOK_FILE_RENAMED: + return 'bookFileRenamed'; + case ReadarrEventType.BOOK_IMPORT_INCOMPLETE: + return 'bookImportIncomplete'; + case ReadarrEventType.DOWNLOAD_IMPORTED: + return 'downloadImported'; + case ReadarrEventType.BOOK_FILE_RETAGGED: + return 'bookFileRetagged'; + case ReadarrEventType.DOWNLOAD_IGNORED: + return 'downloadIgnored'; + default: + return null; + } + } + + String? get readable { + switch (this) { + case ReadarrEventType.GRABBED: + return 'Grabbed'; + case ReadarrEventType.BOOK_FILE_IMPORTED: + return 'Book File Imported'; + case ReadarrEventType.DOWNLOAD_FAILED: + return 'Download Failed'; + case ReadarrEventType.BOOK_FILE_DELETED: + return 'Book File Deleted'; + case ReadarrEventType.BOOK_FILE_RENAMED: + return 'Book File Renamed'; + case ReadarrEventType.BOOK_IMPORT_INCOMPLETE: + return 'Book Import Incomplete'; + case ReadarrEventType.DOWNLOAD_IMPORTED: + return 'Download Imported'; + case ReadarrEventType.BOOK_FILE_RETAGGED: + return 'Book File Retagged'; + case ReadarrEventType.DOWNLOAD_IGNORED: + return 'Download Ignored'; + default: + return null; + } + } +} diff --git a/lib/modules/readarr/api/src/types/history_sort_key.dart b/lib/modules/readarr/api/src/types/history_sort_key.dart new file mode 100644 index 0000000000..4d265918ae --- /dev/null +++ b/lib/modules/readarr/api/src/types/history_sort_key.dart @@ -0,0 +1,33 @@ +part of readarr_types; + +enum ReadarrHistorySortKey { + DATE, + SERIES_TITLE, +} + +/// Extension on [ReadarrHistorySortKey] to implement extended functionality. +extension ReadarrHistorySortKeyExtension on ReadarrHistorySortKey { + /// Given a String, will return the correct `ReadarrHistorySortKey` object. + ReadarrHistorySortKey? from(String? type) { + switch (type) { + case 'date': + return ReadarrHistorySortKey.DATE; + case 'series.title': + return ReadarrHistorySortKey.SERIES_TITLE; + default: + return null; + } + } + + /// The actual value/key for sorting directions used in Readarr. + String? get value { + switch (this) { + case ReadarrHistorySortKey.DATE: + return 'date'; + case ReadarrHistorySortKey.SERIES_TITLE: + return 'series.title'; + default: + return null; + } + } +} diff --git a/lib/modules/readarr/api/src/types/protocol_type.dart b/lib/modules/readarr/api/src/types/protocol_type.dart new file mode 100644 index 0000000000..0749150303 --- /dev/null +++ b/lib/modules/readarr/api/src/types/protocol_type.dart @@ -0,0 +1,41 @@ +part of readarr_types; + +enum ReadarrProtocol { + USENET, + TORRENT, +} + +extension ReadarrProtocolExtension on ReadarrProtocol { + ReadarrProtocol? from(String? type) { + switch (type) { + case 'usenet': + return ReadarrProtocol.USENET; + case 'torrent': + return ReadarrProtocol.TORRENT; + default: + return null; + } + } + + String? get value { + switch (this) { + case ReadarrProtocol.USENET: + return 'usenet'; + case ReadarrProtocol.TORRENT: + return 'torrent'; + default: + return null; + } + } + + String? get readable { + switch (this) { + case ReadarrProtocol.USENET: + return 'Usenet'; + case ReadarrProtocol.TORRENT: + return 'Torrent'; + default: + return null; + } + } +} diff --git a/lib/modules/readarr/api/src/types/queue_sort_key.dart b/lib/modules/readarr/api/src/types/queue_sort_key.dart new file mode 100644 index 0000000000..bfeed704df --- /dev/null +++ b/lib/modules/readarr/api/src/types/queue_sort_key.dart @@ -0,0 +1,83 @@ +part of readarr_types; + +enum ReadarrQueueSortKey { + EPISODE, + TIME_LEFT, + ESTIMATED_COMPLETION_TIME, + PROTOCOL, + INDEXER, + DOWNLOAD_CLIENT, + LANGUAGE, + QUALITY, + STATUS, + SERIES_SORT_TITLE, + TITLE, + EPISODE_AIRDATE_UTC, + EPISODE_TITLE, +} + +extension ReadarrQueueSortKeyExtension on ReadarrQueueSortKey { + ReadarrQueueSortKey? from(String? type) { + switch (type) { + case 'episode': + return ReadarrQueueSortKey.EPISODE; + case 'timeleft': + return ReadarrQueueSortKey.TIME_LEFT; + case 'estimatedCompletionTime': + return ReadarrQueueSortKey.ESTIMATED_COMPLETION_TIME; + case 'protocol': + return ReadarrQueueSortKey.PROTOCOL; + case 'indexer': + return ReadarrQueueSortKey.INDEXER; + case 'downloadClient': + return ReadarrQueueSortKey.DOWNLOAD_CLIENT; + case 'language': + return ReadarrQueueSortKey.LANGUAGE; + case 'quality': + return ReadarrQueueSortKey.QUALITY; + case 'status': + return ReadarrQueueSortKey.STATUS; + case 'series.sortTitle': + return ReadarrQueueSortKey.SERIES_SORT_TITLE; + case 'title': + return ReadarrQueueSortKey.TITLE; + case 'episode.airDateUtc': + return ReadarrQueueSortKey.EPISODE_AIRDATE_UTC; + case 'episode.title': + return ReadarrQueueSortKey.EPISODE_TITLE; + default: + return null; + } + } + + String? get value { + switch (this) { + case ReadarrQueueSortKey.EPISODE: + return 'episode'; + case ReadarrQueueSortKey.TIME_LEFT: + return 'timeleft'; + case ReadarrQueueSortKey.ESTIMATED_COMPLETION_TIME: + return 'estimatedCompletionTime'; + case ReadarrQueueSortKey.PROTOCOL: + return 'protocol'; + case ReadarrQueueSortKey.INDEXER: + return 'indexer'; + case ReadarrQueueSortKey.DOWNLOAD_CLIENT: + return 'downloadClient'; + case ReadarrQueueSortKey.LANGUAGE: + return 'language'; + case ReadarrQueueSortKey.QUALITY: + return 'quality'; + case ReadarrQueueSortKey.STATUS: + return 'status'; + case ReadarrQueueSortKey.SERIES_SORT_TITLE: + return 'series.sortTitle'; + case ReadarrQueueSortKey.TITLE: + return 'title'; + case ReadarrQueueSortKey.EPISODE_AIRDATE_UTC: + return 'episode.airDateUtc'; + case ReadarrQueueSortKey.EPISODE_TITLE: + return 'episode.title'; + } + } +} diff --git a/lib/modules/readarr/api/src/types/queue_status_type.dart b/lib/modules/readarr/api/src/types/queue_status_type.dart new file mode 100644 index 0000000000..953b2a7c0e --- /dev/null +++ b/lib/modules/readarr/api/src/types/queue_status_type.dart @@ -0,0 +1,60 @@ +part of readarr_types; + +enum ReadarrQueueStatus { + DOWNLOADING, + PAUSED, + QUEUED, + COMPLETED, + DELAY, + DOWNLOAD_CLIENT_UNAVAILABLE, + FAILED, + WARNING, +} + +extension ReadarrQueueStatusExtension on ReadarrQueueStatus { + ReadarrQueueStatus? from(String? type) { + switch (type) { + case 'downloading': + return ReadarrQueueStatus.DOWNLOADING; + case 'paused': + return ReadarrQueueStatus.PAUSED; + case 'queued': + return ReadarrQueueStatus.QUEUED; + case 'completed': + return ReadarrQueueStatus.COMPLETED; + case 'delay': + return ReadarrQueueStatus.DELAY; + case 'downloadClientUnavailable': + return ReadarrQueueStatus.DOWNLOAD_CLIENT_UNAVAILABLE; + case 'failed': + return ReadarrQueueStatus.FAILED; + case 'warning': + return ReadarrQueueStatus.WARNING; + default: + return null; + } + } + + String? get value { + switch (this) { + case ReadarrQueueStatus.DOWNLOADING: + return 'downloading'; + case ReadarrQueueStatus.PAUSED: + return 'paused'; + case ReadarrQueueStatus.QUEUED: + return 'queued'; + case ReadarrQueueStatus.COMPLETED: + return 'completed'; + case ReadarrQueueStatus.DELAY: + return 'delay'; + case ReadarrQueueStatus.DOWNLOAD_CLIENT_UNAVAILABLE: + return 'downloadClientUnavailable'; + case ReadarrQueueStatus.FAILED: + return 'failed'; + case ReadarrQueueStatus.WARNING: + return 'warning'; + default: + return null; + } + } +} diff --git a/lib/modules/readarr/api/src/types/queue_tracked_download_state_type.dart b/lib/modules/readarr/api/src/types/queue_tracked_download_state_type.dart new file mode 100644 index 0000000000..f5ff5616df --- /dev/null +++ b/lib/modules/readarr/api/src/types/queue_tracked_download_state_type.dart @@ -0,0 +1,40 @@ +part of readarr_types; + +enum ReadarrTrackedDownloadState { + DOWNLOADING, + IMPORT_PENDING, + IMPORTING, + FAILED_PENDING, +} + +extension ReadarrTrackedDownloadStateExtension on ReadarrTrackedDownloadState { + ReadarrTrackedDownloadState? from(String? type) { + switch (type) { + case 'downloading': + return ReadarrTrackedDownloadState.DOWNLOADING; + case 'importPending': + return ReadarrTrackedDownloadState.IMPORT_PENDING; + case 'importing': + return ReadarrTrackedDownloadState.IMPORTING; + case 'failedPending': + return ReadarrTrackedDownloadState.FAILED_PENDING; + default: + return null; + } + } + + String? get value { + switch (this) { + case ReadarrTrackedDownloadState.DOWNLOADING: + return 'downloading'; + case ReadarrTrackedDownloadState.IMPORT_PENDING: + return 'importPending'; + case ReadarrTrackedDownloadState.IMPORTING: + return 'importing'; + case ReadarrTrackedDownloadState.FAILED_PENDING: + return 'failedPending'; + default: + return null; + } + } +} diff --git a/lib/modules/readarr/api/src/types/queue_tracked_download_status_type.dart b/lib/modules/readarr/api/src/types/queue_tracked_download_status_type.dart new file mode 100644 index 0000000000..4a27bbdff3 --- /dev/null +++ b/lib/modules/readarr/api/src/types/queue_tracked_download_status_type.dart @@ -0,0 +1,35 @@ +part of readarr_types; + +enum ReadarrTrackedDownloadStatus { + OK, + WARNING, + ERROR, +} + +extension ReadarrTrackedDownloadStatusExtension on ReadarrTrackedDownloadStatus { + ReadarrTrackedDownloadStatus? from(String? type) { + switch (type) { + case 'ok': + return ReadarrTrackedDownloadStatus.OK; + case 'warning': + return ReadarrTrackedDownloadStatus.WARNING; + case 'error': + return ReadarrTrackedDownloadStatus.ERROR; + default: + return null; + } + } + + String? get value { + switch (this) { + case ReadarrTrackedDownloadStatus.OK: + return 'ok'; + case ReadarrTrackedDownloadStatus.WARNING: + return 'warning'; + case ReadarrTrackedDownloadStatus.ERROR: + return 'error'; + default: + return null; + } + } +} diff --git a/lib/modules/readarr/api/src/types/sort_dir.dart b/lib/modules/readarr/api/src/types/sort_dir.dart new file mode 100644 index 0000000000..f91556fe13 --- /dev/null +++ b/lib/modules/readarr/api/src/types/sort_dir.dart @@ -0,0 +1,21 @@ +part of readarr_types; + +enum ReadarrSortDirection { + ASCENDING, + DESCENDING, +} + +/// Extension on [ReadarrSortDirection] to implement extended functionality. +extension ReadarrSortDirectionExtension on ReadarrSortDirection { + /// The actual value/key for sorting directions used in Readarr. + String? get value { + switch (this) { + case ReadarrSortDirection.ASCENDING: + return 'ascending'; + case ReadarrSortDirection.DESCENDING: + return 'descending'; + default: + return null; + } + } +} diff --git a/lib/modules/readarr/api/src/types/wanted_missing_sort_key.dart b/lib/modules/readarr/api/src/types/wanted_missing_sort_key.dart new file mode 100644 index 0000000000..55203714b3 --- /dev/null +++ b/lib/modules/readarr/api/src/types/wanted_missing_sort_key.dart @@ -0,0 +1,33 @@ +part of readarr_types; + +enum ReadarrWantedMissingSortKey { + RELEASE_DATE, + AUTHOR_TITLE, +} + +/// Extension on [ReadarrWantedMissingSortKey] to implement extended functionality. +extension ReadarrWantedMissingSortKeyExtension on ReadarrWantedMissingSortKey { + /// Given a String, will return the correct `ReadarrWantedMissingSortKey` object. + ReadarrWantedMissingSortKey? from(String? type) { + switch (type) { + case 'releaseDate': + return ReadarrWantedMissingSortKey.RELEASE_DATE; + case 'authorMetadata.sortName': + return ReadarrWantedMissingSortKey.AUTHOR_TITLE; + default: + return null; + } + } + + /// The actual value/key for sorting directions used in Readarr. + String? get value { + switch (this) { + case ReadarrWantedMissingSortKey.RELEASE_DATE: + return 'releaseDate'; + case ReadarrWantedMissingSortKey.AUTHOR_TITLE: + return 'authorMetadata.sortName'; + default: + return null; + } + } +} diff --git a/lib/modules/readarr/api/types.dart b/lib/modules/readarr/api/types.dart new file mode 100644 index 0000000000..a29b1c7561 --- /dev/null +++ b/lib/modules/readarr/api/types.dart @@ -0,0 +1,13 @@ +/// Library containing all type definitions for Readarr data. +library readarr_types; + +part 'src/types/event_type.dart'; +part 'src/types/history_sort_key.dart'; +part 'src/types/protocol_type.dart'; +part 'src/types/queue_sort_key.dart'; +part 'src/types/queue_status_type.dart'; +part 'src/types/queue_tracked_download_state_type.dart'; +part 'src/types/queue_tracked_download_status_type.dart'; +part 'src/types/author_monitor_type.dart'; +part 'src/types/sort_dir.dart'; +part 'src/types/wanted_missing_sort_key.dart'; diff --git a/lib/modules/readarr/api/utilities.dart b/lib/modules/readarr/api/utilities.dart new file mode 100644 index 0000000000..ba718152da --- /dev/null +++ b/lib/modules/readarr/api/utilities.dart @@ -0,0 +1,62 @@ +library readarr_utilities; + +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrUtilities { + ReadarrUtilities._(); + + static DateTime? dateTimeFromJson(String? date) { + return DateTime.tryParse(date ?? ''); + } + + static String? dateTimeToJson(DateTime? date) { + return date?.toIso8601String(); + } + + static ReadarrEventType? eventTypeFromJson(String? type) { + return ReadarrEventType.GRABBED.from(type); + } + + static String? eventTypeToJson(ReadarrEventType? type) { + return type?.value; + } + + static ReadarrHistorySortKey? historySortKeyFromJson(String? key) { + return ReadarrHistorySortKey.DATE.from(key); + } + + static String? historySortKeyToJson(ReadarrHistorySortKey? key) { + return key?.value; + } + + static ReadarrWantedMissingSortKey? wantedMissingSortKeyFromJson( + String? key) { + return ReadarrWantedMissingSortKey.RELEASE_DATE.from(key); + } + + static String? wantedMissingSortKeyToJson(ReadarrWantedMissingSortKey? key) { + return key?.value; + } + + static ReadarrProtocol? protocolFromJson(String? protocol) => + ReadarrProtocol.USENET.from(protocol); + static String? protocolToJson(ReadarrProtocol? protocol) => protocol?.value; + + static ReadarrQueueStatus? queueStatusFromJson(String? status) => + ReadarrQueueStatus.DOWNLOADING.from(status); + static String? queueStatusToJson(ReadarrQueueStatus? status) => status?.value; + + static ReadarrTrackedDownloadState? queueTrackedDownloadStateFromJson( + String? state) => + ReadarrTrackedDownloadState.DOWNLOADING.from(state); + static String? queueTrackedDownloadStateToJson( + ReadarrTrackedDownloadState? state) => + state?.value; + + static ReadarrTrackedDownloadStatus? queueTrackedDownloadStatusFromJson( + String? status) => + ReadarrTrackedDownloadStatus.OK.from(status); + static String? queueTrackedDownloadStatusToJson( + ReadarrTrackedDownloadStatus? status) => + status?.value; +} diff --git a/lib/modules/readarr/core.dart b/lib/modules/readarr/core.dart new file mode 100644 index 0000000000..05057539f3 --- /dev/null +++ b/lib/modules/readarr/core.dart @@ -0,0 +1,8 @@ +export 'core/api_controller.dart'; +export 'core/database.dart'; +export 'core/dialogs.dart'; +export 'core/extensions.dart'; +export 'core/router.dart'; +export 'core/state.dart'; +export 'core/types.dart'; +export 'core/webhooks.dart'; diff --git a/lib/modules/readarr/core/api_controller.dart b/lib/modules/readarr/core/api_controller.dart new file mode 100644 index 0000000000..f85becd8c8 --- /dev/null +++ b/lib/modules/readarr/core/api_controller.dart @@ -0,0 +1,738 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAPIController { + Future downloadRelease({ + required BuildContext context, + required ReadarrRelease release, + bool showSnackbar = true, + }) async { + if (context.read().enabled) { + return context + .read() + .api! + .release + .add( + indexerId: release.indexerId!, + guid: release.guid!, + ) + .then((_) { + if (showSnackbar) { + showLunaSuccessSnackBar( + title: 'readarr.DownloadingRelease'.tr(), + message: lunaSafeString(release.title), + ); + } + return true; + }).catchError((error, stack) { + LunaLogger().error( + 'Failed to set download release (${release.guid})', + error, + stack, + ); + if (showSnackbar) { + showLunaErrorSnackBar( + title: 'readarr.FailedToDownloadRelease'.tr(), + error: error, + ); + } + return false; + }); + } + return false; + } + + Future toggleBookMonitored({ + required BuildContext context, + required ReadarrBook book, + bool showSnackbar = true, + }) async { + ReadarrBook _book = book.clone(); + _book.monitored = !_book.monitored!; + if (context.read().enabled) { + return context.read().api!.book.setMonitored( + bookIds: [_book.id!], + monitored: _book.monitored!, + ).then((_) { + context.read().setSingleBook(_book); + if (showSnackbar) { + showLunaSuccessSnackBar( + title: _book.monitored! + ? 'readarr.Monitoring'.tr() + : 'readarr.NoLongerMonitoring'.tr(), + message: _book.title, + ); + } + return true; + }).catchError((error, stack) { + LunaLogger().error( + 'Failed to set episode monitored state (${_book.id})', + error, + stack, + ); + if (showSnackbar) { + showLunaErrorSnackBar( + title: _book.monitored! + ? 'readarr.FailedToMonitorBook'.tr() + : 'readarr.FailedToUnmonitorBook'.tr(), + error: error, + ); + } + return false; + }); + } + return false; + } + + Future deleteBookFile({ + required BuildContext context, + required ReadarrBookFile bookFile, + bool showSnackbar = true, + }) async { + if (context.read().enabled) { + return context + .read() + .api! + .bookFile + .delete(bookFileId: bookFile.id!) + .then((response) { + //context.read().setSingleEpisode(_episode); + if (showSnackbar) { + showLunaSuccessSnackBar( + title: 'readarr.EpisodeFileDeleted'.tr(), + message: bookFile.path, + ); + } + return true; + }).catchError((error, stack) { + LunaLogger().error( + 'Failed to delete episode (${bookFile.id})', + error, + stack, + ); + if (showSnackbar) { + showLunaErrorSnackBar( + title: 'readarr.FailedToDeleteEpisodeFile'.tr(), + error: error, + ); + } + return false; + }); + } + return false; + } + + Future authorSearch({ + required BuildContext context, + required ReadarrAuthor author, + bool showSnackbar = true, + }) async { + if (context.read().enabled) { + return context + .read() + .api! + .command + .authorSearch(authorId: author.id!) + .then((response) { + if (showSnackbar) { + showLunaSuccessSnackBar( + title: 'readarr.SearchingForAuthor'.tr(), + message: author.title, + ); + } + return true; + }).catchError((error, stack) { + LunaLogger().error( + 'Failed to search for author: ${author.id}', + error, + stack, + ); + if (showSnackbar) { + showLunaErrorSnackBar( + title: 'readarr.FailedToSearch'.tr(), + error: error, + ); + } + return false; + }); + } + return false; + } + + Future bookSearch({ + required BuildContext context, + required ReadarrBook book, + bool showSnackbar = true, + }) async { + if (context.read().enabled) { + return context + .read() + .api! + .command + .bookSearch(bookIds: [book.id!]).then((response) { + if (showSnackbar) { + showLunaSuccessSnackBar( + title: 'readarr.SearchingForBook'.tr(), + message: book.title, + ); + } + return true; + }).catchError((error, stack) { + LunaLogger().error( + 'Failed to search for book: ${book.id}', + error, + stack, + ); + if (showSnackbar) { + showLunaErrorSnackBar( + title: 'readarr.FailedToSearch'.tr(), + error: error, + ); + } + return false; + }); + } + return false; + } + + Future toggleAuthorMonitored({ + required BuildContext context, + required ReadarrAuthor author, + bool showSnackbar = true, + }) async { + if (context.read().enabled) { + ReadarrAuthor authorCopy = author.clone(); + authorCopy.monitored = !author.monitored!; + return await context + .read() + .api! + .author + .update(author: authorCopy) + .then((data) async { + return await context + .read() + .setSingleAuthor(authorCopy) + .then((_) { + if (showSnackbar) { + showLunaSuccessSnackBar( + title: authorCopy.monitored! + ? 'readarr.Monitoring'.tr() + : 'readarr.NoLongerMonitoring'.tr(), + message: authorCopy.title, + ); + } + return true; + }); + }).catchError((error, stack) { + LunaLogger().error( + 'Unable to toggle monitored state: ${author.monitored.toString()} to ${authorCopy.monitored.toString()}', + error, + stack, + ); + if (showSnackbar) + showLunaErrorSnackBar( + title: author.monitored! + ? 'readarr.FailedToUnmonitorAuthor'.tr() + : 'readarr.FailedToMonitorAuthor'.tr(), + error: error, + ); + return false; + }); + } + return false; + } + + Future addTag({ + required BuildContext context, + required String label, + bool showSnackbar = true, + }) async { + if (context.read().enabled) { + return await context + .read() + .api! + .tag + .create(label: label) + .then((tag) { + showLunaSuccessSnackBar( + title: 'readarr.AddedTag'.tr(), + message: tag.label, + ); + return true; + }).catchError((error, stack) { + LunaLogger().error('Failed to add tag: $label', error, stack); + if (showSnackbar) + showLunaErrorSnackBar( + title: 'readarr.FailedToAddTag'.tr(), + error: error, + ); + return false; + }); + } + return false; + } + + Future updateAuthor({ + required BuildContext context, + required ReadarrAuthor series, + bool showSnackbar = true, + }) async { + if (context.read().enabled) { + return await context + .read() + .api! + .author + .update(author: series) + .then((_) async { + return await context + .read() + .setSingleAuthor(series) + .then((_) { + if (showSnackbar) { + showLunaSuccessSnackBar( + title: 'readarr.UpdatedSeries'.tr(), + message: series.title, + ); + } + return true; + }); + }).catchError((error, stack) { + LunaLogger().error( + 'Failed to update series: ${series.id}', + error, + stack, + ); + showLunaErrorSnackBar( + title: 'readarr.FailedToUpdateAuthor'.tr(), + error: error, + ); + return false; + }); + } + return true; + } + + Future backupDatabase({ + required BuildContext context, + bool showSnackbar = true, + }) async { + if (context.read().enabled) { + return await context.read().api!.command.backup().then((_) { + if (showSnackbar) { + showLunaSuccessSnackBar( + title: 'readarr.BackingUpDatabase'.tr(args: [LunaUI.TEXT_ELLIPSIS]), + message: 'readarr.BackingUpDatabaseDescription'.tr(), + ); + } + return true; + }).catchError((error, stack) { + LunaLogger().error( + 'Readarr: Unable to backup database', + error, + stack, + ); + if (showSnackbar) + showLunaErrorSnackBar( + title: 'readarr.FailedToBackupDatabase'.tr(), + error: error, + ); + return false; + }); + } + return false; + } + + Future automaticSeasonSearch({ + required BuildContext context, + required int? authorId, + required int? seasonNumber, + bool showSnackbar = true, + }) async { + if (context.read().enabled) { + return await context + .read() + .api! + .command + .seasonSearch(authorId: authorId!, seasonNumber: seasonNumber!) + .then((_) { + if (showSnackbar) + showLunaSuccessSnackBar( + title: + 'readarr.SearchingForSeason'.tr(args: [LunaUI.TEXT_ELLIPSIS]), + message: seasonNumber == 0 + ? 'readarr.Specials'.tr() + : 'readarr.SeasonNumber'.tr(args: [seasonNumber.toString()]), + ); + return true; + }).catchError((error, stack) { + LunaLogger().error( + 'Failed to season search ($authorId, $seasonNumber)', + error, + stack, + ); + if (showSnackbar) + showLunaErrorSnackBar( + title: 'readarr.FailedToSeasonSearch'.tr(), + error: error, + ); + return false; + }); + } + return false; + } + + Future runRSSSync({ + required BuildContext context, + bool showSnackbar = true, + }) async { + if (context.read().enabled) { + return await context + .read() + .api! + .command + .rssSync() + .then((_) { + if (showSnackbar) + showLunaSuccessSnackBar( + title: 'readarr.RunningRSSSync'.tr(args: [LunaUI.TEXT_ELLIPSIS]), + message: 'readarr.RunningRSSSyncDescription'.tr(), + ); + return true; + }).catchError((error, stack) { + LunaLogger().error( + 'Unable to run RSS sync', + error, + stack, + ); + if (showSnackbar) + showLunaErrorSnackBar( + title: 'readarr.FailedToRunRSSSync'.tr(), + error: error, + ); + return false; + }); + } + return false; + } + + Future updateLibrary({ + required BuildContext context, + bool showSnackbar = true, + }) async { + if (context.read().enabled) { + return await context + .read() + .api! + .command + .refreshAuthor() + .then((_) { + if (showSnackbar) + showLunaSuccessSnackBar( + title: 'readarr.UpdatingLibrary'.tr(args: [LunaUI.TEXT_ELLIPSIS]), + message: 'readarr.UpdatingLibraryDescription'.tr(), + ); + return true; + }).catchError((error, stack) { + LunaLogger().error( + 'Unable to update library', + error, + stack, + ); + if (showSnackbar) + showLunaErrorSnackBar( + title: 'readarr.FailedToUpdateLibrary'.tr(), + error: error, + ); + return false; + }); + } + return false; + } + + Future missingBooksSearch({ + required BuildContext context, + bool showSnackbar = true, + }) async { + if (context.read().enabled) { + return await context + .read() + .api! + .command + .missingBooksSearch() + .then((_) { + if (showSnackbar) + showLunaSuccessSnackBar( + title: 'readarr.Searching'.tr(args: [LunaUI.TEXT_ELLIPSIS]), + message: 'readarr.SearchingDescription'.tr(), + ); + return true; + }).catchError((error, stack) { + LunaLogger().error( + 'Readarr: Unable to search for all missing episodes', + error, + stack, + ); + if (showSnackbar) + showLunaErrorSnackBar( + title: 'readarr.FailedToSearch'.tr(), + error: error, + ); + return false; + }); + } + return false; + } + + Future refreshAuthor({ + required BuildContext context, + required ReadarrAuthor series, + bool showSnackbar = true, + }) async { + if (context.read().enabled) { + return await context + .read() + .api! + .command + .refreshAuthor(authorId: series.id) + .then((_) { + if (showSnackbar) + showLunaSuccessSnackBar( + title: 'lunasea.Refreshing'.tr(), + message: series.title, + ); + return true; + }).catchError((error, stack) { + LunaLogger().error( + 'Readarr: Unable to refresh book: ${series.id}', + error, + stack, + ); + if (showSnackbar) + showLunaErrorSnackBar( + title: 'readarr.FailedToRefresh'.tr(), + error: error, + ); + return false; + }); + } + return false; + } + + Future refreshBook({ + required BuildContext context, + required ReadarrBook book, + bool showSnackbar = true, + }) async { + if (context.read().enabled) { + return await context + .read() + .api! + .command + .refreshBook(bookId: book.id) + .then((_) { + if (showSnackbar) + showLunaSuccessSnackBar( + title: 'lunasea.Refreshing'.tr(), + message: book.title, + ); + return true; + }).catchError((error, stack) { + LunaLogger().error( + 'Readarr: Unable to refresh book: ${book.id}', + error, + stack, + ); + if (showSnackbar) + showLunaErrorSnackBar( + title: 'readarr.FailedToRefresh'.tr(), + error: error, + ); + return false; + }); + } + return false; + } + + Future removeAuthor({ + required BuildContext context, + required ReadarrAuthor author, + bool showSnackbar = true, + }) async { + if (context.read().enabled) { + return await context + .read() + .api! + .author + .delete( + authorId: author.id!, + deleteFiles: ReadarrDatabaseValue.REMOVE_SERIES_DELETE_FILES.data, + addImportListExclusion: + ReadarrDatabaseValue.REMOVE_SERIES_EXCLUSION_LIST.data, + ) + .then((_) async { + return await context + .read() + .removeSingleAuthor(author.id!) + .then((_) { + if (showSnackbar) + showLunaSuccessSnackBar( + title: ReadarrDatabaseValue.REMOVE_SERIES_DELETE_FILES.data + ? 'readarr.RemovedAuthorWithFiles'.tr() + : 'readarr.RemovedAuthor'.tr(), + message: author.title, + ); + return true; + }); + }).catchError((error, stack) { + LunaLogger().error( + 'Failed to remove author: ${author.id}', + error, + stack, + ); + if (showSnackbar) + showLunaErrorSnackBar( + title: 'readarr.FailedToRemoveAuthor'.tr(), + error: error, + ); + return false; + }); + } + return false; + } + + Future removeBook({ + required BuildContext context, + required ReadarrBook book, + bool showSnackbar = true, + }) async { + if (context.read().enabled) { + return await context + .read() + .api! + .book + .delete( + bookId: book.id!, + deleteFiles: ReadarrDatabaseValue.REMOVE_BOOK_DELETE_FILES.data, + addImportListExclusion: + ReadarrDatabaseValue.REMOVE_BOOK_EXCLUSION_LIST.data, + ) + .then((_) async { + return await context + .read() + .removeSingleBook(book.id!) + .then((_) { + if (showSnackbar) + showLunaSuccessSnackBar( + title: ReadarrDatabaseValue.REMOVE_BOOK_DELETE_FILES.data + ? 'readarr.RemovedBookWithFiles'.tr() + : 'readarr.RemovedBook'.tr(), + message: book.title, + ); + return true; + }); + }).catchError((error, stack) { + LunaLogger().error( + 'Failed to remove series: ${book.id}', + error, + stack, + ); + if (showSnackbar) + showLunaErrorSnackBar( + title: 'readarr.FailedToRemoveBook'.tr(), + error: error, + ); + return false; + }); + } + return false; + } + + Future addAuthor({ + required BuildContext context, + required ReadarrAuthor author, + required ReadarrQualityProfile qualityProfile, + required ReadarrMetadataProfile metadataProfile, + required ReadarrRootFolder rootFolder, + required ReadarrAuthorMonitorType monitorType, + required List tags, + bool showSnackbar = true, + }) async { + if (context.read().enabled) { + author.id = 0; + return await context + .read() + .api! + .author + .create( + author: author, + qualityProfile: qualityProfile, + metadataProfile: metadataProfile, + rootFolder: rootFolder, + monitorType: monitorType, + tags: tags, + searchForMissingEpisodes: + ReadarrDatabaseValue.ADD_SERIES_SEARCH_FOR_MISSING.data, + searchForCutoffUnmetEpisodes: + ReadarrDatabaseValue.ADD_SERIES_SEARCH_FOR_CUTOFF_UNMET.data, + ) + .then((series) { + if (showSnackbar) { + showLunaSuccessSnackBar( + title: 'readarr.AddedSeries'.tr(), + message: series.title, + ); + } + return series; + }).catchError((error, stack) { + LunaLogger().error( + 'Failed to add series (foreignId: ${author.foreignAuthorId})', + error, + stack, + ); + if (showSnackbar) { + showLunaErrorSnackBar( + title: 'readarr.FailedToAddAuthor'.tr(), + error: error, + ); + } + }); + } + return null; + } + + Future removeFromQueue({ + required BuildContext context, + required ReadarrQueueRecord queueRecord, + bool showSnackbar = true, + }) async { + if (context.read().enabled) { + return context + .read() + .api! + .queue + .delete(id: queueRecord.id!) + .then((_) { + if (showSnackbar) + showLunaSuccessSnackBar( + title: 'readarr.RemovedFromQueue'.tr(), + message: queueRecord.title, + ); + return true; + }).catchError((error, stack) { + LunaLogger().error( + 'Failed to remove queue record: ${queueRecord.id}', + error, + stack, + ); + if (showSnackbar) + showLunaErrorSnackBar( + title: 'readarr.FailedToRemoveFromQueue'.tr(), + error: error, + ); + return false; + }); + } + return false; + } +} diff --git a/lib/modules/readarr/core/database.dart b/lib/modules/readarr/core/database.dart new file mode 100644 index 0000000000..59c97603fa --- /dev/null +++ b/lib/modules/readarr/core/database.dart @@ -0,0 +1,278 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +enum ReadarrDatabaseValue { + NAVIGATION_INDEX, + NAVIGATION_INDEX_SERIES_DETAILS, + NAVIGATION_INDEX_SEASON_DETAILS, + ADD_SERIES_SEARCH_FOR_MISSING, + ADD_SERIES_SEARCH_FOR_CUTOFF_UNMET, + ADD_SERIES_DEFAULT_MONITORED, + ADD_SERIES_DEFAULT_MONITOR_TYPE, + ADD_SERIES_DEFAULT_LANGUAGE_PROFILE, + ADD_SERIES_DEFAULT_QUALITY_PROFILE, + ADD_SERIES_DEFAULT_ROOT_FOLDER, + ADD_SERIES_DEFAULT_TAGS, + DEFAULT_VIEW_SERIES, + DEFAULT_FILTERING_SERIES, + DEFAULT_FILTERING_RELEASES, + DEFAULT_SORTING_SERIES, + DEFAULT_SORTING_RELEASES, + DEFAULT_SORTING_SERIES_ASCENDING, + DEFAULT_SORTING_RELEASES_ASCENDING, + REMOVE_SERIES_DELETE_FILES, + REMOVE_SERIES_EXCLUSION_LIST, + UPCOMING_FUTURE_DAYS, + QUEUE_PAGE_SIZE, + QUEUE_REFRESH_RATE, + QUEUE_REMOVE_DOWNLOAD_CLIENT, + QUEUE_ADD_BLOCKLIST, + CONTENT_PAGE_SIZE, + REMOVE_BOOK_DELETE_FILES, + REMOVE_BOOK_EXCLUSION_LIST, +} + +class ReadarrDatabase extends LunaModuleDatabase { + @override + void registerAdapters() { + // Active adapters + Hive.registerAdapter(ReadarrAuthorSortingAdapter()); + Hive.registerAdapter(ReadarrAuthorFilterAdapter()); + Hive.registerAdapter(ReadarrReleasesSortingAdapter()); + Hive.registerAdapter(ReadarrReleasesFilterAdapter()); + } + + @override + Map export() { + Map data = {}; + for (ReadarrDatabaseValue value in ReadarrDatabaseValue.values) { + switch (value) { + // Non-primitive values + case ReadarrDatabaseValue.DEFAULT_SORTING_SERIES: + data[value.key] = (ReadarrDatabaseValue.DEFAULT_SORTING_SERIES.data + as ReadarrAuthorSorting) + .key; + break; + case ReadarrDatabaseValue.DEFAULT_SORTING_RELEASES: + data[value.key] = (ReadarrDatabaseValue.DEFAULT_SORTING_RELEASES.data + as ReadarrReleasesSorting) + .key; + break; + case ReadarrDatabaseValue.DEFAULT_FILTERING_SERIES: + data[value.key] = (ReadarrDatabaseValue.DEFAULT_FILTERING_SERIES.data + as ReadarrAuthorFilter) + .key; + break; + case ReadarrDatabaseValue.DEFAULT_FILTERING_RELEASES: + data[value.key] = (ReadarrDatabaseValue + .DEFAULT_FILTERING_RELEASES.data as ReadarrReleasesFilter) + .key; + break; + case ReadarrDatabaseValue.DEFAULT_VIEW_SERIES: + data[value.key] = (ReadarrDatabaseValue.DEFAULT_VIEW_SERIES.data + as LunaListViewOption?)! + .key; + break; + // Primitive values + default: + data[value.key] = value.data; + break; + } + } + return data; + } + + @override + void import(Map config) { + for (String key in config.keys) { + ReadarrDatabaseValue? value = valueFromKey(key); + if (value != null) + switch (value) { + // Non-primitive values + case ReadarrDatabaseValue.DEFAULT_SORTING_SERIES: + value.put(ReadarrAuthorSorting.ALPHABETICAL.fromKey(config[key])); + break; + case ReadarrDatabaseValue.DEFAULT_SORTING_RELEASES: + value.put(ReadarrReleasesSorting.ALPHABETICAL.fromKey(config[key])); + break; + case ReadarrDatabaseValue.DEFAULT_FILTERING_SERIES: + value.put(ReadarrAuthorFilter.ALL.fromKey(config[key])); + break; + case ReadarrDatabaseValue.DEFAULT_FILTERING_RELEASES: + value.put(ReadarrReleasesFilter.ALL.fromKey(config[key])); + break; + case ReadarrDatabaseValue.DEFAULT_VIEW_SERIES: + value.put(LunaListViewOption.GRID_VIEW.fromKey(config[key])); + break; + // Primitive values + default: + value.put(config[key]); + break; + } + } + } + + @override + ReadarrDatabaseValue? valueFromKey(String key) { + for (ReadarrDatabaseValue value in ReadarrDatabaseValue.values) { + if (value.key == key) return value; + } + return null; + } +} + +extension ReadarrDatabaseValueExtension on ReadarrDatabaseValue { + String get key { + return 'READARR_${this.name}'; + } + + dynamic get data { + return Database.lunasea.box.get(this.key, defaultValue: this._defaultValue); + } + + void put(dynamic value) { + if (this._isTypeValid(value)) { + Database.lunasea.box.put(this.key, value); + } else { + LunaLogger().warning( + this.runtimeType.toString(), + 'put', + 'Invalid Database Insert (${this.key}, ${value.runtimeType})', + ); + } + } + + ValueListenableBuilder listen({ + Key? key, + required Widget Function(BuildContext, dynamic, Widget?) builder, + }) { + return ValueListenableBuilder( + key: key, + valueListenable: Database.lunasea.box.listenable(keys: [this.key]), + builder: builder, + ); + } + + bool _isTypeValid(dynamic value) { + switch (this) { + case ReadarrDatabaseValue.NAVIGATION_INDEX: + return value is int; + case ReadarrDatabaseValue.NAVIGATION_INDEX_SERIES_DETAILS: + return value is int; + case ReadarrDatabaseValue.NAVIGATION_INDEX_SEASON_DETAILS: + return value is int; + case ReadarrDatabaseValue.ADD_SERIES_DEFAULT_MONITORED: + return value is bool; + case ReadarrDatabaseValue.ADD_SERIES_DEFAULT_MONITOR_TYPE: + return value is String; + case ReadarrDatabaseValue.ADD_SERIES_DEFAULT_LANGUAGE_PROFILE: + return value is int; + case ReadarrDatabaseValue.ADD_SERIES_DEFAULT_QUALITY_PROFILE: + return value is int; + case ReadarrDatabaseValue.ADD_SERIES_DEFAULT_ROOT_FOLDER: + return value is int; + case ReadarrDatabaseValue.ADD_SERIES_DEFAULT_TAGS: + return value is List; + case ReadarrDatabaseValue.DEFAULT_SORTING_SERIES: + return value is ReadarrAuthorSorting; + case ReadarrDatabaseValue.DEFAULT_FILTERING_SERIES: + return value is ReadarrAuthorFilter; + case ReadarrDatabaseValue.DEFAULT_FILTERING_RELEASES: + return value is ReadarrReleasesFilter; + case ReadarrDatabaseValue.DEFAULT_SORTING_RELEASES: + return value is ReadarrReleasesSorting; + case ReadarrDatabaseValue.DEFAULT_SORTING_SERIES_ASCENDING: + return value is bool; + case ReadarrDatabaseValue.DEFAULT_SORTING_RELEASES_ASCENDING: + return value is bool; + case ReadarrDatabaseValue.UPCOMING_FUTURE_DAYS: + return value is int; + case ReadarrDatabaseValue.QUEUE_PAGE_SIZE: + return value is int; + case ReadarrDatabaseValue.QUEUE_REFRESH_RATE: + return value is int; + case ReadarrDatabaseValue.CONTENT_PAGE_SIZE: + return value is int; + case ReadarrDatabaseValue.REMOVE_SERIES_DELETE_FILES: + return value is bool; + case ReadarrDatabaseValue.REMOVE_SERIES_EXCLUSION_LIST: + return value is bool; + case ReadarrDatabaseValue.ADD_SERIES_SEARCH_FOR_CUTOFF_UNMET: + return value is bool; + case ReadarrDatabaseValue.ADD_SERIES_SEARCH_FOR_MISSING: + return value is bool; + case ReadarrDatabaseValue.QUEUE_REMOVE_DOWNLOAD_CLIENT: + return value is bool; + case ReadarrDatabaseValue.QUEUE_ADD_BLOCKLIST: + return value is bool; + case ReadarrDatabaseValue.DEFAULT_VIEW_SERIES: + return value is LunaListViewOption; + case ReadarrDatabaseValue.REMOVE_BOOK_DELETE_FILES: + return value is bool; + case ReadarrDatabaseValue.REMOVE_BOOK_EXCLUSION_LIST: + return value is bool; + } + } + + dynamic get _defaultValue { + switch (this) { + case ReadarrDatabaseValue.NAVIGATION_INDEX: + return 0; + case ReadarrDatabaseValue.NAVIGATION_INDEX_SERIES_DETAILS: + return 0; + case ReadarrDatabaseValue.NAVIGATION_INDEX_SEASON_DETAILS: + return 0; + case ReadarrDatabaseValue.UPCOMING_FUTURE_DAYS: + return 7; + case ReadarrDatabaseValue.QUEUE_PAGE_SIZE: + return 50; + case ReadarrDatabaseValue.QUEUE_REFRESH_RATE: + return 15; + case ReadarrDatabaseValue.CONTENT_PAGE_SIZE: + return 25; + case ReadarrDatabaseValue.ADD_SERIES_DEFAULT_MONITORED: + return true; + case ReadarrDatabaseValue.ADD_SERIES_DEFAULT_MONITOR_TYPE: + return ReadarrAuthorMonitorType.ALL.value; + case ReadarrDatabaseValue.ADD_SERIES_DEFAULT_LANGUAGE_PROFILE: + return null; + case ReadarrDatabaseValue.ADD_SERIES_DEFAULT_QUALITY_PROFILE: + return null; + case ReadarrDatabaseValue.ADD_SERIES_DEFAULT_ROOT_FOLDER: + return null; + case ReadarrDatabaseValue.ADD_SERIES_DEFAULT_TAGS: + return []; + case ReadarrDatabaseValue.DEFAULT_SORTING_SERIES: + return ReadarrAuthorSorting.ALPHABETICAL; + case ReadarrDatabaseValue.DEFAULT_FILTERING_SERIES: + return ReadarrAuthorFilter.ALL; + case ReadarrDatabaseValue.DEFAULT_FILTERING_RELEASES: + return ReadarrReleasesFilter.ALL; + case ReadarrDatabaseValue.DEFAULT_SORTING_RELEASES: + return ReadarrReleasesSorting.WEIGHT; + case ReadarrDatabaseValue.DEFAULT_SORTING_SERIES_ASCENDING: + return true; + case ReadarrDatabaseValue.DEFAULT_SORTING_RELEASES_ASCENDING: + return true; + case ReadarrDatabaseValue.REMOVE_SERIES_DELETE_FILES: + return false; + case ReadarrDatabaseValue.REMOVE_SERIES_EXCLUSION_LIST: + return false; + case ReadarrDatabaseValue.ADD_SERIES_SEARCH_FOR_CUTOFF_UNMET: + return false; + case ReadarrDatabaseValue.ADD_SERIES_SEARCH_FOR_MISSING: + return false; + case ReadarrDatabaseValue.QUEUE_REMOVE_DOWNLOAD_CLIENT: + return false; + case ReadarrDatabaseValue.QUEUE_ADD_BLOCKLIST: + return false; + case ReadarrDatabaseValue.DEFAULT_VIEW_SERIES: + return LunaListViewOption.BLOCK_VIEW; + case ReadarrDatabaseValue.REMOVE_BOOK_DELETE_FILES: + return false; + case ReadarrDatabaseValue.REMOVE_BOOK_EXCLUSION_LIST: + return false; + } + } +} diff --git a/lib/modules/readarr/core/dialogs.dart b/lib/modules/readarr/core/dialogs.dart new file mode 100644 index 0000000000..3de9fc790f --- /dev/null +++ b/lib/modules/readarr/core/dialogs.dart @@ -0,0 +1,823 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrDialogs { + Future> globalSettings( + BuildContext context, + ) async { + bool _flag = false; + ReadarrGlobalSettingsType? _value; + + void _setValues(bool flag, ReadarrGlobalSettingsType value) { + _flag = flag; + _value = value; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LunaDialog.dialog( + context: context, + title: 'lunasea.Settings'.tr(), + content: List.generate( + ReadarrGlobalSettingsType.values.length, + (index) => LunaDialog.tile( + text: ReadarrGlobalSettingsType.values[index].name, + icon: ReadarrGlobalSettingsType.values[index].icon, + iconColor: LunaColours().byListIndex(index), + onTap: () => + _setValues(true, ReadarrGlobalSettingsType.values[index]), + ), + ), + contentPadding: LunaDialog.listDialogContentPadding(), + ); + return Tuple2(_flag, _value); + } + + Future> authorSettings( + BuildContext context, + ReadarrAuthor series, + ) async { + bool _flag = false; + ReadarrAuthorSettingsType? _value; + + void _setValues(bool flag, ReadarrAuthorSettingsType value) { + _flag = flag; + _value = value; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LunaDialog.dialog( + context: context, + title: series.title, + content: List.generate( + ReadarrAuthorSettingsType.values.length, + (index) => LunaDialog.tile( + text: ReadarrAuthorSettingsType.values[index].name(series), + icon: ReadarrAuthorSettingsType.values[index].icon(series), + iconColor: LunaColours().byListIndex(index), + onTap: () => + _setValues(true, ReadarrAuthorSettingsType.values[index]), + ), + ), + contentPadding: LunaDialog.listDialogContentPadding(), + ); + return Tuple2(_flag, _value); + } + + Future> bookSettings( + BuildContext context, + ReadarrBook book, + ) async { + bool _flag = false; + ReadarrBookSettingsType? _value; + + void _setValues(bool flag, ReadarrBookSettingsType value) { + _flag = flag; + _value = value; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LunaDialog.dialog( + context: context, + title: book.title, + content: List.generate( + ReadarrBookSettingsType.values.length, + (index) => LunaDialog.tile( + text: ReadarrBookSettingsType.values[index].name(book), + icon: ReadarrBookSettingsType.values[index].icon(book), + iconColor: LunaColours().byListIndex(index), + onTap: () => _setValues(true, ReadarrBookSettingsType.values[index]), + ), + ), + contentPadding: LunaDialog.listDialogContentPadding(), + ); + return Tuple2(_flag, _value); + } + + static Future> setDefaultPage( + BuildContext context, { + required List titles, + required List icons, + }) async { + bool _flag = false; + int _index = 0; + + void _setValues(bool flag, int index) { + _flag = flag; + _index = index; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LunaDialog.dialog( + context: context, + title: 'Page', + content: List.generate( + titles.length, + (index) => LunaDialog.tile( + text: titles[index], + icon: icons[index], + iconColor: LunaColours().byListIndex(index), + onTap: () => _setValues(true, index), + ), + ), + contentPadding: LunaDialog.listDialogContentPadding(), + ); + + return [_flag, _index]; + } + + Future setAddTags(BuildContext context) async { + await showDialog( + context: context, + builder: (dContext) => ChangeNotifierProvider.value( + value: context.read(), + builder: (context, _) => + Selector>?>( + selector: (_, state) => state.tags, + builder: (context, future, _) => FutureBuilder( + future: future, + builder: (context, AsyncSnapshot> snapshot) { + return AlertDialog( + actions: [ + const ReadarrTagsAppBarActionAddTag(asDialogButton: true), + LunaDialog.button( + text: 'Close', + onPressed: () => + Navigator.of(context, rootNavigator: true).pop(), + ), + ], + title: LunaDialog.title(text: 'Tags'), + content: Builder( + builder: (context) { + if ((snapshot.data?.length ?? 0) == 0) + return LunaDialog.content( + children: [ + LunaDialog.textContent(text: 'No Tags Found'), + ], + ); + return LunaDialog.content( + children: List.generate( + snapshot.data!.length, + (index) => LunaDialog.checkbox( + title: snapshot.data![index].label!, + value: context + .watch() + .tags + .where( + (tag) => tag.id == snapshot.data![index].id) + .isNotEmpty, + onChanged: (selected) { + List _tags = context + .read() + .tags; + selected! + ? _tags.add(snapshot.data![index]) + : _tags.removeWhere((tag) => + tag.id == snapshot.data![index].id); + context.read().tags = + _tags; + }, + ), + ), + ); + }, + ), + contentPadding: (snapshot.data?.length ?? 0) == 0 + ? LunaDialog.textDialogContentPadding() + : LunaDialog.listDialogContentPadding(), + shape: LunaUI.shapeBorder, + ); + }, + ), + ), + ), + ); + } + + Future setEditTags(BuildContext context) async { + await showDialog( + context: context, + builder: (dContext) => ChangeNotifierProvider.value( + value: context.read(), + builder: (context, _) => + Selector>?>( + selector: (_, state) => state.tags, + builder: (context, future, _) => FutureBuilder( + future: future, + builder: (context, AsyncSnapshot> snapshot) { + return AlertDialog( + actions: [ + const ReadarrTagsAppBarActionAddTag(asDialogButton: true), + LunaDialog.button( + text: 'Close', + onPressed: () => + Navigator.of(context, rootNavigator: true).pop(), + ), + ], + title: LunaDialog.title(text: 'Tags'), + content: Builder( + builder: (context) { + if ((snapshot.data?.length ?? 0) == 0) + return LunaDialog.content( + children: [ + LunaDialog.textContent(text: 'No Tags Found'), + ], + ); + return LunaDialog.content( + children: List.generate( + snapshot.data!.length, + (index) => LunaDialog.checkbox( + title: snapshot.data![index].label!, + value: context + .watch() + .tags + ?.where((t) => t.id == snapshot.data![index].id) + .isNotEmpty, + onChanged: (selected) { + List _tags = + context.read().tags!; + selected! + ? _tags.add(snapshot.data![index]) + : _tags.removeWhere((tag) => + tag.id == snapshot.data![index].id); + context.read().tags = _tags; + }, + ), + ), + ); + }, + ), + contentPadding: (snapshot.data?.length ?? 0) == 0 + ? LunaDialog.textDialogContentPadding() + : LunaDialog.listDialogContentPadding(), + shape: LunaUI.shapeBorder, + ); + }, + ), + ), + ), + ); + } + + Future> addNewTag(BuildContext context) async { + bool _flag = false; + final _formKey = GlobalKey(); + final _textController = TextEditingController(); + + void _setValues(bool flag) { + if (_formKey.currentState!.validate()) { + _flag = flag; + Navigator.of(context, rootNavigator: true).pop(); + } + } + + await LunaDialog.dialog( + context: context, + title: 'Add Tag', + buttons: [ + LunaDialog.button( + text: 'Add', + onPressed: () => _setValues(true), + ), + ], + content: [ + Form( + key: _formKey, + child: LunaDialog.textFormInput( + controller: _textController, + title: 'Tag Label', + onSubmitted: (_) => _setValues(true), + validator: (value) { + if (value?.isEmpty ?? true) return 'Label cannot be empty'; + return null; + }, + ), + ), + ], + contentPadding: LunaDialog.inputDialogContentPadding(), + ); + return Tuple2(_flag, _textController.text); + } + + Future searchAllMissingEpisodes( + BuildContext context, + ) async { + bool _flag = false; + + void _setValues(bool flag) { + _flag = flag; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LunaDialog.dialog( + context: context, + title: 'readarr.MissingEpisodes'.tr(), + buttons: [ + LunaDialog.button( + text: 'readarr.Search'.tr(), + onPressed: () => _setValues(true), + ), + ], + content: [ + LunaDialog.textContent( + text: 'readarr.MissingEpisodesHint1'.tr(), + ), + ], + contentPadding: LunaDialog.textDialogContentPadding(), + ); + return _flag; + } + + Future deleteTag(BuildContext context) async { + bool _flag = false; + + void _setValues(bool flag) { + _flag = flag; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LunaDialog.dialog( + context: context, + title: 'Delete Tag', + buttons: [ + LunaDialog.button( + text: 'Delete', + textColor: LunaColours.red, + onPressed: () => _setValues(true), + ), + ], + content: [ + LunaDialog.textContent( + text: 'Are you sure you want to delete this tag?'), + ], + contentPadding: LunaDialog.textDialogContentPadding(), + ); + return _flag; + } + + Future> editMetadataProfiles( + BuildContext context, List profiles) async { + bool _flag = false; + ReadarrMetadataProfile? profile; + + void _setValues(bool flag, ReadarrMetadataProfile? value) { + _flag = flag; + profile = value; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LunaDialog.dialog( + context: context, + title: 'Language Profile', + content: List.generate( + profiles.length, + (index) => LunaDialog.tile( + text: profiles[index]!.name!, + icon: Icons.portrait_rounded, + iconColor: LunaColours().byListIndex(index), + onTap: () => _setValues(true, profiles[index]), + ), + ), + contentPadding: LunaDialog.listDialogContentPadding(), + ); + return Tuple2(_flag, profile); + } + + Future> editQualityProfile( + BuildContext context, List profiles) async { + bool _flag = false; + ReadarrQualityProfile? profile; + + void _setValues(bool flag, ReadarrQualityProfile? value) { + _flag = flag; + profile = value; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LunaDialog.dialog( + context: context, + title: 'Quality Profile', + content: List.generate( + profiles.length, + (index) => LunaDialog.tile( + text: profiles[index]!.name!, + icon: Icons.portrait_rounded, + iconColor: LunaColours().byListIndex(index), + onTap: () => _setValues(true, profiles[index]), + ), + ), + contentPadding: LunaDialog.listDialogContentPadding(), + ); + return Tuple2(_flag, profile); + } + + Future> editRootFolder( + BuildContext context, List folders) async { + bool _flag = false; + ReadarrRootFolder? _folder; + + void _setValues(bool flag, ReadarrRootFolder value) { + _flag = flag; + _folder = value; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LunaDialog.dialog( + context: context, + title: 'Root Folder', + content: List.generate( + folders.length, + (index) => LunaDialog.tile( + text: folders[index].path!, + subtitle: LunaDialog.richText( + children: [ + LunaDialog.bolded( + text: folders[index].freeSpace.lunaBytesToString(), + fontSize: LunaDialog.BUTTON_SIZE, + ), + ], + ) as RichText?, + icon: Icons.folder_rounded, + iconColor: LunaColours().byListIndex(index), + onTap: () => _setValues(true, folders[index]), + ), + ), + contentPadding: LunaDialog.listDialogContentPadding(), + ); + return Tuple2(_flag, _folder); + } + + Future> editMonitorType( + BuildContext context) async { + bool _flag = false; + ReadarrAuthorMonitorType? _type; + + void _setValues(bool flag, ReadarrAuthorMonitorType type) { + _flag = flag; + _type = type; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LunaDialog.dialog( + context: context, + title: 'Monitoring Options', + content: List.generate( + ReadarrAuthorMonitorType.values.length, + (index) => LunaDialog.tile( + text: ReadarrAuthorMonitorType.values[index].lunaName, + icon: Icons.view_list_rounded, + iconColor: LunaColours().byListIndex(index), + onTap: () => _setValues(true, ReadarrAuthorMonitorType.values[index]), + ), + ), + contentPadding: LunaDialog.listDialogContentPadding(), + ); + return Tuple2(_flag, _type); + } + + Future removeAuthor(BuildContext context) async { + bool _flag = false; + + void _setValues(bool flag) { + _flag = flag; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LunaDialog.dialog( + context: context, + title: 'readarr.RemoveAuthor'.tr(), + buttons: [ + LunaDialog.button( + text: 'lunasea.Remove'.tr(), + textColor: LunaColours.red, + onPressed: () => _setValues(true), + ), + ], + content: [ + ReadarrDatabaseValue.REMOVE_SERIES_EXCLUSION_LIST.listen( + builder: (context, value, _) => LunaDialog.checkbox( + title: 'readarr.AddToExclusionList'.tr(), + value: ReadarrDatabaseValue.REMOVE_SERIES_EXCLUSION_LIST.data, + onChanged: (value) => + ReadarrDatabaseValue.REMOVE_SERIES_EXCLUSION_LIST.put(value), + ), + ), + ReadarrDatabaseValue.REMOVE_SERIES_DELETE_FILES.listen( + builder: (context, value, _) => LunaDialog.checkbox( + title: 'readarr.DeleteFiles'.tr(), + value: ReadarrDatabaseValue.REMOVE_SERIES_DELETE_FILES.data, + onChanged: (value) => + ReadarrDatabaseValue.REMOVE_SERIES_DELETE_FILES.put(value), + ), + ), + ], + contentPadding: LunaDialog.listDialogContentPadding(), + ); + return _flag; + } + + Future removeBook(BuildContext context) async { + bool _flag = false; + + void _setValues(bool flag) { + _flag = flag; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LunaDialog.dialog( + context: context, + title: 'readarr.RemoveBook'.tr(), + buttons: [ + LunaDialog.button( + text: 'lunasea.Remove'.tr(), + textColor: LunaColours.red, + onPressed: () => _setValues(true), + ), + ], + content: [ + ReadarrDatabaseValue.REMOVE_BOOK_EXCLUSION_LIST.listen( + builder: (context, value, _) => LunaDialog.checkbox( + title: 'readarr.AddToExclusionList'.tr(), + value: ReadarrDatabaseValue.REMOVE_BOOK_EXCLUSION_LIST.data, + onChanged: (value) => + ReadarrDatabaseValue.REMOVE_BOOK_EXCLUSION_LIST.put(value), + ), + ), + ReadarrDatabaseValue.REMOVE_BOOK_DELETE_FILES.listen( + builder: (context, value, _) => LunaDialog.checkbox( + title: 'readarr.DeleteFiles'.tr(), + value: ReadarrDatabaseValue.REMOVE_BOOK_DELETE_FILES.data, + onChanged: (value) => + ReadarrDatabaseValue.REMOVE_BOOK_DELETE_FILES.put(value), + ), + ), + ], + contentPadding: LunaDialog.listDialogContentPadding(), + ); + return _flag; + } + + Future addSeriesOptions(BuildContext context) async { + await LunaDialog.dialog( + context: context, + title: 'lunasea.Options'.tr(), + buttons: [ + LunaDialog.button( + text: 'lunasea.Close'.tr(), + onPressed: () => Navigator.of(context, rootNavigator: true).pop(), + ), + ], + showCancelButton: false, + content: [ + ReadarrDatabaseValue.ADD_SERIES_SEARCH_FOR_MISSING.listen( + builder: (context, value, _) => LunaDialog.checkbox( + title: 'readarr.StartSearchForMissingEpisodes'.tr(), + value: ReadarrDatabaseValue.ADD_SERIES_SEARCH_FOR_MISSING.data, + onChanged: (value) => + ReadarrDatabaseValue.ADD_SERIES_SEARCH_FOR_MISSING.put(value), + ), + ), + ReadarrDatabaseValue.ADD_SERIES_SEARCH_FOR_CUTOFF_UNMET.listen( + builder: (context, value, _) => LunaDialog.checkbox( + title: 'readarr.StartSearchForCutoffUnmetEpisodes'.tr(), + value: ReadarrDatabaseValue.ADD_SERIES_SEARCH_FOR_CUTOFF_UNMET.data, + onChanged: (value) => ReadarrDatabaseValue + .ADD_SERIES_SEARCH_FOR_CUTOFF_UNMET + .put(value), + ), + ), + ], + contentPadding: LunaDialog.listDialogContentPadding(), + ); + } + + Future deleteBookFile(BuildContext context) async { + bool _flag = false; + + void _setValues(bool flag) { + _flag = flag; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LunaDialog.dialog( + context: context, + title: 'readarr.DeleteBookFile'.tr(), + buttons: [ + LunaDialog.button( + text: 'lunasea.Delete'.tr(), + textColor: LunaColours.red, + onPressed: () => _setValues(true), + ), + ], + content: [ + LunaDialog.textContent(text: 'readarr.DeleteEpisodeFileHint1'.tr()), + ], + contentPadding: LunaDialog.textDialogContentPadding(), + ); + return _flag; + } + + Future confirmSeasonSearch( + BuildContext context, + int seasonNumber, + ) async { + bool _flag = false; + + void _setValues(bool flag) { + _flag = flag; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LunaDialog.dialog( + context: context, + title: 'Season Search', + buttons: [ + LunaDialog.button( + text: 'Search', + onPressed: () => _setValues(true), + ), + ], + content: [ + LunaDialog.textContent( + text: seasonNumber == 0 + ? 'Search for all episodes in specials?' + : 'Search for all episodes in season $seasonNumber?', + ), + ], + contentPadding: LunaDialog.textDialogContentPadding(), + ); + return _flag; + } + + Future removeFromQueue(BuildContext context) async { + bool _flag = false; + + void _setValues(bool flag) { + _flag = flag; + Navigator.of(context, rootNavigator: true).pop(); + } + + await LunaDialog.dialog( + context: context, + title: 'readarr.RemoveFromQueue'.tr(), + buttons: [ + LunaDialog.button( + text: 'lunasea.Remove'.tr(), + textColor: LunaColours.red, + onPressed: () => _setValues(true), + ), + ], + content: [ + ReadarrDatabaseValue.QUEUE_REMOVE_DOWNLOAD_CLIENT.listen( + builder: (context, value, _) => LunaDialog.checkbox( + title: 'readarr.RemoveFromDownloadClient'.tr(), + value: ReadarrDatabaseValue.QUEUE_REMOVE_DOWNLOAD_CLIENT.data, + onChanged: (value) => + ReadarrDatabaseValue.QUEUE_REMOVE_DOWNLOAD_CLIENT.put(value), + ), + ), + ReadarrDatabaseValue.QUEUE_ADD_BLOCKLIST.listen( + builder: (context, value, _) => LunaDialog.checkbox( + title: 'readarr.AddReleaseToBlocklist'.tr(), + value: ReadarrDatabaseValue.QUEUE_ADD_BLOCKLIST.data, + onChanged: (value) => + ReadarrDatabaseValue.QUEUE_ADD_BLOCKLIST.put(value), + ), + ), + ], + contentPadding: LunaDialog.listDialogContentPadding(), + ); + return _flag; + } + + Future showQueueStatusMessages( + BuildContext context, + List messages, + ) async { + if (messages.isEmpty) { + return LunaDialogs().textPreview( + context, + 'readarr.Messages'.tr(), + 'readarr.NoMessagesFound'.tr(), + ); + } + await LunaDialog.dialog( + context: context, + title: 'readarr.Messages'.tr(), + cancelButtonText: 'lunasea.Close'.tr(), + contentPadding: LunaDialog.listDialogContentPadding(), + content: List.generate( + messages.length, + (index) => Padding( + padding: LunaDialog.tileContentPadding(), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.only(right: 32.0), + child: Icon( + Icons.warning_amber_rounded, + color: LunaColours.orange, + size: 24.0, + ), + ), + Expanded( + child: Text( + messages[index].title!, + style: const TextStyle( + fontSize: LunaDialog.BODY_SIZE, + color: LunaColours.orange, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: LunaUI.DEFAULT_MARGIN_SIZE / 4, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (messages[index].messages!.isNotEmpty) + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32.0 + LunaUI.ICON_SIZE, + ), + child: LunaDialog.richText( + children: [ + TextSpan( + text: messages[index] + .messages! + .map((s) => '${LunaUI.TEXT_BULLET} $s') + .join('\n'), + ), + ], + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Future> setQueuePageSize(BuildContext context) async { + bool _flag = false; + GlobalKey _formKey = GlobalKey(); + TextEditingController _textController = TextEditingController( + text: ReadarrDatabaseValue.QUEUE_PAGE_SIZE.data.toString(), + ); + + void _setValues(bool flag) { + if (_formKey.currentState!.validate()) { + _flag = flag; + Navigator.of(context, rootNavigator: true).pop(); + } + } + + await LunaDialog.dialog( + context: context, + title: 'Queue Size', + buttons: [ + LunaDialog.button( + text: 'Set', + onPressed: () => _setValues(true), + ), + ], + content: [ + LunaDialog.textContent( + text: 'Set the amount of items fetched for the queue.'), + Form( + key: _formKey, + child: LunaDialog.textFormInput( + controller: _textController, + title: 'Queue Page Size', + onSubmitted: (_) => _setValues(true), + validator: (value) { + int? _value = int.tryParse(value!); + if (_value != null && _value >= 1) return null; + return 'Minimum of 1 Item'; + }, + keyboardType: TextInputType.number, + ), + ), + ], + contentPadding: LunaDialog.inputTextDialogContentPadding(), + ); + + return Tuple2(_flag, int.tryParse(_textController.text) ?? 50); + } +} diff --git a/lib/modules/readarr/core/extensions.dart b/lib/modules/readarr/core/extensions.dart new file mode 100644 index 0000000000..6a20d55b5f --- /dev/null +++ b/lib/modules/readarr/core/extensions.dart @@ -0,0 +1,11 @@ +export 'extensions/readarr_book_file.dart'; +export 'extensions/readarr_book.dart'; +export 'extensions/readarr_event_type.dart'; +export 'extensions/readarr_history.dart'; +export 'extensions/readarr_protocol.dart'; +export 'extensions/readarr_queue_record.dart'; +export 'extensions/readarr_queue_status.dart'; +export 'extensions/readarr_author_monitor_type.dart'; +export 'extensions/readarr_release.dart'; +export 'extensions/readarr_author.dart'; +export 'extensions/readarr_author_season.dart'; diff --git a/lib/modules/readarr/core/extensions/readarr_author.dart b/lib/modules/readarr/core/extensions/readarr_author.dart new file mode 100644 index 0000000000..4bc97bf125 --- /dev/null +++ b/lib/modules/readarr/core/extensions/readarr_author.dart @@ -0,0 +1,70 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +extension ReadarrAuthorExtension on ReadarrAuthor { + String get lunaGenres { + if (this.genres?.isNotEmpty ?? false) return this.genres!.join('\n'); + return LunaUI.TEXT_EMDASH; + } + + String lunaTags(List tags) { + if (tags.isNotEmpty) return tags.map((t) => t.label!).join('\n'); + return LunaUI.TEXT_EMDASH; + } + + int get lunaPercentageComplete { + int _total = this.statistics?.episodeCount ?? 0; + int _available = this.statistics?.episodeFileCount ?? 0; + return _total == 0 ? 0 : ((_available / _total) * 100).round(); + } + + String get lunaDateAdded { + if (this.added == null) { + return 'lunasea.Unknown'.tr(); + } + return DateFormat('MMMM dd, y').format(this.added!.toLocal()); + } + + String get lunaDateAddedShort { + if (this.added == null) { + return 'lunasea.Unknown'.tr(); + } + return DateFormat('MMM dd, y').format(this.added!.toLocal()); + } + + String get lunaSizeOnDisk { + if (this.statistics?.sizeOnDisk == null) { + return '0.0 B'; + } + return this.statistics!.sizeOnDisk.lunaBytesToString(decimals: 1); + } + + String? get lunaOverview { + if (this.overview == null || this.overview!.isEmpty) { + return 'readarr.NoSummaryAvailable'.tr(); + } + return this.overview; + } + + String get lunaEpisodeCount { + int episodeFileCount = this.statistics?.episodeFileCount ?? 0; + int episodeCount = this.statistics?.episodeCount ?? 0; + int percentage = this.lunaPercentageComplete; + return '$episodeFileCount/$episodeCount ($percentage%)'; + } + + /// Creates a clone of the [ReadarrAuthor] object (deep copy). + ReadarrAuthor clone() => ReadarrAuthor.fromJson(this.toJson()); + + /// Copies changes from a [ReadarrAuthorEditState] state object back to the [ReadarrAuthor] object. + ReadarrAuthor updateEdits(ReadarrAuthorEditState edits) { + ReadarrAuthor series = this.clone(); + series.monitored = edits.monitored; + series.path = edits.seriesPath; + series.qualityProfileId = edits.qualityProfile?.id ?? this.qualityProfileId; + series.metadataProfileId = + edits.metadataProfile.id ?? this.metadataProfileId; + series.tags = edits.tags?.map((t) => t.id!).toList() ?? []; + return series; + } +} diff --git a/lib/modules/readarr/core/extensions/readarr_author_monitor_type.dart b/lib/modules/readarr/core/extensions/readarr_author_monitor_type.dart new file mode 100644 index 0000000000..3c3e1f62e1 --- /dev/null +++ b/lib/modules/readarr/core/extensions/readarr_author_monitor_type.dart @@ -0,0 +1,27 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +extension LunaReadarrAuthorMonitorTypeExtension on ReadarrAuthorMonitorType { + String get lunaName { + switch (this) { + case ReadarrAuthorMonitorType.ALL: + return 'All Episodes'; + case ReadarrAuthorMonitorType.FUTURE: + return 'Future Episodes'; + case ReadarrAuthorMonitorType.MISSING: + return 'Missing Episodes'; + case ReadarrAuthorMonitorType.EXISTING: + return 'Existing Episodes'; + case ReadarrAuthorMonitorType.PILOT: + return 'Pilot Episode'; + case ReadarrAuthorMonitorType.FIRST_SEASON: + return 'Only First Season'; + case ReadarrAuthorMonitorType.LATEST_SEASON: + return 'Only Latest Season'; + case ReadarrAuthorMonitorType.NONE: + return 'None'; + default: + return 'lunasea.Unknown'.tr(); + } + } +} diff --git a/lib/modules/readarr/core/extensions/readarr_author_season.dart b/lib/modules/readarr/core/extensions/readarr_author_season.dart new file mode 100644 index 0000000000..febad71c4f --- /dev/null +++ b/lib/modules/readarr/core/extensions/readarr_author_season.dart @@ -0,0 +1,21 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +extension ReadarrAuthorSeasonExtension on ReadarrAuthorSeason { + String get lunaTitle { + if (this.seasonNumber == 0) return 'readarr.Specials'.tr(); + return 'readarr.SeasonNumber'.tr(args: [ + this.seasonNumber?.toString() ?? 'lunasea.Unknown'.tr(), + ]); + } + + int get lunaPercentageComplete { + int _total = this.statistics?.episodeCount ?? 0; + int _available = this.statistics?.episodeFileCount ?? 0; + return _total == 0 ? 0 : ((_available / _total) * 100).round(); + } + + String get lunaEpisodesAvailable { + return '${this.statistics?.episodeFileCount ?? 0}/${this.statistics?.episodeCount ?? 0}'; + } +} diff --git a/lib/modules/readarr/core/extensions/readarr_book.dart b/lib/modules/readarr/core/extensions/readarr_book.dart new file mode 100644 index 0000000000..83c245ca6d --- /dev/null +++ b/lib/modules/readarr/core/extensions/readarr_book.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +extension ReadarrBookExtension on ReadarrBook { + /// Creates a clone of the [ReadarrBook] object (deep copy). + ReadarrBook clone() => ReadarrBook.fromJson(this.toJson()); + + String get lunaGenres { + if (this.genres?.isNotEmpty ?? false) return this.genres!.join('\n'); + return LunaUI.TEXT_EMDASH; + } + + String lunaReleaseDate() { + if (this.releaseDate == null) return 'lunasea.UnknownDate'.tr(); + return DateFormat.yMMMMd().format(this.releaseDate!.toLocal()); + } + + String lunaDownloadedQuality( + ReadarrBookFile? file, + ReadarrQueueRecord? queueRecord, + ) { + if (queueRecord != null) { + return [ + queueRecord.lunaPercentage(), + LunaUI.TEXT_EMDASH, + queueRecord.lunaStatusParameters().item1, + ].join(' '); + } + +/* + if (!this.hasFile!) { + if (_hasAired()) return 'readarr.Unaired'.tr(); + return 'readarr.Missing'.tr(); + } + if (file == null) return 'lunasea.Unknown'.tr(); + String quality = file.quality?.quality?.name ?? 'lunasea.Unknown'.tr(); + String size = file.size?.lunaBytesToString() ?? '0.00 B'; + return '$quality ${LunaUI.TEXT_EMDASH} $size'; + */ + + return "unknown"; + } + + Color lunaDownloadedQualityColor( + ReadarrBookFile? file, + ReadarrQueueRecord? queueRecord, + ) { + if (queueRecord != null) { + return queueRecord.lunaStatusParameters(canBeWhite: false).item3; + } + + return LunaColours.blueGrey; +/* + if (!this.hasFile!) { + if (_hasAired()) return LunaColours.blue; + return LunaColours.red; + } + if (file == null) return LunaColours.blueGrey; + if (file.qualityCutoffNotMet!) return LunaColours.orange; + return LunaColours.accent;*/ + } +} diff --git a/lib/modules/readarr/core/extensions/readarr_book_file.dart b/lib/modules/readarr/core/extensions/readarr_book_file.dart new file mode 100644 index 0000000000..979584728e --- /dev/null +++ b/lib/modules/readarr/core/extensions/readarr_book_file.dart @@ -0,0 +1,26 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +extension LunaReadarrBookFileExtension on ReadarrBookFile { + String get lunaPath { + if (this.path?.isNotEmpty ?? false) return this.path!; + return LunaUI.TEXT_EMDASH; + } + + String get lunaSize { + if ((this.size ?? 0) != 0) return this.size.lunaBytesToString(decimals: 1); + return LunaUI.TEXT_EMDASH; + } + + String get lunaQuality { + if (this.quality?.quality?.name != null) + return this.quality!.quality!.name!; + return LunaUI.TEXT_EMDASH; + } + + String get lunaDateAdded { + if (this.dateAdded != null) + return this.dateAdded!.lunaDateTimeReadable(timeOnNewLine: true); + return LunaUI.TEXT_EMDASH; + } +} diff --git a/lib/modules/readarr/core/extensions/readarr_event_type.dart b/lib/modules/readarr/core/extensions/readarr_event_type.dart new file mode 100644 index 0000000000..b84460db46 --- /dev/null +++ b/lib/modules/readarr/core/extensions/readarr_event_type.dart @@ -0,0 +1,321 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +extension ReadarrEventTypeLunaExtension on ReadarrEventType { + Color lunaColour() { + switch (this) { + case ReadarrEventType.GRABBED: + return LunaColours.orange; + case ReadarrEventType.BOOK_FILE_IMPORTED: + return LunaColours.accent; + case ReadarrEventType.DOWNLOAD_FAILED: + return LunaColours.red; + case ReadarrEventType.BOOK_FILE_DELETED: + return LunaColours.red; + case ReadarrEventType.BOOK_FILE_RENAMED: + return LunaColours.blue; + case ReadarrEventType.BOOK_IMPORT_INCOMPLETE: + return LunaColours.orange; + case ReadarrEventType.DOWNLOAD_IMPORTED: + return LunaColours.accent; + case ReadarrEventType.BOOK_FILE_RETAGGED: + return LunaColours.blue; + case ReadarrEventType.DOWNLOAD_IGNORED: + return LunaColours.purple; + } + } + + IconData lunaIcon() { + switch (this) { + case ReadarrEventType.GRABBED: + return Icons.cloud_download_rounded; + case ReadarrEventType.BOOK_FILE_IMPORTED: + return Icons.download_rounded; + case ReadarrEventType.DOWNLOAD_FAILED: + return Icons.cloud_download_rounded; + case ReadarrEventType.BOOK_FILE_DELETED: + return Icons.delete_rounded; + case ReadarrEventType.BOOK_FILE_RENAMED: + return Icons.drive_file_rename_outline_rounded; + case ReadarrEventType.BOOK_IMPORT_INCOMPLETE: + return Icons.cloud_download_rounded; + case ReadarrEventType.DOWNLOAD_IMPORTED: + return Icons.download_rounded; + case ReadarrEventType.BOOK_FILE_RETAGGED: + return Icons.create_rounded; + case ReadarrEventType.DOWNLOAD_IGNORED: + return Icons.cancel_rounded; + } + } + + Color lunaIconColour() { + switch (this) { + case ReadarrEventType.GRABBED: + return Colors.white; + case ReadarrEventType.BOOK_FILE_IMPORTED: + return Colors.white; + case ReadarrEventType.DOWNLOAD_FAILED: + return LunaColours.red; + case ReadarrEventType.BOOK_FILE_DELETED: + return Colors.white; + case ReadarrEventType.BOOK_FILE_RENAMED: + return Colors.white; + case ReadarrEventType.BOOK_IMPORT_INCOMPLETE: + return Colors.orange; + case ReadarrEventType.DOWNLOAD_IMPORTED: + return Colors.white; + case ReadarrEventType.BOOK_FILE_RETAGGED: + return Colors.white; + case ReadarrEventType.DOWNLOAD_IGNORED: + return Colors.white; + } + } + + String? lunaReadable(ReadarrHistoryRecord record) { + switch (this) { + case ReadarrEventType.GRABBED: + return 'readarr.GrabbedFrom'.tr( + args: [record.data!['indexer'] ?? 'lunasea.Unknown'.tr()], + ); + case ReadarrEventType.BOOK_FILE_IMPORTED: + return 'readarr.BookFileImported'.tr( + args: [record.quality?.quality?.name ?? 'lunasea.Unknown'.tr()], + ); + case ReadarrEventType.DOWNLOAD_FAILED: + return 'readarr.DownloadFailed'.tr(); + case ReadarrEventType.BOOK_FILE_DELETED: + return 'readarr.BookFileDeleted'.tr(); + case ReadarrEventType.BOOK_FILE_RENAMED: + return 'readarr.BookFileRenamed'.tr(); + case ReadarrEventType.BOOK_IMPORT_INCOMPLETE: + return 'readarr.BookImportIncomplete'.tr(); + case ReadarrEventType.DOWNLOAD_IMPORTED: + return 'readarr.DownloadImported'.tr( + args: [record.quality?.quality?.name ?? 'lunasea.Unknown'.tr()], + ); + case ReadarrEventType.BOOK_FILE_RETAGGED: + return 'readarr.BookFileRetagged'.tr(); + case ReadarrEventType.DOWNLOAD_IGNORED: + return 'readarr.DownloadIgnored'.tr(); + } + } + + List lunaTableContent({ + required ReadarrHistoryRecord history, + required bool showSourceTitle, + }) { + switch (this) { + case ReadarrEventType.GRABBED: + return _grabbedTableContent(history, showSourceTitle); + case ReadarrEventType.BOOK_FILE_IMPORTED: + return _downloadFolderImportedTableContent(history, showSourceTitle); + case ReadarrEventType.DOWNLOAD_FAILED: + return _downloadFailedTableContent(history, showSourceTitle); + case ReadarrEventType.BOOK_FILE_DELETED: + return _episodeFileDeletedTableContent(history, showSourceTitle); + case ReadarrEventType.BOOK_FILE_RENAMED: + return _episodeFileRenamedTableContent(history); + case ReadarrEventType.BOOK_IMPORT_INCOMPLETE: + return _bookImportIncompleteTableContent(history, showSourceTitle); + case ReadarrEventType.DOWNLOAD_IMPORTED: + return _downloadFolderImportedTableContent(history, showSourceTitle); + case ReadarrEventType.DOWNLOAD_IGNORED: + return _downloadIgnoredTableContent(history, showSourceTitle); + default: + return _defaultTableContent(history, showSourceTitle); + } + } + + List _downloadFailedTableContent( + ReadarrHistoryRecord history, + bool showSourceTitle, + ) { + return [ + if (showSourceTitle) + LunaTableContent( + title: 'readarr.SourceTitle'.tr(), + body: history.sourceTitle, + ), + LunaTableContent( + title: 'readarr.Message'.tr(), + body: history.data!['message'], + ), + ]; + } + + List _downloadFolderImportedTableContent( + ReadarrHistoryRecord history, + bool showSourceTitle, + ) { + return [ + if (showSourceTitle) + LunaTableContent( + title: 'readarr.SourceTitle'.tr(), + body: history.sourceTitle, + ), + LunaTableContent( + title: 'readarr.Quality'.tr(), + body: history.quality?.quality?.name ?? LunaUI.TEXT_EMDASH, + ), + LunaTableContent( + title: 'readarr.Client'.tr(), + body: history.data!['downloadClient'] ?? LunaUI.TEXT_EMDASH, + ), + LunaTableContent( + title: 'readarr.Source'.tr(), + body: history.data!['droppedPath'], + ), + LunaTableContent( + title: 'readarr.ImportedTo'.tr(), + body: history.data!['importedPath'], + ), + ]; + } + + List _downloadIgnoredTableContent( + ReadarrHistoryRecord history, + bool showSourceTitle, + ) { + return [ + if (showSourceTitle) + LunaTableContent( + title: 'readarr.Name'.tr(), + body: history.sourceTitle, + ), + LunaTableContent( + title: 'readarr.Message'.tr(), + body: history.data!['message'], + ), + ]; + } + + List _episodeFileDeletedTableContent( + ReadarrHistoryRecord history, + bool showSourceTitle, + ) { + String _reasonMapping(String? reason) { + switch (reason) { + case 'Upgrade': + return 'readarr.DeleteReasonUpgrade'.tr(); + case 'MissingFromDisk': + return 'readarr.DeleteReasonMissingFromDisk'.tr(); + case 'Manual': + return 'readarr.DeleteReasonManual'.tr(); + default: + return 'lunasea.Unknown'.tr(); + } + } + + return [ + if (showSourceTitle) + LunaTableContent( + title: 'readarr.SourceTitle'.tr(), + body: history.sourceTitle, + ), + LunaTableContent( + title: 'readarr.Reason'.tr(), + body: _reasonMapping(history.data!['reason']), + ), + ]; + } + + List _episodeFileRenamedTableContent( + ReadarrHistoryRecord history, + ) { + return [ + LunaTableContent( + title: 'readarr.Source'.tr(), + body: history.data!['sourcePath'], + ), + LunaTableContent( + title: 'readarr.SourceRelative'.tr(), + body: history.data!['sourceRelativePath'], + ), + LunaTableContent( + title: 'readarr.Destination'.tr(), + body: history.data!['path'], + ), + LunaTableContent( + title: 'readarr.DestinationRelative'.tr(), + body: history.data!['relativePath'], + ), + ]; + } + + List _bookImportIncompleteTableContent( + ReadarrHistoryRecord history, + bool showSourceTitle, + ) { + return [ + if (showSourceTitle) + LunaTableContent( + title: 'readarr.SourceTitle'.tr(), + body: history.sourceTitle, + ), + LunaTableContent( + title: 'readarr.Messages'.tr(), + body: history.data!['statusMessages'], + ), + ]; + } + + List _grabbedTableContent( + ReadarrHistoryRecord history, + bool showSourceTitle, + ) { + return [ + if (showSourceTitle) + LunaTableContent( + title: 'readarr.SourceTitle'.tr(), + body: history.sourceTitle, + ), + LunaTableContent( + title: 'readarr.Quality'.tr(), + body: history.quality?.quality?.name ?? LunaUI.TEXT_EMDASH, + ), + LunaTableContent( + title: 'readarr.Indexer'.tr(), + body: history.data!['indexer'], + ), + LunaTableContent( + title: 'readarr.ReleaseGroup'.tr(), + body: history.data!['releaseGroup'], + ), + LunaTableContent( + title: 'readarr.InfoURL'.tr(), + body: history.data!['nzbInfoUrl'], + bodyIsUrl: history.data!['nzbInfoUrl'] != null, + ), + LunaTableContent( + title: 'readarr.Client'.tr(), + body: history.data!['downloadClientName'], + ), + LunaTableContent( + title: 'readarr.DownloadID'.tr(), + body: history.data!['downloadId'], + ), + LunaTableContent( + title: 'readarr.Age'.tr(), + body: double.tryParse(history.data!['ageHours'])?.asTimeAgo, + ), + LunaTableContent( + title: 'readarr.PublishedDate'.tr(), + body: DateTime.tryParse(history.data!['publishedDate']) + ?.lunaDateTimeReadable(timeOnNewLine: true)), + ]; + } + + List _defaultTableContent( + ReadarrHistoryRecord history, + bool showSourceTitle, + ) { + return [ + if (showSourceTitle) + LunaTableContent( + title: 'readarr.Name'.tr(), + body: history.sourceTitle, + ), + ]; + } +} diff --git a/lib/modules/readarr/core/extensions/readarr_history.dart b/lib/modules/readarr/core/extensions/readarr_history.dart new file mode 100644 index 0000000000..b6c72126be --- /dev/null +++ b/lib/modules/readarr/core/extensions/readarr_history.dart @@ -0,0 +1,23 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +extension ReadarrHistoryRecordLunaExtension on ReadarrHistoryRecord { + String lunaSeriesTitle() { + return this.series?.title ?? LunaUI.TEXT_EMDASH; + } + + bool lunaHasPreferredWordScore() { + return (this.data!['preferredWordScore'] ?? '0') != '0'; + } + + String lunaPreferredWordScore() { + if (lunaHasPreferredWordScore()) { + int? _preferredScore = int.tryParse(this.data!['preferredWordScore']); + if (_preferredScore != null) { + String _prefix = _preferredScore > 0 ? '+' : ''; + return '$_prefix${this.data!['preferredWordScore']}'; + } + } + return LunaUI.TEXT_EMDASH; + } +} diff --git a/lib/modules/readarr/core/extensions/readarr_protocol.dart b/lib/modules/readarr/core/extensions/readarr_protocol.dart new file mode 100644 index 0000000000..503277c90a --- /dev/null +++ b/lib/modules/readarr/core/extensions/readarr_protocol.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +extension LunaReadarrProtocolExtension on ReadarrProtocol { + Color lunaProtocolColor({ + ReadarrRelease? release, + }) { + if (this == ReadarrProtocol.USENET) return LunaColours.accent; + if (release == null) return LunaColours.blue; + + int seeders = release.seeders ?? 0; + if (seeders > 10) return LunaColours.blue; + if (seeders > 0) return LunaColours.orange; + return LunaColours.red; + } + + String lunaReadable() { + switch (this) { + case ReadarrProtocol.USENET: + return 'readarr.Usenet'.tr(); + case ReadarrProtocol.TORRENT: + return 'readarr.Torrent'.tr(); + } + } +} diff --git a/lib/modules/readarr/core/extensions/readarr_queue_record.dart b/lib/modules/readarr/core/extensions/readarr_queue_record.dart new file mode 100644 index 0000000000..a66c49d645 --- /dev/null +++ b/lib/modules/readarr/core/extensions/readarr_queue_record.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +extension ReadarrQueueRecordExtension on ReadarrQueueRecord { + Tuple3 lunaStatusParameters({ + bool canBeWhite = true, + }) { + ReadarrQueueStatus? _status = this.status; + ReadarrTrackedDownloadStatus? _tStatus = this.trackedDownloadStatus; + ReadarrTrackedDownloadState? _tState = this.trackedDownloadState; + + String _title = 'readarr.Downloading'.tr(); + IconData _icon = Icons.cloud_download_rounded; + Color _color = canBeWhite ? Colors.white : LunaColours.blueGrey; + + // Paused + if (_status == ReadarrQueueStatus.PAUSED) { + _icon = Icons.pause_rounded; + _title = 'readarr.Paused'.tr(); + } + + // Queued + if (_status == ReadarrQueueStatus.QUEUED) { + _icon = Icons.cloud_rounded; + _title = 'readarr.Queued'.tr(); + } + + // Complete + if (_status == ReadarrQueueStatus.COMPLETED) { + _title = 'readarr.Downloaded'.tr(); + _icon = Icons.file_download_rounded; + + if (_tState == ReadarrTrackedDownloadState.IMPORT_PENDING) { + _title = 'readarr.DownloadedWaitingToImport'.tr(); + _color = LunaColours.purple; + } + if (_tState == ReadarrTrackedDownloadState.IMPORTING) { + _title = 'readarr.DownloadedImporting'.tr(); + _color = LunaColours.purple; + } + if (_tState == ReadarrTrackedDownloadState.FAILED_PENDING) { + _title = 'readarr.DownloadedWaitingToProcess'.tr(); + _color = LunaColours.red; + } + } + + if (_tStatus == ReadarrTrackedDownloadStatus.WARNING) { + _color = LunaColours.orange; + } + + // Delay + if (_status == ReadarrQueueStatus.DELAY) { + _title = 'readarr.Pending'.tr(); + _icon = Icons.schedule_rounded; + } + + // Download Client Unavailable + if (_status == ReadarrQueueStatus.DOWNLOAD_CLIENT_UNAVAILABLE) { + _title = 'readarr.PendingWithMessage'.tr( + args: ['readarr.DownloadClientUnavailable'.tr()], + ); + _icon = Icons.schedule_rounded; + _color = LunaColours.orange; + } + + // Failed + if (_status == ReadarrQueueStatus.FAILED) { + _title = 'readarr.DownloadFailed'.tr(); + _icon = Icons.cloud_download_rounded; + _color = LunaColours.red; + } + + // Warning + if (_status == ReadarrQueueStatus.WARNING) { + _title = 'readarr.DownloadWarningWithMessage'.tr(args: [ + 'readarr.CheckDownloadClient'.tr(), + ]); + _icon = Icons.cloud_download_rounded; + _color = LunaColours.orange; + } + + // Error + if (_tStatus == ReadarrTrackedDownloadStatus.ERROR) { + if (_status == ReadarrQueueStatus.COMPLETED) { + _title = 'readarr.ImportFailed'.tr(); + _icon = Icons.file_download_rounded; + _color = LunaColours.red; + } else { + _title = 'readarr.DownloadFailed'.tr(); + _icon = Icons.cloud_download_rounded; + _color = LunaColours.red; + } + } + + return Tuple3(_title, _icon, _color); + } + + String lunaPercentage() { + if (this.sizeleft == null || this.size == null || this.size == 0) + return '0%'; + double sizeFetched = this.size! - this.sizeleft!; + int percentage = ((sizeFetched / this.size!) * 100).round(); + return '$percentage%'; + } + + String lunaTimeLeft() { + return this.timeleft ?? LunaUI.TEXT_EMDASH; + } +} diff --git a/lib/modules/readarr/core/extensions/readarr_queue_status.dart b/lib/modules/readarr/core/extensions/readarr_queue_status.dart new file mode 100644 index 0000000000..64f926316d --- /dev/null +++ b/lib/modules/readarr/core/extensions/readarr_queue_status.dart @@ -0,0 +1,25 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +extension LunaReadarrQueueStatusExtension on ReadarrQueueStatus { + String lunaStatus() { + switch (this) { + case ReadarrQueueStatus.DOWNLOADING: + return 'readarr.Downloading'.tr(); + case ReadarrQueueStatus.PAUSED: + return 'readarr.Paused'.tr(); + case ReadarrQueueStatus.QUEUED: + return 'readarr.Queued'.tr(); + case ReadarrQueueStatus.COMPLETED: + return 'readarr.Downloaded'.tr(); + case ReadarrQueueStatus.DELAY: + return 'readarr.Pending'.tr(); + case ReadarrQueueStatus.DOWNLOAD_CLIENT_UNAVAILABLE: + return 'readarr.DownloadClientUnavailable'.tr(); + case ReadarrQueueStatus.FAILED: + return 'readarr.DownloadFailed'.tr(); + case ReadarrQueueStatus.WARNING: + return 'readarr.DownloadWarning'.tr(); + } + } +} diff --git a/lib/modules/readarr/core/extensions/readarr_release.dart b/lib/modules/readarr/core/extensions/readarr_release.dart new file mode 100644 index 0000000000..9fccce4563 --- /dev/null +++ b/lib/modules/readarr/core/extensions/readarr_release.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +extension ReadarrReleaseExtension on ReadarrRelease { + IconData get lunaTrailingIcon { + if (this.approved!) return Icons.download_rounded; + return Icons.report_outlined; + } + + Color get lunaTrailingColor { + if (this.approved!) return Colors.white; + return LunaColours.red; + } + + String get lunaProtocol { + if (this.protocol != null) { + return this.protocol == ReadarrProtocol.TORRENT + ? '${this.protocol!.lunaReadable()} (${this.seeders ?? 0}/${this.leechers ?? 0})' + : this.protocol!.lunaReadable(); + } + return LunaUI.TEXT_EMDASH; + } + + String? get lunaIndexer { + if (this.indexer != null && this.indexer!.isNotEmpty) return this.indexer; + return LunaUI.TEXT_EMDASH; + } + + String get lunaAge { + if (this.ageHours != null) return this.ageHours!.asTimeAgo; + return LunaUI.TEXT_EMDASH; + } + + String? get lunaQuality { + if (this.quality != null && this.quality!.quality != null) + return this.quality!.quality!.name; + return LunaUI.TEXT_EMDASH; + } + + String get lunaSize { + if (this.size != null) return this.size.lunaBytesToString(); + return LunaUI.TEXT_EMDASH; + } + + String? lunaPreferredWordScore({bool nullOnEmpty = false}) { + if ((this.preferredWordScore ?? 0) != 0) { + String _prefix = this.preferredWordScore! > 0 ? '+' : ''; + return '$_prefix${this.preferredWordScore}'; + } + if (nullOnEmpty) return null; + return LunaUI.TEXT_EMDASH; + } +} diff --git a/lib/modules/readarr/core/router.dart b/lib/modules/readarr/core/router.dart new file mode 100644 index 0000000000..acdd319415 --- /dev/null +++ b/lib/modules/readarr/core/router.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrRouter extends LunaModuleRouter { + @override + void defineAllRoutes(FluroRouter router) { + ReadarrHomeRouter().defineRoute(router); + // Series + ReadarrAddSeriesRouter().defineRoute(router); + ReadarrAddSeriesDetailsRouter().defineRoute(router); + ReadarrEditAuthorRouter().defineRoute(router); + ReadarrAuthorDetailsRouter().defineRoute(router); + ReadarrBookDetailsRouter().defineRoute(router); + // Other + ReadarrHistoryRouter().defineRoute(router); + ReadarrQueueRouter().defineRoute(router); + ReadarrReleasesRouter().defineRoute(router); + ReadarrTagsRouter().defineRoute(router); + } +} + +abstract class ReadarrPageRouter extends LunaPageRouter { + ReadarrPageRouter(String route) : super(route); + + @override + void noParameterRouteDefinition( + FluroRouter router, { + bool homeRoute = false, + }) { + router.define( + fullRoute, + handler: Handler( + handlerFunc: (context, params) { + if (!homeRoute && !context!.read().enabled) { + return LunaNotEnabledRoute(module: 'Readarr'); + } + return widget(); + }, + ), + transitionType: LunaRouter.transitionType, + ); + } + + @override + void withParameterRouteDefinition( + FluroRouter router, + Widget Function(BuildContext?, Map>) handlerFunc, { + bool homeRoute = false, + }) { + router.define( + fullRoute, + handler: Handler( + handlerFunc: (context, params) { + if (!homeRoute && !context!.read().enabled) { + return LunaNotEnabledRoute(module: 'Readarr'); + } + return handlerFunc(context, params); + }, + ), + transitionType: LunaRouter.transitionType, + ); + } +} diff --git a/lib/modules/readarr/core/state.dart b/lib/modules/readarr/core/state.dart new file mode 100644 index 0000000000..1f2c08f206 --- /dev/null +++ b/lib/modules/readarr/core/state.dart @@ -0,0 +1,307 @@ +import 'dart:async'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrState extends LunaModuleState { + ReadarrState() { + reset(); + } + + @override + void reset() { + _authors = null; + _books = null; + _upcoming = null; + _missing = null; + _rootFolders = null; + _qualityProfiles = null; + _metadataProfiles = null; + _tags = null; + + resetProfile(); + if (_enabled) { + fetchAllAuthors(); + fetchAllBooks(); + fetchUpcoming(); + fetchMissing(); + fetchRootFolders(); + fetchQualityProfiles(); + fetchMetadataProfiles(); + fetchTags(); + } + notifyListeners(); + } + + /////////////// + /// PROFILE /// + /////////////// + + /// API handler instance + Readarr? _api; + Readarr? get api => _api; + + /// Is the API enabled? + bool _enabled = false; + bool get enabled => _enabled; + + /// Readarr host + String _host = ''; + String get host => _host; + + /// Readarr API key + String _apiKey = ''; + String get apiKey => _apiKey; + + /// Headers to attach to all requests + Map _headers = {}; + Map get headers => _headers; + + /// Reset the profile data, reinitializes API instance + void resetProfile() { + ProfileHiveObject _profile = LunaProfile.current; + // Copy profile into state + _api = null; + _enabled = _profile.readarrEnabled ?? false; + _host = _profile.readarrHost ?? ''; + _apiKey = _profile.readarrKey ?? ''; + _headers = _profile.readarrHeaders ?? {}; + // Create the API instance if Readarr is enabled + if (_enabled) { + _api = Readarr( + host: _host, + apiKey: _apiKey, + headers: Map.from(_headers), + ); + } + } + + ///////////////// + /// CATALOGUE /// + ///////////////// + + LunaListViewOption _seriesViewType = + ReadarrDatabaseValue.DEFAULT_VIEW_SERIES.data; + LunaListViewOption get seriesViewType => _seriesViewType; + set seriesViewType(LunaListViewOption seriesViewType) { + _seriesViewType = seriesViewType; + notifyListeners(); + } + + String _seriesSearchQuery = ''; + String get seriesSearchQuery => _seriesSearchQuery; + set seriesSearchQuery(String seriesSearchQuery) { + _seriesSearchQuery = seriesSearchQuery; + notifyListeners(); + } + + ReadarrAuthorSorting _seriesSortType = + ReadarrDatabaseValue.DEFAULT_SORTING_SERIES.data; + ReadarrAuthorSorting get seriesSortType => _seriesSortType; + set seriesSortType(ReadarrAuthorSorting seriesSortType) { + _seriesSortType = seriesSortType; + notifyListeners(); + } + + ReadarrAuthorFilter _seriesFilterType = + ReadarrDatabaseValue.DEFAULT_FILTERING_SERIES.data; + ReadarrAuthorFilter get seriesFilterType => _seriesFilterType; + set seriesFilterType(ReadarrAuthorFilter seriesFilterType) { + _seriesFilterType = seriesFilterType; + notifyListeners(); + } + + bool _seriesSortAscending = + ReadarrDatabaseValue.DEFAULT_SORTING_SERIES_ASCENDING.data; + bool get seriesSortAscending => _seriesSortAscending; + set seriesSortAscending(bool seriesSortAscending) { + _seriesSortAscending = seriesSortAscending; + notifyListeners(); + } + + /////////////// + /// AUTHORS /// + /////////////// + + Future>? _authors; + Future>? get authors => _authors; + void fetchAllAuthors() { + if (_api != null) { + _authors = _api!.author.getAll().then((series) { + return { + for (ReadarrAuthor s in series) s.id!: s, + }; + }); + } + notifyListeners(); + } + + Future fetchAuthor(int authorId) async { + if (_api != null) { + ReadarrAuthor series = await _api!.author.get(authorId: authorId); + (await _authors)![authorId] = series; + } + notifyListeners(); + } + + Future setSingleAuthor(ReadarrAuthor series) async { + (await _authors)![series.id!] = series; + notifyListeners(); + } + + Future removeSingleAuthor(int authorId) async { + (await _authors)!.remove(authorId); + notifyListeners(); + } + + ///////////// + /// BOOKS /// + ///////////// + + Future>? _books; + Future>? get books => _books; + void fetchAllBooks() { + if (_api != null) { + _books = _api!.book.getAll().then((books) { + return { + for (ReadarrBook b in books) b.id!: b, + }; + }); + } + notifyListeners(); + } + + Future setSingleBook(ReadarrBook book) async { + (await _books)![book.id!] = book; + notifyListeners(); + } + + Future removeSingleBook(int bookId) async { + (await _books)!.remove(bookId); + notifyListeners(); + } + + /////////////// + /// MISSING /// + /////////////// + + Future? _missing; + Future? get missing => _missing; + set missing(Future? missing) { + _missing = missing; + notifyListeners(); + } + + void fetchMissing() { + if (_api != null) + _missing = _api!.wanted.getMissing( + pageSize: ReadarrDatabaseValue.CONTENT_PAGE_SIZE.data, + sortDir: ReadarrSortDirection.DESCENDING, + sortKey: ReadarrWantedMissingSortKey.RELEASE_DATE, + ); + notifyListeners(); + } + + //////////////// + /// UPCOMING /// + //////////////// + + Future>? _upcoming; + Future>? get upcoming => _upcoming; + set upcoming(Future>? upcoming) { + _upcoming = upcoming; + notifyListeners(); + } + + void fetchUpcoming() { + DateTime start = DateTime.now(); + DateTime end = start + .add(Duration(days: ReadarrDatabaseValue.UPCOMING_FUTURE_DAYS.data)); + if (_api != null) + _upcoming = _api!.calendar.get( + start: start, + end: end, + includeEpisodeFile: true, + ); + notifyListeners(); + } + + //////////////// + /// PROFILES /// + //////////////// + + Future>? _qualityProfiles; + Future>? get qualityProfiles => _qualityProfiles; + set qualityProfiles(Future>? qualityProfiles) { + _qualityProfiles = qualityProfiles; + notifyListeners(); + } + + void fetchQualityProfiles() { + if (_api != null) _qualityProfiles = _api!.profile.getQualityProfiles(); + notifyListeners(); + } + + Future>? _metadataProfiles; + Future>? get metadataProfiles => + _metadataProfiles; + set metadataProfiles(Future>? metadataProfiles) { + _metadataProfiles = metadataProfiles; + notifyListeners(); + } + + void fetchMetadataProfiles() { + if (_api != null) _metadataProfiles = _api!.profile.getMetadataProfiles(); + notifyListeners(); + } + + //////////////////// + /// ROOT FOLDERS /// + //////////////////// + + Future>? _rootFolders; + Future>? get rootFolders => _rootFolders; + void fetchRootFolders() { + if (_api != null) _rootFolders = _api!.rootFolder.get(); + notifyListeners(); + } + + //////////// + /// TAGS /// + //////////// + + Future>? _tags; + Future>? get tags => _tags; + set tags(Future>? tags) { + _tags = tags; + notifyListeners(); + } + + void fetchTags() { + if (_api != null) _tags = _api!.tag.getAll(); + notifyListeners(); + } + + ////////////// + /// IMAGES /// + ////////////// + + String _baseImageURL() { + return _host.endsWith('/') + ? '${_host}api/v1/MediaCover' + : '$_host/api/v1/MediaCover'; + } + + String? getAuthorPosterURL(int? authorId) { + if (_enabled) { + return '${_baseImageURL()}/author/$authorId/poster-500.jpg?apikey=$_apiKey'; + } + return null; + } + + String? getBookCoverURL(int? bookId) { + if (_enabled) { + return '${_baseImageURL()}/book/$bookId/cover-500.jpg?apikey=$_apiKey'; + } + return null; + } +} diff --git a/lib/modules/readarr/core/types.dart b/lib/modules/readarr/core/types.dart new file mode 100644 index 0000000000..f6f859ecbe --- /dev/null +++ b/lib/modules/readarr/core/types.dart @@ -0,0 +1,7 @@ +export 'types/filter_releases.dart'; +export 'types/filter_author.dart'; +export 'types/settings_episode.dart'; +export 'types/settings_global.dart'; +export 'types/settings_author.dart'; +export 'types/sorting_releases.dart'; +export 'types/sorting_author.dart'; diff --git a/lib/modules/readarr/core/types/filter_author.dart b/lib/modules/readarr/core/types/filter_author.dart new file mode 100644 index 0000000000..f5ffdd329e --- /dev/null +++ b/lib/modules/readarr/core/types/filter_author.dart @@ -0,0 +1,120 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'filter_author.g.dart'; + +@HiveType(typeId: 31, adapterName: 'ReadarrAuthorFilterAdapter') +enum ReadarrAuthorFilter { + @HiveField(0) + ALL, + @HiveField(1) + MONITORED, + @HiveField(2) + UNMONITORED, + @HiveField(3) + CONTINUING, + @HiveField(4) + ENDED, + @HiveField(5) + MISSING, +} + +extension ReadarrAuthorFilterExtension on ReadarrAuthorFilter { + String get key { + switch (this) { + case ReadarrAuthorFilter.ALL: + return 'all'; + case ReadarrAuthorFilter.MONITORED: + return 'monitored'; + case ReadarrAuthorFilter.UNMONITORED: + return 'unmonitored'; + case ReadarrAuthorFilter.CONTINUING: + return 'continuing'; + case ReadarrAuthorFilter.ENDED: + return 'ended'; + case ReadarrAuthorFilter.MISSING: + return 'missing'; + } + } + + ReadarrAuthorFilter? fromKey(String? key) { + switch (key) { + case 'all': + return ReadarrAuthorFilter.ALL; + case 'monitored': + return ReadarrAuthorFilter.MONITORED; + case 'unmonitored': + return ReadarrAuthorFilter.UNMONITORED; + case 'continuing': + return ReadarrAuthorFilter.CONTINUING; + case 'ended': + return ReadarrAuthorFilter.ENDED; + case 'missing': + return ReadarrAuthorFilter.MISSING; + default: + return null; + } + } + + String get readable { + switch (this) { + case ReadarrAuthorFilter.ALL: + return 'readarr.All'.tr(); + case ReadarrAuthorFilter.MONITORED: + return 'readarr.Monitored'.tr(); + case ReadarrAuthorFilter.UNMONITORED: + return 'readarr.Unmonitored'.tr(); + case ReadarrAuthorFilter.CONTINUING: + return 'readarr.Continuing'.tr(); + case ReadarrAuthorFilter.ENDED: + return 'readarr.Ended'.tr(); + case ReadarrAuthorFilter.MISSING: + return 'readarr.Missing'.tr(); + } + } + + List filter(List series) => + _Sorter().byType(series, this); +} + +class _Sorter { + List byType( + List series, + ReadarrAuthorFilter type, + ) { + switch (type) { + case ReadarrAuthorFilter.ALL: + return series; + case ReadarrAuthorFilter.MONITORED: + return _monitored(series); + case ReadarrAuthorFilter.UNMONITORED: + return _unmonitored(series); + case ReadarrAuthorFilter.CONTINUING: + return _continuing(series); + case ReadarrAuthorFilter.ENDED: + return _ended(series); + case ReadarrAuthorFilter.MISSING: + return _missing(series); + } + } + + List _monitored(List series) => + series.where((s) => s.monitored!).toList(); + + List _unmonitored(List series) => + series.where((s) => !s.monitored!).toList(); + + List _continuing(List series) => + series.where((s) => !s.ended!).toList(); + + List _ended(List series) => + series.where((s) => s.ended!).toList(); + + List _missing(List series) { + return series + .where((s) => + (s.statistics?.episodeCount ?? 0) != + (s.statistics?.episodeFileCount ?? 0)) + .toList(); + } +} diff --git a/lib/modules/readarr/core/types/filter_releases.dart b/lib/modules/readarr/core/types/filter_releases.dart new file mode 100644 index 0000000000..af932d3e7b --- /dev/null +++ b/lib/modules/readarr/core/types/filter_releases.dart @@ -0,0 +1,75 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'filter_releases.g.dart'; + +@HiveType(typeId: 30, adapterName: 'ReadarrReleasesFilterAdapter') +enum ReadarrReleasesFilter { + @HiveField(0) + ALL, + @HiveField(1) + APPROVED, + @HiveField(2) + REJECTED, +} + +extension ReadarrReleasesFilterExtension on ReadarrReleasesFilter { + ReadarrReleasesFilter? fromKey(String? key) { + switch (key) { + case 'all': + return ReadarrReleasesFilter.ALL; + case 'approved': + return ReadarrReleasesFilter.APPROVED; + case 'rejected': + return ReadarrReleasesFilter.REJECTED; + default: + return null; + } + } + + String get key { + switch (this) { + case ReadarrReleasesFilter.ALL: + return 'all'; + case ReadarrReleasesFilter.APPROVED: + return 'approved'; + case ReadarrReleasesFilter.REJECTED: + return 'rejected'; + } + } + + String get readable { + switch (this) { + case ReadarrReleasesFilter.ALL: + return 'readarr.All'.tr(); + case ReadarrReleasesFilter.APPROVED: + return 'readarr.Approved'.tr(); + case ReadarrReleasesFilter.REJECTED: + return 'readarr.Rejected'.tr(); + } + } + + List filter(List releases) => + _Filterer().byType(releases, this); +} + +class _Filterer { + List byType( + List releases, + ReadarrReleasesFilter type, + ) { + switch (type) { + case ReadarrReleasesFilter.ALL: + return releases; + case ReadarrReleasesFilter.APPROVED: + return _approved(releases); + case ReadarrReleasesFilter.REJECTED: + return _rejected(releases); + } + } + + List _approved(List releases) => + releases.where((element) => element.approved!).toList(); + List _rejected(List releases) => + releases.where((element) => !element.approved!).toList(); +} diff --git a/lib/modules/readarr/core/types/settings_author.dart b/lib/modules/readarr/core/types/settings_author.dart new file mode 100644 index 0000000000..1c2566db1a --- /dev/null +++ b/lib/modules/readarr/core/types/settings_author.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +enum ReadarrAuthorSettingsType { + EDIT, + REFRESH, + DELETE, + MONITORED, +} + +extension ReadarrAuthorSettingsTypeExtension on ReadarrAuthorSettingsType { + IconData icon(ReadarrAuthor series) { + switch (this) { + case ReadarrAuthorSettingsType.MONITORED: + return series.monitored! + ? Icons.turned_in_not_rounded + : Icons.turned_in_rounded; + case ReadarrAuthorSettingsType.EDIT: + return Icons.edit_rounded; + case ReadarrAuthorSettingsType.REFRESH: + return Icons.refresh_rounded; + case ReadarrAuthorSettingsType.DELETE: + return Icons.delete_rounded; + } + } + + String name(ReadarrAuthor series) { + switch (this) { + case ReadarrAuthorSettingsType.MONITORED: + return series.monitored! + ? 'readarr.UnmonitorAuthor'.tr() + : 'readarr.MonitorAuthor'.tr(); + case ReadarrAuthorSettingsType.EDIT: + return 'readarr.EditAuthor'.tr(); + case ReadarrAuthorSettingsType.REFRESH: + return 'readarr.RefreshAuthor'.tr(); + case ReadarrAuthorSettingsType.DELETE: + return 'readarr.RemoveAuthor'.tr(); + } + } + + Future execute(BuildContext context, ReadarrAuthor series) async { + switch (this) { + case ReadarrAuthorSettingsType.EDIT: + await ReadarrEditAuthorRouter().navigateTo(context, series.id!); + break; + case ReadarrAuthorSettingsType.REFRESH: + await ReadarrAPIController().refreshAuthor( + context: context, + series: series, + ); + break; + case ReadarrAuthorSettingsType.DELETE: + bool result = await ReadarrDialogs().removeAuthor(context); + if (result) { + await ReadarrAPIController() + .removeAuthor(context: context, author: series) + .then((_) => Navigator.of(context).lunaSafetyPop()); + } + break; + case ReadarrAuthorSettingsType.MONITORED: + await ReadarrAPIController().toggleAuthorMonitored( + context: context, + author: series, + ); + break; + } + } +} diff --git a/lib/modules/readarr/core/types/settings_episode.dart b/lib/modules/readarr/core/types/settings_episode.dart new file mode 100644 index 0000000000..26056c67df --- /dev/null +++ b/lib/modules/readarr/core/types/settings_episode.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +enum ReadarrBookSettingsType { + AUTOMATIC_SEARCH, + INTERACTIVE_SEARCH, + REFRESH, + DELETE, + MONITORED, +} + +extension ReadarrEpisodeSettingsTypeExtension on ReadarrBookSettingsType { + IconData icon(ReadarrBook book) { + switch (this) { + case ReadarrBookSettingsType.MONITORED: + return book.monitored! + ? Icons.turned_in_not_rounded + : Icons.turned_in_rounded; + case ReadarrBookSettingsType.AUTOMATIC_SEARCH: + return Icons.search_rounded; + case ReadarrBookSettingsType.INTERACTIVE_SEARCH: + return Icons.youtube_searched_for_rounded; + case ReadarrBookSettingsType.REFRESH: + return Icons.refresh_rounded; + case ReadarrBookSettingsType.DELETE: + return Icons.delete_rounded; + } + } + + String name(ReadarrBook book) { + switch (this) { + case ReadarrBookSettingsType.MONITORED: + return book.monitored! + ? 'readarr.UnmonitorBook'.tr() + : 'readarr.MonitorBook'.tr(); + case ReadarrBookSettingsType.AUTOMATIC_SEARCH: + return 'readarr.AutomaticSearch'.tr(); + case ReadarrBookSettingsType.INTERACTIVE_SEARCH: + return 'readarr.InteractiveSearch'.tr(); + case ReadarrBookSettingsType.REFRESH: + return 'readarr.RefreshBook'.tr(); + case ReadarrBookSettingsType.DELETE: + return 'readarr.RemoveBook'.tr(); + } + } + + Future execute( + BuildContext context, + ReadarrBook book, + ) async { + switch (this) { + case ReadarrBookSettingsType.MONITORED: + await ReadarrAPIController().toggleBookMonitored( + context: context, + book: book, + ); + break; + case ReadarrBookSettingsType.AUTOMATIC_SEARCH: + await ReadarrAPIController().bookSearch( + context: context, + book: book, + ); + break; + case ReadarrBookSettingsType.INTERACTIVE_SEARCH: + await ReadarrReleasesRouter().navigateTo( + context, + bookId: book.id, + ); + break; + case ReadarrBookSettingsType.REFRESH: + await ReadarrAPIController().refreshBook( + context: context, + book: book, + ); + break; + case ReadarrBookSettingsType.DELETE: + bool result = await ReadarrDialogs().removeBook(context); + if (result) { + await ReadarrAPIController() + .removeBook(context: context, book: book) + .then((_) => Navigator.of(context).lunaSafetyPop()); + } + break; + } + } +} diff --git a/lib/modules/readarr/core/types/settings_global.dart b/lib/modules/readarr/core/types/settings_global.dart new file mode 100644 index 0000000000..f601b13c03 --- /dev/null +++ b/lib/modules/readarr/core/types/settings_global.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +enum ReadarrGlobalSettingsType { + WEB_GUI, + RUN_RSS_SYNC, + SEARCH_ALL_MISSING, + UPDATE_LIBRARY, + BACKUP_DATABASE, +} + +extension ReadarrGlobalSettingsTypeExtension on ReadarrGlobalSettingsType { + IconData get icon { + switch (this) { + case ReadarrGlobalSettingsType.WEB_GUI: + return Icons.language_rounded; + case ReadarrGlobalSettingsType.UPDATE_LIBRARY: + return Icons.autorenew_rounded; + case ReadarrGlobalSettingsType.RUN_RSS_SYNC: + return Icons.rss_feed_rounded; + case ReadarrGlobalSettingsType.SEARCH_ALL_MISSING: + return Icons.search_rounded; + case ReadarrGlobalSettingsType.BACKUP_DATABASE: + return Icons.save_rounded; + } + } + + String get name { + switch (this) { + case ReadarrGlobalSettingsType.WEB_GUI: + return 'readarr.ViewWebGUI'.tr(); + case ReadarrGlobalSettingsType.UPDATE_LIBRARY: + return 'readarr.UpdateLibrary'.tr(); + case ReadarrGlobalSettingsType.RUN_RSS_SYNC: + return 'readarr.RunRSSSync'.tr(); + case ReadarrGlobalSettingsType.SEARCH_ALL_MISSING: + return 'readarr.SearchAllMissing'.tr(); + case ReadarrGlobalSettingsType.BACKUP_DATABASE: + return 'readarr.BackupDatabase'.tr(); + } + } + + Future execute(BuildContext context) async { + switch (this) { + case ReadarrGlobalSettingsType.WEB_GUI: + return _webGUI(context); + case ReadarrGlobalSettingsType.RUN_RSS_SYNC: + return _runRssSync(context); + case ReadarrGlobalSettingsType.SEARCH_ALL_MISSING: + return _searchAllMissing(context); + case ReadarrGlobalSettingsType.UPDATE_LIBRARY: + return _updateLibrary(context); + case ReadarrGlobalSettingsType.BACKUP_DATABASE: + return _backupDatabase(context); + } + } + + Future _webGUI(BuildContext context) async => + context.read().host.lunaOpenGenericLink( + headers: context.read().headers, + ); + Future _backupDatabase(BuildContext context) async => + ReadarrAPIController().backupDatabase(context: context); + Future _searchAllMissing(BuildContext context) async { + bool result = await ReadarrDialogs().searchAllMissingEpisodes(context); + if (result) ReadarrAPIController().missingBooksSearch(context: context); + } + + Future _runRssSync(BuildContext context) async => + ReadarrAPIController().runRSSSync(context: context); + Future _updateLibrary(BuildContext context) async => + ReadarrAPIController().updateLibrary(context: context); +} diff --git a/lib/modules/readarr/core/types/sorting_author.dart b/lib/modules/readarr/core/types/sorting_author.dart new file mode 100644 index 0000000000..2ba15b5cc8 --- /dev/null +++ b/lib/modules/readarr/core/types/sorting_author.dart @@ -0,0 +1,175 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'sorting_author.g.dart'; + +@HiveType(typeId: 33, adapterName: 'ReadarrAuthorSortingAdapter') +enum ReadarrAuthorSorting { + @HiveField(0) + ALPHABETICAL, + @HiveField(1) + DATE_ADDED, + @HiveField(2) + EPISODES, + @HiveField(3) + QUALITY, + @HiveField(4) + SIZE, +} + +extension ReadarrAuthorSortingExtension on ReadarrAuthorSorting { + String get key { + switch (this) { + case ReadarrAuthorSorting.ALPHABETICAL: + return 'abc'; + case ReadarrAuthorSorting.DATE_ADDED: + return 'date_added'; + case ReadarrAuthorSorting.EPISODES: + return 'episodes'; + case ReadarrAuthorSorting.SIZE: + return 'size'; + case ReadarrAuthorSorting.QUALITY: + return 'quality'; + } + } + + String get readable { + switch (this) { + case ReadarrAuthorSorting.ALPHABETICAL: + return 'Alphabetical'; + case ReadarrAuthorSorting.DATE_ADDED: + return 'Date Added'; + case ReadarrAuthorSorting.EPISODES: + return 'Episodes'; + case ReadarrAuthorSorting.SIZE: + return 'Size'; + case ReadarrAuthorSorting.QUALITY: + return 'Quality Profile'; + } + } + + String value(ReadarrAuthor series, ReadarrQualityProfile? profile) { + switch (this) { + case ReadarrAuthorSorting.ALPHABETICAL: + return series.lunaEpisodeCount; + case ReadarrAuthorSorting.DATE_ADDED: + return series.lunaDateAddedShort; + case ReadarrAuthorSorting.EPISODES: + return series.lunaEpisodeCount; + case ReadarrAuthorSorting.QUALITY: + return profile?.name ?? LunaUI.TEXT_EMDASH; + case ReadarrAuthorSorting.SIZE: + return series.lunaSizeOnDisk; + } + } + + ReadarrAuthorSorting? fromKey(String? key) { + switch (key) { + case 'abc': + return ReadarrAuthorSorting.ALPHABETICAL; + case 'date_added': + return ReadarrAuthorSorting.DATE_ADDED; + case 'episodes': + return ReadarrAuthorSorting.EPISODES; + case 'size': + return ReadarrAuthorSorting.SIZE; + case 'quality': + return ReadarrAuthorSorting.QUALITY; + default: + return null; + } + } + + List sort(List data, bool ascending) => + _Sorter().byType(data, this, ascending); +} + +class _Sorter { + List byType( + List data, + ReadarrAuthorSorting type, + bool ascending, + ) { + switch (type) { + case ReadarrAuthorSorting.DATE_ADDED: + return _dateAdded(data, ascending); + case ReadarrAuthorSorting.EPISODES: + return _episodes(data, ascending); + case ReadarrAuthorSorting.SIZE: + return _size(data, ascending); + case ReadarrAuthorSorting.ALPHABETICAL: + return _alphabetical(data, ascending); + case ReadarrAuthorSorting.QUALITY: + return _quality(data, ascending); + } + } + + List _alphabetical( + List series, bool ascending) { + ascending + ? series.sort((a, b) => + a.sortTitle!.toLowerCase().compareTo(b.sortTitle!.toLowerCase())) + : series.sort((a, b) => + b.sortTitle!.toLowerCase().compareTo(a.sortTitle!.toLowerCase())); + return series; + } + + List _dateAdded(List series, bool ascending) { + series.sort((a, b) { + if (ascending) { + if (a.added == null) return 1; + if (b.added == null) return -1; + int _comparison = a.added!.compareTo(b.added!); + return _comparison == 0 + ? a.sortTitle!.toLowerCase().compareTo(b.sortTitle!.toLowerCase()) + : _comparison; + } else { + if (b.added == null) return -1; + if (a.added == null) return 1; + int _comparison = b.added!.compareTo(a.added!); + return _comparison == 0 + ? a.sortTitle!.toLowerCase().compareTo(b.sortTitle!.toLowerCase()) + : _comparison; + } + }); + return series; + } + + List _episodes(List series, bool ascending) { + series.sort((a, b) { + int _comparison = ascending + ? a.lunaPercentageComplete.compareTo(b.lunaPercentageComplete) + : b.lunaPercentageComplete.compareTo(a.lunaPercentageComplete); + return _comparison == 0 + ? a.sortTitle!.toLowerCase().compareTo(b.sortTitle!.toLowerCase()) + : _comparison; + }); + return series; + } + + List _quality(List series, bool ascending) { + series.sort((a, b) { + int _comparison = ascending + ? (a.qualityProfileId ?? 0).compareTo(b.qualityProfileId ?? 0) + : (b.qualityProfileId ?? 0).compareTo(a.qualityProfileId ?? 0); + return _comparison == 0 + ? a.sortTitle!.toLowerCase().compareTo(b.sortTitle!.toLowerCase()) + : _comparison; + }); + return series; + } + + List _size(List series, bool ascending) { + series.sort((a, b) { + int _comparison = ascending + ? (a.statistics?.sizeOnDisk ?? 0) + .compareTo(b.statistics?.sizeOnDisk ?? 0) + : (b.statistics?.sizeOnDisk ?? 0) + .compareTo(a.statistics?.sizeOnDisk ?? 0); + return _comparison == 0 + ? a.sortTitle!.toLowerCase().compareTo(b.sortTitle!.toLowerCase()) + : _comparison; + }); + return series; + } +} diff --git a/lib/modules/readarr/core/types/sorting_releases.dart b/lib/modules/readarr/core/types/sorting_releases.dart new file mode 100644 index 0000000000..bf7e7531b5 --- /dev/null +++ b/lib/modules/readarr/core/types/sorting_releases.dart @@ -0,0 +1,186 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +part 'sorting_releases.g.dart'; + +@HiveType(typeId: 32, adapterName: 'ReadarrReleasesSortingAdapter') +enum ReadarrReleasesSorting { + @HiveField(0) + AGE, + @HiveField(1) + ALPHABETICAL, + @HiveField(2) + SEEDERS, + @HiveField(3) + SIZE, + @HiveField(4) + TYPE, + @HiveField(5) + WEIGHT, + @HiveField(6) + WORD_SCORE, +} + +extension ReadarrReleasesSortingExtension on ReadarrReleasesSorting { + String get key { + switch (this) { + case ReadarrReleasesSorting.AGE: + return 'age'; + case ReadarrReleasesSorting.ALPHABETICAL: + return 'abc'; + case ReadarrReleasesSorting.SEEDERS: + return 'seeders'; + case ReadarrReleasesSorting.WEIGHT: + return 'weight'; + case ReadarrReleasesSorting.TYPE: + return 'type'; + case ReadarrReleasesSorting.SIZE: + return 'size'; + case ReadarrReleasesSorting.WORD_SCORE: + return 'word_score'; + } + } + + String get readable { + switch (this) { + case ReadarrReleasesSorting.AGE: + return 'Age'; + case ReadarrReleasesSorting.ALPHABETICAL: + return 'Alphabetical'; + case ReadarrReleasesSorting.SEEDERS: + return 'Seeders'; + case ReadarrReleasesSorting.WEIGHT: + return 'Weight'; + case ReadarrReleasesSorting.TYPE: + return 'Type'; + case ReadarrReleasesSorting.SIZE: + return 'Size'; + case ReadarrReleasesSorting.WORD_SCORE: + return 'readarr.WordScore'.tr(); + } + } + + ReadarrReleasesSorting? fromKey(String? key) { + switch (key) { + case 'age': + return ReadarrReleasesSorting.AGE; + case 'abc': + return ReadarrReleasesSorting.ALPHABETICAL; + case 'seeders': + return ReadarrReleasesSorting.SEEDERS; + case 'size': + return ReadarrReleasesSorting.SIZE; + case 'type': + return ReadarrReleasesSorting.TYPE; + case 'weight': + return ReadarrReleasesSorting.WEIGHT; + case 'word_score': + return ReadarrReleasesSorting.WORD_SCORE; + default: + return null; + } + } + + List sort(List releases, bool ascending) => + _Sorter().byType(releases, this, ascending); +} + +class _Sorter { + List byType( + List releases, + ReadarrReleasesSorting type, + bool ascending, + ) { + switch (type) { + case ReadarrReleasesSorting.AGE: + return _age(releases, ascending); + case ReadarrReleasesSorting.ALPHABETICAL: + return _alphabetical(releases, ascending); + case ReadarrReleasesSorting.SEEDERS: + return _seeders(releases, ascending); + case ReadarrReleasesSorting.WEIGHT: + return _weight(releases, ascending); + case ReadarrReleasesSorting.TYPE: + return _type(releases, ascending); + case ReadarrReleasesSorting.SIZE: + return _size(releases, ascending); + case ReadarrReleasesSorting.WORD_SCORE: + return _wordScore(releases, ascending); + } + } + + List _alphabetical( + List releases, bool ascending) { + ascending + ? releases.sort( + (a, b) => a.title!.toLowerCase().compareTo(b.title!.toLowerCase())) + : releases.sort( + (a, b) => b.title!.toLowerCase().compareTo(a.title!.toLowerCase())); + return releases; + } + + List _age(List releases, bool ascending) { + ascending + ? releases.sort((a, b) => a.ageHours!.compareTo(b.ageHours!)) + : releases.sort((a, b) => b.ageHours!.compareTo(a.ageHours!)); + return releases; + } + + List _seeders(List releases, bool ascending) { + List _torrent = _weight( + releases + .where((release) => release.protocol == ReadarrProtocol.TORRENT) + .toList(), + true); + List _usenet = _weight( + releases + .where((release) => release.protocol == ReadarrProtocol.USENET) + .toList(), + true); + ascending + ? _torrent + .sort((a, b) => (a.seeders ?? -1).compareTo((b.seeders ?? -1))) + : _torrent + .sort((a, b) => (b.seeders ?? -1).compareTo((a.seeders ?? -1))); + return [..._torrent, ..._usenet]; + } + + List _weight(List releases, bool ascending) { + ascending + ? releases.sort((a, b) => + (a.releaseWeight ?? -1).compareTo((b.releaseWeight ?? -1))) + : releases.sort((a, b) => + (b.releaseWeight ?? -1).compareTo((a.releaseWeight ?? -1))); + return releases; + } + + List _type(List releases, bool ascending) { + List _torrent = _weight( + releases + .where((release) => release.protocol == ReadarrProtocol.TORRENT) + .toList(), + true); + List _usenet = _weight( + releases + .where((release) => release.protocol == ReadarrProtocol.USENET) + .toList(), + true); + return ascending ? [..._torrent, ..._usenet] : [..._usenet, ..._torrent]; + } + + List _size(List releases, bool ascending) { + ascending + ? releases.sort((a, b) => (a.size ?? -1).compareTo((b.size ?? -1))) + : releases.sort((a, b) => (b.size ?? -1).compareTo((a.size ?? -1))); + return releases; + } + + List _wordScore(List releases, bool ascending) { + ascending + ? releases.sort((a, b) => + (b.preferredWordScore ?? 0).compareTo((a.preferredWordScore ?? 0))) + : releases.sort((a, b) => + (a.preferredWordScore ?? 0).compareTo((b.preferredWordScore ?? 0))); + return releases; + } +} diff --git a/lib/modules/readarr/core/webhooks.dart b/lib/modules/readarr/core/webhooks.dart new file mode 100644 index 0000000000..f69d1eeb23 --- /dev/null +++ b/lib/modules/readarr/core/webhooks.dart @@ -0,0 +1,104 @@ +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrWebhooks extends LunaWebhooks { + @override + Future handle(Map data) async { + _EventType? event = _EventType.GRAB.fromKey(data['event']); + if (event == null) + LunaLogger().warning( + 'ReadarrWebhooks', + 'handle', + 'Unknown event type: ${data['event'] ?? 'null'}', + ); + event?.execute(data); + } +} + +enum _EventType { + DOWNLOAD, + EPISODE_FILE_DELETE, + GRAB, + HEALTH, + RENAME, + SERIES_DELETE, + TEST, +} + +extension _EventTypeExtension on _EventType { + _EventType? fromKey(String? key) { + switch (key) { + case 'Download': + return _EventType.DOWNLOAD; + case 'EpisodeFileDelete': + return _EventType.EPISODE_FILE_DELETE; + case 'Grab': + return _EventType.GRAB; + case 'Health': + return _EventType.HEALTH; + case 'Rename': + return _EventType.RENAME; + case 'SeriesDelete': + return _EventType.SERIES_DELETE; + case 'Test': + return _EventType.TEST; + } + return null; + } + + Future execute(Map data) async { + switch (this) { + case _EventType.DOWNLOAD: + return _downloadEvent(data); + case _EventType.EPISODE_FILE_DELETE: + return _episodeFileDeleteEvent(data); + case _EventType.GRAB: + return _grabEvent(data); + case _EventType.HEALTH: + return _healthEvent(data); + case _EventType.RENAME: + return _renameEvent(data); + case _EventType.SERIES_DELETE: + return _seriesDeleteEvent(data); + case _EventType.TEST: + return _testEvent(data); + } + } + + Future _downloadEvent(Map data) async => + _goToSeasonDetails( + int.tryParse(data['authorId']), int.tryParse(data['seasonNumber'])); + Future _episodeFileDeleteEvent(Map data) async => + _goToSeasonDetails( + int.tryParse(data['authorId']), int.tryParse(data['seasonNumber'])); + Future _grabEvent(Map data) async => + ReadarrQueueRouter().navigateTo(LunaState.navigatorKey.currentContext!); + Future _healthEvent(Map data) async => + LunaModule.READARR.launch(); + Future _renameEvent(Map data) async => + _goToSeriesDetails(int.tryParse(data['authorId'])); + Future _seriesDeleteEvent(Map data) async => + LunaModule.READARR.launch(); + Future _testEvent(Map data) async => + LunaModule.READARR.launch(); + + Future _goToSeriesDetails(int? authorId) async { + if (authorId != null) { + return ReadarrAuthorDetailsRouter().navigateTo( + LunaState.navigatorKey.currentContext!, + authorId, + ); + } + return LunaModule.READARR.launch(); + } + + Future _goToSeasonDetails(int? bookId, int? seasonNumber) async { + if (bookId != null) { + return ReadarrBookDetailsRouter().navigateTo( + LunaState.navigatorKey.currentContext!, + bookId, + ); + } + return LunaModule.READARR.launch(); + } +} diff --git a/lib/modules/readarr/routes.dart b/lib/modules/readarr/routes.dart new file mode 100644 index 0000000000..c34f4b9fd5 --- /dev/null +++ b/lib/modules/readarr/routes.dart @@ -0,0 +1,14 @@ +export 'routes/add_author.dart'; +export 'routes/add_author_details.dart'; +export 'routes/book_details.dart'; +export 'routes/catalogue.dart'; +export 'routes/edit_author.dart'; +export 'routes/history.dart'; +export 'routes/missing.dart'; +export 'routes/more.dart'; +export 'routes/queue.dart'; +export 'routes/releases.dart'; +export 'routes/author_details.dart'; +export 'routes/readarr.dart'; +export 'routes/tags.dart'; +export 'routes/upcoming.dart'; diff --git a/lib/modules/readarr/routes/add_author.dart b/lib/modules/readarr/routes/add_author.dart new file mode 100644 index 0000000000..69bef83493 --- /dev/null +++ b/lib/modules/readarr/routes/add_author.dart @@ -0,0 +1,3 @@ +export 'add_author/route.dart'; +export 'add_author/state.dart'; +export 'add_author/widgets.dart'; diff --git a/lib/modules/readarr/routes/add_author/route.dart b/lib/modules/readarr/routes/add_author/route.dart new file mode 100644 index 0000000000..3d887c7bf8 --- /dev/null +++ b/lib/modules/readarr/routes/add_author/route.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class _ReadarrAddSeriesArguments { + final String query; + + _ReadarrAddSeriesArguments(this.query); +} + +class ReadarrAddSeriesRouter extends ReadarrPageRouter { + ReadarrAddSeriesRouter() : super('/readarr/addseries'); + + @override + _Widget widget() => _Widget(); + + @override + Future navigateTo( + BuildContext context, [ + String query = '', + ]) async { + LunaRouter.router.navigateTo( + context, + route(), + routeSettings: RouteSettings(arguments: _ReadarrAddSeriesArguments(query)), + ); + } + + @override + void defineRoute(FluroRouter router) { + super.noParameterRouteDefinition(router); + } +} + +class _Widget extends StatefulWidget { + @override + State createState() => _State(); +} + +class _State extends State<_Widget> with LunaScrollControllerMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + _ReadarrAddSeriesArguments? _arguments; + + @override + Widget build(BuildContext context) { + _arguments = ModalRoute.of(context)!.settings.arguments + as _ReadarrAddSeriesArguments?; + return ChangeNotifierProvider( + create: (context) => ReadarrAddSeriesState( + context, + _arguments?.query ?? '', + ), + builder: (context, _) => LunaScaffold( + scaffoldKey: _scaffoldKey, + appBar: _appBar() as PreferredSizeWidget?, + body: _body(), + ), + ); + } + + Widget _appBar() { + return ReadarrAuthorAddAppBar( + scrollController: scrollController, + query: _arguments?.query, + autofocus: (_arguments?.query ?? '').isEmpty, + ); + } + + Widget _body() { + return ReadarrAddSeriesSearchPage(scrollController: scrollController); + } +} diff --git a/lib/modules/readarr/routes/add_author/state.dart b/lib/modules/readarr/routes/add_author/state.dart new file mode 100644 index 0000000000..985138d8f5 --- /dev/null +++ b/lib/modules/readarr/routes/add_author/state.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAddSeriesState extends ChangeNotifier { + ReadarrAddSeriesState(BuildContext context, String query) { + _searchQuery = query; + fetchExclusions(context); + } + + late String _searchQuery; + String get searchQuery => _searchQuery; + set searchQuery(String searchQuery) { + _searchQuery = searchQuery; + notifyListeners(); + } + + Future>? _lookup; + Future>? get lookup => _lookup; + void fetchLookup(BuildContext context) { + if (context.read().enabled) { + _lookup = context + .read() + .api! + .authorLookup + .get(term: _searchQuery); + } + notifyListeners(); + } + + Future>? _exclusions; + Future>? get exclusions => _exclusions; + void fetchExclusions(BuildContext context) { + if ((context.read().enabled)) { + _exclusions = context.read().api!.importList.get(); + } + notifyListeners(); + } +} diff --git a/lib/modules/readarr/routes/add_author/widgets.dart b/lib/modules/readarr/routes/add_author/widgets.dart new file mode 100644 index 0000000000..817652592f --- /dev/null +++ b/lib/modules/readarr/routes/add_author/widgets.dart @@ -0,0 +1,3 @@ +export 'widgets/appbar.dart'; +export 'widgets/page_search.dart'; +export 'widgets/search_results_tile.dart'; diff --git a/lib/modules/readarr/routes/add_author/widgets/appbar.dart b/lib/modules/readarr/routes/add_author/widgets/appbar.dart new file mode 100644 index 0000000000..a963894b90 --- /dev/null +++ b/lib/modules/readarr/routes/add_author/widgets/appbar.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +// ignore: non_constant_identifier_names +Widget ReadarrAuthorAddAppBar({ + required ScrollController scrollController, + required String? query, + required bool autofocus, +}) => + LunaAppBar( + title: 'readarr.AddAuthor'.tr(), + scrollControllers: [scrollController], + bottom: _SearchBar( + scrollController: scrollController, + query: query ?? '', + autofocus: autofocus, + ), + ); + +class _SearchBar extends StatefulWidget implements PreferredSizeWidget { + final String query; + final bool autofocus; + final ScrollController scrollController; + + const _SearchBar({ + Key? key, + required this.query, + required this.autofocus, + required this.scrollController, + }) : super(key: key); + + @override + Size get preferredSize => + const Size.fromHeight(LunaTextInputBar.defaultAppBarHeight); + + @override + State<_SearchBar> createState() => _State(); +} + +class _State extends State<_SearchBar> { + final TextEditingController _controller = TextEditingController(); + + @override + void initState() { + super.initState(); + _controller.text = widget.query; + } + + @override + Widget build(BuildContext context) => Consumer( + builder: (context, state, _) => SizedBox( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: LunaTextInputBar( + controller: _controller, + scrollController: widget.scrollController, + autofocus: widget.autofocus, + onChanged: (value) => + context.read().searchQuery = value, + onSubmitted: (value) { + if (value.isNotEmpty) + context.read().fetchLookup(context); + }, + margin: LunaTextInputBar.appBarMargin, + ), + ), + ], + ), + height: LunaTextInputBar.defaultAppBarHeight, + ), + ); +} diff --git a/lib/modules/readarr/routes/add_author/widgets/page_search.dart b/lib/modules/readarr/routes/add_author/widgets/page_search.dart new file mode 100644 index 0000000000..5f00d1d45f --- /dev/null +++ b/lib/modules/readarr/routes/add_author/widgets/page_search.dart @@ -0,0 +1,112 @@ +import 'package:collection/collection.dart' show IterableExtension; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAddSeriesSearchPage extends StatefulWidget { + final ScrollController scrollController; + + const ReadarrAddSeriesSearchPage({ + Key? key, + required this.scrollController, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State + with LunaLoadCallbackMixin { + final GlobalKey _refreshKey = + GlobalKey(); + + @override + Future loadCallback() async { + if (context.read().searchQuery.isNotEmpty) { + context.read().fetchLookup(context); + await context.read().lookup; + } + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, state, _) => Selector< + ReadarrAddSeriesState, + Tuple2>?, + Future>?>>( + selector: (_, state) => Tuple2(state.lookup, state.exclusions), + builder: (context, tuple, _) { + if (tuple.item1 == null) return Container(); + return _builder( + lookup: tuple.item1, + exclusions: tuple.item2, + series: state.authors, + ); + }, + ), + ); + } + + Widget _builder({ + required Future>? lookup, + required Future>? exclusions, + required Future>? series, + }) { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: FutureBuilder( + future: Future.wait([lookup!, series!, exclusions!]), + builder: (context, AsyncSnapshot snapshot) { + if (snapshot.hasError) { + if (snapshot.connectionState != ConnectionState.waiting) + LunaLogger().error( + 'Unable to fetch Readarr series lookup', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState!.show); + } + if (snapshot.hasData) + return _list( + snapshot.data![0], + snapshot.data![1], + snapshot.data![2], + ); + return const LunaLoader(); + }, + ), + ); + } + + Widget _list( + List results, + Map series, + List exclusions, + ) { + if (results.isEmpty) + return LunaListView( + controller: widget.scrollController, + children: [ + LunaMessage.inList(text: 'readarr.NoResultsFound'.tr()), + ], + ); + return LunaListViewBuilder( + controller: widget.scrollController, + itemExtent: ReadarrAuthorAddSearchResultTile.extent, + itemCount: results.length, + itemBuilder: (context, index) { + ReadarrExclusion? exclusion = exclusions.firstWhereOrNull( + (exclusion) => exclusion.foreignId == results[index].foreignAuthorId, + ); + return ReadarrAuthorAddSearchResultTile( + series: results[index], + exists: series[results[index].id] != null, + isExcluded: exclusion != null, + ); + }, + ); + } +} diff --git a/lib/modules/readarr/routes/add_author/widgets/search_results_tile.dart b/lib/modules/readarr/routes/add_author/widgets/search_results_tile.dart new file mode 100644 index 0000000000..50fbe2e75f --- /dev/null +++ b/lib/modules/readarr/routes/add_author/widgets/search_results_tile.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorAddSearchResultTile extends StatefulWidget { + static final double extent = LunaBlock.calculateItemExtent( + 1, + hasBottom: true, + bottomHeight: LunaBlock.SUBTITLE_HEIGHT * 2, + ); + + final ReadarrAuthor series; + final bool onTapShowOverview; + final bool exists; + final bool isExcluded; + + const ReadarrAuthorAddSearchResultTile({ + Key? key, + required this.series, + required this.exists, + required this.isExcluded, + this.onTapShowOverview = false, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return LunaBlock( + backgroundUrl: widget.series.remotePoster, + posterUrl: widget.series.remotePoster, + posterHeaders: context.watch().headers, + posterPlaceholderIcon: LunaIcons.BOOK, + title: widget.series.title, + titleColor: widget.isExcluded ? LunaColours.red : Colors.white, + disabled: widget.exists, + body: [_subtitle1()], + bottom: _subtitle2(), + bottomHeight: LunaBlock.SUBTITLE_HEIGHT * 2, + onTap: _onTap, + onLongPress: _onLongPress, + ); + } + + TextSpan _subtitle1() { + return TextSpan(children: [ + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + ]); + } + + Widget _subtitle2() { + return SizedBox( + height: LunaBlock.SUBTITLE_HEIGHT * 2, + child: RichText( + text: TextSpan( + style: const TextStyle( + fontStyle: FontStyle.italic, + fontSize: LunaUI.FONT_SIZE_H3, + color: LunaColours.grey, + ), + children: [ + LunaTextSpan.extended(text: widget.series.lunaOverview), + ], + ), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ); + } + + Future _onTap() async { + if (widget.onTapShowOverview) { + LunaDialogs().textPreview( + context, + widget.series.title, + widget.series.overview ?? 'readarr.NoSummaryAvailable'.tr(), + ); + } else if (widget.exists) { + ReadarrAuthorDetailsRouter().navigateTo( + context, + widget.series.id ?? -1, + ); + } else { + ReadarrAddSeriesDetailsRouter().navigateTo(context, widget.series); + } + } + + Future? _onLongPress() async => + widget.series.foreignAuthorId?.lunaOpenGoodreadsAuthor(); +} diff --git a/lib/modules/readarr/routes/add_author_details.dart b/lib/modules/readarr/routes/add_author_details.dart new file mode 100644 index 0000000000..7799aa0d47 --- /dev/null +++ b/lib/modules/readarr/routes/add_author_details.dart @@ -0,0 +1,3 @@ +export 'add_author_details/route.dart'; +export 'add_author_details/state.dart'; +export 'add_author_details/widgets.dart'; diff --git a/lib/modules/readarr/routes/add_author_details/route.dart b/lib/modules/readarr/routes/add_author_details/route.dart new file mode 100644 index 0000000000..b892f27cac --- /dev/null +++ b/lib/modules/readarr/routes/add_author_details/route.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class _Args { + final ReadarrAuthor series; + + _Args({ + required this.series, + }); +} + +class ReadarrAddSeriesDetailsRouter extends ReadarrPageRouter { + ReadarrAddSeriesDetailsRouter() : super('/readarr/addseries/details'); + + @override + _Widget widget() => _Widget(); + + @override + Future navigateTo( + BuildContext context, [ + ReadarrAuthor? series, + ]) { + assert(series != null); + return LunaRouter.router.navigateTo( + context, + route(), + routeSettings: RouteSettings( + arguments: _Args( + series: series!, + ), + ), + ); + } + + @override + void defineRoute(FluroRouter router) { + super.noParameterRouteDefinition(router); + } +} + +class _Widget extends StatefulWidget { + @override + State createState() => _State(); +} + +class _State extends State<_Widget> + with LunaLoadCallbackMixin, LunaScrollControllerMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = + GlobalKey(); + + @override + Future loadCallback() async { + context.read().fetchRootFolders(); + context.read().fetchTags(); + context.read().fetchQualityProfiles(); + context.read().fetchMetadataProfiles(); + } + + @override + Widget build(BuildContext context) { + _Args? arguments = ModalRoute.of(context)!.settings.arguments as _Args?; + if (arguments == null) { + return LunaInvalidRoute( + title: 'readarr.AddAuthor'.tr(), + message: 'readarr.NoSeriesFound'.tr(), + ); + } + return ChangeNotifierProvider( + create: (_) => ReadarrAuthorAddDetailsState( + series: arguments.series, + ), + builder: (context, _) => LunaScaffold( + scaffoldKey: _scaffoldKey, + appBar: _appBar() as PreferredSizeWidget?, + body: _body(context), + bottomNavigationBar: const ReadarrAddSeriesDetailsActionBar(), + ), + ); + } + + Widget _appBar() { + return LunaAppBar( + title: 'readarr.AddAuthor'.tr(), + scrollControllers: [scrollController], + ); + } + + Widget _body(BuildContext context) { + return FutureBuilder( + future: Future.wait( + [ + context.watch().rootFolders!, + context.watch().tags!, + context.watch().qualityProfiles!, + context.watch().metadataProfiles!, + ], + ), + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasError) { + if (snapshot.connectionState != ConnectionState.waiting) { + LunaLogger().error( + 'Unable to fetch Readarr add series data', + snapshot.error, + snapshot.stackTrace, + ); + } + return LunaMessage.error(onTap: _refreshKey.currentState!.show); + } + if (snapshot.hasData) { + return _content( + context, + rootFolders: snapshot.data![0] as List, + tags: snapshot.data![1] as List, + qualityProfiles: snapshot.data![2] as List, + metadataProfiles: snapshot.data![3] as List, + ); + } + return const LunaLoader(); + }, + ); + } + + Widget _content( + BuildContext context, { + required List rootFolders, + required List qualityProfiles, + required List metadataProfiles, + required List tags, + }) { + context.read().initializeMonitored(); + context.read().initializeMonitorType(); + context + .read() + .initializeRootFolder(rootFolders); + context + .read() + .initializeQualityProfile(qualityProfiles); + context + .read() + .initializeMetadataProfile(metadataProfiles); + context.read().initializeTags(tags); + context.read().canExecuteAction = true; + return LunaListView( + controller: scrollController, + children: [ + ReadarrAuthorAddSearchResultTile( + series: context.read().series, + onTapShowOverview: true, + exists: false, + isExcluded: false, + ), + const ReadarrAuthorAddDetailsRootFolderTile(), + const ReadarrAuthorAddDetailsMonitorTile(), + const ReadarrAuthorAddDetailsQualityProfileTile(), + const ReadarrAuthorAddDetailsMetadataProfileTile(), + const ReadarrAuthorAddDetailsTagsTile(), + ], + ); + } +} diff --git a/lib/modules/readarr/routes/add_author_details/state.dart b/lib/modules/readarr/routes/add_author_details/state.dart new file mode 100644 index 0000000000..10c937f3c2 --- /dev/null +++ b/lib/modules/readarr/routes/add_author_details/state.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorAddDetailsState extends ChangeNotifier { + final ReadarrAuthor series; + bool canExecuteAction = false; + + ReadarrAuthorAddDetailsState({ + required this.series, + }); + + bool _monitored = true; + bool get monitored => _monitored; + set monitored(bool monitored) { + _monitored = monitored; + notifyListeners(); + } + + void initializeMonitored() { + _monitored = ReadarrDatabaseValue.ADD_SERIES_DEFAULT_MONITORED.data ?? true; + } + + late ReadarrAuthorMonitorType _monitorType; + ReadarrAuthorMonitorType get monitorType => _monitorType; + set monitorType(ReadarrAuthorMonitorType monitorType) { + _monitorType = monitorType; + notifyListeners(); + } + + void initializeMonitorType() { + _monitorType = ReadarrAuthorMonitorType.values.firstWhere( + (element) => + element.value == + ReadarrDatabaseValue.ADD_SERIES_DEFAULT_MONITOR_TYPE.data, + orElse: () => ReadarrAuthorMonitorType.ALL, + ); + } + + late ReadarrRootFolder _rootFolder; + ReadarrRootFolder get rootFolder => _rootFolder; + set rootFolder(ReadarrRootFolder rootFolder) { + _rootFolder = rootFolder; + notifyListeners(); + } + + void initializeRootFolder(List rootFolders) { + _rootFolder = rootFolders.firstWhere( + (element) => + element.id == + ReadarrDatabaseValue.ADD_SERIES_DEFAULT_ROOT_FOLDER.data, + orElse: () => rootFolders.isNotEmpty + ? rootFolders[0] + : ReadarrRootFolder(id: -1, freeSpace: 0, path: LunaUI.TEXT_EMDASH), + ); + } + + late ReadarrQualityProfile _qualityProfile; + ReadarrQualityProfile get qualityProfile => _qualityProfile; + set qualityProfile(ReadarrQualityProfile qualityProfile) { + _qualityProfile = qualityProfile; + notifyListeners(); + } + + void initializeQualityProfile(List qualityProfiles) { + _qualityProfile = qualityProfiles.firstWhere( + (element) => + element.id == + ReadarrDatabaseValue.ADD_SERIES_DEFAULT_QUALITY_PROFILE.data, + orElse: () => qualityProfiles.isNotEmpty + ? qualityProfiles[0] + : ReadarrQualityProfile(id: -1, name: LunaUI.TEXT_EMDASH), + ); + } + + late ReadarrMetadataProfile _metadataProfile; + ReadarrMetadataProfile get metadataProfile => _metadataProfile; + set metadataProfile(ReadarrMetadataProfile metadataProfile) { + _metadataProfile = metadataProfile; + notifyListeners(); + } + + void initializeMetadataProfile( + List metadataProfiles) { + _metadataProfile = metadataProfiles.firstWhere( + (element) => + element.id == + ReadarrDatabaseValue.ADD_SERIES_DEFAULT_LANGUAGE_PROFILE.data, + orElse: () => metadataProfiles.isNotEmpty + ? metadataProfiles[0] + : ReadarrMetadataProfile(id: -1, name: LunaUI.TEXT_EMDASH), + ); + } + + late List _tags; + List get tags => _tags; + set tags(List tags) { + _tags = tags; + ReadarrDatabaseValue.ADD_SERIES_DEFAULT_TAGS + .put(tags.map((tag) => tag.id).toList()); + notifyListeners(); + } + + void initializeTags(List tags) { + _tags = tags + .where((tag) => + ((ReadarrDatabaseValue.ADD_SERIES_DEFAULT_TAGS.data as List?) ?? []) + .contains(tag.id)) + .toList(); + } + + LunaLoadingState _state = LunaLoadingState.INACTIVE; + LunaLoadingState get state => _state; + set state(LunaLoadingState state) { + _state = state; + notifyListeners(); + } +} diff --git a/lib/modules/readarr/routes/add_author_details/widgets.dart b/lib/modules/readarr/routes/add_author_details/widgets.dart new file mode 100644 index 0000000000..24924c5aff --- /dev/null +++ b/lib/modules/readarr/routes/add_author_details/widgets.dart @@ -0,0 +1,6 @@ +export 'widgets/bottom_action_bar.dart'; +export 'widgets/tile_metadata_profile.dart'; +export 'widgets/tile_monitor.dart'; +export 'widgets/tile_quality_profile.dart'; +export 'widgets/tile_root_folder.dart'; +export 'widgets/tile_tags.dart'; diff --git a/lib/modules/readarr/routes/add_author_details/widgets/bottom_action_bar.dart b/lib/modules/readarr/routes/add_author_details/widgets/bottom_action_bar.dart new file mode 100644 index 0000000000..079573ed92 --- /dev/null +++ b/lib/modules/readarr/routes/add_author_details/widgets/bottom_action_bar.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAddSeriesDetailsActionBar extends StatelessWidget { + const ReadarrAddSeriesDetailsActionBar({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBottomActionBar( + actions: [ + LunaActionBarCard( + title: 'lunasea.Options'.tr(), + subtitle: 'readarr.StartSearchFor'.tr(), + onTap: () async => ReadarrDialogs().addSeriesOptions(context), + ), + LunaButton( + type: LunaButtonType.TEXT, + text: 'lunasea.Add'.tr(), + icon: Icons.add_rounded, + onTap: () async => _onTap(context), + loadingState: context.watch().state, + ), + ], + ); + } + + Future _onTap(BuildContext context) async { + if (context.read().canExecuteAction) { + context.read().state = + LunaLoadingState.ACTIVE; + ReadarrAuthorAddDetailsState _state = + context.read(); + await ReadarrAPIController() + .addAuthor( + context: context, + author: _state.series, + qualityProfile: _state.qualityProfile, + metadataProfile: _state.metadataProfile, + rootFolder: _state.rootFolder, + tags: _state.tags, + monitorType: _state.monitorType, + ) + .then((series) async { + context.read().fetchAllAuthors(); + context.read().series.id = series!.id; + Navigator.of(context) + .popAndPushNamed(ReadarrAuthorDetailsRouter().route(series.id!)); + }).catchError((error, stack) { + context.read().state = + LunaLoadingState.ERROR; + }); + context.read().state = + LunaLoadingState.INACTIVE; + } + } +} diff --git a/lib/modules/readarr/routes/add_author_details/widgets/tile_metadata_profile.dart b/lib/modules/readarr/routes/add_author_details/widgets/tile_metadata_profile.dart new file mode 100644 index 0000000000..449613f5b2 --- /dev/null +++ b/lib/modules/readarr/routes/add_author_details/widgets/tile_metadata_profile.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorAddDetailsMetadataProfileTile extends StatelessWidget { + const ReadarrAuthorAddDetailsMetadataProfileTile({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBlock( + title: 'readarr.MetadataProfile'.tr(), + body: [ + TextSpan( + text: context + .watch() + .metadataProfile + .name ?? + LunaUI.TEXT_EMDASH, + ), + ], + trailing: const LunaIconButton.arrow(), + onTap: () async => _onTap(context), + ); + } + + Future _onTap(BuildContext context) async { + List _profiles = + await context.read().metadataProfiles!; + Tuple2 result = + await ReadarrDialogs().editMetadataProfiles(context, _profiles); + if (result.item1) { + context.read().metadataProfile = + result.item2!; + ReadarrDatabaseValue.ADD_SERIES_DEFAULT_LANGUAGE_PROFILE + .put(result.item2!.id); + } + } +} diff --git a/lib/modules/readarr/routes/add_author_details/widgets/tile_monitor.dart b/lib/modules/readarr/routes/add_author_details/widgets/tile_monitor.dart new file mode 100644 index 0000000000..a4c70add58 --- /dev/null +++ b/lib/modules/readarr/routes/add_author_details/widgets/tile_monitor.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorAddDetailsMonitorTile extends StatelessWidget { + const ReadarrAuthorAddDetailsMonitorTile({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBlock( + title: 'readarr.Monitor'.tr(), + body: [ + TextSpan( + text: context + .watch() + .monitorType + .lunaName, + ), + ], + trailing: const LunaIconButton.arrow(), + onTap: () async => _onTap(context), + ); + } + + Future _onTap(BuildContext context) async { + Tuple2 result = + await ReadarrDialogs().editMonitorType(context); + if (result.item1) { + context.read().monitorType = result.item2!; + ReadarrDatabaseValue.ADD_SERIES_DEFAULT_MONITOR_TYPE + .put(result.item2!.value); + } + } +} diff --git a/lib/modules/readarr/routes/add_author_details/widgets/tile_quality_profile.dart b/lib/modules/readarr/routes/add_author_details/widgets/tile_quality_profile.dart new file mode 100644 index 0000000000..b8545e0ca9 --- /dev/null +++ b/lib/modules/readarr/routes/add_author_details/widgets/tile_quality_profile.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorAddDetailsQualityProfileTile extends StatelessWidget { + const ReadarrAuthorAddDetailsQualityProfileTile({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBlock( + title: 'readarr.QualityProfile'.tr(), + body: [ + TextSpan( + text: context + .watch() + .qualityProfile + .name ?? + LunaUI.TEXT_EMDASH, + ), + ], + trailing: const LunaIconButton.arrow(), + onTap: () async => _onTap(context), + ); + } + + Future _onTap(BuildContext context) async { + List _profiles = + await context.read().qualityProfiles!; + Tuple2 result = + await ReadarrDialogs().editQualityProfile(context, _profiles); + if (result.item1) { + context.read().qualityProfile = + result.item2!; + ReadarrDatabaseValue.ADD_SERIES_DEFAULT_QUALITY_PROFILE + .put(result.item2!.id); + } + } +} diff --git a/lib/modules/readarr/routes/add_author_details/widgets/tile_root_folder.dart b/lib/modules/readarr/routes/add_author_details/widgets/tile_root_folder.dart new file mode 100644 index 0000000000..58f9cb7112 --- /dev/null +++ b/lib/modules/readarr/routes/add_author_details/widgets/tile_root_folder.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorAddDetailsRootFolderTile extends StatelessWidget { + const ReadarrAuthorAddDetailsRootFolderTile({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBlock( + title: 'readarr.RootFolder'.tr(), + body: [ + TextSpan( + text: context.watch().rootFolder.path ?? + LunaUI.TEXT_EMDASH, + ), + ], + trailing: const LunaIconButton.arrow(), + onTap: () async => _onTap(context), + ); + } + + Future _onTap(BuildContext context) async { + List _folders = + await context.read().rootFolders!; + Tuple2 result = + await ReadarrDialogs().editRootFolder(context, _folders); + if (result.item1) { + context.read().rootFolder = result.item2!; + ReadarrDatabaseValue.ADD_SERIES_DEFAULT_ROOT_FOLDER.put(result.item2!.id); + } + } +} diff --git a/lib/modules/readarr/routes/add_author_details/widgets/tile_tags.dart b/lib/modules/readarr/routes/add_author_details/widgets/tile_tags.dart new file mode 100644 index 0000000000..7f7a8b6e69 --- /dev/null +++ b/lib/modules/readarr/routes/add_author_details/widgets/tile_tags.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorAddDetailsTagsTile extends StatelessWidget { + const ReadarrAuthorAddDetailsTagsTile({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + List _tags = context.watch().tags; + return LunaBlock( + title: 'readarr.Tags'.tr(), + body: [ + TextSpan( + text: _tags.isEmpty + ? LunaUI.TEXT_EMDASH + : _tags.map((e) => e.label).join(', '), + ), + ], + trailing: const LunaIconButton.arrow(), + onTap: () async => await ReadarrDialogs().setAddTags(context), + ); + } +} diff --git a/lib/modules/readarr/routes/author_details.dart b/lib/modules/readarr/routes/author_details.dart new file mode 100644 index 0000000000..b332825704 --- /dev/null +++ b/lib/modules/readarr/routes/author_details.dart @@ -0,0 +1,3 @@ +export 'author_details/route.dart'; +export 'author_details/state.dart'; +export 'author_details/widgets.dart'; diff --git a/lib/modules/readarr/routes/author_details/route.dart b/lib/modules/readarr/routes/author_details/route.dart new file mode 100644 index 0000000000..fb45d82dcf --- /dev/null +++ b/lib/modules/readarr/routes/author_details/route.dart @@ -0,0 +1,239 @@ +import 'package:collection/collection.dart' show IterableExtension; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorDetailsRouter extends ReadarrPageRouter { + ReadarrAuthorDetailsRouter() : super('/readarr/series/:seriesid'); + + @override + _ReadarrAuthorDetails widget([ + int authorId = -1, + ]) { + return _ReadarrAuthorDetails(authorId: authorId); + } + + @override + Future navigateTo( + BuildContext context, [ + int authorId = -1, + ]) async { + return LunaRouter.router.navigateTo(context, route(authorId)); + } + + @override + String route([int authorId = -1]) { + return fullRoute.replaceFirst( + ':seriesid', + authorId.toString(), + ); + } + + @override + void defineRoute(FluroRouter router) { + super.withParameterRouteDefinition( + router, + (context, params) { + int authorId = (params['seriesid']?.isNotEmpty ?? false) + ? (int.tryParse(params['seriesid']![0]) ?? -1) + : -1; + return _ReadarrAuthorDetails(authorId: authorId); + }, + ); + } +} + +class _ReadarrAuthorDetails extends StatefulWidget { + final int authorId; + + const _ReadarrAuthorDetails({ + Key? key, + required this.authorId, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State<_ReadarrAuthorDetails> with LunaLoadCallbackMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + ReadarrAuthor? author; + PageController? _pageController; + + @override + Future loadCallback() async { + if (widget.authorId > 0) { + ReadarrAuthor? result = + (await context.read().authors)![widget.authorId]; + setState(() => author = result); + context.read().fetchQualityProfiles(); + context.read().fetchMetadataProfiles(); + context.read().fetchTags(); + context.read().fetchAllBooks(); + await context.read().fetchAuthor(widget.authorId); + } + } + + @override + void initState() { + super.initState(); + _pageController = PageController( + initialPage: ReadarrDatabaseValue.NAVIGATION_INDEX_SERIES_DETAILS.data, + ); + } + + List _findTags( + List? tagIds, + List tags, + ) { + return tags.where((tag) => tagIds!.contains(tag.id)).toList(); + } + + ReadarrQualityProfile? _findQualityProfile( + int? qualityProfileId, + List profiles, + ) { + return profiles.firstWhereOrNull( + (profile) => profile.id == qualityProfileId, + ); + } + + ReadarrMetadataProfile? _findMetadataProfile( + int? metadataProfileId, + List profiles, + ) { + return profiles.firstWhereOrNull( + (profile) => profile.id == metadataProfileId, + ); + } + + @override + Widget build(BuildContext context) { + if (widget.authorId <= 0) { + return LunaInvalidRoute( + title: 'readarr.AuthorDetails'.tr(), + message: 'readarr.AuthorNotFound'.tr(), + ); + } + return LunaScaffold( + scaffoldKey: _scaffoldKey, + module: LunaModule.READARR, + appBar: _appBar() as PreferredSizeWidget?, + bottomNavigationBar: + context.watch().enabled ? _bottomNavigationBar() : null, + body: _body(), + ); + } + + Widget _appBar() { + List? _actions = author == null + ? null + : [ + LunaIconButton( + icon: Icons.edit_rounded, + onPressed: () async => ReadarrEditAuthorRouter().navigateTo( + context, + widget.authorId, + ), + ), + ReadarrAppBarSeriesSettingsAction(authorId: widget.authorId), + ]; + return LunaAppBar( + title: 'readarr.AuthorDetails'.tr(), + scrollControllers: ReadarrAuthorDetailsNavigationBar.scrollControllers, + pageController: _pageController, + actions: _actions, + ); + } + + Widget? _bottomNavigationBar() { + if (author == null) return null; + return ReadarrAuthorDetailsNavigationBar( + pageController: _pageController, author: author); + } + + Widget _body() { + return Consumer( + builder: (context, state, _) => FutureBuilder( + future: Future.wait([ + state.qualityProfiles!, + state.metadataProfiles!, + state.tags!, + state.authors!, + state.books! + ]), + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasError) { + if (snapshot.connectionState != ConnectionState.waiting) { + LunaLogger().error( + 'Unable to pull Readarr author details', + snapshot.error, + snapshot.stackTrace, + ); + } + return LunaMessage.error(onTap: loadCallback); + } + if (snapshot.hasData) { + author = + (snapshot.data![3] as Map)[widget.authorId]; + if (author == null) { + return LunaMessage.goBack( + text: 'readarr.AuthorNotFound'.tr(), + context: context, + ); + } + List books = + (snapshot.data![4] as Map) + .values + .where((b) => b.authorId == widget.authorId) + .toList(); + ReadarrQualityProfile? quality = _findQualityProfile( + author!.qualityProfileId, + snapshot.data![0] as List, + ); + ReadarrMetadataProfile? language = _findMetadataProfile( + author!.metadataProfileId, + snapshot.data![1] as List, + ); + List tags = + _findTags(author!.tags, snapshot.data![2] as List); + + return _pages( + qualityProfile: quality, + metadataProfile: language, + tags: tags, + books: books); + } + return const LunaLoader(); + }, + ), + ); + } + + Widget _pages( + {required ReadarrQualityProfile? qualityProfile, + required ReadarrMetadataProfile? metadataProfile, + required List tags, + required List books}) { + return ChangeNotifierProvider( + create: (context) => ReadarrAuthorDetailsState( + context: context, + author: author!, + books: books, + ), + builder: (context, _) => LunaPageView( + controller: _pageController, + children: [ + ReadarrAuthorDetailsOverviewPage( + author: author!, + qualityProfile: qualityProfile, + metadataProfile: metadataProfile, + tags: tags, + ), + ReadarrAuthorDetailsBooksPage(author: author, books: books), + const ReadarrAuthorDetailsHistoryPage(), + ], + ), + ); + } +} diff --git a/lib/modules/readarr/routes/author_details/state.dart b/lib/modules/readarr/routes/author_details/state.dart new file mode 100644 index 0000000000..cbd47bdaec --- /dev/null +++ b/lib/modules/readarr/routes/author_details/state.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorDetailsState extends ChangeNotifier { + final ReadarrAuthor author; + final List books; + + ReadarrAuthorDetailsState({ + required BuildContext context, + required this.author, + required this.books, + }) { + fetchHistory(context); + } + + Future fetchHistory(BuildContext context) async { + ReadarrState state = context.read(); + if (state.enabled) { + _history = state.api!.history.getByAuthor( + authorId: author.id!, + includeBook: true, + ); + } + notifyListeners(); + await _history; + } + + Future>? _history; + Future>? get history => _history; +} diff --git a/lib/modules/readarr/routes/author_details/widgets.dart b/lib/modules/readarr/routes/author_details/widgets.dart new file mode 100644 index 0000000000..996e7d5fff --- /dev/null +++ b/lib/modules/readarr/routes/author_details/widgets.dart @@ -0,0 +1,9 @@ +export 'widgets/appbar_settings_action.dart'; +export 'widgets/navigation_bar.dart'; +export 'widgets/overview_author_description_tile.dart'; +export 'widgets/overview_author_information_block.dart.dart'; +export 'widgets/overview_author_links_section.dart'; +export 'widgets/page_history.dart'; +export 'widgets/page_overview.dart'; +export 'widgets/page_books.dart'; +export 'widgets/book_tile.dart'; diff --git a/lib/modules/readarr/routes/author_details/widgets/appbar_settings_action.dart b/lib/modules/readarr/routes/author_details/widgets/appbar_settings_action.dart new file mode 100644 index 0000000000..67ddc1c9d2 --- /dev/null +++ b/lib/modules/readarr/routes/author_details/widgets/appbar_settings_action.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAppBarSeriesSettingsAction extends StatelessWidget { + final int authorId; + + const ReadarrAppBarSeriesSettingsAction({ + Key? key, + required this.authorId, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, state, _) => FutureBuilder( + future: state.authors, + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasError) return Container(); + if (snapshot.hasData) { + ReadarrAuthor? series = snapshot.data![authorId]; + if (series != null) + return LunaIconButton( + icon: Icons.more_vert_rounded, + onPressed: () async { + Tuple2 values = + await ReadarrDialogs().authorSettings(context, series); + if (values.item1) values.item2!.execute(context, series); + }, + ); + } + return Container(); + }, + ), + ); + } +} diff --git a/lib/modules/readarr/routes/author_details/widgets/book_tile.dart b/lib/modules/readarr/routes/author_details/widgets/book_tile.dart new file mode 100644 index 0000000000..aa5775e357 --- /dev/null +++ b/lib/modules/readarr/routes/author_details/widgets/book_tile.dart @@ -0,0 +1,110 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorDetailsBookTile extends StatefulWidget { + final ReadarrBook book; + final int? authorId; + + const ReadarrAuthorDetailsBookTile({ + Key? key, + required this.book, + required this.authorId, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + LunaLoadingState _loadingState = LunaLoadingState.INACTIVE; + + @override + Widget build(BuildContext context) { + return LunaBlock( + posterPlaceholderIcon: LunaIcons.BOOK, + posterUrl: context.read().getBookCoverURL(widget.book.id), + posterHeaders: context.read().headers, + //title: widget.book.lunaTitle, + title: widget.book.title, + disabled: !widget.book.monitored!, + body: [ + _subtitle1(), + _subtitle2(), + _subtitle3(), + ], + trailing: _trailing(), + onTap: _onTap, + //onLongPress: _onLongPress, + ); + } + + Future _onTap() async => + ReadarrBookDetailsRouter().navigateTo(context, widget.book.id ?? -1); + +/* + Future _onLongPress() async { + Tuple2 result = await ReadarrDialogs() + .seasonSettings(context, widget.book.seasonNumber); + if (result.item1) + result.item2!.execute( + context, + widget.authorId, + widget.book.seasonNumber, + ); + }*/ + + TextSpan _subtitle1() { + return TextSpan( + text: widget.book.releaseDate?.lunaDateReadable(shortMonth: false) ?? + LunaUI.TEXT_EMDASH, + style: const TextStyle( + color: LunaColours.accent, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + ); + } + + TextSpan _subtitle2() { + return TextSpan( + text: [ + (widget.book.pageCount ?? 0) > 0 + ? widget.book.pageCount + : LunaUI.TEXT_EMDASH, + 'readarr.Pages'.tr() + ].join(' ')); + } + + TextSpan _subtitle3() { + return TextSpan( + text: + widget.book.statistics?.sizeOnDisk?.lunaBytesToString(decimals: 1) ?? + LunaUI.TEXT_EMDASH, + ); + } + + Widget _trailing() { + Future setLoadingState(LunaLoadingState state) async { + if (this.mounted) setState(() => _loadingState = state); + } + + return LunaIconButton( + icon: widget.book.monitored! + ? Icons.turned_in_rounded + : Icons.turned_in_not_rounded, + color: LunaColours.white, + loadingState: _loadingState, + onPressed: () async { + setLoadingState(LunaLoadingState.ACTIVE); + await ReadarrAPIController() + .toggleBookMonitored( + context: context, + book: widget.book, + ) + .whenComplete(() => setLoadingState(LunaLoadingState.INACTIVE)); + }, + ); + } +} diff --git a/lib/modules/readarr/routes/author_details/widgets/navigation_bar.dart b/lib/modules/readarr/routes/author_details/widgets/navigation_bar.dart new file mode 100644 index 0000000000..8f0acdf169 --- /dev/null +++ b/lib/modules/readarr/routes/author_details/widgets/navigation_bar.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorDetailsNavigationBar extends StatefulWidget { + static const List icons = [ + Icons.subject_rounded, + Icons.menu_book_rounded, + Icons.history_rounded, + ]; + static final List titles = [ + 'readarr.Overview'.tr(), + 'readarr.Books'.tr(), + 'readarr.History'.tr(), + ]; + static List scrollControllers = + List.generate(icons.length, (_) => ScrollController()); + final PageController? pageController; + final ReadarrAuthor? author; + + const ReadarrAuthorDetailsNavigationBar({ + Key? key, + required this.pageController, + required this.author, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + LunaLoadingState _automaticLoadingState = LunaLoadingState.INACTIVE; + + @override + Widget build(BuildContext context) { + return LunaBottomNavigationBar( + pageController: widget.pageController, + scrollControllers: ReadarrAuthorDetailsNavigationBar.scrollControllers, + icons: ReadarrAuthorDetailsNavigationBar.icons, + titles: ReadarrAuthorDetailsNavigationBar.titles, + topActions: [ + LunaButton( + type: LunaButtonType.TEXT, + text: 'Automatic', + icon: Icons.search_rounded, + onTap: _automatic, + loadingState: _automaticLoadingState, + ), + LunaButton.text( + text: 'Interactive', + icon: Icons.person_rounded, + onTap: _manual, + ), + ], + ); + } + + Future _automatic() async { + setState(() => _automaticLoadingState = LunaLoadingState.ACTIVE); + ReadarrAPIController() + .authorSearch(context: context, author: widget.author!) + .then((value) { + if (mounted) + setState(() { + _automaticLoadingState = + value ? LunaLoadingState.INACTIVE : LunaLoadingState.ERROR; + }); + }); + } + + Future _manual() async => + ReadarrReleasesRouter().navigateTo(context, authorId: widget.author!.id); +} diff --git a/lib/modules/readarr/routes/author_details/widgets/overview_author_description_tile.dart b/lib/modules/readarr/routes/author_details/widgets/overview_author_description_tile.dart new file mode 100644 index 0000000000..3bcf672a99 --- /dev/null +++ b/lib/modules/readarr/routes/author_details/widgets/overview_author_description_tile.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorDetailsOverviewDescriptionTile extends StatelessWidget { + final ReadarrAuthor? series; + + const ReadarrAuthorDetailsOverviewDescriptionTile({ + Key? key, + required this.series, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBlock( + posterPlaceholderIcon: LunaIcons.BOOK, + posterUrl: context.read().getAuthorPosterURL(series!.id), + posterHeaders: context.read().headers, + title: series!.title, + body: [ + LunaTextSpan.extended( + text: series!.overview == null || series!.overview!.isEmpty + ? 'readarr.NoSummaryAvailable'.tr() + : series!.overview, + ), + ], + customBodyMaxLines: 3, + onTap: () async => LunaDialogs().textPreview( + context, + series!.title, + series!.overview!, + ), + ); + } +} diff --git a/lib/modules/readarr/routes/author_details/widgets/overview_author_information_block.dart.dart b/lib/modules/readarr/routes/author_details/widgets/overview_author_information_block.dart.dart new file mode 100644 index 0000000000..d60279266b --- /dev/null +++ b/lib/modules/readarr/routes/author_details/widgets/overview_author_information_block.dart.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorDetailsOverviewInformationBlock extends StatelessWidget { + final ReadarrAuthor? series; + final ReadarrQualityProfile? qualityProfile; + final ReadarrMetadataProfile? metadataProfile; + final List tags; + + const ReadarrAuthorDetailsOverviewInformationBlock({ + Key? key, + required this.series, + required this.qualityProfile, + required this.metadataProfile, + required this.tags, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaTableCard( + content: [ + LunaTableContent( + title: 'readarr.Monitoring'.tr(), + body: (series?.monitored ?? false) ? 'Yes' : 'No', + ), + LunaTableContent( + title: 'path', + body: series?.path, + ), + LunaTableContent( + title: 'quality', + body: qualityProfile?.name, + ), + LunaTableContent( + title: 'metadata', + body: metadataProfile?.name, + ), + LunaTableContent( + title: 'tags', + body: series?.lunaTags(tags), + ), + LunaTableContent(title: '', body: ''), + LunaTableContent( + title: 'status', + body: series?.status?.toTitleCase(), + ), + LunaTableContent( + title: 'added on', + body: series?.lunaDateAdded, + ), + LunaTableContent(title: '', body: ''), + LunaTableContent( + title: 'rating', + body: series?.ratings?.value.toString(), + ), + ], + ); + } +} diff --git a/lib/modules/readarr/routes/author_details/widgets/overview_author_links_section.dart b/lib/modules/readarr/routes/author_details/widgets/overview_author_links_section.dart new file mode 100644 index 0000000000..8f19281257 --- /dev/null +++ b/lib/modules/readarr/routes/author_details/widgets/overview_author_links_section.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorDetailsOverviewLinksSection extends StatelessWidget { + final ReadarrAuthor series; + + const ReadarrAuthorDetailsOverviewLinksSection({ + Key? key, + required this.series, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaButtonContainer( + buttonsPerRow: 1, + children: [ + if (series.foreignAuthorId != null && + series.foreignAuthorId!.isNotEmpty) + LunaCard( + context: context, + child: InkWell( + child: Padding( + child: Image.asset(LunaAssets.serviceGoodreads), + padding: const EdgeInsets.all(12.0), + ), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + onTap: () async => + await series.foreignAuthorId!.lunaOpenGoodreadsAuthor(), + ), + height: 50.0, + margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0), + ), + ], + ); + } +} diff --git a/lib/modules/readarr/routes/author_details/widgets/page_books.dart b/lib/modules/readarr/routes/author_details/widgets/page_books.dart new file mode 100644 index 0000000000..0bc7d8a747 --- /dev/null +++ b/lib/modules/readarr/routes/author_details/widgets/page_books.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorDetailsBooksPage extends StatefulWidget { + final ReadarrAuthor? author; + final List? books; + + const ReadarrAuthorDetailsBooksPage({ + Key? key, + required this.author, + required this.books, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + final _scaffoldKey = GlobalKey(); + final _refreshKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return LunaScaffold( + scaffoldKey: _scaffoldKey, + module: LunaModule.READARR, + hideDrawer: true, + body: _body(), + ); + } + + Widget _body() { + return LunaRefreshIndicator( + key: _refreshKey, + context: context, + onRefresh: () async => context.read().fetchAuthor( + widget.author!.id!, + ), + child: _list(), + ); + } + + Widget _list() { + if (widget.books?.isEmpty ?? true) { + return LunaMessage( + text: 'readarr.NoBooksFound'.tr(), + buttonText: 'lunasea.Refresh'.tr(), + onTap: _refreshKey.currentState!.show, + ); + } + List _books = widget.books!; + _books.sort((a, b) => a.releaseDate!.compareTo(b.releaseDate!)); + return LunaListView( + controller: ReadarrAuthorDetailsNavigationBar.scrollControllers[1], + children: [ + ...List.generate( + _books.length, + (index) => ReadarrAuthorDetailsBookTile( + authorId: widget.author!.id, + book: _books[_books.length - 1 - index], + ), + ), + ], + ); + } +} diff --git a/lib/modules/readarr/routes/author_details/widgets/page_history.dart b/lib/modules/readarr/routes/author_details/widgets/page_history.dart new file mode 100644 index 0000000000..7f07f603f3 --- /dev/null +++ b/lib/modules/readarr/routes/author_details/widgets/page_history.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorDetailsHistoryPage extends StatefulWidget { + const ReadarrAuthorDetailsHistoryPage({ + Key? key, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State + with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = + GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return LunaScaffold( + scaffoldKey: _scaffoldKey, + module: LunaModule.READARR, + hideDrawer: true, + body: _body(), + ); + } + + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async => + context.read().fetchHistory(context), + child: FutureBuilder( + future: context.select>?>((s) => s.history), + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasError) { + LunaLogger().error( + 'Unable to fetch Readarr series history: ${context.read().author.id}', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState!.show); + } + if (snapshot.hasData) return _list(snapshot.data); + return const LunaLoader(); + }, + ), + ); + } + + Widget _list(List? history) { + if ((history?.length ?? 0) == 0) + return LunaMessage( + text: 'readarr.NoHistoryFound'.tr(), + buttonText: 'lunasea.Refresh'.tr(), + onTap: _refreshKey.currentState!.show, + ); + return LunaListViewBuilder( + controller: ReadarrAuthorDetailsNavigationBar.scrollControllers[2], + itemCount: history!.length, + itemBuilder: (context, index) => ReadarrHistoryTile( + history: history[index], + type: ReadarrHistoryTileType.SERIES, + ), + ); + } +} diff --git a/lib/modules/readarr/routes/author_details/widgets/page_overview.dart b/lib/modules/readarr/routes/author_details/widgets/page_overview.dart new file mode 100644 index 0000000000..f66e56cf1d --- /dev/null +++ b/lib/modules/readarr/routes/author_details/widgets/page_overview.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorDetailsOverviewPage extends StatefulWidget { + final ReadarrAuthor author; + final ReadarrQualityProfile? qualityProfile; + final ReadarrMetadataProfile? metadataProfile; + final List tags; + + const ReadarrAuthorDetailsOverviewPage({ + Key? key, + required this.author, + required this.qualityProfile, + required this.metadataProfile, + required this.tags, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State + with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return LunaScaffold( + scaffoldKey: _scaffoldKey, + module: LunaModule.READARR, + hideDrawer: true, + body: Selector>?>( + selector: (_, state) => state.authors, + builder: (context, movies, _) => LunaListView( + controller: ReadarrAuthorDetailsNavigationBar.scrollControllers[0], + children: [ + ReadarrAuthorDetailsOverviewDescriptionTile(series: widget.author), + ReadarrAuthorDetailsOverviewLinksSection(series: widget.author), + ReadarrAuthorDetailsOverviewInformationBlock( + series: widget.author, + qualityProfile: widget.qualityProfile, + metadataProfile: widget.metadataProfile, + tags: widget.tags, + ) + ], + ), + ), + ); + } +} diff --git a/lib/modules/readarr/routes/book_details.dart b/lib/modules/readarr/routes/book_details.dart new file mode 100644 index 0000000000..e71062de9d --- /dev/null +++ b/lib/modules/readarr/routes/book_details.dart @@ -0,0 +1,3 @@ +export 'book_details/route.dart'; +export 'book_details/state.dart'; +export 'book_details/widgets.dart'; diff --git a/lib/modules/readarr/routes/book_details/route.dart b/lib/modules/readarr/routes/book_details/route.dart new file mode 100644 index 0000000000..1173249685 --- /dev/null +++ b/lib/modules/readarr/routes/book_details/route.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrBookDetailsRouter extends ReadarrPageRouter { + ReadarrBookDetailsRouter() : super('/readarr/book/:bookid'); + + @override + Widget widget([ + int bookId = -1, + ]) { + return _Widget(bookId: bookId); + } + + @override + Future navigateTo( + BuildContext context, [ + int bookId = -1, + ]) async { + LunaRouter.router.navigateTo(context, route(bookId)); + } + + @override + String route([ + int bookId = -1, + ]) { + return fullRoute.replaceFirst(':bookid', bookId.toString()); + } + + @override + void defineRoute(FluroRouter router) { + super.withParameterRouteDefinition( + router, + (context, params) { + int bookId = params['bookid'] == null || params['bookid']!.isEmpty + ? -1 + : (int.tryParse(params['bookid']![0]) ?? -1); + return _Widget(bookId: bookId); + }, + ); + } +} + +class _Widget extends StatefulWidget { + final int bookId; + + const _Widget({ + Key? key, + required this.bookId, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State<_Widget> with LunaLoadCallbackMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + ReadarrBook? book; + PageController? _pageController; + + @override + Future loadCallback() async { + if (widget.bookId > 0) { + ReadarrBook? result = + (await context.read().books)![widget.bookId]; + setState(() => book = result); + context.read().fetchQualityProfiles(); + context.read().fetchTags(); + //await context.read().resetSingleBook(widget.bookId); + } + } + + @override + void initState() { + super.initState(); + _pageController = PageController( + initialPage: ReadarrDatabaseValue.NAVIGATION_INDEX_SEASON_DETAILS.data, + ); + } + + @override + Widget build(BuildContext context) { + if (widget.bookId <= 0) + return LunaInvalidRoute( + title: 'Book Details', + message: 'Book Not Found', + ); + return LunaScaffold( + scaffoldKey: _scaffoldKey, + module: LunaModule.RADARR, + appBar: _appBar(), + bottomNavigationBar: + context.watch().enabled ? _bottomNavigationBar() : null, + body: _body(), + ); + } + + PreferredSizeWidget _appBar() { + List? _actions = book == null + ? null + : [ + LunaIconButton( + iconSize: LunaUI.ICON_SIZE, + icon: Icons.edit_rounded, + //onPressed: () async => + // ReadarrBooksEditRouter().navigateTo(context, widget.bookId), + ), + ReadarrAppBarBookSettingsAction(bookId: widget.bookId), + ]; + return LunaAppBar( + pageController: _pageController, + scrollControllers: ReadarrBookDetailsNavigationBar.scrollControllers, + title: 'Book Details', + actions: _actions, + ); + } + + Widget? _bottomNavigationBar() { + if (book == null) return null; + return ReadarrBookDetailsNavigationBar( + pageController: _pageController, + book: book, + ); + } + + Widget _body() { + return Consumer( + builder: (context, state, _) => FutureBuilder( + future: Future.wait([ + state.qualityProfiles!, + state.tags!, + state.books!, + ]), + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasError) { + if (snapshot.connectionState != ConnectionState.waiting) + LunaLogger().error( + 'Unable to pull Readarr book details', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: loadCallback); + } + if (snapshot.hasData) { + book = (snapshot.data![2] as Map)[widget.bookId]; + if (book == null) + return LunaMessage.goBack( + text: 'Book Not Found', + context: context, + ); + return _pages(); + } + return const LunaLoader(); + }, + ), + ); + } + + Widget _pages() { + return ChangeNotifierProvider( + create: (context) => + ReadarrBookDetailsState(context: context, book: book!), + builder: (context, _) => LunaPageView( + controller: _pageController, + children: [ + ReadarrBookDetailsOverviewPage( + book: book, + ), + const ReadarrBookDetailsFilesPage(), + ReadarrBookDetailsHistoryPage(movie: book), + ], + ), + ); + } +} diff --git a/lib/modules/readarr/routes/book_details/state.dart b/lib/modules/readarr/routes/book_details/state.dart new file mode 100644 index 0000000000..014bed7d7a --- /dev/null +++ b/lib/modules/readarr/routes/book_details/state.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrBookDetailsState extends ChangeNotifier { + final ReadarrBook book; + late Future> history; + late Future> movieFiles; + + ReadarrBookDetailsState({ + required BuildContext context, + required this.book, + }) { + fetchFiles(context); + fetchHistory(context); + } + + Future fetchHistory(BuildContext context) async { + ReadarrState state = context.read(); + if (state.enabled) { + history = state.api!.history + .getByAuthor(authorId: book.authorId!, bookId: book.id!); + } + notifyListeners(); + await history; + } + + Future fetchFiles(BuildContext context) async { + ReadarrState state = context.read(); + if (state.enabled) { + movieFiles = state.api!.bookFile.get(bookId: book.id!); + } + notifyListeners(); + await Future.wait([ + movieFiles, + ]); + } +} diff --git a/lib/modules/readarr/routes/book_details/widgets.dart b/lib/modules/readarr/routes/book_details/widgets.dart new file mode 100644 index 0000000000..2c981076bb --- /dev/null +++ b/lib/modules/readarr/routes/book_details/widgets.dart @@ -0,0 +1,9 @@ +export 'widgets/appbar_settings_action.dart'; +export 'widgets/files_file_block.dart'; +export 'widgets/navigation_bar.dart'; +export 'widgets/overview_movie_description_tile.dart'; +export 'widgets/overview_movie_information_block.dart'; +export 'widgets/overview_movie_links_section.dart'; +export 'widgets/page_files.dart'; +export 'widgets/page_history.dart'; +export 'widgets/page_overview.dart'; diff --git a/lib/modules/readarr/routes/book_details/widgets/appbar_settings_action.dart b/lib/modules/readarr/routes/book_details/widgets/appbar_settings_action.dart new file mode 100644 index 0000000000..21ffdcfed7 --- /dev/null +++ b/lib/modules/readarr/routes/book_details/widgets/appbar_settings_action.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAppBarBookSettingsAction extends StatelessWidget { + final int bookId; + + const ReadarrAppBarBookSettingsAction({ + Key? key, + required this.bookId, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, state, _) => FutureBuilder( + future: state.books, + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasError) return Container(); + if (snapshot.hasData) { + ReadarrBook? movie = snapshot.data![bookId]; + if (movie != null) + return LunaIconButton( + icon: Icons.more_vert_rounded, + iconSize: LunaUI.ICON_SIZE, + onPressed: () async { + Tuple2 values = + await ReadarrDialogs().bookSettings(context, movie); + if (values.item1) values.item2!.execute(context, movie); + }, + ); + } + return Container(); + }, + ), + ); + } +} diff --git a/lib/modules/readarr/routes/book_details/widgets/files_file_block.dart b/lib/modules/readarr/routes/book_details/widgets/files_file_block.dart new file mode 100644 index 0000000000..1f74aed4a0 --- /dev/null +++ b/lib/modules/readarr/routes/book_details/widgets/files_file_block.dart @@ -0,0 +1,148 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrBookDetailsFilesFileBlock extends StatefulWidget { + final ReadarrBookFile file; + + const ReadarrBookDetailsFilesFileBlock({ + Key? key, + required this.file, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + LunaLoadingState _deleteFileState = LunaLoadingState.INACTIVE; + + @override + Widget build(BuildContext context) { + return LunaTableCard( + content: [ + LunaTableContent( + title: 'path', + body: widget.file.path, + ), + LunaTableContent( + title: 'size', + body: widget.file.lunaSize, + ), + LunaTableContent( + title: 'quality', + body: widget.file.lunaQuality, + ), + LunaTableContent( + title: 'added on', + body: widget.file.lunaDateAdded, + ), + ], + buttons: [ +/* if (widget.file.mediaInfo != null) + LunaButton.text( + text: 'Media Info', + icon: Icons.info_outline_rounded, + onTap: () async => _viewMediaInfo(), + ),*/ + LunaButton( + type: LunaButtonType.TEXT, + text: 'Delete', + icon: Icons.delete_rounded, + onTap: () async => _deleteBookFile(), + color: LunaColours.red, + loadingState: _deleteFileState, + ), + ], + ); + } + + Future _deleteBookFile() async { + setState(() => _deleteFileState = LunaLoadingState.ACTIVE); + bool result = await ReadarrDialogs().deleteBookFile(context); + if (result) { + bool execute = await ReadarrAPIController() + .deleteBookFile(context: context, bookFile: widget.file); + if (execute) context.read().fetchFiles(context); + } + setState(() => _deleteFileState = LunaLoadingState.INACTIVE); + } + +/* + Future _viewMediaInfo() async { + LunaBottomModalSheet().show( + context: context, + builder: (context) => LunaListViewModal( + children: [ + const LunaHeader(text: 'Video'), + LunaTableCard( + content: [ + LunaTableContent( + title: 'bit depth', + body: widget.file.mediaInfo?.lunaVideoBitDepth, + ), + LunaTableContent( + title: 'bitrate', + body: widget.file.mediaInfo?.lunaVideoBitrate, + ), + LunaTableContent( + title: 'codec', + body: widget.file.mediaInfo?.lunaVideoCodec, + ), + LunaTableContent( + title: 'fps', + body: widget.file.mediaInfo?.lunaVideoFps, + ), + LunaTableContent( + title: 'resolution', + body: widget.file.mediaInfo?.lunaVideoResolution, + ), + ], + ), + const LunaHeader(text: 'Audio'), + LunaTableCard( + content: [ + LunaTableContent( + title: 'bitrate', + body: widget.file.mediaInfo?.lunaAudioBitrate, + ), + LunaTableContent( + title: 'channels', + body: widget.file.mediaInfo?.lunaAudioChannels, + ), + LunaTableContent( + title: 'codec', + body: widget.file.mediaInfo?.lunaAudioCodec, + ), + LunaTableContent( + title: 'features', + body: widget.file.mediaInfo?.lunaAudioAdditionalFeatures, + ), + LunaTableContent( + title: 'languages', + body: widget.file.mediaInfo?.lunaAudioLanguages, + ), + LunaTableContent( + title: 'streams', + body: widget.file.mediaInfo?.lunaAudioStreamCount, + ), + ], + ), + const LunaHeader(text: 'Other'), + LunaTableCard( + content: [ + LunaTableContent( + title: 'runtime', + body: widget.file.mediaInfo?.lunaRunTime, + ), + LunaTableContent( + title: 'subtitles', + body: widget.file.mediaInfo?.lunaSubtitles, + ), + ], + ), + ], + ), + ); + }*/ +} diff --git a/lib/modules/readarr/routes/book_details/widgets/navigation_bar.dart b/lib/modules/readarr/routes/book_details/widgets/navigation_bar.dart new file mode 100644 index 0000000000..cd24ea8507 --- /dev/null +++ b/lib/modules/readarr/routes/book_details/widgets/navigation_bar.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrBookDetailsNavigationBar extends StatefulWidget { + static const List icons = [ + Icons.subject_rounded, + Icons.insert_drive_file_outlined, + Icons.history_rounded, + ]; + static final List titles = [ + 'readarr.Overview'.tr(), + 'readarr.Files'.tr(), + 'readarr.History'.tr(), + ]; + static List scrollControllers = + List.generate(icons.length, (_) => ScrollController()); + final PageController? pageController; + final ReadarrBook? book; + + const ReadarrBookDetailsNavigationBar({ + Key? key, + required this.pageController, + required this.book, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + LunaLoadingState _automaticLoadingState = LunaLoadingState.INACTIVE; + + @override + Widget build(BuildContext context) { + return LunaBottomNavigationBar( + pageController: widget.pageController, + scrollControllers: ReadarrBookDetailsNavigationBar.scrollControllers, + icons: ReadarrBookDetailsNavigationBar.icons, + titles: ReadarrBookDetailsNavigationBar.titles, + topActions: [ + LunaButton( + type: LunaButtonType.TEXT, + text: 'Automatic', + icon: Icons.search_rounded, + onTap: _automatic, + loadingState: _automaticLoadingState, + ), + LunaButton.text( + text: 'Interactive', + icon: Icons.person_rounded, + onTap: _manual, + ), + ], + ); + } + + Future _automatic() async { + setState(() => _automaticLoadingState = LunaLoadingState.ACTIVE); + ReadarrAPIController() + .bookSearch(context: context, book: widget.book!) + .then((value) { + if (mounted) + setState(() { + _automaticLoadingState = + value ? LunaLoadingState.INACTIVE : LunaLoadingState.ERROR; + }); + }); + } + + Future _manual() async => + ReadarrReleasesRouter().navigateTo(context, bookId: widget.book!.id); +} diff --git a/lib/modules/readarr/routes/book_details/widgets/overview_movie_description_tile.dart b/lib/modules/readarr/routes/book_details/widgets/overview_movie_description_tile.dart new file mode 100644 index 0000000000..8663ae452e --- /dev/null +++ b/lib/modules/readarr/routes/book_details/widgets/overview_movie_description_tile.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrBookDetailsOverviewDescriptionTile extends StatelessWidget { + final ReadarrBook? book; + + const ReadarrBookDetailsOverviewDescriptionTile({ + Key? key, + required this.book, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBlock( + posterPlaceholderIcon: LunaIcons.VIDEO_CAM, + backgroundUrl: + context.read().getAuthorPosterURL(book!.authorId), + posterUrl: context.read().getBookCoverURL(book!.id), + posterHeaders: context.read().headers, + title: book!.title, + body: [ + LunaTextSpan.extended( + text: book!.overview == null || book!.overview!.isEmpty + ? 'sonarr.NoSummaryAvailable'.tr() + : book!.overview, + ), + ], + customBodyMaxLines: 3, + onTap: () async => + LunaDialogs().textPreview(context, book!.title, book!.overview!), + ); + } +} diff --git a/lib/modules/readarr/routes/book_details/widgets/overview_movie_information_block.dart b/lib/modules/readarr/routes/book_details/widgets/overview_movie_information_block.dart new file mode 100644 index 0000000000..99e8fdd472 --- /dev/null +++ b/lib/modules/readarr/routes/book_details/widgets/overview_movie_information_block.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrBookDetailsOverviewInformationBlock extends StatelessWidget { + final ReadarrBook? book; + + const ReadarrBookDetailsOverviewInformationBlock({ + Key? key, + required this.book, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaTableCard( + content: [ + LunaTableContent( + title: 'monitoring', + body: (book?.monitored ?? false) ? 'Yes' : 'No', + ), + LunaTableContent(title: 'added on', body: book?.lunaReleaseDate()), + LunaTableContent(title: '', body: ''), + LunaTableContent(title: 'genres', body: book?.lunaGenres), + ], + ); + } +} diff --git a/lib/modules/readarr/routes/book_details/widgets/overview_movie_links_section.dart b/lib/modules/readarr/routes/book_details/widgets/overview_movie_links_section.dart new file mode 100644 index 0000000000..08037d712c --- /dev/null +++ b/lib/modules/readarr/routes/book_details/widgets/overview_movie_links_section.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrBookDetailsOverviewLinksSection extends StatelessWidget { + final ReadarrBook? movie; + + const ReadarrBookDetailsOverviewLinksSection({ + Key? key, + required this.movie, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaButtonContainer( + buttonsPerRow: 1, + children: [ + if (movie!.foreignBookId != null && movie!.foreignBookId!.isNotEmpty) + LunaCard( + context: context, + child: InkWell( + child: Padding( + child: Image.asset(LunaAssets.serviceGoodreads), + padding: const EdgeInsets.all(12.0), + ), + borderRadius: BorderRadius.circular(LunaUI.BORDER_RADIUS), + onTap: () async => + await movie?.foreignBookId?.lunaOpenGoodreadsBook(), + ), + height: 50.0, + margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 6.0), + ), + ], + ); + } +} diff --git a/lib/modules/readarr/routes/book_details/widgets/page_files.dart b/lib/modules/readarr/routes/book_details/widgets/page_files.dart new file mode 100644 index 0000000000..67e6ed1902 --- /dev/null +++ b/lib/modules/readarr/routes/book_details/widgets/page_files.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrBookDetailsFilesPage extends StatefulWidget { + const ReadarrBookDetailsFilesPage({ + Key? key, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State + with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = + GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return LunaScaffold( + module: LunaModule.RADARR, + hideDrawer: true, + scaffoldKey: _scaffoldKey, + body: _body(), + ); + } + + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async => + context.read().fetchFiles(context), + child: FutureBuilder( + future: Future.wait([ + context.watch().movieFiles, + ]), + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasError) { + LunaLogger().error( + 'Unable to fetch Readarr files: ${context.read().book.id}', + snapshot.error, + snapshot.stackTrace, + ); + return LunaMessage.error(onTap: _refreshKey.currentState!.show); + } + if (snapshot.hasData) { + return _list( + bookFiles: snapshot.requireData[0] as List, + ); + } + return const LunaLoader(); + }, + ), + ); + } + + Widget _list({ + required List bookFiles, + }) { + if (bookFiles.isEmpty) { + return LunaMessage( + text: 'No Files Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState!.show, + ); + } + return LunaListView( + controller: ReadarrBookDetailsNavigationBar.scrollControllers[1], + children: [ + if (bookFiles.isNotEmpty) ..._filesTiles(bookFiles), + ], + ); + } + + List _filesTiles(List movieFiles) { + return List.generate( + movieFiles.length, + (idx) => ReadarrBookDetailsFilesFileBlock(file: movieFiles[idx]), + ); + } +} diff --git a/lib/modules/readarr/routes/book_details/widgets/page_history.dart b/lib/modules/readarr/routes/book_details/widgets/page_history.dart new file mode 100644 index 0000000000..05b9743b36 --- /dev/null +++ b/lib/modules/readarr/routes/book_details/widgets/page_history.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrBookDetailsHistoryPage extends StatefulWidget { + final ReadarrBook? movie; + + const ReadarrBookDetailsHistoryPage({ + Key? key, + required this.movie, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State + with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = + GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return LunaScaffold( + module: LunaModule.RADARR, + hideDrawer: true, + scaffoldKey: _scaffoldKey, + body: _body(), + ); + } + + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async => + context.read().fetchHistory(context), + child: FutureBuilder( + future: context.watch().history, + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasError) { + LunaLogger().error( + 'Unable to fetch Readarr movie history: ${widget.movie!.id}', + snapshot.error, + snapshot.stackTrace); + return LunaMessage.error(onTap: _refreshKey.currentState!.show); + } + if (snapshot.hasData) return _list(snapshot.data); + return const LunaLoader(); + }, + ), + ); + } + + Widget _list(List? history) { + if ((history?.length ?? 0) == 0) + return LunaMessage( + text: 'No History Found', + buttonText: 'Refresh', + onTap: _refreshKey.currentState!.show, + ); + return LunaListViewBuilder( + controller: ReadarrBookDetailsNavigationBar.scrollControllers[2], + itemCount: history!.length, + itemBuilder: (context, index) => ReadarrHistoryTile( + history: history[index], type: ReadarrHistoryTileType.EPISODE), + ); + } +} diff --git a/lib/modules/readarr/routes/book_details/widgets/page_overview.dart b/lib/modules/readarr/routes/book_details/widgets/page_overview.dart new file mode 100644 index 0000000000..c726bc433e --- /dev/null +++ b/lib/modules/readarr/routes/book_details/widgets/page_overview.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrBookDetailsOverviewPage extends StatefulWidget { + final ReadarrBook? book; + + const ReadarrBookDetailsOverviewPage({ + Key? key, + required this.book, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State + with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return LunaScaffold( + module: LunaModule.RADARR, + hideDrawer: true, + scaffoldKey: _scaffoldKey, + body: Selector>?>( + selector: (_, state) => state.books, + builder: (context, movies, _) => LunaListView( + controller: ReadarrBookDetailsNavigationBar.scrollControllers[0], + children: [ + ReadarrBookDetailsOverviewDescriptionTile(book: widget.book), + ReadarrBookDetailsOverviewLinksSection(movie: widget.book), + ReadarrBookDetailsOverviewInformationBlock( + book: widget.book, + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/readarr/routes/catalogue.dart b/lib/modules/readarr/routes/catalogue.dart new file mode 100644 index 0000000000..d07bf1a9c4 --- /dev/null +++ b/lib/modules/readarr/routes/catalogue.dart @@ -0,0 +1,2 @@ +export 'catalogue/route.dart'; +export 'catalogue/widgets.dart'; diff --git a/lib/modules/readarr/routes/catalogue/route.dart b/lib/modules/readarr/routes/catalogue/route.dart new file mode 100644 index 0000000000..1b86dbacef --- /dev/null +++ b/lib/modules/readarr/routes/catalogue/route.dart @@ -0,0 +1,212 @@ +import 'dart:math'; + +import 'package:collection/collection.dart' show IterableExtension; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrCatalogueRoute extends StatefulWidget { + const ReadarrCatalogueRoute({ + Key? key, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State + with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = + GlobalKey(); + + @override + bool get wantKeepAlive => true; + + Future _refresh() async { + ReadarrState _state = context.read(); + _state.fetchAllAuthors(); + _state.fetchQualityProfiles(); + _state.fetchMetadataProfiles(); + _state.fetchTags(); + + await Future.wait([ + _state.authors!, + _state.qualityProfiles!, + _state.tags!, + _state.metadataProfiles!, + ]); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return LunaScaffold( + scaffoldKey: _scaffoldKey, + module: LunaModule.READARR, + hideDrawer: true, + body: _body(), + appBar: _appBar() as PreferredSizeWidget?, + ); + } + + Widget _appBar() { + return LunaAppBar.empty( + child: ReadarrAuthorSearchBar( + scrollController: ReadarrNavigationBar.scrollControllers[0], + ), + height: LunaTextInputBar.defaultAppBarHeight, + ); + } + + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: _refresh, + child: Selector< + ReadarrState, + Tuple2>?, + Future>?>>( + selector: (_, state) => Tuple2( + state.authors, + state.qualityProfiles, + ), + builder: (context, tuple, _) => FutureBuilder( + future: Future.wait([ + tuple.item1!, + tuple.item2!, + ]), + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasError) { + if (snapshot.connectionState != ConnectionState.waiting) { + LunaLogger().error( + 'Unable to fetch Readarr series', + snapshot.error, + snapshot.stackTrace, + ); + } + return LunaMessage.error( + onTap: _refreshKey.currentState!.show, + ); + } + if (snapshot.hasData) { + return _series( + snapshot.data![0] as Map, + snapshot.data![1] as List, + ); + } + return const LunaLoader(); + }, + ), + ), + ); + } + + List _filterAndSort( + Map series, + List profiles, + String query, + ) { + if (series.isEmpty) return []; + ReadarrAuthorSorting sorting = context.watch().seriesSortType; + ReadarrAuthorFilter filter = context.watch().seriesFilterType; + bool ascending = context.watch().seriesSortAscending; + // Filter + List filtered = series.values.where((show) { + if (query.isNotEmpty && show.id != null) + return show.title!.toLowerCase().contains(query.toLowerCase()); + return show.id != null; + }).toList(); + filtered = filter.filter(filtered); + // Sort + filtered = sorting.sort(filtered, ascending); + return filtered; + } + + Widget _series( + Map series, + List qualities, + ) { + if (series.isEmpty) + return LunaMessage( + text: 'readarr.NoSeriesFound'.tr(), + buttonText: 'lunasea.Refresh'.tr(), + onTap: _refreshKey.currentState!.show, + ); + return Selector( + selector: (_, state) => state.seriesSearchQuery, + builder: (context, query, _) { + List _filtered = + _filterAndSort(series, qualities, query); + if (_filtered.isEmpty) + return LunaListView( + controller: ReadarrNavigationBar.scrollControllers[0], + children: [ + LunaMessage.inList(text: 'readarr.NoSeriesFound'.tr()), + if (query.isNotEmpty) + LunaButtonContainer( + children: [ + LunaButton.text( + icon: null, + text: query.length > 20 + ? 'readarr.SearchFor'.tr(args: [ + '"${query.substring(0, min(20, query.length))}${LunaUI.TEXT_ELLIPSIS}"' + ]) + : 'readarr.SearchFor'.tr(args: ['"$query"']), + backgroundColor: LunaColours.accent, + onTap: () async => ReadarrAddSeriesRouter().navigateTo( + context, + query, + ), + ), + ], + ), + ], + ); + switch (context.read().seriesViewType) { + case LunaListViewOption.BLOCK_VIEW: + return _blockView(_filtered, qualities); + case LunaListViewOption.GRID_VIEW: + return _gridView(_filtered, qualities); + default: + throw Exception('Invalid moviesViewType'); + } + }, + ); + } + + Widget _blockView( + List series, + List qualities, + ) { + return LunaListViewBuilder( + controller: ReadarrNavigationBar.scrollControllers[0], + itemCount: series.length, + itemExtent: ReadarrAuthorTile.itemExtent, + itemBuilder: (context, index) => ReadarrAuthorTile( + series: series[index], + profile: qualities.firstWhereOrNull( + (element) => element.id == series[index].qualityProfileId, + ), + ), + ); + } + + Widget _gridView( + List series, + List qualities, + ) { + return LunaGridViewBuilder( + controller: ReadarrNavigationBar.scrollControllers[0], + sliverGridDelegate: LunaGridBlock.getMaxCrossAxisExtent(), + itemCount: series.length, + itemBuilder: (context, index) => ReadarrAuthorTile.grid( + series: series[index], + profile: qualities.firstWhereOrNull( + (element) => element.id == series[index].qualityProfileId, + ), + ), + ); + } +} diff --git a/lib/modules/readarr/routes/catalogue/widgets.dart b/lib/modules/readarr/routes/catalogue/widgets.dart new file mode 100644 index 0000000000..3309455d96 --- /dev/null +++ b/lib/modules/readarr/routes/catalogue/widgets.dart @@ -0,0 +1,5 @@ +export 'widgets/search_bar.dart'; +export 'widgets/search_bar_filter_button.dart'; +export 'widgets/search_bar_sort_button.dart'; +export 'widgets/search_bar_view_button.dart'; +export 'widgets/author_tile.dart'; diff --git a/lib/modules/readarr/routes/catalogue/widgets/author_tile.dart b/lib/modules/readarr/routes/catalogue/widgets/author_tile.dart new file mode 100644 index 0000000000..ac099d501d --- /dev/null +++ b/lib/modules/readarr/routes/catalogue/widgets/author_tile.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +enum _ReadarrAuthorTileType { + TILE, + GRID, +} + +class ReadarrAuthorTile extends StatefulWidget { + static final itemExtent = LunaBlock.calculateItemExtent(3); + + final ReadarrAuthor series; + final ReadarrQualityProfile? profile; + final _ReadarrAuthorTileType type; + + const ReadarrAuthorTile({ + Key? key, + required this.series, + required this.profile, + this.type = _ReadarrAuthorTileType.TILE, + }) : super(key: key); + + const ReadarrAuthorTile.grid({ + Key? key, + required this.series, + required this.profile, + this.type = _ReadarrAuthorTileType.GRID, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return Selector>?>( + selector: (_, state) => state.authors, + builder: (context, series, _) { + switch (widget.type) { + case _ReadarrAuthorTileType.TILE: + return _buildBlockTile(); + case _ReadarrAuthorTileType.GRID: + return _buildGridTile(); + default: + throw Exception('Invalid _ReadarrAuthorTileType'); + } + }, + ); + } + + Widget _buildBlockTile() { + return LunaBlock( + posterUrl: + context.read().getAuthorPosterURL(widget.series.id), + posterHeaders: context.read().headers, + posterPlaceholderIcon: LunaIcons.BOOK, + disabled: !widget.series.monitored!, + title: widget.series.title, + body: [ + _subtitle1(), + _subtitle2(), + _subtitle3(), + ], + onTap: _onTap, + onLongPress: _onLongPress, + ); + } + + Widget _buildGridTile() { + ReadarrAuthorSorting _sorting = context.read().seriesSortType; + return LunaGridBlock( + key: ObjectKey(widget.series), + posterUrl: + context.read().getAuthorPosterURL(widget.series.id), + posterHeaders: context.read().headers, + backgroundHeaders: context.read().headers, + posterPlaceholderIcon: LunaIcons.BOOK, + title: widget.series.title, + subtitle: TextSpan(text: _sorting.value(widget.series, widget.profile)), + disabled: !widget.series.monitored!, + onTap: _onTap, + onLongPress: _onLongPress, + ); + } + + TextSpan _buildChildTextSpan(String? text, ReadarrAuthorSorting sorting) { + TextStyle? style; + if (context.read().seriesSortType == sorting) { + style = const TextStyle( + color: LunaColours.accent, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + fontSize: LunaUI.FONT_SIZE_H3, + ); + } + return TextSpan( + text: text, + style: style, + ); + } + + TextSpan _subtitle1() { + return TextSpan( + children: [ + _buildChildTextSpan( + widget.series.lunaEpisodeCount, + ReadarrAuthorSorting.EPISODES, + ), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + _buildChildTextSpan( + widget.series.lunaSizeOnDisk, + ReadarrAuthorSorting.SIZE, + ), + ], + ); + } + + TextSpan _subtitle2() { + return TextSpan( + children: [ + _buildChildTextSpan( + widget.profile?.name ?? LunaUI.TEXT_EMDASH, + ReadarrAuthorSorting.QUALITY, + ), + ], + ); + } + + TextSpan _subtitle3() { + ReadarrAuthorSorting _sorting = context.read().seriesSortType; + return TextSpan( + children: [ + if (_sorting == ReadarrAuthorSorting.DATE_ADDED) + _buildChildTextSpan( + widget.series.lunaDateAdded, + ReadarrAuthorSorting.DATE_ADDED, + ), + ], + ); + } + + Future _onTap() async => ReadarrAuthorDetailsRouter().navigateTo( + context, + widget.series.id!, + ); + + Future _onLongPress() async { + Tuple2 values = + await ReadarrDialogs().authorSettings( + context, + widget.series, + ); + if (values.item1) values.item2!.execute(context, widget.series); + } +} diff --git a/lib/modules/readarr/routes/catalogue/widgets/search_bar.dart b/lib/modules/readarr/routes/catalogue/widgets/search_bar.dart new file mode 100644 index 0000000000..a875951f4f --- /dev/null +++ b/lib/modules/readarr/routes/catalogue/widgets/search_bar.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorSearchBar extends StatefulWidget { + final ScrollController scrollController; + + const ReadarrAuthorSearchBar({ + Key? key, + required this.scrollController, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + final TextEditingController _controller = TextEditingController(); + final FocusNode _focusNode = FocusNode(); + bool _hasFocus = false; + + @override + void initState() { + super.initState(); + _controller.text = context.read().seriesSearchQuery; + _focusNode.addListener(_handleFocus); + } + + void _handleFocus() { + if (_focusNode.hasPrimaryFocus != _hasFocus) + setState(() => _hasFocus = _focusNode.hasPrimaryFocus); + } + + @override + Widget build(BuildContext context) { + ScrollController _sc = widget.scrollController; + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: Consumer( + builder: (context, state, _) => LunaTextInputBar( + controller: _controller, + scrollController: _sc, + focusNode: _focusNode, + autofocus: false, + onChanged: (value) => + context.read().seriesSearchQuery = value, + margin: EdgeInsets.zero, + ), + ), + ), + AnimatedContainer( + duration: const Duration( + milliseconds: LunaUI.ANIMATION_SPEED_SCROLLING, + ), + curve: Curves.easeInOutQuart, + width: _hasFocus + ? 0.0 + : (LunaTextInputBar.defaultHeight * 3 + + LunaUI.DEFAULT_MARGIN_SIZE * 3), + child: Row( + children: [ + Flexible( + child: ReadarrAuthorSearchBarFilterButton(controller: _sc), + ), + Flexible( + child: ReadarrAuthorSearchBarSortButton(controller: _sc), + ), + Flexible( + child: ReadarrAuthorSearchBarViewButton(controller: _sc), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/modules/readarr/routes/catalogue/widgets/search_bar_filter_button.dart b/lib/modules/readarr/routes/catalogue/widgets/search_bar_filter_button.dart new file mode 100644 index 0000000000..b8f2164f49 --- /dev/null +++ b/lib/modules/readarr/routes/catalogue/widgets/search_bar_filter_button.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorSearchBarFilterButton extends StatefulWidget { + final ScrollController controller; + + const ReadarrAuthorSearchBarFilterButton({ + Key? key, + required this.controller, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) => LunaCard( + context: context, + child: Consumer( + builder: (context, state, _) => + LunaPopupMenuButton( + tooltip: 'readarr.FilterCatalogue'.tr(), + icon: Icons.filter_list_rounded, + onSelected: (result) { + state.seriesFilterType = result; + widget.controller.lunaAnimateToStart(); + }, + itemBuilder: (context) => + List>.generate( + ReadarrAuthorFilter.values.length, + (index) => PopupMenuItem( + value: ReadarrAuthorFilter.values[index], + child: Text( + ReadarrAuthorFilter.values[index].readable, + style: TextStyle( + fontSize: LunaUI.FONT_SIZE_H3, + color: state.seriesFilterType == + ReadarrAuthorFilter.values[index] + ? LunaColours.accent + : Colors.white, + ), + ), + ), + ), + ), + ), + height: LunaTextInputBar.defaultHeight, + width: LunaTextInputBar.defaultHeight, + margin: const EdgeInsets.only(left: LunaUI.DEFAULT_MARGIN_SIZE), + color: Theme.of(context).canvasColor, + ); +} diff --git a/lib/modules/readarr/routes/catalogue/widgets/search_bar_sort_button.dart b/lib/modules/readarr/routes/catalogue/widgets/search_bar_sort_button.dart new file mode 100644 index 0000000000..f0817d34b1 --- /dev/null +++ b/lib/modules/readarr/routes/catalogue/widgets/search_bar_sort_button.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorSearchBarSortButton extends StatefulWidget { + final ScrollController controller; + + const ReadarrAuthorSearchBarSortButton({ + Key? key, + required this.controller, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) => LunaCard( + context: context, + child: Consumer( + builder: (context, state, _) => + LunaPopupMenuButton( + tooltip: 'readarr.SortCatalogue'.tr(), + icon: Icons.sort_rounded, + onSelected: (result) { + if (state.seriesSortType == result) { + state.seriesSortAscending = !state.seriesSortAscending; + } else { + state.seriesSortAscending = true; + state.seriesSortType = result; + } + widget.controller.lunaAnimateToStart(); + }, + itemBuilder: (context) => + List>.generate( + ReadarrAuthorSorting.values.length, + (index) => PopupMenuItem( + value: ReadarrAuthorSorting.values[index], + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + ReadarrAuthorSorting.values[index].readable, + style: TextStyle( + fontSize: LunaUI.FONT_SIZE_H3, + color: state.seriesSortType == + ReadarrAuthorSorting.values[index] + ? LunaColours.accent + : Colors.white, + ), + ), + if (state.seriesSortType == + ReadarrAuthorSorting.values[index]) + Icon( + state.seriesSortAscending + ? Icons.arrow_upward_rounded + : Icons.arrow_downward_rounded, + size: LunaUI.FONT_SIZE_H2, + color: LunaColours.accent, + ), + ], + ), + ), + ), + ), + ), + height: LunaTextInputBar.defaultHeight, + width: LunaTextInputBar.defaultHeight, + margin: const EdgeInsets.only(left: LunaUI.DEFAULT_MARGIN_SIZE), + color: Theme.of(context).canvasColor, + ); +} diff --git a/lib/modules/readarr/routes/catalogue/widgets/search_bar_view_button.dart b/lib/modules/readarr/routes/catalogue/widgets/search_bar_view_button.dart new file mode 100644 index 0000000000..c7f193aa0b --- /dev/null +++ b/lib/modules/readarr/routes/catalogue/widgets/search_bar_view_button.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorSearchBarViewButton extends StatefulWidget { + final ScrollController controller; + + const ReadarrAuthorSearchBarViewButton({ + Key? key, + required this.controller, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return LunaCard( + context: context, + child: Consumer( + builder: (context, state, _) => LunaPopupMenuButton( + tooltip: 'lunasea.View'.tr(), + icon: LunaIcons.VIEW, + onSelected: (result) { + state.seriesViewType = result; + widget.controller.lunaAnimateToStart(); + }, + itemBuilder: (context) => + List>.generate( + LunaListViewOption.values.length, + (index) => PopupMenuItem( + value: LunaListViewOption.values[index], + child: Text( + LunaListViewOption.values[index].readable, + style: TextStyle( + fontSize: LunaUI.FONT_SIZE_H3, + color: + state.seriesViewType == LunaListViewOption.values[index] + ? LunaColours.accent + : Colors.white, + ), + ), + ), + ), + ), + ), + margin: const EdgeInsets.only(left: LunaUI.DEFAULT_MARGIN_SIZE), + color: Theme.of(context).canvasColor, + height: LunaTextInputBar.defaultHeight, + width: LunaTextInputBar.defaultHeight, + ); + } +} diff --git a/lib/modules/readarr/routes/edit_author.dart b/lib/modules/readarr/routes/edit_author.dart new file mode 100644 index 0000000000..20163fdb60 --- /dev/null +++ b/lib/modules/readarr/routes/edit_author.dart @@ -0,0 +1,3 @@ +export 'edit_author/route.dart'; +export 'edit_author/state.dart'; +export 'edit_author/widgets.dart'; diff --git a/lib/modules/readarr/routes/edit_author/route.dart b/lib/modules/readarr/routes/edit_author/route.dart new file mode 100644 index 0000000000..ec1f5643e3 --- /dev/null +++ b/lib/modules/readarr/routes/edit_author/route.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrEditAuthorRouter extends ReadarrPageRouter { + ReadarrEditAuthorRouter() : super('/readarr/editmovie/:seriesid'); + + @override + _Widget widget([int authorId = -1]) => _Widget(authorId: authorId); + + @override + Future navigateTo( + BuildContext context, [ + int authorId = -1, + ]) async => + LunaRouter.router.navigateTo(context, route(authorId)); + + @override + String route([int authorId = -1]) => fullRoute.replaceFirst( + ':seriesid', + authorId.toString(), + ); + + @override + void defineRoute( + FluroRouter router, + ) { + super.withParameterRouteDefinition( + router, + (context, params) { + int authorId = (params['seriesid']?.isNotEmpty ?? false) + ? (int.tryParse(params['seriesid']![0]) ?? -1) + : -1; + return _Widget(authorId: authorId); + }, + ); + } +} + +class _Widget extends StatefulWidget { + final int authorId; + + const _Widget({ + Key? key, + required this.authorId, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State<_Widget> + with LunaLoadCallbackMixin, LunaScrollControllerMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Future loadCallback() async { + context.read().fetchTags(); + context.read().fetchQualityProfiles(); + context.read().fetchMetadataProfiles(); + } + + @override + Widget build(BuildContext context) { + if (widget.authorId <= 0) + return LunaInvalidRoute( + title: 'readarr.EditAuhtor'.tr(), + message: 'readarr.AuthorNotFound'.tr(), + ); + return ChangeNotifierProvider( + create: (_) => ReadarrAuthorEditState(), + builder: (context, _) { + LunaLoadingState state = + context.select( + (state) => state.state); + return LunaScaffold( + scaffoldKey: _scaffoldKey, + appBar: _appBar() as PreferredSizeWidget?, + body: + state == LunaLoadingState.ERROR ? _bodyError() : _body(context), + bottomNavigationBar: state == LunaLoadingState.ERROR + ? null + : const ReadarrEditSeriesActionBar(), + ); + }); + } + + Widget _appBar() { + return LunaAppBar( + scrollControllers: [scrollController], + title: 'readarr.EditAuthor'.tr(), + ); + } + + Widget _bodyError() { + return LunaMessage.goBack( + context: context, + text: 'lunasea.AnErrorHasOccurred'.tr(), + ); + } + + Widget _body(BuildContext context) { + return FutureBuilder( + future: Future.wait([ + context.select>?>( + (state) => state.authors)!, + context.select>?>( + (state) => state.qualityProfiles)!, + context.select>?>( + (state) => state.tags)!, + context.select>?>( + (state) => state.metadataProfiles)!, + ]), + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasError) { + return LunaMessage.error(onTap: loadCallback); + } + if (snapshot.hasData) { + ReadarrAuthor? series = (snapshot.data![0] as Map)[widget.authorId]; + if (series == null) return const LunaLoader(); + return _list( + context, + series: series, + qualityProfiles: snapshot.data![1] as List, + tags: snapshot.data![2] as List, + metadataProfiles: snapshot.data![3] as List, + ); + } + return const LunaLoader(); + }, + ); + } + + Widget _list( + BuildContext context, { + required ReadarrAuthor series, + required List qualityProfiles, + required List metadataProfiles, + required List tags, + }) { + if (context.read().series == null) { + context.read().series = series; + context + .read() + .initializeQualityProfile(qualityProfiles); + context + .read() + .initializeMetadataProfile(metadataProfiles); + context.read().initializeTags(tags); + context.read().canExecuteAction = true; + } + return LunaListView( + controller: scrollController, + children: [ + const ReadarrAuthorEditMonitoredTile(), + ReadarrAuthorEditQualityProfileTile(profiles: qualityProfiles), + ReadarrAuthorEditMetadataProfileTile(profiles: metadataProfiles), + const ReadarrAuthorEditSeriesPathTile(), + const ReadarrAuthorEditTagsTile(), + ], + ); + } +} diff --git a/lib/modules/readarr/routes/edit_author/state.dart b/lib/modules/readarr/routes/edit_author/state.dart new file mode 100644 index 0000000000..91b7c48c64 --- /dev/null +++ b/lib/modules/readarr/routes/edit_author/state.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorEditState extends ChangeNotifier { + ReadarrAuthor? _series; + ReadarrAuthor? get series => _series; + set series(ReadarrAuthor? series) { + _series = series; + initializeMonitored(); + initializeSeriesPath(); + } + + bool canExecuteAction = false; + + LunaLoadingState _state = LunaLoadingState.INACTIVE; + LunaLoadingState get state => _state; + set state(LunaLoadingState state) { + _state = state; + notifyListeners(); + } + + bool _monitored = true; + bool get monitored => _monitored; + set monitored(bool monitored) { + _monitored = monitored; + notifyListeners(); + } + + void initializeMonitored() { + _monitored = series!.monitored ?? false; + } + + String _seriesPath = ''; + String get seriesPath => _seriesPath; + set seriesPath(String seriesPath) { + _seriesPath = seriesPath; + notifyListeners(); + } + + void initializeSeriesPath() { + _seriesPath = series!.path ?? ''; + } + + ReadarrQualityProfile? _qualityProfile; + ReadarrQualityProfile? get qualityProfile => _qualityProfile; + set qualityProfile(ReadarrQualityProfile? qualityProfile) { + _qualityProfile = qualityProfile; + notifyListeners(); + } + + void initializeQualityProfile(List qualityProfiles) { + _qualityProfile = qualityProfiles.firstWhere( + (profile) => profile.id == series!.qualityProfileId, + orElse: () => qualityProfiles[0], + ); + } + + late ReadarrMetadataProfile _metadataProfile; + ReadarrMetadataProfile get metadataProfile => _metadataProfile; + set metadataProfile(ReadarrMetadataProfile metadataProfile) { + _metadataProfile = metadataProfile; + notifyListeners(); + } + + void initializeMetadataProfile( + List metadataProfiles) { + _metadataProfile = metadataProfiles.firstWhere( + (p) => p.id == series!.metadataProfileId, + ); + } + + List? _tags; + List? get tags => _tags; + set tags(List? tags) { + _tags = tags; + notifyListeners(); + } + + void initializeTags(List tags) { + _tags = tags.where((tag) => (series!.tags ?? []).contains(tag.id)).toList(); + } +} diff --git a/lib/modules/readarr/routes/edit_author/widgets.dart b/lib/modules/readarr/routes/edit_author/widgets.dart new file mode 100644 index 0000000000..283ba97204 --- /dev/null +++ b/lib/modules/readarr/routes/edit_author/widgets.dart @@ -0,0 +1,6 @@ +export 'widgets/bottom_action_bar.dart'; +export 'widgets/tile_metadata_profile.dart'; +export 'widgets/tile_monitored.dart'; +export 'widgets/tile_quality_profile.dart'; +export 'widgets/tile_author_path.dart'; +export 'widgets/tile_tags.dart'; diff --git a/lib/modules/readarr/routes/edit_author/widgets/bottom_action_bar.dart b/lib/modules/readarr/routes/edit_author/widgets/bottom_action_bar.dart new file mode 100644 index 0000000000..35ac3661e4 --- /dev/null +++ b/lib/modules/readarr/routes/edit_author/widgets/bottom_action_bar.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrEditSeriesActionBar extends StatelessWidget { + const ReadarrEditSeriesActionBar({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBottomActionBar( + actions: [ + LunaButton( + type: LunaButtonType.TEXT, + text: 'lunasea.Update'.tr(), + icon: Icons.edit_rounded, + loadingState: context.watch().state, + onTap: () async => _updateOnTap(context), + ), + ], + ); + } + + Future _updateOnTap(BuildContext context) async { + if (context.read().canExecuteAction) { + context.read().state = LunaLoadingState.ACTIVE; + if (context.read().series != null) { + ReadarrAuthor series = context + .read() + .series! + .updateEdits(context.read()); + bool result = await ReadarrAPIController().updateAuthor( + context: context, + series: series, + ); + if (result) Navigator.of(context).lunaSafetyPop(); + } + } + } +} diff --git a/lib/modules/readarr/routes/edit_author/widgets/tile_author_path.dart b/lib/modules/readarr/routes/edit_author/widgets/tile_author_path.dart new file mode 100644 index 0000000000..1d3cf5eedf --- /dev/null +++ b/lib/modules/readarr/routes/edit_author/widgets/tile_author_path.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorEditSeriesPathTile extends StatelessWidget { + const ReadarrAuthorEditSeriesPathTile({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBlock( + title: 'readarr.AuthorPath'.tr(), + body: [ + TextSpan( + text: context.watch().seriesPath, + ), + ], + trailing: const LunaIconButton.arrow(), + onTap: () async => _onTap(context), + ); + } + + Future _onTap(BuildContext context) async { + Tuple2 _values = await LunaDialogs().editText( + context, + 'readarr.AuthorPath'.tr(), + prefill: context.read().seriesPath, + ); + if (_values.item1) + context.read().seriesPath = _values.item2; + } +} diff --git a/lib/modules/readarr/routes/edit_author/widgets/tile_metadata_profile.dart b/lib/modules/readarr/routes/edit_author/widgets/tile_metadata_profile.dart new file mode 100644 index 0000000000..3f7cd8602c --- /dev/null +++ b/lib/modules/readarr/routes/edit_author/widgets/tile_metadata_profile.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorEditMetadataProfileTile extends StatelessWidget { + final List profiles; + + const ReadarrAuthorEditMetadataProfileTile({ + Key? key, + required this.profiles, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBlock( + title: 'readarr.MetadataProfile'.tr(), + body: [ + TextSpan( + text: context.watch().metadataProfile.name, + ), + ], + trailing: const LunaIconButton.arrow(), + onTap: () async => _onTap(context), + ); + } + + Future _onTap(BuildContext context) async { + Tuple2 result = + await ReadarrDialogs().editMetadataProfiles(context, profiles); + if (result.item1) + context.read().metadataProfile = result.item2!; + } +} diff --git a/lib/modules/readarr/routes/edit_author/widgets/tile_monitored.dart b/lib/modules/readarr/routes/edit_author/widgets/tile_monitored.dart new file mode 100644 index 0000000000..e4230f7482 --- /dev/null +++ b/lib/modules/readarr/routes/edit_author/widgets/tile_monitored.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorEditMonitoredTile extends StatelessWidget { + const ReadarrAuthorEditMonitoredTile({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBlock( + title: 'readarr.Monitored'.tr(), + trailing: LunaSwitch( + value: context.watch().monitored, + onChanged: (value) => + context.read().monitored = value, + ), + ); + } +} diff --git a/lib/modules/readarr/routes/edit_author/widgets/tile_quality_profile.dart b/lib/modules/readarr/routes/edit_author/widgets/tile_quality_profile.dart new file mode 100644 index 0000000000..9662974ef9 --- /dev/null +++ b/lib/modules/readarr/routes/edit_author/widgets/tile_quality_profile.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorEditQualityProfileTile extends StatelessWidget { + final List profiles; + + const ReadarrAuthorEditQualityProfileTile({ + Key? key, + required this.profiles, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBlock( + title: 'readarr.QualityProfile'.tr(), + body: [ + TextSpan( + text: context.watch().qualityProfile?.name ?? + LunaUI.TEXT_EMDASH, + ) + ], + trailing: const LunaIconButton.arrow(), + onTap: () async => _onTap(context), + ); + } + + Future _onTap(BuildContext context) async { + Tuple2 result = + await ReadarrDialogs().editQualityProfile(context, profiles); + if (result.item1) + context.read().qualityProfile = result.item2!; + } +} diff --git a/lib/modules/readarr/routes/edit_author/widgets/tile_tags.dart b/lib/modules/readarr/routes/edit_author/widgets/tile_tags.dart new file mode 100644 index 0000000000..cf280817bb --- /dev/null +++ b/lib/modules/readarr/routes/edit_author/widgets/tile_tags.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAuthorEditTagsTile extends StatelessWidget { + const ReadarrAuthorEditTagsTile({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBlock( + title: 'readarr.Tags'.tr(), + body: [ + TextSpan( + text: (context.watch().tags?.isEmpty ?? true) + ? 'lunasea.NotSet'.tr() + : context + .watch() + .tags + ?.map((e) => e.label) + .join(', '), + ) + ], + trailing: const LunaIconButton.arrow(), + onTap: () async => await ReadarrDialogs().setEditTags(context), + ); + } +} diff --git a/lib/modules/readarr/routes/history.dart b/lib/modules/readarr/routes/history.dart new file mode 100644 index 0000000000..a89dfef1bc --- /dev/null +++ b/lib/modules/readarr/routes/history.dart @@ -0,0 +1,2 @@ +export 'history/route.dart'; +export 'history/widgets.dart'; diff --git a/lib/modules/readarr/routes/history/route.dart b/lib/modules/readarr/routes/history/route.dart new file mode 100644 index 0000000000..1dd9bfee87 --- /dev/null +++ b/lib/modules/readarr/routes/history/route.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrHistoryRouter extends ReadarrPageRouter { + ReadarrHistoryRouter() : super('/readarr/history'); + + @override + Widget widget() => _Widget(); + + @override + void defineRoute(FluroRouter router) { + super.noParameterRouteDefinition(router); + } +} + +class _Widget extends StatefulWidget { + @override + State<_Widget> createState() => _State(); +} + +class _State extends State<_Widget> + with LunaScrollControllerMixin, LunaLoadCallbackMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = + GlobalKey(); + final PagingController _pagingController = + PagingController(firstPageKey: 1); + + @override + Future loadCallback() async { + context.read().fetchAllAuthors(); + } + + @override + void dispose() { + _pagingController.dispose(); + super.dispose(); + } + + Future _fetchPage(int pageKey) async { + await context + .read() + .api! + .history + .get( + page: pageKey, + pageSize: ReadarrDatabaseValue.CONTENT_PAGE_SIZE.data, + sortKey: ReadarrHistorySortKey.DATE, + sortDirection: ReadarrSortDirection.DESCENDING, + includeBook: true, + ) + .then((data) { + if (data.totalRecords! > (data.page! * data.pageSize!)) { + return _pagingController.appendPage(data.records!, pageKey + 1); + } + return _pagingController.appendLastPage(data.records!); + }).catchError((error, stack) { + LunaLogger().error( + 'Unable to fetch Readarr history page: $pageKey', + error, + stack, + ); + _pagingController.error = error; + }); + } + + @override + Widget build(BuildContext context) { + return LunaScaffold( + scaffoldKey: _scaffoldKey, + appBar: _appBar() as PreferredSizeWidget?, + body: _body(), + ); + } + + Widget _appBar() { + return LunaAppBar( + title: 'readarr.History'.tr(), + scrollControllers: [scrollController], + ); + } + + Widget _body() { + return FutureBuilder( + future: context.read().authors, + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasError) { + if (snapshot.connectionState != ConnectionState.waiting) { + LunaLogger().error( + 'Unable to fetch Readarr authors', + snapshot.error, + snapshot.stackTrace, + ); + } + return LunaMessage.error(onTap: _refreshKey.currentState!.show); + } + if (snapshot.hasData) return _list(snapshot.data); + return const LunaLoader(); + }, + ); + } + + Widget _list(Map? series) { + return LunaPagedListView( + refreshKey: _refreshKey, + pagingController: _pagingController, + scrollController: scrollController, + listener: _fetchPage, + noItemsFoundMessage: 'readarr.NoHistoryFound'.tr(), + itemBuilder: (context, history, _) => ReadarrHistoryTile( + history: history, + author: series![history.authorId!], + type: ReadarrHistoryTileType.ALL, + ), + ); + } +} diff --git a/lib/modules/readarr/routes/history/widgets.dart b/lib/modules/readarr/routes/history/widgets.dart new file mode 100644 index 0000000000..54a3049e50 --- /dev/null +++ b/lib/modules/readarr/routes/history/widgets.dart @@ -0,0 +1 @@ +export 'widgets/history_tile.dart'; diff --git a/lib/modules/readarr/routes/history/widgets/history_tile.dart b/lib/modules/readarr/routes/history/widgets/history_tile.dart new file mode 100644 index 0000000000..7e8d033c97 --- /dev/null +++ b/lib/modules/readarr/routes/history/widgets/history_tile.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +enum ReadarrHistoryTileType { + ALL, + SERIES, + SEASON, + EPISODE, +} + +class ReadarrHistoryTile extends StatelessWidget { + final ReadarrHistoryRecord history; + final ReadarrHistoryTileType type; + final ReadarrAuthor? author; + final ReadarrBook? book; + + const ReadarrHistoryTile({ + Key? key, + required this.history, + required this.type, + this.author, + this.book, + }) : super(key: key); + + bool _hasEpisodeInfo() { + if (history.episode != null || book != null) return true; + return false; + } + + bool _hasLongPressAction() { + switch (type) { + case ReadarrHistoryTileType.ALL: + return true; + case ReadarrHistoryTileType.SERIES: + return _hasEpisodeInfo(); + case ReadarrHistoryTileType.SEASON: + case ReadarrHistoryTileType.EPISODE: + default: + return false; + } + } + + @override + Widget build(BuildContext context) { + bool _isThreeLine = + _hasEpisodeInfo() && type != ReadarrHistoryTileType.EPISODE; + return LunaExpandableListTile( + title: type != ReadarrHistoryTileType.ALL + ? history.sourceTitle! + : author?.title ?? LunaUI.TEXT_EMDASH, + collapsedSubtitles: [ + if (_isThreeLine) _subtitle1(), + _subtitle2(), + _subtitle3(), + ], + expandedHighlightedNodes: [ + LunaHighlightedNode( + text: history.eventType?.readable ?? LunaUI.TEXT_EMDASH, + backgroundColor: history.eventType!.lunaColour(), + ), + if (history.lunaHasPreferredWordScore()) + LunaHighlightedNode( + text: history.lunaPreferredWordScore(), + backgroundColor: LunaColours.purple, + ), + if (history.episode?.title != null) + LunaHighlightedNode( + text: history.episode!.title!, + backgroundColor: LunaColours.blueGrey, + ), + ], + expandedTableContent: history.eventType?.lunaTableContent( + history: history, + showSourceTitle: type != ReadarrHistoryTileType.ALL, + ) ?? + [], + onLongPress: + _hasLongPressAction() ? () async => _onLongPress(context) : null, + ); + } + + Future _onLongPress(BuildContext context) async { + switch (type) { + case ReadarrHistoryTileType.ALL: + return ReadarrAuthorDetailsRouter().navigateTo( + context, + history.series?.id ?? author?.id ?? -1, + ); + case ReadarrHistoryTileType.SERIES: + if (_hasEpisodeInfo()) { + return ReadarrBookDetailsRouter().navigateTo( + context, history.bookId ?? history.episode?.id ?? -1); + } + break; + default: + break; + } + } + + TextSpan _subtitle1() { + return TextSpan(children: [ + TextSpan( + text: history.episode?.title ?? book?.title ?? LunaUI.TEXT_EMDASH, + style: const TextStyle( + fontStyle: FontStyle.italic, + ), + ), + ]); + } + + TextSpan _subtitle2() { + return TextSpan( + text: [ + history.date?.lunaAge ?? LunaUI.TEXT_EMDASH, + history.date?.lunaDateTimeReadable() ?? LunaUI.TEXT_EMDASH, + ].join(LunaUI.TEXT_BULLET.lunaPad()), + ); + } + + TextSpan _subtitle3() { + return TextSpan( + text: history.eventType?.lunaReadable(history) ?? LunaUI.TEXT_EMDASH, + style: TextStyle( + color: history.eventType?.lunaColour() ?? LunaColours.blueGrey, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + ); + } +} diff --git a/lib/modules/readarr/routes/missing.dart b/lib/modules/readarr/routes/missing.dart new file mode 100644 index 0000000000..1ef0049e0c --- /dev/null +++ b/lib/modules/readarr/routes/missing.dart @@ -0,0 +1,2 @@ +export 'missing/route.dart'; +export 'missing/widgets.dart'; diff --git a/lib/modules/readarr/routes/missing/route.dart b/lib/modules/readarr/routes/missing/route.dart new file mode 100644 index 0000000000..49316d7937 --- /dev/null +++ b/lib/modules/readarr/routes/missing/route.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrMissingRoute extends StatefulWidget { + const ReadarrMissingRoute({ + Key? key, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State + with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = + GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Future loadCallback() async { + ReadarrState _state = Provider.of(context, listen: false); + _state.fetchMissing(); + await _state.missing; + } + + @override + Widget build(BuildContext context) { + super.build(context); + return LunaScaffold( + scaffoldKey: _scaffoldKey, + module: LunaModule.READARR, + hideDrawer: true, + body: _body(), + ); + } + + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: Selector>?, Future?>>( + selector: (_, state) => Tuple2(state.authors, state.missing), + builder: (context, tuple, _) => FutureBuilder( + future: Future.wait([tuple.item1!, tuple.item2!]), + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasError) { + if (snapshot.connectionState != ConnectionState.waiting) { + LunaLogger().error( + 'Unable to fetch Readarr missing episodes', + snapshot.error, + snapshot.stackTrace, + ); + } + return LunaMessage.error(onTap: _refreshKey.currentState!.show); + } + if (snapshot.hasData) + return _episodes( + snapshot.data![0] as Map, + snapshot.data![1] as ReadarrMissing, + ); + return const LunaLoader(); + }, + ), + ), + ); + } + + Widget _episodes(Map series, ReadarrMissing missing) { + if ((missing.records?.length ?? 0) == 0) + return LunaMessage( + text: 'readarr.NoEpisodesFound'.tr(), + buttonText: 'lunasea.Refresh'.tr(), + onTap: _refreshKey.currentState?.show, + ); + return LunaListViewBuilder( + controller: ReadarrNavigationBar.scrollControllers[2], + itemCount: missing.records!.length, + itemExtent: ReadarrMissingTile.itemExtent, + itemBuilder: (context, index) => ReadarrMissingTile( + record: missing.records![index], + series: series[missing.records![index].authorId!], + ), + ); + } +} diff --git a/lib/modules/readarr/routes/missing/widgets.dart b/lib/modules/readarr/routes/missing/widgets.dart new file mode 100644 index 0000000000..c7f5c93dfd --- /dev/null +++ b/lib/modules/readarr/routes/missing/widgets.dart @@ -0,0 +1 @@ +export 'widgets/missing_tile.dart'; diff --git a/lib/modules/readarr/routes/missing/widgets/missing_tile.dart b/lib/modules/readarr/routes/missing/widgets/missing_tile.dart new file mode 100644 index 0000000000..97bee5227e --- /dev/null +++ b/lib/modules/readarr/routes/missing/widgets/missing_tile.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrMissingTile extends StatefulWidget { + static final itemExtent = LunaBlock.calculateItemExtent(3); + + final ReadarrBook record; + final ReadarrAuthor? series; + + const ReadarrMissingTile({ + Key? key, + required this.record, + this.series, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return LunaBlock( + posterUrl: context + .read() + .getAuthorPosterURL(widget.record.authorId), + posterHeaders: context.read().headers, + posterPlaceholderIcon: LunaIcons.BOOK, + title: widget.record.series?.title ?? + widget.series?.title ?? + LunaUI.TEXT_EMDASH, + body: [ + _subtitle1(), + _subtitle2(), + ], + disabled: !widget.record.monitored!, + onTap: _onTap, + onLongPress: _onLongPress, + trailing: _trailing(), + ); + } + + Widget _trailing() { + return LunaIconButton( + icon: Icons.search_rounded, + onPressed: _trailingOnTap, + onLongPress: _trailingOnLongPress, + ); + } + + TextSpan _subtitle1() { + return TextSpan( + style: const TextStyle( + fontStyle: FontStyle.italic, + ), + text: widget.record.title ?? 'lunasea.Unknown'.tr(), + ); + } + + TextSpan _subtitle2() { + return TextSpan( + style: const TextStyle( + fontSize: LunaUI.FONT_SIZE_H3, + color: LunaColours.red, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + children: [ + TextSpan( + text: widget.record.releaseDate == null + ? 'Released' + : 'Released ${widget.record.releaseDate!.toLocal().lunaAge}'), + ], + ); + } + + Future _onTap() async => ReadarrBookDetailsRouter().navigateTo( + context, + widget.record.id ?? -1, + ); + + Future _onLongPress() async => ReadarrAuthorDetailsRouter().navigateTo( + context, + widget.record.authorId!, + ); + + Future _trailingOnTap() async { + Provider.of(context, listen: false) + .api! + .command + .bookSearch(bookIds: [widget.record.id!]) + .then((_) => showLunaSuccessSnackBar( + title: 'Searching for Episode...', + message: widget.record.title, + )) + .catchError((error, stack) { + LunaLogger().error( + 'Failed to search for episode: ${widget.record.id}', + error, + stack); + showLunaErrorSnackBar( + title: 'Failed to Search', + error: error, + ); + }); + } + + Future _trailingOnLongPress() async => + ReadarrReleasesRouter().navigateTo( + context, + bookId: widget.record.id, + ); +} diff --git a/lib/modules/readarr/routes/more.dart b/lib/modules/readarr/routes/more.dart new file mode 100644 index 0000000000..56a2ef266a --- /dev/null +++ b/lib/modules/readarr/routes/more.dart @@ -0,0 +1 @@ +export 'more/route.dart'; diff --git a/lib/modules/readarr/routes/more/route.dart b/lib/modules/readarr/routes/more/route.dart new file mode 100644 index 0000000000..ff05282afa --- /dev/null +++ b/lib/modules/readarr/routes/more/route.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrMoreRoute extends StatefulWidget { + const ReadarrMoreRoute({ + Key? key, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State with AutomaticKeepAliveClientMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return LunaScaffold( + scaffoldKey: _scaffoldKey, + module: LunaModule.READARR, + hideDrawer: true, + body: _body(), + ); + } + + // ignore: unused_element + Future _showComingSoonMessage() async { + showLunaInfoSnackBar( + title: 'lunasea.ComingSoon'.tr(), + message: 'This feature is still being developed!', + ); + } + + Widget _body() { + return LunaListView( + controller: ReadarrNavigationBar.scrollControllers[3], + children: [ + LunaBlock( + title: 'readarr.History'.tr(), + body: [TextSpan(text: 'readarr.HistoryDescription'.tr())], + trailing: LunaIconButton( + icon: Icons.history_rounded, + color: LunaColours().byListIndex(0), + ), + onTap: () async => ReadarrHistoryRouter().navigateTo(context), + ), + // LunaBlock( + // title: 'readarr.ManualImport'.tr(), + // body: [TextSpan(text: 'readarr.ManualImportDescription'.tr())], + // trailing: LunaIconButton( + // icon: Icons.download_done_rounded, + // color: LunaColours().byListIndex(1), + // ), + // onTap: () async => _showComingSoonMessage(), + // ), + LunaBlock( + title: 'readarr.Queue'.tr(), + body: [TextSpan(text: 'readarr.QueueDescription'.tr())], + trailing: LunaIconButton( + icon: Icons.queue_play_next_rounded, + color: LunaColours().byListIndex(1), + ), + onTap: () async => ReadarrQueueRouter().navigateTo(context), + ), + // LunaBlock( + // title: 'readarr.SystemStatus'.tr(), + // body: [TextSpan(text: 'readarr.SystemStatusDescription'.tr())], + // trailing: LunaIconButton( + // icon: Icons.computer_rounded, + // color: LunaColours().byListIndex(3), + // ), + // onTap: () async => _showComingSoonMessage(), + // ), + LunaBlock( + title: 'readarr.Tags'.tr(), + body: [TextSpan(text: 'readarr.TagsDescription'.tr())], + trailing: LunaIconButton( + icon: Icons.style_rounded, + color: LunaColours().byListIndex(2), + ), + onTap: () async => ReadarrTagsRouter().navigateTo(context), + ), + ], + ); + } +} diff --git a/lib/modules/readarr/routes/queue.dart b/lib/modules/readarr/routes/queue.dart new file mode 100644 index 0000000000..d0ee7f7610 --- /dev/null +++ b/lib/modules/readarr/routes/queue.dart @@ -0,0 +1,3 @@ +export 'queue/route.dart'; +export 'queue/state.dart'; +export 'queue/widgets.dart'; diff --git a/lib/modules/readarr/routes/queue/route.dart b/lib/modules/readarr/routes/queue/route.dart new file mode 100644 index 0000000000..f5050e5768 --- /dev/null +++ b/lib/modules/readarr/routes/queue/route.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrQueueRouter extends ReadarrPageRouter { + ReadarrQueueRouter() : super('/readarr/queue'); + + @override + _Widget widget() => _Widget(); + + @override + void defineRoute(FluroRouter router) { + super.noParameterRouteDefinition(router); + } +} + +class _Widget extends StatefulWidget { + @override + State createState() => _State(); +} + +class _State extends State<_Widget> with LunaScrollControllerMixin { + final _scaffoldKey = GlobalKey(); + final _refreshKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => ReadarrQueueState(context), + builder: (context, _) => LunaScaffold( + scaffoldKey: _scaffoldKey, + appBar: _appBar() as PreferredSizeWidget?, + body: _body(context), + ), + ); + } + + Future _onRefresh(BuildContext context) async { + await context.read().fetchQueue( + context, + hardCheck: true, + ); + await context.read().queue; + } + + Widget _appBar() { + return LunaAppBar( + title: 'readarr.Queue'.tr(), + scrollControllers: [scrollController], + ); + } + + Widget _body(BuildContext context) { + return LunaRefreshIndicator( + key: _refreshKey, + context: context, + onRefresh: () async => _onRefresh(context), + child: FutureBuilder( + future: context.watch().queue, + builder: (context, AsyncSnapshot snapshot) { + if (snapshot.hasError) { + if (snapshot.connectionState != ConnectionState.waiting) { + LunaLogger().error( + 'Unable to fetch Readarr queue', + snapshot.error, + snapshot.stackTrace, + ); + } + return LunaMessage.error( + onTap: _refreshKey.currentState!.show, + ); + } + if (snapshot.hasData) { + return _list(snapshot.data!); + } + return const LunaLoader(); + }, + ), + ); + } + + Widget _list(ReadarrQueue queue) { + if (queue.records!.isEmpty) { + return LunaMessage( + text: 'readarr.EmptyQueue'.tr(), + buttonText: 'lunasea.Refresh'.tr(), + onTap: _refreshKey.currentState!.show, + ); + } + return LunaListViewBuilder( + controller: scrollController, + itemCount: queue.records!.length, + itemBuilder: (context, index) => ReadarrQueueTile( + key: ObjectKey(queue.records![index].id), + queueRecord: queue.records![index], + type: ReadarrQueueTileType.ALL, + ), + ); + } +} diff --git a/lib/modules/readarr/routes/queue/state.dart b/lib/modules/readarr/routes/queue/state.dart new file mode 100644 index 0000000000..7aba950a43 --- /dev/null +++ b/lib/modules/readarr/routes/queue/state.dart @@ -0,0 +1,54 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrQueueState extends ChangeNotifier { + ReadarrQueueState(BuildContext context) { + fetchQueue(context); + } + + Timer? _timer; + void cancelTimer() => _timer?.cancel(); + void createTimer(BuildContext context) { + _timer = Timer.periodic( + Duration(seconds: ReadarrDatabaseValue.QUEUE_REFRESH_RATE.data), + (_) => fetchQueue(context), + ); + } + + late Future _queue; + Future get queue => _queue; + set queue(Future queue) { + this.queue = queue; + notifyListeners(); + } + + Future fetchQueue( + BuildContext context, { + bool hardCheck = false, + }) async { + cancelTimer(); + if (context.read().enabled) { + if (hardCheck) { + // "Hard" check by telling Readarr to refresh the monitored downloads + // Give it 500 ms to internally check and then continue to fetch queue + await context + .read() + .api! + .command + .refreshMonitoredDownloads() + .then( + (_) => Future.delayed(const Duration(milliseconds: 500), () {}), + ); + } + _queue = context.read().api!.queue.get( + includeBook: true, + includeAuthor: true, + pageSize: ReadarrDatabaseValue.QUEUE_PAGE_SIZE.data, + ); + createTimer(context); + } + notifyListeners(); + } +} diff --git a/lib/modules/readarr/routes/queue/widgets.dart b/lib/modules/readarr/routes/queue/widgets.dart new file mode 100644 index 0000000000..f6ee9af585 --- /dev/null +++ b/lib/modules/readarr/routes/queue/widgets.dart @@ -0,0 +1 @@ +export 'widgets/queue_tile.dart'; diff --git a/lib/modules/readarr/routes/queue/widgets/queue_tile.dart b/lib/modules/readarr/routes/queue/widgets/queue_tile.dart new file mode 100644 index 0000000000..73b48815e1 --- /dev/null +++ b/lib/modules/readarr/routes/queue/widgets/queue_tile.dart @@ -0,0 +1,210 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +enum ReadarrQueueTileType { + ALL, + EPISODE, +} + +class ReadarrQueueTile extends StatefulWidget { + final ReadarrQueueRecord queueRecord; + final ReadarrQueueTileType type; + + const ReadarrQueueTile({ + Key? key, + required this.queueRecord, + required this.type, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return LunaExpandableListTile( + title: widget.queueRecord.title!, + collapsedSubtitles: [ + if (widget.type == ReadarrQueueTileType.ALL) _subtitle1(), + if (widget.type == ReadarrQueueTileType.ALL) _subtitle2(), + _subtitle3(), + ], + expandedTableContent: _expandedTableContent(), + expandedHighlightedNodes: _expandedHighlightedNodes(), + expandedTableButtons: _tableButtons(), + collapsedTrailing: _collapsedTrailing(), + onLongPress: _onLongPress, + ); + } + + Future _onLongPress() async { + switch (widget.type) { + case ReadarrQueueTileType.ALL: + ReadarrAuthorDetailsRouter().navigateTo( + context, + widget.queueRecord.authorId!, + ); + break; + case ReadarrQueueTileType.EPISODE: + ReadarrQueueRouter().navigateTo(context); + break; + } + } + + Widget _collapsedTrailing() { + Tuple3 _status = + widget.queueRecord.lunaStatusParameters(); + return LunaIconButton( + icon: _status.item2, + color: _status.item3, + ); + } + + TextSpan _subtitle1() { + return TextSpan( + text: widget.queueRecord.series!.title ?? LunaUI.TEXT_EMDASH, + ); + } + + TextSpan _subtitle2() { + return TextSpan( + children: [ + TextSpan( + text: widget.queueRecord.quality?.quality?.name ?? LunaUI.TEXT_EMDASH, + ), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + TextSpan( + text: widget.queueRecord.lunaTimeLeft(), + ), + ], + ); + } + + TextSpan _subtitle3() { + Tuple3 _params = + widget.queueRecord.lunaStatusParameters(canBeWhite: false); + return TextSpan( + style: TextStyle( + color: _params.item3, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + children: [ + TextSpan(text: widget.queueRecord.lunaPercentage()), + TextSpan(text: LunaUI.TEXT_EMDASH.lunaPad()), + TextSpan(text: _params.item1), + ], + ); + } + + List _expandedHighlightedNodes() { + Tuple3 _status = + widget.queueRecord.lunaStatusParameters(canBeWhite: false); + return [ + LunaHighlightedNode( + text: widget.queueRecord.protocol!.lunaReadable(), + backgroundColor: widget.queueRecord.protocol!.lunaProtocolColor(), + ), + LunaHighlightedNode( + text: widget.queueRecord.lunaPercentage(), + backgroundColor: _status.item3, + ), + LunaHighlightedNode( + text: widget.queueRecord.status!.lunaStatus(), + backgroundColor: _status.item3, + ), + ]; + } + + List _expandedTableContent() { + return [ + if (widget.type == ReadarrQueueTileType.ALL) + LunaTableContent( + title: 'readarr.Author'.tr(), + body: widget.queueRecord.series?.title ?? LunaUI.TEXT_EMDASH, + ), + if (widget.type == ReadarrQueueTileType.ALL) + LunaTableContent( + title: 'readarr.Title'.tr(), + body: widget.queueRecord.episode?.title ?? LunaUI.TEXT_EMDASH, + ), + if (widget.type == ReadarrQueueTileType.ALL) + LunaTableContent(title: '', body: ''), + LunaTableContent( + title: 'readarr.Quality'.tr(), + body: widget.queueRecord.quality?.quality?.name ?? LunaUI.TEXT_EMDASH, + ), + LunaTableContent( + title: 'readarr.Client'.tr(), + body: widget.queueRecord.downloadClient ?? LunaUI.TEXT_EMDASH, + ), + LunaTableContent( + title: 'readarr.Size'.tr(), + body: widget.queueRecord.size?.floor().lunaBytesToString() ?? + LunaUI.TEXT_EMDASH, + ), + LunaTableContent( + title: 'readarr.TimeLeft'.tr(), + body: widget.queueRecord.lunaTimeLeft(), + ), + ]; + } + + List _tableButtons() { + return [ + if ((widget.queueRecord.statusMessages ?? []).isNotEmpty) + LunaButton.text( + icon: Icons.messenger_outline_rounded, + color: LunaColours.orange, + text: 'readarr.Messages'.tr(), + onTap: () async { + ReadarrDialogs().showQueueStatusMessages( + context, + widget.queueRecord.statusMessages!, + ); + }, + ), + // if (widget.queueRecord.status == ReadarrQueueStatus.COMPLETED && + // widget.queueRecord?.trackedDownloadStatus == + // ReadarrTrackedDownloadStatus.WARNING) + // LunaButton.text( + // icon: Icons.download_done_rounded, + // text: 'readarr.Import'.tr(), + // onTap: () async {}, + // ), + LunaButton.text( + icon: Icons.delete_rounded, + color: LunaColours.red, + text: 'lunasea.Remove'.tr(), + onTap: () async { + bool result = await ReadarrDialogs().removeFromQueue(context); + if (result) { + ReadarrAPIController() + .removeFromQueue( + context: context, + queueRecord: widget.queueRecord, + ) + .then((_) { + switch (widget.type) { + case ReadarrQueueTileType.ALL: + context.read().fetchQueue( + context, + hardCheck: true, + ); + break; + case ReadarrQueueTileType.EPISODE: + /*context.read().fetchState( + context, + shouldFetchEpisodes: false, + shouldFetchFiles: false, + );*/ + break; + } + }); + } + }, + ), + ]; + } +} diff --git a/lib/modules/readarr/routes/readarr.dart b/lib/modules/readarr/routes/readarr.dart new file mode 100644 index 0000000000..8aaf6c2ac2 --- /dev/null +++ b/lib/modules/readarr/routes/readarr.dart @@ -0,0 +1,2 @@ +export 'readarr/route.dart'; +export 'readarr/widgets.dart'; diff --git a/lib/modules/readarr/routes/readarr/route.dart b/lib/modules/readarr/routes/readarr/route.dart new file mode 100644 index 0000000000..ac8adeabfd --- /dev/null +++ b/lib/modules/readarr/routes/readarr/route.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrHomeRouter extends ReadarrPageRouter { + ReadarrHomeRouter() : super('/readarr'); + + @override + _Widget widget() => _Widget(); + + @override + void defineRoute(FluroRouter router) => super.noParameterRouteDefinition( + router, + homeRoute: true, + ); +} + +class _Widget extends StatefulWidget { + @override + State<_Widget> createState() => _State(); +} + +class _State extends State<_Widget> { + final GlobalKey _scaffoldKey = GlobalKey(); + LunaPageController? _pageController; + + @override + void initState() { + super.initState(); + _pageController = LunaPageController( + initialPage: ReadarrDatabaseValue.NAVIGATION_INDEX.data, + ); + } + + @override + Widget build(BuildContext context) { + return LunaScaffold( + scaffoldKey: _scaffoldKey, + module: LunaModule.READARR, + drawer: _drawer(), + appBar: _appBar() as PreferredSizeWidget?, + bottomNavigationBar: _bottomNavigationBar(), + body: _body(), + ); + } + + Widget _drawer() { + return LunaDrawer(page: LunaModule.READARR.key); + } + + Widget? _bottomNavigationBar() { + if (context.read().enabled) { + return ReadarrNavigationBar(pageController: _pageController); + } + return null; + } + + Widget _appBar() { + List profiles = Database.profiles.box.keys.fold( + [], + (value, element) { + if (Database.profiles.box.get(element)?.readarrEnabled ?? false) { + value.add(element); + } + return value; + }, + ); + List? actions; + if (context.watch().enabled) { + actions = [ + const ReadarrAppBarAddSeriesAction(), + const ReadarrAppBarGlobalSettingsAction(), + ]; + } + return LunaAppBar.dropdown( + title: LunaModule.READARR.name, + useDrawer: true, + profiles: profiles, + actions: actions, + pageController: _pageController, + scrollControllers: ReadarrNavigationBar.scrollControllers, + ); + } + + Widget _body() { + return Selector( + selector: (_, state) => state.enabled, + builder: (context, enabled, _) { + if (!enabled!) { + return LunaMessage.moduleNotEnabled( + context: context, + module: 'Readarr', + ); + } + return LunaPageView( + controller: _pageController, + children: const [ + ReadarrCatalogueRoute(), + ReadarrUpcomingRoute(), + ReadarrMissingRoute(), + ReadarrMoreRoute(), + ], + ); + }, + ); + } +} diff --git a/lib/modules/readarr/routes/readarr/widgets.dart b/lib/modules/readarr/routes/readarr/widgets.dart new file mode 100644 index 0000000000..217275c4d5 --- /dev/null +++ b/lib/modules/readarr/routes/readarr/widgets.dart @@ -0,0 +1,3 @@ +export 'widgets/appbar_add_author_action.dart'; +export 'widgets/appbar_global_settings_action.dart'; +export 'widgets/navigation_bar.dart'; diff --git a/lib/modules/readarr/routes/readarr/widgets/appbar_add_author_action.dart b/lib/modules/readarr/routes/readarr/widgets/appbar_add_author_action.dart new file mode 100644 index 0000000000..acc0d75e70 --- /dev/null +++ b/lib/modules/readarr/routes/readarr/widgets/appbar_add_author_action.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAppBarAddSeriesAction extends StatelessWidget { + const ReadarrAppBarAddSeriesAction({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaIconButton( + icon: Icons.add_rounded, + onPressed: () async => ReadarrAddSeriesRouter().navigateTo(context), + ); + } +} diff --git a/lib/modules/readarr/routes/readarr/widgets/appbar_global_settings_action.dart b/lib/modules/readarr/routes/readarr/widgets/appbar_global_settings_action.dart new file mode 100644 index 0000000000..7fe5096bfc --- /dev/null +++ b/lib/modules/readarr/routes/readarr/widgets/appbar_global_settings_action.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrAppBarGlobalSettingsAction extends StatelessWidget { + const ReadarrAppBarGlobalSettingsAction({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaIconButton( + icon: Icons.more_vert_rounded, + onPressed: () async { + Tuple2 values = + await ReadarrDialogs().globalSettings(context); + if (values.item1) values.item2!.execute(context); + }, + ); + } +} diff --git a/lib/modules/readarr/routes/readarr/widgets/navigation_bar.dart b/lib/modules/readarr/routes/readarr/widgets/navigation_bar.dart new file mode 100644 index 0000000000..8315ccd50e --- /dev/null +++ b/lib/modules/readarr/routes/readarr/widgets/navigation_bar.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; + +class ReadarrNavigationBar extends StatelessWidget { + final PageController? pageController; + static List scrollControllers = List.generate( + icons.length, + (_) => ScrollController(), + ); + + static const List icons = [ + Icons.people_rounded, + Icons.insert_invitation_rounded, + Icons.event_busy_rounded, + Icons.more_horiz_rounded, + ]; + + static List get titles => [ + 'readarr.Authors'.tr(), + 'readarr.Upcoming'.tr(), + 'readarr.Missing'.tr(), + 'readarr.More'.tr(), + ]; + + const ReadarrNavigationBar({ + Key? key, + required this.pageController, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LunaBottomNavigationBar( + pageController: pageController, + scrollControllers: scrollControllers, + icons: icons, + titles: titles, + ); + } +} diff --git a/lib/modules/readarr/routes/releases.dart b/lib/modules/readarr/routes/releases.dart new file mode 100644 index 0000000000..50a4785c1e --- /dev/null +++ b/lib/modules/readarr/routes/releases.dart @@ -0,0 +1,3 @@ +export 'releases/route.dart'; +export 'releases/state.dart'; +export 'releases/widgets.dart'; diff --git a/lib/modules/readarr/routes/releases/route.dart b/lib/modules/readarr/routes/releases/route.dart new file mode 100644 index 0000000000..770e45766a --- /dev/null +++ b/lib/modules/readarr/routes/releases/route.dart @@ -0,0 +1,200 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrReleasesRouter extends ReadarrPageRouter { + ReadarrReleasesRouter() : super('/readarr/releases'); + + @override + Widget widget({ + int? bookId, + int? authorId, + }) => + _Widget( + bookId: bookId, + authorId: authorId, + ); + + @override + Future navigateTo( + BuildContext context, { + int? bookId, + int? authorId, + }) async => + LunaRouter.router.navigateTo( + context, + route( + bookId: bookId, + authorId: authorId, + ), + ); + + @override + String route({ + int? bookId, + int? authorId, + }) { + if (bookId != null) { + return '$fullRoute/book/$bookId'; + } else if (authorId != null) { + return '$fullRoute/author/$authorId'; + } else { + throw Exception('bookId or authorId must be passed to this route'); + } + } + + @override + void defineRoute(FluroRouter router) { + router.define( + '$fullRoute/book/:bookid', + handler: Handler( + handlerFunc: (context, params) { + if (!context!.read().enabled) { + return LunaNotEnabledRoute(module: LunaModule.READARR.name); + } + int bookId = int.tryParse(params['bookid']![0]) ?? -1; + return _Widget( + bookId: bookId, + ); + }, + ), + transitionType: LunaRouter.transitionType, + ); + router.define( + '$fullRoute/author/:authorid', + handler: Handler( + handlerFunc: (context, params) { + if (!context!.read().enabled) { + return LunaNotEnabledRoute(module: LunaModule.READARR.name); + } + int authorId = int.tryParse(params['authorid']![0]) ?? -1; + return _Widget( + authorId: authorId, + ); + }, + ), + transitionType: LunaRouter.transitionType, + ); + } +} + +class _Widget extends StatefulWidget { + final int? bookId; + final int? authorId; + + const _Widget({ + Key? key, + this.bookId, + this.authorId, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State<_Widget> with LunaScrollControllerMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = + GlobalKey(); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => ReadarrReleasesState( + context: context, + bookId: widget.bookId, + authorId: widget.authorId, + ), + builder: (context, _) => LunaScaffold( + scaffoldKey: _scaffoldKey, + appBar: _appBar(context) as PreferredSizeWidget?, + body: _body(context), + ), + ); + } + + Widget _appBar(BuildContext context) { + return LunaAppBar( + title: 'readarr.Releases'.tr(), + scrollControllers: [scrollController], + bottom: ReadarrReleasesSearchBar(scrollController: scrollController), + ); + } + + Widget _body(BuildContext context) { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: () async { + context.read().refreshReleases(context); + await context.read().releases; + }, + child: FutureBuilder( + future: context.read().releases, + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasError) { + if (snapshot.connectionState != ConnectionState.waiting) { + LunaLogger().error( + 'Unable to fetch Readarr releases', + snapshot.error, + snapshot.stackTrace, + ); + } + return LunaMessage.error( + onTap: () => _refreshKey.currentState!.show, + ); + } + if (snapshot.hasData) return _list(context, snapshot.data); + return const LunaLoader(); + }, + ), + ); + } + + Widget _list(BuildContext context, List? releases) { + return Consumer( + builder: (context, state, _) { + if (releases?.isEmpty ?? true) { + return LunaMessage( + text: 'readarr.NoReleasesFound'.tr(), + buttonText: 'lunasea.Refresh'.tr(), + onTap: _refreshKey.currentState!.show, + ); + } + List _processed = _filterAndSortReleases( + releases ?? [], + state, + ); + return LunaListViewBuilder( + controller: scrollController, + itemCount: _processed.isEmpty ? 1 : _processed.length, + itemBuilder: (context, index) { + if (_processed.isEmpty) { + return LunaMessage.inList(text: 'readarr.NoReleasesFound'.tr()); + } + return ReadarrReleasesTile(release: _processed[index]); + }, + ); + }, + ); + } + + List _filterAndSortReleases( + List releases, + ReadarrReleasesState state, + ) { + if (releases.isEmpty) return releases; + List filtered = releases.where( + (release) { + String _query = state.searchQuery; + if (_query.isNotEmpty) { + return release.title!.toLowerCase().contains(_query.toLowerCase()); + } + return true; + }, + ).toList(); + filtered = state.filterType.filter(filtered); + filtered = state.sortType.sort(filtered, state.sortAscending); + return filtered; + } +} diff --git a/lib/modules/readarr/routes/releases/state.dart b/lib/modules/readarr/routes/releases/state.dart new file mode 100644 index 0000000000..3162d565b8 --- /dev/null +++ b/lib/modules/readarr/routes/releases/state.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrReleasesState extends ChangeNotifier { + final int? authorId; + final int? bookId; + + ReadarrReleasesState({ + required BuildContext context, + this.authorId, + this.bookId, + }) { + refreshReleases(context); + } + + Future>? _releases; + Future>? get releases => _releases; + void refreshReleases(BuildContext context) { + if (context.read().enabled) { + if (bookId != null) { + _releases = + context.read().api!.release.get(bookId: bookId!); + } else if (authorId != null) { + _releases = context + .read() + .api! + .release + .getAuthorPack(authorId: authorId!) + .then((releases) => releases.toList()); + } else { + throw Exception('Must supply either bookId or authorId'); + } + } + notifyListeners(); + } + + String _searchQuery = ''; + String get searchQuery => _searchQuery; + set searchQuery(String searchQuery) { + _searchQuery = searchQuery; + notifyListeners(); + } + + ReadarrReleasesFilter? _filterType = + ReadarrDatabaseValue.DEFAULT_FILTERING_RELEASES.data; + ReadarrReleasesFilter get filterType => _filterType!; + set filterType(ReadarrReleasesFilter filterType) { + _filterType = filterType; + notifyListeners(); + } + + ReadarrReleasesSorting? _sortType = + ReadarrDatabaseValue.DEFAULT_SORTING_RELEASES.data; + ReadarrReleasesSorting get sortType => _sortType!; + set sortType(ReadarrReleasesSorting sortType) { + _sortType = sortType; + notifyListeners(); + } + + bool? _sortAscending = + ReadarrDatabaseValue.DEFAULT_SORTING_RELEASES_ASCENDING.data; + bool get sortAscending => _sortAscending!; + set sortAscending(bool sortAscending) { + _sortAscending = sortAscending; + notifyListeners(); + } +} diff --git a/lib/modules/readarr/routes/releases/widgets.dart b/lib/modules/readarr/routes/releases/widgets.dart new file mode 100644 index 0000000000..410e7d79df --- /dev/null +++ b/lib/modules/readarr/routes/releases/widgets.dart @@ -0,0 +1,4 @@ +export 'widgets/release_tile.dart'; +export 'widgets/search_bar.dart'; +export 'widgets/search_bar_filter_button.dart'; +export 'widgets/search_bar_sort_button.dart'; diff --git a/lib/modules/readarr/routes/releases/widgets/release_tile.dart b/lib/modules/readarr/routes/releases/widgets/release_tile.dart new file mode 100644 index 0000000000..3050975f32 --- /dev/null +++ b/lib/modules/readarr/routes/releases/widgets/release_tile.dart @@ -0,0 +1,170 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrReleasesTile extends StatefulWidget { + final ReadarrRelease release; + + const ReadarrReleasesTile({ + required this.release, + Key? key, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + LunaLoadingState _downloadState = LunaLoadingState.INACTIVE; + + @override + Widget build(BuildContext context) { + return LunaExpandableListTile( + title: widget.release.title!, + collapsedSubtitles: [ + _subtitle1(), + _subtitle2(), + ], + collapsedTrailing: _trailing(), + expandedHighlightedNodes: _highlightedNodes(), + expandedTableContent: _tableContent(), + expandedTableButtons: _tableButtons(), + ); + } + + Widget _trailing() { + return LunaIconButton( + icon: widget.release.lunaTrailingIcon, + color: widget.release.lunaTrailingColor, + onPressed: () async => + widget.release.rejected! ? _showWarnings() : _startDownload(), + onLongPress: _startDownload, + loadingState: _downloadState, + ); + } + + TextSpan _subtitle1() { + return TextSpan( + children: [ + TextSpan( + text: widget.release.lunaProtocol, + style: TextStyle( + color: widget.release.protocol!.lunaProtocolColor( + release: widget.release, + ), + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + ), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + TextSpan(text: widget.release.lunaIndexer), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + TextSpan(text: widget.release.lunaAge), + ], + ); + } + + TextSpan _subtitle2() { + String? _preferredWordScore = + widget.release.lunaPreferredWordScore(nullOnEmpty: true); + return TextSpan( + children: [ + if (_preferredWordScore != null) + TextSpan( + text: _preferredWordScore, + style: const TextStyle( + color: LunaColours.purple, + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + ), + ), + if (_preferredWordScore != null) + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + TextSpan(text: widget.release.lunaQuality), + TextSpan(text: LunaUI.TEXT_BULLET.lunaPad()), + TextSpan(text: widget.release.lunaSize), + ], + ); + } + + List _highlightedNodes() { + return [ + LunaHighlightedNode( + text: widget.release.protocol!.lunaReadable(), + backgroundColor: widget.release.protocol!.lunaProtocolColor( + release: widget.release, + ), + ), + if (widget.release.lunaPreferredWordScore(nullOnEmpty: true) != null) + LunaHighlightedNode( + text: widget.release.lunaPreferredWordScore()!, + backgroundColor: LunaColours.purple, + ), + ]; + } + + List _tableContent() { + return [ + LunaTableContent( + title: 'readarr.Age'.tr(), + body: widget.release.lunaAge, + ), + LunaTableContent( + title: 'readarr.Indexer'.tr(), + body: widget.release.lunaIndexer, + ), + LunaTableContent( + title: 'readarr.Size'.tr(), + body: widget.release.lunaSize, + ), + LunaTableContent( + title: 'readarr.Quality'.tr(), + body: widget.release.lunaQuality, + ), + if (widget.release.seeders != null) + LunaTableContent( + title: 'readarr.Seeders'.tr(), + body: '${widget.release.seeders}', + ), + if (widget.release.leechers != null) + LunaTableContent( + title: 'readarr.Leechers'.tr(), + body: '${widget.release.leechers}', + ), + ]; + } + + List _tableButtons() { + return [ + LunaButton( + type: LunaButtonType.TEXT, + text: 'readarr.Download'.tr(), + icon: Icons.download_rounded, + onTap: _startDownload, + loadingState: _downloadState, + ), + if (widget.release.rejected!) + LunaButton.text( + text: 'readarr.Rejected'.tr(), + icon: Icons.report_outlined, + color: LunaColours.red, + onTap: _showWarnings, + ), + ]; + } + + Future _startDownload() async { + Future setDownloadState(LunaLoadingState state) async { + if (this.mounted) setState(() => _downloadState = state); + } + + setDownloadState(LunaLoadingState.ACTIVE); + ReadarrAPIController() + .downloadRelease( + context: context, + release: widget.release, + ) + .whenComplete(() async => setDownloadState(LunaLoadingState.INACTIVE)); + } + + Future _showWarnings() async => await LunaDialogs() + .showRejections(context, widget.release.rejections ?? []); +} diff --git a/lib/modules/readarr/routes/releases/widgets/search_bar.dart b/lib/modules/readarr/routes/releases/widgets/search_bar.dart new file mode 100644 index 0000000000..505a081ea4 --- /dev/null +++ b/lib/modules/readarr/routes/releases/widgets/search_bar.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrReleasesSearchBar extends StatefulWidget + implements PreferredSizeWidget { + final ScrollController scrollController; + + const ReadarrReleasesSearchBar({ + Key? key, + required this.scrollController, + }) : super(key: key); + + @override + Size get preferredSize => + const Size.fromHeight(LunaTextInputBar.defaultAppBarHeight); + + @override + State createState() => _State(); +} + +class _State extends State { + final TextEditingController _controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: Consumer( + builder: (context, state, _) => LunaTextInputBar( + controller: _controller, + scrollController: widget.scrollController, + autofocus: false, + onChanged: (value) => + context.read().searchQuery = value, + margin: const EdgeInsets.fromLTRB(12.0, 0.0, 12.0, 14.0), + ), + ), + ), + ReadarrReleasesAppBarFilterButton(controller: widget.scrollController), + ReadarrReleasesAppBarSortButton(controller: widget.scrollController), + ], + ); + } +} diff --git a/lib/modules/readarr/routes/releases/widgets/search_bar_filter_button.dart b/lib/modules/readarr/routes/releases/widgets/search_bar_filter_button.dart new file mode 100644 index 0000000000..d5387fe511 --- /dev/null +++ b/lib/modules/readarr/routes/releases/widgets/search_bar_filter_button.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrReleasesAppBarFilterButton extends StatefulWidget { + final ScrollController controller; + + const ReadarrReleasesAppBarFilterButton({ + Key? key, + required this.controller, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return LunaCard( + context: context, + child: Consumer( + builder: (context, state, _) => + LunaPopupMenuButton( + tooltip: 'readarr.FilterReleases'.tr(), + icon: Icons.filter_list_rounded, + onSelected: (result) { + state.filterType = result; + widget.controller.lunaAnimateToStart(); + }, + itemBuilder: (context) => + List>.generate( + ReadarrReleasesFilter.values.length, + (index) => PopupMenuItem( + value: ReadarrReleasesFilter.values[index], + child: Text( + ReadarrReleasesFilter.values[index].readable, + style: TextStyle( + fontSize: LunaUI.FONT_SIZE_H3, + color: state.filterType == ReadarrReleasesFilter.values[index] + ? LunaColours.accent + : Colors.white, + ), + ), + ), + ), + ), + ), + height: LunaTextInputBar.defaultHeight, + width: LunaTextInputBar.defaultHeight, + margin: const EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 14.0), + color: Theme.of(context).canvasColor, + ); + } +} diff --git a/lib/modules/readarr/routes/releases/widgets/search_bar_sort_button.dart b/lib/modules/readarr/routes/releases/widgets/search_bar_sort_button.dart new file mode 100644 index 0000000000..73e2615a08 --- /dev/null +++ b/lib/modules/readarr/routes/releases/widgets/search_bar_sort_button.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrReleasesAppBarSortButton extends StatefulWidget { + final ScrollController controller; + + const ReadarrReleasesAppBarSortButton({ + Key? key, + required this.controller, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return LunaCard( + context: context, + child: Consumer( + builder: (context, state, _) => + LunaPopupMenuButton( + tooltip: 'readarr.SortReleases'.tr(), + icon: Icons.sort_rounded, + onSelected: (result) { + if (state.sortType == result) { + state.sortAscending = !state.sortAscending; + } else { + state.sortAscending = true; + state.sortType = result; + } + widget.controller.lunaAnimateToStart(); + }, + itemBuilder: (context) => + List>.generate( + ReadarrReleasesSorting.values.length, + (index) => PopupMenuItem( + value: ReadarrReleasesSorting.values[index], + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + ReadarrReleasesSorting.values[index].readable, + style: TextStyle( + fontSize: LunaUI.FONT_SIZE_H3, + color: + state.sortType == ReadarrReleasesSorting.values[index] + ? LunaColours.accent + : Colors.white, + ), + ), + if (state.sortType == ReadarrReleasesSorting.values[index]) + Icon( + state.sortAscending + ? Icons.arrow_upward_rounded + : Icons.arrow_downward_rounded, + size: LunaUI.FONT_SIZE_H2, + color: LunaColours.accent, + ), + ], + ), + ), + ), + ), + ), + height: LunaTextInputBar.defaultHeight, + width: LunaTextInputBar.defaultHeight, + margin: const EdgeInsets.fromLTRB(0.0, 0.0, 12.0, 13.5), + color: Theme.of(context).canvasColor, + ); + } +} diff --git a/lib/modules/readarr/routes/tags.dart b/lib/modules/readarr/routes/tags.dart new file mode 100644 index 0000000000..3b3bba4d28 --- /dev/null +++ b/lib/modules/readarr/routes/tags.dart @@ -0,0 +1,2 @@ +export 'tags/route.dart'; +export 'tags/widgets.dart'; diff --git a/lib/modules/readarr/routes/tags/route.dart b/lib/modules/readarr/routes/tags/route.dart new file mode 100644 index 0000000000..f53a6f3d3a --- /dev/null +++ b/lib/modules/readarr/routes/tags/route.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrTagsRouter extends ReadarrPageRouter { + ReadarrTagsRouter() : super('/readarr/tags'); + + @override + _Widget widget() => _Widget(); + + @override + void defineRoute(FluroRouter router) { + super.noParameterRouteDefinition(router); + } +} + +class _Widget extends StatefulWidget { + @override + State createState() => _State(); +} + +class _State extends State<_Widget> + with LunaScrollControllerMixin, LunaLoadCallbackMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = + GlobalKey(); + + @override + Future loadCallback() async { + context.read().fetchTags(); + await context.read().tags; + } + + @override + Widget build(BuildContext context) { + return LunaScaffold( + scaffoldKey: _scaffoldKey, + appBar: _appBar() as PreferredSizeWidget?, + body: _body(), + ); + } + + Widget _appBar() { + return LunaAppBar( + title: 'Tags', + scrollControllers: [scrollController], + actions: const [ + ReadarrTagsAppBarActionAddTag(), + ], + ); + } + + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: FutureBuilder( + future: context.watch().tags, + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasError) { + if (snapshot.connectionState != ConnectionState.waiting) { + LunaLogger().error( + 'Unable to fetch Readarr tags', + snapshot.error, + snapshot.stackTrace, + ); + } + return LunaMessage.error(onTap: _refreshKey.currentState!.show); + } + if (snapshot.hasData) return _list(snapshot.data); + return const LunaLoader(); + }, + ), + ); + } + + Widget _list(List? tags) { + if ((tags?.length ?? 0) == 0) + return LunaMessage( + text: 'readarr.NoTagsFound'.tr(), + buttonText: 'lunasea.Refresh'.tr(), + onTap: _refreshKey.currentState?.show, + ); + return LunaListViewBuilder( + controller: scrollController, + itemCount: tags!.length, + itemBuilder: (context, index) => ReadarrTagsTagTile( + key: ObjectKey(tags[index].id), + tag: tags[index], + ), + ); + } +} diff --git a/lib/modules/readarr/routes/tags/widgets.dart b/lib/modules/readarr/routes/tags/widgets.dart new file mode 100644 index 0000000000..913899a372 --- /dev/null +++ b/lib/modules/readarr/routes/tags/widgets.dart @@ -0,0 +1,2 @@ +export 'widgets/appbar_action_add_tag.dart'; +export 'widgets/tag_tile.dart'; diff --git a/lib/modules/readarr/routes/tags/widgets/appbar_action_add_tag.dart b/lib/modules/readarr/routes/tags/widgets/appbar_action_add_tag.dart new file mode 100644 index 0000000000..1c5fc7e565 --- /dev/null +++ b/lib/modules/readarr/routes/tags/widgets/appbar_action_add_tag.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrTagsAppBarActionAddTag extends StatelessWidget { + final bool asDialogButton; + + const ReadarrTagsAppBarActionAddTag({ + Key? key, + this.asDialogButton = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (asDialogButton) + return LunaDialog.button( + text: 'lunasea.Add'.tr(), + textColor: Colors.white, + onPressed: () async => _onPressed(context), + ); + return LunaIconButton( + icon: Icons.add_rounded, + onPressed: () async => _onPressed(context), + ); + } + + Future _onPressed(BuildContext context) async { + Tuple2 result = await ReadarrDialogs().addNewTag(context); + if (result.item1) + ReadarrAPIController() + .addTag(context: context, label: result.item2) + .then((value) { + if (value) context.read().fetchTags(); + }); + } +} diff --git a/lib/modules/readarr/routes/tags/widgets/tag_tile.dart b/lib/modules/readarr/routes/tags/widgets/tag_tile.dart new file mode 100644 index 0000000000..c530f08790 --- /dev/null +++ b/lib/modules/readarr/routes/tags/widgets/tag_tile.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrTagsTagTile extends StatefulWidget { + final ReadarrTag tag; + + const ReadarrTagsTagTile({ + Key? key, + required this.tag, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State with LunaLoadCallbackMixin { + List? seriesList; + + @override + Future loadCallback() async { + await context.read().authors!.then((series) { + List _series = []; + series.values.forEach((element) { + if (element.tags!.contains(widget.tag.id)) _series.add(element.title); + }); + _series.sort(); + if (mounted) + setState(() { + seriesList = _series; + }); + }).catchError((error) { + if (mounted) + setState(() { + seriesList = null; + }); + }); + } + + @override + Widget build(BuildContext context) { + return LunaBlock( + title: widget.tag.label, + body: [TextSpan(text: subtitle())], + trailing: (seriesList?.isNotEmpty ?? true) + ? null + : LunaIconButton( + icon: LunaIcons.DELETE, + color: LunaColours.red, + onPressed: _handleDelete, + ), + onTap: _handleInfo, + ); + } + + String subtitle() { + if (seriesList == null) return 'Loading...'; + if (seriesList!.isEmpty) return 'No Series'; + return '${seriesList!.length} Series'; + } + + Future _handleInfo() async { + return LunaDialogs().textPreview( + context, + 'Series List', + (seriesList?.isEmpty ?? true) ? 'No Series' : seriesList!.join('\n'), + ); + } + + Future _handleDelete() async { + if (seriesList?.isNotEmpty ?? true) { + showLunaErrorSnackBar( + title: 'Cannot Delete Tag', + message: 'The tag must not be attached to any series', + ); + } else { + bool result = await ReadarrDialogs().deleteTag(context); + if (result) + context + .read() + .api! + .tag + .delete(id: widget.tag.id!) + .then((_) { + showLunaSuccessSnackBar( + title: 'Deleted Tag', + message: widget.tag.label, + ); + context.read().fetchTags(); + }).catchError((error, stack) { + LunaLogger().error( + 'Failed to delete tag: ${widget.tag.id}', + error, + stack, + ); + showLunaErrorSnackBar( + title: 'Failed to Delete Tag', + error: error, + ); + }); + } + } +} diff --git a/lib/modules/readarr/routes/upcoming.dart b/lib/modules/readarr/routes/upcoming.dart new file mode 100644 index 0000000000..be57f945fb --- /dev/null +++ b/lib/modules/readarr/routes/upcoming.dart @@ -0,0 +1,2 @@ +export 'upcoming/route.dart'; +export 'upcoming/widgets.dart'; diff --git a/lib/modules/readarr/routes/upcoming/route.dart b/lib/modules/readarr/routes/upcoming/route.dart new file mode 100644 index 0000000000..28fea3e256 --- /dev/null +++ b/lib/modules/readarr/routes/upcoming/route.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrUpcomingRoute extends StatefulWidget { + const ReadarrUpcomingRoute({ + Key? key, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State + with AutomaticKeepAliveClientMixin, LunaLoadCallbackMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + final GlobalKey _refreshKey = + GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + Future loadCallback() async { + context.read().fetchUpcoming(); + await context.read().upcoming; + } + + @override + Widget build(BuildContext context) { + super.build(context); + return LunaScaffold( + scaffoldKey: _scaffoldKey, + module: LunaModule.READARR, + hideDrawer: true, + body: _body(), + ); + } + + Widget _body() { + return LunaRefreshIndicator( + context: context, + key: _refreshKey, + onRefresh: loadCallback, + child: Selector< + ReadarrState, + Tuple2>?, + Future>?>>( + selector: (_, state) => Tuple2(state.authors, state.upcoming), + builder: (context, tuple, _) => FutureBuilder( + future: Future.wait([tuple.item1!, tuple.item2!]), + builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.hasError) { + if (snapshot.connectionState != ConnectionState.waiting) { + LunaLogger().error( + 'Unable to fetch Readarr upcoming episodes', + snapshot.error, + snapshot.stackTrace, + ); + } + return LunaMessage.error(onTap: _refreshKey.currentState!.show); + } + if (snapshot.hasData) + return _episodes( + snapshot.data![0] as Map, + snapshot.data![1] as List, + ); + return const LunaLoader(); + }, + ), + ), + ); + } + + Widget _episodes( + Map series, + List upcoming, + ) { + if (upcoming.isEmpty) { + return LunaMessage( + text: 'readarr.NoEpisodesFound'.tr(), + buttonText: 'lunasea.Refresh'.tr(), + onTap: _refreshKey.currentState?.show, + ); + } + // Split episodes into days into a map + Map> _episodeMap = + upcoming.fold({}, (map, entry) { + if (entry.releaseDate == null) return map; + String _date = DateFormat('y-MM-dd').format(entry.releaseDate!.toLocal()); + if (!map.containsKey(_date)) + map[_date] = { + 'date': DateFormat('EEEE / MMMM dd, y') + .format(entry.releaseDate!.toLocal()), + 'entries': [], + }; + (map[_date]!['entries'] as List).add(entry); + return map; + }); + // Build the widgets + List> _episodeWidgets = []; + _episodeMap.keys.toList() + ..sort() + ..forEach((key) => { + _episodeWidgets.add(_buildDay( + (_episodeMap[key]!['date'] as String?), + (_episodeMap[key]!['entries'] as List).cast(), + series, + )), + }); + // Return the list + return LunaListView( + controller: ReadarrNavigationBar.scrollControllers[1], + children: _episodeWidgets.expand((e) => e).toList(), + ); + } + + List _buildDay( + String? date, + List upcoming, + Map series, + ) => + [ + LunaHeader( + text: date, + // subtitle: 'This is a test', + ), + ...List.generate( + upcoming.length, + (index) => ReadarrUpcomingTile( + record: upcoming[index], + series: series[upcoming[index].authorId!], + ), + ), + ]; +} diff --git a/lib/modules/readarr/routes/upcoming/widgets.dart b/lib/modules/readarr/routes/upcoming/widgets.dart new file mode 100644 index 0000000000..e1c776b442 --- /dev/null +++ b/lib/modules/readarr/routes/upcoming/widgets.dart @@ -0,0 +1 @@ +export 'widgets/upcoming_tile.dart'; diff --git a/lib/modules/readarr/routes/upcoming/widgets/upcoming_tile.dart b/lib/modules/readarr/routes/upcoming/widgets/upcoming_tile.dart new file mode 100644 index 0000000000..bd9fed3c0b --- /dev/null +++ b/lib/modules/readarr/routes/upcoming/widgets/upcoming_tile.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class ReadarrUpcomingTile extends StatefulWidget { + final ReadarrBook record; + final ReadarrAuthor? series; + + const ReadarrUpcomingTile({ + Key? key, + required this.record, + this.series, + }) : super(key: key); + + @override + State createState() => _State(); +} + +class _State extends State { + @override + Widget build(BuildContext context) { + return LunaBlock( + posterUrl: context.read().getBookCoverURL(widget.record.id), + backgroundUrl: context + .read() + .getAuthorPosterURL(widget.record.authorId), + posterHeaders: context.read().headers, + posterPlaceholderIcon: LunaIcons.BOOK, + title: widget.record.series?.title ?? + widget.series?.title ?? + LunaUI.TEXT_EMDASH, + body: [ + _subtitle1(), + _subtitle2(), + ], + disabled: !widget.record.monitored!, + onTap: _onTap, + onLongPress: _onLongPress, + trailing: _trailing(), + ); + } + + Widget _trailing() => LunaIconButton( + icon: Icons.search_rounded, + onPressed: _trailingOnPressed, + onLongPress: _trailingOnLongPress, + ); + + TextSpan _subtitle1() { + return TextSpan( + style: const TextStyle(fontStyle: FontStyle.italic), + children: [ + TextSpan(text: widget.record.title ?? 'Unknown Title'), + ], + ); + } + + TextSpan _subtitle2() { + return TextSpan( + text: [ + (widget.record.pageCount ?? 0) > 0 + ? widget.record.pageCount + : LunaUI.TEXT_EMDASH, + 'readarr.Pages'.tr() + ].join(' ')); + } + +/* +TO FIX - need to fetch book files + TextSpan _subtitle3() { + Color color = widget.record.hasFile! + ? LunaColours.accent + : widget.record.lunaHasAired + ? LunaColours.red + : LunaColours.blue; + return TextSpan( + style: TextStyle( + fontWeight: LunaUI.FONT_WEIGHT_BOLD, + color: color, + ), + children: [ + if (!widget.record.hasFile!) + TextSpan(text: widget.record.lunaHasAired ? 'Missing' : 'Unreleased'), + if (widget.record.hasFile!) + TextSpan( + text: + 'Downloaded (${widget.record.episodeFile?.quality?.quality?.name ?? 'Unknown'})', + ), + ], + ); + }*/ + + Future _onTap() async => ReadarrBookDetailsRouter().navigateTo( + context, + widget.record.id ?? -1, + ); + + Future _onLongPress() async => ReadarrAuthorDetailsRouter().navigateTo( + context, + widget.record.authorId!, + ); + + Future _trailingOnPressed() async { + Provider.of(context, listen: false) + .api! + .command + .bookSearch(bookIds: [widget.record.id!]) + .then((_) => showLunaSuccessSnackBar( + title: 'Searching for Book...', + message: widget.record.title, + )) + .catchError((error, stack) { + LunaLogger().error( + 'Failed to search for book: ${widget.record.id}', error, stack); + showLunaErrorSnackBar( + title: 'Failed to Search', + error: error, + ); + }); + } + + Future _trailingOnLongPress() async => + ReadarrReleasesRouter().navigateTo( + context, + bookId: widget.record.id, + ); +} diff --git a/lib/modules/settings/core/pages/headers.dart b/lib/modules/settings/core/pages/headers.dart index 6fad66514b..547c4b0104 100644 --- a/lib/modules/settings/core/pages/headers.dart +++ b/lib/modules/settings/core/pages/headers.dart @@ -100,6 +100,8 @@ class _State extends State with LunaScrollControllerMixin { return LunaProfile.current.lidarrHeaders; case LunaModule.RADARR: return LunaProfile.current.radarrHeaders; + case LunaModule.READARR: + return LunaProfile.current.readarrHeaders; case LunaModule.SONARR: return LunaProfile.current.sonarrHeaders; case LunaModule.SABNZBD: @@ -129,6 +131,8 @@ class _State extends State with LunaScrollControllerMixin { return; case LunaModule.RADARR: return context.read().reset(); + case LunaModule.READARR: + return context.read().reset(); case LunaModule.SONARR: return context.read().reset(); case LunaModule.SABNZBD: diff --git a/lib/modules/settings/core/router.dart b/lib/modules/settings/core/router.dart index 38953a4e1c..dcb7c98ae5 100644 --- a/lib/modules/settings/core/router.dart +++ b/lib/modules/settings/core/router.dart @@ -41,6 +41,12 @@ class SettingsRouter extends LunaModuleRouter { SettingsConfigurationRadarrDefaultOptionsRouter().defineRoute(router); SettingsConfigurationRadarrDefaultPagesRouter().defineRoute(router); SettingsConfigurationRadarrHeadersRouter().defineRoute(router); + // Configuration/Readarr + SettingsConfigurationReadarrRouter().defineRoute(router); + SettingsConfigurationReadarrConnectionDetailsRouter().defineRoute(router); + SettingsConfigurationReadarrDefaultOptionsRouter().defineRoute(router); + SettingsConfigurationReadarrDefaultPagesRouter().defineRoute(router); + SettingsConfigurationReadarrHeadersRouter().defineRoute(router); // Configuration/SABnzbd SettingsConfigurationSABnzbdRouter().defineRoute(router); SettingsConfigurationSABnzbdConnectionDetailsRouter().defineRoute(router); diff --git a/lib/modules/settings/routes.dart b/lib/modules/settings/routes.dart index eda32f13d9..35ae1468bb 100644 --- a/lib/modules/settings/routes.dart +++ b/lib/modules/settings/routes.dart @@ -11,6 +11,7 @@ export 'routes/configuration_nzbget.dart'; export 'routes/configuration_overseerr.dart'; export 'routes/configuration_quickactions.dart'; export 'routes/configuration_radarr.dart'; +export 'routes/configuration_readarr.dart'; export 'routes/configuration_sabnzbd.dart'; export 'routes/configuration_search.dart'; export 'routes/configuration_sonarr.dart'; diff --git a/lib/modules/settings/routes/configuration_readarr.dart b/lib/modules/settings/routes/configuration_readarr.dart new file mode 100644 index 0000000000..8bfd6b8a49 --- /dev/null +++ b/lib/modules/settings/routes/configuration_readarr.dart @@ -0,0 +1,2 @@ +export 'configuration_readarr/pages.dart'; +export 'configuration_readarr/route.dart'; diff --git a/lib/modules/settings/routes/configuration_readarr/pages.dart b/lib/modules/settings/routes/configuration_readarr/pages.dart new file mode 100644 index 0000000000..962d6d84e1 --- /dev/null +++ b/lib/modules/settings/routes/configuration_readarr/pages.dart @@ -0,0 +1,4 @@ +export 'pages/connection_details.dart'; +export 'pages/default_options.dart'; +export 'pages/default_pages.dart'; +export 'pages/headers.dart'; diff --git a/lib/modules/settings/routes/configuration_readarr/pages/connection_details.dart b/lib/modules/settings/routes/configuration_readarr/pages/connection_details.dart new file mode 100644 index 0000000000..c0fe1f48cf --- /dev/null +++ b/lib/modules/settings/routes/configuration_readarr/pages/connection_details.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/settings.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class SettingsConfigurationReadarrConnectionDetailsRouter + extends SettingsPageRouter { + SettingsConfigurationReadarrConnectionDetailsRouter() + : super('/settings/configuration/readarr/connection'); + + @override + _Widget widget() => _Widget(); + + @override + void defineRoute(FluroRouter router) => + super.noParameterRouteDefinition(router); +} + +class _Widget extends StatefulWidget { + @override + State<_Widget> createState() => _State(); +} + +class _State extends State<_Widget> with LunaScrollControllerMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return LunaScaffold( + scaffoldKey: _scaffoldKey, + appBar: _appBar() as PreferredSizeWidget?, + body: _body(), + bottomNavigationBar: _bottomActionBar(), + ); + } + + Widget _appBar() { + return LunaAppBar( + title: 'Connection Details', + scrollControllers: [scrollController], + ); + } + + Widget _bottomActionBar() { + return LunaBottomActionBar( + actions: [ + _testConnection(), + ], + ); + } + + Widget _body() { + return ValueListenableBuilder( + valueListenable: Database.profiles.box.listenable(), + builder: (context, dynamic box, _) => LunaListView( + controller: scrollController, + children: [ + _host(), + _apiKey(), + _customHeaders(), + ], + ), + ); + } + + Widget _host() { + String host = LunaProfile.current.readarrHost ?? ''; + return LunaBlock( + title: 'settings.Host'.tr(), + body: [TextSpan(text: host.isEmpty ? 'lunasea.NotSet'.tr() : host)], + trailing: const LunaIconButton.arrow(), + onTap: () async { + Tuple2 _values = await SettingsDialogs().editHost( + context, + prefill: host, + ); + if (_values.item1) { + LunaProfile.current.readarrHost = _values.item2; + LunaProfile.current.save(); + context.read().reset(); + } + }, + ); + } + + Widget _apiKey() { + String apiKey = LunaProfile.current.readarrKey ?? ''; + return LunaBlock( + title: 'settings.ApiKey'.tr(), + body: [ + TextSpan( + text: apiKey.isEmpty + ? 'lunasea.NotSet'.tr() + : LunaUI.TEXT_OBFUSCATED_PASSWORD, + ), + ], + trailing: const LunaIconButton.arrow(), + onTap: () async { + Tuple2 _values = await LunaDialogs().editText( + context, + 'settings.ApiKey'.tr(), + prefill: apiKey, + ); + if (_values.item1) { + LunaProfile.current.readarrKey = _values.item2; + LunaProfile.current.save(); + context.read().reset(); + } + }, + ); + } + + Widget _testConnection() { + return LunaButton.text( + text: 'settings.TestConnection'.tr(), + icon: LunaIcons.CONNECTION_TEST, + onTap: () async { + ProfileHiveObject _profile = LunaProfile.current; + if (_profile.readarrHost == null || _profile.readarrHost!.isEmpty) { + showLunaErrorSnackBar( + title: 'Host Required', + message: 'Host is required to connect to Readarr', + ); + return; + } + if (_profile.readarrKey == null || _profile.readarrKey!.isEmpty) { + showLunaErrorSnackBar( + title: 'API Key Required', + message: 'API key is required to connect to Readarr', + ); + return; + } + Readarr( + host: _profile.readarrHost!, + apiKey: _profile.readarrKey!, + headers: Map.from( + _profile.readarrHeaders ?? {}, + ), + ).system.getStatus().then((_) { + showLunaSuccessSnackBar( + title: 'Connected Successfully', + message: 'Readarr is ready to use with LunaSea', + ); + }).catchError((error, trace) { + LunaLogger().error( + 'Connection Test Failed', + error, + trace, + ); + showLunaErrorSnackBar( + title: 'Connection Test Failed', + error: error, + ); + }); + }, + ); + } + + Widget _customHeaders() { + return LunaBlock( + title: 'settings.CustomHeaders'.tr(), + body: [TextSpan(text: 'settings.CustomHeadersDescription'.tr())], + trailing: const LunaIconButton.arrow(), + onTap: () async => SettingsConfigurationReadarrHeadersRouter().navigateTo( + context, + ), + ); + } +} diff --git a/lib/modules/settings/routes/configuration_readarr/pages/default_options.dart b/lib/modules/settings/routes/configuration_readarr/pages/default_options.dart new file mode 100644 index 0000000000..36286de5b2 --- /dev/null +++ b/lib/modules/settings/routes/configuration_readarr/pages/default_options.dart @@ -0,0 +1,249 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/settings.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class SettingsConfigurationReadarrDefaultOptionsRouter + extends SettingsPageRouter { + SettingsConfigurationReadarrDefaultOptionsRouter() + : super('/settings/configuration/readarr/options'); + + @override + _Widget widget() => _Widget(); + + @override + void defineRoute(FluroRouter router) => + super.noParameterRouteDefinition(router); +} + +class _Widget extends StatefulWidget { + @override + State<_Widget> createState() => _State(); +} + +class _State extends State<_Widget> with LunaScrollControllerMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return LunaScaffold( + scaffoldKey: _scaffoldKey, + appBar: _appBar() as PreferredSizeWidget?, + body: _body(), + ); + } + + Widget _appBar() { + return LunaAppBar( + title: 'settings.DefaultOptions'.tr(), + scrollControllers: [scrollController], + ); + } + + Widget _body() { + return LunaListView( + controller: scrollController, + children: [ + LunaHeader(text: 'readarr.Authors'.tr()), + _filteringSeries(), + _sortingSeries(), + _sortingSeriesDirection(), + _viewSeries(), + LunaHeader(text: 'readarr.Releases'.tr()), + _filteringReleases(), + _sortingReleases(), + _sortingReleasesDirection(), + ], + ); + } + + Widget _viewSeries() { + ReadarrDatabaseValue _db = ReadarrDatabaseValue.DEFAULT_VIEW_SERIES; + return _db.listen( + builder: (context, box, _) { + LunaListViewOption _view = _db.data; + return LunaBlock( + title: 'lunasea.View'.tr(), + body: [TextSpan(text: _view.readable)], + trailing: const LunaIconButton.arrow(), + onTap: () async { + List titles = LunaListViewOption.values + .map((view) => view.readable) + .toList(); + List icons = LunaListViewOption.values + .map((view) => view.icon) + .toList(); + + Tuple2 values = await SettingsDialogs().setDefaultOption( + context, + title: 'lunasea.View'.tr(), + values: titles, + icons: icons, + ); + + if (values.item1) { + LunaListViewOption _opt = LunaListViewOption.values[values.item2]; + context.read().seriesViewType = _opt; + _db.put(_opt); + } + }, + ); + }, + ); + } + + Widget _sortingSeries() { + ReadarrDatabaseValue _db = ReadarrDatabaseValue.DEFAULT_SORTING_SERIES; + return _db.listen( + builder: (context, box, _) => LunaBlock( + title: 'settings.SortCategory'.tr(), + body: [TextSpan(text: (_db.data as ReadarrAuthorSorting).readable)], + trailing: const LunaIconButton.arrow(), + onTap: () async { + List titles = ReadarrAuthorSorting.values + .map((sorting) => sorting.readable) + .toList(); + List icons = List.filled(titles.length, LunaIcons.SORT); + + Tuple2 values = await SettingsDialogs().setDefaultOption( + context, + title: 'settings.SortCategory'.tr(), + values: titles, + icons: icons, + ); + + if (values.item1) { + _db.put(ReadarrAuthorSorting.values[values.item2]); + context.read().seriesSortType = _db.data; + context.read().seriesSortAscending = + ReadarrDatabaseValue.DEFAULT_SORTING_SERIES_ASCENDING.data; + } + }, + ), + ); + } + + Widget _sortingSeriesDirection() { + ReadarrDatabaseValue _db = + ReadarrDatabaseValue.DEFAULT_SORTING_SERIES_ASCENDING; + return _db.listen( + builder: (context, box, _) => LunaBlock( + title: 'settings.SortDirection'.tr(), + body: [ + TextSpan( + text: + _db.data ? 'lunasea.Ascending'.tr() : 'lunasea.Descending'.tr(), + ), + ], + trailing: LunaSwitch( + value: _db.data, + onChanged: (value) => _db.put(value), + ), + ), + ); + } + + Widget _filteringSeries() { + ReadarrDatabaseValue _db = ReadarrDatabaseValue.DEFAULT_FILTERING_SERIES; + return _db.listen( + builder: (context, box, _) => LunaBlock( + title: 'settings.FilterCategory'.tr(), + body: [TextSpan(text: (_db.data as ReadarrAuthorFilter).readable)], + trailing: const LunaIconButton.arrow(), + onTap: () async { + List titles = ReadarrAuthorFilter.values + .map((sorting) => sorting.readable) + .toList(); + List icons = List.filled(titles.length, LunaIcons.FILTER); + + Tuple2 values = await SettingsDialogs().setDefaultOption( + context, + title: 'settings.FilterCategory'.tr(), + values: titles, + icons: icons, + ); + + if (values.item1) { + _db.put(ReadarrAuthorFilter.values[values.item2]); + } + }, + ), + ); + } + + Widget _sortingReleases() { + ReadarrDatabaseValue _db = ReadarrDatabaseValue.DEFAULT_SORTING_RELEASES; + return _db.listen( + builder: (context, box, _) => LunaBlock( + title: 'settings.SortCategory'.tr(), + body: [TextSpan(text: (_db.data as ReadarrReleasesSorting).readable)], + trailing: const LunaIconButton.arrow(), + onTap: () async { + List titles = ReadarrReleasesSorting.values + .map((sorting) => sorting.readable) + .toList(); + List icons = List.filled(titles.length, LunaIcons.SORT); + + Tuple2 values = await SettingsDialogs().setDefaultOption( + context, + title: 'settings.SortCategory'.tr(), + values: titles, + icons: icons, + ); + + if (values.item1) { + _db.put(ReadarrReleasesSorting.values[values.item2]); + } + }, + ), + ); + } + + Widget _sortingReleasesDirection() { + ReadarrDatabaseValue _db = + ReadarrDatabaseValue.DEFAULT_SORTING_RELEASES_ASCENDING; + return _db.listen( + builder: (context, box, _) => LunaBlock( + title: 'settings.SortDirection'.tr(), + body: [ + TextSpan( + text: + _db.data ? 'lunasea.Ascending'.tr() : 'lunasea.Descending'.tr(), + ), + ], + trailing: LunaSwitch( + value: _db.data, + onChanged: (value) => _db.put(value), + ), + ), + ); + } + + Widget _filteringReleases() { + ReadarrDatabaseValue _db = ReadarrDatabaseValue.DEFAULT_FILTERING_RELEASES; + return _db.listen( + builder: (context, box, _) => LunaBlock( + title: 'settings.FilterCategory'.tr(), + body: [TextSpan(text: (_db.data as ReadarrReleasesFilter).readable)], + trailing: const LunaIconButton.arrow(), + onTap: () async { + List titles = ReadarrReleasesFilter.values + .map((sorting) => sorting.readable) + .toList(); + List icons = List.filled(titles.length, LunaIcons.FILTER); + + Tuple2 values = await SettingsDialogs().setDefaultOption( + context, + title: 'settings.FilterCategory'.tr(), + values: titles, + icons: icons, + ); + + if (values.item1) { + _db.put(ReadarrReleasesFilter.values[values.item2]); + } + }, + ), + ); + } +} diff --git a/lib/modules/settings/routes/configuration_readarr/pages/default_pages.dart b/lib/modules/settings/routes/configuration_readarr/pages/default_pages.dart new file mode 100644 index 0000000000..c9cd763d21 --- /dev/null +++ b/lib/modules/settings/routes/configuration_readarr/pages/default_pages.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/settings.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class SettingsConfigurationReadarrDefaultPagesRouter + extends SettingsPageRouter { + SettingsConfigurationReadarrDefaultPagesRouter() + : super('/settings/configuration/readarr/pages'); + + @override + _Widget widget() => _Widget(); + + @override + void defineRoute(FluroRouter router) => + super.noParameterRouteDefinition(router); +} + +class _Widget extends StatefulWidget { + @override + State<_Widget> createState() => _State(); +} + +class _State extends State<_Widget> with LunaScrollControllerMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return LunaScaffold( + scaffoldKey: _scaffoldKey, + appBar: _appBar() as PreferredSizeWidget?, + body: _body(), + ); + } + + Widget _appBar() { + return LunaAppBar( + title: 'settings.DefaultPages'.tr(), + scrollControllers: [scrollController], + ); + } + + Widget _body() { + return LunaListView( + controller: scrollController, + children: [ + _homePage(), + _seriesDetailsPage(), + _seasonDetailsPage(), + ], + ); + } + + Widget _homePage() { + ReadarrDatabaseValue _db = ReadarrDatabaseValue.NAVIGATION_INDEX; + return _db.listen( + builder: (context, box, _) { + return LunaBlock( + title: 'lunasea.Home'.tr(), + body: [TextSpan(text: ReadarrNavigationBar.titles[_db.data])], + trailing: LunaIconButton(icon: ReadarrNavigationBar.icons[_db.data]), + onTap: () async { + List values = await ReadarrDialogs.setDefaultPage( + context, + titles: ReadarrNavigationBar.titles, + icons: ReadarrNavigationBar.icons, + ); + if (values[0]) _db.put(values[1]); + }, + ); + }, + ); + } + + Widget _seriesDetailsPage() { + ReadarrDatabaseValue _db = + ReadarrDatabaseValue.NAVIGATION_INDEX_SERIES_DETAILS; + return _db.listen( + builder: (context, box, _) { + return LunaBlock( + title: 'readarr.AuthorDetails'.tr(), + body: [ + TextSpan(text: ReadarrAuthorDetailsNavigationBar.titles[_db.data]) + ], + trailing: LunaIconButton( + icon: ReadarrAuthorDetailsNavigationBar.icons[_db.data]), + onTap: () async { + List values = await ReadarrDialogs.setDefaultPage( + context, + titles: ReadarrAuthorDetailsNavigationBar.titles, + icons: ReadarrAuthorDetailsNavigationBar.icons, + ); + if (values[0]) _db.put(values[1]); + }, + ); + }, + ); + } + + Widget _seasonDetailsPage() { + ReadarrDatabaseValue _db = + ReadarrDatabaseValue.NAVIGATION_INDEX_SEASON_DETAILS; + return _db.listen( + builder: (context, box, _) { + return LunaBlock( + title: 'readarr.BookDetails'.tr(), + body: [ + TextSpan(text: ReadarrBookDetailsNavigationBar.titles[_db.data]) + ], + trailing: LunaIconButton( + icon: ReadarrBookDetailsNavigationBar.icons[_db.data]), + onTap: () async { + List values = await ReadarrDialogs.setDefaultPage( + context, + titles: ReadarrBookDetailsNavigationBar.titles, + icons: ReadarrBookDetailsNavigationBar.icons, + ); + if (values[0]) _db.put(values[1]); + }, + ); + }, + ); + } +} diff --git a/lib/modules/settings/routes/configuration_readarr/pages/headers.dart b/lib/modules/settings/routes/configuration_readarr/pages/headers.dart new file mode 100644 index 0000000000..accb928f75 --- /dev/null +++ b/lib/modules/settings/routes/configuration_readarr/pages/headers.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/settings.dart'; + +class SettingsConfigurationReadarrHeadersRouter extends SettingsPageRouter { + SettingsConfigurationReadarrHeadersRouter() + : super('/settings/configuration/readarr/headers'); + + @override + Widget widget() => const SettingsHeaderRoute(module: LunaModule.READARR); + + @override + void defineRoute(FluroRouter router) => + super.noParameterRouteDefinition(router); +} diff --git a/lib/modules/settings/routes/configuration_readarr/route.dart b/lib/modules/settings/routes/configuration_readarr/route.dart new file mode 100644 index 0000000000..8fda840a85 --- /dev/null +++ b/lib/modules/settings/routes/configuration_readarr/route.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import 'package:lunasea/core.dart'; +import 'package:lunasea/modules/settings.dart'; +import 'package:lunasea/modules/readarr.dart'; + +class SettingsConfigurationReadarrRouter extends SettingsPageRouter { + SettingsConfigurationReadarrRouter() : super('/settings/configuration/readarr'); + + @override + _Widget widget() => _Widget(); + + @override + void defineRoute(FluroRouter router) => + super.noParameterRouteDefinition(router); +} + +class _Widget extends StatefulWidget { + @override + State<_Widget> createState() => _State(); +} + +class _State extends State<_Widget> with LunaScrollControllerMixin { + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return LunaScaffold( + scaffoldKey: _scaffoldKey, + appBar: _appBar() as PreferredSizeWidget?, + body: _body(), + ); + } + + Widget _appBar() { + return LunaAppBar( + title: 'Readarr', + scrollControllers: [scrollController], + ); + } + + Widget _body() { + return LunaListView( + controller: scrollController, + children: [ + LunaModule.READARR.informationBanner(), + _enabledToggle(), + _connectionDetailsPage(), + LunaDivider(), + _defaultOptionsPage(), + _defaultPagesPage(), + _queueSize(), + ], + ); + } + + Widget _enabledToggle() { + return ValueListenableBuilder( + valueListenable: Database.profiles.box.listenable(), + builder: (context, dynamic _, __) => LunaBlock( + title: 'Enable ${LunaModule.READARR.name}', + trailing: LunaSwitch( + value: LunaProfile.current.readarrEnabled ?? false, + onChanged: (value) { + LunaProfile.current.readarrEnabled = value; + LunaProfile.current.save(); + context.read().reset(); + }, + ), + ), + ); + } + + Widget _connectionDetailsPage() { + return LunaBlock( + title: 'settings.ConnectionDetails'.tr(), + body: [ + TextSpan( + text: 'settings.ConnectionDetailsDescription'.tr( + args: [LunaModule.READARR.name], + ), + ) + ], + trailing: const LunaIconButton.arrow(), + onTap: () async => SettingsConfigurationReadarrConnectionDetailsRouter() + .navigateTo(context), + ); + } + + Widget _defaultPagesPage() { + return LunaBlock( + title: 'settings.DefaultPages'.tr(), + body: [TextSpan(text: 'settings.DefaultPagesDescription'.tr())], + trailing: const LunaIconButton.arrow(), + onTap: () async => + SettingsConfigurationReadarrDefaultPagesRouter().navigateTo(context), + ); + } + + Widget _defaultOptionsPage() { + return LunaBlock( + title: 'settings.DefaultOptions'.tr(), + body: [ + TextSpan(text: 'settings.DefaultOptionsDescription'.tr()), + ], + trailing: const LunaIconButton.arrow(), + onTap: () async => + SettingsConfigurationReadarrDefaultOptionsRouter().navigateTo(context), + ); + } + + Widget _queueSize() { + ReadarrDatabaseValue _db = ReadarrDatabaseValue.QUEUE_PAGE_SIZE; + return _db.listen( + builder: (context, _, __) => LunaBlock( + title: 'Queue Size', + body: [TextSpan(text: _db.data == 1 ? '1 Item' : '${_db.data} Items')], + trailing: const LunaIconButton(icon: Icons.queue_play_next_rounded), + onTap: () async { + Tuple2 result = + await ReadarrDialogs().setQueuePageSize(context); + if (result.item1) _db.put(result.item2); + }, + ), + ); + } +} diff --git a/localization/readarr/en.json b/localization/readarr/en.json new file mode 100644 index 0000000000..393fb8e829 --- /dev/null +++ b/localization/readarr/en.json @@ -0,0 +1,218 @@ +{ + "readarr.AddAuthor": "Add Author", + "readarr.AddReleaseToBlocklist": "Add Release to Blocklist", + "readarr.AddToExclusionList": "Add to Exclusion List", + "readarr.AddedAuthor": "Added Author", + "readarr.AddedOn": "Added On", + "readarr.AddedTag": "Added Tag", + "readarr.Age": "Age", + "readarr.All": "All", + "readarr.AllSeasons": "All Seasons", + "readarr.Approved": "Approved", + "readarr.Audio": "Audio", + "readarr.Author": "Author", + "readarr.AuthorDetails": "Author Details", + "readarr.AuthorEnded": "Author Ended", + "readarr.AuthorFolderImported": "Episode Imported from Author Folder", + "readarr.AuthorNotFound": "Author Not Found", + "readarr.AuthorPath": "Author Path", + "readarr.AuthorType": "Author Type", + "readarr.Authors": "Authors", + "readarr.Automatic": "Automatic", + "readarr.AutomaticSearch": "Automatic Search", + "readarr.BackingUpDatabase": "Backing Up Database{}", + "readarr.BackingUpDatabaseDescription": "Backing up the database in the background", + "readarr.BackupDatabase": "Backup Database", + "readarr.BitDepth": "Bit Depth", + "readarr.Bitrate": "Bitrate", + "readarr.Books": "Books", + "readarr.BookDetails": "Book Details", + "readarr.BookFileDeleted": "Book File Deleted", + "readarr.BookFileImported": "Book File Imported", + "readarr.BookFileRenamed": "Book File Renamed", + "readarr.BookImportIncomplete": "Files downloaded but could not be imported", + "readarr.Channels": "Channels", + "readarr.CheckDownloadClient": "Check download client for more details", + "readarr.Client": "Client", + "readarr.Codec": "Codec", + "readarr.Continuing": "Continuing", + "readarr.DeleteBookFile": "Delete Book File", + "readarr.DeleteBookFileHint1": "Are you sure you want to delete this book file?", + "readarr.DeleteFile": "Delete File", + "readarr.DeleteFiles": "Delete Files", + "readarr.DeleteReasonManual": "File was deleted by via UI", + "readarr.DeleteReasonMissingFromDisk": "readarr was unable to find the file on disk so it was removed", + "readarr.DeleteReasonUpgrade": "File was deleted to import an upgrade", + "readarr.Destination": "Destination", + "readarr.DestinationRelative": "Destination Relative", + "readarr.Download": "Download", + "readarr.DownloadClientUnavailable": "Download Client is Unavailable", + "readarr.DownloadFailed": "Download Failed", + "readarr.DownloadID": "Download ID", + "readarr.DownloadIgnored": "Download Ignored", + "readarr.DownloadWarning": "Download Warning", + "readarr.DownloadWarningWithMessage": "Download Warning: {}", + "readarr.Downloaded": "Downloaded", + "readarr.DownloadedImporting": "Downloaded: Importing", + "readarr.DownloadedWaitingToImport": "Downloaded: Waiting to Import", + "readarr.DownloadedWaitingToProcess": "Downloaded: Waiting to Process", + "readarr.Downloading": "Downloading", + "readarr.DownloadingRelease": "Downloading Release…", + "readarr.DownloadingWithMessage": "Downloading: {}", + "readarr.EditAuthor": "Edit Author", + "readarr.EmptyQueue": "Empty Queue", + "readarr.Ended": "Ended", + "readarr.Episode": "Episode", + "readarr.EpisodeImported": "Episode Imported ({})", + "readarr.EpisodeNumber": "Episode {}", + "readarr.Episodes": "Episodes", + "readarr.EpisodesAvailable": "Episodes Available", + "readarr.FPS": "FPS", + "readarr.FailedToAddAuthor": "Failed to Add Author", + "readarr.FailedToAddTag": "Failed to Add Tag", + "readarr.FailedToBackupDatabase": "Failed to Backup Database", + "readarr.FailedToDeleteBookFile": "Failed to Delete Book File", + "readarr.FailedToDownloadRelease": "Failed to Download Release", + "readarr.FailedToMonitorAuthor": "Failed to Monitor Author", + "readarr.FailedToMonitorEpisode": "Failed to Monitor Episode", + "readarr.FailedToMonitorSeason": "Failed to Monitor Season", + "readarr.FailedToRefresh": "Failed to Refresh", + "readarr.FailedToRemoveAuthor": "Failed to Remove Author", + "readarr.FailedToRemoveBook": "Failed to Remove Book", + "readarr.FailedToRemoveFromQueue": "Failed to Remove From Queue", + "readarr.FailedToRunRSSSync": "Failed to Run RSS Sync", + "readarr.FailedToSearch": "Failed to Search", + "readarr.FailedToSeasonSearch": "Failed to Season Search", + "readarr.FailedToUnmonitorAuthor": "Failed to Unmonitor Author", + "readarr.FailedToUnmonitorBook": "Failed to Unmonitor Book", + "readarr.FailedToUpdateAuthor": "Failed to Update Author", + "readarr.FailedToUpdateLibrary": "Failed to Update Library", + "readarr.Files": "Files", + "readarr.FilterCatalogue": "Filter Catalogue", + "readarr.FilterReleases": "Filter Releases", + "readarr.GrabbedFrom": "Grabbed from {}", + "readarr.History": "History", + "readarr.HistoryDescription": "View Recent Activity", + "readarr.Import": "Import", + "readarr.ImportFailed": "Imported Failed", + "readarr.ImportedTo": "Imported To", + "readarr.Indexer": "Indexer", + "readarr.InfoURL": "Info URL", + "readarr.Interactive": "Interactive", + "readarr.InteractiveSearch": "Interactive Search", + "readarr.Language": "Language", + "readarr.LanguageProfile": "Language Profile", + "readarr.Languages": "Languages", + "readarr.Leechers": "Leechers", + "readarr.ManualImport": "Manual Import", + "readarr.ManualImportDescription": "Import Content from the Filesystem", + "readarr.ManySeasons": "{} Seasons", + "readarr.MediaInfo": "Media Info", + "readarr.Message": "Message", + "readarr.Messages": "Messages", + "readarr.MetadataProfile": "Metadata Profile", + "readarr.Missing": "Missing", + "readarr.MissingEpisodes": "Missing Episodes", + "readarr.MissingEpisodesHint1": "Are you sure you want to search for all missing episodes?", + "readarr.Monitor": "Monitor", + "readarr.MonitorAuthor": "Monitor Author", + "readarr.MonitorEpisode": "Monitor Episode", + "readarr.Monitored": "Monitored", + "readarr.MonitoredDescription": "Download monitored episodes in this Author", + "readarr.Monitoring": "Monitoring", + "readarr.More": "More", + "readarr.Name": "Name", + "readarr.NoAuthorFound": "No Author Found", + "readarr.NoEpisodesFound": "No Books Found", + "readarr.NoHistoryFound": "No History Found", + "readarr.NoLongerMonitoring": "No Longer Monitoring", + "readarr.NoMessagesFound": "No Messages Found", + "readarr.NoReleasesFound": "No Releases Found", + "readarr.NoResultsFound": "No Results Found", + "readarr.NoSeasonsFound": "No Seasons Found", + "readarr.NoSummaryAvailable": "No Summary Available", + "readarr.NoTagsFound": "No Tags Found", + "readarr.OneSeason": "1 Season", + "readarr.Other": "Other", + "readarr.Overview": "Overview", + "readarr.Pages": "Pages", + "readarr.Path": "Path", + "readarr.Paused": "Paused", + "readarr.Pending": "Pending", + "readarr.PendingWithMessage": "Pending: {}", + "readarr.PublishedDate": "Published Date", + "readarr.Quality": "Quality", + "readarr.QualityProfile": "Quality Profile", + "readarr.Queue": "Queue", + "readarr.QueueDescription": "View Active & Queued Content", + "readarr.Queued": "Queued", + "readarr.Reason": "Reason", + "readarr.RefreshAuthor": "Refresh Author", + "readarr.Rejected": "Rejected", + "readarr.RelativePath": "Relative Path", + "readarr.ReleaseGroup": "Release Group", + "readarr.Releases": "Releases", + "readarr.RemoveAuthor": "Remove Author", + "readarr.RemoveFromDownloadClient": "Remove From Download Client", + "readarr.RemoveFromQueue": "Remove From Queue", + "readarr.RemovedAuthor": "Removed Author", + "readarr.RemovedAuthorWithFiles": "Removed Author (With Files)", + "readarr.RemovedBook": "Removed Book", + "readarr.RemovedBookWithFiles": "Removed Book (With Files)", + "readarr.RemovedFromQueue": "Removed From Queue", + "readarr.Resolution": "Resolution", + "readarr.RootFolder": "Root Folder", + "readarr.RunRSSSync": "Run RSS Sync", + "readarr.RunningRSSSync": "Running RSS Sync{}", + "readarr.RunningRSSSyncDescription": "Running RSS sync in the background", + "readarr.Runtime": "Runtime", + "readarr.ScanType": "Scan Type", + "readarr.Search": "Search", + "readarr.SearchAllMissing": "Search All Missing", + "readarr.SearchFor": "Search for {}", + "readarr.Searching": "Searching{}", + "readarr.SearchingDescription": "Searching for all missing episodes", + "readarr.SearchingForEpisode": "Searching for Episode…", + "readarr.SearchingForSeason": "Searching for Season…", + "readarr.Season": "Season", + "readarr.SeasonDetails": "Season Details", + "readarr.SeasonFolders": "Season Folders", + "readarr.SeasonFoldersDescription": "Sort episodes into season folders", + "readarr.SeasonNumber": "Season {}", + "readarr.Seeders": "Seeders", + "readarr.Size": "Size", + "readarr.SortCatalogue": "Sort Catalogue", + "readarr.SortReleases": "Sort Releases", + "readarr.Source": "Source", + "readarr.SourceRelative": "Source Relative", + "readarr.SourceTitle": "Source Title", + "readarr.Specials": "Specials", + "readarr.StartSearchFor": "Start Search For…", + "readarr.StartSearchForCutoffUnmetEpisodes": "Start search for cutoff unmet episodes", + "readarr.StartSearchForMissingEpisodes": "Start search for missing episodes", + "readarr.Streams": "Streams", + "readarr.Subtitles": "Subtitles", + "readarr.SystemStatus": "System Status", + "readarr.SystemStatusDescription": "System Status & Disk Space", + "readarr.Tags": "Tags", + "readarr.TagsDescription": "Manage Your Tags", + "readarr.TimeLeft": "Time Left", + "readarr.Title": "Title", + "readarr.Torrent": "Torrent", + "readarr.Unaired": "Unaired", + "readarr.UnmonitorAuthor": "Unmonitor Author", + "readarr.UnmonitorEpisode": "Unmonitor Episode", + "readarr.Unmonitored": "Unmonitored", + "readarr.Upcoming": "Upcoming", + "readarr.UpdateAuthor": "Update Author", + "readarr.UpdateLibrary": "Update Library", + "readarr.UpdatedAuthor": "Updated Author", + "readarr.UpdatingLibrary": "Updating Library", + "readarr.UpdatingLibraryDescription": "Updating library in the background", + "readarr.UseSeasonFolders": "Use Season Folders", + "readarr.UseSeasonFoldersDescription": "Sort episodes into season folders", + "readarr.Usenet": "Usenet", + "readarr.Video": "Video", + "readarr.ViewWebGUI": "View Web GUI", + "readarr.WordScore": "Word Score" +} \ No newline at end of file