From 34eb624438756d8ed52aaf6c141696fd1004318a Mon Sep 17 00:00:00 2001 From: stash-translation-bot <94573628+stash-translation-bot@users.noreply.github.com> Date: Wed, 25 May 2022 19:33:11 -0700 Subject: [PATCH 01/34] Translations update from Stash (#2604) * Translated using Weblate (Chinese (Simplified)) Currently translated at 98.7% (806 of 816 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/zh_Hans/ * Translated using Weblate (Korean) Currently translated at 77.6% (634 of 816 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ko/ * Translated using Weblate (French) Currently translated at 86.0% (702 of 816 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/ * Translated using Weblate (French) Currently translated at 87.2% (712 of 816 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/ * Translated using Weblate (French) Currently translated at 87.2% (712 of 816 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/ * Translated using Weblate (Finnish) Currently translated at 93.2% (761 of 816 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fi/ * Translated using Weblate (French) Currently translated at 83.7% (683 of 816 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/ * Translated using Weblate (Korean) Currently translated at 100.0% (816 of 816 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ko/ * Translated using Weblate (French) Currently translated at 87.6% (715 of 816 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/ * Translated using Weblate (French) Currently translated at 87.5% (714 of 816 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/ * Translated using Weblate (French) Currently translated at 87.5% (714 of 816 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/ Co-authored-by: LiboSUN Co-authored-by: Weblate Co-authored-by: yc Co-authored-by: - Co-authored-by: Aa --- ui/v2.5/src/locales/fi-FI.json | 48 ++++- ui/v2.5/src/locales/fr-FR.json | 291 +++++++++++++++++------------ ui/v2.5/src/locales/ko-KR.json | 322 +++++++++++++++++++++++++++++++-- ui/v2.5/src/locales/zh-CN.json | 18 +- 4 files changed, 543 insertions(+), 136 deletions(-) diff --git a/ui/v2.5/src/locales/fi-FI.json b/ui/v2.5/src/locales/fi-FI.json index 811094440..dbb5e2508 100644 --- a/ui/v2.5/src/locales/fi-FI.json +++ b/ui/v2.5/src/locales/fi-FI.json @@ -28,10 +28,12 @@ "delete_file": "Poista tiedosto", "delete_file_and_funscript": "Poista tiedosto (ja funscript)", "delete_generated_supporting_files": "Poista generoidut lisätiedostot", + "delete_stashid": "Poista StashID", "disallow": "Kiellä", "download": "Lataa", "download_backup": "Lataa varmuuskopio", "edit": "Muokkaa", + "edit_entity": "Muokkaa {entityType}", "export": "Vie…", "export_all": "Vie kaikki…", "find": "Etsi", @@ -50,11 +52,13 @@ "ignore": "Jätä huomiotta", "import": "Tuo…", "import_from_file": "Tuo tiedostosta", + "logout": "Kirjaudu ulos", "merge": "Yhdistä", "merge_from": "Yhdistä kohteesta", "merge_into": "Yhdistä kohteeseen", "next_action": "Seuraava", "not_running": "ei käynnissä", + "open_in_external_player": "Avaa ulkoisessa soittimessa", "open_random": "Avaa satunnainen", "overwrite": "Ylikirjoita", "play_random": "Toista satunnainen", @@ -65,6 +69,7 @@ "reload_plugins": "Lataa lisäosat uudelleen", "reload_scrapers": "Lataa kaapija uudelleen", "remove": "Poista", + "remove_from_gallery": "Poista galleriasta", "rename_gen_files": "Nimeä generoidut tiedostot uudelleen", "rescan": "Skannaa uudelleen", "reshuffle": "Sekoita uudelleen", @@ -79,6 +84,7 @@ "scrape_with": "Kaavi…", "search": "Hae", "select_all": "Valitse Kaikki", + "select_entity": "Valitse {entityType}", "select_folders": "Valitse kansiot", "select_none": "Peruuta Valinta", "selective_auto_tag": "Valikoiva automaattinen tunnisteiden asetus", @@ -193,9 +199,13 @@ "dlna": { "allow_temp_ip": "Salli {tempIP}", "allowed_ip_addresses": "Sallitut IP -osoitteet", + "allowed_ip_temporarily": "Väliaikaisesti sallittu IP", "default_ip_whitelist": "Oletus IP Whitelist", "default_ip_whitelist_desc": "Oletus IP -osoitteet, joilla on DLNA -pääsy. Käytä {wildcard} salliaksesi kaikki IP -osoitteet.", + "disabled_dlna_temporarily": "DLNA poistettu käytöstä väliaikaisesti", + "disallowed_ip": "Estetty IP", "enabled_by_default": "Sallittu oletuksena", + "enabled_dlna_temporarily": "DNLA päällä väliaikaisesti", "network_interfaces": "Rajapinnat", "network_interfaces_desc": "Rajapinnat, joille DLNA palvelin näytetään. Tyhjä lista sallii kaikki rajapinnat. Vaatii DLNA -palvelimen uudelleenkäynnistyksen, mikäli asetusta muokataan.", "recent_ip_addresses": "Viimeisimmät IP -osoitteet", @@ -268,6 +278,9 @@ "number_of_parallel_task_for_scan_generation_head": "Rinnakkaisten skannaus- ja generointitehtävien määrä", "parallel_scan_head": "Rinnakkainen skannaus ja generointi", "preview_generation": "Esikatselun generointi", + "python_path": { + "heading": "Pythonin polku" + }, "scraper_user_agent": "Kaapijan käyttäjäagentti", "scraper_user_agent_desc": "Käyttäjäagenttikenttä, jota kaavittaessa käytetään http pyynnöissä", "scrapers_path": { @@ -422,6 +435,10 @@ "funscript_offset": { "description": "Viive millisekunneissa interaktiivisille skripteille kun toistetaan." }, + "handy_connection": { + "connect": "Yhdistä", + "sync": "Synkronoi" + }, "images": { "heading": "Kuvat", "options": { @@ -431,6 +448,7 @@ } } }, + "interactive_options": "Interaktiivisuuden asetukset", "language": { "heading": "Kieli" }, @@ -563,6 +581,7 @@ }, "scroll_mode": { "label": "Vieritystila", + "pan_y": "Pan Y", "zoom": "Zoomaus" } }, @@ -651,7 +670,9 @@ "scale": "Skaalaa", "warmth": "Lämpötila" }, + "empty_server": "Lisää kohtauksia palvelimeesi niin näet suosituksia tällä sivulla.", "ethnicity": "Etninen tausta", + "existing_value": "nykyinen arvo", "eye_color": "Silmien väri", "fake_tits": "Tekorinnat", "false": "Ei", @@ -679,6 +700,13 @@ "TRANSGENDER_MALE": "Transmies" }, "hair_color": "Hiusten väri", + "handy_connection_status": { + "connecting": "Yhdistetään", + "disconnected": "Ei yhdistetty", + "error": "Virhe yhdistettäessä Handyyn", + "missing": "Puuttuu", + "ready": "Valmis" + }, "hasMarkers": "On merkki", "height": "Pituus", "help": "Apua", @@ -736,10 +764,12 @@ "parent_tags": "Ylätunnisteet", "part_of": "Osa {parent}", "path": "Polku", + "perceptual_similarity": "Aistinvarainen samankaltaisuus (phash)", "performer": "Esiintyjä", "performerTags": "Esiintyjien tunnisteet", "performer_age": "Esiintyjän ikä", "performer_count": "Esiintyjien määrä", + "performer_favorite": "Esiintyjä suosikeissa", "performer_image": "Esiintyjän kuva", "performer_tagger": { "add_new_performers": "Lisää uusia esiintyjiä", @@ -758,11 +788,14 @@ "network_error": "Verkkovirhe", "no_results_found": "Ei tuloksia.", "number_of_performers_will_be_processed": "{performer_count} esintyjää prosessoidaan", + "performer_already_tagged": "Esiintyjälle on jo asetettu tunnisteet", "performer_names_separated_by_comma": "Erota esiintyjien nimet pilkulla", "performer_selection": "Esiintyjän valinta", + "performer_successfully_tagged": "Esiintyjälle on asetettu tunnisteet:", "query_all_performers_in_the_database": "Kaikki esiintyjät tietokannassa", "status_tagging_job_queued": "Tila: Tunnisteiden asettaminen laitettu jonoon", "status_tagging_performers": "Tila: Asetetaan esiintyjien tunnisteita", + "untagged_performers": "Esiintyjät joille ei ole asetettu tunnisteita", "update_performer": "Päivitä esiintyjä", "update_performers": "Päivitä esiintyjät" }, @@ -771,6 +804,11 @@ "queue": "Jono", "random": "Satunnainen", "rating": "Arvio", + "recently_added_performers": "Viimeksi lisätyt esiintyjät", + "recently_added_studios": "Viimeksi lisätyt studiot", + "recently_released_galleries": "Viimeksi julkaistut galleriat", + "recently_released_movies": "Viimeksi julkaistut elokuvat", + "recently_released_scenes": "Viimeksi julkaistut kohtaukset", "resolution": "Resoluutio", "scene": "Kohtaus", "sceneTagger": "Kohtauksien tunnistetila", @@ -807,6 +845,9 @@ "something_went_wrong_description": "Näyttää siltä, että olet syöttänyt jotain omituista. Palaa takaisin korjataksesi ne. Muussa tapauksessa tee ilmoitus bugista {githubLink} tai pyydä apua {discordLink}.", "something_went_wrong_while_setting_up_your_system": "Järjestelmän asettamisessa meni jotain vikaan. Tässä on virhe jonka saimme: {error}" }, + "folder": { + "up_dir": "Ylös" + }, "github_repository": "Github repository", "migrate": { "backup_database_path_leave_empty_to_disable_backup": "Tietokannan varmuuskopion polku (jätä tyhjäksi jos et halua varmuuskopiointia):", @@ -851,10 +892,7 @@ "next_step": "Kun olet valmis etenemään järjestelmän luontiin, paina Seuraava.", "unable_to_locate_specified_config": "Mikäli luet tätä, Stash ei löydä konfiguraatiotiedostoa, joka on määritelty joko komentorivillä tai muualla. Tämä velho auttaa sinua uuden konfiguraation luomisessa." }, - "welcome_to_stash": "Tervetuloa Stashiin", - "folder": { - "up_dir": "Ylös" - } + "welcome_to_stash": "Tervetuloa Stashiin" }, "stash_id": "Stash ID", "stash_ids": "Stash ID:t", @@ -863,6 +901,7 @@ "submission_failed": "Lähettäminen ei onnistunut", "submission_successful": "Lähettäminen onnistui" }, + "statistics": "Tilastot", "stats": { "image_size": "Kuvien koko", "scenes_duration": "Kohtausten kesto", @@ -904,6 +943,7 @@ "updated_at": "Päivitetty", "url": "URL", "videos": "Videot", + "view_all": "Näytä kaikki", "weight": "Paino", "years_old": "-vuotias" } diff --git a/ui/v2.5/src/locales/fr-FR.json b/ui/v2.5/src/locales/fr-FR.json index 9a42cfd69..fb2e4667b 100644 --- a/ui/v2.5/src/locales/fr-FR.json +++ b/ui/v2.5/src/locales/fr-FR.json @@ -7,96 +7,108 @@ "allow": "Autoriser", "allow_temporarily": "Autoriser temporairement", "apply": "Appliquer", - "auto_tag": "Taggage automatique", + "auto_tag": "Étiquetage auto", "backup": "Sauvegarde", "browse_for_image": "Sélectionner une image…", "cancel": "Annuler", "clean": "Nettoyer", - "clear": "Vider", - "clear_back_image": "Suppr. l'image Verso", - "clear_front_image": "Suppr. l'image Recto", - "clear_image": "Supprimer l'image", + "clear": "Effacer", + "clear_back_image": "Effacer l'image verso", + "clear_front_image": "Effacer l'image recto", + "clear_image": "Effacer l'image", "close": "Fermer", "confirm": "Confirmer", "continue": "Continuer", "create": "Créer", "create_entity": "Créer {entityType}", - "create_marker": "Créer un Marqueur", - "created_entity": "Créé : {entity_type} : {entity_name}", + "create_marker": "Créer un marqueur", + "created_entity": "Créé {entity_type} : {entity_name}", "delete": "Supprimer", "delete_entity": "Supprimer {entityType}", "delete_file": "Supprimer le fichier", - "delete_generated_supporting_files": "Supprimer les fichiers générés", + "delete_file_and_funscript": "Supprimer le fichier (et funscript)", + "delete_generated_supporting_files": "Supprimer les fichiers générés associés", + "delete_stashid": "Supprimer StashID", "disallow": "Refuser", "download": "Télécharger", "download_backup": "Télécharger une sauvegarde", - "edit": "Editer", + "edit": "Éditer", + "edit_entity": "Éditer {entityType}", "export": "Exporter…", "export_all": "Exporter tout…", - "find": "Trouver", + "find": "Rechercher", "finish": "Terminer", "from_file": "A partir du fichier…", "from_url": "A partir de l'URL…", - "full_export": "Export Complet", - "full_import": "Import Complet", + "full_export": "Export complet", + "full_import": "Import complet", "generate": "Générer", - "generate_thumb_default": "Générer la miniature par défaut", - "generate_thumb_from_current": "Générer une miniature", + "generate_thumb_default": "Générer une vignette par défaut", + "generate_thumb_from_current": "Générer une vignette à partir de l'image courante", "hash_migration": "Migration du hash", "hide": "Masquer", + "hide_configuration": "Masquer la configuration", "identify": "Identifier", "ignore": "Ignorer", "import": "Importer…", - "import_from_file": "Importer à partir d'un fichier", + "import_from_file": "Importation depuis un fichier", + "logout": "Déconnecter", "merge": "Fusionner", "merge_from": "Fusionner depuis", "merge_into": "Fusionner dans", "next_action": "Suivant", - "not_running": "pas en cours d'exécution", + "not_running": "non exécuté", + "open_in_external_player": "Ouvrir dans un lecteur externe", "open_random": "Ouvrir au hasard", - "overwrite": "Ecraser", + "overwrite": "Écraser", "play_random": "Lecture aléatoire", "play_selected": "Lire la sélection", "preview": "Aperçu", "previous_action": "Précédent", "refresh": "Rafraichir", - "reload_plugins": "Recharger les Plugins", - "reload_scrapers": "Recharger les Scrapers", + "reload_plugins": "Recharger les plugins", + "reload_scrapers": "Recharger les scrapers", "remove": "Retirer", + "remove_from_gallery": "Supprimer de la galerie", "rename_gen_files": "Renommer les fichiers générés", - "rescan": "Scanner à nouveau", + "rescan": "Analyser à nouveau", "reshuffle": "Mélanger à nouveau", "running": "en cours d'exécution", "save": "Sauvegarder", - "save_delete_settings": "Utilisez ces options par défaut lors de la suppression", + "save_delete_settings": "Utiliser ces options par défaut lors de la suppression", "save_filter": "Sauvegarder le filtre", - "scan": "Scanner", - "scrape": "Scrape", - "scrape_query": "Requête de Scrape", - "scrape_scene_fragment": "Scrape par fragment", - "scrape_with": "Scrape avec…", - "search": "Chercher", + "scan": "Analyser", + "scrape": "Scraper", + "scrape_query": "Requête Scrape", + "scrape_scene_fragment": "Scraper par fragment", + "scrape_with": "Scraper avec…", + "search": "Recherche", "select_all": "Sélectionner tout", + "select_entity": "Sélectionner {entityType}", "select_folders": "Sélectionner des répertoires", "select_none": "Ne rien sélectionner", - "selective_auto_tag": "Taggage automatique de la sélection", + "selective_auto_tag": "Étiquetage auto de la sélection", "selective_clean": "Nettoyage sélectif", - "selective_scan": "Scan sélectif", - "set_as_default": "Définir comme valeur par défaut", - "set_back_image": "Image Verso…", - "set_front_image": "Image Recto…", - "set_image": "Choisir l'image…", + "selective_scan": "Analyse sélective", + "set_as_default": "Définir par défaut", + "set_back_image": "Image verso…", + "set_front_image": "Image recto…", + "set_image": "Définir l'image…", "show": "Montrer", + "show_configuration": "Afficher la configuration", "skip": "Passer", "stop": "Stop", + "submit": "Soumettre", + "submit_stash_box": "Soumettre à Stash-Box", "tasks": { - "clean_confirm_message": "Êtes-vous sûr de vouloir nettoyer ? Cela supprimera les informations de la base de données et le contenu généré pour toutes les Vidéos et Galeries qui ne se trouvent plus dans le système de fichiers.", - "dry_mode_selected": "Essais à blanc. Aucune suppression n'aura lieu.", - "import_warning": "Êtes-vous sûr de vouloir importer ? Cela supprimera la base de données et réimportera des données à partir de vos métadonnées exportées." + "clean_confirm_message": "Êtes-vous sûr de vouloir nettoyer ? Cette opération supprimera les informations de la base de données et le contenu généré pour toutes les scènes et galeries qui ne se trouvent plus dans le système de fichiers.", + "dry_mode_selected": "Essais à blanc. Aucune suppression réelle n'aura lieu, seulement une journalisation.", + "import_warning": "Êtes-vous sûr de vouloir importer ? Cela supprimera la base de données et la réimportera à partir de vos métadonnées exportées." }, "temp_disable": "Désactiver temporairement…", "temp_enable": "Activer temporairement…", - "use_default": "Utiliser la valeur par défaut", + "unset": "Désactiver", + "use_default": "Utiliser par défaut", "view_random": "Visionner au hasard" }, "actions_name": "Actions", @@ -108,58 +120,59 @@ "average_resolution": "Résolution moyenne", "birth_year": "Année de naissance", "birthdate": "Date de naissance", - "bitrate": "BitRate", + "bitrate": "Débit", + "captions": "Légendes", "career_length": "Durée de la carrière", "component_tagger": { "config": { - "active_instance": "Instance stash-box active :", - "blacklist_desc": "Éléments à exclure de la requête, sous la forme d'une expression régulière insensible à la casse. Certains caractères doivent être échapés avec un backslash : {chars_require_escape}", - "blacklist_label": "Exclure", + "active_instance": "Instance Stash-Box active :", + "blacklist_desc": "Les éléments de la liste noire sont exclus des requêtes. Notez que ce sont des expressions régulières insensibles à la casse. Certains caractères doivent être échappés par une barre oblique inversée : {chars_require_escape}", + "blacklist_label": "Liste noire", "query_mode_auto": "Automatique", - "query_mode_auto_desc": "Se base sur les métadonnées du fichier si présentes, sinon utilise le nom du fichier", + "query_mode_auto_desc": "Utilise les métadonnées si présentes, ou le nom de fichier", "query_mode_dir": "Répertoire parent", - "query_mode_dir_desc": "Se base uniquement sur le nom du répertoire parent", + "query_mode_dir_desc": "Utilise uniquement le répertoire parent du fichier vidéo", "query_mode_filename": "Nom de fichier", - "query_mode_filename_desc": "Se base uniquement sur le nom du fichier", + "query_mode_filename_desc": "Utilise uniquement le nom du fichier", "query_mode_label": "Mode", "query_mode_metadata": "Métadonnées", - "query_mode_metadata_desc": "Se base uniquement sur les métadonnées du fichier", - "query_mode_path": "Chemin complet", - "query_mode_path_desc": "Se base sur le chemin complet du fichier", - "set_cover_desc": "Si une vignette a été trouvée, remplace la vignette de la vidéo par la vignette trouvée.", - "set_cover_label": "Enregistrer la vignette", - "set_tag_desc": "Ajoute les Tags à la vidéo, en fusionnant avec ou en écrasant les Tags existants.", - "set_tag_label": "Enregistrer les Tags", - "show_male_desc": "Cochez si vous voulez Taguer également les Acteurs (hommes).", - "show_male_label": "Montrer également les Acteurs", + "query_mode_metadata_desc": "Utilise uniquement les métadonnées", + "query_mode_path": "Chemin", + "query_mode_path_desc": "Utilise le chemin complet du fichier", + "set_cover_desc": "Remplace la couverture de la scène si une est trouvée.", + "set_cover_label": "Définir l'image de couverture de la scène", + "set_tag_desc": "Attache des étiquettes à la scène, en écrasant ou en fusionnant avec des étiquettes existantes.", + "set_tag_label": "Définir les étiquettes", + "show_male_desc": "Cocher si les performeurs masculins seront disponibles pour le marquage.", + "show_male_label": "Montrer les performeurs masculins", "source": "Source" }, "noun_query": "Requête", "results": { - "duration_off": "La durée diffère d'au moins {number} secondes", + "duration_off": "Durée différente d'au moins {number}s", "duration_unknown": "Durée inconnue", - "fp_found": "{fpCount, plural, =0 {Aucune empreinte correspondante} other {# nouvelles empreintes correspondantes}}", + "fp_found": "{fpCount, plural, =0 {Aucune nouvelle correspondance d'empreinte trouvée} other {# nouvelles correspondances d'empreintes trouvées}}", "fp_matches": "La durée correspond", - "fp_matches_multi": "La durée correspond à {matchCount}/{durationsLength} empreintes(s)", - "hash_matches": "Correspondance trouvée : {hash_type}", - "match_failed_already_tagged": "Vidéo déjà taguée", + "fp_matches_multi": "La durée correspond à {matchCount}/{durationsLength} empreinte(s)", + "hash_matches": "{hash_type} est une correspondance", + "match_failed_already_tagged": "Scène déjà étiquetée", "match_failed_no_result": "Aucun résultat trouvé", - "match_success": "Scène Taguée avec succès", - "phash_matches": "{count} PHashe(s) correspondant(s)", - "unnamed": "Anonyme" + "match_success": "Scène étiquetée avec succès", + "phash_matches": "{count} PHashes correspondants", + "unnamed": "Sans nom" }, "verb_match_fp": "Empreintes correspondantes", "verb_matched": "Associé", "verb_scrape_all": "Extraire tout", "verb_submit_fp": "Soumettre {fpCount, plural, one{# Empreinte} other{# Empreintes}}", "verb_toggle_config": "{toggle} {configuration}", - "verb_toggle_unmatched": "{toggle} vidéos associées" + "verb_toggle_unmatched": "{toggle} scènes incomparables" }, "config": { "about": { - "build_hash": "Hash de la version installée :", - "build_time": "Date de la version installée :", - "check_for_new_version": "Rechercher une mise à jour", + "build_hash": "Hash de construction :", + "build_time": "Date de construction :", + "check_for_new_version": "Rechercher une nouvelle version", "latest_version": "Dernière version", "latest_version_build_hash": "Dernière version de hachage :", "new_version_notice": "[Nouveautés]", @@ -188,20 +201,25 @@ "dlna": { "allow_temp_ip": "Autoriser {tempIP}", "allowed_ip_addresses": "Adresses IP autorisées", + "allowed_ip_temporarily": "IP autorisée temporairement", "default_ip_whitelist": "Liste blanche d'adresses IP", - "default_ip_whitelist_desc": "Liste d'adresses IP que seront autorisées par défaut à accéder au serveur DLNA. Utilisez {wildcard} pour autoriser toutes les adresses.", - "enabled_by_default": "Activer par défaut", + "default_ip_whitelist_desc": "Adresses IP par défaut autorisées pour accéder à DLNA. Utiliser {wildcard} pour autoriser toutes les adresses IP.", + "disabled_dlna_temporarily": "Désactivation temporaire de DLNA", + "disallowed_ip": "IP non autorisé", + "enabled_by_default": "Activé par défaut", + "enabled_dlna_temporarily": "Activation temporaire de DLNA", "network_interfaces": "Interfaces", - "network_interfaces_desc": "Interfaces réseaux sur lesquelles exposer le serveur DLNA. Si rien n'est spécifié, le serveur sera exposé sur toutes les interfaces disponibles. Redémarrez le serveur DLNA pour appliquer la modification.", + "network_interfaces_desc": "Interfaces sur lesquelles exposer le serveur DLNA. Une liste vide entraîne l'exécution sur toutes les interfaces. Nécessite le redémarrage de DLNA après modification.", "recent_ip_addresses": "Adresses IP récentes", - "server_display_name": "Nom du serveur DLNA", - "server_display_name_desc": "Nom du serveur DLNA. Si aucun nom n'est spécifié, le nom du serveur sera {server_name}.", + "server_display_name": "Nom d'affichage du serveur", + "server_display_name_desc": "Nom d'affichage du serveur DLNA. Par défaut {server_name} si vide.", + "successfully_cancelled_temporary_behaviour": "Le comportement temporaire a été annulé avec succès", "until_restart": "jusqu'au redémarrage" }, "general": { "auth": { "api_key": "Clé API", - "api_key_desc": "Clé API pour système tier. Requis uniquement lorsque un nom d'utilisateur et un mot de passe ont été spécifiés. Le nom d'utilisateur doit avoir été enregistré avant de générer la clé API.", + "api_key_desc": "Clé API pour les systèmes externes. Nécessaire uniquement lorsque le nom d'utilisateur/mot de passe est configuré. Le nom d'utilisateur doit être enregistré avant de générer la clé API.", "authentication": "Authentification", "clear_api_key": "Effacer la clé API", "credentials": { @@ -210,38 +228,38 @@ }, "generate_api_key": "Générer une clé API", "log_file": "Fichier journal", - "log_file_desc": "Chemin vers le fichier journal. Laissez vide pour désactiver la journalisation. Nécéssite un redémarrage.", + "log_file_desc": "Chemin d'accès au fichier de sortie de journalisation. Vide pour désactiver la journalisation du fichier. Nécessite un redémarrage.", "log_http": "Journaliser les accès HTTP", - "log_http_desc": "Journalise les accès HTTP dans le terminal. Nécéssite un redémarrage.", + "log_http_desc": "Journalise les accès HTTP dans le terminal. Nécessite un redémarrage.", "log_to_terminal": "Journaliser dans le terminal", - "log_to_terminal_desc": "Journalise dans le terminal en plus du fichier journal. Actif par défaut si la journalisation dans le fichier journal est désactivée. Nécéssite un redémarrage.", + "log_to_terminal_desc": "Journalise dans le terminal en complément d'un fichier. Toujours valide si la journalisation des fichiers est désactivée. Nécessite un redémarrage.", "maximum_session_age": "Durée maximum de session", - "maximum_session_age_desc": "Temps d'inactivité maximum avant la déconnexion automatique de la session.", + "maximum_session_age_desc": "Temps d'inactivité maximal avant expiration d'une session de connexion, en secondes.", "password": "Mot de passe", - "password_desc": "Mot de passe pour accéder à Stash. Laissez vide pour désactiver l'authentification.", - "stash-box_integration": "Integration de stash-box", + "password_desc": "Mot de passe pour accéder à Stash. Laisser vide pour désactiver l'authentification utilisateur", + "stash-box_integration": "Intégration de Stash-Box", "username": "Nom d'utilisateur", - "username_desc": "Nom d'utilisateur pour accéder à Stash. Laissez vide pour désactiver l'authentification." + "username_desc": "Nom d'utilisateur pour accéder à Stash. Laisser vide pour désactiver l'authentification utilisateur" }, - "cache_location": "Chemin vers le répertoire qui sera utilisé pour la mise en cache.", - "cache_path_head": "Répertorie de mise en cache", - "calculate_md5_and_ohash_desc": "Calculer l'empreinte MD5 en plus de OSHash. Activer cette option rendra plus lent le scan initial des fichiers. Le hachage des nom de fichiers doit être défini sur oshash pour désactiver le calcul du MD5.", + "cache_location": "Emplacement du répertoire du cache", + "cache_path_head": "Chemin du cache", + "calculate_md5_and_ohash_desc": "Calculer la somme de contrôle MD5 en complément de oshash. Son activation entraîne un ralentissement des analyses initiales. Le hachage du nom de fichier doit être défini sur oshash pour désactiver le calcul MD5.", "calculate_md5_and_ohash_label": "Calculer le MD5 pour les vidéos", "check_for_insecure_certificates": "Vérifier les certificats non sécurisés", - "check_for_insecure_certificates_desc": "Certains sites Web utilisent des certificats SSL non sécurisés. Lorsque cette option est décochée, le Scraper ignore la vérification des certificats non sécurisés et permet le Scraping de ces sites. Si vous obtenez une erreur de certificat lors du Scraping, décochez cette option.", - "chrome_cdp_path": "Chrome CDP", - "chrome_cdp_path_desc": "Chemin vers l'exécutable Chrome, ou une adresse distante (commençant par http:// ou https://, par exemple http://localhost:9222/json/version) vers une instance Chrome.", - "create_galleries_from_folders_desc": "Si coché, crée des Galeries à partir des dossiers contenant des images.", - "create_galleries_from_folders_label": "Créer des Galeries à partir des dossiers contenant des fichiers images", - "db_path_head": "Base de données", - "directory_locations_to_your_content": "Emplacements vers vos bibliothèques de contenu", - "excluded_image_gallery_patterns_desc": "Expressions régulières des fichiers ou chemins vers les images ou galeries à exclure de l'analyse et à ajouter au nettoyage.", - "excluded_image_gallery_patterns_head": "Images/galeries à exclure", - "excluded_video_patterns_desc": "Expressions régulières des fichiers ou chemins vers les vidéos à exclure de l'analyse et à ajouter au nettoyage.", - "excluded_video_patterns_head": "Vidéos à exclure", - "gallery_ext_desc": "Liste des extensions de fichier Archive qui seront considérés comme des Galeries d'Images. Séparez les extentions par une virgule.", - "gallery_ext_head": "Extensions de fichiers d'Archive pour les Galeries", - "generated_file_naming_hash_desc": "Utiliser MD5 ou OSHash pour le nommage des fichiers générés. Modifier ce réglage nécéssite que le MD5/OSHash ai déjà été généré pour toutes les Vidéos et Images. Après avoir modifié ce réglage, les fichiers générés existants devront être migrés ou régénérés. Voir la page Tâches pour la migration.", + "check_for_insecure_certificates_desc": "Certains sites utilisent des certificats SSL non sécurisés. Lorsque cette option est décochée, le scraper ignore la vérification des certificats non sécurisés et autorise le scraping de ces sites. Si vous obtenez une erreur de certificat lors du scraping, décochez cette option.", + "chrome_cdp_path": "Chemin Chrome CDP (Chrome Debugging Protocol)", + "chrome_cdp_path_desc": "Chemin de l'exécutable Chrome, ou adresse distante (commençant par http:// ou https://, par exemple http://localhost:9222/json/version) d'une instance de Chrome.", + "create_galleries_from_folders_desc": "Coché, crée des galeries à partir de dossiers contenant des images.", + "create_galleries_from_folders_label": "Créer des galeries à partir de dossiers contenant des images", + "db_path_head": "Chemin de la base de données", + "directory_locations_to_your_content": "Emplacements vers votre contenu", + "excluded_image_gallery_patterns_desc": "Expression régulière de fichiers images et galeries ou de chemins d'accès à exclure de l'analyse et à ajouter au nettoyage", + "excluded_image_gallery_patterns_head": "Modèles d'image ou galerie exclués", + "excluded_video_patterns_desc": "Expressions régulières de fichiers vidéo ou de chemins d'accès à exclure de l'analyse et à ajouter au nettoyage", + "excluded_video_patterns_head": "Modèles de vidéo exclués", + "gallery_ext_desc": "Liste séparée par des virgules des extensions de fichiers qui seront reconnues comme des archives zip de la galerie.", + "gallery_ext_head": "Extensions zip de la galerie", + "generated_file_naming_hash_desc": "Utilisez MD5 ou oshash pour le nommage des fichiers générés. Le modifier exige que toutes les scènes soient renseignées avec une valeur MD5/oshash appropriée. Après avoir modifié cette valeur, les fichiers générés existants devront être migrés ou régénérés. Voir la page Tâches pour la migration.", "generated_file_naming_hash_head": "Algorithme de hachage pour le nommage des fichiers générés", "generated_files_location": "Emplacement pour les fichiers générés (marqueurs, apercus, sprites, etc)", "generated_path_head": "Emplacement des fichiers générés", @@ -263,13 +281,18 @@ "number_of_parallel_task_for_scan_generation_head": "Nombre de tâches en parallèles pour le scan et la génération", "parallel_scan_head": "Scan/Génération en parallèle", "preview_generation": "Générer les aperçus", + "python_path": { + "description": "Emplacement de l'exécutable python. Utilisé par les scrapers et les plugins. Si vide, python sera résolu à partir de l'environnement", + "heading": "Chemin de Python" + }, "scraper_user_agent": "User-Agent pour les Scraper", "scraper_user_agent_desc": "Chaîne User-Agent utilisée dans les requêtes http lors du Scraping.", "scrapers_path": { + "description": "Emplacement du répertoire des fichiers de configuration scraper", "heading": "Chemin des scrapers" }, "scraping": "Scraping", - "sqlite_location": "Emplacement du fichier de base de données SQLite (nécéssite un redémarrage)", + "sqlite_location": "Emplacement du fichier de base de données SQLite (nécessite un redémarrage)", "video_ext_desc": "Liste des extensions de fichiers Vidéos.", "video_ext_head": "Extensions de fichiers Vidéo", "video_head": "Vidéo" @@ -333,9 +356,9 @@ "generate_desc": "Génère les fichiers images, sprite, vidéo, vtt autres fichiers.", "generate_phashes_during_scan": "Générer des hachages perceptuels", "generate_phashes_during_scan_tooltip": "Pour la déduplication et l'identification de scènes.", - "generate_previews_during_scan": "Générer également les aperçus image pendant le scan (WebP animé, requis seulement si le Type d'apercu est définis sur Image Animée)", + "generate_previews_during_scan": "Générer des aperçus d'images animées", "generate_previews_during_scan_tooltip": "Générez des aperçus WebP animés (requis uniquement si le type d'aperçu est défini sur Image animée).", - "generate_sprites_during_scan": "Générer les Sprites pendant le scan (pour l'aperçu sur la barre de progression)", + "generate_sprites_during_scan": "Générer les sprites de progression", "generate_thumbnails_during_scan": "Générer les miniatures pour les images", "generate_video_previews_during_scan": "Générer les aperçus", "generate_video_previews_during_scan_tooltip": "Générez des aperçus vidéo qui s'activent lorsque vous survolez une scène", @@ -360,7 +383,7 @@ "sources": "Sources", "strategy": "Stratégie" }, - "import_from_exported_json": "Importez à partir du fichier JSON exporté dans le répertoire des métadonnées. Cela supprimera la base de données existante.", + "import_from_exported_json": "Importation à partir du JSON exporté dans le répertoire des métadonnées. Efface la base de données existante.", "incremental_import": "Importation incrémentielle à partir d'un fichier zip d'exportation fourni.", "job_queue": "File d'attente des tâches", "maintenance": "Maintenance", @@ -411,6 +434,8 @@ }, "desktop_integration": { "desktop_integration": "Intégration avec le Bureau", + "notifications_enabled": "Activer les notifications", + "send_desktop_notifications_for_events": "Envoyer des notifications bureau pour les événements", "skip_opening_browser": "Ne pas ouvrir de navigateur", "skip_opening_browser_on_startup": "ne pas ouvrir de navigateur au démarrage de l'application" }, @@ -425,6 +450,13 @@ "description": "Décalage temporel (en millisecondes) à appliquer lors de la lecture des scripts interactifs.", "heading": "Décalage Funscript (ms)" }, + "handy_connection": { + "connect": "Connecter", + "server_offset": { + "heading": "Offset serveur" + }, + "sync": "Synchroniser" + }, "handy_connection_key": { "description": "Clé de connexion Handy pour les Vidéos interactives. Définir cette clé permettra à Stash de partager les informations de votre scène actuelle avec handyfeeling.com", "heading": "Clé de connexion Handy" @@ -493,9 +525,13 @@ "toggle_sound": "Activer le son" } }, + "scroll_attempts_before_change": { + "description": "Nombre de tentatives de défilement avant de passer à l'élément suivant/précédent. S'applique uniquement au mode de défilement Pan Y.", + "heading": "Tentatives de défilement avant transition" + }, "slideshow_delay": { "description": "Diaporama disponible dans Galeries en mode de visionnage Mur", - "heading": "Durée de défilement du diaporama" + "heading": "Délai du diaporama (secondes)" }, "title": "Interface utilisateur" } @@ -552,7 +588,7 @@ "delete_entity_title": "{count, plural, one {Delete {singularEntity}} other {Delete {pluralEntity}}}", "delete_galleries_extra": "…ainsi que tout fichier image non-associé à une autre Galerie.", "delete_gallery_files": "Supprime le dossier ou l'archive de la galerie, ainsi que toute image non associées à une autre galerie.", - "delete_object_desc": "Êtes-vous sûr de vouloir supprimer : {count, plural, one {this {singularEntity}} other {these {pluralEntity}}} ?", + "delete_object_desc": "Êtes-vous sûr de vouloir supprimer {count, plural, one {this {singularEntity}} other {these {pluralEntity}}} ?", "delete_object_overflow": "…et {count} {count, plural, one {autre {singularEntity}} other {autres {pluralEntity}}}.", "delete_object_title": "Supprimer {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "edit_entity_title": "Editer {count, plural, one {{singularEntity}} other {{pluralEntity}}}", @@ -587,14 +623,17 @@ "scene_gen": { "force_transcodes": "Forcer la génération de transcodage", "force_transcodes_tooltip": "Par défaut, les transcodages ne sont générés que lorsque le fichier vidéo n'est pas pris en charge dans le navigateur. Lorsqu'il est activé, les transcodes seront générés même lorsque le fichier vidéo semble être pris en charge dans le navigateur.", - "image_previews": "Aperçus image (Image WebP animée. Requis seulement si le type d'Aperçu est défini sur Image Animée)", + "image_previews": "Aperçus d'images animées", "image_previews_tooltip": "Aperçus animés (en WebP), requis uniquement si le type d'aperçu est défini sur Image animée.", - "interactive_heatmap_speed": "Générer des cartes thermiques et des vitesses pour des scènes interactives", - "marker_image_previews": "Aperçus Marqueurs (Image WebP animée. Requis seulement si le type d'aperçu est défini sur Image Animée)", + "interactive_heatmap_speed": "Générer des cartes thermiques et des vitesses pour les scènes interactives", + "marker_image_previews": "Aperçu des images animées par marqueur", "marker_image_previews_tooltip": "Aperçus animé des marqueurs (en WebP), requis uniquement si le type d'aperçu est défini sur Image animée.", - "marker_screenshots": "Capture d'écran Marqueurs (Image JPG fixe. Requis seulement si le type d'Aperçu est défini sur Image Fixe)", + "marker_screenshots": "Captures d'écran du marqueur", + "marker_screenshots_tooltip": "Marquer les images JPG statiques, requis uniquement si le type d'aperçu est défini sur Image statique.", "markers": "Aperçus des Marqueurs", - "markers_tooltip": "Vidéo de 20 secondes qui commence au timecode indiqué.", + "markers_tooltip": "Vidéos de 20 secondes qui commencent au code temporel donné.", + "override_preview_generation_options": "Remplacer les options de génération d'aperçu", + "override_preview_generation_options_desc": "Remplacer les options de génération d'aperçu pour cette opération. Les valeurs par défaut sont définies dans Système -> Génération d'aperçus.", "overwrite": "Ecraser les fichiers générés existants", "phash": "Hachage perceptif (pour la déduplication)", "preview_exclude_end_time_desc": "Exclure les x dernières secondes de la vidéo pour la génération de l'aperçu. La valeur peut-être exprimée en secondes ou bien en pourcentage (ex : 2%) de la durée totale de la vidéo.", @@ -609,12 +648,12 @@ "preview_seg_count_head": "Nombre de segments dans un aperçu", "preview_seg_duration_desc": "Durée de chaque segment d'aperçu (en secondes).", "preview_seg_duration_head": "Durée d'un segment d'aperçu", - "sprites": "Sprites (pour l'aperçu sur la barre de progression)", - "sprites_tooltip": "Sprites (pour l'aperçu sur la barre de progression)", + "sprites": "Sprites de progression de scène", + "sprites_tooltip": "Sprites (pour la progression de scène)", "transcodes": "Transcodages", "transcodes_tooltip": "Conversion au format MP4 des fichiers vidéo dont le format n'est pas supporté par le lecteur vidéo", "video_previews": "Aperçus", - "video_previews_tooltip": "Lire les aperçus vidéos lors du survol des vidéos" + "video_previews_tooltip": "Prévisualisation de la vidéo lors du survol d'une scène" }, "scenes_found": "{count} Vidéo(s) trouvée(s)", "scrape_entity_query": "Requête de Scrape {entity_type}", @@ -755,13 +794,30 @@ "path": "Chemin", "performer": "Actrice/Acteur", "performerTags": "Tags d'Actrice/Acteur", + "performer_age": "Âge de l'artiste", "performer_count": "Nombre d'Actrices/Acteurs", + "performer_favorite": "Artiste favori", "performer_image": "Photo", + "performer_tagger": { + "current_page": "Page actuelle", + "name_already_exists": "Le nom existe déjà", + "network_error": "Erreur réseau", + "no_results_found": "Aucun résultat trouvé.", + "performer_already_tagged": "Artiste déjà étiqueté", + "performer_successfully_tagged": "Artiste étiqueté avec succès :", + "update_performer": "Mise à jour de l'artiste", + "update_performers": "Mise à jour des artistes" + }, "performers": "Actrices/Acteurs", "piercings": "Piercings", "queue": "File de lecture", "random": "Aléatoire", "rating": "Note", + "recently_added_performers": "Artistes récemment ajoutés", + "recently_added_studios": "Studios récemment ajoutés", + "recently_released_galleries": "Galeries récemment publiées", + "recently_released_movies": "Films récemment sortis", + "recently_released_scenes": "Scènes récemment publiées", "resolution": "Résolution", "scene": "Vidéo", "sceneTagger": "Taggueur de Scène", @@ -798,6 +854,10 @@ "something_went_wrong_description": "Si vous pensez qu'il peut y avoir une erreur avec les données fournies, veuillez cliquer en arrière pour la corriger. Sinon, veuillez ouvrir un ticket sur {githubLink} ou demander de l'aide à {discordLink}.", "something_went_wrong_while_setting_up_your_system": "Une erreur s'est produite lors de la configuration de votre système. Voici l'erreur que nous avons reçue : {error}" }, + "folder": { + "file_path": "Chemin de fichier", + "up_dir": "Remonter d'un répertoire" + }, "github_repository": "Dépôt Github", "migrate": { "backup_database_path_leave_empty_to_disable_backup": "Chemin de la sauvegarde de la base de données (laissez vide pour désactiver la sauvegarde) :", @@ -838,7 +898,7 @@ "your_system_has_been_created": "Succès ! Votre système a été créé !" }, "welcome": { - "config_path_logic_explained": "Stash essaie d'abord de trouver son fichier de configuration (config.yml) dans le répertoire de travail actuel, et s'il ne le trouve pas là, il revient à $HOME/.stash/config. yml (sous Windows, ce sera %USERPROFILE%\\.stash\\config.yml). Vous pouvez également faire en sorte que Stash soit lu à partir d'un fichier de configuration spécifique en l'exécutant avec les options -c ou --config .", + "config_path_logic_explained": "Stash essaie d'abord de trouver son fichier de configuration (config.yml) dans le répertoire de travail actuel, et s'il ne le trouve pas là, il revient à $HOME/.stash/config. yml (sous Windows, ce sera %USERPROFILE%\\.stash\\config.yml). Vous pouvez également faire en sorte que Stash lise à partir d'un fichier de configuration spécifique en l'exécutant avec les options -c ou --config .", "in_current_stash_directory": "Dans le répertoire $HOME/.stash", "in_the_current_working_directory": "Dans le répertoire de travail actuel", "next_step": "Si vous êtes prêt à créer un nouvel environnement, veuillez sélectionner l'emplacement où vous souhaitez enregistrer votre fichier de configuration et cliquez sur Suivant.", @@ -851,13 +911,15 @@ "next_step": "Lorsque vous êtes prêt à procéder à la configuration d'un nouveau système, cliquez sur Suivant.", "unable_to_locate_specified_config": "Si vous lisez ceci, alors Stash n'a pas pu trouver le fichier de configuration spécifié sur la ligne de commande ou l'environnement. Cet assistant vous guidera tout au long du processus de configuration d'une nouvelle configuration." }, - "welcome_to_stash": "Bienvenue sur Stash", - "folder": { - "up_dir": "Remonter d'un répertoire" - } + "welcome_to_stash": "Bienvenue sur Stash" }, "stash_id": "Stash ID", "stash_ids": "Stash IDs", + "stashbox": { + "submission_failed": "Échec de l'envoi", + "submission_successful": "Envoi effectué" + }, + "statistics": "Statistiques", "stats": { "image_size": "Taille Images", "scenes_duration": "Durée Vidéos", @@ -899,6 +961,7 @@ "updated_at": "Date de modification", "url": "URL", "videos": "Vidéos", + "view_all": "Voir tout", "weight": "Poids", "years_old": "ans" } diff --git a/ui/v2.5/src/locales/ko-KR.json b/ui/v2.5/src/locales/ko-KR.json index f37512f69..c07fde25e 100644 --- a/ui/v2.5/src/locales/ko-KR.json +++ b/ui/v2.5/src/locales/ko-KR.json @@ -45,13 +45,17 @@ "generate": "만들기", "generate_thumb_default": "기본 썸네일 만들기", "generate_thumb_from_current": "현재 화면으로 썸네일 만들기", + "hash_migration": "해쉬 값 마이그레이션", "hide": "숨기기", "hide_configuration": "설정 숨기기", + "identify": "인증", "ignore": "무시", "import": "불러오기…", "import_from_file": "파일 불러오기", "logout": "로그아웃", "merge": "합치기", + "merge_from": "...에서 합치기", + "merge_into": "...로 합치기", "next_action": "다음", "not_running": "실행 중이 아님", "open_in_external_player": "외부 플레이어에서 열기", @@ -75,11 +79,17 @@ "save_filter": "필터 저장", "scan": "스캔", "scrape": "스크레이핑하기", - "scrape_query": "스크레이핑 쿼리", + "scrape_query": "쿼리 스크레이핑하기", + "scrape_scene_fragment": "단편적 스크레이핑하기", + "scrape_with": "스크레이핑하기…", "search": "검색", "select_all": "모두 검색", "select_entity": "{entityType} 선택", "select_folders": "폴더 선택", + "select_none": "선택하지 않음", + "selective_auto_tag": "선택적 자동 태깅", + "selective_clean": "선택적 데이터베이스 정리", + "selective_scan": "선택적 스캔", "set_as_default": "기본값으로 설정", "set_back_image": "이전 사진…", "set_front_image": "처음 사진…", @@ -91,10 +101,13 @@ "submit": "제출", "submit_stash_box": "Stash-Box에 제출하기", "tasks": { + "clean_confirm_message": "정말로 데이터베이스 정리를 하시겠습니까? 파일 시스템에 존재하지 않는 파일의 데이터베이스 정보와 컨텐츠가 삭제될 것입니다.", + "dry_mode_selected": "삭제하지 않기 모드가 선택되었습니다. 삭제를 진행하지 않고, 로깅만 할 것입니다.", "import_warning": "정말 불러오기를 하시겠습니까? 데이터베이스를 삭제하고 불러온 메타데이터로 덮어쓰게 됩니다." }, "temp_disable": "임시 비활성화…", "temp_enable": "임시 활성화…", + "unset": "설정 해제", "use_default": "기본값 사용", "view_random": "랜덤 보기" }, @@ -116,6 +129,7 @@ "blacklist_desc": "블랙리스트에 있는 아이템들은 쿼리에서 제외됩니다. (주의: 아이템들은 정규 표현식으로 적혀 있어야 하며 대소문자를 구별합니다. 다음 문자들의 앞에는 백슬래쉬(\\)를 넣어주어야 합니다: {chars_require_escape})", "blacklist_label": "블랙리스트", "query_mode_auto": "자동", + "query_mode_auto_desc": "메타데이터가 있다면 사용하고, 그렇지 않다면 파일 이름을 사용합니다", "query_mode_dir": "디렉토리", "query_mode_dir_desc": "비디오 파일의 상위 경로만 사용", "query_mode_filename": "파일 이름", @@ -125,24 +139,43 @@ "query_mode_metadata_desc": "메타데이터만 사용", "query_mode_path": "경로", "query_mode_path_desc": "전체 파일 경로 사용", + "set_cover_desc": "영상 커버가 있다면 그 이미지로 교체합니다.", "set_cover_label": "영상 커버 이미지 설정", + "set_tag_desc": "영상에 이미 존재하는 태그들을 덮어쓰기/합치기 함으로써 태그를 영상에 추가합니다.", "set_tag_label": "태그 설정", - "show_male_label": "남성 배우 보여주기" + "show_male_desc": "남성 배우들의 태그 가능 여부 설정을 켜거나 끕니다.", + "show_male_label": "남성 배우 보여주기", + "source": "출처" }, "noun_query": "쿼리", "results": { + "duration_off": "영상 길이가 최소 {number}초 차이남", + "duration_unknown": "영상 길이 알 수 없음", + "fp_found": "{fpCount, plural, =0 {일치하는 새로운 식별값을 찾지 못했습니다.} other {# 일치하는 새로운 식별값을 찾았습니다.}}", + "fp_matches": "영상 길이가 일치함", + "fp_matches_multi": "영상 길이가 {durationsLength}개 중 {matchCount}개의 식별값과 일치합니다", + "hash_matches": "{hash_type}이 일치함", "match_failed_already_tagged": "이미 태깅된 영상", "match_failed_no_result": "결과 없음", "match_success": "영상 태깅 성공", + "phash_matches": "{count}개의 PHash가 일치함", "unnamed": "이름 없음" }, - "verb_scrape_all": "모두 스크레이핑하기" + "verb_match_fp": "식별값 비교하기", + "verb_matched": "일치함", + "verb_scrape_all": "모두 스크레이핑하기", + "verb_submit_fp": "{fpCount, plural, one{# 식별값} other{# 식별값들}} 제출하기", + "verb_toggle_config": "{configuration} {toggle}", + "verb_toggle_unmatched": "일치하지 않는 영상 {toggle}" }, "config": { "about": { + "build_hash": "빌드 해쉬 값:", + "build_time": "빌드된 시간:", "check_for_new_version": "새로운 버전 체크", "latest_version": "최신 버전", "latest_version_build_hash": "최신 버전 빌드 해쉬:", + "new_version_notice": "[새 버전]", "stash_discord": "디스코드: {url}", "stash_home": "깃허브: {url}", "stash_open_collective": "후원: {url}", @@ -153,6 +186,7 @@ "heading": "앱 경로" }, "categories": { + "about": "프로그램 정보", "interface": "인터페이스", "logs": "로그", "metadata_providers": "메타데이터", @@ -167,17 +201,29 @@ "dlna": { "allow_temp_ip": "{tempIP} 허용", "allowed_ip_addresses": "허용된 IP 주소", + "allowed_ip_temporarily": "임시로 허용된 IP", "default_ip_whitelist": "IP 화이트리스트 기본값", + "default_ip_whitelist_desc": "기본 IP 주소들은 DLNA에 접근할 수 있습니다. 모든 IP 주소들을 허용하려면 {wildcard} 문자를 사용하세요.", + "disabled_dlna_temporarily": "임시로 DNLA를 비활성화했습니다", "disallowed_ip": "금지된 IP", + "enabled_by_default": "기본값으로 활성화됨", + "enabled_dlna_temporarily": "임시로 DLNA를 활성화함", "network_interfaces": "인터페이스", - "recent_ip_addresses": "최근 IP 주소" + "network_interfaces_desc": "DLNA 서버를 노출시키기 위한 인터페이스입니다. 빈 리스트로 두면 모든 인터페이스에서 작동하게 됩니다. 변경 후 DLNA를 재시작해야 합니다.", + "recent_ip_addresses": "최근 IP 주소", + "server_display_name": "서버 이름 (display name)", + "server_display_name_desc": "DLNA 서버를 위한 이름(display name)입니다. 빈 칸으로 두면 기본값으로 {server_name}를 사용합니다.", + "successfully_cancelled_temporary_behaviour": "임시 설정을 취소하는 데에 성공했습니다", + "until_restart": "재시작 전까지" }, "general": { "auth": { "api_key": "API 키", + "api_key_desc": "외부 시스템을 위한 API 키입니다. 아이디/비밀번호가 설정되었을 때에만 필요합니다. 아이디는 API 키 생성 전에 저장되어야만 합니다.", "authentication": "인증", "clear_api_key": "API 키 삭제", "credentials": { + "description": "Stash로의 접속을 제한하기 위한 자격 요건입니다.", "heading": "자격증명서" }, "generate_api_key": "API 키 생성", @@ -191,29 +237,56 @@ "maximum_session_age_desc": "사용되지 않을 때 자동으로 로그아웃되기까지의 시간입니다 (단위: 초).", "password": "비밀번호", "password_desc": "Stash에 접속하기 위한 비밀번호입니다. 로그인 과정을 생략하려면 빈 칸으로 두십시오", + "stash-box_integration": "Stash-box 통합", "username": "아이디", "username_desc": "Stash에 접속하기 위한 아이디입니다. 로그인을 생략하려면 빈 칸으로 두십시오" }, "cache_location": "캐시 폴더 경로", "cache_path_head": "캐쉬 경로", + "calculate_md5_and_ohash_desc": "oshash 외에 MD5 체크섬도 계산합니다. 활성화하면 초기 스캔을 더 느리게 만들 것입니다. MD5 계산을 사용하지 않으려면 파일 이름 해쉬를 oshash로 설정해야 합니다.", + "calculate_md5_and_ohash_label": "비디오 MD5 계산하기", + "check_for_insecure_certificates": "안전하지 않은 자격증명을 검사", + "check_for_insecure_certificates_desc": "일부 사이트에서는 안전하지 않은 SSL 인증서를 사용합니다. 스크레이퍼를 선택하지 않으면 안전하지 않은 인증서 검사를 건너뛰고 해당 사이트를 스크레이핑할 수 있습니다. 스크레이핑 시 인증서 오류가 발생하면 이 체크 표시를 해제하세요.", "chrome_cdp_path": "Chrome CDP 경로", + "chrome_cdp_path_desc": "Chrome 실행 파일의 경로, 또는 Chrome 인스턴스의 원격 주소입니다(http:// 또는 https://로 시작합니다. 예시: http://localhost:9222/json/version).", + "create_galleries_from_folders_desc": "체크하면, 이미지를 포함한 폴더들로부터 갤러리를 생성합니다.", "create_galleries_from_folders_label": "이미지가 들어있는 폴더로부터 갤러리 생성", "db_path_head": "데이터베이스 경로", + "directory_locations_to_your_content": "컨텐츠가 있는 폴더 위치", + "excluded_image_gallery_patterns_desc": "스캔과 데이터베이스 정리에서 제외할 이미지와 갤러리 파일/경로의 정규표현식", + "excluded_image_gallery_patterns_head": "제외된 이미지/갤러리 패턴", + "excluded_video_patterns_desc": "스캔과 데이터베이스 정리에서 제외할 비디오 파일/경로의 정규표현식", + "excluded_video_patterns_head": "제외된 비디오 패턴", + "gallery_ext_desc": "갤러리 zip 파일로 인식될 파일 확장자입니다 (쉼표로 구분합니다).", + "gallery_ext_head": "갤러리 zip 확장자", + "generated_file_naming_hash_desc": "생성된 파일 이름을 정할 때 MD5 또는 oshash를 사용합니다. 이를 변경하려면 모든 영상에 해당 MD5/osash 값이 채워져 있어야 합니다. 이 값을 변경한 후에는 기존에 생성된 파일을 마이그레이션하거나 재생성해야 합니다. 마이그레이션은 '작업' 페이지를 참조하세요.", + "generated_file_naming_hash_head": "생성된 파일 이름 해쉬", "generated_files_location": "생성된 파일들의 폴더 위치 (영상 마커, 영상 미리보기, 스프라이트 등등)", "generated_path_head": "생성된 파일 경로", "hashing": "해싱", + "image_ext_desc": "이미지로 인식될 파일 확장자입니다 (쉼표로 구분합니다).", "image_ext_head": "이미지 확장 프로그램", + "include_audio_desc": "미리보기를 생성할 때 소리를 포함합니다.", "include_audio_head": "소리 포함", "logging": "로깅", + "maximum_streaming_transcode_size_desc": "트랜스코딩된 스트림의 최대 크기", + "maximum_streaming_transcode_size_head": "최대 스트리밍 트랜스코드 크기", + "maximum_transcode_size_desc": "생성된 트랜스코드의 최대 크기", + "maximum_transcode_size_head": "최대 트랜스코드 크기", "metadata_path": { "description": "전체 내보내기 또는 전체 불러오기를 실행할 때 사용되는 폴더 위치", "heading": "메타데이터 경로" }, + "number_of_parallel_task_for_scan_generation_desc": "자동으로 설정하려면 0을 입력하세요. 경고: 100% CPU 활용률을 달성하는 데 필요한 작업보다 더 많은 작업을 실행하면 성능이 저하되고 잠재적으로 다른 문제가 발생할 수 있습니다.", + "number_of_parallel_task_for_scan_generation_head": "스캔/생성 병렬 작업 수", "parallel_scan_head": "병렬 스캔/생성", "preview_generation": "생성 미리보기", "python_path": { + "description": "파이썬 실행 파일의 위치입니다. 스크립트 스크레이퍼와 플러그인의 실행에 사용됩니다. 빈 칸으로 두면, 시스템 환경 설정으로부터 위치를 받아 옵니다", "heading": "Python 경로" }, + "scraper_user_agent": "스크레이퍼 사용자 에이전트", + "scraper_user_agent_desc": "HTTP 요청 스크레이핑 중 사용되는 사용자 에이전트 문자열", "scrapers_path": { "description": "스크레이퍼 설정 파일의 폴더 위치", "heading": "스크레이퍼 경로" @@ -232,9 +305,15 @@ "logs": { "log_level": "로그 수준" }, + "plugins": { + "hooks": "후크", + "triggers_on": "트리거 켜기" + }, "scraping": { "entity_metadata": "{entityType} 메타데이터", "entity_scrapers": "{entityType} 스크레이퍼", + "excluded_tag_patterns_desc": "스크레이핑 결과에서 제외할 태그 이름의 정규표현식", + "excluded_tag_patterns_head": "제외된 태그 패턴", "scraper": "스크레이퍼", "scrapers": "스크레이퍼", "search_by_name": "이름으로 찾기", @@ -244,10 +323,21 @@ "stashbox": { "add_instance": "stash-box 인스턴스 추가", "api_key": "API 키", - "name": "이름" + "description": "Stash-box는 식별값 및 파일 이름을 기반으로 영상 및 배우를 자동 태깅합니다.\n엔드포인트 및 API 키는 Stash-Box 인스턴스의 계정 페이지에서 찾을 수 있습니다. 인스턴스를 두 개 이상 추가할 경우 이름이 필요합니다.", + "endpoint": "엔드포인트", + "graphql_endpoint": "GraphQL 엔드포인트", + "name": "이름", + "title": "Stash-box 엔드포인트" + }, + "system": { + "transcoding": "트랜스코딩" }, "tasks": { "added_job_to_queue": "작업 대기열에 {operation_name}을 추가했습니다", + "auto_tag": { + "auto_tagging_all_paths": "모든 경로 자동 태깅 중", + "auto_tagging_paths": "다음 경로 자동 태깅 중" + }, "auto_tag_based_on_filenames": "파일 이름을 통해 컨텐츠에 자동으로 태깅합니다.", "auto_tagging": "자동 태깅", "backing_up_database": "데이터베이스 백업 중", @@ -259,24 +349,51 @@ "dont_include_file_extension_as_part_of_the_title": "제목에 파일 확장자 포함하지 않기", "empty_queue": "실행 중인 작업이 없습니다.", "export_to_json": "메타데이터 폴더에 데이터베이스 컨텐츠를 JSON 형식으로 내보냅니다.", + "generate": { + "generating_from_paths": "다음 경로에서 영상 생성 중", + "generating_scenes": "{num}개의 {scene} 생성 중" + }, + "generate_desc": "이미지, 스프라이트, 비디오, vtt 등 파일을 생성합니다.", "generate_phashes_during_scan": "컨텐츠 해쉬 값 생성", "generate_phashes_during_scan_tooltip": "중복된 파일 확인과 영상 식별에 사용됩니다.", "generate_previews_during_scan": "움직이는 이미지 미리보기 생성", + "generate_previews_during_scan_tooltip": "애니메이션 WebP 미리보기를 생성합니다. 미리보기 유형이 애니메이션 이미지로 설정된 경우에만 필요합니다.", + "generate_sprites_during_scan": "스크러버 스프라이트 생성", "generate_thumbnails_during_scan": "이미지 썸네일 생성", "generate_video_previews_during_scan": "미리보기 생성", "generate_video_previews_during_scan_tooltip": "마우스를 위에 올려놓았을 때 재생되는 비디오 미리보기 생성", "generated_content": "생성된 컨텐츠", "identify": { + "and_create_missing": "또한 누락된 항목 생성", + "create_missing": "누락된 항목 생성", "default_options": "기본값 옵션", + "description": "Stash-Box 및 스크레이퍼 소스를 사용하여 영상 메타데이터를 자동으로 설정합니다.", + "explicit_set_description": "소스 별 옵션에서 재정의되지 않는 경우 다음 옵션이 사용됩니다.", + "field": "항목", + "field_behaviour": "{strategy} {field}", "field_options": "항목 옵션", + "heading": "식별", + "identifying_from_paths": "다음 경로에서 영상 식별 중", + "identifying_scenes": "{num}개의 {scene} 식별 중", "include_male_performers": "남성 배우 포함", - "set_cover_images": "커버 이미지 설정" + "set_cover_images": "커버 이미지 설정", + "set_organized": "'정리됨' 상태로 설정", + "source": "소스", + "source_options": "{source} 옵션", + "sources": "소스", + "strategy": "방법" }, + "import_from_exported_json": "메타데이터 폴더에서 내보낸 JSON 파일에서 가져오기 작업을 합니다. 기존 데이터베이스를 지웁니다.", + "incremental_import": "내보낸 zip 파일에서 증가한 부분만 가져옵니다.", "job_queue": "작업 대기열", "maintenance": "관리", + "migrate_hash_files": "생성된 파일 이름 해쉬를 변경한 후, 기존 생성된 파일의 이름을 새 해쉬 형식으로 바꾸기 위해 사용됩니다.", + "migrations": "마이그레이션", "only_dry_run": "체크만 합니다. 아무 것도 삭제하지 않습니다", + "plugin_tasks": "플러그인 작업", "scan": { - "scanning_all_paths": "모든 경로 스캔 중" + "scanning_all_paths": "모든 경로 스캔 중", + "scanning_paths": "다음 경로 스캔 중" }, "scan_for_content_desc": "새로운 컨텐츠를 스캔하고 데이터베이스에 추가합니다.", "set_name_date_details_from_metadata_if_present": "파일 속성을 통해 이름, 날짜, 세부 사항들을 설정합니다" @@ -287,9 +404,14 @@ "add_field": "항목 추가", "capitalize_title": "제목 앞 글자를 대문자로", "display_fields": "항목 표시하기", + "escape_chars": "\\를 사용하여 리터럴 문자를 이스케이프합니다", "filename": "파일 이름", "filename_pattern": "파일 이름 패턴", + "ignore_organized": "'정리됨' 상태의 영상을 무시", "ignored_words": "무시된 단어들", + "matches_with": "{i}와 일치", + "select_parser_recipe": "파서 레시피 선택", + "title": "영상 파일 이름 파서", "whitespace_chars": "공백 문자", "whitespace_chars_desc": "이 문자들은 제목에서 공백으로 대체됩니다" }, @@ -304,9 +426,14 @@ }, "delete_options": { "description": "이미지, 갤러리, 영상을 삭제할 때의 설정 기본값입니다.", - "heading": "옵션 삭제" + "heading": "옵션 삭제", + "options": { + "delete_file": "기본값으로 파일을 지웁니다", + "delete_generated_supporting_files": "생성된 지원 파일을 기본값으로 삭제합니다" + } }, "desktop_integration": { + "desktop_integration": "데스크탑 통합", "notifications_enabled": "알림 활성화", "send_desktop_notifications_for_events": "이벤트가 발생했을 때 데스크탑 알림을 보냅니다", "skip_opening_browser": "브라우저 자동 열기 해제", @@ -320,6 +447,7 @@ "heading": "수정하기" }, "funscript_offset": { + "description": "대화형 스크립트 재생의 시간 오프셋(밀리초)입니다.", "heading": "Funscript 오프셋 (단위: 밀리초)" }, "handy_connection": { @@ -327,14 +455,35 @@ "server_offset": { "heading": "서버 오프셋" }, + "status": { + "heading": "Handy 연결 상태" + }, "sync": "동기화" }, - "images": { - "heading": "이미지" + "handy_connection_key": { + "description": "대화형 영상에 사용할 수 있는 Handy 연결 키입니다. 이 키를 설정하면 현재 장면 정보를 handyfeeling.com과 공유할 수 있습니다", + "heading": "Handy 연결 키" }, + "image_lightbox": { + "heading": "이미지 라이트박스" + }, + "images": { + "heading": "이미지", + "options": { + "write_image_thumbnails": { + "description": "즉시 생성된 경우 디스크에 이미지 썸네일 쓰기", + "heading": "이미지 썸네일 디스크에 저장하기" + } + } + }, + "interactive_options": "상호작용 옵션", "language": { "heading": "언어" }, + "max_loop_duration": { + "description": "영상 플레이어가 비디오를 루프하는 최대 영상 지속 시간 - 0을 입력하면 비활성화합니다", + "heading": "최대 구간 길이" + }, "menu_items": { "description": "탐색 바에 여러 종류의 컨텐츠들이 보여지게 하거나 숨깁니다", "heading": "메뉴 항목" @@ -342,6 +491,7 @@ "performers": { "options": { "image_location": { + "description": "배우의 기본 이미지를 저장하기 위한 경로입니다. 빈 칸으로 두면 기본값을 사용합니다", "heading": "커스텀 배우 이미지 경로" } } @@ -365,10 +515,15 @@ "heading": "영상 플레이어", "options": { "auto_start_video": "비디오 자동 재생", + "auto_start_video_on_play_selected": { + "description": "'영상' 페이지에서 선택하거나 랜덤 재생한 영상을 자동 시작", + "heading": "선택한 항목을 재생할 때 비디오 자동 시작" + }, "continue_playlist_default": { "description": "비디오가 끝나면 대기열에 있는 다음 영상을 재생합니다", "heading": "플레이리스트 이어보기" - } + }, + "show_scrubber": "스크러버 보이기" } }, "scene_wall": { @@ -378,13 +533,29 @@ "toggle_sound": "소리 켜기" } }, + "scroll_attempts_before_change": { + "description": "이전/다음 항목으로 이동하기 전에 스크롤을 시도하는 횟수입니다. Y축 스크롤 허용 모드에만 적용됩니다.", + "heading": "전환 전 스크롤 시도 횟수" + }, "slideshow_delay": { + "description": "월 보기 모드일 때 갤러리에서 슬라이드 쇼를 사용할 수 있습니다", "heading": "슬라이드쇼 딜레이 (단위: 초)" }, "title": "UI" } }, "configuration": "설정", + "countables": { + "files": "{count, plural, one {파일} other {파일들}}", + "galleries": "{count, plural, one {갤러리} other {갤러리들}}", + "images": "{count, plural, one {이미지} other {이미지들}}", + "markers": "{count, plural, one {마커} other {마커들}}", + "movies": "{count, plural, one {영화} other {영화들}}", + "performers": "{count, plural, one {배우} other {배우들}}", + "scenes": "{count, plural, one {영상} other {영상들}}", + "studios": "{count, plural, one {스튜디오} other {스튜디오들}}", + "tags": "{count, plural, one {태그} other {태그들}}" + }, "country": "국적", "cover_image": "커버 이미지", "created_at": "만든 날짜", @@ -418,43 +589,101 @@ "details": "세부사항", "developmentVersion": "개발 버전", "dialogs": { + "aliases_must_be_unique": "별칭은 유일해야 합니다", + "delete_alert": "다음 {count, plural, one {{singularEntity}} other {{pluralEntity}}}이(가) 영구 삭제될 것입니다:", "delete_confirm": "정말 {entityName}을 삭제하시겠습니까?", + "delete_entity_desc": "정말로 {count, plural, one {{singularEntity}} other {{pluralEntity}}}을(를) 삭제하시겠습니까? 원본 파일이 삭제되지 않으면, 스캔을 진행한 후 {count, plural, one {{singularEntity}} other {{pluralEntity}}}이(가) 다시 추가될 것입니다.", + "delete_entity_title": "{count, plural, one {{singularEntity} 삭제} other {{pluralEntity} 삭제}}", + "delete_galleries_extra": "…그리고 다른 어떤 갤러리에도 없는 이미지 파일들까지.", + "delete_gallery_files": "갤러리 폴더/zip 파일 및 다른 어떤 갤러리에도 존재하지 않는 이미지를 삭제합니다.", + "delete_object_desc": "정말로 {count, plural, one {this {singularEntity}} other {these {pluralEntity}}}을(를) 삭제하시겠습니까?", + "delete_object_overflow": "...그리고 {count} 개의 {count, plural, one {{singularEntity}} other {{pluralEntity}}}.", + "delete_object_title": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} 삭제", + "edit_entity_title": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} 수정", + "export_include_related_objects": "내보내기 할 때 관련된 개체를 포합합니다", "export_title": "내보내기", "lightbox": { "delay": "딜레이 (단위: 초)", "display_mode": { "fit_horizontally": "가로로 맞추기", "fit_to_screen": "스크린 크기에 맞추기", - "label": "표시 모드" + "label": "표시 모드", + "original": "기본 모드" }, "options": "옵션", + "reset_zoom_on_nav": "이미지를 바꿀 때 줌 수준을 초기화합니다", + "scale_up": { + "description": "작은 이미지들이 화면을 채우도록 확대합니다", + "label": "화면에 딱 맞게 확대합니다" + }, "scroll_mode": { "description": "임시로 다른 모드를 사용하려면 Shift 키를 누르세요.", "label": "스크롤 모드", + "pan_y": "수직 스크롤 모드", "zoom": "확대" } }, + "merge_tags": { + "destination": "다른 태그와 합쳐질 태그", + "source": "다른 태그로 합쳐질 태그" + }, + "overwrite_filter_confirm": "정말 원래 저장되어 있었던 쿼리 {entityName}을 덮어쓰시겠습니까?", "scene_gen": { + "force_transcodes": "강제 트랜스코드 생성", + "force_transcodes_tooltip": "기본적으로 트랜스코드는 비디오 파일이 브라우저에서 지원되지 않는 경우에만 생성됩니다. 이 옵션을 선택하면 비디오 파일이 브라우저에서 지원되는 것으로 보이는 경우에도 트랜스코드가 생성됩니다.", "image_previews": "움직이는 이미지 미리보기", + "image_previews_tooltip": "애니메이션 WebP 미리보기. 미리보기 유형이 애니메이션 이미지로 설정된 경우에만 필요합니다.", + "interactive_heatmap_speed": "대화형 영상을 위한 히트맵 및 스피드 생성", "marker_image_previews": "마커 움직이는 이미지 미리보기", + "marker_image_previews_tooltip": "애니메이션 마커 WebP 미리보기. 미리보기 유형이 애니메이션 이미지로 설정된 경우에만 필요합니다.", "marker_screenshots": "마커 스크린샷", + "marker_screenshots_tooltip": "마커 JPG 이미지. 미리보기 유형이 이미지로 설정된 경우에만 필요합니다.", "markers": "마커 미리보기", + "markers_tooltip": "주어진 시간 코드에서 시작하는 20초 짜리 비디오입니다.", + "override_preview_generation_options": "미리보기 생성 옵션 재정의", + "override_preview_generation_options_desc": "이 작업에 대한 미리보기 생성 옵션을 재정의합니다. 기본값은 '시스템' -> '미리보기 생성'에서 설정됩니다.", "overwrite": "이미 생성된 파일들 덮어쓰기", "phash": "해쉬 (중복 방지용)", + "preview_exclude_end_time_desc": "영상 미리보기에서 마지막 x 초를 제외합니다. 초 단위, 혹은 전체 영상 길이에서의 비율(예: 2%)로 나타낼 수 있습니다.", + "preview_exclude_end_time_head": "마지막 영상 부분 제외", + "preview_exclude_start_time_desc": "영상 미리보기에서 처음 x 초를 제외합니다. 초 단위, 혹은 전체 영상 길이에서의 비율(예: 2%)로 나타낼 수 있습니다.", + "preview_exclude_start_time_head": "처음 영상 부분 제외", + "preview_generation_options": "미리보기 생성 옵션", "preview_options": "옵션 미리보기", - "preview_preset_head": "인코딩 프리셋 미리보기" + "preview_preset_desc": "이 설정은 영상 미리보기의 크기, 품질, 미리보기 생성 인코딩 시간을 조절합니다. 설정값을 높인다고 해서 결과가 비례하여 좋아지는 것이 아니므로, \"느림\" 이상으로 설정하는 것을 추천하지 않습니다.", + "preview_preset_head": "인코딩 프리셋 미리보기", + "preview_seg_count_desc": "미리보기 파일에서의 사진 개수입니다.", + "preview_seg_count_head": "미리보기의 사진 개수", + "preview_seg_duration_desc": "미리보기 사진이 표시되는 시간입니다 (초).", + "preview_seg_duration_head": "미리보기 사진 길이", + "sprites": "영상 스크러버 스프라이트", + "sprites_tooltip": "스프라이트 (영상 스크러버 용)", + "transcodes": "트랜스코딩", + "transcodes_tooltip": "지원되지 않는 비디오 형식을 MP4로 변환하기", + "video_previews": "미리보기", + "video_previews_tooltip": "영상 위로 마우스를 올렸을 때 표시되는 비디오 미리보기" }, + "scenes_found": "{count}개의 영상을 찾았습니다", + "scrape_entity_query": "{entity_type} 스크레이핑 쿼리", + "scrape_entity_title": "{entity_type} 스크레이핑 결과", + "scrape_results_existing": "존재", + "scrape_results_scraped": "스크레이핑됨", "set_image_url_title": "이미지 URL", "unsaved_changes": "저장되지 않은 변경 사항들이 있습니다. 그래도 나가겠습니까?" }, + "dimensions": "크기", + "director": "감독", "display_mode": { "grid": "격자", "list": "목록", - "unknown": "알 수 없음" + "tagger": "태거", + "unknown": "알 수 없음", + "wall": "월 모드" }, "donate": "후원", "dupe_check": { "description": "'정확' 이하의 수준에서는 계산이 오래 걸릴 수 있습니다. 낮은 정밀도 수준에서는 부정확한 결과가 함께 나올 수 있습니다.", + "found_sets": "{setCount, plural, one{# 개의 중복된 파일을 찾았습니다.} other {# 개의 중복된 파일들을 찾았습니다.}}", "options": { "exact": "정확", "high": "높음", @@ -464,8 +693,10 @@ "search_accuracy_label": "검색 정밀도", "title": "중복된 영상" }, + "duplicated_phash": "영상 필터에서 선택할 수 있습니다.", "duration": "길이", "effect_filters": { + "aspect": "방향", "blue": "청색", "blur": "흐리게", "brightness": "밝기", @@ -485,8 +716,12 @@ "scale": "크기", "warmth": "따뜻함" }, + "empty_server": "이 페이지에서 추천 영상들을 확인하려면 영상을 추가하세요.", "ethnicity": "인종", + "existing_value": "존재하는 값", "eye_color": "눈동자 색", + "fake_tits": "가짜 가슴", + "false": "거짓", "favourite": "즐겨찾기", "file": "파일", "file_info": "파일 정보", @@ -514,10 +749,13 @@ "handy_connection_status": { "connecting": "접속 중", "disconnected": "접속 끊김", + "error": "Handy에 접속 중 오류 발생", + "missing": "연결 끊김", "ready": "준비됨", "syncing": "서버와 동기화 중", "uploading": "스크립트 업로드 중" }, + "hasMarkers": "마커 유무", "height": "키", "help": "도움말", "ignore_auto_tag": "자동 태깅 무시하기", @@ -528,39 +766,54 @@ "include_sub_studios": "자회사 스튜디오 포함", "include_sub_tags": "하위 태그 포함", "instagram": "인스타그램", + "interactive": "인터렉티브", + "interactive_speed": "인터랙티브 속도", + "isMissing": "누락됨", "library": "라이브러리", "loading": { "generic": "로드 중…" }, "marker_count": "마커 개수", "markers": "마커", + "measurements": "치수", "media_info": { "audio_codec": "오디오 코덱", "checksum": "체크섬", "downloaded_from": "다운로드 출처", "hash": "해쉬", + "interactive_speed": "인터랙티브 속도", "performer_card": { + "age": "{age} {years_old}", "age_context": "작품에서 {age} {years_old}" }, + "phash": "PHash", "stream": "스트림", "video_codec": "비디오 코덱" }, "megabits_per_second": "초당 {value} 메가비트", "metadata": "메타데이터", "movie": "영화", + "movie_scene_number": "영화 씬 번호", "movies": "영화", "name": "이름", + "new": "새로 만들기", + "none": "없음", "o_counter": "싼 횟수", + "operations": "작업", + "organized": "정리됨", "pagination": { "first": "처음", "last": "마지막", "next": "다음", "previous": "이전" }, + "parent_of": "{children}의 상위 태그", "parent_studios": "모회사 스튜디오", "parent_tag_count": "상위 태그 개수", "parent_tags": "상위 태그", + "part_of": "{parent}의 하위 태그", "path": "경로", + "perceptual_similarity": "영상 정렬에서 선택할 수 있습니다.", "performer": "배우", "performerTags": "배우 태그", "performer_age": "배우 나이", @@ -569,6 +822,9 @@ "performer_image": "배우 이미지", "performer_tagger": { "add_new_performers": "새 배우 추가", + "any_names_entered_will_be_queried": "입력되는 이름들은, 원격 Stash-Box 개체에 존재하면 추가됩니다. 정확하게 일치해야만 합니다.", + "batch_add_performers": "배우 일괄 추가", + "batch_update_performers": "배우 일괄 수정", "config": { "active_stash-box_instance": "stash-box 인스턴스 활성화:", "edit_excluded_fields": "제외된 항목 수정", @@ -582,17 +838,22 @@ "name_already_exists": "이름이 이미 존재합니다", "network_error": "네트워크 오류", "no_results_found": "결과가 없습니다.", + "number_of_performers_will_be_processed": "{performer_count}명의 배우들이 처리됩니다", + "performer_already_tagged": "이 배우에 이미 존재하는 태그입니다", "performer_names_separated_by_comma": "배우 이름 (,으로 구분)", + "performer_selection": "배우 선택", "performer_successfully_tagged": "배우 태깅에 성공했습니다:", "query_all_performers_in_the_database": "데이터베이스의 모든 배우", "refresh_tagged_performers": "태그된 배우 새로고침", + "refreshing_will_update_the_data": "새로고침하면 Stash-box 인스턴스에 있는 태그된 배우들의 데이터가 업데이트될 것입니다.", "status_tagging_job_queued": "상태: 태그 작업 대기열 추가됨", "status_tagging_performers": "상태: 배우 태그 중", "tag_status": "태그 상태", "to_use_the_performer_tagger": "배우 태거를 사용하기 위해서는 stash-box 인스턴스가 설정되어야 합니다.", "untagged_performers": "태그되지 않은 배우", "update_performer": "배우 수정", - "update_performers": "배우 수정" + "update_performers": "배우 수정", + "updating_untagged_performers_description": "태그가 지정되지 않은 배우를 업데이트하면, Stash ID가 없는 배우와 비교해본 뒤 메타데이터를 업데이트할 것입니다." }, "performers": "배우", "piercings": "피어싱", @@ -606,6 +867,7 @@ "recently_released_scenes": "최근에 만들어진 영상", "resolution": "해상도", "scene": "영상", + "sceneTagger": "영상 태거", "sceneTags": "영상 태그", "scene_count": "영상 개수", "scene_id": "영상 ID", @@ -646,10 +908,15 @@ "github_repository": "깃허브 저장소", "migrate": { "backup_database_path_leave_empty_to_disable_backup": "백업 데이터베이스 경로 (백업을 하지 않으려면 빈 칸으로 두세요):", + "backup_recommended": "마이그레이션 하기 전 원래 데이터베이스를 백업하는 것을 추천합니다. {defaultBackupPath}에 데이터베이스 복사본을 만들어 드릴 수 있습니다.", "migrating_database": "데이터베이스 마이그레이션 중", "migration_failed": "마이그레이션 실패", "migration_failed_error": "데이터베이스를 마이그레이션 하는 동안 다음 에러가 발생했습니다:", - "perform_schema_migration": "스키마 마이그레이션 실행" + "migration_failed_help": "올바른 내용을 입력했는지 확인하고 수정한 뒤 다시 시도해보세요. 그렇지 않다면, {githubLink}에 버그를 제보하거나 {discordLink}에서 도움이 될 만한 정보를 찾아보세요.", + "migration_irreversible_warning": "스키마 마이그레이션 작업은 돌이킬 수 없습니다. 마이그레이션이 진행된 이후에는, 데이터베이스가 이전 버전의 Stash와 호환되지 않을 것입니다.", + "migration_required": "마이그레이션 필요", + "perform_schema_migration": "스키마 마이그레이션 실행", + "schema_too_old": "현재 Stash 데이터베이스의 스키마 버전은 {databaseSchema}이고, {appSchema} 버전으로 마이그레이션되어야 합니다.이 Stash 버전은 데이터베이스 마이그레이션 없이는 동작하지 않을 것입니다." }, "paths": { "database_filename_empty_for_default": "데이터베이스 파일 이름 (빈 칸으로 두면 기본값을 사용합니다)", @@ -660,30 +927,47 @@ "where_can_stash_store_its_database": "어디에 Stash 데이터베이스를 저장할까요?", "where_can_stash_store_its_database_description": "Stash는 야동 메타데이터를 저장할 때 sqlite 데이터베이스를 사용합니다. 기본값으로, 데이터베이스 파일은 stash-go.sqlite라는 이름으로 설정 파일이 포함된 폴더 안에 생성될 것입니다. 데이터베이스 파일 이름을 바꾸고 싶다면, 절대 경로 또는 상대 경로(현재 경로 기준)를 입력하세요.", "where_can_stash_store_its_generated_content": "생성된 컨텐츠를 어디에 저장할까요?", + "where_can_stash_store_its_generated_content_description": "Stash에서는 썸네일, 미리보기, 스프라이트로 사용할 이미지와 비디오 파일을 생성합니다. 여기에는 지원되지 않는 파일 형식들의 변환본도 포함됩니다. Stash에서는 기본값으로, 설정 파일이 위치한 폴더 안에 generated 폴더를 만들 것입니다. 생성된 미디어 파일들이 저장되는 위치를 변경하고 싶다면, 절대 경로 혹은 상대 경로(현재 폴더 기준)를 적어주세요. 적혀진 경로에 해당 폴더가 없다면 자동으로 생성됩니다.", "where_is_your_porn_located": "야동이 있는 위치가 어딘가요?", "where_is_your_porn_located_description": "야동 폴더를 추가하세요. 비디오와 이미지를 스캐닝할 때 사용됩니다." }, "stash_setup_wizard": "Stash 설정 마법사", "success": { + "getting_help": "도움 받기", "help_links": "문제가 발생하거나 질문, 제안할 점이 있다면, {githubLink}에 이슈를 만들거나, {discordLink}의 커뮤니티를 방문하세요.", "in_app_manual_explained": "상단 우측에 있는 아이콘({icon})을 통해 매뉴얼을 확인해보세요", + "next_config_step_one": "다음으로 설정 페이지에 갈 것입니다. 설정 페이지에서는 포함하거나 제외시킬 파일 설정, 시스템을 보호할 아이디와 비밀번호 설정, 그리고 그 외 여러 가지 옵션들을 설정합니다.", + "next_config_step_two": "이 설정에 만족한다면, {localized_task} 버튼과 {localized_scan} 버튼을 눌러 여러분의 컨텐츠를 스캔할 수 있습니다.", + "open_collective": "Stash가 지속적으로 업데이트되도록 하기 위해 어떻게 기여할 수 있는지 보려면 {open_collective_link}를 확인해보세요.", "support_us": "후원", "thanks_for_trying_stash": "Stash를 사용해주셔서 감사합니다!", + "welcome_contrib": "프로그래밍, 테스팅, 버그 제보, 개선 또는 기능 추가 요청, 사용자 지원 등에 기여하는 것을 환영합니다. Stash 인앱 매뉴얼의 '기여' 항목에서 세부사항을 확인할 수 있습니다.", "your_system_has_been_created": "성공했습니다! 시스템이 생성되었습니다!" }, "welcome": { + "config_path_logic_explained": "Stash에서는 설정 파일을 현재 폴더에서 먼저 찾아보고, 없다면 %USERPROFILE%\\.stash\\config.yml을 찾습니다 (윈도우가 아닌 운영체제에서는 $HOME/.stash/config.yml을 찾습니다). 또는 Stash를 -c <설정 파일 경로> 또는 --config <설정 파일 경로> 옵션을 사용해 실행시켜 특정한 설정 파일을 읽도록 할 수 있습니다.", + "in_current_stash_directory": "$HOME/.stash 폴더 안", + "in_the_current_working_directory": "현재 폴더 안", + "next_step": "그 모든 것을 제외하고, 새로운 시스템 설정을 시작할 준비가 되었다면, 어디에 설정 파일을 저장할지 선택한 뒤 '다음' 버튼을 누르세요.", "store_stash_config": "어디에 Stash 설정 파일을 저장할까요?", "unable_to_locate_config": "이 화면이 나온다면, 설정 파일을 찾는 데에 실패한 것입니다. 새로운 설정 파일을 만드는 과정을 거쳐야 합니다.", "unexpected_explained": "예상치 못하게 이 화면이 나온다면, 올바른 폴더에서 Stash를 재실행하거나, 터미널의 경우 -c 플래그와 함께 실행해보세요." }, "welcome_specific_config": { "config_path": "Stash에서 다음 설정 파일 경로를 사용합니다: {path}", - "next_step": "새로운 시스템을 설정할 준비가 되었다면, '다음'을 누르세요." + "next_step": "새로운 시스템을 설정할 준비가 되었다면, '다음'을 누르세요.", + "unable_to_locate_specified_config": "이 오류 문구가 출력되었다면, 명령어 또는 환경에서 지정된 설정 파일을 찾지 못한 것입니다. 설정 마법사가 새로운 설정 파일을 만드는 과정을 도와줄 것입니다." }, "welcome_to_stash": "Stash에 오신 것을 환영합니다" }, "stash_id": "Stash ID", "stash_ids": "Stash IDs", + "stashbox": { + "go_review_draft": "초안을 검토하려면 {endpoint_name}(으)로 이동하십시오.", + "selected_stash_box": "Stash-Box 엔드포인트를 선택했습니다", + "submission_failed": "데이터 제출 실패", + "submission_successful": "데이터 제출 성공" + }, "statistics": "통계", "stats": { "image_size": "전체 이미지 크기", @@ -709,7 +993,11 @@ "added_generation_job_to_queue": "생성 작업을 대기열에 추가했습니다", "created_entity": "{entity}를 생성했습니다", "default_filter_set": "기본 필터 셋", + "delete_entity": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} 삭제", + "delete_past_tense": "{count, plural, one {{singularEntity}} other {{pluralEntity}}}이(가) 삭제되었습니다", "generating_screenshot": "스크린샷을 생성하는 중…", + "merged_tags": "병합된 태그", + "rescanning_entity": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} 다시 스캔하는 중…", "saved_entity": "{entity}를 저장했습니다", "started_auto_tagging": "자동 태깅을 시작했습니다", "started_generating": "생성을 시작했습니다", diff --git a/ui/v2.5/src/locales/zh-CN.json b/ui/v2.5/src/locales/zh-CN.json index 82c7ca47a..e269f3e91 100644 --- a/ui/v2.5/src/locales/zh-CN.json +++ b/ui/v2.5/src/locales/zh-CN.json @@ -191,7 +191,7 @@ "metadata_providers": "元数据提供者", "plugins": "插件", "scraping": "挖掘", - "security": "安保", + "security": "安全性", "services": "服务", "system": "系统", "tasks": "任务", @@ -449,6 +449,10 @@ "description": "交互式脚本播放的时间偏移量(以毫秒为单位)。", "heading": "Funscript偏移量(毫秒)" }, + "handy_connection": { + "connect": "连接", + "sync": "同步" + }, "handy_connection_key": { "description": "用于互动场景的快速连接密钥。设定此密匙会允许Stash分享你当前的短片资料到handyfeeling.com", "heading": "快速连接密钥" @@ -730,6 +734,11 @@ "TRANSGENDER_MALE": "跨性别男性" }, "hair_color": "头发颜色", + "handy_connection_status": { + "connecting": "连接中", + "disconnected": "连接已断开", + "syncing": "正在和服务器同步" + }, "hasMarkers": "含有章节标记", "height": "身高", "help": "说明", @@ -835,6 +844,11 @@ "queue": "序列", "random": "随机", "rating": "评分", + "recently_added_performers": "最近新增的演员", + "recently_added_studios": "最近新增的工作室", + "recently_released_galleries": "最近新增的图片", + "recently_released_movies": "最近新增的电影", + "recently_released_scenes": "最近新增的短片", "resolution": "分辨率", "scene": "短片", "sceneTagger": "短片标记器", @@ -938,6 +952,7 @@ "submission_failed": "提交失败", "submission_successful": "成功提交" }, + "statistics": "统计", "stats": { "image_size": "图片大小", "scenes_duration": "短片长度", @@ -979,6 +994,7 @@ "updated_at": "更新时间", "url": "链接", "videos": "视频", + "view_all": "查看全部", "weight": "体重", "years_old": "岁" } From ba2979096a3ff7279c0a91802d18f96840fc4b15 Mon Sep 17 00:00:00 2001 From: kermieisinthehouse Date: Wed, 25 May 2022 19:35:24 -0700 Subject: [PATCH 02/34] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1dff1bf58..ddfb8159a 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,9 @@ Many community-maintained scrapers are available for download at the [Community # Translation [![Translate](https://translate.stashapp.cc/widgets/stash/-/stash-desktop-client/svg-badge.svg)](https://translate.stashapp.cc/engage/stash/) -🇧🇷 🇨🇳 🇩🇰 🇳🇱 🇬🇧 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇵🇱 🇪🇸 🇸🇪 🇹🇼 🇹🇷 +🇧🇷 🇨🇳 🇩🇰 🇳🇱 🇬🇧 🇫🇮 🇫🇷 🇩🇪 🇮🇹 🇯🇵 🇰🇷 🇵🇱 🇪🇸 🇸🇪 🇹🇼 🇹🇷 -Stash is available in 15 languages (so far!) and it could be in your language too. If you want to help us translate Stash into your language, you can make an account at [translate.stashapp.cc](https://translate.stashapp.cc/projects/stash/stash-desktop-client/) to get started contributing new languages or improving existing ones. Thanks! +Stash is available in 16 languages (so far!) and it could be in your language too. If you want to help us translate Stash into your language, you can make an account at [translate.stashapp.cc](https://translate.stashapp.cc/projects/stash/stash-desktop-client/) to get started contributing new languages or improving existing ones. Thanks! # Support (FAQ) From 88111e40641a2540507abb36f1cb6938cb0e915f Mon Sep 17 00:00:00 2001 From: kermieisinthehouse Date: Wed, 25 May 2022 19:37:31 -0700 Subject: [PATCH 03/34] Remove (Preview) From Korean --- .../Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index ecbf9e639..b3fde0c1a 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -98,7 +98,7 @@ export const SettingsInterfacePanel: React.FC = () => { - + From 1c18ec1501ade9f7e62a46d170ddca2fc5911186 Mon Sep 17 00:00:00 2001 From: HijackHornet Date: Mon, 30 May 2022 17:43:01 +0200 Subject: [PATCH 04/34] Fixed windows install links (#2635) --- docs/DEVELOPMENT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 41ea82dcd..4119575a9 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -15,10 +15,10 @@ NOTE: You may need to run the `go get` commands outside the project directory to ### Windows 1. Download and install [Go for Windows](https://golang.org/dl/) -2. Download and install [MingW](https://sourceforge.net/projects/mingw/) and select packages `mingw32-base` +2. Download and extract [MingW64](https://sourceforge.net/projects/mingw-w64/files/) (scroll down and select x86_64-posix-seh, dont use the autoinstaller it doesnt work) 3. Search for "advanced system settings" and open the system properties dialog. 1. Click the `Environment Variables` button - 2. Under system variables find the `Path`. Edit and add `C:\MinGW\bin` (replace * with the correct path). + 2. Under system variables find the `Path`. Edit and add `C:\MinGW\bin` (replace with the correct path to where you extracted MingW64). NOTE: The `make` command in Windows will be `mingw32-make` with MingW. For example `make pre-ui` will be `mingw32-make pre-ui` From 49f579e08ee8edc3990db10c1e4a7bc21d6f653d Mon Sep 17 00:00:00 2001 From: DingDongSoLong4 <99329275+DingDongSoLong4@users.noreply.github.com> Date: Wed, 1 Jun 2022 03:58:44 +0200 Subject: [PATCH 05/34] Fix gallery updating (#2611) --- pkg/gallery/scan.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/gallery/scan.go b/pkg/gallery/scan.go index 3528c8b93..f45a26d77 100644 --- a/pkg/gallery/scan.go +++ b/pkg/gallery/scan.go @@ -187,7 +187,8 @@ func (scanner *Scanner) ScanNew(ctx context.Context, file file.SourceFile) (retG scanner.PluginCache.ExecutePostHooks(ctx, g.ID, plugin.GalleryUpdatePost, nil, nil) } - scanImages = isNewGallery + // Also scan images if zip file has been moved (ie updated) as the image paths are no longer valid + scanImages = isNewGallery || isUpdatedGallery retGallery = g return From e51083c26d7151600f870c41faadac08d51877b1 Mon Sep 17 00:00:00 2001 From: InfiniteTF Date: Wed, 1 Jun 2022 04:59:06 +0200 Subject: [PATCH 06/34] Update stash-box fingerprint query to fully support distance matching (#2509) --- graphql/schema/schema.graphql | 6 - graphql/stash-box/query.graphql | 6 + internal/api/resolver_query_scraper.go | 63 +- internal/manager/task_identify.go | 3 +- .../stashbox/graphql/generated_client.go | 547 +++++++++++------- .../stashbox/graphql/generated_models.go | 116 ++-- pkg/scraper/stashbox/stash_box.go | 169 ++---- 7 files changed, 466 insertions(+), 444 deletions(-) diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index 9b5bf6ed7..b81168a9a 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -114,12 +114,6 @@ type Query { """Scrape a list of performers from a query""" scrapeFreeonesPerformerList(query: String!): [String!]! @deprecated(reason: "use scrapeSinglePerformer with scraper_id = builtin_freeones") - """Query StashBox for scenes""" - queryStashBoxScene(input: StashBoxSceneQueryInput!): [ScrapedScene!]! @deprecated(reason: "use scrapeSingleScene or scrapeMultiScenes") - """Query StashBox for performers""" - queryStashBoxPerformer(input: StashBoxPerformerQueryInput!): [StashBoxPerformerQueryResult!]! @deprecated(reason: "use scrapeSinglePerformer or scrapeMultiPerformers") - # === end deprecated methods === - # Plugins """List loaded plugins""" plugins: [Plugin!] diff --git a/graphql/stash-box/query.graphql b/graphql/stash-box/query.graphql index 39bce5d3c..12f12d2a5 100644 --- a/graphql/stash-box/query.graphql +++ b/graphql/stash-box/query.graphql @@ -129,6 +129,12 @@ query FindScenesByFullFingerprints($fingerprints: [FingerprintQueryInput!]!) { } } +query FindScenesBySceneFingerprints($fingerprints: [[FingerprintQueryInput!]!]!) { + findScenesBySceneFingerprints(fingerprints: $fingerprints) { + ...SceneFragment + } +} + query SearchScene($term: String!) { searchScene(term: $term) { ...SceneFragment diff --git a/internal/api/resolver_query_scraper.go b/internal/api/resolver_query_scraper.go index 2208628d5..8fd6c345a 100644 --- a/internal/api/resolver_query_scraper.go +++ b/internal/api/resolver_query_scraper.go @@ -227,46 +227,6 @@ func (r *queryResolver) ScrapeMovieURL(ctx context.Context, url string) (*models return marshalScrapedMovie(content) } -func (r *queryResolver) QueryStashBoxScene(ctx context.Context, input models.StashBoxSceneQueryInput) ([]*models.ScrapedScene, error) { - boxes := config.GetInstance().GetStashBoxes() - - if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) { - return nil, fmt.Errorf("%w: invalid stash_box_index %d", ErrInput, input.StashBoxIndex) - } - - client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager) - - if len(input.SceneIds) > 0 { - return client.FindStashBoxScenesByFingerprintsFlat(ctx, input.SceneIds) - } - - if input.Q != nil { - return client.QueryStashBoxScene(ctx, *input.Q) - } - - return nil, nil -} - -func (r *queryResolver) QueryStashBoxPerformer(ctx context.Context, input models.StashBoxPerformerQueryInput) ([]*models.StashBoxPerformerQueryResult, error) { - boxes := config.GetInstance().GetStashBoxes() - - if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) { - return nil, fmt.Errorf("%w: invalid stash_box_index %d", ErrInput, input.StashBoxIndex) - } - - client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager) - - if len(input.PerformerIds) > 0 { - return client.FindStashBoxPerformersByNames(ctx, input.PerformerIds) - } - - if input.Q != nil { - return client.QueryStashBoxPerformer(ctx, *input.Q) - } - - return nil, nil -} - func (r *queryResolver) getStashBoxClient(index int) (*stashbox.Client, error) { boxes := config.GetInstance().GetStashBoxes() @@ -280,6 +240,15 @@ func (r *queryResolver) getStashBoxClient(index int) (*stashbox.Client, error) { func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source models.ScraperSourceInput, input models.ScrapeSingleSceneInput) ([]*models.ScrapedScene, error) { var ret []*models.ScrapedScene + var sceneID int + if input.SceneID != nil { + var err error + sceneID, err = strconv.Atoi(*input.SceneID) + if err != nil { + return nil, fmt.Errorf("%w: sceneID is not an integer: '%s'", ErrInput, *input.SceneID) + } + } + switch { case source.ScraperID != nil: var err error @@ -288,11 +257,6 @@ func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source models.Scr switch { case input.SceneID != nil: - var sceneID int - sceneID, err = strconv.Atoi(*input.SceneID) - if err != nil { - return nil, fmt.Errorf("%w: sceneID is not an integer: '%s'", ErrInput, *input.SceneID) - } c, err = r.scraperCache().ScrapeID(ctx, *source.ScraperID, sceneID, models.ScrapeContentTypeScene) if c != nil { content = []models.ScrapedContent{c} @@ -324,7 +288,7 @@ func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source models.Scr switch { case input.SceneID != nil: - ret, err = client.FindStashBoxScenesByFingerprintsFlat(ctx, []string{*input.SceneID}) + ret, err = client.FindStashBoxSceneByFingerprints(ctx, sceneID) case input.Query != nil: ret, err = client.QueryStashBoxScene(ctx, *input.Query) default: @@ -352,7 +316,12 @@ func (r *queryResolver) ScrapeMultiScenes(ctx context.Context, source models.Scr return nil, err } - return client.FindStashBoxScenesByFingerprints(ctx, input.SceneIds) + sceneIDs, err := stringslice.StringSliceToIntSlice(input.SceneIds) + if err != nil { + return nil, err + } + + return client.FindStashBoxScenesByFingerprints(ctx, sceneIDs) } return nil, errors.New("scraper_id or stash_box_index must be set") diff --git a/internal/manager/task_identify.go b/internal/manager/task_identify.go index 54bb063e4..678d0c7b3 100644 --- a/internal/manager/task_identify.go +++ b/internal/manager/task_identify.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strconv" "github.com/stashapp/stash/internal/identify" "github.com/stashapp/stash/pkg/job" @@ -212,7 +211,7 @@ type stashboxSource struct { } func (s stashboxSource) ScrapeScene(ctx context.Context, sceneID int) (*models.ScrapedScene, error) { - results, err := s.FindStashBoxScenesByFingerprintsFlat(ctx, []string{strconv.Itoa(sceneID)}) + results, err := s.FindStashBoxSceneByFingerprints(ctx, sceneID) if err != nil { return nil, fmt.Errorf("error querying stash-box using scene ID %d: %w", sceneID, err) } diff --git a/pkg/scraper/stashbox/graphql/generated_client.go b/pkg/scraper/stashbox/graphql/generated_client.go index 37985a20f..248f3e711 100644 --- a/pkg/scraper/stashbox/graphql/generated_client.go +++ b/pkg/scraper/stashbox/graphql/generated_client.go @@ -12,6 +12,7 @@ import ( type StashBoxGraphQLClient interface { FindSceneByFingerprint(ctx context.Context, fingerprint FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByFingerprint, error) FindScenesByFullFingerprints(ctx context.Context, fingerprints []*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesByFullFingerprints, error) + FindScenesBySceneFingerprints(ctx context.Context, fingerprints [][]*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesBySceneFingerprints, error) SearchScene(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchScene, error) SearchPerformer(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchPerformer, error) FindPerformerByID(ctx context.Context, id string, httpRequestOptions ...client.HTTPRequestOption) (*FindPerformerByID, error) @@ -31,32 +32,33 @@ func NewClient(cli *http.Client, baseURL string, options ...client.HTTPRequestOp } type Query struct { - FindPerformer *Performer "json:\"findPerformer\" graphql:\"findPerformer\"" - QueryPerformers QueryPerformersResultType "json:\"queryPerformers\" graphql:\"queryPerformers\"" - FindStudio *Studio "json:\"findStudio\" graphql:\"findStudio\"" - QueryStudios QueryStudiosResultType "json:\"queryStudios\" graphql:\"queryStudios\"" - FindTag *Tag "json:\"findTag\" graphql:\"findTag\"" - QueryTags QueryTagsResultType "json:\"queryTags\" graphql:\"queryTags\"" - FindTagCategory *TagCategory "json:\"findTagCategory\" graphql:\"findTagCategory\"" - QueryTagCategories QueryTagCategoriesResultType "json:\"queryTagCategories\" graphql:\"queryTagCategories\"" - FindScene *Scene "json:\"findScene\" graphql:\"findScene\"" - FindSceneByFingerprint []*Scene "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\"" - FindScenesByFingerprints []*Scene "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\"" - FindScenesByFullFingerprints []*Scene "json:\"findScenesByFullFingerprints\" graphql:\"findScenesByFullFingerprints\"" - QueryScenes QueryScenesResultType "json:\"queryScenes\" graphql:\"queryScenes\"" - FindSite *Site "json:\"findSite\" graphql:\"findSite\"" - QuerySites QuerySitesResultType "json:\"querySites\" graphql:\"querySites\"" - FindEdit *Edit "json:\"findEdit\" graphql:\"findEdit\"" - QueryEdits QueryEditsResultType "json:\"queryEdits\" graphql:\"queryEdits\"" - FindUser *User "json:\"findUser\" graphql:\"findUser\"" - QueryUsers QueryUsersResultType "json:\"queryUsers\" graphql:\"queryUsers\"" - Me *User "json:\"me\" graphql:\"me\"" - SearchPerformer []*Performer "json:\"searchPerformer\" graphql:\"searchPerformer\"" - SearchScene []*Scene "json:\"searchScene\" graphql:\"searchScene\"" - FindDraft *Draft "json:\"findDraft\" graphql:\"findDraft\"" - FindDrafts []*Draft "json:\"findDrafts\" graphql:\"findDrafts\"" - Version Version "json:\"version\" graphql:\"version\"" - GetConfig StashBoxConfig "json:\"getConfig\" graphql:\"getConfig\"" + FindPerformer *Performer "json:\"findPerformer\" graphql:\"findPerformer\"" + QueryPerformers QueryPerformersResultType "json:\"queryPerformers\" graphql:\"queryPerformers\"" + FindStudio *Studio "json:\"findStudio\" graphql:\"findStudio\"" + QueryStudios QueryStudiosResultType "json:\"queryStudios\" graphql:\"queryStudios\"" + FindTag *Tag "json:\"findTag\" graphql:\"findTag\"" + QueryTags QueryTagsResultType "json:\"queryTags\" graphql:\"queryTags\"" + FindTagCategory *TagCategory "json:\"findTagCategory\" graphql:\"findTagCategory\"" + QueryTagCategories QueryTagCategoriesResultType "json:\"queryTagCategories\" graphql:\"queryTagCategories\"" + FindScene *Scene "json:\"findScene\" graphql:\"findScene\"" + FindSceneByFingerprint []*Scene "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\"" + FindScenesByFingerprints []*Scene "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\"" + FindScenesByFullFingerprints []*Scene "json:\"findScenesByFullFingerprints\" graphql:\"findScenesByFullFingerprints\"" + FindScenesBySceneFingerprints [][]*Scene "json:\"findScenesBySceneFingerprints\" graphql:\"findScenesBySceneFingerprints\"" + QueryScenes QueryScenesResultType "json:\"queryScenes\" graphql:\"queryScenes\"" + FindSite *Site "json:\"findSite\" graphql:\"findSite\"" + QuerySites QuerySitesResultType "json:\"querySites\" graphql:\"querySites\"" + FindEdit *Edit "json:\"findEdit\" graphql:\"findEdit\"" + QueryEdits QueryEditsResultType "json:\"queryEdits\" graphql:\"queryEdits\"" + FindUser *User "json:\"findUser\" graphql:\"findUser\"" + QueryUsers QueryUsersResultType "json:\"queryUsers\" graphql:\"queryUsers\"" + Me *User "json:\"me\" graphql:\"me\"" + SearchPerformer []*Performer "json:\"searchPerformer\" graphql:\"searchPerformer\"" + SearchScene []*Scene "json:\"searchScene\" graphql:\"searchScene\"" + FindDraft *Draft "json:\"findDraft\" graphql:\"findDraft\"" + FindDrafts []*Draft "json:\"findDrafts\" graphql:\"findDrafts\"" + Version Version "json:\"version\" graphql:\"version\"" + GetConfig StashBoxConfig "json:\"getConfig\" graphql:\"getConfig\"" } type Mutation struct { SceneCreate *Scene "json:\"sceneCreate\" graphql:\"sceneCreate\"" @@ -95,6 +97,10 @@ type Mutation struct { PerformerEdit Edit "json:\"performerEdit\" graphql:\"performerEdit\"" StudioEdit Edit "json:\"studioEdit\" graphql:\"studioEdit\"" TagEdit Edit "json:\"tagEdit\" graphql:\"tagEdit\"" + SceneEditUpdate Edit "json:\"sceneEditUpdate\" graphql:\"sceneEditUpdate\"" + PerformerEditUpdate Edit "json:\"performerEditUpdate\" graphql:\"performerEditUpdate\"" + StudioEditUpdate Edit "json:\"studioEditUpdate\" graphql:\"studioEditUpdate\"" + TagEditUpdate Edit "json:\"tagEditUpdate\" graphql:\"tagEditUpdate\"" EditVote Edit "json:\"editVote\" graphql:\"editVote\"" EditComment Edit "json:\"editComment\" graphql:\"editComment\"" ApplyEdit Edit "json:\"applyEdit\" graphql:\"applyEdit\"" @@ -190,6 +196,9 @@ type FindSceneByFingerprint struct { type FindScenesByFullFingerprints struct { FindScenesByFullFingerprints []*SceneFragment "json:\"findScenesByFullFingerprints\" graphql:\"findScenesByFullFingerprints\"" } +type FindScenesBySceneFingerprints struct { + FindScenesBySceneFingerprints [][]*SceneFragment "json:\"findScenesBySceneFingerprints\" graphql:\"findScenesBySceneFingerprints\"" +} type SearchScene struct { SearchScene []*SceneFragment "json:\"searchScene\" graphql:\"searchScene\"" } @@ -240,6 +249,10 @@ fragment StudioFragment on Studio { ... ImageFragment } } +fragment TagFragment on Tag { + name + id +} fragment PerformerFragment on Performer { id name @@ -274,16 +287,6 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} fragment BodyModificationFragment on BodyModification { location description @@ -324,16 +327,22 @@ fragment ImageFragment on Image { width height } -fragment TagFragment on Tag { - name - id -} fragment PerformerAppearanceFragment on PerformerAppearance { as performer { ... PerformerFragment } } +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} ` func (c *Client) FindSceneByFingerprint(ctx context.Context, fingerprint FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByFingerprint, error) { @@ -354,10 +363,6 @@ const FindScenesByFullFingerprintsDocument = `query FindScenesByFullFingerprints ... SceneFragment } } -fragment URLFragment on URL { - url - type -} fragment StudioFragment on Studio { name id @@ -368,11 +373,9 @@ fragment StudioFragment on Studio { ... ImageFragment } } -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } +fragment TagFragment on Tag { + name + id } fragment PerformerFragment on Performer { id @@ -412,6 +415,15 @@ fragment FuzzyDateFragment on FuzzyDate { date accuracy } +fragment BodyModificationFragment on BodyModification { + location + description +} +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration +} fragment SceneFragment on Scene { id title @@ -437,9 +449,81 @@ fragment SceneFragment on Scene { ... FingerprintFragment } } -fragment TagFragment on Tag { - name +fragment ImageFragment on Image { id + url + width + height +} +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} +fragment URLFragment on URL { + url + type +} +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } +} +` + +func (c *Client) FindScenesByFullFingerprints(ctx context.Context, fingerprints []*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesByFullFingerprints, error) { + vars := map[string]interface{}{ + "fingerprints": fingerprints, + } + + var res FindScenesByFullFingerprints + if err := c.Client.Post(ctx, "FindScenesByFullFingerprints", FindScenesByFullFingerprintsDocument, &res, vars, httpRequestOptions...); err != nil { + return nil, err + } + + return &res, nil +} + +const FindScenesBySceneFingerprintsDocument = `query FindScenesBySceneFingerprints ($fingerprints: [[FingerprintQueryInput!]!]!) { + findScenesBySceneFingerprints(fingerprints: $fingerprints) { + ... SceneFragment + } +} +fragment PerformerFragment on Performer { + id + name + disambiguation + aliases + gender + merged_ids + urls { + ... URLFragment + } + images { + ... ImageFragment + } + birthdate { + ... FuzzyDateFragment + } + ethnicity + country + eye_color + hair_color + height + measurements { + ... MeasurementsFragment + } + breast_type + career_start_year + career_end_year + tattoos { + ... BodyModificationFragment + } + piercings { + ... BodyModificationFragment + } } fragment MeasurementsFragment on Measurements { band_size @@ -462,15 +546,68 @@ fragment ImageFragment on Image { width height } +fragment URLFragment on URL { + url + type +} +fragment StudioFragment on Studio { + name + id + urls { + ... URLFragment + } + images { + ... ImageFragment + } +} +fragment TagFragment on Tag { + name + id +} +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } +} +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} +fragment SceneFragment on Scene { + id + title + details + duration + date + urls { + ... URLFragment + } + images { + ... ImageFragment + } + studio { + ... StudioFragment + } + tags { + ... TagFragment + } + performers { + ... PerformerAppearanceFragment + } + fingerprints { + ... FingerprintFragment + } +} ` -func (c *Client) FindScenesByFullFingerprints(ctx context.Context, fingerprints []*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesByFullFingerprints, error) { +func (c *Client) FindScenesBySceneFingerprints(ctx context.Context, fingerprints [][]*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesBySceneFingerprints, error) { vars := map[string]interface{}{ "fingerprints": fingerprints, } - var res FindScenesByFullFingerprints - if err := c.Client.Post(ctx, "FindScenesByFullFingerprints", FindScenesByFullFingerprintsDocument, &res, vars, httpRequestOptions...); err != nil { + var res FindScenesBySceneFingerprints + if err := c.Client.Post(ctx, "FindScenesBySceneFingerprints", FindScenesBySceneFingerprintsDocument, &res, vars, httpRequestOptions...); err != nil { return nil, err } @@ -490,72 +627,6 @@ fragment TagFragment on Tag { name id } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} -fragment BodyModificationFragment on BodyModification { - location - description -} -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration -} -fragment SceneFragment on Scene { - id - title - details - duration - date - urls { - ... URLFragment - } - images { - ... ImageFragment - } - studio { - ... StudioFragment - } - tags { - ... TagFragment - } - performers { - ... PerformerAppearanceFragment - } - fingerprints { - ... FingerprintFragment - } -} -fragment ImageFragment on Image { - id - url - width - height -} -fragment StudioFragment on Studio { - name - id - urls { - ... URLFragment - } - images { - ... ImageFragment - } -} -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} fragment PerformerFragment on Performer { id name @@ -590,6 +661,72 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } +fragment SceneFragment on Scene { + id + title + details + duration + date + urls { + ... URLFragment + } + images { + ... ImageFragment + } + studio { + ... StudioFragment + } + tags { + ... TagFragment + } + performers { + ... PerformerAppearanceFragment + } + fingerprints { + ... FingerprintFragment + } +} +fragment StudioFragment on Studio { + name + id + urls { + ... URLFragment + } + images { + ... ImageFragment + } +} +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } +} +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} +fragment BodyModificationFragment on BodyModification { + location + description +} +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration +} +fragment ImageFragment on Image { + id + url + width + height +} ` func (c *Client) SearchScene(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchScene, error) { @@ -610,16 +747,6 @@ const SearchPerformerDocument = `query SearchPerformer ($term: String!) { ... PerformerFragment } } -fragment URLFragment on URL { - url - type -} -fragment ImageFragment on Image { - id - url - width - height -} fragment FuzzyDateFragment on FuzzyDate { date accuracy @@ -668,6 +795,16 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } +fragment URLFragment on URL { + url + type +} +fragment ImageFragment on Image { + id + url + width + height +} ` func (c *Client) SearchPerformer(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchPerformer, error) { @@ -688,6 +825,16 @@ const FindPerformerByIDDocument = `query FindPerformerByID ($id: ID!) { ... PerformerFragment } } +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} fragment BodyModificationFragment on BodyModification { location description @@ -736,16 +883,6 @@ fragment ImageFragment on Image { width height } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} ` func (c *Client) FindPerformerByID(ctx context.Context, id string, httpRequestOptions ...client.HTTPRequestOption) (*FindPerformerByID, error) { @@ -766,63 +903,11 @@ const FindSceneByIDDocument = `query FindSceneByID ($id: ID!) { ... SceneFragment } } -fragment PerformerFragment on Performer { +fragment ImageFragment on Image { id - name - disambiguation - aliases - gender - merged_ids - urls { - ... URLFragment - } - images { - ... ImageFragment - } - birthdate { - ... FuzzyDateFragment - } - ethnicity - country - eye_color - hair_color - height - measurements { - ... MeasurementsFragment - } - breast_type - career_start_year - career_end_year - tattoos { - ... BodyModificationFragment - } - piercings { - ... BodyModificationFragment - } -} -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} -fragment BodyModificationFragment on BodyModification { - location - description -} -fragment URLFragment on URL { url - type -} -fragment StudioFragment on Studio { - name - id - urls { - ... URLFragment - } - images { - ... ImageFragment - } + width + height } fragment TagFragment on Tag { name @@ -834,14 +919,15 @@ fragment PerformerAppearanceFragment on PerformerAppearance { ... PerformerFragment } } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip } -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration +fragment BodyModificationFragment on BodyModification { + location + description } fragment SceneFragment on Scene { id @@ -868,11 +954,62 @@ fragment SceneFragment on Scene { ... FingerprintFragment } } -fragment ImageFragment on Image { - id +fragment URLFragment on URL { url - width + type +} +fragment StudioFragment on Studio { + name + id + urls { + ... URLFragment + } + images { + ... ImageFragment + } +} +fragment PerformerFragment on Performer { + id + name + disambiguation + aliases + gender + merged_ids + urls { + ... URLFragment + } + images { + ... ImageFragment + } + birthdate { + ... FuzzyDateFragment + } + ethnicity + country + eye_color + hair_color height + measurements { + ... MeasurementsFragment + } + breast_type + career_start_year + career_end_year + tattoos { + ... BodyModificationFragment + } + piercings { + ... BodyModificationFragment + } +} +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration } ` diff --git a/pkg/scraper/stashbox/graphql/generated_models.go b/pkg/scraper/stashbox/graphql/generated_models.go index 2ce0301bb..cfa893c23 100644 --- a/pkg/scraper/stashbox/graphql/generated_models.go +++ b/pkg/scraper/stashbox/graphql/generated_models.go @@ -88,8 +88,8 @@ type DraftEntity struct { ID *string `json:"id,omitempty"` } -func (DraftEntity) IsSceneDraftPerformer() {} func (DraftEntity) IsSceneDraftStudio() {} +func (DraftEntity) IsSceneDraftPerformer() {} func (DraftEntity) IsSceneDraftTag() {} type DraftEntityInput struct { @@ -130,7 +130,9 @@ type Edit struct { Status VoteStatusEnum `json:"status"` Applied bool `json:"applied"` Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Updated *time.Time `json:"updated,omitempty"` + Closed *time.Time `json:"closed,omitempty"` + Expires *time.Time `json:"expires,omitempty"` } type EditComment struct { @@ -149,8 +151,6 @@ type EditInput struct { // Not required for create type ID *string `json:"id,omitempty"` Operation OperationEnum `json:"operation"` - // Required for amending an existing edit - EditID *string `json:"edit_id,omitempty"` // Only required for merge type MergeSourceIds []string `json:"merge_source_ids,omitempty"` Comment *string `json:"comment,omitempty"` @@ -206,15 +206,13 @@ type Fingerprint struct { } type FingerprintEditInput struct { - UserIds []string `json:"user_ids,omitempty"` - Hash string `json:"hash"` - Algorithm FingerprintAlgorithm `json:"algorithm"` - Duration int `json:"duration"` - Created time.Time `json:"created"` - // @deprecated(reason: "unused") - Submissions *int `json:"submissions,omitempty"` - // @deprecated(reason: "unused") - Updated *time.Time `json:"updated,omitempty"` + UserIds []string `json:"user_ids,omitempty"` + Hash string `json:"hash"` + Algorithm FingerprintAlgorithm `json:"algorithm"` + Duration int `json:"duration"` + Created time.Time `json:"created"` + Submissions *int `json:"submissions,omitempty"` + Updated *time.Time `json:"updated,omitempty"` } type FingerprintInput struct { @@ -241,11 +239,6 @@ type FuzzyDate struct { Accuracy DateAccuracyEnum `json:"accuracy"` } -type FuzzyDateInput struct { - Date string `json:"date"` - Accuracy DateAccuracyEnum `json:"accuracy"` -} - type GrantInviteInput struct { UserID string `json:"user_id"` Amount int `json:"amount"` @@ -294,13 +287,6 @@ type Measurements struct { Hip *int `json:"hip,omitempty"` } -type MeasurementsInput struct { - CupSize *string `json:"cup_size,omitempty"` - BandSize *int `json:"band_size,omitempty"` - Waist *int `json:"waist,omitempty"` - Hip *int `json:"hip,omitempty"` -} - type MultiIDCriterionInput struct { Value []string `json:"value,omitempty"` Modifier CriterionModifier `json:"modifier"` @@ -324,6 +310,7 @@ type Performer struct { Gender *GenderEnum `json:"gender,omitempty"` Urls []*URL `json:"urls,omitempty"` Birthdate *FuzzyDate `json:"birthdate,omitempty"` + BirthDate *string `json:"birth_date,omitempty"` Age *int `json:"age,omitempty"` Ethnicity *EthnicityEnum `json:"ethnicity,omitempty"` Country *string `json:"country,omitempty"` @@ -332,6 +319,10 @@ type Performer struct { // Height in cm Height *int `json:"height,omitempty"` Measurements *Measurements `json:"measurements,omitempty"` + CupSize *string `json:"cup_size,omitempty"` + BandSize *int `json:"band_size,omitempty"` + WaistSize *int `json:"waist_size,omitempty"` + HipSize *int `json:"hip_size,omitempty"` BreastType *BreastTypeEnum `json:"breast_type,omitempty"` CareerStartYear *int `json:"career_start_year,omitempty"` CareerEndYear *int `json:"career_end_year,omitempty"` @@ -348,8 +339,8 @@ type Performer struct { Updated time.Time `json:"updated"` } -func (Performer) IsSceneDraftPerformer() {} func (Performer) IsEditTarget() {} +func (Performer) IsSceneDraftPerformer() {} type PerformerAppearance struct { Performer *Performer `json:"performer,omitempty"` @@ -369,13 +360,16 @@ type PerformerCreateInput struct { Aliases []string `json:"aliases,omitempty"` Gender *GenderEnum `json:"gender,omitempty"` Urls []*URLInput `json:"urls,omitempty"` - Birthdate *FuzzyDateInput `json:"birthdate,omitempty"` + Birthdate *string `json:"birthdate,omitempty"` Ethnicity *EthnicityEnum `json:"ethnicity,omitempty"` Country *string `json:"country,omitempty"` EyeColor *EyeColorEnum `json:"eye_color,omitempty"` HairColor *HairColorEnum `json:"hair_color,omitempty"` Height *int `json:"height,omitempty"` - Measurements *MeasurementsInput `json:"measurements,omitempty"` + CupSize *string `json:"cup_size,omitempty"` + BandSize *int `json:"band_size,omitempty"` + WaistSize *int `json:"waist_size,omitempty"` + HipSize *int `json:"hip_size,omitempty"` BreastType *BreastTypeEnum `json:"breast_type,omitempty"` CareerStartYear *int `json:"career_start_year,omitempty"` CareerEndYear *int `json:"career_end_year,omitempty"` @@ -390,6 +384,7 @@ type PerformerDestroyInput struct { } type PerformerDraft struct { + ID *string `json:"id,omitempty"` Name string `json:"name"` Aliases *string `json:"aliases,omitempty"` Gender *string `json:"gender,omitempty"` @@ -412,6 +407,7 @@ type PerformerDraft struct { func (PerformerDraft) IsDraftData() {} type PerformerDraftInput struct { + ID *string `json:"id,omitempty"` Name string `json:"name"` Aliases *string `json:"aliases,omitempty"` Gender *string `json:"gender,omitempty"` @@ -432,19 +428,18 @@ type PerformerDraftInput struct { } type PerformerEdit struct { - Name *string `json:"name,omitempty"` - Disambiguation *string `json:"disambiguation,omitempty"` - AddedAliases []string `json:"added_aliases,omitempty"` - RemovedAliases []string `json:"removed_aliases,omitempty"` - Gender *GenderEnum `json:"gender,omitempty"` - AddedUrls []*URL `json:"added_urls,omitempty"` - RemovedUrls []*URL `json:"removed_urls,omitempty"` - Birthdate *string `json:"birthdate,omitempty"` - BirthdateAccuracy *string `json:"birthdate_accuracy,omitempty"` - Ethnicity *EthnicityEnum `json:"ethnicity,omitempty"` - Country *string `json:"country,omitempty"` - EyeColor *EyeColorEnum `json:"eye_color,omitempty"` - HairColor *HairColorEnum `json:"hair_color,omitempty"` + Name *string `json:"name,omitempty"` + Disambiguation *string `json:"disambiguation,omitempty"` + AddedAliases []string `json:"added_aliases,omitempty"` + RemovedAliases []string `json:"removed_aliases,omitempty"` + Gender *GenderEnum `json:"gender,omitempty"` + AddedUrls []*URL `json:"added_urls,omitempty"` + RemovedUrls []*URL `json:"removed_urls,omitempty"` + Birthdate *string `json:"birthdate,omitempty"` + Ethnicity *EthnicityEnum `json:"ethnicity,omitempty"` + Country *string `json:"country,omitempty"` + EyeColor *EyeColorEnum `json:"eye_color,omitempty"` + HairColor *HairColorEnum `json:"hair_color,omitempty"` // Height in cm Height *int `json:"height,omitempty"` CupSize *string `json:"cup_size,omitempty"` @@ -461,6 +456,11 @@ type PerformerEdit struct { AddedImages []*Image `json:"added_images,omitempty"` RemovedImages []*Image `json:"removed_images,omitempty"` DraftID *string `json:"draft_id,omitempty"` + Aliases []string `json:"aliases,omitempty"` + Urls []*URL `json:"urls,omitempty"` + Images []*Image `json:"images,omitempty"` + Tattoos []*BodyModification `json:"tattoos,omitempty"` + Piercings []*BodyModification `json:"piercings,omitempty"` } func (PerformerEdit) IsEditDetails() {} @@ -471,13 +471,16 @@ type PerformerEditDetailsInput struct { Aliases []string `json:"aliases,omitempty"` Gender *GenderEnum `json:"gender,omitempty"` Urls []*URLInput `json:"urls,omitempty"` - Birthdate *FuzzyDateInput `json:"birthdate,omitempty"` + Birthdate *string `json:"birthdate,omitempty"` Ethnicity *EthnicityEnum `json:"ethnicity,omitempty"` Country *string `json:"country,omitempty"` EyeColor *EyeColorEnum `json:"eye_color,omitempty"` HairColor *HairColorEnum `json:"hair_color,omitempty"` Height *int `json:"height,omitempty"` - Measurements *MeasurementsInput `json:"measurements,omitempty"` + CupSize *string `json:"cup_size,omitempty"` + BandSize *int `json:"band_size,omitempty"` + WaistSize *int `json:"waist_size,omitempty"` + HipSize *int `json:"hip_size,omitempty"` BreastType *BreastTypeEnum `json:"breast_type,omitempty"` CareerStartYear *int `json:"career_start_year,omitempty"` CareerEndYear *int `json:"career_end_year,omitempty"` @@ -510,7 +513,7 @@ type PerformerEditOptionsInput struct { } type PerformerQueryInput struct { - // Searches name and aliases - assumes like query unless quoted + // Searches name and disambiguation - assumes like query unless quoted Names *string `json:"names,omitempty"` // Searches name only - assumes like query unless quoted Name *string `json:"name,omitempty"` @@ -557,13 +560,16 @@ type PerformerUpdateInput struct { Aliases []string `json:"aliases,omitempty"` Gender *GenderEnum `json:"gender,omitempty"` Urls []*URLInput `json:"urls,omitempty"` - Birthdate *FuzzyDateInput `json:"birthdate,omitempty"` + Birthdate *string `json:"birthdate,omitempty"` Ethnicity *EthnicityEnum `json:"ethnicity,omitempty"` Country *string `json:"country,omitempty"` EyeColor *EyeColorEnum `json:"eye_color,omitempty"` HairColor *HairColorEnum `json:"hair_color,omitempty"` Height *int `json:"height,omitempty"` - Measurements *MeasurementsInput `json:"measurements,omitempty"` + CupSize *string `json:"cup_size,omitempty"` + BandSize *int `json:"band_size,omitempty"` + WaistSize *int `json:"waist_size,omitempty"` + HipSize *int `json:"hip_size,omitempty"` BreastType *BreastTypeEnum `json:"breast_type,omitempty"` CareerStartYear *int `json:"career_start_year,omitempty"` CareerEndYear *int `json:"career_end_year,omitempty"` @@ -631,6 +637,7 @@ type Scene struct { Title *string `json:"title,omitempty"` Details *string `json:"details,omitempty"` Date *string `json:"date,omitempty"` + ReleaseDate *string `json:"release_date,omitempty"` Urls []*URL `json:"urls,omitempty"` Studio *Studio `json:"studio,omitempty"` Tags []*Tag `json:"tags,omitempty"` @@ -652,7 +659,7 @@ type SceneCreateInput struct { Title *string `json:"title,omitempty"` Details *string `json:"details,omitempty"` Urls []*URLInput `json:"urls,omitempty"` - Date *string `json:"date,omitempty"` + Date string `json:"date"` StudioID *string `json:"studio_id,omitempty"` Performers []*PerformerAppearanceInput `json:"performers,omitempty"` TagIds []string `json:"tag_ids,omitempty"` @@ -668,6 +675,7 @@ type SceneDestroyInput struct { } type SceneDraft struct { + ID *string `json:"id,omitempty"` Title *string `json:"title,omitempty"` Details *string `json:"details,omitempty"` URL *URL `json:"url,omitempty"` @@ -701,6 +709,11 @@ type SceneEdit struct { Director *string `json:"director,omitempty"` Code *string `json:"code,omitempty"` DraftID *string `json:"draft_id,omitempty"` + Urls []*URL `json:"urls,omitempty"` + Performers []*PerformerAppearance `json:"performers,omitempty"` + Tags []*Tag `json:"tags,omitempty"` + Images []*Image `json:"images,omitempty"` + Fingerprints []*Fingerprint `json:"fingerprints,omitempty"` } func (SceneEdit) IsEditDetails() {} @@ -855,6 +868,8 @@ type StudioEdit struct { Parent *Studio `json:"parent,omitempty"` AddedImages []*Image `json:"added_images,omitempty"` RemovedImages []*Image `json:"removed_images,omitempty"` + Images []*Image `json:"images,omitempty"` + Urls []*URL `json:"urls,omitempty"` } func (StudioEdit) IsEditDetails() {} @@ -909,8 +924,8 @@ type Tag struct { Updated time.Time `json:"updated"` } -func (Tag) IsSceneDraftTag() {} func (Tag) IsEditTarget() {} +func (Tag) IsSceneDraftTag() {} type TagCategory struct { ID string `json:"id"` @@ -953,6 +968,7 @@ type TagEdit struct { AddedAliases []string `json:"added_aliases,omitempty"` RemovedAliases []string `json:"removed_aliases,omitempty"` Category *TagCategory `json:"category,omitempty"` + Aliases []string `json:"aliases,omitempty"` } func (TagEdit) IsEditDetails() {} @@ -1256,16 +1272,18 @@ type EditSortEnum string const ( EditSortEnumCreatedAt EditSortEnum = "CREATED_AT" EditSortEnumUpdatedAt EditSortEnum = "UPDATED_AT" + EditSortEnumClosedAt EditSortEnum = "CLOSED_AT" ) var AllEditSortEnum = []EditSortEnum{ EditSortEnumCreatedAt, EditSortEnumUpdatedAt, + EditSortEnumClosedAt, } func (e EditSortEnum) IsValid() bool { switch e { - case EditSortEnumCreatedAt, EditSortEnumUpdatedAt: + case EditSortEnumCreatedAt, EditSortEnumUpdatedAt, EditSortEnumClosedAt: return true } return false diff --git a/pkg/scraper/stashbox/stash_box.go b/pkg/scraper/stashbox/stash_box.go index 8b0af9f55..105fe3d24 100644 --- a/pkg/scraper/stashbox/stash_box.go +++ b/pkg/scraper/stashbox/stash_box.go @@ -14,7 +14,6 @@ import ( "strings" "github.com/Yamashou/gqlgenc/client" - "github.com/corona10/goimagehash" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -24,7 +23,6 @@ import ( "github.com/stashapp/stash/pkg/match" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/scraper/stashbox/graphql" - "github.com/stashapp/stash/pkg/sliceutil/intslice" "github.com/stashapp/stash/pkg/sliceutil/stringslice" "github.com/stashapp/stash/pkg/utils" ) @@ -78,127 +76,21 @@ func (c Client) QueryStashBoxScene(ctx context.Context, queryStr string) ([]*mod return ret, nil } -func phashMatches(hash, other int64) bool { - // HACK - stash-box match distance is configurable. This needs to be fixed on - // the stash-box end. - const stashBoxDistance = 4 - - imageHash := goimagehash.NewImageHash(uint64(hash), goimagehash.PHash) - otherHash := goimagehash.NewImageHash(uint64(other), goimagehash.PHash) - - distance, _ := imageHash.Distance(otherHash) - return distance <= stashBoxDistance +// FindStashBoxScenesByFingerprints queries stash-box for a scene using the +// scene's MD5/OSHASH checksum, or PHash. +func (c Client) FindStashBoxSceneByFingerprints(ctx context.Context, sceneID int) ([]*models.ScrapedScene, error) { + res, err := c.FindStashBoxScenesByFingerprints(ctx, []int{sceneID}) + if len(res) > 0 { + return res[0], err + } + return nil, err } // FindStashBoxScenesByFingerprints queries stash-box for scenes using every // scene's MD5/OSHASH checksum, or PHash, and returns results in the same order // as the input slice. -func (c Client) FindStashBoxScenesByFingerprints(ctx context.Context, sceneIDs []string) ([][]*models.ScrapedScene, error) { - ids, err := stringslice.StringSliceToIntSlice(sceneIDs) - if err != nil { - return nil, err - } - - var fingerprints []*graphql.FingerprintQueryInput - // map fingerprints to their scene index - fpToScene := make(map[string][]int) - phashToScene := make(map[int64][]int) - - if err := c.txnManager.WithReadTxn(ctx, func(r models.ReaderRepository) error { - qb := r.Scene() - - for index, sceneID := range ids { - scene, err := qb.Find(sceneID) - if err != nil { - return err - } - - if scene == nil { - return fmt.Errorf("scene with id %d not found", sceneID) - } - - if scene.Checksum.Valid { - fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{ - Hash: scene.Checksum.String, - Algorithm: graphql.FingerprintAlgorithmMd5, - }) - fpToScene[scene.Checksum.String] = append(fpToScene[scene.Checksum.String], index) - } - - if scene.OSHash.Valid { - fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{ - Hash: scene.OSHash.String, - Algorithm: graphql.FingerprintAlgorithmOshash, - }) - fpToScene[scene.OSHash.String] = append(fpToScene[scene.OSHash.String], index) - } - - if scene.Phash.Valid { - phashStr := utils.PhashToString(scene.Phash.Int64) - fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{ - Hash: phashStr, - Algorithm: graphql.FingerprintAlgorithmPhash, - }) - fpToScene[phashStr] = append(fpToScene[phashStr], index) - phashToScene[scene.Phash.Int64] = append(phashToScene[scene.Phash.Int64], index) - } - } - - return nil - }); err != nil { - return nil, err - } - - allScenes, err := c.findStashBoxScenesByFingerprints(ctx, fingerprints) - if err != nil { - return nil, err - } - - // set the matched scenes back in their original order - ret := make([][]*models.ScrapedScene, len(sceneIDs)) - for _, s := range allScenes { - var addedTo []int - - addScene := func(sceneIndexes []int) { - for _, index := range sceneIndexes { - if !intslice.IntInclude(addedTo, index) { - addedTo = append(addedTo, index) - ret[index] = append(ret[index], s) - } - } - } - - for _, fp := range s.Fingerprints { - addScene(fpToScene[fp.Hash]) - - // HACK - we really need stash-box to return specific hash-to-result sets - if fp.Algorithm == graphql.FingerprintAlgorithmPhash.String() { - hash, err := utils.StringToPhash(fp.Hash) - if err != nil { - continue - } - - for phash, sceneIndexes := range phashToScene { - if phashMatches(hash, phash) { - addScene(sceneIndexes) - } - } - } - } - } - - return ret, nil -} - -// FindStashBoxScenesByFingerprintsFlat queries stash-box for scenes using every -// scene's MD5/OSHASH checksum, or PHash, and returns results a flat slice. -func (c Client) FindStashBoxScenesByFingerprintsFlat(ctx context.Context, sceneIDs []string) ([]*models.ScrapedScene, error) { - ids, err := stringslice.StringSliceToIntSlice(sceneIDs) - if err != nil { - return nil, err - } - - var fingerprints []*graphql.FingerprintQueryInput +func (c Client) FindStashBoxScenesByFingerprints(ctx context.Context, ids []int) ([][]*models.ScrapedScene, error) { + var fingerprints [][]*graphql.FingerprintQueryInput if err := c.txnManager.WithReadTxn(ctx, func(r models.ReaderRepository) error { qb := r.Scene() @@ -213,26 +105,31 @@ func (c Client) FindStashBoxScenesByFingerprintsFlat(ctx context.Context, sceneI return fmt.Errorf("scene with id %d not found", sceneID) } + var sceneFPs []*graphql.FingerprintQueryInput + if scene.Checksum.Valid { - fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{ + sceneFPs = append(sceneFPs, &graphql.FingerprintQueryInput{ Hash: scene.Checksum.String, Algorithm: graphql.FingerprintAlgorithmMd5, }) } if scene.OSHash.Valid { - fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{ + sceneFPs = append(sceneFPs, &graphql.FingerprintQueryInput{ Hash: scene.OSHash.String, Algorithm: graphql.FingerprintAlgorithmOshash, }) } if scene.Phash.Valid { - fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{ - Hash: utils.PhashToString(scene.Phash.Int64), + phashStr := utils.PhashToString(scene.Phash.Int64) + sceneFPs = append(sceneFPs, &graphql.FingerprintQueryInput{ + Hash: phashStr, Algorithm: graphql.FingerprintAlgorithmPhash, }) } + + fingerprints = append(fingerprints, sceneFPs) } return nil @@ -243,27 +140,29 @@ func (c Client) FindStashBoxScenesByFingerprintsFlat(ctx context.Context, sceneI return c.findStashBoxScenesByFingerprints(ctx, fingerprints) } -func (c Client) findStashBoxScenesByFingerprints(ctx context.Context, fingerprints []*graphql.FingerprintQueryInput) ([]*models.ScrapedScene, error) { - var ret []*models.ScrapedScene - for i := 0; i < len(fingerprints); i += 100 { +func (c Client) findStashBoxScenesByFingerprints(ctx context.Context, scenes [][]*graphql.FingerprintQueryInput) ([][]*models.ScrapedScene, error) { + var ret [][]*models.ScrapedScene + for i := 0; i < len(scenes); i += 40 { end := i + 100 - if end > len(fingerprints) { - end = len(fingerprints) + if end > len(scenes) { + end = len(scenes) } - scenes, err := c.client.FindScenesByFullFingerprints(ctx, fingerprints[i:end]) + scenes, err := c.client.FindScenesBySceneFingerprints(ctx, scenes[i:end]) if err != nil { return nil, err } - sceneFragments := scenes.FindScenesByFullFingerprints - - for _, s := range sceneFragments { - ss, err := c.sceneFragmentToScrapedScene(ctx, s) - if err != nil { - return nil, err + for _, sceneFragments := range scenes.FindScenesBySceneFingerprints { + var sceneResults []*models.ScrapedScene + for _, scene := range sceneFragments { + ss, err := c.sceneFragmentToScrapedScene(ctx, scene) + if err != nil { + return nil, err + } + sceneResults = append(sceneResults, ss) } - ret = append(ret, ss) + ret = append(ret, sceneResults) } } From d68d022893aee62c5a3f4c81fefe9098f5309fc4 Mon Sep 17 00:00:00 2001 From: InfiniteTF Date: Wed, 1 Jun 2022 06:53:31 +0200 Subject: [PATCH 07/34] Add support for submitting stash-box scene updates by draft (#2577) --- .../stashbox/graphql/generated_client.go | 512 +++++++++--------- .../stashbox/graphql/generated_models.go | 6 +- pkg/scraper/stashbox/graphql/override.go | 1 + pkg/scraper/stashbox/stash_box.go | 26 + .../src/components/Changelog/Changelog.tsx | 10 +- .../components/Changelog/versions/v0160.md | 5 + .../src/components/Dialogs/SubmitDraft.tsx | 35 +- ui/v2.5/src/locales/en-GB.json | 4 +- 8 files changed, 332 insertions(+), 267 deletions(-) create mode 100644 ui/v2.5/src/components/Changelog/versions/v0160.md diff --git a/pkg/scraper/stashbox/graphql/generated_client.go b/pkg/scraper/stashbox/graphql/generated_client.go index 248f3e711..a41380740 100644 --- a/pkg/scraper/stashbox/graphql/generated_client.go +++ b/pkg/scraper/stashbox/graphql/generated_client.go @@ -239,6 +239,52 @@ fragment URLFragment on URL { url type } +fragment ImageFragment on Image { + id + url + width + height +} +fragment TagFragment on Tag { + name + id +} +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration +} +fragment SceneFragment on Scene { + id + title + details + duration + date + urls { + ... URLFragment + } + images { + ... ImageFragment + } + studio { + ... StudioFragment + } + tags { + ... TagFragment + } + performers { + ... PerformerAppearanceFragment + } + fingerprints { + ... FingerprintFragment + } +} fragment StudioFragment on Studio { name id @@ -249,9 +295,11 @@ fragment StudioFragment on Studio { ... ImageFragment } } -fragment TagFragment on Tag { - name - id +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } } fragment PerformerFragment on Performer { id @@ -287,61 +335,13 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } -fragment BodyModificationFragment on BodyModification { - location - description -} -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration -} -fragment SceneFragment on Scene { - id - title - details - duration - date - urls { - ... URLFragment - } - images { - ... ImageFragment - } - studio { - ... StudioFragment - } - tags { - ... TagFragment - } - performers { - ... PerformerAppearanceFragment - } - fingerprints { - ... FingerprintFragment - } -} -fragment ImageFragment on Image { - id - url - width - height -} -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} fragment FuzzyDateFragment on FuzzyDate { date accuracy } -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip +fragment BodyModificationFragment on BodyModification { + location + description } ` @@ -363,6 +363,35 @@ const FindScenesByFullFingerprintsDocument = `query FindScenesByFullFingerprints ... SceneFragment } } +fragment SceneFragment on Scene { + id + title + details + duration + date + urls { + ... URLFragment + } + images { + ... ImageFragment + } + studio { + ... StudioFragment + } + tags { + ... TagFragment + } + performers { + ... PerformerAppearanceFragment + } + fingerprints { + ... FingerprintFragment + } +} +fragment URLFragment on URL { + url + type +} fragment StudioFragment on Studio { name id @@ -373,6 +402,27 @@ fragment StudioFragment on Studio { ... ImageFragment } } +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } +} +fragment BodyModificationFragment on BodyModification { + location + description +} +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration +} +fragment ImageFragment on Image { + id + url + width + height +} fragment TagFragment on Tag { name id @@ -415,62 +465,12 @@ fragment FuzzyDateFragment on FuzzyDate { date accuracy } -fragment BodyModificationFragment on BodyModification { - location - description -} -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration -} -fragment SceneFragment on Scene { - id - title - details - duration - date - urls { - ... URLFragment - } - images { - ... ImageFragment - } - studio { - ... StudioFragment - } - tags { - ... TagFragment - } - performers { - ... PerformerAppearanceFragment - } - fingerprints { - ... FingerprintFragment - } -} -fragment ImageFragment on Image { - id - url - width - height -} fragment MeasurementsFragment on Measurements { band_size cup_size waist hip } -fragment URLFragment on URL { - url - type -} -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} ` func (c *Client) FindScenesByFullFingerprints(ctx context.Context, fingerprints []*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesByFullFingerprints, error) { @@ -491,6 +491,26 @@ const FindScenesBySceneFingerprintsDocument = `query FindScenesBySceneFingerprin ... SceneFragment } } +fragment URLFragment on URL { + url + type +} +fragment ImageFragment on Image { + id + url + width + height +} +fragment StudioFragment on Studio { + name + id + urls { + ... URLFragment + } + images { + ... ImageFragment + } +} fragment PerformerFragment on Performer { id name @@ -531,49 +551,11 @@ fragment MeasurementsFragment on Measurements { waist hip } -fragment BodyModificationFragment on BodyModification { - location - description -} fragment FingerprintFragment on Fingerprint { algorithm hash duration } -fragment ImageFragment on Image { - id - url - width - height -} -fragment URLFragment on URL { - url - type -} -fragment StudioFragment on Studio { - name - id - urls { - ... URLFragment - } - images { - ... ImageFragment - } -} -fragment TagFragment on Tag { - name - id -} -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} fragment SceneFragment on Scene { id title @@ -599,6 +581,24 @@ fragment SceneFragment on Scene { ... FingerprintFragment } } +fragment TagFragment on Tag { + name + id +} +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } +} +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} +fragment BodyModificationFragment on BodyModification { + location + description +} ` func (c *Client) FindScenesBySceneFingerprints(ctx context.Context, fingerprints [][]*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesBySceneFingerprints, error) { @@ -619,10 +619,41 @@ const SearchSceneDocument = `query SearchScene ($term: String!) { ... SceneFragment } } +fragment SceneFragment on Scene { + id + title + details + duration + date + urls { + ... URLFragment + } + images { + ... ImageFragment + } + studio { + ... StudioFragment + } + tags { + ... TagFragment + } + performers { + ... PerformerAppearanceFragment + } + fingerprints { + ... FingerprintFragment + } +} fragment URLFragment on URL { url type } +fragment ImageFragment on Image { + id + url + width + height +} fragment TagFragment on Tag { name id @@ -661,30 +692,14 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } -fragment SceneFragment on Scene { - id - title - details - duration +fragment FuzzyDateFragment on FuzzyDate { date - urls { - ... URLFragment - } - images { - ... ImageFragment - } - studio { - ... StudioFragment - } - tags { - ... TagFragment - } - performers { - ... PerformerAppearanceFragment - } - fingerprints { - ... FingerprintFragment - } + accuracy +} +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration } fragment StudioFragment on Studio { name @@ -702,10 +717,6 @@ fragment PerformerAppearanceFragment on PerformerAppearance { ... PerformerFragment } } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} fragment MeasurementsFragment on Measurements { band_size cup_size @@ -716,17 +727,6 @@ fragment BodyModificationFragment on BodyModification { location description } -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration -} -fragment ImageFragment on Image { - id - url - width - height -} ` func (c *Client) SearchScene(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchScene, error) { @@ -747,20 +747,6 @@ const SearchPerformerDocument = `query SearchPerformer ($term: String!) { ... PerformerFragment } } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} -fragment BodyModificationFragment on BodyModification { - location - description -} fragment PerformerFragment on Performer { id name @@ -805,6 +791,20 @@ fragment ImageFragment on Image { width height } +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} +fragment BodyModificationFragment on BodyModification { + location + description +} ` func (c *Client) SearchPerformer(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchPerformer, error) { @@ -825,20 +825,6 @@ const FindPerformerByIDDocument = `query FindPerformerByID ($id: ID!) { ... PerformerFragment } } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} -fragment BodyModificationFragment on BodyModification { - location - description -} fragment PerformerFragment on Performer { id name @@ -883,6 +869,20 @@ fragment ImageFragment on Image { width height } +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} +fragment BodyModificationFragment on BodyModification { + location + description +} ` func (c *Client) FindPerformerByID(ctx context.Context, id string, httpRequestOptions ...client.HTTPRequestOption) (*FindPerformerByID, error) { @@ -903,6 +903,14 @@ const FindSceneByIDDocument = `query FindSceneByID ($id: ID!) { ... SceneFragment } } +fragment BodyModificationFragment on BodyModification { + location + description +} +fragment URLFragment on URL { + url + type +} fragment ImageFragment on Image { id url @@ -913,11 +921,43 @@ fragment TagFragment on Tag { name id } -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment +fragment PerformerFragment on Performer { + id + name + disambiguation + aliases + gender + merged_ids + urls { + ... URLFragment } + images { + ... ImageFragment + } + birthdate { + ... FuzzyDateFragment + } + ethnicity + country + eye_color + hair_color + height + measurements { + ... MeasurementsFragment + } + breast_type + career_start_year + career_end_year + tattoos { + ... BodyModificationFragment + } + piercings { + ... BodyModificationFragment + } +} +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy } fragment MeasurementsFragment on Measurements { band_size @@ -925,10 +965,6 @@ fragment MeasurementsFragment on Measurements { waist hip } -fragment BodyModificationFragment on BodyModification { - location - description -} fragment SceneFragment on Scene { id title @@ -954,10 +990,6 @@ fragment SceneFragment on Scene { ... FingerprintFragment } } -fragment URLFragment on URL { - url - type -} fragment StudioFragment on Studio { name id @@ -968,43 +1000,11 @@ fragment StudioFragment on Studio { ... ImageFragment } } -fragment PerformerFragment on Performer { - id - name - disambiguation - aliases - gender - merged_ids - urls { - ... URLFragment +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment } - images { - ... ImageFragment - } - birthdate { - ... FuzzyDateFragment - } - ethnicity - country - eye_color - hair_color - height - measurements { - ... MeasurementsFragment - } - breast_type - career_start_year - career_end_year - tattoos { - ... BodyModificationFragment - } - piercings { - ... BodyModificationFragment - } -} -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy } fragment FingerprintFragment on Fingerprint { algorithm diff --git a/pkg/scraper/stashbox/graphql/generated_models.go b/pkg/scraper/stashbox/graphql/generated_models.go index cfa893c23..341f91d14 100644 --- a/pkg/scraper/stashbox/graphql/generated_models.go +++ b/pkg/scraper/stashbox/graphql/generated_models.go @@ -88,8 +88,8 @@ type DraftEntity struct { ID *string `json:"id,omitempty"` } -func (DraftEntity) IsSceneDraftStudio() {} func (DraftEntity) IsSceneDraftPerformer() {} +func (DraftEntity) IsSceneDraftStudio() {} func (DraftEntity) IsSceneDraftTag() {} type DraftEntityInput struct { @@ -339,8 +339,8 @@ type Performer struct { Updated time.Time `json:"updated"` } -func (Performer) IsEditTarget() {} func (Performer) IsSceneDraftPerformer() {} +func (Performer) IsEditTarget() {} type PerformerAppearance struct { Performer *Performer `json:"performer,omitempty"` @@ -846,8 +846,8 @@ type Studio struct { Updated time.Time `json:"updated"` } -func (Studio) IsSceneDraftStudio() {} func (Studio) IsEditTarget() {} +func (Studio) IsSceneDraftStudio() {} type StudioCreateInput struct { Name string `json:"name"` diff --git a/pkg/scraper/stashbox/graphql/override.go b/pkg/scraper/stashbox/graphql/override.go index 492a55a06..d80b74307 100644 --- a/pkg/scraper/stashbox/graphql/override.go +++ b/pkg/scraper/stashbox/graphql/override.go @@ -5,6 +5,7 @@ import "github.com/99designs/gqlgen/graphql" // Override for generated struct due to mistaken omitempty // https://github.com/Yamashou/gqlgenc/issues/77 type SceneDraftInput struct { + ID *string `json:"id,omitempty"` Title *string `json:"title,omitempty"` Details *string `json:"details,omitempty"` URL *string `json:"url,omitempty"` diff --git a/pkg/scraper/stashbox/stash_box.go b/pkg/scraper/stashbox/stash_box.go index 105fe3d24..232bfdba5 100644 --- a/pkg/scraper/stashbox/stash_box.go +++ b/pkg/scraper/stashbox/stash_box.go @@ -825,6 +825,19 @@ func (c Client) SubmitSceneDraft(ctx context.Context, sceneID int, endpoint stri } } + stashIDs, err := qb.GetStashIDs(sceneID) + if err != nil { + return err + } + var stashID *string + for _, v := range stashIDs { + if v.Endpoint == endpoint { + stashID = &v.StashID + break + } + } + draft.ID = stashID + return nil }); err != nil { return nil, err @@ -910,6 +923,19 @@ func (c Client) SubmitPerformerDraft(ctx context.Context, performer *models.Perf draft.Urls = urls } + stashIDs, err := pqb.GetStashIDs(performer.ID) + if err != nil { + return err + } + var stashID *string + for _, v := range stashIDs { + if v.Endpoint == endpoint { + stashID = &v.StashID + break + } + } + draft.ID = stashID + return nil }); err != nil { return nil, err diff --git a/ui/v2.5/src/components/Changelog/Changelog.tsx b/ui/v2.5/src/components/Changelog/Changelog.tsx index 030d581dc..2a970fdb1 100644 --- a/ui/v2.5/src/components/Changelog/Changelog.tsx +++ b/ui/v2.5/src/components/Changelog/Changelog.tsx @@ -19,6 +19,7 @@ import V0130 from "./versions/v0130.md"; import V0131 from "./versions/v0131.md"; import V0140 from "./versions/v0140.md"; import V0150 from "./versions/v0150.md"; +import V0160 from "./versions/v0160.md"; import { MarkdownPage } from "../Shared/MarkdownPage"; // to avoid use of explicit any @@ -57,9 +58,9 @@ const Changelog: React.FC = () => { // after new release: // add entry to releases, using the current* fields // then update the current fields. - const currentVersion = stashVersion || "v0.15.0"; + const currentVersion = stashVersion || "v0.16.0"; const currentDate = buildDate; - const currentPage = V0150; + const currentPage = V0160; const releases: IStashRelease[] = [ { @@ -68,6 +69,11 @@ const Changelog: React.FC = () => { page: currentPage, defaultOpen: true, }, + { + version: "v0.15.0", + date: "2022-05-18", + page: V0150, + }, { version: "v0.14.0", date: "2022-04-11", diff --git a/ui/v2.5/src/components/Changelog/versions/v0160.md b/ui/v2.5/src/components/Changelog/versions/v0160.md new file mode 100644 index 000000000..61a4dd282 --- /dev/null +++ b/ui/v2.5/src/components/Changelog/versions/v0160.md @@ -0,0 +1,5 @@ +### ✨ New Features +* Support submitting stash-box scene updates for scenes with stash ids. ([#2577](https://github.com/stashapp/stash/pull/2577)) + +### 🐛 Bug fixes +* Fix moved gallery zip files not being rescanned. ([#2611](https://github.com/stashapp/stash/pull/2611)) \ No newline at end of file diff --git a/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx b/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx index c3eceae8b..6fb6d81fa 100644 --- a/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx +++ b/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx @@ -8,7 +8,12 @@ import { FormattedMessage, useIntl } from "react-intl"; interface IProps { show: boolean; - entity: { name?: string | null; id: string; title?: string | null }; + entity: { + name?: string | null; + id: string; + title?: string | null; + stash_ids: { stash_id: string; endpoint: string }[]; + }; boxes: Pick[]; query: DocumentNode; onHide: () => void; @@ -59,6 +64,12 @@ export const SubmitStashBoxDraft: React.FC = ({ const handleSelectBox = (e: React.ChangeEvent) => setSelectedBox(Number.parseInt(e.currentTarget.value) ?? 0); + // If the scene has an attached stash_id from that endpoint, the operation will be an update + const isUpdate = + entity.stash_ids.find( + (id) => id.endpoint === boxes[selectedBox].endpoint + ) !== undefined; + return ( = ({ ))} - +
+ {isUpdate && ( + + + + )} + +
) : ( <> diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 31ddca247..daba6cde8 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -106,6 +106,7 @@ "view_random": "View Random", "continue": "Continue", "submit": "Submit", + "submit_update": "Submit update", "logout": "Log out", "remove_from_gallery": "Remove from Gallery", "delete_stashid": "Delete StashID", @@ -976,7 +977,8 @@ "selected_stash_box": "Selected Stash-Box endpoint", "submission_successful": "Submission successful", "submission_failed": "Submission failed", - "go_review_draft": "Go to {endpoint_name} to review draft." + "go_review_draft": "Go to {endpoint_name} to review draft.", + "submit_update": "Already exists in {endpoint_name}" }, "performer_tagger": { "network_error": "Network Error", From 8a1c34997626839bf5e350ba53119fe665d78017 Mon Sep 17 00:00:00 2001 From: Emilo2 <99644577+Emilo2@users.noreply.github.com> Date: Fri, 3 Jun 2022 02:37:24 +0300 Subject: [PATCH 08/34] Fix scraping more than 40 scenes from stash-box (#2638) --- pkg/scraper/stashbox/stash_box.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/scraper/stashbox/stash_box.go b/pkg/scraper/stashbox/stash_box.go index 232bfdba5..8470505d8 100644 --- a/pkg/scraper/stashbox/stash_box.go +++ b/pkg/scraper/stashbox/stash_box.go @@ -143,7 +143,7 @@ func (c Client) FindStashBoxScenesByFingerprints(ctx context.Context, ids []int) func (c Client) findStashBoxScenesByFingerprints(ctx context.Context, scenes [][]*graphql.FingerprintQueryInput) ([][]*models.ScrapedScene, error) { var ret [][]*models.ScrapedScene for i := 0; i < len(scenes); i += 40 { - end := i + 100 + end := i + 40 if end > len(scenes) { end = len(scenes) } From 803d865348a41fea4890ac47a95c317ec8a7a669 Mon Sep 17 00:00:00 2001 From: stash-translation-bot <94573628+stash-translation-bot@users.noreply.github.com> Date: Sun, 5 Jun 2022 17:06:17 -0700 Subject: [PATCH 09/34] Translations update from Stash (#2627) * Translated using Weblate (French) Currently translated at 88.8% (725 of 816 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/ * Translated using Weblate (German) Currently translated at 97.6% (797 of 816 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/de/ * Translated using Weblate (French) Currently translated at 89.4% (730 of 816 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/ * Translated using Weblate (French) Currently translated at 89.4% (730 of 816 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (816 of 816 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/zh_Hant/ * Added translation using Weblate (Hungarian) * Translated using Weblate (German) Currently translated at 98.1% (803 of 818 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/de/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 98.7% (808 of 818 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/pt_BR/ * Translated using Weblate (Polish) Currently translated at 100.0% (818 of 818 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/pl/ * Translated using Weblate (Hungarian) Currently translated at 48.5% (397 of 818 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/hu/ * Translated using Weblate (Italian) Currently translated at 100.0% (818 of 818 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/it/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 99.7% (816 of 818 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/pt_BR/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (818 of 818 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/zh_Hant/ * Translated using Weblate (Finnish) Currently translated at 93.7% (767 of 818 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fi/ * Translated using Weblate (French) Currently translated at 91.5% (749 of 818 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/ * Translated using Weblate (French) Currently translated at 91.5% (749 of 818 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/ Co-authored-by: - Co-authored-by: Weblate Co-authored-by: Phasetime Co-authored-by: Still Co-authored-by: Foltin Co-authored-by: ponei Co-authored-by: Coscosname Co-authored-by: BViking78 Co-authored-by: Aa --- ui/v2.5/src/locales/de-DE.json | 104 ++++++- ui/v2.5/src/locales/fi-FI.json | 10 +- ui/v2.5/src/locales/fr-FR.json | 462 ++++++++++++++-------------- ui/v2.5/src/locales/hu-HU.json | 539 +++++++++++++++++++++++++++++++++ ui/v2.5/src/locales/it-IT.json | 4 +- ui/v2.5/src/locales/pl-PL.json | 4 +- ui/v2.5/src/locales/pt-BR.json | 178 +++++++---- ui/v2.5/src/locales/zh-TW.json | 36 ++- 8 files changed, 1042 insertions(+), 295 deletions(-) create mode 100644 ui/v2.5/src/locales/hu-HU.json diff --git a/ui/v2.5/src/locales/de-DE.json b/ui/v2.5/src/locales/de-DE.json index 5f50c3bfe..af9ee40d5 100644 --- a/ui/v2.5/src/locales/de-DE.json +++ b/ui/v2.5/src/locales/de-DE.json @@ -7,7 +7,7 @@ "allow": "Erlauben", "allow_temporarily": "Vorübergehend erlauben", "apply": "Übernehmen", - "auto_tag": "Auto Tag", + "auto_tag": "Auto-Tag", "backup": "Backup", "browse_for_image": "Nach Bild suchen…", "cancel": "Abbrechen", @@ -28,10 +28,12 @@ "delete_file": "Lösche Datei", "delete_file_and_funscript": "Datei löschen (inkl. funscript)", "delete_generated_supporting_files": "Lösche generierte Hilfsdaten", + "delete_stashid": "StashID löschen", "disallow": "Nicht erlauben", "download": "Herunterladen", "download_backup": "Lade Backup herunter", "edit": "Bearbeiten", + "edit_entity": "Bearbeiten {entityType}", "export": "Exportieren…", "export_all": "Alle exportieren…", "find": "Suchen", @@ -45,15 +47,18 @@ "generate_thumb_from_current": "Erstelle Vorschaubild vom Gegenwärtigen", "hash_migration": "Hash Umwandlung", "hide": "Verstecke", + "hide_configuration": "Konfiguration ausblenden", "identify": "Identifizieren", "ignore": "Ignorieren", "import": "Importieren…", "import_from_file": "Importieren aus Datei", + "logout": "Ausloggen", "merge": "Zusammenführen", "merge_from": "Zusammenführen aus", "merge_into": "Zusammenführen in", "next_action": "Nächste", "not_running": "wird nicht ausgeführt", + "open_in_external_player": "In externem Player öffnen", "open_random": "Öffne Zufällig", "overwrite": "Überschreiben", "play_random": "Zufällige Wiedergabe", @@ -64,6 +69,7 @@ "reload_plugins": "Plugins neu laden", "reload_scrapers": "Scraper neu laden", "remove": "Entfernen", + "remove_from_gallery": "Aus Gallerie entfernen", "rename_gen_files": "Hilfsdaten umbenennen", "rescan": "Erneut scannen", "reshuffle": "Neu mischen", @@ -78,6 +84,7 @@ "scrape_with": "Scrape mit…", "search": "Suchen", "select_all": "Alle auswählen", + "select_entity": "{entityType} auswählen", "select_folders": "Ordner auswählen", "select_none": "Nichts auswählen", "selective_auto_tag": "Automatisch selektiv taggen", @@ -88,9 +95,12 @@ "set_front_image": "Vorderseite…", "set_image": "Bild festlegen…", "show": "Anzeigen", + "show_configuration": "Konfiguration anzeigen", "skip": "Überspringen", "stop": "Stopp", + "submit": "Einreichen", "submit_stash_box": "Zu Stash-Box übermitteln", + "submit_update": "Aktualisierung übermitteln", "tasks": { "clean_confirm_message": "Wollen Sie wirklich die Datenbank aufräumen? Dies wird alle Informationen und Hilfsdaten für Szenen und Galerien löschen, die nicht mehr auf dem Dateisystem vorhanden sind.", "dry_mode_selected": "Trockenmodus ausgewählt. Es findet keine Löschung der Daten statt, lediglich Protokollierung.", @@ -98,6 +108,7 @@ }, "temp_disable": "Vorübergehend deaktivieren…", "temp_enable": "Vorübergehend aktivieren…", + "unset": "Aufheben", "use_default": "Standard verwenden", "view_random": "Zeige Zufällige" }, @@ -111,6 +122,7 @@ "birth_year": "Geburtsjahr", "birthdate": "Geburtsdatum", "bitrate": "Bitrate", + "captions": "Untertitel", "career_length": "Karrierelänge", "component_tagger": { "config": { @@ -190,14 +202,19 @@ "dlna": { "allow_temp_ip": "Erlaube {tempIP}", "allowed_ip_addresses": "Erlaubte IP Adressen", + "allowed_ip_temporarily": "Temporär erlaubte IP", "default_ip_whitelist": "Standard IP Whitelist", "default_ip_whitelist_desc": "Standard IP Adressen, welche DLNA nutzen dürfen. Nutze {wildcard} um alle IP Adressen zu erlauben.", + "disabled_dlna_temporarily": "DLNA vorübergehend deaktiviert", + "disallowed_ip": "Unzulässige IP", "enabled_by_default": "Standardmäßig aktiviert", + "enabled_dlna_temporarily": "DLNA vorübergehend aktiviert", "network_interfaces": "Netzwerkoberflächen", "network_interfaces_desc": "Netzwerkoberflächen auf denen DLNA sichtbar ist. Eine leere Liste führt dazu, dass DLNA auf allen Oberflächen ausgeführt wird. Benötigt Neustart des DLNA nach Änderungen.", "recent_ip_addresses": "Letzte IP Adressen", "server_display_name": "Server Anzeigename", "server_display_name_desc": "Anzeigename des DLNA-Servers. Standardmäßig {server_name} bei leerem Feld.", + "successfully_cancelled_temporary_behaviour": "Erfolgreich temporäres Verhalten aufgehoben", "until_restart": "bis Neustart" }, "general": { @@ -265,6 +282,10 @@ "number_of_parallel_task_for_scan_generation_head": "Anzahl paralleler Tasks für Scan/Generierung", "parallel_scan_head": "Paralleler Scan/Generierung", "preview_generation": "Vorschau-Generierung", + "python_path": { + "description": "Ort der Python-Programmdatei. Wird für Script-Scraper und Plugins verwendet. Wenn leer, wird python aus der Umgebung aufgelöst", + "heading": "Python Pfad" + }, "scraper_user_agent": "Scraper-Benutzeragent", "scraper_user_agent_desc": "User-Agent-String, der während Scrape-HTTP-Anfragen verwendet wird", "scrapers_path": { @@ -430,10 +451,23 @@ "description": "Zeitversatz in Millisekunden für interaktive Skriptwiedergabe.", "heading": "Funscript Zeitversatz (ms)" }, + "handy_connection": { + "connect": "Verbinden", + "server_offset": { + "heading": "Server Kompensation" + }, + "status": { + "heading": "Handy Verbindungsstatus" + }, + "sync": "Synchronisieren" + }, "handy_connection_key": { "description": "Handy Verbindungsschlüssel für interaktive Szenen. Wenn dieser Schlüssel gesetzt wird, kann Stash aktuellen Szeneinformationen mit handyfeeling.com teilen", "heading": "Handy Verbindungsschlüssel" }, + "image_lightbox": { + "heading": "Bild-Lightbox" + }, "images": { "heading": "Bilder", "options": { @@ -489,7 +523,8 @@ "continue_playlist_default": { "description": "Nächste Szene in der Warteschlange spielen", "heading": "Standardmäßig die Wiedergabeliste fortsetzen" - } + }, + "show_scrubber": "Scrubber anzeigen" } }, "scene_wall": { @@ -499,9 +534,13 @@ "toggle_sound": "Sound einschalten" } }, + "scroll_attempts_before_change": { + "description": "Anzahl der Versuche, einen Bildlauf durchzuführen, bevor zum nächsten/vorherigen Element gewechselt wird. Gilt nur für den Bildlaufmodus Schwenkung Y.", + "heading": "Anzahl Scroll-Versuche vor Übergang" + }, "slideshow_delay": { "description": "Die Diashow ist in Galerien in der Wandansicht verfügbar", - "heading": "Verzögerung der Diashow" + "heading": "Verzögerung der Diashow (Sekunden)" }, "title": "Benutzeroberfläche" } @@ -602,6 +641,8 @@ "marker_screenshots_tooltip": "Statische JPG-Bilder für Markierungen, nur erforderlich, wenn der Vorschautyp auf Statisches Bild eingestellt ist.", "markers": "Vorschau für Markierungen", "markers_tooltip": "20-Sekunden-Videos, die zum angegebenen Zeitpunkt beginnen.", + "override_preview_generation_options": "Überschreibe Optionen zur Erstellung von Vorschauen", + "override_preview_generation_options_desc": "Überschreibe die Optionen zur Erstellung von Vorschauen für diesen Vorgang. Die Standardeinstellungen werden unter System -> \tVorschau-Generierung festgelegt.", "overwrite": "Vorhandene generierte Dateien überschreiben", "phash": "Perzeptuelle Hashes (zur Deduplizierung)", "preview_exclude_end_time_desc": "Schließen Sie die letzten x Sekunden von der Szenenvorschau aus. Dies kann ein Wert in Sekunden oder ein Prozentsatz (zB 2%) der gesamten Szenendauer sein.", @@ -653,6 +694,7 @@ "search_accuracy_label": "Suchgenauigkeit", "title": "Szenen-Duplikate" }, + "duplicated_phash": "Dopplung (phash)", "duration": "Dauer", "effect_filters": { "aspect": "Seitenverhältnis", @@ -675,7 +717,9 @@ "scale": "Skalieren", "warmth": "Wärme" }, + "empty_server": "Fügen Sie Ihrem Server einige Szenen hinzu, um Empfehlungen auf dieser Seite anzuzeigen.", "ethnicity": "Ethnizität", + "existing_value": "vorhandener Wert", "eye_color": "Augenfarbe", "fake_tits": "Brustvergrößerungen", "false": "Falsch", @@ -703,9 +747,19 @@ "TRANSGENDER_MALE": "Trans* männlich" }, "hair_color": "Haarfarbe", + "handy_connection_status": { + "connecting": "Verbindet", + "disconnected": "Getrennt", + "error": "Fehler bei der Verbindung zu Handy", + "missing": "Fehlt", + "ready": "Bereit", + "syncing": "Synchronisiert mit Server", + "uploading": "Skript wird hochgeladen" + }, "hasMarkers": "Hat Markierungen", "height": "Größe", "help": "Hilfe", + "ignore_auto_tag": "Auto-Tag ignorieren", "image": "Bild", "image_count": "Bilderanzahl", "images": "Bilder", @@ -760,15 +814,41 @@ "parent_tags": "Übergeordnete Tags", "part_of": "Übergeordnet von {parent}", "path": "Pfad", + "perceptual_similarity": "Wahrnehmungsähnlichkeit (phash)", "performer": "Darsteller", "performerTags": "Darsteller-Tags", + "performer_age": "Alter der Darsteller", "performer_count": "Darstelleranzahl", + "performer_favorite": "Darsteller favorisiert", "performer_image": "Darsteller-Bild", + "performer_tagger": { + "add_new_performers": "Neue Darsteller hinzufügen", + "current_page": "Aktuelle Seite", + "failed_to_save_performer": "Fehler beim Speichern der Darsteller \"{performer}\"", + "name_already_exists": "Name bereits vergeben", + "network_error": "Netzwerkfehler", + "no_results_found": "Keine Ergebnisse gefunden.", + "performer_already_tagged": "Darsteller bereits getagged", + "performer_successfully_tagged": "Darsteller erfolgreich getagged:", + "query_all_performers_in_the_database": "Alle Darsteller in der Datenbank", + "refresh_tagged_performers": "Aktualisieren getaggter Darsteller", + "refreshing_will_update_the_data": "Bei der Aktualisierung werden die Metadaten aller getaggten Darsteller über die stash-box-Instanz aktualisiert.", + "tag_status": "Tag Status", + "untagged_performers": "Nicht getaggte Darsteller", + "update_performer": "Darsteller aktualisieren", + "update_performers": "Darsteller aktualisieren", + "updating_untagged_performers_description": "Bei der Aktualisierung von nicht getaggten Darstellern wird versucht die Metadaten alle Darsteller, welche keine StashID haben, zu aktualisieren." + }, "performers": "Darsteller", "piercings": "Piercings", "queue": "Playlist", "random": "Zufällig", "rating": "Wertung", + "recently_added_performers": "Kürzlich hinzugefügte Darsteller", + "recently_added_studios": "Kürzlich hinzugefügte Studios", + "recently_released_galleries": "Kürzlich hinzugefügte Gallerien", + "recently_released_movies": "Kürzlich erschienene Filme", + "recently_released_scenes": "Kürzlich erschienene Szenen", "resolution": "Auflösung", "scene": "Szene", "sceneTagger": "Szenen-Tagger", @@ -805,6 +885,10 @@ "something_went_wrong_description": "Es sieht so aus, als gäbe es Probleme mit deinen Eingaben, klicke Zurück und repariere sie. Falls du nicht weißt was du falsch gemacht hast, helfen wir gerne auf {discordLink}. Solltest du dir sicher sein einen Bug gefunden zu haben, schau doch mal auf {githubLink} vorbei.", "something_went_wrong_while_setting_up_your_system": "Etwas lief bei der Erstellung des Systems falsch. Hier ist die Fehlermeldung: {error}" }, + "folder": { + "file_path": "Dateipfad", + "up_dir": "Ein Verzeichnis hoch" + }, "github_repository": "Github Repository", "migrate": { "backup_database_path_leave_empty_to_disable_backup": "Backup Datenbank Pfad (Leer lassen, um Backups aus zu schalten):", @@ -858,13 +942,18 @@ "next_step": "Wenn du bereit bist ein neues System anzulegen, klicke Weiter.", "unable_to_locate_specified_config": "Wenn du das hier liest, konnte Stash die Konfigurationsdatei, welche spezifiziert wurde, nicht finden. Dieser Wizard wird dich deshalb durch den Prozess führen, eine neue Konfiguration anzulegen." }, - "welcome_to_stash": "Willkommen zu Stash", - "folder": { - "up_dir": "Ein Verzeichnis hoch" - } + "welcome_to_stash": "Willkommen zu Stash" }, "stash_id": "Stash-ID", "stash_ids": "Stash IDs", + "stashbox": { + "go_review_draft": "Gehe zu {endpoint_name}, um Entwurf zu begutachten.", + "selected_stash_box": "Ausgewählter Stash-Box Endpunkt", + "submission_failed": "Einreichen fehlgeschlagen", + "submission_successful": "Einreichen erfolgreich", + "submit_update": "Existiert bereits in {endpoint_name}" + }, + "statistics": "Statistiken", "stats": { "image_size": "Bildspeicher", "scenes_duration": "Szenendauer", @@ -906,6 +995,7 @@ "updated_at": "Aktualisiert am", "url": "URL", "videos": "Videos", + "view_all": "Alle ansehen", "weight": "Gewicht", "years_old": "Jahre alt" } diff --git a/ui/v2.5/src/locales/fi-FI.json b/ui/v2.5/src/locales/fi-FI.json index dbb5e2508..6c41f17f3 100644 --- a/ui/v2.5/src/locales/fi-FI.json +++ b/ui/v2.5/src/locales/fi-FI.json @@ -100,6 +100,7 @@ "stop": "Pysäytä", "submit": "Lähetä", "submit_stash_box": "Lähetä Stash-Boxiin", + "submit_update": "Lähetä päivitys", "tasks": { "clean_confirm_message": "Haluatko varmasti puhdistaa? Tämä poistaa tietokannan tiedot ja poistaa kaikki generoidut tukitiedostot kaikista kohtauksista ja gallerioista, eikä niitä enää ole löydettävissä levyltä.", "dry_mode_selected": "Kuivatila käytössä. Poistoa ei oikeasti tehdä, vain lokikirjaus.", @@ -120,6 +121,7 @@ "birth_year": "Syntymävuosi", "birthdate": "Syntymäpäivä", "bitrate": "Bittinopeus", + "captions": "Tekstitykset", "career_length": "Uran pituus", "component_tagger": { "config": { @@ -705,7 +707,9 @@ "disconnected": "Ei yhdistetty", "error": "Virhe yhdistettäessä Handyyn", "missing": "Puuttuu", - "ready": "Valmis" + "ready": "Valmis", + "syncing": "Synkronoidaan palvelimelle", + "uploading": "Ladataan skriptiä" }, "hasMarkers": "On merkki", "height": "Pituus", @@ -846,6 +850,7 @@ "something_went_wrong_while_setting_up_your_system": "Järjestelmän asettamisessa meni jotain vikaan. Tässä on virhe jonka saimme: {error}" }, "folder": { + "file_path": "Tiedostopolku", "up_dir": "Ylös" }, "github_repository": "Github repository", @@ -899,7 +904,8 @@ "stashbox": { "go_review_draft": "Mene {endpoint_name} katsoaksesi luonnosta.", "submission_failed": "Lähettäminen ei onnistunut", - "submission_successful": "Lähettäminen onnistui" + "submission_successful": "Lähettäminen onnistui", + "submit_update": "On jo kohteessa {endpoint_name}" }, "statistics": "Tilastot", "stats": { diff --git a/ui/v2.5/src/locales/fr-FR.json b/ui/v2.5/src/locales/fr-FR.json index fb2e4667b..874837443 100644 --- a/ui/v2.5/src/locales/fr-FR.json +++ b/ui/v2.5/src/locales/fr-FR.json @@ -252,7 +252,7 @@ "create_galleries_from_folders_desc": "Coché, crée des galeries à partir de dossiers contenant des images.", "create_galleries_from_folders_label": "Créer des galeries à partir de dossiers contenant des images", "db_path_head": "Chemin de la base de données", - "directory_locations_to_your_content": "Emplacements vers votre contenu", + "directory_locations_to_your_content": "Emplacements du répertoire de votre contenu", "excluded_image_gallery_patterns_desc": "Expression régulière de fichiers images et galeries ou de chemins d'accès à exclure de l'analyse et à ajouter au nettoyage", "excluded_image_gallery_patterns_head": "Modèles d'image ou galerie exclués", "excluded_video_patterns_desc": "Expressions régulières de fichiers vidéo ou de chemins d'accès à exclure de l'analyse et à ajouter au nettoyage", @@ -261,40 +261,40 @@ "gallery_ext_head": "Extensions zip de la galerie", "generated_file_naming_hash_desc": "Utilisez MD5 ou oshash pour le nommage des fichiers générés. Le modifier exige que toutes les scènes soient renseignées avec une valeur MD5/oshash appropriée. Après avoir modifié cette valeur, les fichiers générés existants devront être migrés ou régénérés. Voir la page Tâches pour la migration.", "generated_file_naming_hash_head": "Algorithme de hachage pour le nommage des fichiers générés", - "generated_files_location": "Emplacement pour les fichiers générés (marqueurs, apercus, sprites, etc)", - "generated_path_head": "Emplacement des fichiers générés", + "generated_files_location": "Emplacement du répertoire des fichiers générés (marqueurs de scène, aperçus de scène, sprites, etc.)", + "generated_path_head": "Chemin des fichiers générés", "hashing": "Hachage", - "image_ext_desc": "Liste des extensions de fichiers Images.", - "image_ext_head": "Extensions de fichiers Images", - "include_audio_desc": "Inclure l'audio dans la génération des aperçus.", + "image_ext_desc": "Liste délimitée par des virgules des extensions de fichiers qui seront reconnues comme des images.", + "image_ext_head": "Extensions des images", + "include_audio_desc": "Inclure le flux audio lors de la génération des aperçus.", "include_audio_head": "Inclure l'audio", "logging": "Journalisation", - "maximum_streaming_transcode_size_desc": "Résolution maximum pour la conversions des stream", - "maximum_streaming_transcode_size_head": "Résolution maximum de conversion pour les stream", - "maximum_transcode_size_desc": "Résolution maximum pour la conversions des fichiers", - "maximum_transcode_size_head": "Résolution maximum de conversion", + "maximum_streaming_transcode_size_desc": "Résolution maximale pour les flux transcodés", + "maximum_streaming_transcode_size_head": "Résolution maximale du flux transcodé", + "maximum_transcode_size_desc": "Résolution maximale pour les transcodes générés", + "maximum_transcode_size_head": "Résolution maximale de transcodage", "metadata_path": { - "description": "Emplacement du répertoire qui sera utilisé pour un export ou un import de métadonnées", - "heading": "Emplacement des fichiers de Métadonnées" + "description": "Emplacement du répertoire utilisé lors d'une exportation ou d'une importation complète", + "heading": "Chemin des métadonnées" }, - "number_of_parallel_task_for_scan_generation_desc": "0 pour détection automatique. Attention : une valeur trop élevée peut réduire les performances et causer d'autres problèmes.", - "number_of_parallel_task_for_scan_generation_head": "Nombre de tâches en parallèles pour le scan et la génération", - "parallel_scan_head": "Scan/Génération en parallèle", - "preview_generation": "Générer les aperçus", + "number_of_parallel_task_for_scan_generation_desc": "Définissez à 0 pour une détection automatique. Avertissement exécuter plus de tâches que ce qui est nécessaire pour atteindre une utilisation à 100% du processeur diminuera les performances et pourra causer d'autres problèmes.", + "number_of_parallel_task_for_scan_generation_head": "Nombre de tâches parallèles pour l'analyse et la génération", + "parallel_scan_head": "Analyse ou génération en parallèle", + "preview_generation": "Génération d'aperçu", "python_path": { "description": "Emplacement de l'exécutable python. Utilisé par les scrapers et les plugins. Si vide, python sera résolu à partir de l'environnement", "heading": "Chemin de Python" }, "scraper_user_agent": "User-Agent pour les Scraper", - "scraper_user_agent_desc": "Chaîne User-Agent utilisée dans les requêtes http lors du Scraping.", + "scraper_user_agent_desc": "Chaîne User-Agent utilisée dans les requêtes http lors du Scraping", "scrapers_path": { - "description": "Emplacement du répertoire des fichiers de configuration scraper", + "description": "Emplacement du répertoire des fichiers de configuration du scraper", "heading": "Chemin des scrapers" }, "scraping": "Scraping", "sqlite_location": "Emplacement du fichier de base de données SQLite (nécessite un redémarrage)", - "video_ext_desc": "Liste des extensions de fichiers Vidéos.", - "video_ext_head": "Extensions de fichiers Vidéo", + "video_ext_desc": "Liste délimitée par des virgules des extensions de fichiers qui seront reconnus comme des vidéos.", + "video_ext_head": "Extensions de fichiers vidéo", "video_head": "Vidéo" }, "library": { @@ -312,8 +312,8 @@ "scraping": { "entity_metadata": "{entityType} Métadonnées", "entity_scrapers": "{entityType} scrapers", - "excluded_tag_patterns_desc": "Expressions régulières pour l'exclusion de certains Tags des résultats de Scraping", - "excluded_tag_patterns_head": "Exclusion de Tags", + "excluded_tag_patterns_desc": "Expressions régulières de noms d'étiquettes à exclure des résultats de scraping", + "excluded_tag_patterns_head": "Modèles d'étiquette excluse", "scraper": "Scraper", "scrapers": "Scrapers", "search_by_name": "Recherche par nom", @@ -321,13 +321,13 @@ "supported_urls": "URLs" }, "stashbox": { - "add_instance": "Ajouter une instance stash-box", + "add_instance": "Ajouter une instance Stash-Box", "api_key": "Clé API", - "description": "Stash-box facilite le taggage automatique des vidéos et des acteur.trice.s en se basant sur l'empreinte et le nom des fichiers. Le Endpoint et la Clé API peuvent être trouvé dans votre compte stash-box. Si vous spécifiez plusieurs instances stash-box, le nom est requis.", - "endpoint": "Endpoint", - "graphql_endpoint": "Endpoint GraphQL", + "description": "Stash-Box simplifie l'étiquetage automatique des scènes et des performeurs en se basant sur les empreintes digitales et les noms de fichiers.\nLe point de connexion et la clé API se trouvent sur la page de votre compte de l'instance de stash-box. Les noms sont requis lorsque plusieurs instances sont ajoutées.", + "endpoint": "Point de connexion", + "graphql_endpoint": "Point de connexion GraphQL", "name": "Nom", - "title": "Endpoints Stash-box" + "title": "Points de connexion Stash-Box" }, "system": { "transcoding": "Transcodage" @@ -335,49 +335,49 @@ "tasks": { "added_job_to_queue": "{operation_name} ajouté(e) à la liste des tâches", "auto_tag": { - "auto_tagging_all_paths": "Marquage automatique de tous les chemins", - "auto_tagging_paths": "Marquage automatique des chemins suivants" + "auto_tagging_all_paths": "Étiquetage automatique de tous les chemins", + "auto_tagging_paths": "Étiquetage automatique des chemins suivants" }, - "auto_tag_based_on_filenames": "Taggage automatique basé sur le nom des fichiers.", - "auto_tagging": "Taggage automatique", - "backing_up_database": "Sauvegarder la base de données", - "backup_and_download": "Sauvegarde la base de données et télécharge le fichier de sauvegarde.", - "backup_database": "Sauvegarde la base de données au même emplacement que le fichier de base de données. Le fichier se sauvegarde aura le format suivant : {filename_format}", - "cleanup_desc": "Cherche et supprime les fichiers orphelins de la base de données. Cette action est destructive.", + "auto_tag_based_on_filenames": "Étiquetage automatique du contenu en se basant sur les noms de fichiers.", + "auto_tagging": "Étiquetage automatique", + "backing_up_database": "Sauvegarde de la base de données", + "backup_and_download": "Effectue une sauvegarde de la base de données et télécharge le fichier résultant.", + "backup_database": "Effectue une sauvegarde de la base de données dans le même répertoire que celle-ci, avec le format de nom de fichier {filename_format}", + "cleanup_desc": "Vérifier les fichiers manquants et les supprimer de la base de données. Cette action est destructive.", "data_management": "Gestion des données", - "defaults_set": "Les valeurs par défaut ont été définies et seront utilisées lorsque vous cliquerez sur le bouton {action} sur la page Tâches.", + "defaults_set": "Les valeurs par défaut ont été définies et seront utilisées en cliquant sur le bouton {action} de la page Tâches.", "dont_include_file_extension_as_part_of_the_title": "Ne pas inclure l'extension du fichier dans le titre", "empty_queue": "Aucune tâche n'est en cours d'exécution.", - "export_to_json": "Exporte la base de données au format JSON dans le dossier metadata.", + "export_to_json": "Exporte le contenu de la base de données au format JSON dans le répertoire des métadonnées.", "generate": { - "generating_from_paths": "Génération pour les scènes des chemins suivants", + "generating_from_paths": "Génération pour les scènes à partir des chemins suivants", "generating_scenes": "Génération pour {num} {scene}" }, - "generate_desc": "Génère les fichiers images, sprite, vidéo, vtt autres fichiers.", + "generate_desc": "Générer les images associées, images animées, vidéos, vtt et autres fichiers.", "generate_phashes_during_scan": "Générer des hachages perceptuels", "generate_phashes_during_scan_tooltip": "Pour la déduplication et l'identification de scènes.", "generate_previews_during_scan": "Générer des aperçus d'images animées", "generate_previews_during_scan_tooltip": "Générez des aperçus WebP animés (requis uniquement si le type d'aperçu est défini sur Image animée).", - "generate_sprites_during_scan": "Générer les sprites de progression", - "generate_thumbnails_during_scan": "Générer les miniatures pour les images", + "generate_sprites_during_scan": "Générer les images animées de progression", + "generate_thumbnails_during_scan": "Générer des vignettes pour les images", "generate_video_previews_during_scan": "Générer les aperçus", - "generate_video_previews_during_scan_tooltip": "Générez des aperçus vidéo qui s'activent lorsque vous survolez une scène", + "generate_video_previews_during_scan_tooltip": "Générer des aperçus vidéo joués lors du survol d'une scène", "generated_content": "Contenu généré", "identify": { - "and_create_missing": "et créer si manquant", - "create_missing": "Créer si manquant", + "and_create_missing": "et créer les manquants", + "create_missing": "Créer les manquants", "default_options": "Options par défaut", - "description": "Associe automatiquement les métadonnées aux Vidéos en utilisant stash-box et les Scrapers.", - "explicit_set_description": "Ces options seront utilisées par défaut à moins qu'une Source ne les définissent autrement.", + "description": "Définissez automatiquement les métadonnées de la scène en utilisant les sources Stash-Box et scraper.", + "explicit_set_description": "Les options suivantes seront utilisées si elles ne sont pas remplacées par les options spécifiques à la source.", "field": "Champ", "field_behaviour": "{strategy} {field}", - "field_options": "options pour les champs", + "field_options": "Options de champ", "heading": "Identifier", - "identifying_from_paths": "Identifier les vidéos à partir des emplacements suivants", + "identifying_from_paths": "Identifier des scènes à partir des chemins suivants", "identifying_scenes": "Identifier {num} {scene}", - "include_male_performers": "Inclure les acteurs hommes", - "set_cover_images": "Enregistrer l'image de couverture", - "set_organized": "Marquer comme Organisé", + "include_male_performers": "Inclure les performeurs masculins", + "set_cover_images": "Définir les images de couverture", + "set_organized": "Définir le drapeau organisé", "source": "Source", "source_options": "Options pour {source}", "sources": "Sources", @@ -387,67 +387,67 @@ "incremental_import": "Importation incrémentielle à partir d'un fichier zip d'exportation fourni.", "job_queue": "File d'attente des tâches", "maintenance": "Maintenance", - "migrate_hash_files": "A utiliser si vous avez modifié l'algorithme de hachage pour le nommage des fichiers générés. Renomme les fichiers générés en fonction de l'algorithme de hachage sélectionné.", + "migrate_hash_files": "Utilisé après modification du hachage des fichiers générés pour renommer les existants au nouveau format.", "migrations": "Migrations", - "only_dry_run": "Effectuer un essai à blanc. Ne supprime rien du tout.", - "plugin_tasks": "Tâches des Plugins", + "only_dry_run": "Effectuer un essai à blanc. Ne supprime rien", + "plugin_tasks": "Tâches de Plugin", "scan": { - "scanning_all_paths": "Scanner tous les chemins", - "scanning_paths": "Scanner les chemins suivants" + "scanning_all_paths": "Analyse tous les chemins", + "scanning_paths": "Analyse les chemins suivants" }, - "scan_for_content_desc": "Scanner pour des nouveaux contenus et les ajouter à la base de données.", - "set_name_date_details_from_metadata_if_present": "Définissez le titre, la date et d'autres détails à partir des métadonnées du fichier (si présentes)" + "scan_for_content_desc": "Analyser le nouveau contenu et l'ajouter à la base de données.", + "set_name_date_details_from_metadata_if_present": "Définir le nom, date, détails à partir des métadonnées intégrées au fichier" }, "tools": { - "scene_duplicate_checker": "Vérificateur de doublons Vidéos", + "scene_duplicate_checker": "Vérificateur de doublons de scènes", "scene_filename_parser": { "add_field": "Ajouter un champ", "capitalize_title": "Titre en majuscule", "display_fields": "Afficher les champs", - "escape_chars": "Utilisez \\ pour échapper les caractères littéraux", - "filename": "Nom du fichier", - "filename_pattern": "Pattern des noms de fichier", - "ignore_organized": "Ignorer les Vidéos déjà Organisées", - "ignored_words": "Mots à ignorer", + "escape_chars": "Utiliser \\ pour échapper les caractères littéraux", + "filename": "Nom de fichier", + "filename_pattern": "Modèle de nom de fichier", + "ignore_organized": "Ignorer les scènes organisées", + "ignored_words": "Mots ignorés", "matches_with": "Correspond à {i}", - "select_parser_recipe": "Sélectionner une recette d'analyse", - "title": "Analyseur de nom de fichier Vidéos", - "whitespace_chars": "Espaces", + "select_parser_recipe": "Sélectionner une formule d'analyse", + "title": "Analyseur de noms de fichiers de scènes", + "whitespace_chars": "Caractères d'espacement", "whitespace_chars_desc": "Ces caractères seront remplacés par un espace dans le titre" }, - "scene_tools": "Outils Vidéos" + "scene_tools": "Outils de scène" }, "ui": { "basic_settings": "Paramètres de base", "custom_css": { "description": "La page doit être rafraichie pour que les changements prennent effet.", - "heading": "CSS personalisé", - "option_label": "Activer le CSS personalisé" + "heading": "CSS personnalisé", + "option_label": "Activer le CSS personnalisé" }, "delete_options": { - "description": "Réglages par défaut lors de la suppression des Vidéo, Images et Galeries.", + "description": "Réglages par défaut lors de la suppression d'images, galeries, et scènes.", "heading": "Options de suppression", "options": { - "delete_file": "Par défaut, effacer les fichiers", - "delete_generated_supporting_files": "Par défaut, effacer les fichiers générés" + "delete_file": "Supprimer le fichier par défaut", + "delete_generated_supporting_files": "Supprimer par défaut les fichiers associés générés" } }, "desktop_integration": { - "desktop_integration": "Intégration avec le Bureau", + "desktop_integration": "Intégration au bureau", "notifications_enabled": "Activer les notifications", - "send_desktop_notifications_for_events": "Envoyer des notifications bureau pour les événements", + "send_desktop_notifications_for_events": "Envoyer des notifications au bureau en cas d'événements", "skip_opening_browser": "Ne pas ouvrir de navigateur", - "skip_opening_browser_on_startup": "ne pas ouvrir de navigateur au démarrage de l'application" + "skip_opening_browser_on_startup": "Ignorer l'ouverture automatique du navigateur lors du démarrage" }, "editing": { "disable_dropdown_create": { - "description": "Désactive la possibilité de créer des nouveaux Actrices/Acteurs, Studio et Tags depuis la liste déroulante.", - "heading": "Désactiver la création depuis la liste déroulante." + "description": "Supprimer la possibilité de créer de nouveaux objets à partir des sélecteurs de liste déroulante", + "heading": "Désactiver la création depuis la liste déroulante" }, - "heading": "Edition" + "heading": "Édition" }, "funscript_offset": { - "description": "Décalage temporel (en millisecondes) à appliquer lors de la lecture des scripts interactifs.", + "description": "Décalage temporel en millisecondes pour la lecture des scripts interactifs.", "heading": "Décalage Funscript (ms)" }, "handy_connection": { @@ -455,18 +455,24 @@ "server_offset": { "heading": "Offset serveur" }, + "status": { + "heading": "Statut de connexion Handy" + }, "sync": "Synchroniser" }, "handy_connection_key": { - "description": "Clé de connexion Handy pour les Vidéos interactives. Définir cette clé permettra à Stash de partager les informations de votre scène actuelle avec handyfeeling.com", + "description": "Clé de connexion Handy à utiliser pour les scènes interactives. En définissant cette clé, vous permettez à Stash de partager les informations de votre scène actuelle avec handyfeeling.com", "heading": "Clé de connexion Handy" }, + "image_lightbox": { + "heading": "Visionneuse d'images" + }, "images": { "heading": "Images", "options": { "write_image_thumbnails": { - "description": "Écrire les miniatures des images sur le disque lorsqu'elles sont générées à la volée", - "heading": "Enregistrer les miniatures des images" + "description": "Écrire les vignettes des images sur le disque lorsqu'elles sont générées à la volée", + "heading": "Enregistrer les vignettes des images" } } }, @@ -475,8 +481,8 @@ "heading": "Langue" }, "max_loop_duration": { - "description": "Durée maximum de la vidéo pendant laquelle le lecteur rebouclera la vidéo - 0 pour désactiver", - "heading": "Durée maximum de rebouclage" + "description": "Durée maximale de la scène pendant laquelle le lecteur bouclera la vidéo - 0 pour désactiver", + "heading": "Durée maximale de la boucle" }, "menu_items": { "description": "Afficher ou masquer différents types de contenus dans la barre de navigation", @@ -485,13 +491,13 @@ "performers": { "options": { "image_location": { - "description": "Chemin vers l'image à utiliser pour les Actrices/Acteurs qui n'ont pas d'image. Laissez vide pour utiliser l'image par défaut de Stash.", - "heading": "Image par défaut personalisée pour les actrices/acteurs" + "description": "Chemin personnalisé pour les images par défaut de performeur. Laisser vide pour utiliser les valeurs par défaut intégrées", + "heading": "Chemin de l'image du performeur personnalisé" } } }, "preview_type": { - "description": "Configuration des éléments du Mur", + "description": "Configuration des éléments du mur", "heading": "Type d'aperçu", "options": { "animated": "Image animée", @@ -500,28 +506,30 @@ } }, "scene_list": { - "heading": "Liste Vidéo", + "heading": "Liste de scène", "options": { - "show_studio_as_text": "Afficher les Studios en tant que texte" + "show_studio_as_text": "Afficher les studios sous format texte" } }, "scene_player": { - "heading": "Lecteur Vidéo", + "heading": "Lecteur de scène", "options": { - "auto_start_video": "Lecture automatique", + "auto_start_video": "Démarrer automatiquement la vidéo", "auto_start_video_on_play_selected": { - "description": "Lancer automatiquement la lecture de la vidéo de la scène lorsque « lecture » est sélectionné ou qu'une scène aléatoire est sélectionnée à partir de la page de la scène", - "heading": "Démarrer automatiquement la vidéo lorsque \"play\" est sélectionné" + "description": "Démarrer automatiquement les scènes vidéo lorsque lecture est sélectionnée ou aléatoire à partir de la page Scènes", + "heading": "Démarrer automatiquement la vidéo lorsque lecture est sélectionnée" }, "continue_playlist_default": { - "description": "Lire la scène suivante dans la file d'attente lorsque la vidéo est terminée", + "description": "Lire la scène suivante dans la file d'attente lorsque la vidéo se termine", "heading": "Continuer la liste de lecture par défaut" - } + }, + "show_scrubber": "Montrer la barre de progression" } }, "scene_wall": { + "heading": "Mur de scènes et marqueurs", "options": { - "display_title": "Afficher le titre et les tags", + "display_title": "Afficher le titre et étiquettes", "toggle_sound": "Activer le son" } }, @@ -530,7 +538,7 @@ "heading": "Tentatives de défilement avant transition" }, "slideshow_delay": { - "description": "Diaporama disponible dans Galeries en mode de visionnage Mur", + "description": "Le diaporama est disponible dans galerie en mode de vue mural", "heading": "Délai du diaporama (secondes)" }, "title": "Interface utilisateur" @@ -543,14 +551,14 @@ "images": "{count, plural, one {Image} other {Images}}", "markers": "{count, plural, one {Marqueur} other {Marqueurs}}", "movies": "{count, plural, one {Film} other {Films}}", - "performers": "{count, plural, one {Actrice/Acteur} other {Actrices/Acteurs}}", - "scenes": "{count, plural, one {Vidéo} other {Vidéos}}", + "performers": "{count, plural, one {Performeur} other {Performeurs}}", + "scenes": "{count, plural, one {Scène} other {Scènes}}", "studios": "{count, plural, one {Studio} other {Studios}}", - "tags": "{count, plural, one {Tag} other {Tags}}" + "tags": "{count, plural, one {Étiquette} other {Étiquettes}}" }, "country": "Pays", "cover_image": "Image de couverture", - "created_at": "Date de création", + "created_at": "Créé le", "criterion": { "greater_than": "Supérieur à", "less_than": "Inférieur à", @@ -564,52 +572,52 @@ "greater_than": "est plus grand que", "includes": "contient", "includes_all": "contient tout", - "is_null": "est null", + "is_null": "est nul", "less_than": "est plus petit que", - "matches_regex": "match l'expression régulière", + "matches_regex": "correspond à l'expression régulière", "not_between": "en dehors", "not_equals": "n'est pas égal à", - "not_matches_regex": "ne match pas l'expression régulière", - "not_null": "n'est pas null" + "not_matches_regex": "ne correspond pas à l'expression régulière", + "not_null": "n'est pas nul" }, - "custom": "Personalisé", + "custom": "Personnalisé", "date": "Date", - "death_date": "Date de décès", - "death_year": "Année de décès", + "death_date": "Date du décès", + "death_year": "Année du décès", "descending": "Descendant", "detail": "Détail", "details": "Détails", - "developmentVersion": "Version de Développement", + "developmentVersion": "Version de développement", "dialogs": { "aliases_must_be_unique": "Les alias doivent être uniques", "delete_alert": "{count, plural, one {Ce/Cette {singularEntity} sera supprimé(e)} other {Ces {pluralEntity} seront supprimé(e)s}} définitivement :", "delete_confirm": "Êtes-vous sûr de vouloir supprimer {entityName} ?", - "delete_entity_desc": "{count, plural, one {Êtes-vous sûr de vouloir supprimer cette {singularEntity} ? À moins que le fichier ne soit également supprimé, cette {singularEntity} sera ajoutée à nouveau lors du prochain Scan.} other {Êtes-vous sûr de vouloir supprimer ces {pluralEntity} ? À moins que les fichiers ne soient également supprimés, ces {pluralEntity} seront ajoutées à nouveau lors du prochain Scan.}}", + "delete_entity_desc": "{count, plural, one {Êtes-vous sûr de vouloir supprimer cette {singularEntity} ? À moins que le fichier ne soit également supprimé, cette {singularEntity} sera ajoutée à nouveau lors de la prochaine analyse.} other {Êtes-vous sûr de vouloir supprimer ces {pluralEntity} ? À moins que les fichiers ne soient également supprimés, ces {pluralEntity} seront ajoutées à nouveau lors de la prochaine analyse.}}", "delete_entity_title": "{count, plural, one {Delete {singularEntity}} other {Delete {pluralEntity}}}", - "delete_galleries_extra": "…ainsi que tout fichier image non-associé à une autre Galerie.", - "delete_gallery_files": "Supprime le dossier ou l'archive de la galerie, ainsi que toute image non associées à une autre galerie.", + "delete_galleries_extra": "…ainsi que tous fichiers image qui ne sont pas associés à une autre galerie.", + "delete_gallery_files": "Supprime le répertoire ou l'archive zip de la galerie et toutes images qui ne sont pas associées à une autre galerie.", "delete_object_desc": "Êtes-vous sûr de vouloir supprimer {count, plural, one {this {singularEntity}} other {these {pluralEntity}}} ?", "delete_object_overflow": "…et {count} {count, plural, one {autre {singularEntity}} other {autres {pluralEntity}}}.", "delete_object_title": "Supprimer {count, plural, one {{singularEntity}} other {{pluralEntity}}}", - "edit_entity_title": "Editer {count, plural, one {{singularEntity}} other {{pluralEntity}}}", - "export_include_related_objects": "Inclure les entités liées", + "edit_entity_title": "Éditer {count, plural, one {{singularEntity}} other {{pluralEntity}}}", + "export_include_related_objects": "Inclure les objets liés dans l'exportation", "export_title": "Export", "lightbox": { - "delay": "Délais (Secondes)", + "delay": "Délai (Secondes)", "display_mode": { - "fit_horizontally": "Adapter horizontalement", + "fit_horizontally": "Ajustement horizontal", "fit_to_screen": "Adapter à l'écran", "label": "Mode d'affichage", "original": "Original" }, "options": "Options", - "reset_zoom_on_nav": "Remise à zero du niveau de zoom au changement d'image", + "reset_zoom_on_nav": "Réinitialisation du facteur de zoom lors d'un changement d'image", "scale_up": { - "description": "Agrandir l'image pour adapter à l'écran", - "label": "Adapter à l'écran" + "description": "Redimensionner les petites images pour les adapter à l'écran", + "label": "Mise à l'échelle" }, "scroll_mode": { - "description": "Maintenir MAJ pour utiliser temporairement l'autre mode.", + "description": "Maintenir la touche shift pour utiliser temporairement l'autre mode.", "label": "Mode de défilement", "pan_y": "Panoramique axe Y", "zoom": "Zoom" @@ -619,62 +627,62 @@ "destination": "Destination", "source": "Source" }, - "overwrite_filter_confirm": "Êtes-vous sûr de vouloir écraser le filtre sauvegardé {entityName} ?", + "overwrite_filter_confirm": "Êtes-vous sûr de vouloir remplacer la requête sauvegardée existante {entityName} ?", "scene_gen": { - "force_transcodes": "Forcer la génération de transcodage", - "force_transcodes_tooltip": "Par défaut, les transcodages ne sont générés que lorsque le fichier vidéo n'est pas pris en charge dans le navigateur. Lorsqu'il est activé, les transcodes seront générés même lorsque le fichier vidéo semble être pris en charge dans le navigateur.", + "force_transcodes": "Forcer la génération du transcodage", + "force_transcodes_tooltip": "Par défaut, les transcodes ne sont générés que lorsque le fichier vidéo n'est pas pris en charge par le navigateur. Activé, les transcodes seront générés même si le fichier vidéo semble être pris en charge par le navigateur.", "image_previews": "Aperçus d'images animées", - "image_previews_tooltip": "Aperçus animés (en WebP), requis uniquement si le type d'aperçu est défini sur Image animée.", + "image_previews_tooltip": "Aperçu WebP animé, requis uniquement si le mode d'aperçu est défini sur Image animée.", "interactive_heatmap_speed": "Générer des cartes thermiques et des vitesses pour les scènes interactives", "marker_image_previews": "Aperçu des images animées par marqueur", - "marker_image_previews_tooltip": "Aperçus animé des marqueurs (en WebP), requis uniquement si le type d'aperçu est défini sur Image animée.", + "marker_image_previews_tooltip": "Aperçus WebP de marqueurs animés, requis uniquement si le mode d'aperçu est défini sur Image animée.", "marker_screenshots": "Captures d'écran du marqueur", - "marker_screenshots_tooltip": "Marquer les images JPG statiques, requis uniquement si le type d'aperçu est défini sur Image statique.", + "marker_screenshots_tooltip": "Marquer les images JPG statiques, requis uniquement si le mode d'aperçu est défini sur Image statique.", "markers": "Aperçus des Marqueurs", - "markers_tooltip": "Vidéos de 20 secondes qui commencent au code temporel donné.", + "markers_tooltip": "Vidéos de 20 secondes qui débutent au repère temporel donné.", "override_preview_generation_options": "Remplacer les options de génération d'aperçu", "override_preview_generation_options_desc": "Remplacer les options de génération d'aperçu pour cette opération. Les valeurs par défaut sont définies dans Système -> Génération d'aperçus.", - "overwrite": "Ecraser les fichiers générés existants", - "phash": "Hachage perceptif (pour la déduplication)", - "preview_exclude_end_time_desc": "Exclure les x dernières secondes de la vidéo pour la génération de l'aperçu. La valeur peut-être exprimée en secondes ou bien en pourcentage (ex : 2%) de la durée totale de la vidéo.", - "preview_exclude_end_time_head": "Exclure à la fin", - "preview_exclude_start_time_desc": "Exclure les x premières secondes de la vidéo pour la génération de l'aperçu. La valeur peut-être exprimée en secondes ou bien en pourcentage (ex : 2%) de la durée totale de la vidéo.", - "preview_exclude_start_time_head": "Exclure au début", - "preview_generation_options": "Options des générations d'aperçu", - "preview_options": "Options d'Aperçus", - "preview_preset_desc": "Le Preset d'encodage régule la taille, la qualité et le temps d'encodage des aperçus. Les Preset plus bas que “slow” n'apportent pas de gain significatif et ne sont pas recommandés.", - "preview_preset_head": "Preset d'encodage de l'aperçu", - "preview_seg_count_desc": "Nombre total de segments dans un aperçu.", - "preview_seg_count_head": "Nombre de segments dans un aperçu", - "preview_seg_duration_desc": "Durée de chaque segment d'aperçu (en secondes).", - "preview_seg_duration_head": "Durée d'un segment d'aperçu", + "overwrite": "Remplacer les fichiers générés existants", + "phash": "Hachages perceptuels (pour la déduplication)", + "preview_exclude_end_time_desc": "Exclure les x dernières secondes des aperçus de la scène. Cela peut être une valeur en secondes, ou un pourcentage (par exemple 2%) de la durée totale de la scène.", + "preview_exclude_end_time_head": "Exclure le temps de fin", + "preview_exclude_start_time_desc": "Exclure les x premières secondes des aperçus de la scène. Cela peut être une valeur en secondes, ou un pourcentage (par exemple 2%) de la durée totale de la scène.", + "preview_exclude_start_time_head": "Exclure le temps de départ", + "preview_generation_options": "Options de génération d'aperçus", + "preview_options": "Options d'aperçu", + "preview_preset_desc": "Le préréglage règle la taille, la qualité et le temps d'encodage de la génération d'aperçu. Les préréglages autres que \"slow\" ont un résultat moindre et ne sont pas recommandés.", + "preview_preset_head": "Préréglage de l'encodage d'aperçu", + "preview_seg_count_desc": "Nombre de segments dans les fichiers de l'aperçu.", + "preview_seg_count_head": "Nombre de segments dans l'aperçu", + "preview_seg_duration_desc": "Durée de chaque segment d'aperçu, en secondes.", + "preview_seg_duration_head": "Durée du segment d'aperçu", "sprites": "Sprites de progression de scène", "sprites_tooltip": "Sprites (pour la progression de scène)", - "transcodes": "Transcodages", - "transcodes_tooltip": "Conversion au format MP4 des fichiers vidéo dont le format n'est pas supporté par le lecteur vidéo", + "transcodes": "Transcoder", + "transcodes_tooltip": "Conversion en MP4 des formats vidéo non pris en charge", "video_previews": "Aperçus", "video_previews_tooltip": "Prévisualisation de la vidéo lors du survol d'une scène" }, - "scenes_found": "{count} Vidéo(s) trouvée(s)", + "scenes_found": "{count} scènes trouvées", "scrape_entity_query": "Requête de Scrape {entity_type}", - "scrape_entity_title": "Résultat du Scraping {entity_type}", + "scrape_entity_title": "Résultats du Scraping {entity_type}", "scrape_results_existing": "Existant", "scrape_results_scraped": "Scraped", "set_image_url_title": "URL de l'image", - "unsaved_changes": "Changements non-sauvegardés. Êtes-vous sûr de vouloir quitter ?" + "unsaved_changes": "Modifications non sauvegardées. Vous êtes sûr de vouloir quitter ?" }, - "dimensions": "Dimensions de l'image", + "dimensions": "Dimensions", "director": "Réalisateur", "display_mode": { "grid": "Grille", "list": "Liste", - "tagger": "Taggueur", + "tagger": "Étiqueteur", "unknown": "Inconnu", "wall": "Mur" }, "donate": "Faire un don", "dupe_check": { - "description": "Les précisions plus faibles que 'Exacte' peuvent prendre plus de temps à être calculées. Les niveaux de précisions faibles peuvent également mener à des résultats faux-positifs.", + "description": "Les niveaux en-deça de \"Exact\" peuvent prendre plus de temps à calculer. Des faux positifs peuvent également être retournés à de faibles précisions.", "found_sets": "{setCount, plural, one{# ensemble de doublons trouvé.} other {# ensembles de doublons trouvés.}}", "options": { "exact": "Exacte", @@ -682,9 +690,10 @@ "low": "Basse", "medium": "Moyenne" }, - "search_accuracy_label": "Précision", - "title": "Scènes en double" + "search_accuracy_label": "La pertinence de la recherche", + "title": "Scènes dupliquées" }, + "duplicated_phash": "Dupliqué (phash)", "duration": "Durée", "effect_filters": { "aspect": "Aspect", @@ -697,68 +706,80 @@ "hue": "Teinte", "name": "Filtres", "name_transforms": "Transformations", - "red": "Red", - "reset_filters": "Annuler les filtres", - "reset_transforms": "Annuler les transformations", + "red": "Rouge", + "reset_filters": "Rétablir les filtres", + "reset_transforms": "Rétablir les transformations", "rotate": "Rotation", - "rotate_left_and_scale": "Rotation Gauche & Dimensionnement", - "rotate_right_and_scale": "Rotation Droite & Dimensionnement", + "rotate_left_and_scale": "Rotation à gauche et mise à l'échelle", + "rotate_right_and_scale": "Rotation à droite et mise à l'échelle", "saturation": "Saturation", - "scale": "Dimension", + "scale": "Mise à l'échelle", "warmth": "Chaleur" }, + "empty_server": "Ajoutez quelques scènes à votre serveur pour afficher les recommandations sur cette page.", "ethnicity": "Ethnicité", + "existing_value": "valeur existante", "eye_color": "Couleur des yeux", - "fake_tits": "Implants mammaires", + "fake_tits": "Faux seins", "false": "Faux", "favourite": "Favoris", "file": "fichier", "file_info": "Infos fichier", - "file_mod_time": "Date de modification fichier", - "files": "fichier", + "file_mod_time": "Date de modification du fichier", + "files": "fichiers", "filesize": "Taille du fichier", "filter": "Filtre", "filter_name": "Nom du filtre", "filters": "Filtres", - "framerate": "Fréquence d'images", - "frames_per_second": "{value} ips", + "framerate": "Fréquence de rafraîchissement", + "frames_per_second": "{value} images par seconde", "galleries": "Galeries", "gallery": "Galerie", - "gallery_count": "Nombre de Galeries", + "gallery_count": "Nombre de galeries", "gender": "Genre", "gender_types": { "FEMALE": "Femme", "INTERSEX": "Intersexe", "MALE": "Homme", - "NON_BINARY": "Non Binaire", + "NON_BINARY": "Non binaire", "TRANSGENDER_FEMALE": "Femme transgenre", "TRANSGENDER_MALE": "Homme transgenre" }, "hair_color": "Couleur des cheveux", - "hasMarkers": "Possède des marqueurs", - "height": "Taille", + "handy_connection_status": { + "connecting": "Connexion", + "disconnected": "Déconnecté", + "error": "Erreur de connexion à Handy", + "missing": "Manquant", + "ready": "Prêt", + "syncing": "Synchronisation avec le serveur", + "uploading": "Script de chargement" + }, + "hasMarkers": "Dispose de marqueurs", + "height": "Hauteur", "help": "Aide", + "ignore_auto_tag": "Ignorer l'étiquetage automatique", "image": "Image", "image_count": "Nombre d'Images", "images": "Images", - "include_parent_tags": "Inclure les Tags parents", - "include_sub_studios": "Inclure les studios affiliés / filiales", - "include_sub_tags": "Inclure les sous-Tags", + "include_parent_tags": "Inclure les étiquettes parentes", + "include_sub_studios": "Inclure les studios affiliés", + "include_sub_tags": "Inclure les sous-étiquettes", "instagram": "Instagram", "interactive": "Interactif", "interactive_speed": "Vitesse interactive", - "isMissing": "Manquant", + "isMissing": "Est manquant", "library": "Bibliothèque", "loading": { "generic": "Chargement…" }, - "marker_count": "Nombre de Marqueurs", + "marker_count": "Nombre de marqueurs", "markers": "Marqueurs", "measurements": "Mensurations", "media_info": { - "audio_codec": "Codec Audio", + "audio_codec": "Codec audio", "checksum": "Somme de contrôle", - "downloaded_from": "Téléchargé de", + "downloaded_from": "Téléchargé depuis", "hash": "Hachage", "interactive_speed": "Vitesse interactive", "performer_card": { @@ -769,7 +790,7 @@ "stream": "Stream", "video_codec": "Codec vidéo" }, - "megabits_per_second": "{value} Mbps", + "megabits_per_second": "{value} mégabits par seconde", "metadata": "Métadonnées", "movie": "Film", "movie_scene_number": "Nombre de Scènes de films", @@ -777,7 +798,7 @@ "name": "Nom", "new": "Nouveau", "none": "Aucun", - "o_counter": "O-mètre", + "o_counter": "O-Compteur", "operations": "Opérations", "organized": "Organisé", "pagination": { @@ -788,47 +809,48 @@ }, "parent_of": "Parent de {children}", "parent_studios": "Studio parent", - "parent_tag_count": "Nombre de Tags Parents", - "parent_tags": "Tag parent", + "parent_tag_count": "Nombre d'étiquettes parentes", + "parent_tags": "Étiquettes parentes", "part_of": "Fait partie de {parent}", "path": "Chemin", - "performer": "Actrice/Acteur", - "performerTags": "Tags d'Actrice/Acteur", - "performer_age": "Âge de l'artiste", - "performer_count": "Nombre d'Actrices/Acteurs", - "performer_favorite": "Artiste favori", - "performer_image": "Photo", + "perceptual_similarity": "Similitude perceptuelle (phash)", + "performer": "Performeur", + "performerTags": "Étiquettes de performeur", + "performer_age": "Âge du performeur", + "performer_count": "Nombre de performeur", + "performer_favorite": "Performeur favori", + "performer_image": "Photo du performeur", "performer_tagger": { "current_page": "Page actuelle", "name_already_exists": "Le nom existe déjà", "network_error": "Erreur réseau", "no_results_found": "Aucun résultat trouvé.", - "performer_already_tagged": "Artiste déjà étiqueté", - "performer_successfully_tagged": "Artiste étiqueté avec succès :", - "update_performer": "Mise à jour de l'artiste", - "update_performers": "Mise à jour des artistes" + "performer_already_tagged": "Performeur déjà étiqueté", + "performer_successfully_tagged": "Performeur étiqueté avec succès :", + "update_performer": "Mise à jour du performeur", + "update_performers": "Mise à jour des performeurs" }, - "performers": "Actrices/Acteurs", + "performers": "Performeurs", "piercings": "Piercings", "queue": "File de lecture", "random": "Aléatoire", "rating": "Note", - "recently_added_performers": "Artistes récemment ajoutés", + "recently_added_performers": "Performeurs récemment ajoutés", "recently_added_studios": "Studios récemment ajoutés", - "recently_released_galleries": "Galeries récemment publiées", + "recently_released_galleries": "Galeries récemment sorties", "recently_released_movies": "Films récemment sortis", - "recently_released_scenes": "Scènes récemment publiées", + "recently_released_scenes": "Scènes récemment sorties", "resolution": "Résolution", - "scene": "Vidéo", - "sceneTagger": "Taggueur de Scène", - "sceneTags": "Tags de Scène", - "scene_count": "Nombre de Vidéos", - "scene_id": "Vidéo ID", - "scenes": "Vidéos", - "scenes_updated_at": "Scène modifiée le", + "scene": "Scène", + "sceneTagger": "Étiqueteur de scènes", + "sceneTags": "Étiquettes de scène", + "scene_count": "Nombre de scènes", + "scene_id": "ID de scène", + "scenes": "Scènes", + "scenes_updated_at": "Scène actualisée le", "search_filter": { "add_filter": "Ajouter un filtre", - "name": "Filtres", + "name": "Filtre", "saved_filters": "Filtres sauvegardés", "update_filter": "Mise à jour du filtre" }, @@ -836,10 +858,10 @@ "settings": "Paramètres", "setup": { "confirm": { - "almost_ready": "Nous sommes presque prêts à terminer la configuration. Veuillez confirmer les paramètres suivants. Vous pouvez revenir en arrière pour modifier tout ce qui est incorrect. Si tout semble bon, cliquez sur Confirmer pour créer votre système.", + "almost_ready": "Nous sommes presque prêts à terminer la configuration. Confirmez les paramètres suivants. Vous pouvez revenir en arrière pour modifier toute erreur. Si tout semble correct, cliquez sur Confirmer pour créer votre système.", "configuration_file_location": "Emplacement du fichier de configuration :", - "database_file_path": "Chemin du fichier de la base de données", - "default_db_location": "/stash-go.sqlite", + "database_file_path": "Chemin du fichier de base de données", + "default_db_location": "/stash-go.sqlite", "default_generated_content_location": "/generated", "generated_directory": "Répertoire généré", "nearly_there": "Nous y sommes presque !", @@ -847,12 +869,12 @@ }, "creating": { "creating_your_system": "Création de votre système", - "ffmpeg_notice": "Si ffmpeg n'est pas disponible, soyez patient le temps que Stash le télécharge. Regardez la sortie de la console pour voir la progression du téléchargement." + "ffmpeg_notice": "Si ffmpeg n'est pas encore dans vos chemins, veuillez être patient pendant que stash le télécharge. Consultez la sortie de la console pour voir la progression du téléchargement." }, "errors": { - "something_went_wrong": "Oh non ! Quelque chose n'a pas fonctionné !", - "something_went_wrong_description": "Si vous pensez qu'il peut y avoir une erreur avec les données fournies, veuillez cliquer en arrière pour la corriger. Sinon, veuillez ouvrir un ticket sur {githubLink} ou demander de l'aide à {discordLink}.", - "something_went_wrong_while_setting_up_your_system": "Une erreur s'est produite lors de la configuration de votre système. Voici l'erreur que nous avons reçue : {error}" + "something_went_wrong": "Oh non ! Quelque chose a mal tourné !", + "something_went_wrong_description": "Si cela ressemble à un problème avec vos saisies, continuez et cliquez sur retour pour les corriger. Sinon, créez un bogue sur {githubLink} ou demandez de l'aide sur {discordLink}.", + "something_went_wrong_while_setting_up_your_system": "Un problème est survenu lors de la configuration de votre système. Voici l'erreur que nous avons reçue : {error}" }, "folder": { "file_path": "Chemin de fichier", @@ -860,29 +882,29 @@ }, "github_repository": "Dépôt Github", "migrate": { - "backup_database_path_leave_empty_to_disable_backup": "Chemin de la sauvegarde de la base de données (laissez vide pour désactiver la sauvegarde) :", + "backup_database_path_leave_empty_to_disable_backup": "Chemin de la base de données de sauvegarde (laissez vide pour désactiver la sauvegarde) :", "backup_recommended": "Il est recommandé de sauvegarder votre base de données existante avant de procéder à la migration. Nous pouvons le faire pour vous, en faisant une copie de votre base de données dans {defaultBackupPath}.", "migrating_database": "Migration de la base de données", - "migration_failed": "Échec de la migration", - "migration_failed_error": "L'erreur suivante s'est produite lors de la migration de la base de données :", - "migration_failed_help": "Veuillez apporter les corrections nécessaires et réessayer. Sinon, signalez un bogue sur le {githubLink} ou cherchez de l'aide dans le {discordLink}.", - "migration_irreversible_warning": "Le processus de migration n'est pas réversible. Une fois la migration effectuée, votre base de données sera incompatible avec les versions précédentes de Stash.", + "migration_failed": "La migration a échoué", + "migration_failed_error": "L'erreur suivante a été rencontrée lors de la migration de la base de données :", + "migration_failed_help": "Veuillez apporter les corrections nécessaires et réessayer. Sinon, signalez un bogue sur {githubLink} ou demandez de l'aide sur {discordLink}.", + "migration_irreversible_warning": "Le processus de migration des schémas n'est pas réversible. Une fois la migration effectuée, votre base de données sera incompatible avec les versions précédentes de Stash.", "migration_required": "Migration requise", - "perform_schema_migration": "Effectuer la migration", - "schema_too_old": "La version de la base de données est {databaseSchema} et doit être mise à jour vers {appSchema} . Cette version de Stash ne fonctionnera pas sans migration de la base de données." + "perform_schema_migration": "Procéder à la migration des schémas", + "schema_too_old": "La version du schéma de votre base de données Stash actuelle est {databaseSchema} et doit être migrée vers la version {appSchema}. Cette version de Stash ne fonctionnera pas sans migration de la base de données." }, "paths": { "database_filename_empty_for_default": "Nom de fichier de la base de données (vide par défaut)", - "description": "Ensuite, nous devons déterminer où trouver votre collection de porno, où stocker la base de données de stockage et les fichiers générés. Ces paramètres peuvent être modifiés ultérieurement si nécessaire.", + "description": "Ensuite, nous devons déterminer où trouver votre collection pornographique, où stocker la base de données Stash et les fichiers générés. Ces paramètres peuvent être modifiés ultérieurement si nécessaire.", "path_to_generated_directory_empty_for_default": "Chemin vers le répertoire généré (vide par défaut)", "set_up_your_paths": "Configurez vos chemins", - "stash_alert": "Aucun chemin de bibliothèque n'a été sélectionné. Aucun média ne pourra être analysé dans Stash. Es-tu sûr ?", + "stash_alert": "Aucun chemin de bibliothèque n'a été sélectionné. Aucun média ne pourra être analysé dans Stash. En êtes-vous sûr ?", "where_can_stash_store_its_database": "Où Stash peut-il stocker sa base de données ?", - "where_can_stash_store_its_database_description": "Stash utilise une base de données SQLite pour stocker les métadonnées de votre collection. Par défaut, il sera créé en tant que stash-go.sqlite dans le répertoire où se trouve votre fichier de configuration. Si vous souhaitez modifier cela, veuillez entrer un nom de fichier avec un chemin absolu ou relatif (vers le répertoire de travail actuel).", - "where_can_stash_store_its_generated_content": "Où Stash peut-il stocker le contenu généré ?", - "where_can_stash_store_its_generated_content_description": "Afin de fournir des miniatures, des aperçus et des sprites, Stash génère des images et des vidéos. Cela inclut également les transcodages pour les formats de fichiers non pris en charge. Par défaut, Stash créera un répertoire generated dans le répertoire contenant votre fichier de configuration. Si vous souhaitez modifier l'emplacement de stockage de ce média généré, veuillez saisir un chemin absolu ou relatif (au répertoire de travail actuel). Stash créera ce répertoire s'il n'existe pas déjà.", + "where_can_stash_store_its_database_description": "Stash utilise une base de données sqlite pour stocker vos métadonnées pornographiques. Par défaut, cette base sera créée en tant que stash-go.sqlite dans le répertoire contenant votre fichier de configuration. Si vous souhaitez modifier cela, saisissez un nom de fichier absolu ou relatif ( vers le répertoire de travail actuel).", + "where_can_stash_store_its_generated_content": "Où Stash peut-il stocker son contenu généré ?", + "where_can_stash_store_its_generated_content_description": "Afin de produire les vignettes, les aperçus et les images animées, Stash génère des images et des vidéos. Cela inclut également les transcodes pour les formats de fichiers non pris en charge. Par défaut, Stash crée un répertoire generated dans le répertoire contenant votre fichier de configuration. Si vous souhaitez modifier l'emplacement où seront stockés les médias générés, veuillez saisir un chemin absolu ou relatif ( vers le répertoire de travail actuel). Stash créera ce répertoire s'il n'existe pas déjà.", "where_is_your_porn_located": "Où se trouve votre porno ?", - "where_is_your_porn_located_description": "Ajoutez des répertoires contenant vos vidéos et images porno. Stash utilisera ces répertoires pour rechercher des vidéos et des images lors de l'analyse." + "where_is_your_porn_located_description": "Ajoutez des répertoires contenant vos vidéos et images pornographiques. Stash utilisera ces répertoires pour rechercher les vidéos et les images lors de l'analyse." }, "stash_setup_wizard": "Assistant de configuration de Stash", "success": { diff --git a/ui/v2.5/src/locales/hu-HU.json b/ui/v2.5/src/locales/hu-HU.json new file mode 100644 index 000000000..476be5ae9 --- /dev/null +++ b/ui/v2.5/src/locales/hu-HU.json @@ -0,0 +1,539 @@ +{ + "actions": { + "add": "Hozzáadás", + "add_directory": "Mappa Hozzáadása", + "add_entity": "{entityType} Hozzáadása", + "add_to_entity": "{entityType}hoz Adás", + "allow": "Engedélyez", + "allow_temporarily": "Időszakosan Engedélyez", + "apply": "Alkalmaz", + "auto_tag": "Automatikus Címkézés", + "backup": "Biztonsági Mentés", + "browse_for_image": "Kép tallózása…", + "cancel": "Mégsem", + "clean": "Tisztítás", + "clear": "Törlés", + "clear_back_image": "Hátsó kép törlése", + "clear_front_image": "Első kép törlése", + "clear_image": "Kép Törlése", + "close": "Bezár", + "confirm": "Jóváhagyás", + "continue": "Folytatás", + "create": "Létrehoz", + "create_entity": "{entityType} Létrehozása", + "delete": "Törlés", + "delete_entity": "{entityType} Törlése", + "delete_file": "Fájl Törlése", + "delete_generated_supporting_files": "Létrehozott kiegészítő fájlok törlése", + "disallow": "Tiltás", + "download": "Letöltés", + "download_backup": "Biztonsági Mentés Letöltése", + "edit": "Módosít", + "edit_entity": "{entityType} Módosítása", + "export": "Exportálás…", + "export_all": "Összes exportálása…", + "find": "Keresés", + "finish": "Befejez", + "from_file": "Fáljból…", + "from_url": "URL-ből…", + "full_export": "Teljes Export", + "full_import": "Teljes Import", + "generate": "Generálás", + "generate_thumb_default": "Alapértelmezett bélyegkép generálása", + "generate_thumb_from_current": "Bélyegkép generálása a jelenlegiből", + "hide": "Elrejtés", + "hide_configuration": "Beállítások Elrejtése", + "identify": "Beazonosítás", + "ignore": "Mellőz", + "import": "Importálás…", + "import_from_file": "Importálás fájlból", + "logout": "Kijelentkezés", + "merge": "Egyesítés", + "next_action": "Következő", + "not_running": "nem fut", + "open_random": "Véletlenszerű Megnyitása", + "overwrite": "Felülír", + "play_random": "Véletlenszerű Lejátszása", + "play_selected": "Kiválasztott Lejátszása", + "preview": "Előnézet", + "previous_action": "Vissza", + "refresh": "Frissítés", + "reload_plugins": "Pluginek újratöltése", + "reload_scrapers": "Scrapperek újratöltése", + "remove": "Eltávolítás", + "rename_gen_files": "Generált fájlok átnevezése", + "rescan": "Újra Szkennelés", + "reshuffle": "Újrakeverés", + "running": "fut", + "save": "Mentés", + "save_filter": "Szűrő mentése", + "scan": "Szkennelés", + "search": "Keresés", + "select_all": "Mind Kijelölése", + "select_entity": "{entityType} Kijelölése", + "select_folders": "Mappák kijelölése", + "select_none": "Kijelölés Törlése", + "set_as_default": "Beállítás alapértelmezettként", + "set_image": "Kép beállítása…", + "show": "Megjelenít", + "show_configuration": "Beállítások Megjelenítése", + "skip": "Kihagyás", + "stop": "Megállít", + "submit": "Beküldés", + "submit_stash_box": "Stash-Box-ba Beküldés", + "tasks": { + "clean_confirm_message": "Biztos vagy benne hogy el akarod végezni a Tisztítást? Ez a művelet le fogja törölni a fáljrendszerben már nem megtalálható összes jelenet és galéria adatbázis információit és generált tartalmait.", + "import_warning": "Biztos vagy benne, hogy importálni akarsz? Ez a művelet le fogja törölni az adatbázist és újraimportálja az kiexportált metaadatok alapján." + }, + "use_default": "Alapértelmezett használata" + }, + "age": "Kor", + "aliases": "Álnevek", + "all": "mind", + "ascending": "Növekvő", + "average_resolution": "Átlagos Felbontás", + "birth_year": "Születési Év", + "birthdate": "Születési Dátum", + "bitrate": "Bitráta", + "career_length": "Karier Hossza", + "component_tagger": { + "config": { + "blacklist_label": "Tiltólista", + "query_mode_auto": "Automatikus", + "query_mode_dir": "Mappa", + "query_mode_filename": "Fájlnév", + "query_mode_metadata": "Metaadat", + "query_mode_path": "Elérési út", + "source": "Forrás" + }, + "results": { + "duration_unknown": "Ismeretlen hossz", + "unnamed": "Névtelen" + }, + "verb_matched": "Egyezik" + }, + "config": { + "about": { + "latest_version": "Legfrissebb Verzió", + "version": "Verzió" + }, + "categories": { + "logs": "Logok", + "metadata_providers": "Metaadat Szolgáltatók", + "plugins": "Pluginek", + "security": "Biztonság", + "services": "Szolgáltatások", + "system": "Rendszer", + "tasks": "Feladatok", + "tools": "Eszközök" + }, + "dlna": { + "allow_temp_ip": "{tempIP} Engedélyezése", + "allowed_ip_addresses": "Engedélyezett IP-címek", + "default_ip_whitelist": "Alapértelmezett IP Engedélyezőlista", + "disallowed_ip": "Letiltott IP", + "enabled_by_default": "Alapértelmezetten Engedélyezve", + "network_interfaces": "Kezelőfelületek", + "recent_ip_addresses": "Legutóbbi IP-címek", + "server_display_name": "Szerver Megjelenített Neve", + "until_restart": "újraindításig" + }, + "general": { + "auth": { + "api_key": "API Kulcs", + "authentication": "Hitelesítés", + "clear_api_key": "API kulcs törlése", + "generate_api_key": "API kulcs generálása", + "log_file": "Log fájl", + "password": "Jelszó", + "username": "Felhasználónév" + }, + "calculate_md5_and_ohash_label": "MD5 kiszámítása a videókhoz", + "chrome_cdp_path": "Chrome CDP elérési út", + "create_galleries_from_folders_desc": "Igaz esetén galériákat készít a képeket tartalmazó mappákból.", + "create_galleries_from_folders_label": "Galériák készítése a képeket tartalmazó mappákból", + "db_path_head": "Adatbázis Elérési Út", + "image_ext_desc": "Képként értelmezendő fájlkiterjesztések vesszővel elválasztott listája.", + "include_audio_desc": "Hozzáadja a hangsávot a generált bemutatókhoz.", + "include_audio_head": "Hangsáv hozzáadása", + "logging": "Logolás", + "metadata_path": { + "heading": "Metaadatok Elérési Útja" + }, + "preview_generation": "Bemutató Generálás", + "python_path": { + "heading": "Python Elérési Út" + }, + "sqlite_location": "SQLite adatbázis-fájl elérési útja (újraindítás szükséges)", + "video_ext_desc": "Videóként értelmezendő fájlkiterjesztések vesszővel elválasztott listája.", + "video_head": "Videó" + }, + "library": { + "exclusions": "Kivételek", + "gallery_and_image_options": "Galéria és Kép beállítások" + }, + "logs": { + "log_level": "Logolás Szintje" + }, + "scraping": { + "entity_metadata": "{entityType} Metaadatok", + "search_by_name": "Keresés név szerint", + "supported_types": "Támogatott típusok", + "supported_urls": "URL-ek" + }, + "stashbox": { + "api_key": "API kulcs", + "name": "Név" + }, + "tasks": { + "added_job_to_queue": "{operation_name} hozzáadva a feladatlistához", + "backing_up_database": "Adatbázis biztonsági mentése folyamatban", + "backup_and_download": "Biztonsági mentést hajt végre az adatbázison és letölti a fájlt.", + "cleanup_desc": "Ellenőrzi hogy hiányoznak-e fájlok, és eltávolítja őket az adatbázisból. Ez egy visszavonhatatlan művelet.", + "data_management": "Adatkezelés", + "defaults_set": "Az alapértelmezett értékek be lettek állítva, és ezek lesznek használva a Feladatok oldalon a {action} gomb megnyomásakor.", + "dont_include_file_extension_as_part_of_the_title": "Ne csatolja a fájlkiterjesztést a címhez", + "empty_queue": "Jelenleg nem fut feladat.", + "generate_thumbnails_during_scan": "Bélyegképek generálása a képekhez", + "generate_video_previews_during_scan": "Bemutatók generálása", + "generated_content": "Legenerált Tartalom", + "identify": { + "default_options": "Alapértelmezett Beállítások", + "field": "Mező", + "field_behaviour": "{strategy} {field}", + "field_options": "Mező Beállítások", + "heading": "Azonosítás", + "identifying_scenes": "{num} {scene} azonosítása", + "set_cover_images": "Borítoképek beállítása", + "source": "Forrás", + "source_options": "{source} Beállítás", + "sources": "Források", + "strategy": "Stratégia" + }, + "job_queue": "Feladatlista", + "maintenance": "Karbantartás", + "scan": { + "scanning_all_paths": "Összes elérési út szkennelése" + } + }, + "tools": { + "scene_duplicate_checker": "Dupla Jelenet Ellenőrző", + "scene_filename_parser": { + "add_field": "Mező Hozzáadása", + "capitalize_title": "Nagybetűs cím", + "display_fields": "Megjelenített mezők", + "filename": "Fájlnév", + "filename_pattern": "Fájlnév Minta", + "ignored_words": "Figyelmen kívül hagyott szavak" + }, + "scene_tools": "Jelenet Eszközök" + }, + "ui": { + "basic_settings": "Alapvető Beállítások", + "custom_css": { + "description": "Az oldalt újra be kell tölteni hogy a változtatások életbe lépjenek.", + "heading": "Egyéni CSS", + "option_label": "Egyéni CSS engedélyezve" + }, + "delete_options": { + "description": "Alapértelmezett beállítások képek, galériák és jelenetek törlése esetén.", + "heading": "Törlési Beállítások", + "options": { + "delete_file": "Fájl törlése alapértelmezettként", + "delete_generated_supporting_files": "Generált kiegészítő fájlok törlése alapértelmezettként" + } + }, + "desktop_integration": { + "notifications_enabled": "Értesítések Engedélyezése" + }, + "editing": { + "heading": "Szerkesztés" + }, + "handy_connection": { + "connect": "Csatlakozás" + }, + "images": { + "heading": "Képek" + }, + "interactive_options": "Interaktív Beállítások", + "language": { + "heading": "Nyelv" + }, + "preview_type": { + "options": { + "animated": "Mozgókép", + "static": "Állókép", + "video": "Videó" + } + }, + "scene_list": { + "heading": "Jelenetlista" + }, + "scene_player": { + "heading": "Jelenet-lejátszó", + "options": { + "auto_start_video": "Automatikus videóindítás" + } + }, + "scene_wall": { + "options": { + "toggle_sound": "Hang engedélyezése" + } + }, + "title": "Felhasználói Felület" + } + }, + "configuration": "Beállítások", + "country": "Ország", + "cover_image": "Borítókép", + "created_at": "Létrehozva", + "criterion": { + "greater_than": "Nagyobb mint", + "less_than": "Kisebb mint", + "value": "Érték" + }, + "criterion_modifier": { + "between": "között", + "equals": "egyenlő", + "excludes": "kivéve", + "includes": "beleértve", + "includes_all": "mindet belevéve", + "is_null": "egyenlő null", + "matches_regex": "megfelel regex-nek", + "not_between": "nincs közte", + "not_equals": "nem egyenlő" + }, + "custom": "Egyéni", + "date": "Dátum", + "death_date": "Halál Dátuma", + "death_year": "Halál Éve", + "descending": "Csökkenő", + "detail": "Részlet", + "details": "Részletek", + "developmentVersion": "Fejlesztői Verzió", + "dialogs": { + "delete_object_desc": "Biztos hogy törölni akarod {count, plural, one {ezt a {singularEntity}} ezeket a {these {pluralEntity}}}?", + "export_title": "Exportálás", + "lightbox": { + "display_mode": { + "original": "Eredeti" + }, + "options": "Beállítások", + "scroll_mode": { + "zoom": "Nagyítás" + } + }, + "merge_tags": { + "destination": "Cél", + "source": "Forrás" + }, + "scene_gen": { + "preview_exclude_end_time_desc": "Az utolsó x másodperc kihagyása a jelenetek bemutatóiból. Ez az érték lehet másodpercben, vagy a jelenet teljes hosszának százalékában (pl. 2%) is megadva.", + "preview_exclude_start_time_desc": "Az első x másodperc kihagyása a jelenetek bemutatóiból. Ez az érték lehet másodpercben, vagy a jelenet teljes hosszának százalékában (pl. 2%) is megadva." + }, + "scrape_results_existing": "Létező", + "set_image_url_title": "Kép URL" + }, + "director": "Rendező", + "display_mode": { + "grid": "Háló", + "list": "Lista", + "unknown": "Ismeretlen", + "wall": "Fal" + }, + "donate": "Adomány", + "dupe_check": { + "description": "'Pontos' alatti szintek kiszámítása tovább tarthat. Hibás találatok is megjelenhetnek alacsonyabb pontossági szinteken.", + "options": { + "exact": "Pontos", + "high": "Magas", + "low": "Alacsony", + "medium": "Közepes" + }, + "search_accuracy_label": "Keresési Pontosság", + "title": "Megkettőzött Jelenetek" + }, + "duplicated_phash": "Megkettőzőtt (phash)", + "duration": "Hossz", + "effect_filters": { + "blue": "Kék", + "blur": "Elmosás", + "brightness": "Fényerő", + "contrast": "Kontraszt", + "gamma": "Gamma", + "green": "Zöld", + "hue": "Árnyalat", + "name": "Szűrők", + "red": "Piros", + "reset_filters": "Szűrők Törlése", + "rotate": "Forgat", + "saturation": "Szaturáció", + "scale": "Méretarány", + "warmth": "Melegség" + }, + "ethnicity": "Etnikum", + "eye_color": "Szemszín", + "fake_tits": "Szilikonmellek", + "false": "Hamis", + "favourite": "Kedvenc", + "file": "fájl", + "file_info": "Fájl Információ", + "files": "fájlok", + "filesize": "Fájl Méret", + "filter": "Szűrő", + "filters": "Szűrők", + "galleries": "Galériák", + "gallery": "Galéria", + "gallery_count": "Galéria Száma", + "gender": "Nem", + "gender_types": { + "FEMALE": "Nő", + "INTERSEX": "Nemek közti", + "MALE": "Férfi" + }, + "hair_color": "Hajszín", + "handy_connection_status": { + "connecting": "Kapcsolódás", + "disconnected": "Szétkapcsolt", + "missing": "Hiányzó", + "ready": "Kész", + "uploading": "Szkript feltöltése" + }, + "height": "Magasság", + "help": "Segítség", + "image": "Kép", + "image_count": "Képek Száma", + "images": "Képek", + "instagram": "Instagram", + "interactive": "Interaktív", + "interactive_speed": "Interaktív sebesség", + "isMissing": "Hiányzik", + "library": "Könyvtár", + "loading": { + "generic": "Betöltés…" + }, + "media_info": { + "downloaded_from": "Letöltés Forrása", + "interactive_speed": "Interaktív sebesség", + "performer_card": { + "age": "{age} {years_old}" + } + }, + "metadata": "Metaadatok", + "movie": "Film", + "movies": "Filmek", + "name": "Név", + "new": "Új", + "none": "Nincs", + "o_counter": "O-Számláló", + "operations": "Műveletek", + "organized": "Rendezve", + "pagination": { + "first": "Első", + "last": "Utolsó", + "next": "Következő", + "previous": "Előző" + }, + "parent_tags": "Szülő-címkék", + "path": "Elérési Út", + "performer": "Szereplő", + "performerTags": "Szereplő Címkék", + "performer_age": "Szereplő Kora", + "performer_count": "Szereplők Száma", + "performer_favorite": "Szereplő Kedvencek Közt", + "performer_image": "Szereplő Képe", + "performer_tagger": { + "config": { + "excluded_fields": "Kihagyott mezők:" + }, + "current_page": "Jelenlegi oldal", + "network_error": "Hálózati Hiba", + "tag_status": "Címke Státusza", + "update_performer": "Szereplő Frissítése", + "update_performers": "Szereplők Frissítése" + }, + "performers": "Szereplők", + "piercings": "Piercingek", + "queue": "Sor", + "random": "Véletlenszerű", + "rating": "Értékelés", + "resolution": "Felbontás", + "scene": "Jelenet", + "sceneTagger": "Jelenetcímkéző", + "sceneTags": "Jelenetcímkék", + "scene_count": "Jelenetszám", + "scene_id": "Jelenet ID", + "scenes": "Jelenetek", + "search_filter": { + "add_filter": "Szűrő Hozzáadása", + "name": "Szűrő", + "saved_filters": "Mentett szűrők", + "update_filter": "Szűrő Frissítése" + }, + "seconds": "Másodperc", + "settings": "Beállítások", + "setup": { + "confirm": { + "nearly_there": "Már majdnem kész!" + }, + "errors": { + "something_went_wrong_while_setting_up_your_system": "Valami hiba történt a rendszer beállításakor. Itt a hibaüzenet: {error}" + }, + "folder": { + "file_path": "Fájl elérési út" + }, + "migrate": { + "backup_recommended": "Ajánlott az adatbázis biztonsági mentése az áttelepítés előtt.Meg tudjuk ezt tenni neked az adatbázis átmásolásával a {defaultBackupPath} címre.", + "migrating_database": "Adatbázis áttelepítése", + "migration_failed": "Sikertelen áttelepítés", + "migration_irreversible_warning": "A séma áttelepítése nem visszafordítható folyamat. Amint az áttelepítés elkezdődik, az adatbázis összeegyeztethetetlen lesz a Stash előző verzióival.", + "migration_required": "Áttelepítés szükséges", + "schema_too_old": "A jelenlegi Stash adatbázis verziója {databaseSchema} , amit át kell telepíteni {appSchema} verzióra. A Stash ezen verziója nem fog működni az adatbázis áttelepítése nélkül." + }, + "success": { + "help_links": "Ha problémába ütközöl, kérdésed, vagy javaslatod van, nyugodtan jelezd {githubLink}, vagy kérdezd meg a közösségtől {discordLink}.", + "support_us": "Támogatás" + }, + "welcome": { + "config_path_logic_explained": "A Stash elöszőr a jelenlegi munkakönyvtárban próbálja a konfigurációs fájlját (config.yml) megkeresni. Ha ott nem találja, akkor a következő helyen próbálkozik: $HOME/.stash/config.yml (Windows rendszeren: %USERPROFILE%\\.stash\\config.yml). Meghatározhatja hogy a Stash egy bizonyos konfigurációs fájlt használjon, ammennyiben a -c or --config paraméterekkel indítja.", + "unexpected_explained": "Amennyiben ez a képernyő váratlanul bukkant fel, próbáld újraindítani a Stasht a megfelelő munkakönyvtárban, vagy a -c flag-gel." + }, + "welcome_specific_config": { + "unable_to_locate_specified_config": "Ha ezt olvasod, akkor a Stash nem találta meg a konfigurációs fájlt, amit megadtál a parancssorban. Ez a varázsló végigvezet a lépéseken, hogy új konfigurációs fájlt tudj beállítani." + } + }, + "stashbox": { + "submission_failed": "Beküldés sikertelen", + "submission_successful": "Beküldés sikeres" + }, + "statistics": "Statisztikák", + "stats": { + "image_size": "Képek mérete", + "scenes_duration": "Jelenetek hossza", + "scenes_size": "Jelenetek mérete" + }, + "status": "Státusz: {statusText}", + "studio": "Stúdió", + "studios": "Stúdiók", + "tag": "Címke", + "tag_count": "Címkék Száma", + "tags": "Címkék", + "tattoos": "Tetoválások", + "title": "Cím", + "toast": { + "added_entity": "{entity} Hozzáadva", + "created_entity": "{entity} Létrehozva", + "merged_tags": "Összevont címkék", + "saved_entity": "{entity} Mentve", + "updated_entity": "{entity} Frissítve" + }, + "total": "Összesen", + "true": "Igaz", + "twitter": "Twitter", + "updated_at": "Frissítés Ideje", + "url": "URL", + "videos": "Videók", + "view_all": "Mindegyik Megjelenítése", + "weight": "Súly", + "years_old": "éves" +} diff --git a/ui/v2.5/src/locales/it-IT.json b/ui/v2.5/src/locales/it-IT.json index 3dc92a1f7..4b1013406 100644 --- a/ui/v2.5/src/locales/it-IT.json +++ b/ui/v2.5/src/locales/it-IT.json @@ -100,6 +100,7 @@ "stop": "Stop", "submit": "Invia", "submit_stash_box": "Invia a Stash-Box", + "submit_update": "Invia aggiornamento", "tasks": { "clean_confirm_message": "Sei sicuro di voler Pulire? Questa azione cancellerà informazioni e contenuto creato dal database per tutte le scene e gallerie che non si trovano più nel file system.", "dry_mode_selected": "Dry Mode selezionato. Nessuna cancellazione avverrà, solo log.", @@ -966,7 +967,8 @@ "go_review_draft": "Vai al {endpoint_name} per revisionare la bozza.", "selected_stash_box": "Endpoint Stash-Box selezionato", "submission_failed": "Invio fallito", - "submission_successful": "Invio riuscito" + "submission_successful": "Invio riuscito", + "submit_update": "Già esistente in {endpoint_name}" }, "statistics": "Statistiche", "stats": { diff --git a/ui/v2.5/src/locales/pl-PL.json b/ui/v2.5/src/locales/pl-PL.json index 8c2a4fe70..2d93260ba 100644 --- a/ui/v2.5/src/locales/pl-PL.json +++ b/ui/v2.5/src/locales/pl-PL.json @@ -100,6 +100,7 @@ "stop": "Zatrzymaj", "submit": "Wyślij", "submit_stash_box": "Wyślij do Stash-Box", + "submit_update": "Prześlij aktualizację", "tasks": { "clean_confirm_message": "Czy na pewno chcesz przeprowadzić oczyszczanie? Spowoduje to usunięcie informacji o bazie danych i wygenerowanej zawartości dla wszystkich scen i galerii, które nie znajdują się już w systemie plików.", "dry_mode_selected": "Wybrano tryb próby na sucho. Nie nastąpi faktyczne usunięcie, a jedynie zapisanie w dzienniku.", @@ -966,7 +967,8 @@ "go_review_draft": "Przejdź do {endpoint_name}, aby zapoznać się z projektem.", "selected_stash_box": "Wybrany punkt końcowy Stash-Box", "submission_failed": "Zgłoszenie nie powiodło się", - "submission_successful": "Zgłoszenie przesłano sukcesem" + "submission_successful": "Zgłoszenie przesłano sukcesem", + "submit_update": "Już istnieje w {endpoint_name}" }, "statistics": "Statystyki", "stats": { diff --git a/ui/v2.5/src/locales/pt-BR.json b/ui/v2.5/src/locales/pt-BR.json index 6ce5551f7..3246f3e9a 100644 --- a/ui/v2.5/src/locales/pt-BR.json +++ b/ui/v2.5/src/locales/pt-BR.json @@ -7,7 +7,7 @@ "allow": "Permitir", "allow_temporarily": "Permitir temporariamente", "apply": "Aplicar", - "auto_tag": "Auto tag", + "auto_tag": "Etiquetamento automático", "backup": "Backup", "browse_for_image": "Navegar imagens…", "cancel": "Cancelar", @@ -28,10 +28,12 @@ "delete_file": "Apagar arquivo", "delete_file_and_funscript": "Deletar arquivo (e funscript)", "delete_generated_supporting_files": "Apagar arquivos gerados de suporte", + "delete_stashid": "Deletar StashID", "disallow": "Não permitir", "download": "Download", "download_backup": "Download backup", "edit": "Editar", + "edit_entity": "Editar {entityType}", "export": "Exportar…", "export_all": "Exportar tudo…", "find": "Encontrar", @@ -49,12 +51,14 @@ "identify": "Identificar", "ignore": "Ignorar", "import": "Importar…", - "import_from_file": "Importar do arquivo", + "import_from_file": "Importar de arquivo", + "logout": "Sair", "merge": "Unir", "merge_from": "Unir do", "merge_into": "Unir em", "next_action": "Próximo", "not_running": "não realizado", + "open_in_external_player": "Abrir em um reprodutor externo", "open_random": "Abrir aleatório", "overwrite": "Sobrescrever", "play_random": "Tocar aleatório", @@ -65,6 +69,7 @@ "reload_plugins": "Recarregar plugins", "reload_scrapers": "Recarregar scrapers", "remove": "Remover", + "remove_from_gallery": "Remover da galeria", "rename_gen_files": "Renomear arquivos gerados", "rescan": "Reescanear", "reshuffle": "Reembaralhar", @@ -79,9 +84,10 @@ "scrape_with": "Scrape com…", "search": "Buscar", "select_all": "Selecionar todos", + "select_entity": "Selecionar {entityType}", "select_folders": "Selecionar pastas", "select_none": "Selecionar nenhum", - "selective_auto_tag": "Auto Tag seletivo", + "selective_auto_tag": "Etiquetamento automático seletivo", "selective_clean": "Limpeza Seletiva", "selective_scan": "Escaneamento seletivo", "set_as_default": "Aplicar como padrão", @@ -94,6 +100,7 @@ "stop": "Parar", "submit": "Enviar", "submit_stash_box": "Enviar para o Stash-Box", + "submit_update": "Enviar atualização", "tasks": { "clean_confirm_message": "Tem certeza de que quer limpar? Isto irá apagar as informações do banco de dados e conteúdos gerados de todas as cenas e galerias que não são mais encontradas no sistema.", "dry_mode_selected": "Modo não destrutivo. Nenhum arquivo será apagado, apenas será logado.", @@ -101,6 +108,7 @@ }, "temp_disable": "Desabilitar temporariamente…", "temp_enable": "Habilitar temporariamente…", + "unset": "Desaplicar", "use_default": "Usar padrão", "view_random": "Mostrar aleatoriamente" }, @@ -114,6 +122,7 @@ "birth_year": "Ano de nascimento", "birthdate": "Data de nascimento", "bitrate": "Taxa de bits", + "captions": "Legendas", "career_length": "Duração da carreira", "component_tagger": { "config": { @@ -133,23 +142,23 @@ "query_mode_path_desc": "Usa o caminho inteiro do arquivo", "set_cover_desc": "Substitua a capa da cena se alguma for encontrada.", "set_cover_label": "Definir imagem da capa da cena", - "set_tag_desc": "Anexar tags à cena, sobrescrevendo ou mesclando com as tags existentes na cena.", - "set_tag_label": "Definir tags", - "show_male_desc": "Artistas masculinos estarão disponíveis para tag.", + "set_tag_desc": "Anexar etiquetas à cena, sobrescrevendo ou mesclando com as etiquetas existentes na cena.", + "set_tag_label": "Definir etiquetas", + "show_male_desc": "Define se artistas masculinos estarão disponíveis para etiquetar.", "show_male_label": "Mostrar artistas masculinos", "source": "Fonte" }, "noun_query": "Query", "results": { - "duration_off": "Duração por pelo menos {number}s", + "duration_off": "Duração fora por pelo menos {number}s", "duration_unknown": "Duração desconhecida", "fp_found": "{fpCount, plural, =0 {Nenhuma nova correspondência de impressão digital encontrada} other{# novas correspondências de impressão digital encontradas}}", "fp_matches": "Duração é uma correspondência", "fp_matches_multi": "Duração corresponde {matchCount}/{durationsLength} impressão digital(s)", "hash_matches": "{hash_type} é uma correspondência", - "match_failed_already_tagged": "Cena já marcada", + "match_failed_already_tagged": "Cena já etiquetada", "match_failed_no_result": "Nenhum resultado encontrado", - "match_success": "Cena marcada com sucesso", + "match_success": "Cena etiquetada com sucesso", "phash_matches": "{count} PHashes coincide(m)", "unnamed": "Sem nome" }, @@ -193,14 +202,19 @@ "dlna": { "allow_temp_ip": "Permitir {tempIP}", "allowed_ip_addresses": "Endereços de IP permitidos", - "default_ip_whitelist": "Whitelist de IP padrão", + "allowed_ip_temporarily": "IP permitido temporariamente", + "default_ip_whitelist": "Lista branca de IP padrão", "default_ip_whitelist_desc": "Endereços IP padrão permitidos a acessar DLNA. Use {wildcard} para permitir todos endereços de IP.", + "disabled_dlna_temporarily": "DLNA desativado temporariamente", + "disallowed_ip": "IP não permitido", "enabled_by_default": "Ativado por padrão", + "enabled_dlna_temporarily": "DLNA ativado temporariamente", "network_interfaces": "Interfaces", "network_interfaces_desc": "Interfaces para expor servidor DLNA ativo. Uma lista vazia resulta em execução em todas as interfaces. Requer DLNA ser reiniciado depois de alterar.", "recent_ip_addresses": "Endereços de IP recentes", "server_display_name": "Nome de exibição do servidor", "server_display_name_desc": "Nome de exibição do servidor DLNA. Padrão de {server_name} se vazio.", + "successfully_cancelled_temporary_behaviour": "Comportamento temporário cancelado com sucesso", "until_restart": "até reiniciar" }, "general": { @@ -256,18 +270,22 @@ "include_audio_desc": "Inclui stream de áudio quando gerar pré-visualizações.", "include_audio_head": "Incluir áudio", "logging": "Logging", - "maximum_streaming_transcode_size_desc": "Tamanho máximo para streams transcodados", + "maximum_streaming_transcode_size_desc": "Tamanho máximo para streams transcodificadas", "maximum_streaming_transcode_size_head": "Tamanho máximo de transcodação de streaming", - "maximum_transcode_size_desc": "Tamanho máximo para transcodes gerados", + "maximum_transcode_size_desc": "Tamanho máximo para transcodificações geradas", "maximum_transcode_size_head": "Tamanho máximo de transcodificação", "metadata_path": { "description": "Localização do diretório usado durante importação ou exportação completa dos meta-dados", - "heading": "Caminho dos Meta-dados" + "heading": "Caminho dos Metadados" }, "number_of_parallel_task_for_scan_generation_desc": "Defina como 0 para detecção automática. AVISO Execução de mais tarefas do que é necessário para obter 100% de utilização da CPU diminuirá o desempenho e potencialmente causar outros problemas.", "number_of_parallel_task_for_scan_generation_head": "Número de tarefas paralelas para varredura/geração", - "parallel_scan_head": "Varredura/Geração paralela", - "preview_generation": "Pré visualizar a geração", + "parallel_scan_head": "Escaneamento/Geração paralela", + "preview_generation": "Geração de pré-visualização", + "python_path": { + "description": "Caminho do executável do Python. Utilizado para scripts de scrape e plugins. Se em branco, o caminho do Python será resolvido a partir do ambiente", + "heading": "Caminho do Python" + }, "scraper_user_agent": "Scraper User Agent", "scraper_user_agent_desc": "User-Agent string usado durante solicitações http do scrape", "scrapers_path": { @@ -286,7 +304,7 @@ "media_content_extensions": "Extensões de arquivo de mídia" }, "logs": { - "log_level": "Log Level" + "log_level": "Nível de log" }, "plugins": { "hooks": "Hooks", @@ -295,8 +313,8 @@ "scraping": { "entity_metadata": "{entityType} metadados", "entity_scrapers": "Scrapers de {entityType}", - "excluded_tag_patterns_desc": "Expressões regulares de tags para excluir dos resultados da busca", - "excluded_tag_patterns_head": "Padrões de Tag Excluídos", + "excluded_tag_patterns_desc": "Expressões regulares de etiquetas para excluir dos resultados da busca", + "excluded_tag_patterns_head": "Padrões de etiqueta excluídos", "scraper": "Scraper", "scrapers": "Scrapers", "search_by_name": "Buscar por nome", @@ -306,7 +324,7 @@ "stashbox": { "add_instance": "Adicionar uma instância stash-box", "api_key": "Chave de API", - "description": "Stash-box facilita o tagging automático de cenas e artistas baseados em 'impressões digitais' e nomes de arquivos..\nEndpoint e chave de API pode ser encontrado na sua página de conta na instancia stash-box. Os nomes são necessários quando mais de uma instância são adicionados.", + "description": "Stash-box facilita o etiquetamento automático de cenas e artistas baseados em 'impressões digitais' e nomes de arquivos.\nEndpoint e chave de API pode ser encontrado na sua página de conta na instancia stash-box. Os nomes são necessários quando mais de uma instância são adicionados.", "endpoint": "Endpoint", "graphql_endpoint": "Endpoint GraphQL", "name": "Nome", @@ -318,11 +336,11 @@ "tasks": { "added_job_to_queue": "{operation_name} adicionada para a fila de trabalho", "auto_tag": { - "auto_tagging_all_paths": "Adicionar tags automaticamente em todos os caminhos", - "auto_tagging_paths": "Adicionar tags automaticamente nos seguintes caminhos" + "auto_tagging_all_paths": "Etiquetar automaticamente todos os caminhos", + "auto_tagging_paths": "Etiquetar automaticamente os seguintes caminhos" }, - "auto_tag_based_on_filenames": "Conteúdo automático de tag baseado em nomes de arquivos.", - "auto_tagging": "Auto tagging", + "auto_tag_based_on_filenames": "Etiquetar automaticamente conteúdo baseado em nomes de arquivos.", + "auto_tagging": "Etiquetamento automático", "backing_up_database": "Backup do banco de dados", "backup_and_download": "Executa um backup do banco de dados e baixa do arquivo resultante.", "backup_database": "Executa um backup do banco de dados para o mesmo diretório que o banco de dados, com o formato do nome do arquivo {filename_format}", @@ -433,6 +451,13 @@ "description": "Compensação de tempo em milissegundos para a reprodução de scripts interativos.", "heading": "Compensação de tempo Funscript (ms)" }, + "handy_connection": { + "connect": "Conectar", + "status": { + "heading": "Estado da conexão do Handy" + }, + "sync": "Sincronizar" + }, "handy_connection_key": { "description": "Chave de conexão para usar em cenas interativas. Ativar esta chave permitirá o Stash a compartilhar as informações da cena atual com handyfeeling.com", "heading": "Chave de conexão" @@ -499,13 +524,17 @@ "scene_wall": { "heading": "Muro de cenas/marcadores", "options": { - "display_title": "Exibir título e tags", + "display_title": "Exibir título e etiquetas", "toggle_sound": "Habilitar som" } }, + "scroll_attempts_before_change": { + "description": "Número de vezes para tentar rolar antes de passar para o próximo/prévio item. Só se aplica ao modo de rolagem Movimentar Y.", + "heading": "Tentativas de rolagem antes da transição" + }, "slideshow_delay": { "description": "Slideshow está disponível em galerias quando no modo de exibição de paredão", - "heading": "Atraso do slideshow" + "heading": "Atraso do slideshow (segundos)" }, "title": "Interface de usuário" } @@ -520,7 +549,7 @@ "performers": "{count, plural, one {Artista} other {Artistas}}", "scenes": "{count, plural, one {Cena} other {Cenas}}", "studios": "{count, plural, one {Estúdio} other {Estúdios}}", - "tags": "{count, plural, one {Tag} other {Tags}}" + "tags": "{count, plural, one {Etiqueta} other {Etiquetas}}" }, "country": "País", "cover_image": "Imagem de capa", @@ -580,12 +609,12 @@ "reset_zoom_on_nav": "Restaurar nível de zoom ao trocar de imagem", "scale_up": { "description": "Aumentar imagens menores até que preencham a tela", - "label": "Aumentar para caber" + "label": "Aumentar até caber" }, "scroll_mode": { "description": "Mantenha shift pressionado para usar outro modo temporariamente.", - "label": "Modo scroll", - "pan_y": "Pan Y", + "label": "Modo de rolagem", + "pan_y": "Movimentar Y", "zoom": "Zoom" } }, @@ -606,6 +635,8 @@ "marker_screenshots_tooltip": "Imagens JPG estáticas para marcadores, necessário apenas se o Tipo de Pré-visualização estiver configurado para Imagem Estática.", "markers": "Pré-visualizações de Marcadores", "markers_tooltip": "Vídeos de 20 segundos que iniciam em dado código de tempo.", + "override_preview_generation_options": "Sobrepor as opções da geração de pré-visualização", + "override_preview_generation_options_desc": "Sobrepor as opções da geração de pré-visualização. Os padrões são definidos em Sistema -> Geração de pré-visualização.", "overwrite": "Substituir arquivos gerados existentes", "phash": "Hashes perceptivos (para desduplicação)", "preview_exclude_end_time_desc": "Excluir os últimos x segundos de pré-visualizações de cena. Isso pode ser um valor em segundos, ou uma porcentagem (p. ex. 2%) da duração total da cena.", @@ -638,9 +669,9 @@ "dimensions": "Dimensões", "director": "Diretor(a)", "display_mode": { - "grid": "Grid", + "grid": "Grade", "list": "Lista", - "tagger": "Tagger", + "tagger": "Etiquetador", "unknown": "Desconhecido(a)", "wall": "Paredão" }, @@ -662,7 +693,7 @@ "effect_filters": { "aspect": "Aspecto", "blue": "Azul", - "blur": "Blur", + "blur": "Borrão", "brightness": "Brilho", "contrast": "Contraste", "gamma": "Gama", @@ -680,7 +711,9 @@ "scale": "Escala", "warmth": "Calor" }, + "empty_server": "Adicione algumas cenas ao seu servidor para ver as recomendações nesta página.", "ethnicity": "Etnicidade", + "existing_value": "valor existente", "eye_color": "Cor dos olhos", "fake_tits": "Peitos falsos", "false": "Falso", @@ -708,15 +741,25 @@ "TRANSGENDER_MALE": "Transgênero Masculino" }, "hair_color": "Cor do cabelo", + "handy_connection_status": { + "connecting": "Conectando", + "disconnected": "Desconectado", + "error": "Erro conectando ao Handy", + "missing": "Faltando", + "ready": "Pronto", + "syncing": "Sincronizando com o servidor", + "uploading": "Enviando script" + }, "hasMarkers": "Possui marcadores", "height": "Altura", "help": "Ajuda", + "ignore_auto_tag": "Ignorar etiquetamento automático", "image": "Imagem", "image_count": "Contagem de imagem", "images": "Imagens", - "include_parent_tags": "Incluir tags pai", + "include_parent_tags": "Incluir etiquetas pai", "include_sub_studios": "Incluem estúdios filho", - "include_sub_tags": "Incluir sub-tags", + "include_sub_tags": "Incluir sub-etiquetas", "instagram": "Instagram", "interactive": "Interativo", "interactive_speed": "Velocidade interativa", @@ -761,13 +804,13 @@ }, "parent_of": "Pai de {children}", "parent_studios": "Estúdios pai", - "parent_tag_count": "Contador de tags pai", - "parent_tags": "Tags pai", + "parent_tag_count": "Contador de etiquetas pai", + "parent_tags": "Etiquetas pai", "part_of": "Parte de {parent}", "path": "Caminho", "perceptual_similarity": "Semelhança Perceptiva (phash)", "performer": "Artista", - "performerTags": "Tags de artitas", + "performerTags": "Etiquetas de artistas", "performer_age": "Idade do Artista", "performer_count": "Contagem de artistas", "performer_favorite": "Artista Favoritado", @@ -788,34 +831,39 @@ "current_page": "Página atual", "failed_to_save_performer": "Falha ao salvar artista \"{performer}\"", "name_already_exists": "Nome já existe", - "network_error": "Erro de Rede", + "network_error": "Erro de rede", "no_results_found": "Nenhum resultado encontrado.", "number_of_performers_will_be_processed": "{performer_count} artistas serão processados", - "performer_already_tagged": "Artista já taggeado", + "performer_already_tagged": "Artista já etiquetado", "performer_names_separated_by_comma": "Nomes de artistas separados por vírgula", "performer_selection": "Seleção de artista", - "performer_successfully_tagged": "Artista taggeado com sucesso:", + "performer_successfully_tagged": "Artista etiquetado com sucesso:", "query_all_performers_in_the_database": "Todos os artistas no banco de dados", - "refresh_tagged_performers": "Recarregar artistas taggeados", - "refreshing_will_update_the_data": "Recarregar irá atualizar os dados de qualquer artista taggeado da instância do stash-box.", - "status_tagging_job_queued": "Status: Taggeamento adicionado à fila", - "status_tagging_performers": "Status: Taggeando artistas", - "tag_status": "Status da Tag", - "to_use_the_performer_tagger": "Para usar o tagger de artistas, uma instância do stash-box deve ser configurada.", - "untagged_performers": "Artistas sem tag", + "refresh_tagged_performers": "Recarregar artistas etiquetados", + "refreshing_will_update_the_data": "Recarregar irá atualizar os dados de qualquer artista etiquetado da instância do stash-box.", + "status_tagging_job_queued": "Status: Etiquetamento adicionado à fila", + "status_tagging_performers": "Status: Etiquetando artistas", + "tag_status": "Status da etiqueta", + "to_use_the_performer_tagger": "Para usar o etiquetador de artistas, uma instância do stash-box deve ser configurada.", + "untagged_performers": "Artistas sem etiqueta", "update_performer": "Atualizar Artista", "update_performers": "Atualizar Artistas", - "updating_untagged_performers_description": "A atualização de artistas sem tag tentará corresponder a qualquer artista sem um shashid e atualizar os metadados." + "updating_untagged_performers_description": "A atualização de artistas sem etiqueta tentará corresponder a qualquer artista sem um stashid e atualizar os metadados." }, "performers": "Artistas", "piercings": "Piercings", "queue": "Fila", "random": "Aleatória", "rating": "Avaliação", + "recently_added_performers": "Artistas recentemente adicionados", + "recently_added_studios": "Estúdios recentemente adicionados", + "recently_released_galleries": "Galerias recentemente lançadas", + "recently_released_movies": "Filmes recentemente lançados", + "recently_released_scenes": "Cenas recentemente lançadas", "resolution": "Resolução", "scene": "Cena", - "sceneTagger": "Tagger de cena", - "sceneTags": "Tags de cena", + "sceneTagger": "Etiquetador de cena", + "sceneTags": "Etiquetas da cena", "scene_count": "Contagem de cena", "scene_id": "Cena ID", "scenes": "Cenas", @@ -848,6 +896,10 @@ "something_went_wrong_description": "Se isso parece um problema com os dados fornecidos, clique no em Voltar para corrigi-los. Caso contrário, reporte um bug em {githubLink} ou busque ajuda em {discordLink}.", "something_went_wrong_while_setting_up_your_system": "Algo deu errado enquanto configurávamos seu sistema. Aqui está o erro que recebemos: {error}" }, + "folder": { + "file_path": "Caminho do arquivo", + "up_dir": "Subir um diretório" + }, "github_repository": "Repositório do Github", "migrate": { "backup_database_path_leave_empty_to_disable_backup": "Caminho para o backup do banco de dados (deixe em branco para desabilitar o backup):", @@ -901,10 +953,7 @@ "next_step": "Quando estiver pronto para prosseguir com a criação do novo sistema, clique Próximo.", "unable_to_locate_specified_config": "Se está lendo isto, então o Stash não pôde encontrar o arquivo de configuração especificado na linha de comando ou no ambiente. Este assistente irá te guiar durante o processo de criação de uma nova configuração." }, - "welcome_to_stash": "Bem-vindo ao Stash", - "folder": { - "up_dir": "Subir um diretório" - } + "welcome_to_stash": "Bem-vindo ao Stash" }, "stash_id": "Stash ID", "stash_ids": "Stash IDs", @@ -912,8 +961,10 @@ "go_review_draft": "Vá para {endpoint_name} para revisar rascunho.", "selected_stash_box": "Endpoint do Stash-Box selecionado", "submission_failed": "Falha no envio", - "submission_successful": "Envio bem-sucedido" + "submission_successful": "Envio bem-sucedido", + "submit_update": "Já existe no {endpoint_name}" }, + "statistics": "Estatísticas", "stats": { "image_size": "Tamanho das imagens", "scenes_duration": "Duração das cenas", @@ -923,14 +974,14 @@ "studio": "Estúdio", "studio_depth": "Níveis (vazio para todos)", "studios": "Estúdios", - "sub_tag_count": "Número de sub-tags", - "sub_tag_of": "Sub-tags de {parent}", - "sub_tags": "Sub-tags", + "sub_tag_count": "Contagem de sub-etiquetas", + "sub_tag_of": "Sub-etiqueta de {parent}", + "sub_tags": "Sub-etiquetas", "subsidiary_studios": "Estúdios filhos", "synopsis": "Sinopse", - "tag": "Tag", - "tag_count": "Número de tags", - "tags": "Tags", + "tag": "Etiqueta", + "tag_count": "Contagem de etiquetas", + "tags": "Etiquetas", "tattoos": "Tatuagens", "title": "Título", "toast": { @@ -941,10 +992,10 @@ "delete_entity": "Excluir {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "delete_past_tense": "Excluída {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "generating_screenshot": "Gerando captura de tela…", - "merged_tags": "Tags mescladas", + "merged_tags": "Etiquetas mescladas", "rescanning_entity": "Reescaneando {count, plural, one {{singularEntity}} other {{pluralEntity}}}…", "saved_entity": "{entity} salvo(a)", - "started_auto_tagging": "Auto tagging iniciado", + "started_auto_tagging": "Etiquetamento automático iniciado", "started_generating": "Geração de arquivos multimídia iniciada", "started_importing": "Importação iniciada", "updated_entity": "{entity} atualizado(a)" @@ -955,6 +1006,7 @@ "updated_at": "Atualizado em", "url": "URL", "videos": "Vídeos", + "view_all": "Ver todos", "weight": "Peso", "years_old": "anos" } diff --git a/ui/v2.5/src/locales/zh-TW.json b/ui/v2.5/src/locales/zh-TW.json index c2d937683..0a784868e 100644 --- a/ui/v2.5/src/locales/zh-TW.json +++ b/ui/v2.5/src/locales/zh-TW.json @@ -100,6 +100,7 @@ "stop": "停止", "submit": "提交", "submit_stash_box": "提交至 Stash-Box", + "submit_update": "提交更新", "tasks": { "clean_confirm_message": "您確定要進行清理嗎?這將從資料庫及產生的文件中清除已不在的短片及圖庫。", "dry_mode_selected": "已選擇了模擬作業模式。不會進行任何實際刪除作業,只會進行模擬記錄。", @@ -121,6 +122,7 @@ "birth_year": "出生年分", "birthdate": "出生日期", "bitrate": "位元率", + "captions": "字幕", "career_length": "活躍年代", "component_tagger": { "config": { @@ -449,6 +451,16 @@ "description": "互動式腳本的時間偏移量 (毫秒)。", "heading": "Funscript 偏移量 (毫秒)" }, + "handy_connection": { + "connect": "連接", + "server_offset": { + "heading": "伺服器誤差值" + }, + "status": { + "heading": "Handy 連線狀態" + }, + "sync": "同步" + }, "handy_connection_key": { "description": "播放支援互動性的短片時所用的 Handy 連線金鑰。設定此金鑰後,Stash 將可把當前短片中的對應資訊分享至 handyfeeling.com", "heading": "Handy 連線金鑰" @@ -522,6 +534,10 @@ "toggle_sound": "播放聲音" } }, + "scroll_attempts_before_change": { + "description": "在移動到下一項/上一項之前嘗試滑動的次數。僅適用於『Y軸滑動』模式。", + "heading": "場景變換滑動嘗試次數" + }, "slideshow_delay": { "description": "幻燈片功能僅適用於「圖庫」種類下的預覽牆模式", "heading": "幻燈片延遲 (秒)" @@ -701,6 +717,7 @@ "scale": "大小", "warmth": "暖度" }, + "empty_server": "若要啟用影片推薦,請先於伺服器中新增一些短片。", "ethnicity": "人種", "existing_value": "現有值", "eye_color": "眼睛顏色", @@ -730,6 +747,15 @@ "TRANSGENDER_MALE": "跨型別男性" }, "hair_color": "頭髮顏色", + "handy_connection_status": { + "connecting": "連接中", + "disconnected": "已斷線", + "error": "連接至 Handy 時出錯", + "missing": "遺失", + "ready": "已準備", + "syncing": "與伺服器同步中", + "uploading": "上傳腳本中" + }, "hasMarkers": "含有章節標記", "height": "身高", "help": "說明", @@ -835,6 +861,11 @@ "queue": "佇列", "random": "隨機", "rating": "評比", + "recently_added_performers": "最近新增的演員", + "recently_added_studios": "最近新增的工作室", + "recently_released_galleries": "最近釋出的圖庫", + "recently_released_movies": "最近釋出的電影", + "recently_released_scenes": "最近釋出的短片", "resolution": "解析度", "scene": "短片", "sceneTagger": "短片標籤器", @@ -936,8 +967,10 @@ "go_review_draft": "到 {endpoint_name} 預覽草稿。", "selected_stash_box": "已選擇的 Stash-Box 端點", "submission_failed": "提交失敗", - "submission_successful": "提交成功" + "submission_successful": "提交成功", + "submit_update": "已存在於 {endpoint_name}" }, + "statistics": "統計資訊", "stats": { "image_size": "圖片大小", "scenes_duration": "短片長度", @@ -979,6 +1012,7 @@ "updated_at": "更新於", "url": "連結", "videos": "影片", + "view_all": "檢視所有", "weight": "體重", "years_old": "歲" } From 3d52bad9bdb0b409a3db24ce52e257bd84205dbd Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 8 Jun 2022 08:58:25 +1000 Subject: [PATCH 10/34] Add missing gallery card classes (#2654) --- ui/v2.5/src/components/Galleries/GalleryCard.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ui/v2.5/src/components/Galleries/GalleryCard.tsx b/ui/v2.5/src/components/Galleries/GalleryCard.tsx index 933278b8b..fcc381309 100644 --- a/ui/v2.5/src/components/Galleries/GalleryCard.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryCard.tsx @@ -167,12 +167,16 @@ export const GalleryCard: React.FC = (props) => { } overlays={maybeRenderSceneStudioOverlay()} details={ - <> - {props.gallery.date} +
+ {props.gallery.date}

- +

- +
} popovers={maybeRenderPopoverButtonGroup()} selected={props.selected} From 5e455d6530db575ba1be7ccb1451dbb816d31045 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 8 Jun 2022 08:58:42 +1000 Subject: [PATCH 11/34] Add alias for Laos (#2655) --- ui/v2.5/src/utils/country.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/v2.5/src/utils/country.ts b/ui/v2.5/src/utils/country.ts index d9961a388..68a087c10 100644 --- a/ui/v2.5/src/utils/country.ts +++ b/ui/v2.5/src/utils/country.ts @@ -15,6 +15,7 @@ const fuzzyDict: Record = { "Slovak Republic": "SK", Iran: "IR", Moldova: "MD", + Laos: "LA", }; const getISOCountry = (country: string | null | undefined) => { From 456e9409e0e10662a37f78e890e829d74560e928 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 8 Jun 2022 08:58:59 +1000 Subject: [PATCH 12/34] Update scene screenshot in edit panel (#2657) --- .../src/components/Scenes/SceneDetails/SceneEditPanel.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx index f6f2e7781..48887aa13 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx @@ -77,7 +77,11 @@ export const SceneEditPanel: React.FC = ({ const [coverImagePreview, setCoverImagePreview] = useState< string | undefined - >(scene.paths.screenshot ?? undefined); + >(); + + useEffect(() => { + setCoverImagePreview(scene.paths.screenshot ?? undefined); + }, [scene.paths.screenshot]); const { configuration: stashConfig } = React.useContext(ConfigurationContext); From ff724d82ccd994d1304b8f6e6de11c3275d3b2fa Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 8 Jun 2022 09:02:11 +1000 Subject: [PATCH 13/34] Don't trim extension for folders when auto-tagging galleries (#2658) --- internal/autotag/gallery.go | 14 ++++++---- internal/autotag/scene_test.go | 1 + internal/autotag/tagger.go | 15 +++++----- pkg/match/path.go | 24 ++++++++-------- pkg/scraper/autotag.go | 28 +++++++++++-------- .../components/Changelog/versions/v0160.md | 2 ++ 6 files changed, 49 insertions(+), 35 deletions(-) diff --git a/internal/autotag/gallery.go b/internal/autotag/gallery.go index 603e3e36a..3bdfd3c15 100644 --- a/internal/autotag/gallery.go +++ b/internal/autotag/gallery.go @@ -7,12 +7,16 @@ import ( ) func getGalleryFileTagger(s *models.Gallery, cache *match.Cache) tagger { + // only trim the extension if gallery is file-based + trimExt := s.Zip + return tagger{ - ID: s.ID, - Type: "gallery", - Name: s.GetTitle(), - Path: s.Path.String, - cache: cache, + ID: s.ID, + Type: "gallery", + Name: s.GetTitle(), + Path: s.Path.String, + trimExt: trimExt, + cache: cache, } } diff --git a/internal/autotag/scene_test.go b/internal/autotag/scene_test.go index 190b16b8e..578b9e7f6 100644 --- a/internal/autotag/scene_test.go +++ b/internal/autotag/scene_test.go @@ -34,6 +34,7 @@ func generateNamePatterns(name, separator, ext string) []string { ret = append(ret, fmt.Sprintf("aaa%s%s.%s", separator, name, ext)) ret = append(ret, fmt.Sprintf("aaa%s%s%sbbb.%s", separator, name, separator, ext)) ret = append(ret, fmt.Sprintf("dir/%s%saaa.%s", name, separator, ext)) + ret = append(ret, fmt.Sprintf("dir%sdir/%s%saaa.%s", separator, name, separator, ext)) ret = append(ret, fmt.Sprintf("dir\\%s%saaa.%s", name, separator, ext)) ret = append(ret, fmt.Sprintf("%s%saaa/dir/bbb.%s", name, separator, ext)) ret = append(ret, fmt.Sprintf("%s%saaa\\dir\\bbb.%s", name, separator, ext)) diff --git a/internal/autotag/tagger.go b/internal/autotag/tagger.go index 624d29f5a..4ea1fbc01 100644 --- a/internal/autotag/tagger.go +++ b/internal/autotag/tagger.go @@ -22,10 +22,11 @@ import ( ) type tagger struct { - ID int - Type string - Name string - Path string + ID int + Type string + Name string + Path string + trimExt bool cache *match.Cache } @@ -41,7 +42,7 @@ func (t *tagger) addLog(otherType, otherName string) { } func (t *tagger) tagPerformers(performerReader models.PerformerReader, addFunc addLinkFunc) error { - others, err := match.PathToPerformers(t.Path, performerReader, t.cache) + others, err := match.PathToPerformers(t.Path, performerReader, t.cache, t.trimExt) if err != nil { return err } @@ -62,7 +63,7 @@ func (t *tagger) tagPerformers(performerReader models.PerformerReader, addFunc a } func (t *tagger) tagStudios(studioReader models.StudioReader, addFunc addLinkFunc) error { - studio, err := match.PathToStudio(t.Path, studioReader, t.cache) + studio, err := match.PathToStudio(t.Path, studioReader, t.cache, t.trimExt) if err != nil { return err } @@ -83,7 +84,7 @@ func (t *tagger) tagStudios(studioReader models.StudioReader, addFunc addLinkFun } func (t *tagger) tagTags(tagReader models.TagReader, addFunc addLinkFunc) error { - others, err := match.PathToTags(t.Path, tagReader, t.cache) + others, err := match.PathToTags(t.Path, tagReader, t.cache, t.trimExt) if err != nil { return err } diff --git a/pkg/match/path.go b/pkg/match/path.go index 47a7ad26e..4f20423dd 100644 --- a/pkg/match/path.go +++ b/pkg/match/path.go @@ -37,13 +37,15 @@ func getPathQueryRegex(name string) string { return ret } -func getPathWords(path string) []string { +func getPathWords(path string, trimExt bool) []string { retStr := path - // remove the extension - ext := filepath.Ext(retStr) - if ext != "" { - retStr = strings.TrimSuffix(retStr, ext) + if trimExt { + // remove the extension + ext := filepath.Ext(retStr) + if ext != "" { + retStr = strings.TrimSuffix(retStr, ext) + } } // handle path separators @@ -136,8 +138,8 @@ func getPerformers(words []string, performerReader models.PerformerReader, cache return append(performers, swPerformers...), nil } -func PathToPerformers(path string, reader models.PerformerReader, cache *Cache) ([]*models.Performer, error) { - words := getPathWords(path) +func PathToPerformers(path string, reader models.PerformerReader, cache *Cache, trimExt bool) ([]*models.Performer, error) { + words := getPathWords(path, trimExt) performers, err := getPerformers(words, reader, cache) if err != nil { @@ -172,8 +174,8 @@ func getStudios(words []string, reader models.StudioReader, cache *Cache) ([]*mo // PathToStudio returns the Studio that matches the given path. // Where multiple matching studios are found, the one that matches the latest // position in the path is returned. -func PathToStudio(path string, reader models.StudioReader, cache *Cache) (*models.Studio, error) { - words := getPathWords(path) +func PathToStudio(path string, reader models.StudioReader, cache *Cache, trimExt bool) (*models.Studio, error) { + words := getPathWords(path, trimExt) candidates, err := getStudios(words, reader, cache) if err != nil { @@ -220,8 +222,8 @@ func getTags(words []string, reader models.TagReader, cache *Cache) ([]*models.T return append(tags, swTags...), nil } -func PathToTags(path string, reader models.TagReader, cache *Cache) ([]*models.Tag, error) { - words := getPathWords(path) +func PathToTags(path string, reader models.TagReader, cache *Cache, trimExt bool) ([]*models.Tag, error) { + words := getPathWords(path, trimExt) tags, err := getTags(words, reader, cache) if err != nil { diff --git a/pkg/scraper/autotag.go b/pkg/scraper/autotag.go index 20940fce2..4a86d8df2 100644 --- a/pkg/scraper/autotag.go +++ b/pkg/scraper/autotag.go @@ -21,8 +21,8 @@ type autotagScraper struct { globalConfig GlobalConfig } -func autotagMatchPerformers(path string, performerReader models.PerformerReader) ([]*models.ScrapedPerformer, error) { - p, err := match.PathToPerformers(path, performerReader, nil) +func autotagMatchPerformers(path string, performerReader models.PerformerReader, trimExt bool) ([]*models.ScrapedPerformer, error) { + p, err := match.PathToPerformers(path, performerReader, nil, trimExt) if err != nil { return nil, fmt.Errorf("error matching performers: %w", err) } @@ -45,8 +45,8 @@ func autotagMatchPerformers(path string, performerReader models.PerformerReader) return ret, nil } -func autotagMatchStudio(path string, studioReader models.StudioReader) (*models.ScrapedStudio, error) { - studio, err := match.PathToStudio(path, studioReader, nil) +func autotagMatchStudio(path string, studioReader models.StudioReader, trimExt bool) (*models.ScrapedStudio, error) { + studio, err := match.PathToStudio(path, studioReader, nil, trimExt) if err != nil { return nil, fmt.Errorf("error matching studios: %w", err) } @@ -62,8 +62,8 @@ func autotagMatchStudio(path string, studioReader models.StudioReader) (*models. return nil, nil } -func autotagMatchTags(path string, tagReader models.TagReader) ([]*models.ScrapedTag, error) { - t, err := match.PathToTags(path, tagReader, nil) +func autotagMatchTags(path string, tagReader models.TagReader, trimExt bool) ([]*models.ScrapedTag, error) { + t, err := match.PathToTags(path, tagReader, nil, trimExt) if err != nil { return nil, fmt.Errorf("error matching tags: %w", err) } @@ -85,20 +85,21 @@ func autotagMatchTags(path string, tagReader models.TagReader) ([]*models.Scrape func (s autotagScraper) viaScene(ctx context.Context, _client *http.Client, scene *models.Scene) (*models.ScrapedScene, error) { var ret *models.ScrapedScene + const trimExt = false // populate performers, studio and tags based on scene path if err := s.txnManager.WithReadTxn(ctx, func(r models.ReaderRepository) error { path := scene.Path - performers, err := autotagMatchPerformers(path, r.Performer()) + performers, err := autotagMatchPerformers(path, r.Performer(), trimExt) if err != nil { return fmt.Errorf("autotag scraper viaScene: %w", err) } - studio, err := autotagMatchStudio(path, r.Studio()) + studio, err := autotagMatchStudio(path, r.Studio(), trimExt) if err != nil { return fmt.Errorf("autotag scraper viaScene: %w", err) } - tags, err := autotagMatchTags(path, r.Tag()) + tags, err := autotagMatchTags(path, r.Tag(), trimExt) if err != nil { return fmt.Errorf("autotag scraper viaScene: %w", err) } @@ -125,21 +126,24 @@ func (s autotagScraper) viaGallery(ctx context.Context, _client *http.Client, ga return nil, nil } + // only trim extension if gallery is file-based + trimExt := gallery.Zip + var ret *models.ScrapedGallery // populate performers, studio and tags based on scene path if err := s.txnManager.WithReadTxn(ctx, func(r models.ReaderRepository) error { path := gallery.Path.String - performers, err := autotagMatchPerformers(path, r.Performer()) + performers, err := autotagMatchPerformers(path, r.Performer(), trimExt) if err != nil { return fmt.Errorf("autotag scraper viaGallery: %w", err) } - studio, err := autotagMatchStudio(path, r.Studio()) + studio, err := autotagMatchStudio(path, r.Studio(), trimExt) if err != nil { return fmt.Errorf("autotag scraper viaGallery: %w", err) } - tags, err := autotagMatchTags(path, r.Tag()) + tags, err := autotagMatchTags(path, r.Tag(), trimExt) if err != nil { return fmt.Errorf("autotag scraper viaGallery: %w", err) } diff --git a/ui/v2.5/src/components/Changelog/versions/v0160.md b/ui/v2.5/src/components/Changelog/versions/v0160.md index 61a4dd282..bb0ed9efd 100644 --- a/ui/v2.5/src/components/Changelog/versions/v0160.md +++ b/ui/v2.5/src/components/Changelog/versions/v0160.md @@ -2,4 +2,6 @@ * Support submitting stash-box scene updates for scenes with stash ids. ([#2577](https://github.com/stashapp/stash/pull/2577)) ### 🐛 Bug fixes +* Fix folder-based galleries not auto-tagging correctly if folder name contains `.` characters. ([#2658](https://github.com/stashapp/stash/pull/2658)) +* Fix scene cover in scene edit panel not being updated when changing scenes. ([#2657](https://github.com/stashapp/stash/pull/2657)) * Fix moved gallery zip files not being rescanned. ([#2611](https://github.com/stashapp/stash/pull/2611)) \ No newline at end of file From 9264c1554073e111254206933510bb40bc796df2 Mon Sep 17 00:00:00 2001 From: CJ <72030708+Teda1@users.noreply.github.com> Date: Mon, 13 Jun 2022 19:34:04 -0500 Subject: [PATCH 14/34] Customize recommendations (#2592) * refactored common code in recommendation row * Implement front page options in config * Allow customisation from front page * Rename recommendations to front page * Add generic UI settings * Support adding premade filters Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com> --- go.mod | 2 +- graphql/documents/data/config.graphql | 1 + graphql/documents/mutations/config.graphql | 4 + graphql/documents/queries/filter.graphql | 8 +- graphql/schema/schema.graphql | 8 +- graphql/schema/types/config.graphql | 1 + graphql/schema/types/scalars.graphql | 7 +- internal/api/resolver_mutation_configure.go | 20 + internal/api/resolver_query_configuration.go | 5 + .../api/resolver_query_find_saved_filter.go | 24 +- internal/manager/config/config.go | 22 + internal/manager/config/map.go | 86 ++++ internal/manager/config/map_test.go | 82 ++++ pkg/models/mocks/SavedFilterReaderWriter.go | 46 ++ pkg/models/mocks/SceneReaderWriter.go | 9 +- pkg/models/saved_filter.go | 2 + pkg/sqlite/saved_filter.go | 27 ++ ui/v2.5/src/App.tsx | 4 +- .../components/Changelog/versions/v0160.md | 3 +- ui/v2.5/src/components/FrontPage/Control.tsx | 168 ++++++++ .../src/components/FrontPage/FrontPage.tsx | 82 ++++ .../components/FrontPage/FrontPageConfig.tsx | 407 ++++++++++++++++++ .../FrontPage/RecommendationRow.tsx | 24 ++ .../styles.scss | 121 +++++- .../Galleries/GalleryRecommendationRow.tsx | 47 +- ui/v2.5/src/components/Images/ImageCard.tsx | 4 +- .../Images/ImageRecommendationRow.tsx | 52 +++ .../Movies/MovieRecommendationRow.tsx | 53 ++- .../Performers/PerformerRecommendationRow.tsx | 47 +- .../Recommendations/Recommendations.tsx | 174 -------- .../Scenes/SceneRecommendationRow.tsx | 66 +-- ui/v2.5/src/components/Settings/context.tsx | 66 ++- .../Studios/StudioRecommendationRow.tsx | 47 +- ui/v2.5/src/core/StashService.ts | 15 +- ui/v2.5/src/core/config.ts | 88 ++++ ui/v2.5/src/index.scss | 2 +- ui/v2.5/src/locales/en-GB.json | 15 +- ui/v2.5/src/utils/data.ts | 2 +- 38 files changed, 1549 insertions(+), 292 deletions(-) create mode 100644 internal/manager/config/map.go create mode 100644 internal/manager/config/map_test.go create mode 100644 ui/v2.5/src/components/FrontPage/Control.tsx create mode 100644 ui/v2.5/src/components/FrontPage/FrontPage.tsx create mode 100644 ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx create mode 100644 ui/v2.5/src/components/FrontPage/RecommendationRow.tsx rename ui/v2.5/src/components/{Recommendations => FrontPage}/styles.scss (72%) create mode 100644 ui/v2.5/src/components/Images/ImageRecommendationRow.tsx delete mode 100644 ui/v2.5/src/components/Recommendations/Recommendations.tsx create mode 100644 ui/v2.5/src/core/config.ts diff --git a/go.mod b/go.mod index b225d3150..d6ccb07f6 100644 --- a/go.mod +++ b/go.mod @@ -53,6 +53,7 @@ require ( github.com/kermieisinthehouse/gosx-notifier v0.1.1 github.com/kermieisinthehouse/systray v1.2.4 github.com/lucasb-eyer/go-colorful v1.2.0 + github.com/spf13/cast v1.4.1 github.com/vearutop/statigz v1.1.6 github.com/vektah/gqlparser/v2 v2.4.1 ) @@ -90,7 +91,6 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/zerolog v1.26.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/spf13/cast v1.4.1 // indirect github.com/spf13/cobra v1.4.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/stretchr/objx v0.2.0 // indirect diff --git a/graphql/documents/data/config.graphql b/graphql/documents/data/config.graphql index 9e7f853fe..58ba6d7a9 100644 --- a/graphql/documents/data/config.graphql +++ b/graphql/documents/data/config.graphql @@ -185,4 +185,5 @@ fragment ConfigData on ConfigResult { defaults { ...ConfigDefaultSettingsData } + ui } diff --git a/graphql/documents/mutations/config.graphql b/graphql/documents/mutations/config.graphql index fff7dbeca..dfd53ed75 100644 --- a/graphql/documents/mutations/config.graphql +++ b/graphql/documents/mutations/config.graphql @@ -36,6 +36,10 @@ mutation ConfigureDefaults($input: ConfigDefaultSettingsInput!) { } } +mutation ConfigureUI($input: Map!) { + configureUI(input: $input) +} + mutation GenerateAPIKey($input: GenerateAPIKeyInput!) { generateAPIKey(input: $input) } diff --git a/graphql/documents/queries/filter.graphql b/graphql/documents/queries/filter.graphql index 2c022fde7..67fbaf6cf 100644 --- a/graphql/documents/queries/filter.graphql +++ b/graphql/documents/queries/filter.graphql @@ -1,4 +1,10 @@ -query FindSavedFilters($mode: FilterMode!) { +query FindSavedFilter($id: ID!) { + findSavedFilter(id: $id) { + ...SavedFilterData + } +} + +query FindSavedFilters($mode: FilterMode) { findSavedFilters(mode: $mode) { ...SavedFilterData } diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index b81168a9a..7229dce1d 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -1,7 +1,8 @@ """The query root for this schema""" type Query { # Filters - findSavedFilters(mode: FilterMode!): [SavedFilter!]! + findSavedFilter(id: ID!): SavedFilter + findSavedFilters(mode: FilterMode): [SavedFilter!]! findDefaultFilter(mode: FilterMode!): SavedFilter """Find a scene by ID or Checksum""" @@ -238,6 +239,11 @@ type Mutation { configureScraping(input: ConfigScrapingInput!): ConfigScrapingResult! configureDefaults(input: ConfigDefaultSettingsInput!): ConfigDefaultSettingsResult! + # overwrites the entire UI configuration + configureUI(input: Map!): Map! + # sets a single UI key value + configureUISetting(key: String!, value: Any): Map! + """Generate and set (or clear) API key""" generateAPIKey(input: GenerateAPIKeyInput!): String! diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 281f133c4..9a84f0cc6 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -413,6 +413,7 @@ type ConfigResult { dlna: ConfigDLNAResult! scraping: ConfigScrapingResult! defaults: ConfigDefaultSettingsResult! + ui: Map! } """Directory structure of a path""" diff --git a/graphql/schema/types/scalars.graphql b/graphql/schema/types/scalars.graphql index 439f0d561..f973887a5 100644 --- a/graphql/schema/types/scalars.graphql +++ b/graphql/schema/types/scalars.graphql @@ -4,4 +4,9 @@ Timestamp is a point in time. It is always output as RFC3339-compatible time poi It can be input as a RFC3339 string, or as "<4h" for "4 hours in the past" or ">5m" for "5 minutes in the future" """ -scalar Timestamp \ No newline at end of file +scalar Timestamp + +# generic JSON object +scalar Map + +scalar Any \ No newline at end of file diff --git a/internal/api/resolver_mutation_configure.go b/internal/api/resolver_mutation_configure.go index 906378ca5..7413c413b 100644 --- a/internal/api/resolver_mutation_configure.go +++ b/internal/api/resolver_mutation_configure.go @@ -501,3 +501,23 @@ func (r *mutationResolver) GenerateAPIKey(ctx context.Context, input models.Gene return newAPIKey, nil } + +func (r *mutationResolver) ConfigureUI(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) { + c := config.GetInstance() + c.SetUIConfiguration(input) + + if err := c.Write(); err != nil { + return c.GetUIConfiguration(), err + } + + return c.GetUIConfiguration(), nil +} + +func (r *mutationResolver) ConfigureUISetting(ctx context.Context, key string, value interface{}) (map[string]interface{}, error) { + c := config.GetInstance() + + cfg := c.GetUIConfiguration() + cfg[key] = value + + return r.ConfigureUI(ctx, cfg) +} diff --git a/internal/api/resolver_query_configuration.go b/internal/api/resolver_query_configuration.go index ad0a2c142..d0852ff13 100644 --- a/internal/api/resolver_query_configuration.go +++ b/internal/api/resolver_query_configuration.go @@ -66,6 +66,7 @@ func makeConfigResult() *models.ConfigResult { Dlna: makeConfigDLNAResult(), Scraping: makeConfigScrapingResult(), Defaults: makeConfigDefaultsResult(), + UI: makeConfigUIResult(), } } @@ -216,6 +217,10 @@ func makeConfigDefaultsResult() *models.ConfigDefaultSettingsResult { } } +func makeConfigUIResult() map[string]interface{} { + return config.GetInstance().GetUIConfiguration() +} + func (r *queryResolver) ValidateStashBoxCredentials(ctx context.Context, input models.StashBoxInput) (*models.StashBoxValidationResult, error) { client := stashbox.NewClient(models.StashBox{Endpoint: input.Endpoint, APIKey: input.APIKey}, r.txnManager) user, err := client.GetUser(ctx) diff --git a/internal/api/resolver_query_find_saved_filter.go b/internal/api/resolver_query_find_saved_filter.go index d79697701..a28ef2f59 100644 --- a/internal/api/resolver_query_find_saved_filter.go +++ b/internal/api/resolver_query_find_saved_filter.go @@ -2,13 +2,33 @@ package api import ( "context" + "strconv" "github.com/stashapp/stash/pkg/models" ) -func (r *queryResolver) FindSavedFilters(ctx context.Context, mode models.FilterMode) (ret []*models.SavedFilter, err error) { +func (r *queryResolver) FindSavedFilter(ctx context.Context, id string) (ret *models.SavedFilter, err error) { + idInt, err := strconv.Atoi(id) + if err != nil { + return nil, err + } + if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { - ret, err = repo.SavedFilter().FindByMode(mode) + ret, err = repo.SavedFilter().Find(idInt) + return err + }); err != nil { + return nil, err + } + return ret, err +} + +func (r *queryResolver) FindSavedFilters(ctx context.Context, mode *models.FilterMode) (ret []*models.SavedFilter, err error) { + if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { + if mode != nil { + ret, err = repo.SavedFilter().FindByMode(*mode) + } else { + ret, err = repo.SavedFilter().All() + } return err }); err != nil { return nil, err diff --git a/internal/manager/config/config.go b/internal/manager/config/config.go index 1dbfa1a62..71847608e 100644 --- a/internal/manager/config/config.go +++ b/internal/manager/config/config.go @@ -153,6 +153,8 @@ const ( ImageLightboxScrollMode = "image_lightbox.scroll_mode" ImageLightboxScrollAttemptsBeforeChange = "image_lightbox.scroll_attempts_before_change" + UI = "ui" + defaultImageLightboxSlideshowDelay = 5000 DisableDropdownCreatePerformer = "disable_dropdown_create.performer" @@ -971,6 +973,26 @@ func (i *Instance) GetDisableDropdownCreate() *models.ConfigDisableDropdownCreat } } +func (i *Instance) GetUIConfiguration() map[string]interface{} { + i.RLock() + defer i.RUnlock() + + // HACK: viper changes map keys to case insensitive values, so the workaround is to + // convert map keys to snake case for storage + v := i.viper(UI).GetStringMap(UI) + + return fromSnakeCaseMap(v) +} + +func (i *Instance) SetUIConfiguration(v map[string]interface{}) { + i.RLock() + defer i.RUnlock() + + // HACK: viper changes map keys to case insensitive values, so the workaround is to + // convert map keys to snake case for storage + i.viper(UI).Set(UI, toSnakeCaseMap(v)) +} + func (i *Instance) GetCSSPath() string { // use custom.css in the same directory as the config file configFileUsed := i.GetConfigFile() diff --git a/internal/manager/config/map.go b/internal/manager/config/map.go new file mode 100644 index 000000000..d4944fe5f --- /dev/null +++ b/internal/manager/config/map.go @@ -0,0 +1,86 @@ +package config + +import ( + "bytes" + "unicode" + + "github.com/spf13/cast" +) + +// HACK: viper changes map keys to case insensitive values, so the workaround is to +// convert the map to use snake-case keys + +// toSnakeCase converts a string to snake_case +// NOTE: a double capital will be converted in a way that will yield a different result +// when converted back to camel case. +// For example: someIDs => some_ids => someIds +func toSnakeCase(v string) string { + var buf bytes.Buffer + underscored := false + for i, c := range v { + if !underscored && unicode.IsUpper(c) && i > 0 { + buf.WriteByte('_') + underscored = true + } else { + underscored = false + } + + buf.WriteRune(unicode.ToLower(c)) + } + return buf.String() +} + +func fromSnakeCase(v string) string { + var buf bytes.Buffer + cap := false + for i, c := range v { + switch { + case c == '_' && i > 0: + cap = true + case cap: + buf.WriteRune(unicode.ToUpper(c)) + cap = false + default: + buf.WriteRune(c) + } + } + return buf.String() +} + +// copyAndInsensitiviseMap behaves like insensitiviseMap, but creates a copy of +// any map it makes case insensitive. +func toSnakeCaseMap(m map[string]interface{}) map[string]interface{} { + nm := make(map[string]interface{}) + + for key, val := range m { + adjKey := toSnakeCase(key) + switch v := val.(type) { + case map[interface{}]interface{}: + nm[adjKey] = toSnakeCaseMap(cast.ToStringMap(v)) + case map[string]interface{}: + nm[adjKey] = toSnakeCaseMap(v) + default: + nm[adjKey] = v + } + } + + return nm +} + +func fromSnakeCaseMap(m map[string]interface{}) map[string]interface{} { + nm := make(map[string]interface{}) + + for key, val := range m { + adjKey := fromSnakeCase(key) + switch v := val.(type) { + case map[interface{}]interface{}: + nm[adjKey] = fromSnakeCaseMap(cast.ToStringMap(v)) + case map[string]interface{}: + nm[adjKey] = fromSnakeCaseMap(v) + default: + nm[adjKey] = v + } + } + + return nm +} diff --git a/internal/manager/config/map_test.go b/internal/manager/config/map_test.go new file mode 100644 index 000000000..3c7da15b2 --- /dev/null +++ b/internal/manager/config/map_test.go @@ -0,0 +1,82 @@ +package config + +import ( + "testing" +) + +func Test_toSnakeCase(t *testing.T) { + tests := []struct { + name string + v string + want string + }{ + { + "basic", + "basic", + "basic", + }, + { + "two words", + "twoWords", + "two_words", + }, + { + "three word value", + "threeWordValue", + "three_word_value", + }, + { + "snake case", + "snake_case", + "snake_case", + }, + { + "double capital", + "doubleCApital", + "double_capital", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := toSnakeCase(tt.v); got != tt.want { + t.Errorf("toSnakeCase() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_fromSnakeCase(t *testing.T) { + tests := []struct { + name string + v string + want string + }{ + { + "basic", + "basic", + "basic", + }, + { + "two words", + "two_words", + "twoWords", + }, + { + "three word value", + "three_word_value", + "threeWordValue", + }, + { + "camel case", + "camelCase", + "camelCase", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := fromSnakeCase(tt.v); got != tt.want { + t.Errorf("fromSnakeCase() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/models/mocks/SavedFilterReaderWriter.go b/pkg/models/mocks/SavedFilterReaderWriter.go index 987fdd5fc..952497be2 100644 --- a/pkg/models/mocks/SavedFilterReaderWriter.go +++ b/pkg/models/mocks/SavedFilterReaderWriter.go @@ -12,6 +12,29 @@ type SavedFilterReaderWriter struct { mock.Mock } +// All provides a mock function with given fields: +func (_m *SavedFilterReaderWriter) All() ([]*models.SavedFilter, error) { + ret := _m.Called() + + var r0 []*models.SavedFilter + if rf, ok := ret.Get(0).(func() []*models.SavedFilter); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.SavedFilter) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Create provides a mock function with given fields: obj func (_m *SavedFilterReaderWriter) Create(obj models.SavedFilter) (*models.SavedFilter, error) { ret := _m.Called(obj) @@ -118,6 +141,29 @@ func (_m *SavedFilterReaderWriter) FindDefault(mode models.FilterMode) (*models. return r0, r1 } +// FindMany provides a mock function with given fields: ids, ignoreNotFound +func (_m *SavedFilterReaderWriter) FindMany(ids []int, ignoreNotFound bool) ([]*models.SavedFilter, error) { + ret := _m.Called(ids, ignoreNotFound) + + var r0 []*models.SavedFilter + if rf, ok := ret.Get(0).(func([]int, bool) []*models.SavedFilter); ok { + r0 = rf(ids, ignoreNotFound) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.SavedFilter) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]int, bool) error); ok { + r1 = rf(ids, ignoreNotFound) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // SetDefault provides a mock function with given fields: obj func (_m *SavedFilterReaderWriter) SetDefault(obj models.SavedFilter) (*models.SavedFilter, error) { ret := _m.Called(obj) diff --git a/pkg/models/mocks/SceneReaderWriter.go b/pkg/models/mocks/SceneReaderWriter.go index a200087ea..0635fd200 100644 --- a/pkg/models/mocks/SceneReaderWriter.go +++ b/pkg/models/mocks/SceneReaderWriter.go @@ -482,6 +482,7 @@ func (_m *SceneReaderWriter) FindMany(ids []int) ([]*models.Scene, error) { return r0, r1 } +// GetCaptions provides a mock function with given fields: sceneID func (_m *SceneReaderWriter) GetCaptions(sceneID int) ([]*models.SceneCaption, error) { ret := _m.Called(sceneID) @@ -751,13 +752,13 @@ func (_m *SceneReaderWriter) Update(updatedScene models.ScenePartial) (*models.S return r0, r1 } -// UpdateCaptions provides a mock function with given fields: id, newCaptions -func (_m *SceneReaderWriter) UpdateCaptions(sceneID int, captions []*models.SceneCaption) error { - ret := _m.Called(sceneID, captions) +// UpdateCaptions provides a mock function with given fields: id, captions +func (_m *SceneReaderWriter) UpdateCaptions(id int, captions []*models.SceneCaption) error { + ret := _m.Called(id, captions) var r0 error if rf, ok := ret.Get(0).(func(int, []*models.SceneCaption) error); ok { - r0 = rf(sceneID, captions) + r0 = rf(id, captions) } else { r0 = ret.Error(0) } diff --git a/pkg/models/saved_filter.go b/pkg/models/saved_filter.go index e455d92c4..e6cd2f8e0 100644 --- a/pkg/models/saved_filter.go +++ b/pkg/models/saved_filter.go @@ -1,7 +1,9 @@ package models type SavedFilterReader interface { + All() ([]*SavedFilter, error) Find(id int) (*SavedFilter, error) + FindMany(ids []int, ignoreNotFound bool) ([]*SavedFilter, error) FindByMode(mode FilterMode) ([]*SavedFilter, error) FindDefault(mode FilterMode) (*SavedFilter, error) } diff --git a/pkg/sqlite/saved_filter.go b/pkg/sqlite/saved_filter.go index 8630a14a7..6c507bee3 100644 --- a/pkg/sqlite/saved_filter.go +++ b/pkg/sqlite/saved_filter.go @@ -81,6 +81,24 @@ func (qb *savedFilterQueryBuilder) Find(id int) (*models.SavedFilter, error) { return &ret, nil } +func (qb *savedFilterQueryBuilder) FindMany(ids []int, ignoreNotFound bool) ([]*models.SavedFilter, error) { + var filters []*models.SavedFilter + for _, id := range ids { + filter, err := qb.Find(id) + if err != nil { + return nil, err + } + + if filter == nil && !ignoreNotFound { + return nil, fmt.Errorf("filter with id %d not found", id) + } + + filters = append(filters, filter) + } + + return filters, nil +} + func (qb *savedFilterQueryBuilder) FindByMode(mode models.FilterMode) ([]*models.SavedFilter, error) { // exclude empty-named filters - these are the internal default filters @@ -108,3 +126,12 @@ func (qb *savedFilterQueryBuilder) FindDefault(mode models.FilterMode) (*models. return nil, nil } + +func (qb *savedFilterQueryBuilder) All() ([]*models.SavedFilter, error) { + var ret models.SavedFilters + if err := qb.query(selectAll(savedFilterTable), nil, &ret); err != nil { + return nil, err + } + + return []*models.SavedFilter(ret), nil +} diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index aa28ecac4..7a3abdd2a 100755 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -19,7 +19,7 @@ import Galleries from "./components/Galleries/Galleries"; import { MainNavbar } from "./components/MainNavbar"; import { PageNotFound } from "./components/PageNotFound"; import Performers from "./components/Performers/Performers"; -import Recommendations from "./components/Recommendations/Recommendations"; +import FrontPage from "./components/FrontPage/FrontPage"; import Scenes from "./components/Scenes/Scenes"; import { Settings } from "./components/Settings/Settings"; import { Stats } from "./components/Stats"; @@ -119,7 +119,7 @@ export const App: React.FC = () => { return ( - + diff --git a/ui/v2.5/src/components/Changelog/versions/v0160.md b/ui/v2.5/src/components/Changelog/versions/v0160.md index bb0ed9efd..c8616373b 100644 --- a/ui/v2.5/src/components/Changelog/versions/v0160.md +++ b/ui/v2.5/src/components/Changelog/versions/v0160.md @@ -1,7 +1,8 @@ ### ✨ New Features * Support submitting stash-box scene updates for scenes with stash ids. ([#2577](https://github.com/stashapp/stash/pull/2577)) +* Added support for customizing recommendations on home page. ([#2592](https://github.com/stashapp/stash/pull/2592)) ### 🐛 Bug fixes * Fix folder-based galleries not auto-tagging correctly if folder name contains `.` characters. ([#2658](https://github.com/stashapp/stash/pull/2658)) * Fix scene cover in scene edit panel not being updated when changing scenes. ([#2657](https://github.com/stashapp/stash/pull/2657)) -* Fix moved gallery zip files not being rescanned. ([#2611](https://github.com/stashapp/stash/pull/2611)) \ No newline at end of file +* Fix moved gallery zip files not being rescanned. ([#2611](https://github.com/stashapp/stash/pull/2611)) diff --git a/ui/v2.5/src/components/FrontPage/Control.tsx b/ui/v2.5/src/components/FrontPage/Control.tsx new file mode 100644 index 000000000..501bcafe4 --- /dev/null +++ b/ui/v2.5/src/components/FrontPage/Control.tsx @@ -0,0 +1,168 @@ +import React, { useMemo } from "react"; +import { useIntl } from "react-intl"; +import { + FrontPageContent, + ICustomFilter, + ISavedFilterRow, +} from "src/core/config"; +import * as GQL from "src/core/generated-graphql"; +import { useFindSavedFilter } from "src/core/StashService"; +import { ListFilterModel } from "src/models/list-filter/filter"; +import { GalleryRecommendationRow } from "../Galleries/GalleryRecommendationRow"; +import { ImageRecommendationRow } from "../Images/ImageRecommendationRow"; +import { MovieRecommendationRow } from "../Movies/MovieRecommendationRow"; +import { PerformerRecommendationRow } from "../Performers/PerformerRecommendationRow"; +import { SceneRecommendationRow } from "../Scenes/SceneRecommendationRow"; +import { StudioRecommendationRow } from "../Studios/StudioRecommendationRow"; + +interface IFilter { + mode: GQL.FilterMode; + filter: ListFilterModel; + header: string; +} + +const RecommendationRow: React.FC = ({ mode, filter, header }) => { + function isTouchEnabled() { + return "ontouchstart" in window || navigator.maxTouchPoints > 0; + } + + const isTouch = isTouchEnabled(); + + switch (mode) { + case GQL.FilterMode.Scenes: + return ( + + ); + case GQL.FilterMode.Studios: + return ( + + ); + case GQL.FilterMode.Movies: + return ( + + ); + case GQL.FilterMode.Performers: + return ( + + ); + case GQL.FilterMode.Galleries: + return ( + + ); + case GQL.FilterMode.Images: + return ( + + ); + default: + return <>; + } +}; + +interface ISavedFilterResults { + savedFilterID: string; +} + +const SavedFilterResults: React.FC = ({ + savedFilterID, +}) => { + const { loading, data } = useFindSavedFilter(savedFilterID.toString()); + + const filter = useMemo(() => { + if (!data?.findSavedFilter) return; + + const { mode, filter: filterJSON } = data.findSavedFilter; + + const ret = new ListFilterModel(mode); + ret.currentPage = 1; + ret.configureFromQueryParameters(JSON.parse(filterJSON)); + ret.randomSeed = -1; + return ret; + }, [data?.findSavedFilter]); + + if (loading || !data?.findSavedFilter || !filter) { + return <>; + } + + const { name, mode } = data.findSavedFilter; + + return ; +}; + +interface ICustomFilterProps { + customFilter: ICustomFilter; +} + +const CustomFilterResults: React.FC = ({ + customFilter, +}) => { + const intl = useIntl(); + + const filter = useMemo(() => { + const itemsPerPage = 25; + const ret = new ListFilterModel(customFilter.mode); + ret.sortBy = customFilter.sortBy; + ret.sortDirection = customFilter.direction; + ret.itemsPerPage = itemsPerPage; + ret.currentPage = 1; + ret.randomSeed = -1; + return ret; + }, [customFilter]); + + const header = customFilter.message + ? intl.formatMessage( + { id: customFilter.message.id }, + customFilter.message.values + ) + : customFilter.title ?? ""; + + return ( + + ); +}; + +interface IProps { + content: FrontPageContent; +} + +export const Control: React.FC = ({ content }) => { + switch (content.__typename) { + case "SavedFilter": + return ( + + ); + case "CustomFilter": + return ; + default: + return <>; + } +}; diff --git a/ui/v2.5/src/components/FrontPage/FrontPage.tsx b/ui/v2.5/src/components/FrontPage/FrontPage.tsx new file mode 100644 index 000000000..cddebca9d --- /dev/null +++ b/ui/v2.5/src/components/FrontPage/FrontPage.tsx @@ -0,0 +1,82 @@ +import React, { useState } from "react"; +import { FormattedMessage, useIntl } from "react-intl"; +import { useConfigureUI } from "src/core/StashService"; +import { LoadingIndicator } from "src/components/Shared"; +import { Button } from "react-bootstrap"; +import { FrontPageConfig } from "./FrontPageConfig"; +import { useToast } from "src/hooks"; +import { Control } from "./Control"; +import { ConfigurationContext } from "src/hooks/Config"; +import { + FrontPageContent, + generateDefaultFrontPageContent, + IUIConfig, +} from "src/core/config"; + +const FrontPage: React.FC = () => { + const intl = useIntl(); + const Toast = useToast(); + + const [isEditing, setIsEditing] = useState(false); + const [saving, setSaving] = useState(false); + + const [saveUI] = useConfigureUI(); + + const { configuration, loading } = React.useContext(ConfigurationContext); + + async function onUpdateConfig(content?: FrontPageContent[]) { + setIsEditing(false); + + if (!content) { + return; + } + + setSaving(true); + try { + await saveUI({ + variables: { + input: { + frontPageContent: content, + }, + }, + }); + } catch (e) { + Toast.error(e); + } + setSaving(false); + } + + if (loading || saving) { + return ; + } + + if (isEditing) { + return onUpdateConfig(content)} />; + } + + const ui = (configuration?.ui ?? {}) as IUIConfig; + + if (!ui.frontPageContent) { + const defaultContent = generateDefaultFrontPageContent(intl); + onUpdateConfig(defaultContent); + } + + const { frontPageContent } = ui; + + return ( +
+
+ {frontPageContent?.map((content: FrontPageContent, i) => ( + + ))} +
+
+ +
+
+ ); +}; + +export default FrontPage; diff --git a/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx b/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx new file mode 100644 index 000000000..4bbf6a7c0 --- /dev/null +++ b/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx @@ -0,0 +1,407 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { FormattedMessage, IntlShape, useIntl } from "react-intl"; +import { useFindSavedFilters } from "src/core/StashService"; +import { LoadingIndicator } from "src/components/Shared"; +import { Button, Form, Modal } from "react-bootstrap"; +import { + FilterMode, + FindSavedFiltersQuery, + SavedFilter, +} from "src/core/generated-graphql"; +import { ConfigurationContext } from "src/hooks/Config"; +import { + IUIConfig, + ISavedFilterRow, + ICustomFilter, + FrontPageContent, + generatePremadeFrontPageContent, +} from "src/core/config"; + +interface IAddSavedFilterModalProps { + onClose: (content?: FrontPageContent) => void; + existingSavedFilterIDs: string[]; + candidates: FindSavedFiltersQuery; +} + +const FilterModeToMessageID = { + [FilterMode.Galleries]: "galleries", + [FilterMode.Images]: "images", + [FilterMode.Movies]: "movies", + [FilterMode.Performers]: "performers", + [FilterMode.SceneMarkers]: "markers", + [FilterMode.Scenes]: "scenes", + [FilterMode.Studios]: "studios", + [FilterMode.Tags]: "tags", +}; + +function filterTitle(intl: IntlShape, f: Pick) { + return `${intl.formatMessage({ id: FilterModeToMessageID[f.mode] })}: ${ + f.name + }`; +} + +const AddContentModal: React.FC = ({ + onClose, + existingSavedFilterIDs, + candidates, +}) => { + const intl = useIntl(); + + const premadeFilterOptions = useMemo( + () => generatePremadeFrontPageContent(intl), + [intl] + ); + + const [contentType, setContentType] = useState( + "front_page.types.premade_filter" + ); + const [premadeFilterIndex, setPremadeFilterIndex] = useState< + number | undefined + >(0); + const [savedFilter, setSavedFilter] = useState(); + + function onTypeSelected(t: string) { + setContentType(t); + + switch (t) { + case "front_page.types.premade_filter": + setPremadeFilterIndex(0); + setSavedFilter(undefined); + break; + case "front_page.types.saved_filter": + setPremadeFilterIndex(undefined); + setSavedFilter(undefined); + break; + } + } + + function isValid() { + switch (contentType) { + case "front_page.types.premade_filter": + return premadeFilterIndex !== undefined; + case "front_page.types.saved_filter": + return savedFilter !== undefined; + } + + return false; + } + + const savedFilterOptions = useMemo(() => { + const ret = [ + { + value: "", + text: "", + }, + ].concat( + candidates.findSavedFilters + .filter((f) => { + // markers not currently supported + return ( + f.mode !== FilterMode.SceneMarkers && + !existingSavedFilterIDs.includes(f.id) + ); + }) + .map((f) => { + return { + value: f.id, + text: filterTitle(intl, f), + }; + }) + ); + + ret.sort((a, b) => { + return a.text.localeCompare(b.text); + }); + + return ret; + }, [candidates, existingSavedFilterIDs, intl]); + + function renderTypeSelect() { + const options = [ + "front_page.types.premade_filter", + "front_page.types.saved_filter", + ]; + return ( + + + + + onTypeSelected(e.target.value)} + className="btn-secondary" + > + {options.map((c) => ( + + ))} + + + ); + } + + function maybeRenderPremadeFiltersSelect() { + if (contentType !== "front_page.types.premade_filter") return; + + return ( + + + + + setPremadeFilterIndex(parseInt(e.target.value))} + className="btn-secondary" + > + {premadeFilterOptions.map((c, i) => ( + + ))} + + + ); + } + + function maybeRenderSavedFiltersSelect() { + if (contentType !== "front_page.types.saved_filter") return; + return ( + + + + + setSavedFilter(e.target.value)} + className="btn-secondary" + > + {savedFilterOptions.map((c) => ( + + ))} + + + ); + } + + function doAdd() { + switch (contentType) { + case "front_page.types.premade_filter": + onClose(premadeFilterOptions[premadeFilterIndex!]); + return; + case "front_page.types.saved_filter": + onClose({ + __typename: "SavedFilter", + savedFilterId: parseInt(savedFilter!), + }); + return; + } + + onClose(); + } + + return ( + onClose()}> + + + + +
+ {renderTypeSelect()} + {maybeRenderSavedFiltersSelect()} + {maybeRenderPremadeFiltersSelect()} +
+
+ + + + +
+ ); +}; + +interface IFilterRowProps { + content: FrontPageContent; + allSavedFilters: Pick[]; + onDelete: () => void; +} + +const ContentRow: React.FC = (props: IFilterRowProps) => { + const intl = useIntl(); + + function title() { + switch (props.content.__typename) { + case "SavedFilter": + const savedFilter = props.allSavedFilters.find( + (f) => + f.id === (props.content as ISavedFilterRow).savedFilterId.toString() + ); + if (!savedFilter) return ""; + return filterTitle(intl, savedFilter); + case "CustomFilter": + const asCustomFilter = props.content as ICustomFilter; + if (asCustomFilter.message) + return intl.formatMessage( + { id: asCustomFilter.message.id }, + asCustomFilter.message.values + ); + return asCustomFilter.title ?? ""; + } + } + + return ( +
+
+
+

{title()}

+
+ +
+
+ ); +}; + +interface IFrontPageConfigProps { + onClose: (content?: FrontPageContent[]) => void; +} + +export const FrontPageConfig: React.FC = ({ + onClose, +}) => { + const { configuration, loading } = React.useContext(ConfigurationContext); + + const ui = configuration?.ui as IUIConfig; + + const { data: allFilters, loading: loading2 } = useFindSavedFilters(); + + const [isAdd, setIsAdd] = useState(false); + const [currentContent, setCurrentContent] = useState([]); + const [dragIndex, setDragIndex] = useState(); + + useEffect(() => { + if (!allFilters?.findSavedFilters) { + return; + } + + if (ui?.frontPageContent) { + setCurrentContent(ui.frontPageContent); + } + }, [allFilters, ui]); + + function onDragStart(event: React.DragEvent, index: number) { + event.dataTransfer.effectAllowed = "move"; + setDragIndex(index); + } + + function onDragOver(event: React.DragEvent, index?: number) { + if (dragIndex !== undefined && index !== undefined && index !== dragIndex) { + const newFilters = [...currentContent]; + const moved = newFilters.splice(dragIndex, 1); + newFilters.splice(index, 0, moved[0]); + setCurrentContent(newFilters); + setDragIndex(index); + } + + event.dataTransfer.dropEffect = "move"; + event.preventDefault(); + } + + function onDragOverDefault(event: React.DragEvent) { + event.dataTransfer.dropEffect = "move"; + event.preventDefault(); + } + + function onDrop() { + // assume we've already set the temp filter list + // feed it up + setDragIndex(undefined); + } + + if (loading || loading2) { + return ; + } + + const existingSavedFilterIDs = currentContent + .filter((f) => f.__typename === "SavedFilter") + .map((f) => (f as ISavedFilterRow).savedFilterId.toString()); + + function addSavedFilter(content?: FrontPageContent) { + setIsAdd(false); + + if (!content) { + return; + } + + setCurrentContent([...currentContent, content]); + } + + function deleteSavedFilter(index: number) { + setCurrentContent(currentContent.filter((f, i) => i !== index)); + } + + return ( + <> + {isAdd && allFilters && ( + + )} +
+
+ {currentContent.map((content, index) => ( +
onDragStart(e, index)} + onDragEnter={(e) => onDragOver(e, index)} + onDrop={() => onDrop()} + > + deleteSavedFilter(index)} + /> +
+ ))} +
+
+ +
+
+
+
+ + +
+
+ + ); +}; diff --git a/ui/v2.5/src/components/FrontPage/RecommendationRow.tsx b/ui/v2.5/src/components/FrontPage/RecommendationRow.tsx new file mode 100644 index 000000000..e6e58b10a --- /dev/null +++ b/ui/v2.5/src/components/FrontPage/RecommendationRow.tsx @@ -0,0 +1,24 @@ +import React, { PropsWithChildren } from "react"; + +interface IProps { + className?: string; + header: String; + link: JSX.Element; +} + +export const RecommendationRow: React.FC> = ({ + className, + header, + link, + children, +}) => ( +
+
+
+

{header}

+
+ {link} +
+ {children} +
+); diff --git a/ui/v2.5/src/components/Recommendations/styles.scss b/ui/v2.5/src/components/FrontPage/styles.scss similarity index 72% rename from ui/v2.5/src/components/Recommendations/styles.scss rename to ui/v2.5/src/components/FrontPage/styles.scss index ab8fb2ab7..4091392e7 100644 --- a/ui/v2.5/src/components/Recommendations/styles.scss +++ b/ui/v2.5/src/components/FrontPage/styles.scss @@ -6,6 +6,17 @@ padding-left: 0; padding-right: 0; } + + .recommendations-footer { + display: flex; + justify-content: right; + margin-bottom: 1em; + margin-top: 1em; + + button:not(:last-child) { + margin-right: 10px; + } + } } .no-recommendations { @@ -24,6 +35,25 @@ padding: 15px 0; } +.recommendations-container-edit { + .recommendation-row { + background-color: $secondary; + margin-bottom: 10px; + + &:not(.recommendation-row-add) { + cursor: grab; + } + } + + .recommendation-row-add .recommendation-row-head { + justify-content: center; + } + + .recommendation-row-head { + padding: 15px 10px; + } +} + .recommendation-row-head h2 { display: inline-flex; font-size: 1.25rem; @@ -41,10 +71,98 @@ .recommendations-container .studio-card hr, .recommendations-container .movie-card hr, -.recommendations-container .gallery-card hr { +.recommendations-container .gallery-card hr, +.recommendations-container .image-card hr { margin-top: auto; } +/* skeletons */ +.skeleton-card { + -webkit-animation: cardLoadingAnimation 2s infinite ease-in-out; + -moz-animation: cardLoadingAnimation 2s infinite ease-in-out; + -o-animation: cardLoadingAnimation 2s infinite ease-in-out; + animation: cardLoadingAnimation 2s infinite ease-in-out; + background-clip: border-box; + background-color: #30404d; + border: 1px solid rgba(0, 0, 0, 0.13); + border-radius: 3px; + box-shadow: 0 0 0 1px #10161a66, 0 0 #10161a00, 0 0 #10161a00; + display: flex; + flex-direction: column; + margin: 5px; + overflow: hidden; + padding: 0; + position: relative; + word-wrap: break-word; +} + +@keyframes cardLoadingAnimation { + 50% { + opacity: 0.5; + } +} + +.scene-skeleton { + max-width: 320px; + min-height: 365px; + min-width: 320px; + + @media (max-width: 576px) { + max-width: 20rem; + min-height: 25.2rem; + min-width: 20rem; + } +} + +.movie-skeleton { + max-width: 240px; + min-height: 540px; + min-width: 240px; + + @media (max-width: 576px) { + max-width: 16rem; + min-height: 34rem; + min-width: 16rem; + } +} + +.performer-skeleton { + max-width: 20rem; + min-height: 39.1rem; + min-width: 20rem; + + @media (max-width: 576px) { + max-width: 16rem; + min-height: 33.1rem; + min-width: 16rem; + } +} + +.image-skeleton, +.gallery-skeleton { + max-width: 320px; + min-height: 403.5px; + min-width: 320px; + + @media (max-width: 576px) { + max-width: 20rem; + min-height: 38.5rem; + min-width: 20rem; + } +} + +.studio-skeleton { + max-width: 360px; + min-height: 278px; + min-width: 360px; + + @media (max-width: 576px) { + max-width: 20rem; + min-height: 19.8rem; + min-width: 20rem; + } +} + /* Slider */ .slick-slider { box-sizing: border-box; @@ -310,7 +428,6 @@ list-style: none; margin: 0; padding: 0; - position: absolute; text-align: center; width: 100%; } diff --git a/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx b/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx index 827e66603..d3ca6823e 100644 --- a/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx @@ -1,37 +1,52 @@ import React, { FunctionComponent } from "react"; -import { FindGalleriesQueryResult } from "src/core/generated-graphql"; +import { useFindGalleries } from "src/core/StashService"; import Slider from "react-slick"; import { GalleryCard } from "./GalleryCard"; import { ListFilterModel } from "src/models/list-filter/filter"; import { getSlickSliderSettings } from "src/core/recommendations"; +import { RecommendationRow } from "../FrontPage/RecommendationRow"; +import { FormattedMessage } from "react-intl"; interface IProps { isTouch: boolean; filter: ListFilterModel; - result: FindGalleriesQueryResult; header: String; - linkText: String; } export const GalleryRecommendationRow: FunctionComponent = ( props: IProps ) => { - const cardCount = props.result.data?.findGalleries.count; + const result = useFindGalleries(props.filter); + const cardCount = result.data?.findGalleries.count; + + if (!result.loading && !cardCount) { + return null; + } + return ( -
-
-
-

{props.header}

-
+ - {props.linkText} + -
- - {props.result.data?.findGalleries.galleries.map((gallery) => ( - - ))} + } + > + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
+ )) + : result.data?.findGalleries.galleries.map((g) => ( + + ))}
-
+
); }; diff --git a/ui/v2.5/src/components/Images/ImageCard.tsx b/ui/v2.5/src/components/Images/ImageCard.tsx index 73a61a4f4..c078cbb5b 100644 --- a/ui/v2.5/src/components/Images/ImageCard.tsx +++ b/ui/v2.5/src/components/Images/ImageCard.tsx @@ -11,9 +11,9 @@ import { RatingBanner } from "../Shared/RatingBanner"; interface IImageCardProps { image: GQL.SlimImageDataFragment; selecting?: boolean; - selected: boolean | undefined; + selected?: boolean | undefined; zoomIndex: number; - onSelectedChanged: (selected: boolean, shiftKey: boolean) => void; + onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void; onPreview?: (ev: MouseEvent) => void; } diff --git a/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx b/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx new file mode 100644 index 000000000..6d76f3e17 --- /dev/null +++ b/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx @@ -0,0 +1,52 @@ +import React, { FunctionComponent } from "react"; +import { useFindImages } from "src/core/StashService"; +import Slider from "react-slick"; +import { ListFilterModel } from "src/models/list-filter/filter"; +import { getSlickSliderSettings } from "src/core/recommendations"; +import { RecommendationRow } from "../FrontPage/RecommendationRow"; +import { FormattedMessage } from "react-intl"; +import { ImageCard } from "./ImageCard"; + +interface IProps { + isTouch: boolean; + filter: ListFilterModel; + header: String; +} + +export const ImageRecommendationRow: FunctionComponent = ( + props: IProps +) => { + const result = useFindImages(props.filter); + const cardCount = result.data?.findImages.count; + + if (!result.loading && !cardCount) { + return null; + } + + return ( + + + + } + > + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
+ )) + : result.data?.findImages.images.map((i) => ( + + ))} +
+
+ ); +}; diff --git a/ui/v2.5/src/components/Movies/MovieRecommendationRow.tsx b/ui/v2.5/src/components/Movies/MovieRecommendationRow.tsx index 90b66e3d4..bcc6557ee 100644 --- a/ui/v2.5/src/components/Movies/MovieRecommendationRow.tsx +++ b/ui/v2.5/src/components/Movies/MovieRecommendationRow.tsx @@ -1,37 +1,50 @@ -import React, { FunctionComponent } from "react"; -import { FindMoviesQueryResult } from "src/core/generated-graphql"; +import React from "react"; +import { useFindMovies } from "src/core/StashService"; import Slider from "react-slick"; import { MovieCard } from "./MovieCard"; import { ListFilterModel } from "src/models/list-filter/filter"; import { getSlickSliderSettings } from "src/core/recommendations"; +import { RecommendationRow } from "../FrontPage/RecommendationRow"; +import { FormattedMessage } from "react-intl"; interface IProps { isTouch: boolean; filter: ListFilterModel; - result: FindMoviesQueryResult; header: String; - linkText: String; } -export const MovieRecommendationRow: FunctionComponent = ( - props: IProps -) => { - const cardCount = props.result.data?.findMovies.count; +export const MovieRecommendationRow: React.FC = (props: IProps) => { + const result = useFindMovies(props.filter); + const cardCount = result.data?.findMovies.count; + + if (!result.loading && !cardCount) { + return null; + } + return ( -
-
-
-

{props.header}

-
+ - {props.linkText} + -
- - {props.result.data?.findMovies.movies.map((p) => ( - - ))} + } + > + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
+ )) + : result.data?.findMovies.movies.map((m) => ( + + ))}
-
+
); }; diff --git a/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx b/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx index 21fa9db0f..8769591a2 100644 --- a/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx +++ b/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx @@ -1,37 +1,52 @@ import React, { FunctionComponent } from "react"; -import { FindPerformersQueryResult } from "src/core/generated-graphql"; +import { useFindPerformers } from "src/core/StashService"; import Slider from "react-slick"; import { PerformerCard } from "./PerformerCard"; import { ListFilterModel } from "src/models/list-filter/filter"; import { getSlickSliderSettings } from "src/core/recommendations"; +import { RecommendationRow } from "../FrontPage/RecommendationRow"; +import { FormattedMessage } from "react-intl"; interface IProps { isTouch: boolean; filter: ListFilterModel; - result: FindPerformersQueryResult; header: String; - linkText: String; } export const PerformerRecommendationRow: FunctionComponent = ( props: IProps ) => { - const cardCount = props.result.data?.findPerformers.count; + const result = useFindPerformers(props.filter); + const cardCount = result.data?.findPerformers.count; + + if (!result.loading && !cardCount) { + return null; + } + return ( -
-
-
-

{props.header}

-
+ - {props.linkText} + -
- - {props.result.data?.findPerformers.performers.map((p) => ( - - ))} + } + > + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
+ )) + : result.data?.findPerformers.performers.map((p) => ( + + ))}
-
+ ); }; diff --git a/ui/v2.5/src/components/Recommendations/Recommendations.tsx b/ui/v2.5/src/components/Recommendations/Recommendations.tsx deleted file mode 100644 index e9e0cb448..000000000 --- a/ui/v2.5/src/components/Recommendations/Recommendations.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import * as GQL from "src/core/generated-graphql"; -import { defineMessages, useIntl } from "react-intl"; -import React from "react"; -import { - useFindScenes, - useFindMovies, - useFindStudios, - useFindGalleries, - useFindPerformers, -} from "src/core/StashService"; -import { SceneRecommendationRow } from "src/components/Scenes/SceneRecommendationRow"; -import { StudioRecommendationRow } from "src/components/Studios/StudioRecommendationRow"; -import { MovieRecommendationRow } from "src/components/Movies/MovieRecommendationRow"; -import { PerformerRecommendationRow } from "src/components/Performers/PerformerRecommendationRow"; -import { GalleryRecommendationRow } from "src/components/Galleries/GalleryRecommendationRow"; -import { SceneQueue } from "src/models/sceneQueue"; -import { ListFilterModel } from "src/models/list-filter/filter"; -import { LoadingIndicator } from "src/components/Shared"; - -const Recommendations: React.FC = () => { - function isTouchEnabled() { - return "ontouchstart" in window || navigator.maxTouchPoints > 0; - } - - const isTouch = isTouchEnabled(); - - const intl = useIntl(); - const itemsPerPage = 25; - const scenefilter = new ListFilterModel(GQL.FilterMode.Scenes); - scenefilter.sortBy = "date"; - scenefilter.sortDirection = GQL.SortDirectionEnum.Desc; - scenefilter.itemsPerPage = itemsPerPage; - const sceneResult = useFindScenes(scenefilter); - const hasScenes = !!sceneResult?.data?.findScenes?.count; - - const studiofilter = new ListFilterModel(GQL.FilterMode.Studios); - studiofilter.sortBy = "created_at"; - studiofilter.sortDirection = GQL.SortDirectionEnum.Desc; - studiofilter.itemsPerPage = itemsPerPage; - const studioResult = useFindStudios(studiofilter); - const hasStudios = !!studioResult?.data?.findStudios?.count; - - const moviefilter = new ListFilterModel(GQL.FilterMode.Movies); - moviefilter.sortBy = "date"; - moviefilter.sortDirection = GQL.SortDirectionEnum.Desc; - moviefilter.itemsPerPage = itemsPerPage; - const movieResult = useFindMovies(moviefilter); - const hasMovies = !!movieResult?.data?.findMovies?.count; - - const performerfilter = new ListFilterModel(GQL.FilterMode.Performers); - performerfilter.sortBy = "created_at"; - performerfilter.sortDirection = GQL.SortDirectionEnum.Desc; - performerfilter.itemsPerPage = itemsPerPage; - const performerResult = useFindPerformers(performerfilter); - const hasPerformers = !!performerResult?.data?.findPerformers?.count; - - const galleryfilter = new ListFilterModel(GQL.FilterMode.Galleries); - galleryfilter.sortBy = "date"; - galleryfilter.sortDirection = GQL.SortDirectionEnum.Desc; - galleryfilter.itemsPerPage = itemsPerPage; - const galleryResult = useFindGalleries(galleryfilter); - const hasGalleries = !!galleryResult?.data?.findGalleries?.count; - - const messages = defineMessages({ - emptyServer: { - id: "empty_server", - defaultMessage: - "Add some scenes to your server to view recommendations on this page.", - }, - recentlyAddedStudios: { - id: "recently_added_studios", - defaultMessage: "Recently Added Studios", - }, - recentlyAddedPerformers: { - id: "recently_added_performers", - defaultMessage: "Recently Added Performers", - }, - recentlyReleasedGalleries: { - id: "recently_released_galleries", - defaultMessage: "Recently Released Galleries", - }, - recentlyReleasedMovies: { - id: "recently_released_movies", - defaultMessage: "Recently Released Movies", - }, - recentlyReleasedScenes: { - id: "recently_released_scenes", - defaultMessage: "Recently Released Scenes", - }, - viewAll: { - id: "view_all", - defaultMessage: "View All", - }, - }); - - if ( - sceneResult.loading || - studioResult.loading || - movieResult.loading || - performerResult.loading || - galleryResult.loading - ) { - return ; - } else { - return ( -
- {!hasScenes && - !hasStudios && - !hasMovies && - !hasPerformers && - !hasGalleries ? ( -
- {intl.formatMessage(messages.emptyServer)} -
- ) : ( -
- {hasScenes && ( - - )} - - {hasStudios && ( - - )} - - {hasMovies && ( - - )} - - {hasPerformers && ( - - )} - - {hasGalleries && ( - - )} -
- )} -
- ); - } -}; - -export default Recommendations; diff --git a/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx b/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx index ecd713d5b..e3d7f34e1 100644 --- a/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx +++ b/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx @@ -1,45 +1,63 @@ -import React, { FunctionComponent } from "react"; -import { FindScenesQueryResult } from "src/core/generated-graphql"; +import React, { FunctionComponent, useMemo } from "react"; +import { useFindScenes } from "src/core/StashService"; import Slider from "react-slick"; import { SceneCard } from "./SceneCard"; import { SceneQueue } from "src/models/sceneQueue"; import { ListFilterModel } from "src/models/list-filter/filter"; import { getSlickSliderSettings } from "src/core/recommendations"; +import { RecommendationRow } from "../FrontPage/RecommendationRow"; +import { FormattedMessage } from "react-intl"; interface IProps { isTouch: boolean; filter: ListFilterModel; - result: FindScenesQueryResult; - queue: SceneQueue; header: String; - linkText: String; } export const SceneRecommendationRow: FunctionComponent = ( props: IProps ) => { - const cardCount = props.result.data?.findScenes.count; + const result = useFindScenes(props.filter); + const cardCount = result.data?.findScenes.count; + + const queue = useMemo(() => { + return SceneQueue.fromListFilterModel(props.filter); + }, [props.filter]); + + if (!result.loading && !cardCount) { + return null; + } + return ( -
-
-
-

{props.header}

-
+ - {props.linkText} + -
- - {props.result.data?.findScenes.scenes.map((scene, index) => ( - - ))} + } + > + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
+ )) + : result.data?.findScenes.scenes.map((scene, index) => ( + + ))}
-
+ ); }; diff --git a/ui/v2.5/src/components/Settings/context.tsx b/ui/v2.5/src/components/Settings/context.tsx index 84e45ef70..2104c71f7 100644 --- a/ui/v2.5/src/components/Settings/context.tsx +++ b/ui/v2.5/src/components/Settings/context.tsx @@ -8,6 +8,7 @@ import React, { useRef, } from "react"; import { Spinner } from "react-bootstrap"; +import { IUIConfig } from "src/core/config"; import * as GQL from "src/core/generated-graphql"; import { useConfiguration, @@ -16,6 +17,7 @@ import { useConfigureGeneral, useConfigureInterface, useConfigureScraping, + useConfigureUI, } from "src/core/StashService"; import { useToast } from "src/hooks"; import { withoutTypename } from "src/utils"; @@ -29,6 +31,7 @@ export interface ISettingsContextState { defaults: GQL.ConfigDefaultSettingsInput; scraping: GQL.ConfigScrapingInput; dlna: GQL.ConfigDlnaInput; + ui: IUIConfig; // apikey isn't directly settable, so expose it here apiKey: string; @@ -38,6 +41,7 @@ export interface ISettingsContextState { saveDefaults: (input: Partial) => void; saveScraping: (input: Partial) => void; saveDLNA: (input: Partial) => void; + saveUI: (input: IUIConfig) => void; } export const SettingStateContext = React.createContext({ @@ -48,12 +52,14 @@ export const SettingStateContext = React.createContext({ defaults: {}, scraping: {}, dlna: {}, + ui: {}, apiKey: "", saveGeneral: () => {}, saveInterface: () => {}, saveDefaults: () => {}, saveScraping: () => {}, saveDLNA: () => {}, + saveUI: () => {}, }); export const SettingsContext: React.FC = ({ children }) => { @@ -92,6 +98,10 @@ export const SettingsContext: React.FC = ({ children }) => { >(); const [updateDLNAConfig] = useConfigureDLNA(); + const [ui, setUI] = useState({}); + const [pendingUI, setPendingUI] = useState<{} | undefined>(); + const [updateUIConfig] = useConfigureUI(); + const [updateSuccess, setUpdateSuccess] = useState(); const [apiKey, setApiKey] = useState(""); @@ -121,6 +131,7 @@ export const SettingsContext: React.FC = ({ children }) => { setDefaults({ ...withoutTypename(data.configuration.defaults) }); setScraping({ ...withoutTypename(data.configuration.scraping) }); setDLNA({ ...withoutTypename(data.configuration.dlna) }); + setUI({ ...withoutTypename(data.configuration.ui) }); setApiKey(data.configuration.general.apiKey); }, [data, error]); @@ -387,6 +398,56 @@ export const SettingsContext: React.FC = ({ children }) => { }); } + // saves the configuration if no further changes are made after a half second + const saveUIConfig = useMemo( + () => + debounce(async (input: IUIConfig) => { + try { + setUpdateSuccess(undefined); + await updateUIConfig({ + variables: { + input, + }, + }); + + setPendingUI(undefined); + onSuccess(); + } catch (e) { + setSaveError(e); + } + }, 500), + [updateUIConfig, onSuccess] + ); + + useEffect(() => { + if (!pendingUI) { + return; + } + + saveUIConfig(pendingUI); + }, [pendingUI, saveUIConfig]); + + function saveUI(input: IUIConfig) { + if (!ui) { + return; + } + + setUI({ + ...ui, + ...input, + }); + + setPendingUI((current) => { + if (!current) { + return input; + } + return { + ...current, + ...input, + }; + }); + } + function maybeRenderLoadingIndicator() { if (updateSuccess === false) { return ( @@ -401,7 +462,8 @@ export const SettingsContext: React.FC = ({ children }) => { pendingInterface || pendingDefaults || pendingScraping || - pendingDLNA + pendingDLNA || + pendingUI ) { return (
@@ -432,11 +494,13 @@ export const SettingsContext: React.FC = ({ children }) => { defaults, scraping, dlna, + ui, saveGeneral, saveInterface, saveDefaults, saveScraping, saveDLNA, + saveUI, }} > {maybeRenderLoadingIndicator()} diff --git a/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx b/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx index ea26e7fbe..aa5dbda1d 100644 --- a/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx +++ b/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx @@ -1,37 +1,52 @@ import React, { FunctionComponent } from "react"; -import { FindStudiosQueryResult } from "src/core/generated-graphql"; +import { useFindStudios } from "src/core/StashService"; import Slider from "react-slick"; import { StudioCard } from "./StudioCard"; import { ListFilterModel } from "src/models/list-filter/filter"; import { getSlickSliderSettings } from "src/core/recommendations"; +import { RecommendationRow } from "../FrontPage/RecommendationRow"; +import { FormattedMessage } from "react-intl"; interface IProps { isTouch: boolean; filter: ListFilterModel; - result: FindStudiosQueryResult; header: String; - linkText: String; } export const StudioRecommendationRow: FunctionComponent = ( props: IProps ) => { - const cardCount = props.result.data?.findStudios.count; + const result = useFindStudios(props.filter); + const cardCount = result.data?.findStudios.count; + + if (!result.loading && !cardCount) { + return null; + } + return ( -
-
-
-

{props.header}

-
+ - {props.linkText} + -
- - {props.result.data?.findStudios.studios.map((studio) => ( - - ))} + } + > + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
+ )) + : result.data?.findStudios.studios.map((s) => ( + + ))}
-
+ ); }; diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index ce6aa8e78..0f1512ef7 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -44,7 +44,14 @@ const deleteCache = (queries: DocumentNode[]) => { }); }; -export const useFindSavedFilters = (mode: GQL.FilterMode) => +export const useFindSavedFilter = (id: string) => + GQL.useFindSavedFilterQuery({ + variables: { + id, + }, + }); + +export const useFindSavedFilters = (mode?: GQL.FilterMode) => GQL.useFindSavedFiltersQuery({ variables: { mode, @@ -813,6 +820,12 @@ export const useConfigureDefaults = () => update: deleteCache([GQL.ConfigurationDocument]), }); +export const useConfigureUI = () => + GQL.useConfigureUiMutation({ + refetchQueries: getQueryNames([GQL.ConfigurationDocument]), + update: deleteCache([GQL.ConfigurationDocument]), + }); + export const useJobsSubscribe = () => GQL.useJobsSubscribeSubscription(); export const useConfigureDLNA = () => diff --git a/ui/v2.5/src/core/config.ts b/ui/v2.5/src/core/config.ts new file mode 100644 index 000000000..007d70e32 --- /dev/null +++ b/ui/v2.5/src/core/config.ts @@ -0,0 +1,88 @@ +import { IntlShape } from "react-intl"; +import { ITypename } from "src/utils"; +import { FilterMode, SortDirectionEnum } from "./generated-graphql"; + +// NOTE: double capitals aren't converted correctly in the backend + +export interface ISavedFilterRow extends ITypename { + __typename: "SavedFilter"; + savedFilterId: number; +} + +export interface IMessage { + id: string; + values: { [key: string]: string }; +} + +export interface ICustomFilter extends ITypename { + __typename: "CustomFilter"; + message?: IMessage; + title?: string; + mode: FilterMode; + sortBy: string; + direction: SortDirectionEnum; +} + +export type FrontPageContent = ISavedFilterRow | ICustomFilter; + +export interface IUIConfig { + frontPageContent?: FrontPageContent[]; +} + +function recentlyReleased( + intl: IntlShape, + mode: FilterMode, + objectsID: string +): ICustomFilter { + return { + __typename: "CustomFilter", + message: { + id: "recently_released_objects", + values: { objects: intl.formatMessage({ id: objectsID }) }, + }, + mode, + sortBy: "date", + direction: SortDirectionEnum.Desc, + }; +} + +function recentlyAdded( + intl: IntlShape, + mode: FilterMode, + objectsID: string +): ICustomFilter { + return { + __typename: "CustomFilter", + message: { + id: "recently_added_objects", + values: { objects: intl.formatMessage({ id: objectsID }) }, + }, + mode, + sortBy: "created_at", + direction: SortDirectionEnum.Desc, + }; +} + +export function generateDefaultFrontPageContent(intl: IntlShape) { + return [ + recentlyReleased(intl, FilterMode.Scenes, "scenes"), + recentlyAdded(intl, FilterMode.Studios, "studios"), + recentlyReleased(intl, FilterMode.Movies, "movies"), + recentlyAdded(intl, FilterMode.Performers, "performers"), + recentlyReleased(intl, FilterMode.Galleries, "galleries"), + ]; +} + +export function generatePremadeFrontPageContent(intl: IntlShape) { + return [ + recentlyReleased(intl, FilterMode.Scenes, "scenes"), + recentlyAdded(intl, FilterMode.Scenes, "scenes"), + recentlyReleased(intl, FilterMode.Galleries, "galleries"), + recentlyAdded(intl, FilterMode.Galleries, "galleries"), + recentlyAdded(intl, FilterMode.Images, "images"), + recentlyReleased(intl, FilterMode.Movies, "movies"), + recentlyAdded(intl, FilterMode.Movies, "movies"), + recentlyAdded(intl, FilterMode.Studios, "studios"), + recentlyAdded(intl, FilterMode.Performers, "performers"), + ]; +} diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss index 99565d469..51ead6d7e 100755 --- a/ui/v2.5/src/index.scss +++ b/ui/v2.5/src/index.scss @@ -8,7 +8,7 @@ @import "src/components/List/styles.scss"; @import "src/components/Movies/styles.scss"; @import "src/components/Performers/styles.scss"; -@import "src/components/Recommendations/styles.scss"; +@import "src/components/FrontPage/styles.scss"; @import "src/components/Scenes/styles.scss"; @import "src/components/SceneDuplicateChecker/styles.scss"; @import "src/components/SceneFilenameParser/styles.scss"; diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index daba6cde8..7e7633397 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -22,6 +22,7 @@ "create_entity": "Create {entityType}", "create_marker": "Create Marker", "created_entity": "Created {entity_type}: {entity_name}", + "customise": "Customise", "delete": "Delete", "delete_entity": "Delete {entityType}", "delete_file": "Delete file", @@ -734,6 +735,12 @@ "filters": "Filters", "framerate": "Frame Rate", "frames_per_second": "{value} frames per second", + "front_page": { + "types": { + "premade_filter": "Premade Filter", + "saved_filter": "Saved Filter" + } + }, "galleries": "Galleries", "gallery": "Gallery", "gallery_count": "Gallery Count", @@ -826,11 +833,8 @@ "queue": "Queue", "random": "Random", "rating": "Rating", - "recently_added_performers": "Recently Added Performers", - "recently_added_studios": "Recently Added Studios", - "recently_released_galleries": "Recently Released Galleries", - "recently_released_movies": "Recently Released Movies", - "recently_released_scenes": "Recently Released Scenes", + "recently_added_objects": "Recently Added {objects}", + "recently_released_objects": "Recently Released {objects}", "resolution": "Resolution", "scene": "Scene", "sceneTagger": "Scene Tagger", @@ -967,6 +971,7 @@ "total": "Total", "true": "True", "twitter": "Twitter", + "type": "Type", "updated_at": "Updated At", "url": "URL", "videos": "Videos", diff --git a/ui/v2.5/src/utils/data.ts b/ui/v2.5/src/utils/data.ts index 7fc08d3f4..df50a2d93 100644 --- a/ui/v2.5/src/utils/data.ts +++ b/ui/v2.5/src/utils/data.ts @@ -1,7 +1,7 @@ export const filterData = (data?: (T | null | undefined)[] | null) => data ? (data.filter((item) => item) as T[]) : []; -interface ITypename { +export interface ITypename { __typename?: string; } From a2e8f690286d6580e67dceacb5b1bc5c406ef550 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Tue, 14 Jun 2022 10:39:46 +1000 Subject: [PATCH 15/34] Fix scene player event handler initialisation (#2656) --- .../components/ScenePlayer/ScenePlayer.tsx | 133 +++++++++--------- 1 file changed, 68 insertions(+), 65 deletions(-) diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index eabf68282..96075cb2e 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -417,16 +417,79 @@ export const ScenePlayer: React.FC = ({ } } - // always stop the interactive client on initialisation - interactiveClient.pause(); - interactiveReady.current = false; + function loadstart(this: VideoJsPlayer) { + // handle offset after loading so that we get the correct current source + handleOffset(this); + } - if (!scene || scene.id === sceneId.current) return; - sceneId.current = scene.id; + function onPlay(this: VideoJsPlayer) { + this.poster(""); + if (scene?.interactive && interactiveReady.current) { + interactiveClient.play(this.currentTime()); + } + } + + function pause() { + interactiveClient.pause(); + } + + function timeupdate(this: VideoJsPlayer) { + if (scene?.interactive && interactiveReady.current) { + interactiveClient.ensurePlaying(this.currentTime()); + } + setTime(this.currentTime()); + } + + function seeking(this: VideoJsPlayer) { + this.play(); + } + + function error() { + handleError(true); + } + + // changing source (eg when seeking) resets the playback rate + // so set the default in addition to the current rate + function ratechange(this: VideoJsPlayer) { + this.defaultPlaybackRate(this.playbackRate()); + } + + function loadedmetadata(this: VideoJsPlayer) { + if (!this.videoWidth() && !this.videoHeight()) { + // Occurs during preload when videos with supported audio/unsupported video are preloaded. + // Treat this as a decoding error and try the next source without playing. + // However on Safari we get an media event when m3u8 is loaded which needs to be ignored. + const currentFile = this.currentSrc(); + if (currentFile != null && !currentFile.includes("m3u8")) { + // const play = !player.paused(); + // handleError(play); + this.error(MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED); + } + } + } const player = playerRef.current; if (!player) return; + // always initialise event handlers since these are destroyed when the + // component is destroyed + player.on("loadstart", loadstart); + player.on("play", onPlay); + player.on("pause", pause); + player.on("timeupdate", timeupdate); + player.on("seeking", seeking); + player.on("error", error); + player.on("ratechange", ratechange); + player.on("loadedmetadata", loadedmetadata); + + // don't re-initialise the player unless the scene has changed + if (!scene || scene.id === sceneId.current) return; + sceneId.current = scene.id; + + // always stop the interactive client on initialisation + interactiveClient.pause(); + interactiveReady.current = false; + const auto = autoplay || (config?.autostartVideo ?? false) || initialTimestamp > 0; if (!auto && scene.paths?.screenshot) player.poster(scene.paths.screenshot); @@ -474,66 +537,6 @@ export const ScenePlayer: React.FC = ({ player.loop(looping); interactiveClient.setLooping(looping); - function loadstart(this: VideoJsPlayer) { - // handle offset after loading so that we get the correct current source - handleOffset(this); - } - - player.on("loadstart", loadstart); - - function onPlay(this: VideoJsPlayer) { - this.poster(""); - if (scene?.interactive && interactiveReady.current) { - interactiveClient.play(this.currentTime()); - } - } - player.on("play", onPlay); - - function pause() { - interactiveClient.pause(); - } - player.on("pause", pause); - - function timeupdate(this: VideoJsPlayer) { - if (scene?.interactive && interactiveReady.current) { - interactiveClient.ensurePlaying(this.currentTime()); - } - setTime(this.currentTime()); - } - player.on("timeupdate", timeupdate); - - function seeking(this: VideoJsPlayer) { - this.play(); - } - player.on("seeking", seeking); - - function error() { - handleError(true); - } - player.on("error", error); - - // changing source (eg when seeking) resets the playback rate - // so set the default in addition to the current rate - function ratechange(this: VideoJsPlayer) { - this.defaultPlaybackRate(this.playbackRate()); - } - player.on("ratechange", ratechange); - - function loadedmetadata(this: VideoJsPlayer) { - if (!this.videoWidth() && !this.videoHeight()) { - // Occurs during preload when videos with supported audio/unsupported video are preloaded. - // Treat this as a decoding error and try the next source without playing. - // However on Safari we get an media event when m3u8 is loaded which needs to be ignored. - const currentFile = this.currentSrc(); - if (currentFile != null && !currentFile.includes("m3u8")) { - // const play = !player.paused(); - // handleError(play); - this.error(MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED); - } - } - } - player.on("loadedmetadata", loadedmetadata); - player.load(); if ((player as any).vttThumbnails?.src) From 900ba936a9d26ecb2bb6c38276611a604db55238 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Tue, 14 Jun 2022 10:41:35 +1000 Subject: [PATCH 16/34] Update changelog --- ui/v2.5/src/components/Changelog/versions/v0160.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/v2.5/src/components/Changelog/versions/v0160.md b/ui/v2.5/src/components/Changelog/versions/v0160.md index c8616373b..9f8de5760 100644 --- a/ui/v2.5/src/components/Changelog/versions/v0160.md +++ b/ui/v2.5/src/components/Changelog/versions/v0160.md @@ -1,8 +1,9 @@ ### ✨ New Features -* Support submitting stash-box scene updates for scenes with stash ids. ([#2577](https://github.com/stashapp/stash/pull/2577)) * Added support for customizing recommendations on home page. ([#2592](https://github.com/stashapp/stash/pull/2592)) +* Support submitting stash-box scene updates for scenes with stash ids. ([#2577](https://github.com/stashapp/stash/pull/2577)) ### 🐛 Bug fixes +* Fix scene scrubber stopping scrolling after editing scene or marker. ([#2600](https://github.com/stashapp/stash/pull/2600)) * Fix folder-based galleries not auto-tagging correctly if folder name contains `.` characters. ([#2658](https://github.com/stashapp/stash/pull/2658)) * Fix scene cover in scene edit panel not being updated when changing scenes. ([#2657](https://github.com/stashapp/stash/pull/2657)) * Fix moved gallery zip files not being rescanned. ([#2611](https://github.com/stashapp/stash/pull/2611)) From 582ffa1420b3a96b388fb562823cc5b87d1da371 Mon Sep 17 00:00:00 2001 From: dumdum7 <95527094+dumdum7@users.noreply.github.com> Date: Tue, 14 Jun 2022 03:19:12 +0200 Subject: [PATCH 17/34] Don't switch to landscape for portrait videos (#2665) --- .../components/Changelog/versions/v0160.md | 1 + .../components/ScenePlayer/ScenePlayer.tsx | 25 ++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/ui/v2.5/src/components/Changelog/versions/v0160.md b/ui/v2.5/src/components/Changelog/versions/v0160.md index 9f8de5760..1675e604b 100644 --- a/ui/v2.5/src/components/Changelog/versions/v0160.md +++ b/ui/v2.5/src/components/Changelog/versions/v0160.md @@ -3,6 +3,7 @@ * Support submitting stash-box scene updates for scenes with stash ids. ([#2577](https://github.com/stashapp/stash/pull/2577)) ### 🐛 Bug fixes +* Fix portrait videos orienting incorrectly in full-screen mode. ([#2665](https://github.com/stashapp/stash/pull/2665)) * Fix scene scrubber stopping scrolling after editing scene or marker. ([#2600](https://github.com/stashapp/stash/pull/2600)) * Fix folder-based galleries not auto-tagging correctly if folder name contains `.` characters. ([#2658](https://github.com/stashapp/stash/pull/2658)) * Fix scene cover in scene edit panel not being updated when changing scenes. ([#2657](https://github.com/stashapp/stash/pull/2657)) diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 96075cb2e..8dae61575 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -185,15 +185,6 @@ export const ScenePlayer: React.FC = ({ }); settings.updateDisplay(); - (player as any).landscapeFullscreen({ - fullscreen: { - enterOnRotate: true, - exitOnRotate: true, - alwaysInLandscapeMode: true, - iOS: false, - }, - }); - (player as any).markers(); (player as any).offset(); (player as any).sourceSelector(); @@ -495,6 +486,22 @@ export const ScenePlayer: React.FC = ({ if (!auto && scene.paths?.screenshot) player.poster(scene.paths.screenshot); else player.poster(""); + const isLandscape = + scene.file.height && + scene.file.width && + scene.file.width > scene.file.height; + + if (isLandscape) { + (player as any).landscapeFullscreen({ + fullscreen: { + enterOnRotate: true, + exitOnRotate: true, + alwaysInLandscapeMode: true, + iOS: false, + }, + }); + } + // clear the offset before loading anything new. // otherwise, the offset will be applied to the next file when // currentTime is called. From 6029918d22b292a8aaa8174b62d953ee7cf55cd2 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 15 Jun 2022 10:31:09 +1000 Subject: [PATCH 18/34] Fix ui config conversion (#2672) --- internal/manager/config/map.go | 46 ++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/internal/manager/config/map.go b/internal/manager/config/map.go index d4944fe5f..c75003adc 100644 --- a/internal/manager/config/map.go +++ b/internal/manager/config/map.go @@ -54,32 +54,46 @@ func toSnakeCaseMap(m map[string]interface{}) map[string]interface{} { for key, val := range m { adjKey := toSnakeCase(key) - switch v := val.(type) { - case map[interface{}]interface{}: - nm[adjKey] = toSnakeCaseMap(cast.ToStringMap(v)) - case map[string]interface{}: - nm[adjKey] = toSnakeCaseMap(v) - default: - nm[adjKey] = v - } + nm[adjKey] = val } return nm } +// convertMapValue converts values into something that can be marshalled in JSON +// This means converting map[interface{}]interface{} to map[string]interface{} where ever +// encountered. +func convertMapValue(val interface{}) interface{} { + switch v := val.(type) { + case map[interface{}]interface{}: + ret := cast.ToStringMap(v) + for k, vv := range ret { + ret[k] = convertMapValue(vv) + } + return ret + case map[string]interface{}: + ret := make(map[string]interface{}) + for k, vv := range v { + ret[k] = convertMapValue(vv) + } + return ret + case []interface{}: + ret := make([]interface{}, len(v)) + for i, vv := range v { + ret[i] = convertMapValue(vv) + } + return ret + default: + return v + } +} + func fromSnakeCaseMap(m map[string]interface{}) map[string]interface{} { nm := make(map[string]interface{}) for key, val := range m { adjKey := fromSnakeCase(key) - switch v := val.(type) { - case map[interface{}]interface{}: - nm[adjKey] = fromSnakeCaseMap(cast.ToStringMap(v)) - case map[string]interface{}: - nm[adjKey] = fromSnakeCaseMap(v) - default: - nm[adjKey] = v - } + nm[adjKey] = convertMapValue(val) } return nm From 75a795b2e6459834ef123209b9167174b4b782cd Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 15 Jun 2022 11:23:39 +1000 Subject: [PATCH 19/34] Add tag recommendation row (#2673) --- ui/v2.5/src/components/FrontPage/Control.tsx | 9 ++++ ui/v2.5/src/components/FrontPage/styles.scss | 15 +++++- .../components/Tags/TagRecommendationRow.tsx | 52 +++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 ui/v2.5/src/components/Tags/TagRecommendationRow.tsx diff --git a/ui/v2.5/src/components/FrontPage/Control.tsx b/ui/v2.5/src/components/FrontPage/Control.tsx index 501bcafe4..019429b3d 100644 --- a/ui/v2.5/src/components/FrontPage/Control.tsx +++ b/ui/v2.5/src/components/FrontPage/Control.tsx @@ -14,6 +14,7 @@ import { MovieRecommendationRow } from "../Movies/MovieRecommendationRow"; import { PerformerRecommendationRow } from "../Performers/PerformerRecommendationRow"; import { SceneRecommendationRow } from "../Scenes/SceneRecommendationRow"; import { StudioRecommendationRow } from "../Studios/StudioRecommendationRow"; +import { TagRecommendationRow } from "../Tags/TagRecommendationRow"; interface IFilter { mode: GQL.FilterMode; @@ -77,6 +78,14 @@ const RecommendationRow: React.FC = ({ mode, filter, header }) => { header={header} /> ); + case GQL.FilterMode.Tags: + return ( + + ); default: return <>; } diff --git a/ui/v2.5/src/components/FrontPage/styles.scss b/ui/v2.5/src/components/FrontPage/styles.scss index 4091392e7..6121131b8 100644 --- a/ui/v2.5/src/components/FrontPage/styles.scss +++ b/ui/v2.5/src/components/FrontPage/styles.scss @@ -72,7 +72,8 @@ .recommendations-container .studio-card hr, .recommendations-container .movie-card hr, .recommendations-container .gallery-card hr, -.recommendations-container .image-card hr { +.recommendations-container .image-card hr, +.recommendations-container .tag-card hr { margin-top: auto; } @@ -163,6 +164,18 @@ } } +.tag-skeleton { + max-width: 240px; + min-height: 365px; + min-width: 240px; + + @media (max-width: 576px) { + max-width: 16rem; + min-height: 26rem; + min-width: 16rem; + } +} + /* Slider */ .slick-slider { box-sizing: border-box; diff --git a/ui/v2.5/src/components/Tags/TagRecommendationRow.tsx b/ui/v2.5/src/components/Tags/TagRecommendationRow.tsx new file mode 100644 index 000000000..019ec16a7 --- /dev/null +++ b/ui/v2.5/src/components/Tags/TagRecommendationRow.tsx @@ -0,0 +1,52 @@ +import React, { FunctionComponent } from "react"; +import { useFindTags } from "src/core/StashService"; +import Slider from "react-slick"; +import { TagCard } from "./TagCard"; +import { ListFilterModel } from "src/models/list-filter/filter"; +import { getSlickSliderSettings } from "src/core/recommendations"; +import { RecommendationRow } from "../FrontPage/RecommendationRow"; +import { FormattedMessage } from "react-intl"; + +interface IProps { + isTouch: boolean; + filter: ListFilterModel; + header: String; +} + +export const TagRecommendationRow: FunctionComponent = ( + props: IProps +) => { + const result = useFindTags(props.filter); + const cardCount = result.data?.findTags.count; + + if (!result.loading && !cardCount) { + return null; + } + + return ( + + + + } + > + + {result.loading + ? [...Array(props.filter.itemsPerPage)].map((i) => ( +
+ )) + : result.data?.findTags.tags.map((p) => ( + + ))} +
+
+ ); +}; From c2f761795261870eb2d0884a9fac1b9ab708a011 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 15 Jun 2022 11:51:05 +1000 Subject: [PATCH 20/34] Separate filter buttons from query field (#2668) --- .../components/Changelog/versions/v0160.md | 3 + ui/v2.5/src/components/List/ListFilter.tsx | 79 +++++++++---------- ui/v2.5/src/components/List/styles.scss | 9 ++- ui/v2.5/src/index.scss | 6 ++ 4 files changed, 56 insertions(+), 41 deletions(-) diff --git a/ui/v2.5/src/components/Changelog/versions/v0160.md b/ui/v2.5/src/components/Changelog/versions/v0160.md index 1675e604b..ec68a3cab 100644 --- a/ui/v2.5/src/components/Changelog/versions/v0160.md +++ b/ui/v2.5/src/components/Changelog/versions/v0160.md @@ -2,6 +2,9 @@ * Added support for customizing recommendations on home page. ([#2592](https://github.com/stashapp/stash/pull/2592)) * Support submitting stash-box scene updates for scenes with stash ids. ([#2577](https://github.com/stashapp/stash/pull/2577)) +### 🎨 Improvements +* Moved Filter and Saved Filters buttons out of the query input field. ([#2668](https://github.com/stashapp/stash/pull/2668)) + ### 🐛 Bug fixes * Fix portrait videos orienting incorrectly in full-screen mode. ([#2665](https://github.com/stashapp/stash/pull/2665)) * Fix scene scrubber stopping scrolling after editing scene or marker. ([#2600](https://github.com/stashapp/stash/pull/2600)) diff --git a/ui/v2.5/src/components/List/ListFilter.tsx b/ui/v2.5/src/components/List/ListFilter.tsx index 1b6237aeb..a1876d1f8 100644 --- a/ui/v2.5/src/components/List/ListFilter.tsx +++ b/ui/v2.5/src/components/List/ListFilter.tsx @@ -208,27 +208,7 @@ export const ListFilter: React.FC = ({ return ( <>
- - - - - - - } - > - - - - - - - +
= ({ > - - - - - } - > - - - - +
+ + + + + + } + > + + + + + + + + + + } + > + + + + diff --git a/ui/v2.5/src/components/List/styles.scss b/ui/v2.5/src/components/List/styles.scss index 109143abb..4c494c017 100644 --- a/ui/v2.5/src/components/List/styles.scss +++ b/ui/v2.5/src/components/List/styles.scss @@ -29,6 +29,13 @@ input[type="range"].zoom-slider { padding-right: 0; } +.query-text-field-group { + align-items: stretch; + display: flex; + flex-wrap: wrap; + position: relative; +} + .query-text-field { border: 0; width: 50%; @@ -41,7 +48,7 @@ input[type="range"].zoom-slider { margin: $btn-padding-y $btn-padding-x; padding: 0; position: absolute; - right: 3em; + right: 0; z-index: 4; &:hover, diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss index 51ead6d7e..1e122c07c 100755 --- a/ui/v2.5/src/index.scss +++ b/ui/v2.5/src/index.scss @@ -800,6 +800,12 @@ div.dropdown-menu { } } +// workaround for dropdown button in button group +.btn-group > .dropdown:not(:last-child) > .btn { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} + dl.details-list { display: grid; grid-column-gap: 10px; From 733ca2aa6ff6b0c7dd60c5af787f52734802a2f9 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Sun, 19 Jun 2022 11:54:00 +1000 Subject: [PATCH 21/34] Run build on all pull requests --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eced257b4..4db7952e2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,6 @@ on: push: branches: [ develop, master ] pull_request: - branches: [ develop ] release: types: [ published ] From abd76f7e58453d54c536fe34ec523fca663d2bc1 Mon Sep 17 00:00:00 2001 From: TgSeed <92082995+TgSeed@users.noreply.github.com> Date: Wed, 22 Jun 2022 00:49:14 +0000 Subject: [PATCH 22/34] Fix/ffprobe unmarshalling error (#2685) Fix/ffprobe unmarshalling error --- pkg/exec/shell_windows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/exec/shell_windows.go b/pkg/exec/shell_windows.go index 9f4b551c0..84b7ea206 100644 --- a/pkg/exec/shell_windows.go +++ b/pkg/exec/shell_windows.go @@ -12,5 +12,5 @@ import ( // hideExecShell hides the windows when executing on Windows. func hideExecShell(cmd *exec.Cmd) { - cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: windows.DETACHED_PROCESS} + cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: windows.DETACHED_PROCESS & windows.CREATE_NO_WINDOW} } From 6cfb7fe79d1577662579454ea5d97f48b9621292 Mon Sep 17 00:00:00 2001 From: bnkai <48220860+bnkai@users.noreply.github.com> Date: Wed, 22 Jun 2022 03:59:39 +0300 Subject: [PATCH 23/34] Fix Synopsis json string in movie jsonschema (#2664) * Fix Synopsis json string in movie jsonschema * backwards compatible movie synopsis import --- pkg/models/jsonschema/movie.go | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/pkg/models/jsonschema/movie.go b/pkg/models/jsonschema/movie.go index 4c33da38f..d4eded802 100644 --- a/pkg/models/jsonschema/movie.go +++ b/pkg/models/jsonschema/movie.go @@ -5,6 +5,8 @@ import ( "os" jsoniter "github.com/json-iterator/go" + + "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models/json" ) @@ -15,7 +17,7 @@ type Movie struct { Date string `json:"date,omitempty"` Rating int `json:"rating,omitempty"` Director string `json:"director,omitempty"` - Synopsis string `json:"sypnopsis,omitempty"` + Synopsis string `json:"synopsis,omitempty"` FrontImage string `json:"front_image,omitempty"` BackImage string `json:"back_image,omitempty"` URL string `json:"url,omitempty"` @@ -24,6 +26,11 @@ type Movie struct { UpdatedAt json.JSONTime `json:"updated_at,omitempty"` } +// Backwards Compatible synopsis for the movie +type MovieSynopsisBC struct { + Synopsis string `json:"sypnopsis,omitempty"` +} + func LoadMovieFile(filePath string) (*Movie, error) { var movie Movie file, err := os.Open(filePath) @@ -37,6 +44,22 @@ func LoadMovieFile(filePath string) (*Movie, error) { if err != nil { return nil, err } + if movie.Synopsis == "" { + // keep backwards compatibility with pre #2664 builds + // attempt to get the synopsis from the alternate (sypnopsis) key + + _, err = file.Seek(0, 0) // seek to start of file + if err == nil { + var synopsis MovieSynopsisBC + err = jsonParser.Decode(&synopsis) + if err == nil { + movie.Synopsis = synopsis.Synopsis + if movie.Synopsis != "" { + logger.Debug("Movie synopsis retrieved from alternate key") + } + } + } + } return &movie, nil } From 1ab02a1748d6235e77ba87041b2ced3246a3d093 Mon Sep 17 00:00:00 2001 From: iampabber <107663471+iampabber@users.noreply.github.com> Date: Tue, 21 Jun 2022 18:33:17 -0700 Subject: [PATCH 24/34] Use hotkeys '[' and ']' to scrub video player forwards and backwards by 10% of the scene (#2678) * Use hotkeys '[' and ']' to scrub video player forwards and backwards by 10% of the scene * Don't loop back to beginning * Add manual keybind entry Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com> --- ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx | 14 ++++++++++++++ ui/v2.5/src/docs/en/KeyboardShortcuts.md | 3 +++ 2 files changed, 17 insertions(+) diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 8dae61575..a689f6a36 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -37,6 +37,14 @@ function handleHotkeys(player: VideoJsPlayer, event: VideoJS.KeyboardEvent) { player.currentTime(time); } + function seekPercentRelative(percent: number) { + const duration = player.duration(); + const currentTime = player.currentTime(); + const time = currentTime + duration * percent; + if (time > duration) return; + player.currentTime(time); + } + if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { return; } @@ -96,6 +104,12 @@ function handleHotkeys(player: VideoJsPlayer, event: VideoJS.KeyboardEvent) { case 57: // 9 seekPercent(0.9); break; + case 221: // ] + seekPercentRelative(0.1); + break; + case 219: // [ + seekPercentRelative(-0.1); + break; } } diff --git a/ui/v2.5/src/docs/en/KeyboardShortcuts.md b/ui/v2.5/src/docs/en/KeyboardShortcuts.md index 022f59a5a..50026de0a 100644 --- a/ui/v2.5/src/docs/en/KeyboardShortcuts.md +++ b/ui/v2.5/src/docs/en/KeyboardShortcuts.md @@ -64,6 +64,9 @@ | `p n` | Play next scene in queue | | `p p` | Play previous scene in queue | | `p r` | Play random scene in queue | +| `{1-9}` | Seek to 10-90% duration | +| `[` | Scrub backwards 10% duration | +| `]` | Scrub forwards 10% duration | ### Scene Markers tab shortcuts From 33b68b446451686977bef19162c22ec10854c19a Mon Sep 17 00:00:00 2001 From: stash-translation-bot <94573628+stash-translation-bot@users.noreply.github.com> Date: Tue, 21 Jun 2022 19:02:34 -0700 Subject: [PATCH 25/34] Translations update from Stash (#2653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Translated using Weblate (Swedish) Currently translated at 100.0% (818 of 818 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/sv/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 99.8% (817 of 818 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/pt_BR/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 99.8% (817 of 818 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/pt_BR/ * Translated using Weblate (French) Currently translated at 93.7% (767 of 818 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/ * Translated using Weblate (German) Currently translated at 100.0% (818 of 818 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/de/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ * Translated using Weblate (Italian) Currently translated at 100.0% (819 of 819 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/it/ * Translated using Weblate (Polish) Currently translated at 100.0% (819 of 819 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/pl/ * Translated using Weblate (Chinese (Traditional)) Currently translated at 99.7% (817 of 819 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/zh_Hant/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ * Translated using Weblate (Spanish) Currently translated at 98.1% (804 of 819 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/es/ * Translated using Weblate (Japanese) Currently translated at 100.0% (819 of 819 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ja/ * Translated using Weblate (French) Currently translated at 93.5% (766 of 819 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/ * Translated using Weblate (French) Currently translated at 93.7% (768 of 819 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/fr/ * Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (819 of 819 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/pt_BR/ * Translated using Weblate (Swedish) Currently translated at 100.0% (819 of 819 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/sv/ * Translated using Weblate (German) Currently translated at 100.0% (819 of 819 strings) Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/de/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Stash/Stash Desktop Client Translate-URL: https://translate.stashapp.cc/projects/stash/stash-desktop-client/ Co-authored-by: Alpaca Serious Co-authored-by: GeneX Co-authored-by: ponei Co-authored-by: Weblate Co-authored-by: - Co-authored-by: Phasetime Co-authored-by: BViking78 Co-authored-by: Coscosname Co-authored-by: Still Co-authored-by: Alex Co-authored-by: 風林火山 Co-authored-by: rather not --- ui/v2.5/src/locales/da-DK.json | 5 -- ui/v2.5/src/locales/de-DE.json | 32 +++++++-- ui/v2.5/src/locales/es-ES.json | 14 ++++ ui/v2.5/src/locales/fi-FI.json | 5 -- ui/v2.5/src/locales/fr-FR.json | 127 +++++++++++++++++++++------------ ui/v2.5/src/locales/it-IT.json | 15 ++-- ui/v2.5/src/locales/ja-JP.json | 19 +++-- ui/v2.5/src/locales/ko-KR.json | 5 -- ui/v2.5/src/locales/pl-PL.json | 15 ++-- ui/v2.5/src/locales/pt-BR.json | 91 ++++++++++++----------- ui/v2.5/src/locales/sv-SE.json | 19 +++-- ui/v2.5/src/locales/zh-CN.json | 5 -- ui/v2.5/src/locales/zh-TW.json | 9 ++- 13 files changed, 222 insertions(+), 139 deletions(-) diff --git a/ui/v2.5/src/locales/da-DK.json b/ui/v2.5/src/locales/da-DK.json index 96c836bb9..79767eeb8 100644 --- a/ui/v2.5/src/locales/da-DK.json +++ b/ui/v2.5/src/locales/da-DK.json @@ -860,11 +860,6 @@ "queue": "Kø", "random": "Tilfældig", "rating": "Bedømmelse", - "recently_added_performers": "Senest Tilføjede Skuespillere", - "recently_added_studios": "Senest Tilføjede Studier", - "recently_released_galleries": "Senest Tilføjede Gallerier", - "recently_released_movies": "Senest Tilføjede Film", - "recently_released_scenes": "Senest Tilføjede Scener", "resolution": "Opløsning", "scene": "Scene", "sceneTagger": "Scenetagger", diff --git a/ui/v2.5/src/locales/de-DE.json b/ui/v2.5/src/locales/de-DE.json index af9ee40d5..98f0a74e0 100644 --- a/ui/v2.5/src/locales/de-DE.json +++ b/ui/v2.5/src/locales/de-DE.json @@ -23,6 +23,7 @@ "create_entity": "Erstelle {entityType}", "create_marker": "Erstelle Markierung", "created_entity": "{entity_type} erstellt: {entity_name}", + "customise": "Anpassen", "delete": "Löschen", "delete_entity": "Lösche {entityType}", "delete_file": "Lösche Datei", @@ -734,6 +735,12 @@ "filters": "Filter", "framerate": "Bildrate", "frames_per_second": "{value} Bilder pro Sekunde", + "front_page": { + "types": { + "premade_filter": "Vorgefertigte Filter", + "saved_filter": "Gespeicherte Filter" + } + }, "galleries": "Galerien", "gallery": "Galerie", "gallery_count": "Galerienanzahl", @@ -823,17 +830,34 @@ "performer_image": "Darsteller-Bild", "performer_tagger": { "add_new_performers": "Neue Darsteller hinzufügen", + "any_names_entered_will_be_queried": "Alle eingetragenen Namen werden bei der stash-box Instanz nachgeschlagen und hinzugefügt, wenn gefunden. Nur exakte Übereinstimmungen werden als Treffer gewertet.", + "batch_add_performers": "Stapelverarbeitung für Darsteller", + "batch_update_performers": "Stapelverarbeitungsaktualisierung für Darsteller", + "config": { + "active_stash-box_instance": "Ausgewählte stash-box Instanz:", + "edit_excluded_fields": "Ausgeschlossene Felder bearbeiten", + "excluded_fields": "Ausgeschlossene Felder:", + "no_fields_are_excluded": "Keine Felder werden ausgeschlossen", + "no_instances_found": "Keine Instanzen gefunden", + "these_fields_will_not_be_changed_when_updating_performers": "Diese Felder werden durch die Aktualisierung nicht verändert." + }, "current_page": "Aktuelle Seite", "failed_to_save_performer": "Fehler beim Speichern der Darsteller \"{performer}\"", "name_already_exists": "Name bereits vergeben", "network_error": "Netzwerkfehler", "no_results_found": "Keine Ergebnisse gefunden.", + "number_of_performers_will_be_processed": "{performer_count} Darsteller werden verarbeitet", "performer_already_tagged": "Darsteller bereits getagged", + "performer_names_separated_by_comma": "Darstellernamen, mit Komma getrennt", + "performer_selection": "Darstellerauswahl", "performer_successfully_tagged": "Darsteller erfolgreich getagged:", "query_all_performers_in_the_database": "Alle Darsteller in der Datenbank", "refresh_tagged_performers": "Aktualisieren getaggter Darsteller", "refreshing_will_update_the_data": "Bei der Aktualisierung werden die Metadaten aller getaggten Darsteller über die stash-box-Instanz aktualisiert.", + "status_tagging_job_queued": "Status: Tagging-Auftrag in der Warteschlange", + "status_tagging_performers": "Status: Tagge Darsteller", "tag_status": "Tag Status", + "to_use_the_performer_tagger": "Um den Darsteller-Tagger zu benutzen, muss eine stash-box Instanz konfiguriert sein.", "untagged_performers": "Nicht getaggte Darsteller", "update_performer": "Darsteller aktualisieren", "update_performers": "Darsteller aktualisieren", @@ -844,11 +868,8 @@ "queue": "Playlist", "random": "Zufällig", "rating": "Wertung", - "recently_added_performers": "Kürzlich hinzugefügte Darsteller", - "recently_added_studios": "Kürzlich hinzugefügte Studios", - "recently_released_galleries": "Kürzlich hinzugefügte Gallerien", - "recently_released_movies": "Kürzlich erschienene Filme", - "recently_released_scenes": "Kürzlich erschienene Szenen", + "recently_added_objects": "Kürzlich hinzugefügte {objects}", + "recently_released_objects": "Kürzlich erschienene {objects}", "resolution": "Auflösung", "scene": "Szene", "sceneTagger": "Szenen-Tagger", @@ -992,6 +1013,7 @@ "total": "Gesamt", "true": "Wahr", "twitter": "Twitter", + "type": "Typ", "updated_at": "Aktualisiert am", "url": "URL", "videos": "Videos", diff --git a/ui/v2.5/src/locales/es-ES.json b/ui/v2.5/src/locales/es-ES.json index 9146f03f7..7596b7b08 100644 --- a/ui/v2.5/src/locales/es-ES.json +++ b/ui/v2.5/src/locales/es-ES.json @@ -23,6 +23,7 @@ "create_entity": "Crear {entityType}", "create_marker": "Crear marcador", "created_entity": "{entity_type} creado: {entity_name}", + "customise": "Personalizar", "delete": "Eliminar", "delete_entity": "Eliminar {entityType}", "delete_file": "Eliminar archivo", @@ -100,6 +101,7 @@ "stop": "Parar", "submit": "Enviar", "submit_stash_box": "Enviar a Stash-Box", + "submit_update": "Enviar Actualización", "tasks": { "clean_confirm_message": "¿Estás seguro que quieres iniciar la limpieza? Esto eliminará la información en la base de datos, y el contenido generado para todas las escenas y galerías que ya no estén disponibles en el sistema de ficheros.", "dry_mode_selected": "Modo de simulación seleccionado. No se eliminará información, solo se guardarán registros de las acciones a realizar.", @@ -121,6 +123,7 @@ "birth_year": "Año de nacimiento", "birthdate": "Cumpleaños", "bitrate": "Tasa de bits", + "captions": "Subtítulos", "career_length": "Años en activo", "component_tagger": { "config": { @@ -449,6 +452,10 @@ "description": "Tiempo de compensación en milisegundos para la reproducción de scripts interactivos.", "heading": "Tiempo de compensación Funscript (mseg)" }, + "handy_connection": { + "connect": "Conectar", + "sync": "Sincronizar" + }, "handy_connection_key": { "description": "Clave para conexión práctica que se usará en las escenas interactivas. Configurar esta clave permitirá a Stash compartir la información actual de las escenas con handyfeeling.com", "heading": "Clave para conexión práctica" @@ -730,6 +737,13 @@ "TRANSGENDER_MALE": "Varón transgénero" }, "hair_color": "Color de pelo", + "handy_connection_status": { + "connecting": "Conectando", + "disconnected": "Desconectado", + "missing": "El archivo no esta disponsible", + "ready": "Conexion Preparada", + "syncing": "Sincronizando con el servidor" + }, "hasMarkers": "Tiene marcadores", "height": "Estatura", "help": "Ayuda", diff --git a/ui/v2.5/src/locales/fi-FI.json b/ui/v2.5/src/locales/fi-FI.json index 6c41f17f3..38b042d87 100644 --- a/ui/v2.5/src/locales/fi-FI.json +++ b/ui/v2.5/src/locales/fi-FI.json @@ -808,11 +808,6 @@ "queue": "Jono", "random": "Satunnainen", "rating": "Arvio", - "recently_added_performers": "Viimeksi lisätyt esiintyjät", - "recently_added_studios": "Viimeksi lisätyt studiot", - "recently_released_galleries": "Viimeksi julkaistut galleriat", - "recently_released_movies": "Viimeksi julkaistut elokuvat", - "recently_released_scenes": "Viimeksi julkaistut kohtaukset", "resolution": "Resoluutio", "scene": "Kohtaus", "sceneTagger": "Kohtauksien tunnistetila", diff --git a/ui/v2.5/src/locales/fr-FR.json b/ui/v2.5/src/locales/fr-FR.json index 874837443..1a6383bc1 100644 --- a/ui/v2.5/src/locales/fr-FR.json +++ b/ui/v2.5/src/locales/fr-FR.json @@ -23,6 +23,7 @@ "create_entity": "Créer {entityType}", "create_marker": "Créer un marqueur", "created_entity": "Créé {entity_type} : {entity_name}", + "customise": "personnaliser", "delete": "Supprimer", "delete_entity": "Supprimer {entityType}", "delete_file": "Supprimer le fichier", @@ -57,7 +58,7 @@ "merge_from": "Fusionner depuis", "merge_into": "Fusionner dans", "next_action": "Suivant", - "not_running": "non exécuté", + "not_running": "pas en cours d'exécution", "open_in_external_player": "Ouvrir dans un lecteur externe", "open_random": "Ouvrir au hasard", "overwrite": "Écraser", @@ -733,6 +734,12 @@ "filters": "Filtres", "framerate": "Fréquence de rafraîchissement", "frames_per_second": "{value} images par seconde", + "front_page": { + "types": { + "premade_filter": "Filtre primaire", + "saved_filter": "Filtre sauvegardé" + } + }, "galleries": "Galeries", "gallery": "Galerie", "gallery_count": "Nombre de galeries", @@ -821,25 +828,47 @@ "performer_favorite": "Performeur favori", "performer_image": "Photo du performeur", "performer_tagger": { + "add_new_performers": "Ajouter de nouveaux performeurs", + "any_names_entered_will_be_queried": "Tout nom saisi sera demandé à l'instance distante StashBox et ajouté si trouvé. Seules les correspondances exactes seront considérées comme équivalentes.", + "batch_add_performers": "Ajouter des performeurs par lots", + "batch_update_performers": "Mises à jour des performeurs par lots", + "config": { + "active_stash-box_instance": "Instance stash-box active :", + "edit_excluded_fields": "Modifier les champs exclus", + "excluded_fields": "Champs exclus :", + "no_fields_are_excluded": "Aucun champ n'est exclu", + "no_instances_found": "Aucune instance trouvée", + "these_fields_will_not_be_changed_when_updating_performers": "Ces champs ne seront pas modifiés lors de la mise à jour des performeurs." + }, "current_page": "Page actuelle", + "failed_to_save_performer": "Échec pour sauvegarder le performeur \"{performer}\"", "name_already_exists": "Le nom existe déjà", "network_error": "Erreur réseau", "no_results_found": "Aucun résultat trouvé.", + "number_of_performers_will_be_processed": "{performer_count} performeurs seront traités", "performer_already_tagged": "Performeur déjà étiqueté", + "performer_names_separated_by_comma": "Noms des performeurs séparés par une virgule", + "performer_selection": "Sélection du performeur", "performer_successfully_tagged": "Performeur étiqueté avec succès :", + "query_all_performers_in_the_database": "Tous les performeurs dans la base de données", + "refresh_tagged_performers": "Actualiser les performeurs étiquetés", + "refreshing_will_update_the_data": "Une actualisation mettra à jour les données de tous les performeurs étiquetés de l'instance stash-box.", + "status_tagging_job_queued": "Statut : Tâche d'étiquetage en file d'attente", + "status_tagging_performers": "Statut : Étiquetage des performeurs", + "tag_status": "Statut de l'étiquette", + "to_use_the_performer_tagger": "Pour utiliser l'étiqueteur de performeurs, une instance stash-box doit être configurée.", + "untagged_performers": "Performeurs non étiquetés", "update_performer": "Mise à jour du performeur", - "update_performers": "Mise à jour des performeurs" + "update_performers": "Mise à jour des performeurs", + "updating_untagged_performers_description": "Une mise à jour des performeurs non étiquetés essaiera de faire correspondre tous les performeurs qui n'ont pas de StashID et actualisera les métadonnées." }, "performers": "Performeurs", "piercings": "Piercings", "queue": "File de lecture", "random": "Aléatoire", "rating": "Note", - "recently_added_performers": "Performeurs récemment ajoutés", - "recently_added_studios": "Studios récemment ajoutés", - "recently_released_galleries": "Galeries récemment sorties", - "recently_released_movies": "Films récemment sortis", - "recently_released_scenes": "Scènes récemment sorties", + "recently_added_objects": "Récemment ajouté {objects}", + "recently_released_objects": "Récemment sortis {objects}", "resolution": "Résolution", "scene": "Scène", "sceneTagger": "Étiqueteur de scènes", @@ -910,77 +939,81 @@ "success": { "getting_help": "Obtenir de l'aide", "help_links": "Si vous rencontrez des problèmes ou avez des questions ou des suggestions, n'hésitez pas à ouvrir un incident sur {githubLink}, ou demandez à la communauté sur {discordLink}.", - "in_app_manual_explained": "Nous vous encourageons à consulter le manuel intégré à l'application, accessible à partir de l'icône dans le coin supérieur droit de l'écran qui ressemble à ceci : {icône}", - "next_config_step_one": "Vous serez redirigé vers la page de configuration suivante. Cette page vous permettra de personnaliser les fichiers à inclure et à exclure, de définir un nom d'utilisateur et un mot de passe pour protéger votre système, et tout un tas d'autres options.", - "next_config_step_two": "Lorsque vous êtes satisfait de ces paramètres, vous pouvez commencer à scanner votre contenu dans Stash en cliquant sur {localized_task}, puis sur {localized_scan}.", + "in_app_manual_explained": "Nous vous encourageons à consulter le manuel de l'application, accessible à partir de l'icône située dans le coin supérieur droit de l'écran, qui ressemble à ceci : {icon}", + "next_config_step_one": "Vous serez ensuite dirigé vers la page de configuration suivante. Cette page vous permettra de personnaliser les fichiers à inclure et à exclure, de définir un nom d'utilisateur et un mot de passe pour protéger votre système, ainsi qu'un grand nombre d'autres options.", + "next_config_step_two": "Lorsque vous êtes satisfait de ces paramètres, vous pouvez commencer à analyser votre contenu dans Stash en cliquant sur {localized_task}, puis sur {localized_scan}.", "open_collective": "Consultez notre {open_collective_link} pour voir comment vous pouvez contribuer au développement continu de Stash.", "support_us": "Soutenez-nous", - "thanks_for_trying_stash": "Merci d'avoir utilisé Stash !", - "welcome_contrib": "Nous accueillons également les contributions sous forme de code (corrections de bogues, améliorations et nouvelles fonctionnalités), de tests, de rapports de bogues, de demandes d'améliorations et de fonctionnalités, et d'assistance aux autres utilisateurs. Les détails sont disponibles dans la section Contribution du manuel intégré à l'application.", - "your_system_has_been_created": "Succès ! Votre système a été créé !" + "thanks_for_trying_stash": "Merci d'avoir essayé Stash !", + "welcome_contrib": "Nous accueillons également les contributions sous forme de code (corrections de bogues, améliorations et nouvelles fonctionnalités), de tests, de rapports de bogues, de demandes d'améliorations et de fonctionnalités, et d'assistance utilisateurs. Vous trouverez plus de précisions dans la section \"Contribution\" du manuel de l'application.", + "your_system_has_been_created": "Bravo ! Votre système a été créé !" }, "welcome": { - "config_path_logic_explained": "Stash essaie d'abord de trouver son fichier de configuration (config.yml) dans le répertoire de travail actuel, et s'il ne le trouve pas là, il revient à $HOME/.stash/config. yml (sous Windows, ce sera %USERPROFILE%\\.stash\\config.yml). Vous pouvez également faire en sorte que Stash lise à partir d'un fichier de configuration spécifique en l'exécutant avec les options -c ou --config .", + "config_path_logic_explained": "Stash essaie d'abord de trouver son fichier de configuration (config.yml) dans le répertoire de travail courant, et s'il ne le trouve pas, il se reporte sur $HOME/.stash/config. yml (sous Windows, ce sera %USERPROFILE%\\.stash\\config.yml). Vous pouvez également faire en sorte que Stash lise un fichier de configuration spécifique en le lançant avec les options -c ou --config .", "in_current_stash_directory": "Dans le répertoire $HOME/.stash", - "in_the_current_working_directory": "Dans le répertoire de travail actuel", - "next_step": "Si vous êtes prêt à créer un nouvel environnement, veuillez sélectionner l'emplacement où vous souhaitez enregistrer votre fichier de configuration et cliquez sur Suivant.", - "store_stash_config": "Où voulez-vous stocker votre configuration Stash ?", - "unable_to_locate_config": "Si vous lisez ceci, Stash n'a pas pu trouver de configuration existante. Cet assistant vous guidera tout au long du processus de configuration d'une nouvelle configuration.", - "unexpected_explained": "Si vous obtenez cet écran de façon inattendue, veuillez essayer de redémarrer Stash dans le bon répertoire de travail ou avec l'indicateur -c." + "in_the_current_working_directory": "Dans le répertoire de travail courant", + "next_step": "Une fois que tout est réglé, si vous êtes prêt à procéder à la configuration d'un nouveau système, choisissez l'emplacement où vous souhaitez stocker votre fichier de configuration et cliquez sur Suivant.", + "store_stash_config": "Où voulez-vous stocker votre configuration de Stash ?", + "unable_to_locate_config": "Si vous lisez ceci, Stash n'a pas pu trouver de configuration existante. Cet assistant vous guidera dans le processus de paramétrage d'une nouvelle configuration.", + "unexpected_explained": "Si vous obtenez cet écran de manière inattendue, essayez de redémarrer Stash dans le bon répertoire de travail ou avec le drapeau -c." }, "welcome_specific_config": { - "config_path": "Stash utilisera le chemin suivant pour le fichier de configuration : {path} ", + "config_path": "Stash utilisera le chemin suivant pour le fichier de configuration : {path}", "next_step": "Lorsque vous êtes prêt à procéder à la configuration d'un nouveau système, cliquez sur Suivant.", - "unable_to_locate_specified_config": "Si vous lisez ceci, alors Stash n'a pas pu trouver le fichier de configuration spécifié sur la ligne de commande ou l'environnement. Cet assistant vous guidera tout au long du processus de configuration d'une nouvelle configuration." + "unable_to_locate_specified_config": "Si vous lisez ceci, Stash n'a pas pu trouver le fichier de configuration spécifié en ligne de commande ou dans l'environnement. Cet assistant vous guidera dans le processus de paramétrage d'une nouvelle configuration." }, "welcome_to_stash": "Bienvenue sur Stash" }, - "stash_id": "Stash ID", - "stash_ids": "Stash IDs", + "stash_id": "ID Stash", + "stash_ids": "IDs Stash", "stashbox": { - "submission_failed": "Échec de l'envoi", - "submission_successful": "Envoi effectué" + "go_review_draft": "Allez à {endpoint_name} pour revoir le document.", + "selected_stash_box": "Point de connexion Stash-Box sélectionné", + "submission_failed": "Envoi échoué", + "submission_successful": "Envoi réussi", + "submit_update": "Existe déjà dans {endpoint_name}" }, "statistics": "Statistiques", "stats": { - "image_size": "Taille Images", - "scenes_duration": "Durée Vidéos", - "scenes_size": "Taille Vidéos" + "image_size": "Taille des images", + "scenes_duration": "Durée des scènes", + "scenes_size": "Taille des scènes" }, - "status": "Statut : {statusText}", + "status": "Statut : {statusText}", "studio": "Studio", - "studio_depth": "Niveaux (empty for all)", + "studio_depth": "Niveaux (vides pour tous)", "studios": "Studios", - "sub_tag_count": "Nombre de sous-Tags", - "sub_tag_of": "Sous-Tag de {parent}", - "sub_tags": "Sous-Tags", - "subsidiary_studios": "Studios affiliés/filiales", - "synopsis": "Synopsis", - "tag": "Tag", - "tag_count": "Nombre de Tags", - "tags": "Tags", + "sub_tag_count": "Nombre de sous-étiquettes", + "sub_tag_of": "Sous-étiquette de {parent}", + "sub_tags": "Sous-étiquettes", + "subsidiary_studios": "Studios affiliés", + "synopsis": "Résumé", + "tag": "Étiquette", + "tag_count": "Nombre d'étiquettes", + "tags": "Étiquettes", "tattoos": "Tatouages", "title": "Titre", "toast": { "added_entity": "{entity} ajouté(e)", - "added_generation_job_to_queue": "Tâche de génération ajoutée dans la file des tâches", + "added_generation_job_to_queue": "Ajout d'une tâche de génération en file d'attente", "created_entity": "Créé(e) {entity}", - "default_filter_set": "Filtre par défaut enregistré", - "delete_entity": "Suppression de {count, plural, one {{singularEntity}} other {{pluralEntity}}}", + "default_filter_set": "Filtre par défaut défini", + "delete_entity": "Supprimer {count, plural, one {{singularEntity}} other {{pluralEntity}}}", "delete_past_tense": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} supprimé(e)(s)", "generating_screenshot": "Génération de la capture d'écran…", - "merged_tags": "Tags fusionnés", - "rescanning_entity": "Rescan de {count, plural, one {{singularEntity}} other {{pluralEntity}}}…", + "merged_tags": "Étiquettes fusionnées", + "rescanning_entity": "Réexamen de {count, plural, one {{singularEntity}} other {{pluralEntity}}}…", "saved_entity": "{entity} sauvegardé(e)", - "started_auto_tagging": "Démarrage du Taggage automatique", - "started_generating": "Génération de fichiers multimédia démarrée", - "started_importing": "Importation commencée", + "started_auto_tagging": "Démarrage de l'étiquetage automatique", + "started_generating": "Démarrage de la génération", + "started_importing": "Démarrage de l'importation", "updated_entity": "{entity} mis(e) à jour" }, "total": "Total", "true": "Vrai", "twitter": "Twitter", - "updated_at": "Date de modification", + "type": "Type", + "updated_at": "Actualisé le", "url": "URL", "videos": "Vidéos", "view_all": "Voir tout", diff --git a/ui/v2.5/src/locales/it-IT.json b/ui/v2.5/src/locales/it-IT.json index 4b1013406..a2e2286f2 100644 --- a/ui/v2.5/src/locales/it-IT.json +++ b/ui/v2.5/src/locales/it-IT.json @@ -23,6 +23,7 @@ "create_entity": "Crea {entityType}", "create_marker": "Crea Marker", "created_entity": "Creata/o {entity_type}: {entity_name}", + "customise": "Personalizza", "delete": "Cancella", "delete_entity": "Cancella {entityType}", "delete_file": "Cancella file", @@ -734,6 +735,12 @@ "filters": "Filtri", "framerate": "Frequenza dei Fotogrammi", "frames_per_second": "{value} fotogrammi per secondo", + "front_page": { + "types": { + "premade_filter": "Filtro Già Creato", + "saved_filter": "Filtro Salvato" + } + }, "galleries": "Gallerie", "gallery": "Galleria", "gallery_count": "Numero Gallerie", @@ -861,11 +868,8 @@ "queue": "Coda", "random": "Casuale", "rating": "Classif.", - "recently_added_performers": "Attori Aggiunti Recentemente", - "recently_added_studios": "Studio Aggiunti Recentemente", - "recently_released_galleries": "Gallerie Distribuite Recentemente", - "recently_released_movies": "Film Distribuiti Recentemente", - "recently_released_scenes": "Video Distribuiti Recentemente", + "recently_added_objects": "{objects} Aggiunto Recentemente", + "recently_released_objects": "{objects} Recentemente Distribuito", "resolution": "Risoluzione", "scene": "Scena", "sceneTagger": "Tagger Scena", @@ -1009,6 +1013,7 @@ "total": "Totale", "true": "Vero", "twitter": "Twitter", + "type": "Tipo", "updated_at": "Aggiornato Al", "url": "URL", "videos": "Video", diff --git a/ui/v2.5/src/locales/ja-JP.json b/ui/v2.5/src/locales/ja-JP.json index 2210a6621..1d1d5d08c 100644 --- a/ui/v2.5/src/locales/ja-JP.json +++ b/ui/v2.5/src/locales/ja-JP.json @@ -23,6 +23,7 @@ "create_entity": "{entityType}を作成", "create_marker": "マーカーを作成", "created_entity": "{entity_type}を作成しました: {entity_name}", + "customise": "カスタマイズ", "delete": "削除", "delete_entity": "{entityType}を削除", "delete_file": "ファイルを削除", @@ -100,6 +101,7 @@ "stop": "停止", "submit": "送信", "submit_stash_box": "Stash-Boxに送信", + "submit_update": "更新を送信", "tasks": { "clean_confirm_message": "クリーニングを実行してもよろしいですか?この操作により、ファイルシステムで利用されていないすべてのシーンとギャラリーから生成されたコンテンツとデータベース情報が削除されます。", "dry_mode_selected": "ドライモードが選択されています。実際の削除は実施されず、ログ処理だけが実行されます。", @@ -733,6 +735,12 @@ "filters": "フィルター", "framerate": "フレームレート", "frames_per_second": "{value}FPS", + "front_page": { + "types": { + "premade_filter": "既製フィルター", + "saved_filter": "保存済みフィルター" + } + }, "galleries": "ギャラリー", "gallery": "ギャラリー", "gallery_count": "ギャラリー数", @@ -860,11 +868,8 @@ "queue": "キュー", "random": "ランダム", "rating": "評価", - "recently_added_performers": "最近追加された出演者", - "recently_added_studios": "最近追加されたスタジオ", - "recently_released_galleries": "最近リリースされたギャラリー", - "recently_released_movies": "最近リリースされた映画", - "recently_released_scenes": "最近リリースされたシーン", + "recently_added_objects": "最近追加された{objects}", + "recently_released_objects": "最近リリースされた{objects}", "resolution": "解像度", "scene": "シーン", "sceneTagger": "シーン一括タグ付け", @@ -966,7 +971,8 @@ "go_review_draft": "下書きを確認するには、{endpoint_name}に移動してください。", "selected_stash_box": "選択済みのStash-Boxエンドポイント", "submission_failed": "送信に失敗しました", - "submission_successful": "送信完了しました" + "submission_successful": "送信完了しました", + "submit_update": "{endpoint_name}に既に存在します" }, "statistics": "統計", "stats": { @@ -1007,6 +1013,7 @@ "total": "合計", "true": "有効", "twitter": "Twitter", + "type": "タイプ", "updated_at": "更新日:", "url": "URL", "videos": "動画", diff --git a/ui/v2.5/src/locales/ko-KR.json b/ui/v2.5/src/locales/ko-KR.json index c07fde25e..2b6c7492a 100644 --- a/ui/v2.5/src/locales/ko-KR.json +++ b/ui/v2.5/src/locales/ko-KR.json @@ -860,11 +860,6 @@ "queue": "대기열", "random": "랜덤", "rating": "별점", - "recently_added_performers": "최근에 추가된 배우", - "recently_added_studios": "최근에 추가된 스튜디오", - "recently_released_galleries": "최근에 만들어진 갤러리", - "recently_released_movies": "최근에 만들어진 영화", - "recently_released_scenes": "최근에 만들어진 영상", "resolution": "해상도", "scene": "영상", "sceneTagger": "영상 태거", diff --git a/ui/v2.5/src/locales/pl-PL.json b/ui/v2.5/src/locales/pl-PL.json index 2d93260ba..1de48a0b1 100644 --- a/ui/v2.5/src/locales/pl-PL.json +++ b/ui/v2.5/src/locales/pl-PL.json @@ -23,6 +23,7 @@ "create_entity": "Utwórz {entityType}", "create_marker": "Utwórz znacznik", "created_entity": "Utworzono {entity_type}: {entity_name}", + "customise": "Dostosuj", "delete": "Usuń", "delete_entity": "Usuń {entityType}", "delete_file": "Usuń plik", @@ -734,6 +735,12 @@ "filters": "Filtry", "framerate": "Liczba klatek na sekundę", "frames_per_second": "{value} klatek na sekundę", + "front_page": { + "types": { + "premade_filter": "Gotowy filtr", + "saved_filter": "Zapisany filtr" + } + }, "galleries": "Galerie", "gallery": "Galeria", "gallery_count": "Liczba Galerii", @@ -861,11 +868,8 @@ "queue": "Kolejka", "random": "Losowo", "rating": "Ocena", - "recently_added_performers": "Niedawno Dodani Wykonawcy", - "recently_added_studios": "Niedawno Dodane Studia", - "recently_released_galleries": "Niedawno Dodane Galerie", - "recently_released_movies": "Niedawno Dodane Filmy", - "recently_released_scenes": "Niedawno Dodane Sceny", + "recently_added_objects": "Ostatnio dodane {objects}", + "recently_released_objects": "Ostatnio wydane {objects}", "resolution": "Rozdzielczość", "scene": "Scena", "sceneTagger": "Otagowywacz scen", @@ -1009,6 +1013,7 @@ "total": "Łącznie", "true": "Tak", "twitter": "Twitter", + "type": "Typ", "updated_at": "Zaktualizowano", "url": "URL", "videos": "Filmy wideo", diff --git a/ui/v2.5/src/locales/pt-BR.json b/ui/v2.5/src/locales/pt-BR.json index 3246f3e9a..e9e79828f 100644 --- a/ui/v2.5/src/locales/pt-BR.json +++ b/ui/v2.5/src/locales/pt-BR.json @@ -9,7 +9,7 @@ "apply": "Aplicar", "auto_tag": "Etiquetamento automático", "backup": "Backup", - "browse_for_image": "Navegar imagens…", + "browse_for_image": "Procurar por imagem…", "cancel": "Cancelar", "clean": "Limpar", "clear": "Limpar", @@ -23,6 +23,7 @@ "create_entity": "Criar {entityType}", "create_marker": "Criar marcador", "created_entity": "Criar {entity_type}: {entity_name}", + "customise": "Customizar", "delete": "Apagar", "delete_entity": "Apagar {entityType}", "delete_file": "Apagar arquivo", @@ -103,7 +104,7 @@ "submit_update": "Enviar atualização", "tasks": { "clean_confirm_message": "Tem certeza de que quer limpar? Isto irá apagar as informações do banco de dados e conteúdos gerados de todas as cenas e galerias que não são mais encontradas no sistema.", - "dry_mode_selected": "Modo não destrutivo. Nenhum arquivo será apagado, apenas será logado.", + "dry_mode_selected": "Modo não destrutivo. Nenhum arquivo será apagado, apenas registrado.", "import_warning": "Tem certeza de que quer importar? Isto irá apagar o banco de dados e re-importar de seus metadados exportados." }, "temp_disable": "Desabilitar temporariamente…", @@ -126,7 +127,7 @@ "career_length": "Duração da carreira", "component_tagger": { "config": { - "active_instance": "Ativar stash-box:", + "active_instance": "Instância do stash-box ativa:", "blacklist_desc": "Os itens da lista negra são excluídos das consultas. Observe que são expressões regulares e também não fazem distinção entre maiúsculas e minúsculas. Certos caracteres devem ser escritos com uma barra invertida: {chars_require_escape}", "blacklist_label": "Lista negra", "query_mode_auto": "Automático", @@ -171,16 +172,16 @@ }, "config": { "about": { - "build_hash": "Build hash:", - "build_time": "Tempo de build:", + "build_hash": "Hash do executável:", + "build_time": "Horário de criação do executável:", "check_for_new_version": "Verificar se há uma nova versão", "latest_version": "Última versão", - "latest_version_build_hash": "Build Hash da última versão:", + "latest_version_build_hash": "Hash do executável da última versão:", "new_version_notice": "[NOVA]", - "stash_discord": "Junte-se ao nosso {url} canal", + "stash_discord": "Junte-se ao nosso servidor no {url}", "stash_home": "Stash home no {url}", "stash_open_collective": "Apoie-nos através de {url}", - "stash_wiki": "Stash {url} página", + "stash_wiki": "Página da Stash {url}", "version": "Versão" }, "application_paths": { @@ -189,10 +190,10 @@ "categories": { "about": "Sobre", "interface": "Interface", - "logs": "Logs", + "logs": "Registros", "metadata_providers": "Provedores de Meta-dados", "plugins": "Plugins", - "scraping": "Scraping", + "scraping": "Extração", "security": "Segurança", "services": "Serviços", "system": "Sistema", @@ -228,12 +229,12 @@ "heading": "Credenciais" }, "generate_api_key": "Gerar Chave de API", - "log_file": "Arquivo de log", - "log_file_desc": "Caminho para o arquivo para o log de saída. Em branco para desativar o registro de arquivos. Requer reinicialização.", - "log_http": "Log de acesso http", - "log_http_desc": "Logs de acesso http para o terminal. Requer reinicialização.", - "log_to_terminal": "Log para terminal", - "log_to_terminal_desc": "Logs para o terminal, além de um arquivo. Sempre ativo se o log de arquivos estiver desativado. Requer reinicialização.", + "log_file": "Arquivo de registro", + "log_file_desc": "Caminho para o arquivo para mandar os registros. Deixe em branco para desativar o arquivo de registro. Requer reinicialização.", + "log_http": "Registrar acesso http", + "log_http_desc": "Registrar acesso http para o terminal. Requer reinicialização.", + "log_to_terminal": "Registrar para o terminal", + "log_to_terminal_desc": "Registrar para o terminal além do arquivo. Sempre ativo se o arquivo de registro estiver desativado. Requer reinicialização.", "maximum_session_age": "Tempo máximo da sessão", "maximum_session_age_desc": "Tempo ocioso máximo antes de uma sessão de login expirar, em segundos.", "password": "Senha", @@ -244,7 +245,7 @@ }, "cache_location": "Localização do diretório do cache", "cache_path_head": "Caminho do cache", - "calculate_md5_and_ohash_desc": "Calcular MD5 checksum além do oshash. A ativação fará com que as varreduras iniciais sejam mais lentas. Nomeação de arquivo Hash deve ser definido para oshash para desabilitar o cálculo MD5.", + "calculate_md5_and_ohash_desc": "Calcular MD5 checksum além do oshash. A ativação fará com que as escaneamentos iniciais sejam mais lentos. Nomeação de arquivo Hash deve ser definido para oshash para desabilitar o cálculo MD5.", "calculate_md5_and_ohash_label": "Calcular MD5 para vídeos", "check_for_insecure_certificates": "Verifique se há certificados inseguros", "check_for_insecure_certificates_desc": "Alguns sites usam ssl certificados inseguros. Quando desmarcado o scraper pula a verificação de certificados inseguros e permite o scraping desses sites. Se você receber um erro de certificado quando scraping desmarque isto.", @@ -254,9 +255,9 @@ "create_galleries_from_folders_label": "Crie galerias de pastas contendo imagens", "db_path_head": "Caminho do banco de dados", "directory_locations_to_your_content": "Locais de diretório para o seu conteúdo", - "excluded_image_gallery_patterns_desc": "Regexps de imagem e galeria de arquivos/caminhos para excluir da Varredura e adicionar para Limpar", + "excluded_image_gallery_patterns_desc": "Regexps de imagem e galeria de arquivos/caminhos para excluir do escaneamento e adicionar para limpar", "excluded_image_gallery_patterns_head": "Padrões de imagem/galeria excluidos", - "excluded_video_patterns_desc": "Regexps de video arquivos/caminhos para excluir da Varredura e adicionar para Limpar", + "excluded_video_patterns_desc": "Regexps de video arquivos/caminhos para excluir do escaneamento e adicionar para limpar", "excluded_video_patterns_head": "Padrões de vídeo excluidos", "gallery_ext_desc": "Lista delimitada por vírgulas de extensões de arquivo que serão identificadas como arquivos ZIP da galeria.", "gallery_ext_head": "Extensões zip da galeria", @@ -264,12 +265,12 @@ "generated_file_naming_hash_head": "Hash de nomeação de arquivo gerado", "generated_files_location": "Local de diretório para os arquivos gerados (marcadores de cena, pré visualizações de cena, sprites, etc)", "generated_path_head": "Caminho gerado", - "hashing": "Hashing", + "hashing": "Criação de Hash", "image_ext_desc": "Lista delimitada por vírgulas de extensões de arquivo que serão identificadas como imagens.", "image_ext_head": "Extensões de imagem", "include_audio_desc": "Inclui stream de áudio quando gerar pré-visualizações.", "include_audio_head": "Incluir áudio", - "logging": "Logging", + "logging": "Registro", "maximum_streaming_transcode_size_desc": "Tamanho máximo para streams transcodificadas", "maximum_streaming_transcode_size_head": "Tamanho máximo de transcodação de streaming", "maximum_transcode_size_desc": "Tamanho máximo para transcodificações geradas", @@ -279,20 +280,20 @@ "heading": "Caminho dos Metadados" }, "number_of_parallel_task_for_scan_generation_desc": "Defina como 0 para detecção automática. AVISO Execução de mais tarefas do que é necessário para obter 100% de utilização da CPU diminuirá o desempenho e potencialmente causar outros problemas.", - "number_of_parallel_task_for_scan_generation_head": "Número de tarefas paralelas para varredura/geração", - "parallel_scan_head": "Escaneamento/Geração paralela", + "number_of_parallel_task_for_scan_generation_head": "Número de tarefas paralelas para escaneamento/geração", + "parallel_scan_head": "Escaneamento/geração paralela", "preview_generation": "Geração de pré-visualização", "python_path": { "description": "Caminho do executável do Python. Utilizado para scripts de scrape e plugins. Se em branco, o caminho do Python será resolvido a partir do ambiente", "heading": "Caminho do Python" }, - "scraper_user_agent": "Scraper User Agent", + "scraper_user_agent": "Agente de Usuário do Extrator", "scraper_user_agent_desc": "User-Agent string usado durante solicitações http do scrape", "scrapers_path": { "description": "Caminho para o diretório para os arquivos de configuração de scrapers", "heading": "Caminho dos scrapers" }, - "scraping": "Scraping", + "scraping": "Extração", "sqlite_location": "Localização do arquivo para o banco de dados SQLite (requer reinicialização)", "video_ext_desc": "Lista delimitada por vírgulas de extensões de arquivo que serão identificadas como vídeos.", "video_ext_head": "Extensões de vídeo", @@ -304,10 +305,10 @@ "media_content_extensions": "Extensões de arquivo de mídia" }, "logs": { - "log_level": "Nível de log" + "log_level": "Nível de registro" }, "plugins": { - "hooks": "Hooks", + "hooks": "Ganchos", "triggers_on": "Triggers on" }, "scraping": { @@ -315,8 +316,8 @@ "entity_scrapers": "Scrapers de {entityType}", "excluded_tag_patterns_desc": "Expressões regulares de etiquetas para excluir dos resultados da busca", "excluded_tag_patterns_head": "Padrões de etiqueta excluídos", - "scraper": "Scraper", - "scrapers": "Scrapers", + "scraper": "Extrator", + "scrapers": "Extratores", "search_by_name": "Buscar por nome", "supported_types": "Tipos suportados", "supported_urls": "URLs" @@ -325,7 +326,7 @@ "add_instance": "Adicionar uma instância stash-box", "api_key": "Chave de API", "description": "Stash-box facilita o etiquetamento automático de cenas e artistas baseados em 'impressões digitais' e nomes de arquivos.\nEndpoint e chave de API pode ser encontrado na sua página de conta na instancia stash-box. Os nomes são necessários quando mais de uma instância são adicionados.", - "endpoint": "Endpoint", + "endpoint": "Ponto Final", "graphql_endpoint": "Endpoint GraphQL", "name": "Nome", "title": "Endpoints do Stash-box" @@ -390,13 +391,13 @@ "maintenance": "Manutenção", "migrate_hash_files": "Usado depois de alterar o hash gerado de nomeação de arquivos para renomear arquivos gerados existentes para o novo formato hash.", "migrations": "Migrações", - "only_dry_run": "Executar apenas modo não destrutivo. Não remova nada", + "only_dry_run": "Executar em modo não destrutivo. Não remova nada", "plugin_tasks": "Tarefas de plugin", "scan": { "scanning_all_paths": "Escaneando todos os caminhos", "scanning_paths": "Escaneando os seguintes caminhos" }, - "scan_for_content_desc": "Varre para novos conteúdos e adicioná-los ao banco de dados.", + "scan_for_content_desc": "Escaneie por novos conteúdos e os adicione ao banco de dados.", "set_name_date_details_from_metadata_if_present": "Definir nome, data e detalhes a partir dos meta-dados do arquivo" }, "tools": { @@ -453,6 +454,9 @@ }, "handy_connection": { "connect": "Conectar", + "server_offset": { + "heading": "Compensação do Servidor" + }, "status": { "heading": "Estado da conexão do Handy" }, @@ -462,6 +466,9 @@ "description": "Chave de conexão para usar em cenas interativas. Ativar esta chave permitirá o Stash a compartilhar as informações da cena atual com handyfeeling.com", "heading": "Chave de conexão" }, + "image_lightbox": { + "heading": "Galeria de imagem" + }, "images": { "heading": "Imagens", "options": { @@ -587,7 +594,7 @@ "aliases_must_be_unique": "apelidos devem ser únicos", "delete_alert": "Os seguintes {count, plural, one {{singularEntity}} other {{pluralEntity}}} serão deletados permanentemente:", "delete_confirm": "Tem certeza de que deseja excluir {entityName}?", - "delete_entity_desc": "{count, plural, one {Tem certeza de que deseja excluir este(a) {singularEntity}? A menos que o arquivo também seja excluído, este(a) {singularEntity} será re-adicionado quando a varredura for executada.} other {Tem certeza de que deseja excluir estes(as) {pluralEntity}? A menos que os arquivos também sejam excluídos, estes(as) {pluralEntity} serão re-adicionados quando a varredura for executada.}}", + "delete_entity_desc": "{count, plural, one {Tem certeza de que deseja excluir este(a) {singularEntity}? A menos que o arquivo também esteja excluído, este(a) {singularEntity} será re-adicionado quando a escaneamento for executado.} other {Tem certeza de que deseja excluir estes(as) {pluralEntity}? A menos que os arquivos também estejam excluídos, estes(as) {pluralEntity} serão re-adicionados quando o escaneamento for executado.}}", "delete_entity_title": "{count, plural, one {Excluir {singularEntity}} other {Excluir {pluralEntity}}}", "delete_galleries_extra": "...e qualquer imagem não anexada a uma galeria.", "delete_gallery_files": "Deletar diretório, arquivo zip ou imagem não anexada a alguma galeria.", @@ -631,7 +638,7 @@ "interactive_heatmap_speed": "Gerar heatmaps e velocidades para cenas interativas", "marker_image_previews": "Pré-visualizações de Imagem Animada para Marcadores", "marker_image_previews_tooltip": "Pré-visualizações WebP animadas para marcadores, necessário apenas de o Tipo de Pré-visualização estiver configurado para Imagem Animada.", - "marker_screenshots": "Capturas de Tela de Marcadores", + "marker_screenshots": "Capturas de tela de Marcadores", "marker_screenshots_tooltip": "Imagens JPG estáticas para marcadores, necessário apenas se o Tipo de Pré-visualização estiver configurado para Imagem Estática.", "markers": "Pré-visualizações de Marcadores", "markers_tooltip": "Vídeos de 20 segundos que iniciam em dado código de tempo.", @@ -728,6 +735,12 @@ "filters": "Filtros", "framerate": "Taxa de quadros", "frames_per_second": "{value} quadros por segundo", + "front_page": { + "types": { + "premade_filter": "Filtro pré-pronto", + "saved_filter": "Filtro salvo" + } + }, "galleries": "Galerias", "gallery": "Galeria", "gallery_count": "Contagem de galeria", @@ -782,7 +795,7 @@ "age_context": "{age} {years_old} nesta cena" }, "phash": "PHash", - "stream": "Stream", + "stream": "Transmissão", "video_codec": "Codec de vídeo" }, "megabits_per_second": "{value} megabits por segundo", @@ -855,11 +868,8 @@ "queue": "Fila", "random": "Aleatória", "rating": "Avaliação", - "recently_added_performers": "Artistas recentemente adicionados", - "recently_added_studios": "Estúdios recentemente adicionados", - "recently_released_galleries": "Galerias recentemente lançadas", - "recently_released_movies": "Filmes recentemente lançados", - "recently_released_scenes": "Cenas recentemente lançadas", + "recently_added_objects": "{objects} Recentemente Adicionados", + "recently_released_objects": "{objects} Recentemente Lançados", "resolution": "Resolução", "scene": "Cena", "sceneTagger": "Etiquetador de cena", @@ -1003,6 +1013,7 @@ "total": "Total", "true": "Verdadeiro", "twitter": "Twitter", + "type": "Tipo", "updated_at": "Atualizado em", "url": "URL", "videos": "Vídeos", diff --git a/ui/v2.5/src/locales/sv-SE.json b/ui/v2.5/src/locales/sv-SE.json index c26b67589..05f6fce9f 100644 --- a/ui/v2.5/src/locales/sv-SE.json +++ b/ui/v2.5/src/locales/sv-SE.json @@ -23,6 +23,7 @@ "create_entity": "Skapa {entityType}", "create_marker": "Skapa markör", "created_entity": "Skapade {entity_type}: {entity_name}", + "customise": "Ändra", "delete": "Radera", "delete_entity": "Radera {entityType}", "delete_file": "Radera fil", @@ -100,6 +101,7 @@ "stop": "Stoppa", "submit": "Skicka", "submit_stash_box": "Skicka till Stash-Box", + "submit_update": "Skicka uppdatering", "tasks": { "clean_confirm_message": "Är du säker att du vill rensa? Detta kommer radera databasinformation och genererade filer för alla scener och gallerier som inte längre finns på filsystemet.", "dry_mode_selected": "Torrt läge valt. Inget kommer raderas utan bara loggning kommer ske.", @@ -733,6 +735,12 @@ "filters": "Filter", "framerate": "Bildhastighet", "frames_per_second": "{value} bilder per sekund", + "front_page": { + "types": { + "premade_filter": "Förhandsgjorda filter", + "saved_filter": "Sparade Filter" + } + }, "galleries": "Gallerier", "gallery": "Galleri", "gallery_count": "Antal Gallerier", @@ -860,11 +868,8 @@ "queue": "Kö", "random": "Slumpad", "rating": "Betyg", - "recently_added_performers": "Nyligen Tillagda Stjärnor", - "recently_added_studios": "Nyligen Tillagda Studior", - "recently_released_galleries": "Nyligen Släppta Gallerier", - "recently_released_movies": "Nyligen Släppta Filmer", - "recently_released_scenes": "Nyligen Släppta Scener", + "recently_added_objects": "Nyligen Tillagda {objects}", + "recently_released_objects": "Nyligen Släppta {objects}", "resolution": "Upplösning", "scene": "Scen", "sceneTagger": "Scentaggaren", @@ -966,7 +971,8 @@ "go_review_draft": "Gå till {endpoint_name} för att granska utkast.", "selected_stash_box": "Vald stash-box adress", "submission_failed": "Misslyckad inskickning", - "submission_successful": "Lyckad inskickning" + "submission_successful": "Lyckad inskickning", + "submit_update": "Existerar redan i {endpoint_name}" }, "statistics": "Statistik", "stats": { @@ -1007,6 +1013,7 @@ "total": "Total", "true": "Sant", "twitter": "Twitter", + "type": "Typ", "updated_at": "Uppdaterad vid", "url": "URL", "videos": "Videor", diff --git a/ui/v2.5/src/locales/zh-CN.json b/ui/v2.5/src/locales/zh-CN.json index e269f3e91..f57fc83cf 100644 --- a/ui/v2.5/src/locales/zh-CN.json +++ b/ui/v2.5/src/locales/zh-CN.json @@ -844,11 +844,6 @@ "queue": "序列", "random": "随机", "rating": "评分", - "recently_added_performers": "最近新增的演员", - "recently_added_studios": "最近新增的工作室", - "recently_released_galleries": "最近新增的图片", - "recently_released_movies": "最近新增的电影", - "recently_released_scenes": "最近新增的短片", "resolution": "分辨率", "scene": "短片", "sceneTagger": "短片标记器", diff --git a/ui/v2.5/src/locales/zh-TW.json b/ui/v2.5/src/locales/zh-TW.json index 0a784868e..fe4bf7675 100644 --- a/ui/v2.5/src/locales/zh-TW.json +++ b/ui/v2.5/src/locales/zh-TW.json @@ -23,6 +23,7 @@ "create_entity": "建立{entityType}", "create_marker": "建立章節標記", "created_entity": "已建立{entity_type}:{entity_name}", + "customise": "自訂", "delete": "刪除", "delete_entity": "刪除{entityType}", "delete_file": "刪除檔案", @@ -861,11 +862,8 @@ "queue": "佇列", "random": "隨機", "rating": "評比", - "recently_added_performers": "最近新增的演員", - "recently_added_studios": "最近新增的工作室", - "recently_released_galleries": "最近釋出的圖庫", - "recently_released_movies": "最近釋出的電影", - "recently_released_scenes": "最近釋出的短片", + "recently_added_objects": "最近新增的{objects}", + "recently_released_objects": "最近釋出的{objects}", "resolution": "解析度", "scene": "短片", "sceneTagger": "短片標籤器", @@ -1009,6 +1007,7 @@ "total": "總計", "true": "是", "twitter": "Twitter", + "type": "種類", "updated_at": "更新於", "url": "連結", "videos": "影片", From 3b4b20e9b26b792e89989f02c7597e468164fce2 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 22 Jun 2022 14:41:31 +1000 Subject: [PATCH 26/34] React code splitting (#2603) * Code split using react lazy * Split locales * Move to lodash-es * Import individual icons --- ui/v2.5/package.json | 4 +- ui/v2.5/src/App.tsx | 179 ++++++++++-------- ui/v2.5/src/components/Changelog/Version.tsx | 3 +- .../src/components/Dialogs/GenerateDialog.tsx | 7 +- .../Dialogs/IdentifyDialog/FieldOptions.tsx | 15 +- .../Dialogs/IdentifyDialog/IdentifyDialog.tsx | 11 +- .../Dialogs/IdentifyDialog/Sources.tsx | 17 +- .../src/components/Dialogs/SubmitDraft.tsx | 5 +- .../Galleries/DeleteGalleriesDialog.tsx | 3 +- .../Galleries/EditGalleriesDialog.tsx | 9 +- .../src/components/Galleries/GalleryCard.tsx | 7 +- .../Galleries/GalleryDetails/Gallery.tsx | 3 +- .../GalleryDetails/GalleryAddPanel.tsx | 4 +- .../GalleryDetails/GalleryEditPanel.tsx | 3 +- .../GalleryDetails/GalleryImagesPanel.tsx | 4 +- .../GalleryDetails/GalleryScrapeDialog.tsx | 4 +- .../src/components/Galleries/GalleryList.tsx | 4 +- .../components/Galleries/GalleryViewer.tsx | 2 + ui/v2.5/src/components/Help/Manual.tsx | 62 +----- ui/v2.5/src/components/Help/context.tsx | 73 +++++++ .../components/Images/DeleteImagesDialog.tsx | 3 +- .../components/Images/EditImagesDialog.tsx | 9 +- ui/v2.5/src/components/Images/ImageCard.tsx | 14 +- .../components/Images/ImageDetails/Image.tsx | 3 +- ui/v2.5/src/components/Images/ImageList.tsx | 4 +- .../src/components/List/AddFilterDialog.tsx | 6 +- ui/v2.5/src/components/List/FilterTags.tsx | 3 +- ui/v2.5/src/components/List/ListFilter.tsx | 36 ++-- .../components/List/ListOperationButtons.tsx | 17 +- .../src/components/List/ListViewOptions.tsx | 14 +- .../src/components/List/SavedFilterList.tsx | 7 +- ui/v2.5/src/components/MainNavbar.tsx | 50 +++-- .../components/Movies/EditMoviesDialog.tsx | 3 +- ui/v2.5/src/components/Movies/MovieCard.tsx | 3 +- .../components/Movies/MovieDetails/Movie.tsx | 3 +- ui/v2.5/src/components/Movies/MovieList.tsx | 4 +- .../Performers/EditPerformersDialog.tsx | 3 +- .../components/Performers/PerformerCard.tsx | 5 +- .../Performers/PerformerDetails/Performer.tsx | 14 +- .../PerformerDetails/PerformerEditPanel.tsx | 11 +- .../PerformerScrapeDialog.tsx | 4 +- .../PerformerDetails/PerformerScrapeModal.tsx | 2 +- .../PerformerStashBoxModal.tsx | 2 +- .../components/Performers/PerformerList.tsx | 4 +- .../Performers/PerformerListTable.tsx | 3 +- .../SceneDuplicateChecker.tsx | 28 ++- .../SceneFilenameParser.tsx | 10 +- .../SceneFilenameParser/SceneParserRow.tsx | 19 +- .../SceneFilenameParser/ShowFields.tsx | 10 +- .../components/ScenePlayer/ScenePlayer.tsx | 6 +- ui/v2.5/src/components/ScenePlayer/util.ts | 6 + .../components/Scenes/DeleteScenesDialog.tsx | 5 +- .../components/Scenes/EditScenesDialog.tsx | 11 +- ui/v2.5/src/components/Scenes/SceneCard.tsx | 20 +- .../SceneDetails/ExternalPlayerButton.tsx | 9 +- .../Scenes/SceneDetails/OCounterButton.tsx | 5 +- .../Scenes/SceneDetails/OrganizedButton.tsx | 5 +- .../Scenes/SceneDetails/PrimaryTags.tsx | 2 +- .../Scenes/SceneDetails/QueueViewer.tsx | 23 ++- .../Scenes/SceneDetails/RatingStars.tsx | 19 +- .../components/Scenes/SceneDetails/Scene.tsx | 50 +++-- .../Scenes/SceneDetails/SceneDetailPanel.tsx | 7 +- .../Scenes/SceneDetails/SceneEditPanel.tsx | 24 ++- .../SceneDetails/SceneFileInfoPanel.tsx | 2 + .../SceneDetails/SceneGalleriesPanel.tsx | 2 + .../Scenes/SceneDetails/SceneMarkerForm.tsx | 4 +- .../Scenes/SceneDetails/SceneMarkersPanel.tsx | 2 + .../Scenes/SceneDetails/SceneMoviePanel.tsx | 2 + .../Scenes/SceneDetails/SceneQueryModal.tsx | 7 +- .../Scenes/SceneDetails/SceneScrapeDialog.tsx | 10 +- .../SceneDetails/SceneVideoFilterPanel.tsx | 6 +- ui/v2.5/src/components/Scenes/SceneList.tsx | 10 +- .../src/components/Scenes/SceneListTable.tsx | 2 +- .../src/components/Scenes/SceneMarkerList.tsx | 6 +- ui/v2.5/src/components/Scenes/Scenes.tsx | 9 +- ui/v2.5/src/components/Settings/Inputs.tsx | 3 +- ui/v2.5/src/components/Settings/Settings.tsx | 2 + .../Settings/SettingsLibraryPanel.tsx | 5 +- .../Settings/SettingsPluginsPanel.tsx | 5 +- .../Settings/SettingsScrapingPanel.tsx | 3 +- .../Settings/SettingsServicesPanel.tsx | 15 +- .../Settings/StashConfiguration.tsx | 3 +- .../Settings/Tasks/DataManagementTasks.tsx | 18 +- .../Tasks/DirectorySelectionDialog.tsx | 11 +- .../Settings/Tasks/ImportDialog.tsx | 3 +- .../components/Settings/Tasks/JobTable.tsx | 23 ++- .../Settings/Tasks/LibraryTasks.tsx | 11 +- ui/v2.5/src/components/Settings/context.tsx | 10 +- ui/v2.5/src/components/SettingsButton.tsx | 3 +- ui/v2.5/src/components/Setup/Migrate.tsx | 2 + ui/v2.5/src/components/Setup/Setup.tsx | 13 +- .../components/Shared/BulkUpdateTextInput.tsx | 5 +- .../src/components/Shared/CollapseButton.tsx | 8 +- .../components/Shared/DeleteEntityDialog.tsx | 5 +- .../components/Shared/DetailsEditNavbar.tsx | 2 +- .../src/components/Shared/DurationInput.tsx | 13 +- .../src/components/Shared/ExportDialog.tsx | 9 +- .../Shared/FolderSelect/FolderSelect.tsx | 8 +- ui/v2.5/src/components/Shared/Icon.tsx | 12 +- ui/v2.5/src/components/Shared/ImageInput.tsx | 7 +- ui/v2.5/src/components/Shared/Modal.tsx | 6 +- .../src/components/Shared/OperationButton.tsx | 2 +- .../Shared/PerformerPopoverButton.tsx | 3 +- .../components/Shared/PopoverCountButton.tsx | 14 +- ui/v2.5/src/components/Shared/RatingStars.tsx | 12 +- .../src/components/Shared/ScrapeDialog.tsx | 27 ++- ui/v2.5/src/components/Shared/Select.tsx | 2 +- .../src/components/Shared/StringListInput.tsx | 7 +- ui/v2.5/src/components/Shared/SuccessIcon.tsx | 5 +- ui/v2.5/src/components/Shared/TagLink.tsx | 3 +- .../components/Shared/ThreeStateCheckbox.tsx | 5 +- .../src/components/Shared/TruncatedText.tsx | 2 +- ui/v2.5/src/components/Shared/URLField.tsx | 5 +- ui/v2.5/src/components/Stats.tsx | 2 + .../Studios/StudioDetails/Studio.tsx | 3 +- .../Studios/StudioDetails/StudioEditPanel.tsx | 3 +- ui/v2.5/src/components/Studios/StudioList.tsx | 4 +- .../src/components/Tagger/IncludeButton.tsx | 3 +- .../Tagger/PerformerFieldSelector.tsx | 5 +- .../src/components/Tagger/PerformerModal.tsx | 21 +- .../Tagger/performers/PerformerTagger.tsx | 7 +- .../Tagger/performers/StashSearchResult.tsx | 3 +- ui/v2.5/src/components/Tagger/queries.ts | 2 +- .../src/components/Tagger/scenes/Config.tsx | 3 +- .../Tagger/scenes/PerformerResult.tsx | 3 +- .../components/Tagger/scenes/SceneTagger.tsx | 3 +- .../Tagger/scenes/StashSearchResult.tsx | 5 +- .../components/Tagger/scenes/StudioModal.tsx | 7 +- .../components/Tagger/scenes/StudioResult.tsx | 3 +- .../components/Tagger/scenes/TaggerScene.tsx | 3 +- .../Tagger/scenes/sceneTaggerModals.tsx | 5 +- ui/v2.5/src/components/Tags/TagCard.tsx | 5 +- .../src/components/Tags/TagDetails/Tag.tsx | 11 +- .../Tags/TagDetails/TagMergeDialog.tsx | 3 +- ui/v2.5/src/components/Tags/TagList.tsx | 9 +- ui/v2.5/src/core/StashService.ts | 2 +- ui/v2.5/src/hooks/Interactive/status.tsx | 3 +- ui/v2.5/src/hooks/Interval.ts | 2 +- ui/v2.5/src/hooks/Lightbox/Lightbox.tsx | 47 ++--- ui/v2.5/src/hooks/Lightbox/context.tsx | 14 +- ui/v2.5/src/hooks/Lightbox/types.ts | 14 ++ ui/v2.5/src/hooks/ListHook.tsx | 25 ++- ui/v2.5/src/hooks/LocalForage.ts | 4 +- ui/v2.5/src/locales/index.ts | 63 +++--- ui/v2.5/src/models/sceneQueue.ts | 2 + ui/v2.5/src/utils/bulkUpdate.ts | 10 +- ui/v2.5/yarn.lock | 21 +- 147 files changed, 969 insertions(+), 610 deletions(-) create mode 100644 ui/v2.5/src/components/Help/context.tsx create mode 100644 ui/v2.5/src/components/ScenePlayer/util.ts create mode 100644 ui/v2.5/src/hooks/Lightbox/types.ts diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index 67d128a9a..1793ccf36 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -48,7 +48,7 @@ "i18n-iso-countries": "^6.4.0", "intersection-observer": "^0.12.0", "localforage": "1.9.0", - "lodash": "^4.17.20", + "lodash-es": "^4.17.21", "mousetrap": "^1.6.5", "mousetrap-pause": "^1.0.0", "normalize-url": "^4.5.1", @@ -92,7 +92,7 @@ "@types/apollo-upload-client": "^14.1.0", "@types/classnames": "^2.2.11", "@types/fslightbox-react": "^1.4.0", - "@types/lodash": "^4.14.168", + "@types/lodash-es": "^4.17.6", "@types/mousetrap": "^1.6.5", "@types/node": "14.14.22", "@types/react": "17.0.31", diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index 7a3abdd2a..f008e1d97 100755 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -1,12 +1,11 @@ -import React, { useEffect } from "react"; +import React, { lazy, Suspense, useEffect, useState } from "react"; import { Route, Switch, useRouteMatch } from "react-router-dom"; import { IntlProvider, CustomFormats } from "react-intl"; import { Helmet } from "react-helmet"; -import { mergeWith } from "lodash"; +import cloneDeep from "lodash-es/cloneDeep"; +import mergeWith from "lodash-es/mergeWith"; import { ToastProvider } from "src/hooks/Toast"; import LightboxProvider from "src/hooks/Lightbox/context"; -import { library } from "@fortawesome/fontawesome-svg-core"; -import { fas } from "@fortawesome/free-solid-svg-icons"; import { initPolyfills } from "src/polyfills"; import locales from "src/locales"; @@ -15,41 +14,48 @@ import { flattenMessages } from "src/utils"; import Mousetrap from "mousetrap"; import MousetrapPause from "mousetrap-pause"; import { ErrorBoundary } from "./components/ErrorBoundary"; -import Galleries from "./components/Galleries/Galleries"; import { MainNavbar } from "./components/MainNavbar"; import { PageNotFound } from "./components/PageNotFound"; -import Performers from "./components/Performers/Performers"; -import FrontPage from "./components/FrontPage/FrontPage"; -import Scenes from "./components/Scenes/Scenes"; -import { Settings } from "./components/Settings/Settings"; -import { Stats } from "./components/Stats"; -import Studios from "./components/Studios/Studios"; -import { SceneFilenameParser } from "./components/SceneFilenameParser/SceneFilenameParser"; -import { SceneDuplicateChecker } from "./components/SceneDuplicateChecker/SceneDuplicateChecker"; -import Movies from "./components/Movies/Movies"; -import Tags from "./components/Tags/Tags"; -import Images from "./components/Images/Images"; -import { Setup } from "./components/Setup/Setup"; -import { Migrate } from "./components/Setup/Migrate"; import * as GQL from "./core/generated-graphql"; import { LoadingIndicator, TITLE_SUFFIX } from "./components/Shared"; + import { ConfigurationProvider } from "./hooks/Config"; -import { ManualProvider } from "./components/Help/Manual"; +import { ManualProvider } from "./components/Help/context"; import { InteractiveProvider } from "./hooks/Interactive/context"; +const Performers = lazy(() => import("./components/Performers/Performers")); +const FrontPage = lazy(() => import("./components/FrontPage/FrontPage")); +const Scenes = lazy(() => import("./components/Scenes/Scenes")); +const Settings = lazy(() => import("./components/Settings/Settings")); +const Stats = lazy(() => import("./components/Stats")); +const Studios = lazy(() => import("./components/Studios/Studios")); +const Galleries = lazy(() => import("./components/Galleries/Galleries")); + +const Movies = lazy(() => import("./components/Movies/Movies")); +const Tags = lazy(() => import("./components/Tags/Tags")); +const Images = lazy(() => import("./components/Images/Images")); +const Setup = lazy(() => import("./components/Setup/Setup")); +const Migrate = lazy(() => import("./components/Setup/Migrate")); + +const SceneFilenameParser = lazy( + () => import("./components/SceneFilenameParser/SceneFilenameParser") +); +const SceneDuplicateChecker = lazy( + () => import("./components/SceneDuplicateChecker/SceneDuplicateChecker") +); + initPolyfills(); MousetrapPause(Mousetrap); -// Set fontawesome/free-solid-svg as default fontawesome icons -library.add(fas); - const intlFormats: CustomFormats = { date: { long: { year: "numeric", month: "long", day: "numeric" }, }, }; +const defaultLocale = "en-GB"; + function languageMessageString(language: string) { return language.replace(/-/, ""); } @@ -57,25 +63,32 @@ function languageMessageString(language: string) { export const App: React.FC = () => { const config = useConfiguration(); const { data: systemStatusData } = useSystemStatus(); - const defaultLocale = "en-GB"; + const language = config.data?.configuration?.interface?.language ?? defaultLocale; - const defaultMessageLanguage = languageMessageString(defaultLocale); - const messageLanguage = languageMessageString(language); // use en-GB as default messages if any messages aren't found in the chosen language - const mergedMessages = mergeWith( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (locales as any)[defaultMessageLanguage], - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (locales as any)[messageLanguage], - (objVal, srcVal) => { - if (srcVal === "") { - return objVal; - } - } - ); - const messages = flattenMessages(mergedMessages); + const [messages, setMessages] = useState<{}>(); + + useEffect(() => { + const setLocale = async () => { + const defaultMessageLanguage = languageMessageString(defaultLocale); + const messageLanguage = languageMessageString(language); + + const defaultMessages = await locales[defaultMessageLanguage](); + const mergedMessages = cloneDeep(Object.assign({}, defaultMessages)); + const chosenMessages = await locales[messageLanguage](); + mergeWith(mergedMessages, chosenMessages, (objVal, srcVal) => { + if (srcVal === "") { + return objVal; + } + }); + + setMessages(flattenMessages(mergedMessages)); + }; + + setLocale(); + }, [language]); const setupMatch = useRouteMatch(["/setup", "/migrate"]); @@ -118,52 +131,64 @@ export const App: React.FC = () => { } return ( - - - - - - - - - - - - - - - - - + }> + + + + + + + + + + + + + + + + + + ); } return ( - - - - - - - - {maybeRenderNavbar()} -
{renderContent()}
-
-
-
-
-
-
+ + + }> + + + + + {maybeRenderNavbar()} +
+ {renderContent()} +
+
+
+
+
+
+
+ + ) : null}
); }; diff --git a/ui/v2.5/src/components/Changelog/Version.tsx b/ui/v2.5/src/components/Changelog/Version.tsx index b274a7c3d..cd5b99442 100644 --- a/ui/v2.5/src/components/Changelog/Version.tsx +++ b/ui/v2.5/src/components/Changelog/Version.tsx @@ -1,3 +1,4 @@ +import { faAngleDown, faAngleUp } from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, Card, Collapse } from "react-bootstrap"; import { FormattedDate, FormattedMessage } from "react-intl"; @@ -33,7 +34,7 @@ const Version: React.FC = ({

} > @@ -205,3 +206,5 @@ export const GenerateDialog: React.FC = ({ ); }; + +export default GenerateDialog; diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/FieldOptions.tsx b/ui/v2.5/src/components/Dialogs/IdentifyDialog/FieldOptions.tsx index fad0ff7a0..8b04da91b 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/FieldOptions.tsx +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/FieldOptions.tsx @@ -5,6 +5,11 @@ import * as GQL from "src/core/generated-graphql"; import { FormattedMessage, useIntl } from "react-intl"; import { multiValueSceneFields, SceneField, sceneFields } from "./constants"; import { ThreeStateBoolean } from "./ThreeStateBoolean"; +import { + faCheck, + faPencilAlt, + faTimes, +} from "@fortawesome/free-solid-svg-icons"; interface IFieldOptionsEditor { options: GQL.IdentifyFieldOptions | undefined; @@ -148,10 +153,10 @@ const FieldOptionsEditor: React.FC = ({ return intl.formatMessage({ id: "actions.use_default" }); } if (value) { - return ; + return ; } - return ; + return ; } const defaultVal = defaultOptions?.fieldOptions?.find( @@ -212,7 +217,7 @@ const FieldOptionsEditor: React.FC = ({ className="minimal text-success" onClick={() => onEditOptions()} > - + ) : ( <> )} diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx b/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx index 49714e51c..f5964ee97 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx @@ -20,6 +20,11 @@ import { Manual } from "src/components/Help/Manual"; import { IScraperSource } from "./constants"; import { OptionsEditor } from "./Options"; import { SourcesEditor, SourcesList } from "./Sources"; +import { + faCogs, + faFolderOpen, + faQuestionCircle, +} from "@fortawesome/free-solid-svg-icons"; const autoTagScraperID = "builtin_autotag"; @@ -167,7 +172,7 @@ export const IdentifyDialog: React.FC = ({ title={intl.formatMessage({ id: "actions.select_folders" })} onClick={() => onClick()} > - +

@@ -403,7 +408,7 @@ export const IdentifyDialog: React.FC = ({ = ({ className="minimal help-button" onClick={() => onShowManual()} > - + } > diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/Sources.tsx b/ui/v2.5/src/components/Dialogs/IdentifyDialog/Sources.tsx index 81d213115..9cc0c6a51 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/Sources.tsx +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/Sources.tsx @@ -5,6 +5,13 @@ import { FormattedMessage, useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import { IScraperSource } from "./constants"; import { OptionsEditor } from "./Options"; +import { + faCog, + faGripVertical, + faMinus, + faPencilAlt, + faPlus, +} from "@fortawesome/free-solid-svg-icons"; interface ISourceEditor { isNew: boolean; @@ -50,7 +57,7 @@ export const SourcesEditor: React.FC = ({ dialogClassName="identify-source-editor" modalProps={{ animation: false, size: "lg" }} show - icon={isNew ? "plus" : "pencil-alt"} + icon={isNew ? faPlus : faPencilAlt} header={intl.formatMessage( { id: headerMsgId }, { @@ -184,19 +191,19 @@ export const SourcesList: React.FC = ({ onMouseEnter={() => setMouseOverIndex(index)} onMouseLeave={() => setMouseOverIndex(undefined)} > - + {s.displayName}
@@ -208,7 +215,7 @@ export const SourcesList: React.FC = ({ className="minimal add-scraper-source-button" onClick={() => editSource()} > - + )} diff --git a/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx b/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx index 6fb6d81fa..78f433ea9 100644 --- a/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx +++ b/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx @@ -5,6 +5,7 @@ import * as GQL from "src/core/generated-graphql"; import { Modal } from "src/components/Shared"; import { getStashboxBase } from "src/utils"; import { FormattedMessage, useIntl } from "react-intl"; +import { faPaperPlane } from "@fortawesome/free-solid-svg-icons"; interface IProps { show: boolean; @@ -72,7 +73,7 @@ export const SubmitStashBoxDraft: React.FC = ({ return ( = ({ ); }; + +export default SubmitStashBoxDraft; diff --git a/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx b/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx index bff3dd5f7..d4122be78 100644 --- a/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx +++ b/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx @@ -6,6 +6,7 @@ import { Modal } from "src/components/Shared"; import { useToast } from "src/hooks"; import { ConfigurationContext } from "src/hooks/Config"; import { FormattedMessage, useIntl } from "react-intl"; +import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; interface IDeleteGalleryDialogProps { selected: GQL.SlimGalleryDataFragment[]; @@ -114,7 +115,7 @@ export const DeleteGalleriesDialog: React.FC = ( return ( = ( if (GalleriestudioID !== updateStudioID) { updateStudioID = undefined; } - if (!_.isEqual(galleryPerformerIDs, updatePerformerIds)) { + if (!isEqual(galleryPerformerIDs, updatePerformerIds)) { updatePerformerIds = []; } - if (!_.isEqual(galleryTagIDs, updateTagIds)) { + if (!isEqual(galleryTagIDs, updateTagIds)) { updateTagIds = []; } if (gallery.organized !== updateOrganized) { @@ -229,7 +230,7 @@ export const EditGalleriesDialog: React.FC = ( return ( = (props) => { content={popoverContent} > @@ -62,7 +63,7 @@ export const GalleryCard: React.FC = (props) => { content={popoverContent} > @@ -113,7 +114,7 @@ export const GalleryCard: React.FC = (props) => { return (
); diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx index b9f2673dc..1ef62af37 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx @@ -21,6 +21,7 @@ import { GalleryImagesPanel } from "./GalleryImagesPanel"; import { GalleryAddPanel } from "./GalleryAddPanel"; import { GalleryFileInfoPanel } from "./GalleryFileInfoPanel"; import { GalleryScenesPanel } from "./GalleryScenesPanel"; +import { faEllipsisV } from "@fortawesome/free-solid-svg-icons"; interface IProps { gallery: GQL.GalleryDataFragment; @@ -116,7 +117,7 @@ export const GalleryPage: React.FC = ({ gallery }) => { className="minimal" title={intl.formatMessage({ id: "operations" })} > - + {gallery.path ? ( diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx index e256251e0..15343f7e4 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryAddPanel.tsx @@ -8,7 +8,7 @@ import { mutateAddGalleryImages } from "src/core/StashService"; import { useToast } from "src/hooks"; import { TextUtils } from "src/utils"; import { useIntl } from "react-intl"; -import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; interface IGalleryAddProps { gallery: GQL.GalleryDataFragment; @@ -88,7 +88,7 @@ export const GalleryAddPanel: React.FC = ({ gallery }) => { onClick: addImages, isDisplayed: showWhenSelected, postRefetch: true, - icon: "plus" as IconProp, + icon: faPlus, }, ]; diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx index b85f1bf7f..e6711da47 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryEditPanel.tsx @@ -34,6 +34,7 @@ import { useFormik } from "formik"; import { FormUtils, TextUtils } from "src/utils"; import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; import { GalleryScrapeDialog } from "./GalleryScrapeDialog"; +import { faSyncAlt } from "@fortawesome/free-solid-svg-icons"; interface IProps { isVisible: boolean; @@ -314,7 +315,7 @@ export const GalleryEditPanel: React.FC< ))} onReloadScrapers()}> - + diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx index e58bdb9c8..1aa7fa2a5 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryImagesPanel.tsx @@ -8,7 +8,7 @@ import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook"; import { useToast } from "src/hooks"; import { TextUtils } from "src/utils"; import { useIntl } from "react-intl"; -import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { faMinus } from "@fortawesome/free-solid-svg-icons"; interface IGalleryDetailsProps { gallery: GQL.GalleryDataFragment; @@ -82,7 +82,7 @@ export const GalleryImagesPanel: React.FC = ({ onClick: removeImages, isDisplayed: showWhenSelected, postRefetch: true, - icon: "minus" as IconProp, + icon: faMinus, buttonVariant: "danger", }, ]; diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx index a0b82a67d..917e72b28 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx @@ -10,7 +10,7 @@ import { ScrapedInputGroupRow, ScrapedTextAreaRow, } from "src/components/Shared/ScrapeDialog"; -import _ from "lodash"; +import clone from "lodash-es/clone"; import { useStudioCreate, usePerformerCreate, @@ -235,7 +235,7 @@ export const GalleryScrapeDialog: React.FC = ( return; } - const ret = _.clone(idList); + const ret = clone(idList); // sort by id numerically ret.sort((a, b) => { return parseInt(a, 10) - parseInt(b, 10); diff --git a/ui/v2.5/src/components/Galleries/GalleryList.tsx b/ui/v2.5/src/components/Galleries/GalleryList.tsx index cb240f13d..83a8eef90 100644 --- a/ui/v2.5/src/components/Galleries/GalleryList.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryList.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { useIntl } from "react-intl"; -import _ from "lodash"; +import cloneDeep from "lodash-es/cloneDeep"; import { Table } from "react-bootstrap"; import { Link, useHistory } from "react-router-dom"; import Mousetrap from "mousetrap"; @@ -84,7 +84,7 @@ export const GalleryList: React.FC = ({ const { count } = result.data.findGalleries; const index = Math.floor(Math.random() * count); - const filterCopy = _.cloneDeep(filter); + const filterCopy = cloneDeep(filter); filterCopy.itemsPerPage = 1; filterCopy.currentPage = index + 1; const singleResult = await queryFindGalleries(filterCopy); diff --git a/ui/v2.5/src/components/Galleries/GalleryViewer.tsx b/ui/v2.5/src/components/Galleries/GalleryViewer.tsx index 6a9ac5136..7c69ede5b 100644 --- a/ui/v2.5/src/components/Galleries/GalleryViewer.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryViewer.tsx @@ -38,3 +38,5 @@ export const GalleryViewer: React.FC = ({ galleryId }) => { ); }; + +export default GalleryViewer; diff --git a/ui/v2.5/src/components/Help/Manual.tsx b/ui/v2.5/src/components/Help/Manual.tsx index 501563472..83f787c2f 100644 --- a/ui/v2.5/src/components/Help/Manual.tsx +++ b/ui/v2.5/src/components/Help/Manual.tsx @@ -1,4 +1,4 @@ -import React, { useState, PropsWithChildren, useEffect } from "react"; +import React, { useState, useEffect } from "react"; import { Modal, Container, Row, Col, Nav, Tab } from "react-bootstrap"; import Introduction from "src/docs/en/Introduction.md"; import Tasks from "src/docs/en/Tasks.md"; @@ -239,62 +239,4 @@ export const Manual: React.FC = ({ ); }; -interface IManualContextState { - openManual: (tab?: string) => void; -} - -export const ManualStateContext = React.createContext({ - openManual: () => {}, -}); - -export const ManualProvider: React.FC = ({ children }) => { - const [showManual, setShowManual] = useState(false); - const [manualLink, setManualLink] = useState(); - - function openManual(tab?: string) { - setManualLink(tab); - setShowManual(true); - } - - useEffect(() => { - if (manualLink) setManualLink(undefined); - }, [manualLink]); - - return ( - - setShowManual(false)} - defaultActiveTab={manualLink} - /> - {children} - - ); -}; - -interface IManualLink { - tab: string; -} - -export const ManualLink: React.FC> = ({ - tab, - children, -}) => { - const { openManual } = React.useContext(ManualStateContext); - - return ( - { - openManual(`${tab}.md`); - e.preventDefault(); - }} - > - {children} - - ); -}; +export default Manual; diff --git a/ui/v2.5/src/components/Help/context.tsx b/ui/v2.5/src/components/Help/context.tsx new file mode 100644 index 000000000..335838429 --- /dev/null +++ b/ui/v2.5/src/components/Help/context.tsx @@ -0,0 +1,73 @@ +import React, { + lazy, + PropsWithChildren, + Suspense, + useEffect, + useState, +} from "react"; + +const Manual = lazy(() => import("./Manual")); + +interface IManualContextState { + openManual: (tab?: string) => void; +} + +export const ManualStateContext = React.createContext({ + openManual: () => {}, +}); + +export const ManualProvider: React.FC = ({ children }) => { + const [showManual, setShowManual] = useState(false); + const [manualLink, setManualLink] = useState(); + + function openManual(tab?: string) { + setManualLink(tab); + setShowManual(true); + } + + useEffect(() => { + if (manualLink) setManualLink(undefined); + }, [manualLink]); + + return ( + + }> + {showManual && ( + setShowManual(false)} + defaultActiveTab={manualLink} + /> + )} + + {children} + + ); +}; + +interface IManualLink { + tab: string; +} + +export const ManualLink: React.FC> = ({ + tab, + children, +}) => { + const { openManual } = React.useContext(ManualStateContext); + + return ( + { + openManual(`${tab}.md`); + e.preventDefault(); + }} + > + {children} + + ); +}; diff --git a/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx b/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx index ee939ae9b..059be9f36 100644 --- a/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx +++ b/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx @@ -6,6 +6,7 @@ import { Modal } from "src/components/Shared"; import { useToast } from "src/hooks"; import { ConfigurationContext } from "src/hooks/Config"; import { FormattedMessage, useIntl } from "react-intl"; +import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; interface IDeleteImageDialogProps { selected: GQL.SlimImageDataFragment[]; @@ -106,7 +107,7 @@ export const DeleteImagesDialog: React.FC = ( return ( = ( if (imageStudioID !== updateStudioID) { updateStudioID = undefined; } - if (!_.isEqual(imagePerformerIDs, updatePerformerIds)) { + if (!isEqual(imagePerformerIDs, updatePerformerIds)) { updatePerformerIds = []; } - if (!_.isEqual(imageTagIDs, updateTagIds)) { + if (!isEqual(imageTagIDs, updateTagIds)) { updateTagIds = []; } if (image.organized !== updateOrganized) { @@ -219,7 +220,7 @@ export const EditImagesDialog: React.FC = ( return ( = ( content={popoverContent} > @@ -76,7 +82,7 @@ export const ImageCard: React.FC = ( content={popoverContent} > @@ -88,7 +94,7 @@ export const ImageCard: React.FC = ( return (
); @@ -146,7 +152,7 @@ export const ImageCard: React.FC = ( {props.onPreview ? (
) : undefined} diff --git a/ui/v2.5/src/components/Images/ImageDetails/Image.tsx b/ui/v2.5/src/components/Images/ImageDetails/Image.tsx index b4144c8f9..338e19e22 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/Image.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/Image.tsx @@ -21,6 +21,7 @@ import { ImageFileInfoPanel } from "./ImageFileInfoPanel"; import { ImageEditPanel } from "./ImageEditPanel"; import { ImageDetailPanel } from "./ImageDetailPanel"; import { DeleteImagesDialog } from "../DeleteImagesDialog"; +import { faEllipsisV } from "@fortawesome/free-solid-svg-icons"; interface IImageParams { id?: string; @@ -132,7 +133,7 @@ export const Image: React.FC = () => { className="minimal" title="Operations" > - + = ({ const { count } = result.data.findImages; const index = Math.floor(Math.random() * count); - const filterCopy = _.cloneDeep(filter); + const filterCopy = cloneDeep(filter); filterCopy.itemsPerPage = 1; filterCopy.currentPage = index + 1; const singleResult = await queryFindImages(filterCopy); diff --git a/ui/v2.5/src/components/List/AddFilterDialog.tsx b/ui/v2.5/src/components/List/AddFilterDialog.tsx index a370fab76..d7c0325bc 100644 --- a/ui/v2.5/src/components/List/AddFilterDialog.tsx +++ b/ui/v2.5/src/components/List/AddFilterDialog.tsx @@ -1,4 +1,4 @@ -import _ from "lodash"; +import cloneDeep from "lodash-es/cloneDeep"; import React, { useEffect, useRef, useState } from "react"; import { Button, Form, Modal } from "react-bootstrap"; import { CriterionModifier } from "src/core/generated-graphql"; @@ -80,13 +80,13 @@ export const AddFilterDialog: React.FC = ({ function onChangedModifierSelect( event: React.ChangeEvent ) { - const newCriterion = _.cloneDeep(criterion); + const newCriterion = cloneDeep(criterion); newCriterion.modifier = event.target.value as CriterionModifier; setCriterion(newCriterion); } function onValueChanged(value: CriterionValue) { - const newCriterion = _.cloneDeep(criterion); + const newCriterion = cloneDeep(criterion); newCriterion.value = value; setCriterion(newCriterion); } diff --git a/ui/v2.5/src/components/List/FilterTags.tsx b/ui/v2.5/src/components/List/FilterTags.tsx index db7a7063e..48e79da98 100644 --- a/ui/v2.5/src/components/List/FilterTags.tsx +++ b/ui/v2.5/src/components/List/FilterTags.tsx @@ -6,6 +6,7 @@ import { } from "src/models/list-filter/criteria/criterion"; import { useIntl } from "react-intl"; import { Icon } from "../Shared"; +import { faTimes } from "@fortawesome/free-solid-svg-icons"; interface IFilterTagsProps { criteria: Criterion[]; @@ -48,7 +49,7 @@ export const FilterTags: React.FC = ({ variant="secondary" onClick={($event) => onRemoveCriterionTag(criterion, $event)} > - + )); diff --git a/ui/v2.5/src/components/List/ListFilter.tsx b/ui/v2.5/src/components/List/ListFilter.tsx index a1876d1f8..56e0a8a76 100644 --- a/ui/v2.5/src/components/List/ListFilter.tsx +++ b/ui/v2.5/src/components/List/ListFilter.tsx @@ -1,4 +1,5 @@ -import _, { debounce } from "lodash"; +import debounce from "lodash-es/debounce"; +import cloneDeep from "lodash-es/cloneDeep"; import React, { HTMLAttributes, useEffect, useRef, useState } from "react"; import cx from "classnames"; import Mousetrap from "mousetrap"; @@ -23,6 +24,15 @@ import { ListFilterOptions } from "src/models/list-filter/filter-options"; import { FormattedMessage, useIntl } from "react-intl"; import { PersistanceLevel } from "src/hooks/ListHook"; import { SavedFilterList } from "./SavedFilterList"; +import { + faBookmark, + faCaretDown, + faCaretUp, + faCheck, + faFilter, + faRandom, + faTimes, +} from "@fortawesome/free-solid-svg-icons"; const maxPageSize = 1000; interface IListFilterProps { @@ -53,7 +63,7 @@ export const ListFilter: React.FC = ({ const [perPageInput, perPageFocus] = useFocus(); const searchCallback = debounce((value: string) => { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); newFilter.searchTerm = value; newFilter.currentPage = 1; onFilterUpdate(newFilter); @@ -101,7 +111,7 @@ export const ListFilter: React.FC = ({ pp = maxPageSize; } - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); newFilter.itemsPerPage = pp; newFilter.currentPage = 1; onFilterUpdate(newFilter); @@ -120,7 +130,7 @@ export const ListFilter: React.FC = ({ } function onChangeSortDirection() { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); if (filter.sortDirection === SortDirectionEnum.Asc) { newFilter.sortDirection = SortDirectionEnum.Desc; } else { @@ -131,14 +141,14 @@ export const ListFilter: React.FC = ({ } function onChangeSortBy(eventKey: string | null) { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); newFilter.sortBy = eventKey ?? undefined; newFilter.currentPage = 1; onFilterUpdate(newFilter); } function onReshuffleRandomSort() { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); newFilter.currentPage = 1; newFilter.randomSeed = -1; onFilterUpdate(newFilter); @@ -225,7 +235,7 @@ export const ListFilter: React.FC = ({ queryClearShowing ? "" : "d-none" )} > - + @@ -241,7 +251,7 @@ export const ListFilter: React.FC = ({ } > - + = ({ onClick={() => openFilterDialog()} active={filterDialogOpen} > - + @@ -291,8 +301,8 @@ export const ListFilter: React.FC = ({ @@ -306,7 +316,7 @@ export const ListFilter: React.FC = ({ } > )} @@ -362,7 +372,7 @@ export const ListFilter: React.FC = ({ ) } > - + diff --git a/ui/v2.5/src/components/List/ListOperationButtons.tsx b/ui/v2.5/src/components/List/ListOperationButtons.tsx index f16159818..efa0eefdc 100644 --- a/ui/v2.5/src/components/List/ListOperationButtons.tsx +++ b/ui/v2.5/src/components/List/ListOperationButtons.tsx @@ -8,14 +8,19 @@ import { } from "react-bootstrap"; import Mousetrap from "mousetrap"; import { FormattedMessage, useIntl } from "react-intl"; -import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { Icon } from "../Shared"; +import { + faEllipsisH, + faPencilAlt, + faTrash, +} from "@fortawesome/free-solid-svg-icons"; interface IListFilterOperation { text: string; onClick: () => void; isDisplayed?: () => boolean; - icon?: IconProp; + icon?: IconDefinition; buttonVariant?: string; } @@ -78,14 +83,14 @@ export const ListOperationButtons: React.FC = ({ if (itemsSelected) { if (onEdit) { buttons.push({ - icon: "pencil-alt", + icon: faPencilAlt, text: intl.formatMessage({ id: "actions.edit" }), onClick: onEdit, }); } if (onDelete) { buttons.push({ - icon: "trash", + icon: faTrash, text: intl.formatMessage({ id: "actions.delete" }), onClick: onDelete, buttonVariant: "danger", @@ -106,7 +111,7 @@ export const ListOperationButtons: React.FC = ({ variant={button.buttonVariant ?? "secondary"} onClick={button.onClick} > - + {button.icon ? : undefined} ); @@ -173,7 +178,7 @@ export const ListOperationButtons: React.FC = ({ return ( - + {options} diff --git a/ui/v2.5/src/components/List/ListViewOptions.tsx b/ui/v2.5/src/components/List/ListViewOptions.tsx index 8cf35673c..487b95ee0 100644 --- a/ui/v2.5/src/components/List/ListViewOptions.tsx +++ b/ui/v2.5/src/components/List/ListViewOptions.tsx @@ -10,6 +10,12 @@ import { import { DisplayMode } from "src/models/list-filter/types"; import { useIntl } from "react-intl"; import { Icon } from "../Shared"; +import { + faList, + faSquare, + faTags, + faThLarge, +} from "@fortawesome/free-solid-svg-icons"; interface IListViewOptionsProps { zoomIndex?: number; @@ -71,13 +77,13 @@ export const ListViewOptions: React.FC = ({ function getIcon(option: DisplayMode) { switch (option) { case DisplayMode.Grid: - return "th-large"; + return faThLarge; case DisplayMode.List: - return "list"; + return faList; case DisplayMode.Wall: - return "square"; + return faSquare; case DisplayMode.Tagger: - return "tags"; + return faTags; } } function getLabel(option: DisplayMode) { diff --git a/ui/v2.5/src/components/List/SavedFilterList.tsx b/ui/v2.5/src/components/List/SavedFilterList.tsx index 887eee360..a66a06fc8 100644 --- a/ui/v2.5/src/components/List/SavedFilterList.tsx +++ b/ui/v2.5/src/components/List/SavedFilterList.tsx @@ -22,6 +22,7 @@ import { LoadingIndicator } from "src/components/Shared"; import { PersistanceLevel } from "src/hooks/ListHook"; import { FormattedMessage, useIntl } from "react-intl"; import { Icon } from "../Shared"; +import { faSave, faTimes } from "@fortawesome/free-solid-svg-icons"; interface ISavedFilterListProps { filter: ListFilterModel; @@ -191,7 +192,7 @@ export const SavedFilterList: React.FC = ({ e.stopPropagation(); }} > - + @@ -344,7 +345,7 @@ export const SavedFilterList: React.FC = ({ onSaveFilter(filterName); }} > - + diff --git a/ui/v2.5/src/components/MainNavbar.tsx b/ui/v2.5/src/components/MainNavbar.tsx index c9a288466..95f170a25 100644 --- a/ui/v2.5/src/components/MainNavbar.tsx +++ b/ui/v2.5/src/components/MainNavbar.tsx @@ -6,22 +6,38 @@ import { useIntl, } from "react-intl"; import { Nav, Navbar, Button, Fade } from "react-bootstrap"; -import { IconName } from "@fortawesome/fontawesome-svg-core"; +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { LinkContainer } from "react-router-bootstrap"; import { Link, NavLink, useLocation, useHistory } from "react-router-dom"; import Mousetrap from "mousetrap"; import { SessionUtils } from "src/utils"; -import { Icon } from "src/components/Shared"; +import Icon from "src/components/Shared/Icon"; import { ConfigurationContext } from "src/hooks/Config"; -import { ManualStateContext } from "./Help/Manual"; +import { ManualStateContext } from "./Help/context"; import { SettingsButton } from "./SettingsButton"; +import { + faBars, + faChartBar, + faFilm, + faHeart, + faImage, + faImages, + faMapMarkerAlt, + faPlayCircle, + faQuestionCircle, + faSignOutAlt, + faTag, + faTimes, + faUser, + faVideo, +} from "@fortawesome/free-solid-svg-icons"; interface IMenuItem { name: string; message: MessageDescriptor; href: string; - icon: IconName; + icon: IconDefinition; hotkey: string; userCreatable?: boolean; } @@ -77,21 +93,21 @@ const allMenuItems: IMenuItem[] = [ name: "scenes", message: messages.scenes, href: "/scenes", - icon: "play-circle", + icon: faPlayCircle, hotkey: "g s", }, { name: "images", message: messages.images, href: "/images", - icon: "image", + icon: faImage, hotkey: "g i", }, { name: "movies", message: messages.movies, href: "/movies", - icon: "film", + icon: faFilm, hotkey: "g v", userCreatable: true, }, @@ -99,14 +115,14 @@ const allMenuItems: IMenuItem[] = [ name: "markers", message: messages.markers, href: "/scenes/markers", - icon: "map-marker-alt", + icon: faMapMarkerAlt, hotkey: "g k", }, { name: "galleries", message: messages.galleries, href: "/galleries", - icon: "images", + icon: faImages, hotkey: "g l", userCreatable: true, }, @@ -114,7 +130,7 @@ const allMenuItems: IMenuItem[] = [ name: "performers", message: messages.performers, href: "/performers", - icon: "user", + icon: faUser, hotkey: "g p", userCreatable: true, }, @@ -122,7 +138,7 @@ const allMenuItems: IMenuItem[] = [ name: "studios", message: messages.studios, href: "/studios", - icon: "video", + icon: faVideo, hotkey: "g u", userCreatable: true, }, @@ -130,7 +146,7 @@ const allMenuItems: IMenuItem[] = [ name: "tags", message: messages.tags, href: "/tags", - icon: "tag", + icon: faTag, hotkey: "g t", userCreatable: true, }, @@ -236,7 +252,7 @@ export const MainNavbar: React.FC = () => { href="/logout" title={intl.formatMessage({ id: "actions.logout" })} > - + ); } @@ -257,7 +273,7 @@ export const MainNavbar: React.FC = () => { className="minimal donate" title={intl.formatMessage({ id: "donate" })} > - + {intl.formatMessage(messages.donate)} @@ -273,7 +289,7 @@ export const MainNavbar: React.FC = () => { className="minimal d-flex align-items-center h-100" title={intl.formatMessage({ id: "statistics" })} > - + { onClick={() => openManual()} title={intl.formatMessage({ id: "help" })} > - + {maybeRenderLogout()} @@ -355,7 +371,7 @@ export const MainNavbar: React.FC = () => { )} {renderUtilityButtons()} - + diff --git a/ui/v2.5/src/components/Movies/EditMoviesDialog.tsx b/ui/v2.5/src/components/Movies/EditMoviesDialog.tsx index 713989f1f..b8b6ed196 100644 --- a/ui/v2.5/src/components/Movies/EditMoviesDialog.tsx +++ b/ui/v2.5/src/components/Movies/EditMoviesDialog.tsx @@ -12,6 +12,7 @@ import { getAggregateRating, getAggregateStudioId, } from "src/utils/bulkUpdate"; +import { faPencilAlt } from "@fortawesome/free-solid-svg-icons"; interface IListOperationProps { selected: GQL.MovieDataFragment[]; @@ -101,7 +102,7 @@ export const EditMoviesDialog: React.FC = ( return ( = (props: IProps) => { content={popoverContent} > diff --git a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx index 2edd6d407..3984d4c33 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx @@ -19,6 +19,7 @@ import { useToast } from "src/hooks"; import { MovieScenesPanel } from "./MovieScenesPanel"; import { MovieDetailsPanel } from "./MovieDetailsPanel"; import { MovieEditPanel } from "./MovieEditPanel"; +import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; interface IProps { movie: GQL.MovieDataFragment; @@ -110,7 +111,7 @@ const MoviePage: React.FC = ({ movie }) => { return ( = ({ filterHook }) => { const { count } = result.data.findMovies; const index = Math.floor(Math.random() * count); - const filterCopy = _.cloneDeep(filter); + const filterCopy = cloneDeep(filter); filterCopy.itemsPerPage = 1; filterCopy.currentPage = index + 1; const singleResult = await queryFindMovies(filterCopy); diff --git a/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx b/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx index 3498af758..634b055c2 100644 --- a/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx +++ b/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx @@ -20,6 +20,7 @@ import { } from "src/utils/gender"; import { IndeterminateCheckbox } from "../Shared/IndeterminateCheckbox"; import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput"; +import { faPencilAlt } from "@fortawesome/free-solid-svg-icons"; interface IListOperationProps { selected: GQL.SlimPerformerDataFragment[]; @@ -183,7 +184,7 @@ export const EditPerformersDialog: React.FC = ( return ( []; @@ -65,7 +66,7 @@ export const PerformerCard: React.FC = ({ } return (
- +
); } @@ -122,7 +123,7 @@ export const PerformerCard: React.FC = ({ return ( diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx index 2a848e72d..27a22a31e 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx @@ -30,6 +30,12 @@ import { PerformerImagesPanel } from "./PerformerImagesPanel"; import { PerformerEditPanel } from "./PerformerEditPanel"; import { PerformerSubmitButton } from "./PerformerSubmitButton"; import GenderIcon from "../GenderIcon"; +import { + faCamera, + faDove, + faHeart, + faLink, +} from "@fortawesome/free-solid-svg-icons"; interface IProps { performer: GQL.PerformerDataFragment; @@ -325,7 +331,7 @@ const PerformerPage: React.FC = ({ performer }) => { )} onClick={() => setFavorite(!performer.favorite)} > - + {performer.url && ( )} @@ -350,7 +356,7 @@ const PerformerPage: React.FC = ({ performer }) => { target="_blank" rel="noopener noreferrer" > - + )} @@ -365,7 +371,7 @@ const PerformerPage: React.FC = ({ performer }) => { target="_blank" rel="noopener noreferrer" > - + )} diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx index 348051d67..3be7d87e9 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx @@ -37,6 +37,11 @@ import { PerformerScrapeDialog } from "./PerformerScrapeDialog"; import PerformerScrapeModal from "./PerformerScrapeModal"; import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal"; import cx from "classnames"; +import { + faPlus, + faSyncAlt, + faTrashAlt, +} from "@fortawesome/free-solid-svg-icons"; const isScraper = ( scraper: GQL.Scraper | GQL.StashBox @@ -192,7 +197,7 @@ export const PerformerEditPanel: React.FC = ({ > {t.name} ))} @@ -594,7 +599,7 @@ export const PerformerEditPanel: React.FC = ({ onClick={() => onReloadScrapers()} > - + @@ -781,7 +786,7 @@ export const PerformerEditPanel: React.FC = ({ title={intl.formatMessage({ id: "actions.delete_stashid" })} onClick={() => removeStashID(stashID)} > - + {link} diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx index 9225cb976..d18844b9e 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx @@ -13,7 +13,7 @@ import { useTagCreate } from "src/core/StashService"; import { Form } from "react-bootstrap"; import { TagSelect } from "src/components/Shared"; import { useToast } from "src/hooks"; -import _ from "lodash"; +import clone from "lodash-es/clone"; import { genderStrings, genderToString, @@ -285,7 +285,7 @@ export const PerformerScrapeDialog: React.FC = ( return; } - const ret = _.clone(idList); + const ret = clone(idList); // sort by id numerically ret.sort((a, b) => { return parseInt(a, 10) - parseInt(b, 10); diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeModal.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeModal.tsx index 5c02f1f42..5fd55cdc5 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeModal.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeModal.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; -import { debounce } from "lodash"; +import debounce from "lodash-es/debounce"; import { Button, Form } from "react-bootstrap"; import { useIntl } from "react-intl"; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerStashBoxModal.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerStashBoxModal.tsx index e2cb93e50..acdca66a7 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerStashBoxModal.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerStashBoxModal.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; -import { debounce } from "lodash"; +import debounce from "lodash-es/debounce"; import { Button, Form } from "react-bootstrap"; import { useIntl } from "react-intl"; diff --git a/ui/v2.5/src/components/Performers/PerformerList.tsx b/ui/v2.5/src/components/Performers/PerformerList.tsx index cdff5c00a..0cd8902ae 100644 --- a/ui/v2.5/src/components/Performers/PerformerList.tsx +++ b/ui/v2.5/src/components/Performers/PerformerList.tsx @@ -1,4 +1,4 @@ -import _ from "lodash"; +import cloneDeep from "lodash-es/cloneDeep"; import React, { useState } from "react"; import { useIntl } from "react-intl"; import { useHistory } from "react-router-dom"; @@ -138,7 +138,7 @@ export const PerformerList: React.FC = ({ if (result.data?.findPerformers) { const { count } = result.data.findPerformers; const index = Math.floor(Math.random() * count); - const filterCopy = _.cloneDeep(filter); + const filterCopy = cloneDeep(filter); filterCopy.itemsPerPage = 1; filterCopy.currentPage = index + 1; const singleResult = await queryFindPerformers(filterCopy); diff --git a/ui/v2.5/src/components/Performers/PerformerListTable.tsx b/ui/v2.5/src/components/Performers/PerformerListTable.tsx index c96e4ce1b..36af0e841 100644 --- a/ui/v2.5/src/components/Performers/PerformerListTable.tsx +++ b/ui/v2.5/src/components/Performers/PerformerListTable.tsx @@ -7,6 +7,7 @@ import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; import { Icon } from "src/components/Shared"; import { NavUtils } from "src/utils"; +import { faHeart } from "@fortawesome/free-solid-svg-icons"; interface IPerformerListTableProps { performers: GQL.PerformerDataFragment[]; @@ -37,7 +38,7 @@ export const PerformerListTable: React.FC = ( {performer.favorite && ( )} diff --git a/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx b/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx index 32c5ea4b0..557ee43f7 100644 --- a/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx +++ b/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx @@ -28,6 +28,16 @@ import { TextUtils } from "src/utils"; import { DeleteScenesDialog } from "src/components/Scenes/DeleteScenesDialog"; import { EditScenesDialog } from "../Scenes/EditScenesDialog"; import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton"; +import { + faBox, + faExclamationTriangle, + faFilm, + faImages, + faMapMarkerAlt, + faPencilAlt, + faTag, + faTrash, +} from "@fortawesome/free-solid-svg-icons"; const CLASSNAME = "duplicate-checker"; @@ -144,7 +154,7 @@ export const SceneDuplicateChecker: React.FC = () => { if (missingPhashes > 0) { return (

- + Missing phashes for {missingPhashes} scenes. Please run the phash generation task.

@@ -173,7 +183,7 @@ export const SceneDuplicateChecker: React.FC = () => { return ( @@ -216,7 +226,7 @@ export const SceneDuplicateChecker: React.FC = () => { className="tag-tooltip" > @@ -236,7 +246,7 @@ export const SceneDuplicateChecker: React.FC = () => { return ( @@ -268,7 +278,7 @@ export const SceneDuplicateChecker: React.FC = () => { return ( @@ -280,7 +290,7 @@ export const SceneDuplicateChecker: React.FC = () => { return (
); @@ -332,7 +342,7 @@ export const SceneDuplicateChecker: React.FC = () => { } > { } > @@ -550,3 +560,5 @@ export const SceneDuplicateChecker: React.FC = () => { ); }; + +export default SceneDuplicateChecker; diff --git a/ui/v2.5/src/components/SceneFilenameParser/SceneFilenameParser.tsx b/ui/v2.5/src/components/SceneFilenameParser/SceneFilenameParser.tsx index f70f0381f..6f1464af1 100644 --- a/ui/v2.5/src/components/SceneFilenameParser/SceneFilenameParser.tsx +++ b/ui/v2.5/src/components/SceneFilenameParser/SceneFilenameParser.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState, useCallback, useRef } from "react"; import { Button, Card, Form, Table } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; -import _ from "lodash"; +import clone from "lodash-es/clone"; import { queryParseSceneFilenames, useScenesUpdate, @@ -155,7 +155,7 @@ export const SceneFilenameParser: React.FC = () => { }, [parserInput, parseSceneFilenames, prevParserInput]); function onPageSizeChanged(newSize: number) { - const newInput = _.clone(parserInput); + const newInput = clone(parserInput); newInput.page = 1; newInput.pageSize = newSize; setParserInput(newInput); @@ -163,14 +163,14 @@ export const SceneFilenameParser: React.FC = () => { function onPageChanged(newPage: number) { if (newPage !== parserInput.page) { - const newInput = _.clone(parserInput); + const newInput = clone(parserInput); newInput.page = newPage; setParserInput(newInput); } } function onFindClicked(input: IParserInput) { - const newInput = _.clone(input); + const newInput = clone(input); newInput.page = 1; newInput.findClicked = true; setParserInput(newInput); @@ -423,3 +423,5 @@ export const SceneFilenameParser: React.FC = () => { ); }; + +export default SceneFilenameParser; diff --git a/ui/v2.5/src/components/SceneFilenameParser/SceneParserRow.tsx b/ui/v2.5/src/components/SceneFilenameParser/SceneParserRow.tsx index 35596ad0c..472b4beba 100644 --- a/ui/v2.5/src/components/SceneFilenameParser/SceneParserRow.tsx +++ b/ui/v2.5/src/components/SceneFilenameParser/SceneParserRow.tsx @@ -1,5 +1,6 @@ import React from "react"; -import _ from "lodash"; +import isEqual from "lodash-es/isEqual"; +import clone from "lodash-es/clone"; import { Form } from "react-bootstrap"; import { ParseSceneFilenamesQuery, @@ -26,7 +27,7 @@ class ParserResult { public setValue(value?: T) { if (value) { this.value = value; - this.isSet = !_.isEqual(this.value, this.originalValue); + this.isSet = !isEqual(this.value, this.originalValue); } } } @@ -332,44 +333,44 @@ interface ISceneParserRowProps { export const SceneParserRow = (props: ISceneParserRowProps) => { function changeParser(result: ParserResult, isSet: boolean, value?: T) { - const newParser = _.clone(result); + const newParser = clone(result); newParser.isSet = isSet; newParser.value = value; return newParser; } function onTitleChanged(set: boolean, value: string) { - const newResult = _.clone(props.scene); + const newResult = clone(props.scene); newResult.title = changeParser(newResult.title, set, value); props.onChange(newResult); } function onDateChanged(set: boolean, value: string) { - const newResult = _.clone(props.scene); + const newResult = clone(props.scene); newResult.date = changeParser(newResult.date, set, value); props.onChange(newResult); } function onRatingChanged(set: boolean, value?: number) { - const newResult = _.clone(props.scene); + const newResult = clone(props.scene); newResult.rating = changeParser(newResult.rating, set, value); props.onChange(newResult); } function onPerformerIdsChanged(set: boolean, value: string[]) { - const newResult = _.clone(props.scene); + const newResult = clone(props.scene); newResult.performers = changeParser(newResult.performers, set, value); props.onChange(newResult); } function onTagIdsChanged(set: boolean, value: string[]) { - const newResult = _.clone(props.scene); + const newResult = clone(props.scene); newResult.tags = changeParser(newResult.tags, set, value); props.onChange(newResult); } function onStudioIdChanged(set: boolean, value: string) { - const newResult = _.clone(props.scene); + const newResult = clone(props.scene); newResult.studio = changeParser(newResult.studio, set, value); props.onChange(newResult); } diff --git a/ui/v2.5/src/components/SceneFilenameParser/ShowFields.tsx b/ui/v2.5/src/components/SceneFilenameParser/ShowFields.tsx index 1e864125f..ea1fcccf9 100644 --- a/ui/v2.5/src/components/SceneFilenameParser/ShowFields.tsx +++ b/ui/v2.5/src/components/SceneFilenameParser/ShowFields.tsx @@ -1,3 +1,9 @@ +import { + faCheck, + faChevronDown, + faChevronRight, + faTimes, +} from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, Collapse } from "react-bootstrap"; import { useIntl } from "react-intl"; @@ -26,7 +32,7 @@ export const ShowFields = (props: IShowFieldsProps) => { handleClick(label); }} > - + {label} )); @@ -34,7 +40,7 @@ export const ShowFields = (props: IShowFieldsProps) => { return (
@@ -214,7 +222,7 @@ export const SceneCard: React.FC = ( className="movie-count tag-tooltip" > @@ -236,7 +244,7 @@ export const SceneCard: React.FC = ( content={popoverContent} > @@ -272,7 +280,7 @@ export const SceneCard: React.FC = ( content={popoverContent} > @@ -284,7 +292,7 @@ export const SceneCard: React.FC = ( return (
); @@ -299,7 +307,7 @@ export const SceneCard: React.FC = ( href={NavUtils.makeScenesPHashMatchUrl(props.scene.phash)} className="minimal" > - +
); diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/ExternalPlayerButton.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/ExternalPlayerButton.tsx index 416c5ab73..dbe21790f 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/ExternalPlayerButton.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/ExternalPlayerButton.tsx @@ -1,9 +1,10 @@ +import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { Button } from "react-bootstrap"; import { useIntl } from "react-intl"; -import { Icon } from "src/components/Shared"; +import Icon from "src/components/Shared/Icon"; import { SceneDataFragment } from "src/core/generated-graphql"; -import { TextUtils } from "src/utils"; +import TextUtils from "src/utils/text"; export interface IExternalPlayerButtonProps { scene: SceneDataFragment; @@ -49,8 +50,10 @@ export const ExternalPlayerButton: React.FC = ({ title={intl.formatMessage({ id: "actions.open_in_external_player" })} > - + ); }; + +export default ExternalPlayerButton; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx index f9bc95162..19d9e337d 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx @@ -1,3 +1,4 @@ +import { faBan, faMinus } from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, ButtonGroup, Dropdown, DropdownButton } from "react-bootstrap"; import { useIntl } from "react-intl"; @@ -58,11 +59,11 @@ export const OCounterButton: React.FC = ( className="pl-0 show-carat" > - + Decrement - + Reset diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/OrganizedButton.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/OrganizedButton.tsx index 84a4abae2..63e82f9d2 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/OrganizedButton.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/OrganizedButton.tsx @@ -1,8 +1,9 @@ import React from "react"; import cx from "classnames"; import { Button, Spinner } from "react-bootstrap"; -import { Icon } from "src/components/Shared"; +import Icon from "src/components/Shared/Icon"; import { defineMessages, useIntl } from "react-intl"; +import { faBox } from "@fortawesome/free-solid-svg-icons"; export interface IOrganizedButtonProps { loading: boolean; @@ -34,7 +35,7 @@ export const OrganizedButton: React.FC = ( )} onClick={props.onClick} > - + ); }; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/PrimaryTags.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/PrimaryTags.tsx index 2832431fb..6cbca4ae0 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/PrimaryTags.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/PrimaryTags.tsx @@ -2,7 +2,7 @@ import React from "react"; import { FormattedMessage } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import { Button, Badge, Card } from "react-bootstrap"; -import { TextUtils } from "src/utils"; +import TextUtils from "src/utils/text"; interface IPrimaryTags { sceneMarkers: GQL.SceneMarkerDataFragment[]; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/QueueViewer.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/QueueViewer.tsx index 07197a720..bebcf3f7c 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/QueueViewer.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/QueueViewer.tsx @@ -2,10 +2,17 @@ import React, { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import cx from "classnames"; import * as GQL from "src/core/generated-graphql"; -import { TextUtils } from "src/utils"; +import TextUtils from "src/utils/text"; import { Button, Form, Spinner } from "react-bootstrap"; -import { Icon } from "src/components/Shared"; +import Icon from "src/components/Shared/Icon"; import { useIntl } from "react-intl"; +import { + faChevronDown, + faChevronUp, + faRandom, + faStepBackward, + faStepForward, +} from "@fortawesome/free-solid-svg-icons"; export interface IPlaylistViewer { scenes?: GQL.SlimSceneDataFragment[]; @@ -113,7 +120,7 @@ export const QueueViewer: React.FC = ({ variant="secondary" onClick={() => onPrevious()} > - + ) : ( "" @@ -124,7 +131,7 @@ export const QueueViewer: React.FC = ({ variant="secondary" onClick={() => onNext()} > - + ) : ( "" @@ -134,7 +141,7 @@ export const QueueViewer: React.FC = ({ variant="secondary" onClick={() => onRandom()} > - + @@ -143,7 +150,7 @@ export const QueueViewer: React.FC = ({
); }; + +export default QueueViewer; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/RatingStars.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/RatingStars.tsx index bdec9167e..010c20c96 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/RatingStars.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/RatingStars.tsx @@ -1,6 +1,8 @@ import React, { useState } from "react"; import { Button } from "react-bootstrap"; -import { Icon } from "src/components/Shared"; +import Icon from "src/components/Shared/Icon"; +import { faStar as fasStar } from "@fortawesome/free-solid-svg-icons"; +import { faStar as farStar } from "@fortawesome/free-regular-svg-icons"; export interface IRatingStarsProps { value?: number; @@ -33,20 +35,20 @@ export const RatingStars: React.FC = ( props.onSetRating(newRating); } - function getIconPrefix(rating: number) { + function getIcon(rating: number) { if (hoverRating && hoverRating >= rating) { if (hoverRating === props.value) { - return "far"; + return farStar; } - return "fas"; + return fasStar; } if (!hoverRating && props.value && props.value >= rating) { - return "fas"; + return fasStar; } - return "far"; + return farStar; } function onMouseOver(rating: number) { @@ -101,10 +103,7 @@ export const RatingStars: React.FC = ( title={getTooltip(rating)} key={`star-${rating}`} > - + ); diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx index 67a36991e..b036b2b17 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx @@ -1,6 +1,6 @@ import { Tab, Nav, Dropdown, Button, ButtonGroup } from "react-bootstrap"; import queryString from "query-string"; -import React, { useEffect, useState, useMemo, useContext } from "react"; +import React, { useEffect, useState, useMemo, useContext, lazy } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useParams, useLocation, useHistory, Link } from "react-router-dom"; import { Helmet } from "react-helmet"; @@ -16,29 +16,41 @@ import { queryFindScenes, queryFindScenesByID, } from "src/core/StashService"; -import { GalleryViewer } from "src/components/Galleries/GalleryViewer"; -import { Icon } from "src/components/Shared"; + +import Icon from "src/components/Shared/Icon"; import { useToast } from "src/hooks"; -import { SubmitStashBoxDraft } from "src/components/Dialogs/SubmitDraft"; -import { ScenePlayer, getPlayerPosition } from "src/components/ScenePlayer"; +import SceneQueue from "src/models/sceneQueue"; import { ListFilterModel } from "src/models/list-filter/filter"; -import { TextUtils } from "src/utils"; +import TextUtils from "src/utils/text"; import Mousetrap from "mousetrap"; -import { SceneQueue } from "src/models/sceneQueue"; -import { QueueViewer } from "./QueueViewer"; -import { SceneMarkersPanel } from "./SceneMarkersPanel"; -import { SceneFileInfoPanel } from "./SceneFileInfoPanel"; -import { SceneEditPanel } from "./SceneEditPanel"; -import { SceneDetailPanel } from "./SceneDetailPanel"; import { OCounterButton } from "./OCounterButton"; -import { ExternalPlayerButton } from "./ExternalPlayerButton"; -import { SceneMoviePanel } from "./SceneMoviePanel"; -import { SceneGalleriesPanel } from "./SceneGalleriesPanel"; -import { DeleteScenesDialog } from "../DeleteScenesDialog"; -import { GenerateDialog } from "../../Dialogs/GenerateDialog"; -import { SceneVideoFilterPanel } from "./SceneVideoFilterPanel"; import { OrganizedButton } from "./OrganizedButton"; import { ConfigurationContext } from "src/hooks/Config"; +import { getPlayerPosition } from "src/components/ScenePlayer/util"; +import { faEllipsisV } from "@fortawesome/free-solid-svg-icons"; + +const SubmitStashBoxDraft = lazy( + () => import("src/components/Dialogs/SubmitDraft") +); +const ScenePlayer = lazy( + () => import("src/components/ScenePlayer/ScenePlayer") +); + +const GalleryViewer = lazy( + () => import("src/components/Galleries/GalleryViewer") +); +const ExternalPlayerButton = lazy(() => import("./ExternalPlayerButton")); + +const QueueViewer = lazy(() => import("./QueueViewer")); +const SceneMarkersPanel = lazy(() => import("./SceneMarkersPanel")); +const SceneFileInfoPanel = lazy(() => import("./SceneFileInfoPanel")); +const SceneEditPanel = lazy(() => import("./SceneEditPanel")); +const SceneDetailPanel = lazy(() => import("./SceneDetailPanel")); +const SceneMoviePanel = lazy(() => import("./SceneMoviePanel")); +const SceneGalleriesPanel = lazy(() => import("./SceneGalleriesPanel")); +const DeleteScenesDialog = lazy(() => import("../DeleteScenesDialog")); +const GenerateDialog = lazy(() => import("../../Dialogs/GenerateDialog")); +const SceneVideoFilterPanel = lazy(() => import("./SceneVideoFilterPanel")); interface IProps { scene: GQL.SceneDataFragment; @@ -237,7 +249,7 @@ const ScenePage: React.FC = ({ className="minimal" title={intl.formatMessage({ id: "operations" })} > - + = (props) => { ); }; + +export default SceneDetailPanel; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx index 48887aa13..94d9687b8 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useMemo } from "react"; +import React, { useEffect, useState, useMemo, lazy } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { Button, @@ -30,7 +30,7 @@ import { ImageInput, URLField, } from "src/components/Shared"; -import { useToast } from "src/hooks"; +import useToast from "src/hooks/Toast"; import { ImageUtils, FormUtils, TextUtils, getStashIDs } from "src/utils"; import { MovieSelect } from "src/components/Shared/Select"; import { useFormik } from "formik"; @@ -39,8 +39,14 @@ import { ConfigurationContext } from "src/hooks/Config"; import { stashboxDisplayName } from "src/utils/stashbox"; import { SceneMovieTable } from "./SceneMovieTable"; import { RatingStars } from "./RatingStars"; -import { SceneScrapeDialog } from "./SceneScrapeDialog"; -import { SceneQueryModal } from "./SceneQueryModal"; +import { + faSearch, + faSyncAlt, + faTrashAlt, +} from "@fortawesome/free-solid-svg-icons"; + +const SceneScrapeDialog = lazy(() => import("./SceneScrapeDialog")); +const SceneQueryModal = lazy(() => import("./SceneQueryModal")); interface IProps { scene: GQL.SceneDataFragment; @@ -401,7 +407,7 @@ export const SceneEditPanel: React.FC = ({ return ( - + @@ -428,7 +434,7 @@ export const SceneEditPanel: React.FC = ({ ))} onReloadScrapers()}> - + @@ -500,7 +506,7 @@ export const SceneEditPanel: React.FC = ({ ))} onReloadScrapers()}> - + @@ -857,7 +863,7 @@ export const SceneEditPanel: React.FC = ({ )} onClick={() => removeStashID(stashID)} > - + {link} @@ -908,3 +914,5 @@ export const SceneEditPanel: React.FC = ({ ); }; + +export default SceneEditPanel; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx index a6d62f20f..f8c1c3063 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx @@ -175,3 +175,5 @@ export const SceneFileInfoPanel: React.FC = ( ); }; + +export default SceneFileInfoPanel; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneGalleriesPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneGalleriesPanel.tsx index 30b8ba83d..4a8ca38f6 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneGalleriesPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneGalleriesPanel.tsx @@ -15,3 +15,5 @@ export const SceneGalleriesPanel: React.FC = ({ return
{cards}
; }; + +export default SceneGalleriesPanel; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx index 352d42e36..2d24dfb8a 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx @@ -13,8 +13,8 @@ import { TagSelect, MarkerTitleSuggest, } from "src/components/Shared"; -import { getPlayerPosition } from "src/components/ScenePlayer"; -import { useToast } from "src/hooks"; +import { getPlayerPosition } from "src/components/ScenePlayer/util"; +import useToast from "src/hooks/Toast"; interface IFormFields { title: string; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkersPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkersPanel.tsx index ec2e6653d..10b8228b6 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkersPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkersPanel.tsx @@ -89,3 +89,5 @@ export const SceneMarkersPanel: React.FC = ( ); }; + +export default SceneMarkersPanel; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMoviePanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMoviePanel.tsx index e3df55308..6961c4761 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMoviePanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMoviePanel.tsx @@ -23,3 +23,5 @@ export const SceneMoviePanel: FunctionComponent = ( ); }; + +export default SceneMoviePanel; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneQueryModal.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneQueryModal.tsx index 560ffae31..1dfd4deed 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneQueryModal.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneQueryModal.tsx @@ -10,7 +10,8 @@ import { Icon, } from "src/components/Shared"; import { queryScrapeSceneQuery } from "src/core/StashService"; -import { useToast } from "src/hooks"; +import useToast from "src/hooks/Toast"; +import { faSearch } from "@fortawesome/free-solid-svg-icons"; interface ISceneSearchResultDetailsProps { scene: GQL.ScrapedSceneDataFragment; @@ -219,7 +220,7 @@ export const SceneQueryModal: React.FC = ({ variant="primary" title={intl.formatMessage({ id: "actions.search" })} > - + @@ -235,3 +236,5 @@ export const SceneQueryModal: React.FC = ({
); }; + +export default SceneQueryModal; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx index 96e462682..a3515940d 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx @@ -10,7 +10,7 @@ import { ScrapedTextAreaRow, ScrapedImageRow, } from "src/components/Shared/ScrapeDialog"; -import _ from "lodash"; +import clone from "lodash-es/clone"; import { useStudioCreate, usePerformerCreate, @@ -18,8 +18,8 @@ import { useTagCreate, makePerformerCreateInput, } from "src/core/StashService"; -import { useToast } from "src/hooks"; -import { DurationUtils } from "src/utils"; +import useToast from "src/hooks/Toast"; +import DurationUtils from "src/utils/duration"; import { useIntl } from "react-intl"; function renderScrapedStudio( @@ -297,7 +297,7 @@ export const SceneScrapeDialog: React.FC = ({ return; } - const ret = _.clone(idList); + const ret = clone(idList); // sort by id numerically ret.sort((a, b) => { return parseInt(a, 10) - parseInt(b, 10); @@ -634,3 +634,5 @@ export const SceneScrapeDialog: React.FC = ({ /> ); }; + +export default SceneScrapeDialog; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneVideoFilterPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneVideoFilterPanel.tsx index af3beafac..f45c2bfae 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneVideoFilterPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneVideoFilterPanel.tsx @@ -1,8 +1,8 @@ import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { Button, Form } from "react-bootstrap"; -import { TruncatedText } from "src/components/Shared"; -import { VIDEO_PLAYER_ID } from "src/components/ScenePlayer"; +import TruncatedText from "src/components/Shared/TruncatedText"; +import { VIDEO_PLAYER_ID } from "src/components/ScenePlayer/util"; import * as GQL from "src/core/generated-graphql"; interface ISceneVideoFilterPanelProps { @@ -670,3 +670,5 @@ export const SceneVideoFilterPanel: React.FC = ( ); }; + +export default SceneVideoFilterPanel; diff --git a/ui/v2.5/src/components/Scenes/SceneList.tsx b/ui/v2.5/src/components/Scenes/SceneList.tsx index 143d552a0..7fe17d46a 100644 --- a/ui/v2.5/src/components/Scenes/SceneList.tsx +++ b/ui/v2.5/src/components/Scenes/SceneList.tsx @@ -1,9 +1,8 @@ import React, { useState } from "react"; -import _ from "lodash"; +import cloneDeep from "lodash-es/cloneDeep"; import { useIntl } from "react-intl"; import { useHistory } from "react-router-dom"; import Mousetrap from "mousetrap"; -import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { FindScenesQueryResult, SlimSceneDataFragment, @@ -25,6 +24,7 @@ import { SceneCardsGrid } from "./SceneCardsGrid"; import { TaggerContext } from "../Tagger/context"; import { IdentifyDialog } from "../Dialogs/IdentifyDialog/IdentifyDialog"; import { ConfigurationContext } from "src/hooks/Config"; +import { faPlay } from "@fortawesome/free-solid-svg-icons"; interface ISceneList { filterHook?: (filter: ListFilterModel) => ListFilterModel; @@ -50,7 +50,7 @@ export const SceneList: React.FC = ({ text: intl.formatMessage({ id: "actions.play_selected" }), onClick: playSelected, isDisplayed: showWhenSelected, - icon: "play" as IconProp, + icon: faPlay, }, { text: intl.formatMessage({ id: "actions.play_random" }), @@ -137,7 +137,7 @@ export const SceneList: React.FC = ({ const indexMax = filter.itemsPerPage < count ? filter.itemsPerPage : count; const index = Math.floor(Math.random() * indexMax); - const filterCopy = _.cloneDeep(filter); + const filterCopy = cloneDeep(filter); filterCopy.currentPage = page; filterCopy.sortBy = "random"; const queryResults = await queryFindScenes(filterCopy); @@ -300,3 +300,5 @@ export const SceneList: React.FC = ({ return {listData.template}; }; + +export default SceneList; diff --git a/ui/v2.5/src/components/Scenes/SceneListTable.tsx b/ui/v2.5/src/components/Scenes/SceneListTable.tsx index d5b8f8f02..b4b7803b0 100644 --- a/ui/v2.5/src/components/Scenes/SceneListTable.tsx +++ b/ui/v2.5/src/components/Scenes/SceneListTable.tsx @@ -102,7 +102,7 @@ export const SceneListTable: React.FC = ( {scene.gallery && ( )} diff --git a/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx b/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx index 97638338d..9958b0432 100644 --- a/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMarkerList.tsx @@ -1,4 +1,4 @@ -import _ from "lodash"; +import cloneDeep from "lodash-es/cloneDeep"; import React from "react"; import { useHistory } from "react-router-dom"; import { useIntl } from "react-intl"; @@ -58,7 +58,7 @@ export const SceneMarkerList: React.FC = ({ filterHook }) => { const { count } = result.data.findSceneMarkers; const index = Math.floor(Math.random() * count); - const filterCopy = _.cloneDeep(filter); + const filterCopy = cloneDeep(filter); filterCopy.itemsPerPage = 1; filterCopy.currentPage = index + 1; const singleResult = await queryFindSceneMarkers(filterCopy); @@ -98,3 +98,5 @@ export const SceneMarkerList: React.FC = ({ filterHook }) => { ); }; + +export default SceneMarkerList; diff --git a/ui/v2.5/src/components/Scenes/Scenes.tsx b/ui/v2.5/src/components/Scenes/Scenes.tsx index b49d5296c..ea2a7befe 100644 --- a/ui/v2.5/src/components/Scenes/Scenes.tsx +++ b/ui/v2.5/src/components/Scenes/Scenes.tsx @@ -1,12 +1,13 @@ -import React from "react"; +import React, { lazy } from "react"; import { Route, Switch } from "react-router-dom"; import { useIntl } from "react-intl"; import { Helmet } from "react-helmet"; import { TITLE_SUFFIX } from "src/components/Shared"; import { PersistanceLevel } from "src/hooks/ListHook"; -import Scene from "./SceneDetails/Scene"; -import { SceneList } from "./SceneList"; -import { SceneMarkerList } from "./SceneMarkerList"; + +const SceneList = lazy(() => import("./SceneList")); +const SceneMarkerList = lazy(() => import("./SceneMarkerList")); +const Scene = lazy(() => import("./SceneDetails/Scene")); const Scenes: React.FC = () => { const intl = useIntl(); diff --git a/ui/v2.5/src/components/Settings/Inputs.tsx b/ui/v2.5/src/components/Settings/Inputs.tsx index a4c322f46..8e70b095e 100644 --- a/ui/v2.5/src/components/Settings/Inputs.tsx +++ b/ui/v2.5/src/components/Settings/Inputs.tsx @@ -1,3 +1,4 @@ +import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, Collapse, Form, Modal, ModalProps } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; @@ -94,7 +95,7 @@ export const SettingGroup: React.FC> = ({ variant="minimal" onClick={() => setOpen(!open)} > - + ); } diff --git a/ui/v2.5/src/components/Settings/Settings.tsx b/ui/v2.5/src/components/Settings/Settings.tsx index bd2defc19..56d526bd7 100644 --- a/ui/v2.5/src/components/Settings/Settings.tsx +++ b/ui/v2.5/src/components/Settings/Settings.tsx @@ -148,3 +148,5 @@ export const Settings: React.FC = () => { ); }; + +export default Settings; diff --git a/ui/v2.5/src/components/Settings/SettingsLibraryPanel.tsx b/ui/v2.5/src/components/Settings/SettingsLibraryPanel.tsx index 55aaa6f48..a7a35237e 100644 --- a/ui/v2.5/src/components/Settings/SettingsLibraryPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsLibraryPanel.tsx @@ -5,6 +5,7 @@ import { SettingSection } from "./SettingSection"; import { BooleanSetting, StringListSetting, StringSetting } from "./Inputs"; import { SettingStateContext } from "./context"; import { useIntl } from "react-intl"; +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; export const SettingsLibraryPanel: React.FC = () => { const intl = useIntl(); @@ -85,7 +86,7 @@ export const SettingsLibraryPanel: React.FC = () => { rel="noopener noreferrer" target="_blank" > - +
} @@ -107,7 +108,7 @@ export const SettingsLibraryPanel: React.FC = () => { rel="noopener noreferrer" target="_blank" > - + } diff --git a/ui/v2.5/src/components/Settings/SettingsPluginsPanel.tsx b/ui/v2.5/src/components/Settings/SettingsPluginsPanel.tsx index afb38b3f3..274c82b1c 100644 --- a/ui/v2.5/src/components/Settings/SettingsPluginsPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsPluginsPanel.tsx @@ -8,6 +8,7 @@ import { TextUtils } from "src/utils"; import { CollapseButton, Icon, LoadingIndicator } from "src/components/Shared"; import { SettingSection } from "./SettingSection"; import { Setting, SettingGroup } from "./Inputs"; +import { faLink, faSyncAlt } from "@fortawesome/free-solid-svg-icons"; export const SettingsPluginsPanel: React.FC = () => { const Toast = useToast(); @@ -30,7 +31,7 @@ export const SettingsPluginsPanel: React.FC = () => { target="_blank" rel="noopener noreferrer" > - + ); @@ -105,7 +106,7 @@ export const SettingsPluginsPanel: React.FC = () => { @@ -364,7 +369,7 @@ export const SettingsServicesPanel: React.FC = () => { title={intl.formatMessage({ id: "actions.allow_temporarily" })} onClick={() => setTempIP(a)} > - + @@ -386,7 +391,7 @@ export const SettingsServicesPanel: React.FC = () => { onClick={() => setTempIP(ipEntry)} disabled={!ipEntry} > - + diff --git a/ui/v2.5/src/components/Settings/StashConfiguration.tsx b/ui/v2.5/src/components/Settings/StashConfiguration.tsx index 04b484b1e..57676b5fc 100644 --- a/ui/v2.5/src/components/Settings/StashConfiguration.tsx +++ b/ui/v2.5/src/components/Settings/StashConfiguration.tsx @@ -1,3 +1,4 @@ +import { faEllipsisV } from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, Form, Row, Col, Dropdown } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; @@ -72,7 +73,7 @@ const Stash: React.FC = ({ id={`stash-menu-${index}`} className="minimal" > - + onEdit()}> diff --git a/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx b/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx index 684aec6d3..663eaa77c 100644 --- a/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx @@ -15,10 +15,16 @@ import { ImportDialog } from "./ImportDialog"; import * as GQL from "src/core/generated-graphql"; import { SettingSection } from "../SettingSection"; import { BooleanSetting, Setting } from "../Inputs"; -import { ManualLink } from "src/components/Help/Manual"; +import { ManualLink } from "src/components/Help/context"; import { Icon } from "src/components/Shared"; import { ConfigurationContext } from "src/hooks/Config"; import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect"; +import { + faMinus, + faPlus, + faQuestionCircle, + faTrashAlt, +} from "@fortawesome/free-solid-svg-icons"; interface ICleanDialog { pathSelection?: boolean; @@ -63,7 +69,7 @@ const CleanDialog: React.FC = ({ return ( = ({ title={intl.formatMessage({ id: "actions.delete" })} onClick={() => removePath(p)} > - + @@ -103,7 +109,7 @@ const CleanDialog: React.FC = ({ variant="secondary" onClick={() => addPath(currentDirectory)} > - + } /> @@ -188,7 +194,7 @@ export const DataManagementTasks: React.FC = ({ return ( = ({ <> - + } diff --git a/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx b/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx index a3ba44c03..84b1e0d5a 100644 --- a/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx @@ -1,3 +1,8 @@ +import { + faMinus, + faPencilAlt, + faPlus, +} from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, Col, Form, Row } from "react-bootstrap"; import { useIntl } from "react-intl"; @@ -41,7 +46,7 @@ export const DirectorySelectionDialog: React.FC show modalProps={{ animation }} disabled={!allowEmpty && paths.length === 0} - icon="pencil-alt" + icon={faPencilAlt} header={intl.formatMessage({ id: "actions.select_folders" })} accept={{ onClick: () => { @@ -69,7 +74,7 @@ export const DirectorySelectionDialog: React.FC title={intl.formatMessage({ id: "actions.delete" })} onClick={() => removePath(p)} > - + @@ -84,7 +89,7 @@ export const DirectorySelectionDialog: React.FC variant="secondary" onClick={() => addPath(currentDirectory)} > - + } /> diff --git a/ui/v2.5/src/components/Settings/Tasks/ImportDialog.tsx b/ui/v2.5/src/components/Settings/Tasks/ImportDialog.tsx index 9a73dc693..ff5025089 100644 --- a/ui/v2.5/src/components/Settings/Tasks/ImportDialog.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/ImportDialog.tsx @@ -5,6 +5,7 @@ import { Modal } from "src/components/Shared"; import * as GQL from "src/core/generated-graphql"; import { useToast } from "src/hooks"; import { useIntl } from "react-intl"; +import { faPencilAlt } from "@fortawesome/free-solid-svg-icons"; interface IImportDialogProps { onClose: () => void; @@ -115,7 +116,7 @@ export const ImportDialog: React.FC = ( return ( { diff --git a/ui/v2.5/src/components/Settings/Tasks/JobTable.tsx b/ui/v2.5/src/components/Settings/Tasks/JobTable.tsx index ba6a04b80..b88107434 100644 --- a/ui/v2.5/src/components/Settings/Tasks/JobTable.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/JobTable.tsx @@ -7,8 +7,15 @@ import { } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; import { Icon } from "src/components/Shared"; -import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { useIntl } from "react-intl"; +import { + faBan, + faCheck, + faCircle, + faCog, + faHourglassStart, + faTimes, +} from "@fortawesome/free-solid-svg-icons"; type JobFragment = Pick< GQL.Job, @@ -68,25 +75,25 @@ const Task: React.FC = ({ job }) => { } function getStatusIcon() { - let icon: IconProp = "circle"; + let icon = faCircle; let iconClass = ""; switch (job.status) { case GQL.JobStatus.Ready: - icon = "hourglass-start"; + icon = faHourglassStart; break; case GQL.JobStatus.Running: - icon = "cog"; + icon = faCog; iconClass = "fa-spin"; break; case GQL.JobStatus.Stopping: - icon = "cog"; + icon = faCog; iconClass = "fa-spin"; break; case GQL.JobStatus.Finished: - icon = "check"; + icon = faCheck; break; case GQL.JobStatus.Cancelled: - icon = "ban"; + icon = faBan; break; } @@ -138,7 +145,7 @@ const Task: React.FC = ({ job }) => { onClick={() => stopJob()} disabled={!canStop()} > - +
diff --git a/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx b/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx index eb9045336..82c4c3b17 100644 --- a/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx @@ -17,8 +17,9 @@ import { useToast } from "src/hooks"; import { GenerateOptions } from "./GenerateOptions"; import { SettingSection } from "../SettingSection"; import { BooleanSetting, Setting, SettingGroup } from "../Inputs"; -import { ManualLink } from "src/components/Help/Manual"; +import { ManualLink } from "src/components/Help/context"; import { Icon } from "src/components/Shared"; +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; interface IAutoTagOptions { options: GQL.AutoTagMetadataInput; @@ -296,7 +297,7 @@ export const LibraryTasks: React.FC = () => { <> - + ), @@ -335,7 +336,7 @@ export const LibraryTasks: React.FC = () => { <> - + } @@ -358,7 +359,7 @@ export const LibraryTasks: React.FC = () => { <> - + ), @@ -399,7 +400,7 @@ export const LibraryTasks: React.FC = () => { <> - + ), diff --git a/ui/v2.5/src/components/Settings/context.tsx b/ui/v2.5/src/components/Settings/context.tsx index 2104c71f7..757d548a0 100644 --- a/ui/v2.5/src/components/Settings/context.tsx +++ b/ui/v2.5/src/components/Settings/context.tsx @@ -1,5 +1,9 @@ import { ApolloError } from "@apollo/client/errors"; -import { debounce } from "lodash"; +import { + faCheckCircle, + faTimesCircle, +} from "@fortawesome/free-solid-svg-icons"; +import debounce from "lodash-es/debounce"; import React, { useState, useEffect, @@ -452,7 +456,7 @@ export const SettingsContext: React.FC = ({ children }) => { if (updateSuccess === false) { return (
- +
); } @@ -477,7 +481,7 @@ export const SettingsContext: React.FC = ({ children }) => { if (updateSuccess) { return (
- +
); } diff --git a/ui/v2.5/src/components/SettingsButton.tsx b/ui/v2.5/src/components/SettingsButton.tsx index a15203da5..b8f9e2a5a 100644 --- a/ui/v2.5/src/components/SettingsButton.tsx +++ b/ui/v2.5/src/components/SettingsButton.tsx @@ -4,6 +4,7 @@ import { Button } from "react-bootstrap"; import { useJobQueue, useJobsSubscribe } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; import { useIntl } from "react-intl"; +import { faCog } from "@fortawesome/free-solid-svg-icons"; type JobFragment = Pick< GQL.Job, @@ -59,7 +60,7 @@ export const SettingsButton: React.FC = () => { className="minimal d-flex align-items-center h-100" title={intl.formatMessage({ id: "settings" })} > - 0} /> + 0} /> ); }; diff --git a/ui/v2.5/src/components/Setup/Migrate.tsx b/ui/v2.5/src/components/Setup/Migrate.tsx index 788481ca8..6dd6851ec 100644 --- a/ui/v2.5/src/components/Setup/Migrate.tsx +++ b/ui/v2.5/src/components/Setup/Migrate.tsx @@ -180,3 +180,5 @@ export const Migrate: React.FC = () => { ); }; + +export default Migrate; diff --git a/ui/v2.5/src/components/Setup/Setup.tsx b/ui/v2.5/src/components/Setup/Setup.tsx index 066c24047..16a10fcd5 100644 --- a/ui/v2.5/src/components/Setup/Setup.tsx +++ b/ui/v2.5/src/components/Setup/Setup.tsx @@ -15,6 +15,11 @@ import { ConfigurationContext } from "src/hooks/Config"; import StashConfiguration from "../Settings/StashConfiguration"; import { Icon, LoadingIndicator, Modal } from "../Shared"; import { FolderSelectDialog } from "../Shared/FolderSelect/FolderSelectDialog"; +import { + faEllipsisH, + faExclamationTriangle, + faQuestionCircle, +} from "@fortawesome/free-solid-svg-icons"; export const Setup: React.FC = () => { const { configuration, loading: configLoading } = useContext( @@ -108,7 +113,7 @@ export const Setup: React.FC = () => { return ( { className="text-input" onClick={() => setShowGeneratedDialog(true)} > - + @@ -528,7 +533,7 @@ export const Setup: React.FC = () => {

}} + values={{ icon: }} />

@@ -640,3 +645,5 @@ export const Setup: React.FC = () => { ); }; + +export default Setup; diff --git a/ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx b/ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx index ba292d1a7..a64754d61 100644 --- a/ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx +++ b/ui/v2.5/src/components/Shared/BulkUpdateTextInput.tsx @@ -1,7 +1,8 @@ +import { faBan } from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { Button, Form, FormControlProps, InputGroup } from "react-bootstrap"; import { useIntl } from "react-intl"; -import { Icon } from "."; +import Icon from "./Icon"; interface IBulkUpdateTextInputProps extends FormControlProps { valueChanged: (value: string | undefined) => void; @@ -37,7 +38,7 @@ export const BulkUpdateTextInput: React.FC = ({ onClick={() => valueChanged(undefined)} title={intl.formatMessage({ id: "actions.unset" })} > - + ) : undefined} diff --git a/ui/v2.5/src/components/Shared/CollapseButton.tsx b/ui/v2.5/src/components/Shared/CollapseButton.tsx index e29097821..2216121de 100644 --- a/ui/v2.5/src/components/Shared/CollapseButton.tsx +++ b/ui/v2.5/src/components/Shared/CollapseButton.tsx @@ -1,6 +1,10 @@ +import { + faChevronDown, + faChevronRight, +} from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, Collapse } from "react-bootstrap"; -import { Icon } from "src/components/Shared"; +import Icon from "src/components/Shared/Icon"; interface IProps { text: string; @@ -17,7 +21,7 @@ export const CollapseButton: React.FC> = ( onClick={() => setOpen(!open)} className="minimal collapse-button" > - + {props.text} diff --git a/ui/v2.5/src/components/Shared/DeleteEntityDialog.tsx b/ui/v2.5/src/components/Shared/DeleteEntityDialog.tsx index fe5cc4922..cf6284982 100644 --- a/ui/v2.5/src/components/Shared/DeleteEntityDialog.tsx +++ b/ui/v2.5/src/components/Shared/DeleteEntityDialog.tsx @@ -2,8 +2,9 @@ import React, { useState } from "react"; import { defineMessages, FormattedMessage, useIntl } from "react-intl"; import { FetchResult } from "@apollo/client"; -import { Modal } from "src/components/Shared"; +import Modal from "src/components/Shared/Modal"; import { useToast } from "src/hooks"; +import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; interface IDeletionEntity { id: string; @@ -78,7 +79,7 @@ const DeleteEntityDialog: React.FC = ({ return ( = (props: IProps) => { disabled={props.disabled} onClick={() => increment()} > - + ); @@ -86,7 +91,7 @@ export const DurationInput: React.FC = (props: IProps) => { if (props.onReset) { return ( ); } diff --git a/ui/v2.5/src/components/Shared/ExportDialog.tsx b/ui/v2.5/src/components/Shared/ExportDialog.tsx index 6e49b740a..3c0ad5b7c 100644 --- a/ui/v2.5/src/components/Shared/ExportDialog.tsx +++ b/ui/v2.5/src/components/Shared/ExportDialog.tsx @@ -1,11 +1,12 @@ import React, { useState } from "react"; import { Form } from "react-bootstrap"; import { mutateExportObjects } from "src/core/StashService"; -import { Modal } from "src/components/Shared"; -import { useToast } from "src/hooks"; -import { downloadFile } from "src/utils"; +import Modal from "src/components/Shared/Modal"; +import useToast from "src/hooks/Toast"; +import downloadFile from "src/utils/download"; import { ExportObjectsInput } from "src/core/generated-graphql"; import { useIntl } from "react-intl"; +import { faCogs } from "@fortawesome/free-solid-svg-icons"; interface IExportDialogProps { exportInput: ExportObjectsInput; @@ -47,7 +48,7 @@ export const ExportDialog: React.FC = ( return ( = ({ {loading ? ( ) : ( - + )} ) : undefined} diff --git a/ui/v2.5/src/components/Shared/Icon.tsx b/ui/v2.5/src/components/Shared/Icon.tsx index 533477575..d1f2616f6 100644 --- a/ui/v2.5/src/components/Shared/Icon.tsx +++ b/ui/v2.5/src/components/Shared/Icon.tsx @@ -1,17 +1,9 @@ import React from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { IconProp, SizeProp, library } from "@fortawesome/fontawesome-svg-core"; -import { faStar as fasStar } from "@fortawesome/free-solid-svg-icons"; -import { - faCheckCircle as farCheckCircle, - faStar as farStar, -} from "@fortawesome/free-regular-svg-icons"; - -// need these to use far and fas styles of stars -library.add(fasStar, farStar, farCheckCircle); +import { IconDefinition, SizeProp } from "@fortawesome/fontawesome-svg-core"; interface IIcon { - icon: IconProp; + icon: IconDefinition; className?: string; color?: string; size?: SizeProp; diff --git a/ui/v2.5/src/components/Shared/ImageInput.tsx b/ui/v2.5/src/components/Shared/ImageInput.tsx index 4b6183779..f1ace6f83 100644 --- a/ui/v2.5/src/components/Shared/ImageInput.tsx +++ b/ui/v2.5/src/components/Shared/ImageInput.tsx @@ -8,8 +8,9 @@ import { Row, } from "react-bootstrap"; import { useIntl } from "react-intl"; -import { Modal } from "."; +import Modal from "./Modal"; import Icon from "./Icon"; +import { faFile, faLink } from "@fortawesome/free-solid-svg-icons"; interface IImageInput { isEditing: boolean; @@ -100,7 +101,7 @@ export const ImageInput: React.FC = ({

= ({
diff --git a/ui/v2.5/src/components/Shared/Modal.tsx b/ui/v2.5/src/components/Shared/Modal.tsx index 1ff9a1a50..8cf3b8029 100644 --- a/ui/v2.5/src/components/Shared/Modal.tsx +++ b/ui/v2.5/src/components/Shared/Modal.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Button, Modal, Spinner, ModalProps } from "react-bootstrap"; -import { Icon } from "src/components/Shared"; -import { IconName } from "@fortawesome/fontawesome-svg-core"; +import Icon from "src/components/Shared/Icon"; +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { FormattedMessage } from "react-intl"; interface IButton { @@ -14,7 +14,7 @@ interface IModal { show: boolean; onHide?: () => void; header?: string; - icon?: IconName; + icon?: IconDefinition; cancel?: IButton; accept?: IButton; isRunning?: boolean; diff --git a/ui/v2.5/src/components/Shared/OperationButton.tsx b/ui/v2.5/src/components/Shared/OperationButton.tsx index a7c23dcf2..c71d9e139 100644 --- a/ui/v2.5/src/components/Shared/OperationButton.tsx +++ b/ui/v2.5/src/components/Shared/OperationButton.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useEffect } from "react"; import { Button, ButtonProps } from "react-bootstrap"; -import { LoadingIndicator } from "src/components/Shared"; +import LoadingIndicator from "src/components/Shared/LoadingIndicator"; interface IOperationButton extends ButtonProps { operation?: () => Promise; diff --git a/ui/v2.5/src/components/Shared/PerformerPopoverButton.tsx b/ui/v2.5/src/components/Shared/PerformerPopoverButton.tsx index f669281a8..d12da10d3 100644 --- a/ui/v2.5/src/components/Shared/PerformerPopoverButton.tsx +++ b/ui/v2.5/src/components/Shared/PerformerPopoverButton.tsx @@ -1,3 +1,4 @@ +import { faUser } from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { Button } from "react-bootstrap"; import { Link } from "react-router-dom"; @@ -36,7 +37,7 @@ export const PerformerPopoverButton: React.FC = ({ performers }) => { content={popoverContent} > diff --git a/ui/v2.5/src/components/Shared/PopoverCountButton.tsx b/ui/v2.5/src/components/Shared/PopoverCountButton.tsx index d7e3904d1..3eb1c731f 100644 --- a/ui/v2.5/src/components/Shared/PopoverCountButton.tsx +++ b/ui/v2.5/src/components/Shared/PopoverCountButton.tsx @@ -1,3 +1,9 @@ +import { + faFilm, + faImage, + faImages, + faPlayCircle, +} from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { Button } from "react-bootstrap"; import { useIntl } from "react-intl"; @@ -24,13 +30,13 @@ export const PopoverCountButton: React.FC = ({ function getIcon() { switch (type) { case "scene": - return "play-circle"; + return faPlayCircle; case "image": - return "image"; + return faImage; case "gallery": - return "images"; + return faImages; case "movie": - return "film"; + return faFilm; } } diff --git a/ui/v2.5/src/components/Shared/RatingStars.tsx b/ui/v2.5/src/components/Shared/RatingStars.tsx index 0847dba0b..3b26fc96e 100644 --- a/ui/v2.5/src/components/Shared/RatingStars.tsx +++ b/ui/v2.5/src/components/Shared/RatingStars.tsx @@ -1,3 +1,5 @@ +import { faStar as fasStar } from "@fortawesome/free-solid-svg-icons"; +import { faStar as farStar } from "@fortawesome/free-regular-svg-icons"; import React from "react"; import Icon from "./Icon"; @@ -12,21 +14,21 @@ interface IProps { export const RatingStars: React.FC = ({ rating }) => rating ? (
- + = 2 ? "fas" : "far", "star"]} + icon={rating >= 2 ? fasStar : farStar} className={rating >= 2 ? CLASSNAME_FILLED : CLASSNAME_UNFILLED} /> = 3 ? "fas" : "far", "star"]} + icon={rating >= 3 ? fasStar : farStar} className={rating >= 3 ? CLASSNAME_FILLED : CLASSNAME_UNFILLED} /> = 4 ? "fas" : "far", "star"]} + icon={rating >= 4 ? fasStar : farStar} className={rating >= 4 ? CLASSNAME_FILLED : CLASSNAME_UNFILLED} />
diff --git a/ui/v2.5/src/components/Shared/ScrapeDialog.tsx b/ui/v2.5/src/components/Shared/ScrapeDialog.tsx index ecaa3bf00..0fb9c8ef5 100644 --- a/ui/v2.5/src/components/Shared/ScrapeDialog.tsx +++ b/ui/v2.5/src/components/Shared/ScrapeDialog.tsx @@ -8,9 +8,18 @@ import { FormControl, Badge, } from "react-bootstrap"; -import { CollapseButton, Icon, Modal } from "src/components/Shared"; -import _ from "lodash"; +import { CollapseButton } from "src/components/Shared/CollapseButton"; +import Icon from "src/components/Shared/Icon"; +import Modal from "src/components/Shared/Modal"; +import isEqual from "lodash-es/isEqual"; +import clone from "lodash-es/clone"; import { FormattedMessage, useIntl } from "react-intl"; +import { + faCheck, + faPencilAlt, + faPlus, + faTimes, +} from "@fortawesome/free-solid-svg-icons"; export class ScrapeResult { public newValue?: T; @@ -22,7 +31,7 @@ export class ScrapeResult { this.originalValue = originalValue ?? undefined; this.newValue = newValue ?? undefined; - const valuesEqual = _.isEqual(originalValue, newValue); + const valuesEqual = isEqual(originalValue, newValue); this.useNewValue = !!this.newValue && !valuesEqual; this.scraped = this.useNewValue; } @@ -33,10 +42,10 @@ export class ScrapeResult { } public cloneWithValue(value?: T) { - const ret = _.clone(this); + const ret = clone(this); ret.newValue = value; - ret.useNewValue = !_.isEqual(ret.newValue, ret.originalValue); + ret.useNewValue = isEqual(ret.newValue, ret.originalValue); ret.scraped = ret.useNewValue; return ret; @@ -73,7 +82,7 @@ function renderButtonIcon(selected: boolean) { return ( ); } @@ -82,7 +91,7 @@ export const ScrapeDialogRow = ( props: IScrapedRowProps ) => { function handleSelectClick(isNew: boolean) { - const ret = _.clone(props.result); + const ret = clone(props.result); ret.useNewValue = isNew; props.onChange(ret); } @@ -111,7 +120,7 @@ export const ScrapeDialogRow = ( > {t.name} ))} @@ -344,7 +353,7 @@ export const ScrapeDialog: React.FC = ( return ( { diff --git a/ui/v2.5/src/components/Shared/Select.tsx b/ui/v2.5/src/components/Shared/Select.tsx index 64790341d..bc494cfc9 100644 --- a/ui/v2.5/src/components/Shared/Select.tsx +++ b/ui/v2.5/src/components/Shared/Select.tsx @@ -8,7 +8,7 @@ import Select, { OptionsType, } from "react-select"; import CreatableSelect from "react-select/creatable"; -import { debounce } from "lodash"; +import debounce from "lodash-es/debounce"; import * as GQL from "src/core/generated-graphql"; import { diff --git a/ui/v2.5/src/components/Shared/StringListInput.tsx b/ui/v2.5/src/components/Shared/StringListInput.tsx index 1c4e7937d..95c4bac50 100644 --- a/ui/v2.5/src/components/Shared/StringListInput.tsx +++ b/ui/v2.5/src/components/Shared/StringListInput.tsx @@ -1,6 +1,7 @@ +import { faMinus, faPlus } from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { Button, Form, InputGroup } from "react-bootstrap"; -import { Icon } from "src/components/Shared"; +import Icon from "src/components/Shared/Icon"; interface IStringListInputProps { value: string[]; @@ -49,7 +50,7 @@ export const StringListInput: React.FC = (props) => { /> @@ -57,7 +58,7 @@ export const StringListInput: React.FC = (props) => { )}
{props.errors}
diff --git a/ui/v2.5/src/components/Shared/SuccessIcon.tsx b/ui/v2.5/src/components/Shared/SuccessIcon.tsx index 292dae0ed..79d56d979 100644 --- a/ui/v2.5/src/components/Shared/SuccessIcon.tsx +++ b/ui/v2.5/src/components/Shared/SuccessIcon.tsx @@ -1,12 +1,13 @@ +import { faCheckCircle } from "@fortawesome/free-regular-svg-icons"; import React from "react"; -import { Icon } from "src/components/Shared"; +import Icon from "src/components/Shared/Icon"; interface ISuccessIconProps { className?: string; } const SuccessIcon: React.FC = ({ className }) => ( - + ); export default SuccessIcon; diff --git a/ui/v2.5/src/components/Shared/TagLink.tsx b/ui/v2.5/src/components/Shared/TagLink.tsx index 8b28e646b..e5e7d8fb7 100644 --- a/ui/v2.5/src/components/Shared/TagLink.tsx +++ b/ui/v2.5/src/components/Shared/TagLink.tsx @@ -10,7 +10,8 @@ import { SceneDataFragment, GalleryDataFragment, } from "src/core/generated-graphql"; -import { NavUtils, TextUtils } from "src/utils"; +import NavUtils from "src/utils/navigation"; +import TextUtils from "src/utils/text"; interface IProps { tag?: Partial; diff --git a/ui/v2.5/src/components/Shared/ThreeStateCheckbox.tsx b/ui/v2.5/src/components/Shared/ThreeStateCheckbox.tsx index 9340b4114..3e7cb8ac5 100644 --- a/ui/v2.5/src/components/Shared/ThreeStateCheckbox.tsx +++ b/ui/v2.5/src/components/Shared/ThreeStateCheckbox.tsx @@ -1,6 +1,7 @@ +import { faCheck, faMinus, faTimes } from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { Button } from "react-bootstrap"; -import { Icon } from "."; +import Icon from "./Icon"; interface IThreeStateCheckbox { value: boolean | undefined; @@ -28,7 +29,7 @@ export const ThreeStateCheckbox: React.FC = ({ return true; } - const icon = value === undefined ? "minus" : value ? "check" : "times"; + const icon = value === undefined ? faMinus : value ? faCheck : faTimes; const labelClassName = value === undefined ? "unset" : value ? "checked" : "not-checked"; diff --git a/ui/v2.5/src/components/Shared/TruncatedText.tsx b/ui/v2.5/src/components/Shared/TruncatedText.tsx index 4c450403c..fd0c925b9 100644 --- a/ui/v2.5/src/components/Shared/TruncatedText.tsx +++ b/ui/v2.5/src/components/Shared/TruncatedText.tsx @@ -1,7 +1,7 @@ import React, { useRef, useState } from "react"; import { Overlay, Tooltip } from "react-bootstrap"; import { Placement } from "react-bootstrap/Overlay"; -import { debounce } from "lodash"; +import debounce from "lodash-es/debounce"; import cx from "classnames"; const CLASSNAME = "TruncatedText"; diff --git a/ui/v2.5/src/components/Shared/URLField.tsx b/ui/v2.5/src/components/Shared/URLField.tsx index cc233f9d9..d9818ddef 100644 --- a/ui/v2.5/src/components/Shared/URLField.tsx +++ b/ui/v2.5/src/components/Shared/URLField.tsx @@ -1,8 +1,9 @@ import React from "react"; import { useIntl } from "react-intl"; import { Button, InputGroup, Form } from "react-bootstrap"; -import { Icon } from "src/components/Shared"; +import Icon from "src/components/Shared/Icon"; import { FormikHandlers } from "formik"; +import { faFileDownload } from "@fortawesome/free-solid-svg-icons"; interface IProps { value: string; @@ -36,7 +37,7 @@ export const URLField: React.FC = (props: IProps) => { disabled={!props.value || !props.urlScrapable(props.value)} title={intl.formatMessage({ id: "actions.scrape" })} > - + diff --git a/ui/v2.5/src/components/Stats.tsx b/ui/v2.5/src/components/Stats.tsx index bfc1ff01b..69007f122 100644 --- a/ui/v2.5/src/components/Stats.tsx +++ b/ui/v2.5/src/components/Stats.tsx @@ -121,3 +121,5 @@ export const Stats: React.FC = () => {
); }; + +export default Stats; diff --git a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx index 56875b40c..6a43ce54e 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx @@ -28,6 +28,7 @@ import { StudioPerformersPanel } from "./StudioPerformersPanel"; import { StudioEditPanel } from "./StudioEditPanel"; import { StudioDetailsPanel } from "./StudioDetailsPanel"; import { StudioMoviesPanel } from "./StudioMoviesPanel"; +import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; interface IProps { studio: GQL.StudioDataFragment; @@ -112,7 +113,7 @@ const StudioPage: React.FC = ({ studio }) => { return ( ; @@ -197,7 +198,7 @@ export const StudioEditPanel: React.FC = ({ )} onClick={() => removeStashID(stashID)} > - + {link} diff --git a/ui/v2.5/src/components/Studios/StudioList.tsx b/ui/v2.5/src/components/Studios/StudioList.tsx index 794ac1046..7e14190df 100644 --- a/ui/v2.5/src/components/Studios/StudioList.tsx +++ b/ui/v2.5/src/components/Studios/StudioList.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { useIntl } from "react-intl"; -import _ from "lodash"; +import cloneDeep from "lodash-es/cloneDeep"; import { useHistory } from "react-router-dom"; import Mousetrap from "mousetrap"; import { @@ -67,7 +67,7 @@ export const StudioList: React.FC = ({ const { count } = result.data.findStudios; const index = Math.floor(Math.random() * count); - const filterCopy = _.cloneDeep(filter); + const filterCopy = cloneDeep(filter); filterCopy.itemsPerPage = 1; filterCopy.currentPage = index + 1; const singleResult = await queryFindStudios(filterCopy); diff --git a/ui/v2.5/src/components/Tagger/IncludeButton.tsx b/ui/v2.5/src/components/Tagger/IncludeButton.tsx index bc0e12ab6..b60e7e12d 100644 --- a/ui/v2.5/src/components/Tagger/IncludeButton.tsx +++ b/ui/v2.5/src/components/Tagger/IncludeButton.tsx @@ -1,3 +1,4 @@ +import { faCheck, faTimes } from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { Button } from "react-bootstrap"; import { Icon } from "../Shared"; @@ -21,7 +22,7 @@ export const IncludeExcludeButton: React.FC = ({ exclude ? "text-danger" : "text-success" } include-exclude-button`} > - + ); diff --git a/ui/v2.5/src/components/Tagger/PerformerFieldSelector.tsx b/ui/v2.5/src/components/Tagger/PerformerFieldSelector.tsx index c7635529f..3862ea7ac 100644 --- a/ui/v2.5/src/components/Tagger/PerformerFieldSelector.tsx +++ b/ui/v2.5/src/components/Tagger/PerformerFieldSelector.tsx @@ -1,3 +1,4 @@ +import { faCheck, faList, faTimes } from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, Row, Col } from "react-bootstrap"; import { useIntl } from "react-intl"; @@ -36,7 +37,7 @@ const PerformerFieldSelect: React.FC = ({ variant="secondary" className={excluded[name] ? "text-muted" : "text-success"} > - + {TextUtils.capitalize(name)} @@ -45,7 +46,7 @@ const PerformerFieldSelect: React.FC = ({ return ( void; excludedPerformerFields?: string[]; header: string; - icon: IconName; + icon: IconDefinition; create?: boolean; endpoint?: string; } @@ -91,7 +98,7 @@ const PerformerModal: React.FC = ({ variant="secondary" className={excluded[name] ? "text-muted" : "text-success"} > - + )} @@ -219,7 +226,7 @@ const PerformerModal: React.FC = ({
Stash-Box Source - +
)} @@ -236,7 +243,7 @@ const PerformerModal: React.FC = ({ excluded.image ? "text-muted" : "text-success" )} > - + )} = ({
Select performer image @@ -265,7 +272,7 @@ const PerformerModal: React.FC = ({ {imageIndex + 1} of {images.length}
diff --git a/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx b/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx index 2d3f8a27e..2a1a1f4b7 100755 --- a/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx +++ b/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx @@ -20,6 +20,7 @@ import PerformerConfig from "./Config"; import { LOCAL_FORAGE_KEY, ITaggerConfig, initialConfig } from "../constants"; import PerformerModal from "../PerformerModal"; import { useUpdatePerformer } from "../queries"; +import { faStar, faTags } from "@fortawesome/free-solid-svg-icons"; type JobFragment = Pick< GQL.Job, @@ -352,7 +353,7 @@ const PerformerTaggerList: React.FC = ({ performer={modalPerformer} onSave={handlePerformerUpdate} excludedPerformerFields={config.excludedPerformerFields} - icon="tags" + icon={faTags} header={intl.formatMessage({ id: "performer_tagger.update_performer", })} @@ -381,7 +382,7 @@ const PerformerTaggerList: React.FC = ({ = ({ = ({ modalVisible={modalPerformer !== undefined} performer={modalPerformer} onSave={handleSave} - icon="tags" + icon={faTags} header="Update Performer" excludedPerformerFields={excludedPerformerFields} endpoint={endpoint} diff --git a/ui/v2.5/src/components/Tagger/queries.ts b/ui/v2.5/src/components/Tagger/queries.ts index 9daa3b1b5..69bf6d116 100644 --- a/ui/v2.5/src/components/Tagger/queries.ts +++ b/ui/v2.5/src/components/Tagger/queries.ts @@ -1,5 +1,5 @@ import * as GQL from "src/core/generated-graphql"; -import { sortBy } from "lodash"; +import sortBy from "lodash-es/sortBy"; export const useUpdatePerformerStashID = () => { const [updatePerformer] = GQL.usePerformerUpdateMutation({ diff --git a/ui/v2.5/src/components/Tagger/scenes/Config.tsx b/ui/v2.5/src/components/Tagger/scenes/Config.tsx index 1c1702687..2eaa94a43 100644 --- a/ui/v2.5/src/components/Tagger/scenes/Config.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/Config.tsx @@ -1,3 +1,4 @@ +import { faTimes } from "@fortawesome/free-solid-svg-icons"; import React, { useRef, useContext } from "react"; import { Badge, @@ -205,7 +206,7 @@ const Config: React.FC = ({ show }) => { className="minimal ml-2" onClick={() => removeBlacklist(index)} > - + ))} diff --git a/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx b/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx index dc0ca049a..12bbef5d9 100755 --- a/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx @@ -11,6 +11,7 @@ import { ValidTypes, } from "src/components/Shared"; import { OptionalField } from "../IncludeButton"; +import { faSave } from "@fortawesome/free-solid-svg-icons"; interface IPerformerResultProps { performer: GQL.ScrapedPerformer; @@ -91,7 +92,7 @@ const PerformerResult: React.FC = ({ operation={onLink} hideChildrenWhenLoading > - + ); } diff --git a/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx b/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx index 1f26bad31..e793bb63a 100755 --- a/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx @@ -12,6 +12,7 @@ import { TaggerScene } from "./TaggerScene"; import { SceneTaggerModals } from "./sceneTaggerModals"; import { SceneSearchResults } from "./StashSearchResult"; import { ConfigurationContext } from "src/hooks/Config"; +import { faCog } from "@fortawesome/free-solid-svg-icons"; interface ITaggerProps { scenes: GQL.SlimSceneDataFragment[]; @@ -83,7 +84,7 @@ export const Tagger: React.FC = ({ scenes, queue }) => { return (
); diff --git a/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx b/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx index 7f39deac1..33b6887f9 100755 --- a/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback, useMemo } from "react"; import cx from "classnames"; import { Badge, Button, Col, Form, Row } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; -import { uniq } from "lodash"; +import uniq from "lodash-es/uniq"; import { blobToBase64 } from "base64-blob"; import { distance } from "src/utils/hamming"; @@ -24,6 +24,7 @@ import { SceneTaggerModalsState } from "./sceneTaggerModals"; import PerformerResult from "./PerformerResult"; import StudioResult from "./StudioResult"; import { useInitialState } from "src/hooks/state"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; const getDurationStatus = ( scene: IScrapedScene, @@ -612,7 +613,7 @@ const StashSearchResult: React.FC = ({ > {t.name} ))} diff --git a/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx b/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx index 35edf62c2..4ce707297 100644 --- a/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/StudioModal.tsx @@ -1,10 +1,11 @@ import React, { useContext } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { IconName } from "@fortawesome/fontawesome-svg-core"; +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import * as GQL from "src/core/generated-graphql"; import { Icon, Modal, TruncatedText } from "src/components/Shared"; import { TaggerStateContext } from "../context"; +import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons"; interface IStudioModalProps { studio: GQL.ScrapedSceneStudioDataFragment; @@ -12,7 +13,7 @@ interface IStudioModalProps { closeModal: () => void; handleStudioCreate: (input: GQL.StudioCreateInput) => void; header: string; - icon: IconName; + icon: IconDefinition; } const StudioModal: React.FC = ({ @@ -93,7 +94,7 @@ const StudioModal: React.FC = ({
Stash-Box Source - +
)} diff --git a/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx b/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx index 0c0d898bb..25a97c112 100755 --- a/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/StudioResult.tsx @@ -12,6 +12,7 @@ import { import * as GQL from "src/core/generated-graphql"; import { OptionalField } from "../IncludeButton"; +import { faSave } from "@fortawesome/free-solid-svg-icons"; interface IStudioResultProps { studio: GQL.ScrapedStudio; @@ -89,7 +90,7 @@ const StudioResult: React.FC = ({ operation={onLink} hideChildrenWhenLoading > - + ); } diff --git a/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx b/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx index 137b62f18..87755c3b5 100644 --- a/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx @@ -14,6 +14,7 @@ import { import { parsePath, prepareQueryString } from "src/components/Tagger/utils"; import { ScenePreview } from "src/components/Scenes/SceneCard"; import { TaggerStateContext } from "../context"; +import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons"; interface ITaggerSceneDetails { scene: GQL.SlimSceneDataFragment; @@ -71,7 +72,7 @@ const TaggerSceneDetails: React.FC = ({ scene }) => { className="minimal collapse-button" size="lg" > - + ); diff --git a/ui/v2.5/src/components/Tagger/scenes/sceneTaggerModals.tsx b/ui/v2.5/src/components/Tagger/scenes/sceneTaggerModals.tsx index 0e40d408a..fe670e32b 100644 --- a/ui/v2.5/src/components/Tagger/scenes/sceneTaggerModals.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/sceneTaggerModals.tsx @@ -5,6 +5,7 @@ import StudioModal from "./StudioModal"; import PerformerModal from "../PerformerModal"; import { TaggerStateContext } from "../context"; import { useIntl } from "react-intl"; +import { faTags } from "@fortawesome/free-solid-svg-icons"; type PerformerModalCallback = (toCreate?: GQL.PerformerCreateInput) => void; type StudioModalCallback = (toCreate?: GQL.StudioCreateInput) => void; @@ -112,7 +113,7 @@ export const SceneTaggerModals: React.FC = ({ children }) => { modalVisible performer={performerToCreate} onSave={handlePerformerSave} - icon="tags" + icon={faTags} header={intl.formatMessage( { id: "actions.create_entity" }, { entityType: intl.formatMessage({ id: "performer" }) } @@ -127,7 +128,7 @@ export const SceneTaggerModals: React.FC = ({ children }) => { modalVisible studio={studioToCreate} handleStudioCreate={handleStudioSave} - icon="tags" + icon={faTags} header={intl.formatMessage( { id: "actions.create_entity" }, { entityType: intl.formatMessage({ id: "studio" }) } diff --git a/ui/v2.5/src/components/Tags/TagCard.tsx b/ui/v2.5/src/components/Tags/TagCard.tsx index 8daa7bfcf..3bb2418ec 100644 --- a/ui/v2.5/src/components/Tags/TagCard.tsx +++ b/ui/v2.5/src/components/Tags/TagCard.tsx @@ -7,6 +7,7 @@ import { FormattedMessage } from "react-intl"; import { Icon } from "../Shared"; import { GridCard } from "../Shared/GridCard"; import { PopoverCountButton } from "../Shared/PopoverCountButton"; +import { faMapMarkerAlt, faUser } from "@fortawesome/free-solid-svg-icons"; interface IProps { tag: GQL.TagDataFragment; @@ -102,7 +103,7 @@ export const TagCard: React.FC = ({ return ( @@ -141,7 +142,7 @@ export const TagCard: React.FC = ({ return ( diff --git a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx index 47bb279f8..b5dc4618b 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx @@ -30,6 +30,11 @@ import { TagGalleriesPanel } from "./TagGalleriesPanel"; import { TagDetailsPanel } from "./TagDetailsPanel"; import { TagEditPanel } from "./TagEditPanel"; import { TagMergeModal } from "./TagMergeDialog"; +import { + faSignInAlt, + faSignOutAlt, + faTrashAlt, +} from "@fortawesome/free-solid-svg-icons"; interface IProps { tag: GQL.TagDataFragment; @@ -165,7 +170,7 @@ const TagPage: React.FC = ({ tag }) => { return ( = ({ tag }) => { className="bg-secondary text-white" onClick={() => setMergeType("from")} > - + ...
@@ -227,7 +232,7 @@ const TagPage: React.FC = ({ tag }) => { className="bg-secondary text-white" onClick={() => setMergeType("into")} > - + ...
diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagMergeDialog.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagMergeDialog.tsx index 891c2d228..a2c51383f 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagMergeDialog.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagMergeDialog.tsx @@ -7,6 +7,7 @@ import { useTagsMerge } from "src/core/StashService"; import { useIntl } from "react-intl"; import { useToast } from "src/hooks"; import { useHistory } from "react-router-dom"; +import { faSignInAlt, faSignOutAlt } from "@fortawesome/free-solid-svg-icons"; interface ITagMergeModalProps { show: boolean; @@ -74,7 +75,7 @@ export const TagMergeModal: React.FC = ({ onMerge(), diff --git a/ui/v2.5/src/components/Tags/TagList.tsx b/ui/v2.5/src/components/Tags/TagList.tsx index 88e804a82..e6201064f 100644 --- a/ui/v2.5/src/components/Tags/TagList.tsx +++ b/ui/v2.5/src/components/Tags/TagList.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import _ from "lodash"; +import cloneDeep from "lodash-es/cloneDeep"; import Mousetrap from "mousetrap"; import { FindTagsQueryResult } from "src/core/generated-graphql"; import { ListFilterModel } from "src/models/list-filter/filter"; @@ -25,6 +25,7 @@ import { Icon, Modal, DeleteEntityDialog } from "src/components/Shared"; import { TagCard } from "./TagCard"; import { ExportDialog } from "../Shared/ExportDialog"; import { tagRelationHook } from "../../core/tags"; +import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; interface ITagList { filterHook?: (filter: ListFilterModel) => ListFilterModel; @@ -82,7 +83,7 @@ export const TagList: React.FC = ({ filterHook }) => { const { count } = result.data.findTags; const index = Math.floor(Math.random() * count); - const filterCopy = _.cloneDeep(filter); + const filterCopy = cloneDeep(filter); filterCopy.itemsPerPage = 1; filterCopy.currentPage = index + 1; const singleResult = await queryFindTags(filterCopy); @@ -240,7 +241,7 @@ export const TagList: React.FC = ({ filterHook }) => { {}} show={!!deletingTag} - icon="trash-alt" + icon={faTrashAlt} accept={{ onClick: onDelete, variant: "danger", @@ -338,7 +339,7 @@ export const TagList: React.FC = ({ filterHook }) => { /> diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index 0f1512ef7..1bb7734df 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -6,7 +6,7 @@ import { getOperationName, } from "@apollo/client/utilities"; import { stringToGender } from "src/utils/gender"; -import { filterData } from "../utils"; +import { filterData } from "../utils/data"; import { ListFilterModel } from "../models/list-filter/filter"; import * as GQL from "./generated-graphql"; diff --git a/ui/v2.5/src/hooks/Interactive/status.tsx b/ui/v2.5/src/hooks/Interactive/status.tsx index 268fca7b8..d630cbb9c 100644 --- a/ui/v2.5/src/hooks/Interactive/status.tsx +++ b/ui/v2.5/src/hooks/Interactive/status.tsx @@ -1,3 +1,4 @@ +import { faCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React from "react"; import { FormattedMessage } from "react-intl"; @@ -35,7 +36,7 @@ export const SceneInteractiveStatus: React.FC = ({}) => { return (
- + {error && : {error}} diff --git a/ui/v2.5/src/hooks/Interval.ts b/ui/v2.5/src/hooks/Interval.ts index 854747c2b..838ea9764 100644 --- a/ui/v2.5/src/hooks/Interval.ts +++ b/ui/v2.5/src/hooks/Interval.ts @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from "react"; -import noop from "lodash/noop"; +import noop from "lodash-es/noop"; const MIN_VALID_INTERVAL = 1000; diff --git a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx index a82ef6f81..91fcffcc8 100644 --- a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx +++ b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx @@ -10,7 +10,7 @@ import { } from "react-bootstrap"; import cx from "classnames"; import Mousetrap from "mousetrap"; -import debounce from "lodash/debounce"; +import debounce from "lodash-es/debounce"; import { Icon, LoadingIndicator } from "src/components/Shared"; import { useInterval, usePageVisibility, useToast } from "src/hooks"; @@ -29,6 +29,19 @@ import { import * as GQL from "src/core/generated-graphql"; import { useInterfaceLocalForage } from "../LocalForage"; import { imageLightboxDisplayModeIntlMap } from "src/core/enums"; +import { ILightboxImage } from "./types"; +import { + faArrowLeft, + faArrowRight, + faChevronLeft, + faChevronRight, + faCog, + faExpand, + faPause, + faPlay, + faSearchMinus, + faTimes, +} from "@fortawesome/free-solid-svg-icons"; const CLASSNAME = "Lightbox"; const CLASSNAME_HEADER = `${CLASSNAME}-header`; @@ -53,18 +66,6 @@ const DEFAULT_SLIDESHOW_DELAY = 5000; const SECONDS_TO_MS = 1000; const MIN_VALID_INTERVAL_SECONDS = 1; -interface IImagePaths { - image?: GQL.Maybe; - thumbnail?: GQL.Maybe; -} -export interface ILightboxImage { - id?: string; - title?: GQL.Maybe; - rating?: GQL.Maybe; - o_counter?: GQL.Maybe; - paths: IImagePaths; -} - interface IProps { images: ILightboxImage[]; isVisible: boolean; @@ -643,7 +644,7 @@ export const LightboxComponent: React.FC = ({ })} onClick={() => setShowOptions(!showOptions)} > - + = ({ onClick={toggleSlideshow} title="Toggle Slideshow" > - + )} {zoom !== 1 && ( @@ -691,7 +692,7 @@ export const LightboxComponent: React.FC = ({ }} title="Reset zoom" > - + )} {document.fullscreenEnabled && ( @@ -700,11 +701,11 @@ export const LightboxComponent: React.FC = ({ onClick={toggleFullscreen} title="Toggle Fullscreen" > - + )}
@@ -715,7 +716,7 @@ export const LightboxComponent: React.FC = ({ onClick={handleLeft} className={`${CLASSNAME_NAVBUTTON} d-none d-lg-block`} > - + )} @@ -757,7 +758,7 @@ export const LightboxComponent: React.FC = ({ onClick={handleRight} className={`${CLASSNAME_NAVBUTTON} d-none d-lg-block`} > - + )} @@ -768,7 +769,7 @@ export const LightboxComponent: React.FC = ({ onClick={() => setIndex(images.length - 1)} className={CLASSNAME_NAVBUTTON} > - + {navItems} )} @@ -813,3 +814,5 @@ export const LightboxComponent: React.FC = ({ ); }; + +export default LightboxComponent; diff --git a/ui/v2.5/src/hooks/Lightbox/context.tsx b/ui/v2.5/src/hooks/Lightbox/context.tsx index c8e9bc106..2f857d5fb 100644 --- a/ui/v2.5/src/hooks/Lightbox/context.tsx +++ b/ui/v2.5/src/hooks/Lightbox/context.tsx @@ -1,5 +1,7 @@ -import React, { useCallback, useState } from "react"; -import { ILightboxImage, LightboxComponent } from "./Lightbox"; +import React, { lazy, Suspense, useCallback, useState } from "react"; +import { ILightboxImage } from "./types"; + +const LightboxComponent = lazy(() => import("./Lightbox")); export interface IState { images: ILightboxImage[]; @@ -48,9 +50,11 @@ const Lightbox: React.FC = ({ children }) => { return ( {children} - {lightboxState.isVisible && ( - - )} + }> + {lightboxState.isVisible && ( + + )} + ); }; diff --git a/ui/v2.5/src/hooks/Lightbox/types.ts b/ui/v2.5/src/hooks/Lightbox/types.ts new file mode 100644 index 000000000..70bd16454 --- /dev/null +++ b/ui/v2.5/src/hooks/Lightbox/types.ts @@ -0,0 +1,14 @@ +import * as GQL from "src/core/generated-graphql"; + +interface IImagePaths { + image?: GQL.Maybe; + thumbnail?: GQL.Maybe; +} + +export interface ILightboxImage { + id?: string; + title?: GQL.Maybe; + rating?: GQL.Maybe; + o_counter?: GQL.Maybe; + paths: IImagePaths; +} diff --git a/ui/v2.5/src/hooks/ListHook.tsx b/ui/v2.5/src/hooks/ListHook.tsx index 5f95cf010..26e50bab5 100644 --- a/ui/v2.5/src/hooks/ListHook.tsx +++ b/ui/v2.5/src/hooks/ListHook.tsx @@ -1,4 +1,5 @@ -import _ from "lodash"; +import clone from "lodash-es/clone"; +import cloneDeep from "lodash-es/cloneDeep"; import queryString from "query-string"; import React, { useCallback, @@ -10,7 +11,7 @@ import React, { import { ApolloError } from "@apollo/client"; import { useHistory, useLocation } from "react-router-dom"; import Mousetrap from "mousetrap"; -import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { SlimSceneDataFragment, SceneMarkerDataFragment, @@ -99,7 +100,7 @@ export interface IListHookOperation { selectedIds: Set ) => boolean; postRefetch?: boolean; - icon?: IconProp; + icon?: IconDefinition; buttonVariant?: string; } @@ -268,7 +269,7 @@ const RenderList = < function singleSelect(id: string, selected: boolean) { setLastClickedId(id); - const newSelectedIds = _.clone(selectedIds); + const newSelectedIds = clone(selectedIds); if (selected) { newSelectedIds.add(id); } else { @@ -339,7 +340,7 @@ const RenderList = < } function onChangeZoom(newZoomIndex: number) { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); newFilter.zoomIndex = newZoomIndex; updateQueryParams(newFilter); } @@ -434,7 +435,7 @@ const RenderList = < } function onChangeDisplayMode(displayMode: DisplayMode) { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); newFilter.displayMode = displayMode; updateQueryParams(newFilter); } @@ -443,7 +444,7 @@ const RenderList = < criterion: Criterion, oldId?: string ) { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); // Find if we are editing an existing criteria, then modify that. Or create a new one. const existingIndex = newFilter.criteria.findIndex((c) => { @@ -469,7 +470,7 @@ const RenderList = < } function onRemoveCriterion(removedCriterion: Criterion) { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); newFilter.criteria = newFilter.criteria.filter( (criterion) => criterion.getId() !== removedCriterion.getId() ); @@ -478,7 +479,7 @@ const RenderList = < } function updateCriteria(c: Criterion[]) { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); newFilter.criteria = c.slice(); setNewCriterion(false); } @@ -713,7 +714,7 @@ const useList = ( const onChangePage = useCallback( (page: number) => { - const newFilter = _.cloneDeep(filter); + const newFilter = cloneDeep(filter); newFilter.currentPage = page; updateQueryParams(newFilter); window.scrollTo(0, 0); @@ -722,9 +723,7 @@ const useList = ( ); const renderFilter = useMemo(() => { - return !options.filterHook - ? filter - : options.filterHook(_.cloneDeep(filter)); + return !options.filterHook ? filter : options.filterHook(cloneDeep(filter)); }, [filter, options]); const { contentTemplate, onSelectChange } = RenderList({ diff --git a/ui/v2.5/src/hooks/LocalForage.ts b/ui/v2.5/src/hooks/LocalForage.ts index 5d47a4c50..b8e6aae9a 100644 --- a/ui/v2.5/src/hooks/LocalForage.ts +++ b/ui/v2.5/src/hooks/LocalForage.ts @@ -1,5 +1,5 @@ import localForage from "localforage"; -import _ from "lodash"; +import isEqual from "lodash-es/isEqual"; import React, { Dispatch, SetStateAction, useEffect } from "react"; import { ConfigImageLightboxInput } from "src/core/generated-graphql"; @@ -69,7 +69,7 @@ export function useLocalForage( }, [loading, key, defaultValue]); useEffect(() => { - if (!_.isEqual(Cache[key], data)) { + if (isEqual(Cache[key], data)) { Cache[key] = { ...Cache[key], ...data, diff --git a/ui/v2.5/src/locales/index.ts b/ui/v2.5/src/locales/index.ts index 0b03b1a64..bf6fd19b4 100644 --- a/ui/v2.5/src/locales/index.ts +++ b/ui/v2.5/src/locales/index.ts @@ -1,41 +1,24 @@ -import deDE from "./de-DE.json"; -import enGB from "./en-GB.json"; -import enUS from "./en-US.json"; -import esES from "./es-ES.json"; -import ptBR from "./pt-BR.json"; -import frFR from "./fr-FR.json"; -import itIT from "./it-IT.json"; -import fiFI from "./fi-FI.json"; -import svSE from "./sv-SE.json"; -import zhTW from "./zh-TW.json"; -import zhCN from "./zh-CN.json"; -import hrHR from "./hr-HR.json"; -import nlNL from "./nl-NL.json"; -import ruRU from "./ru-RU.json"; -import trTR from "./tr-TR.json"; -import jaJP from "./ja-JP.json"; -import plPL from "./pl-PL.json"; -import daDK from "./da-DK.json"; -import koKR from "./ko-KR.json"; +export const localeLoader = { + deDE: () => import("./de-DE.json"), + enGB: () => import("./en-GB.json"), + enUS: () => import("./en-US.json"), + esES: () => import("./es-ES.json"), + ptBR: () => import("./pt-BR.json"), + frFR: () => import("./fr-FR.json"), + itIT: () => import("./it-IT.json"), + fiFI: () => import("./fi-FI.json"), + svSE: () => import("./sv-SE.json"), + zhTW: () => import("./zh-TW.json"), + zhCN: () => import("./zh-CN.json"), + hrHR: () => import("./hr-HR.json"), + nlNL: () => import("./nl-NL.json"), + ruRU: () => import("./ru-RU.json"), + trTR: () => import("./tr-TR.json"), + jaJP: () => import("./ja-JP.json"), + plPL: () => import("./pl-PL.json"), + daDK: () => import("./da-DK.json"), + koKR: () => import("./ko-KR.json"), + // eslint-disable-next-line @typescript-eslint/no-explicit-any +} as { [key: string]: any }; -export default { - deDE, - enGB, - enUS, - esES, - ptBR, - frFR, - itIT, - fiFI, - svSE, - zhTW, - zhCN, - hrHR, - nlNL, - ruRU, - trTR, - jaJP, - plPL, - daDK, - koKR, -}; +export default localeLoader; diff --git a/ui/v2.5/src/models/sceneQueue.ts b/ui/v2.5/src/models/sceneQueue.ts index 948cebc72..3c558493a 100644 --- a/ui/v2.5/src/models/sceneQueue.ts +++ b/ui/v2.5/src/models/sceneQueue.ts @@ -129,3 +129,5 @@ export class SceneQueue { return `/scenes/${sceneID}${params.length ? "?" + params.join("&") : ""}`; } } + +export default SceneQueue; diff --git a/ui/v2.5/src/utils/bulkUpdate.ts b/ui/v2.5/src/utils/bulkUpdate.ts index c91175140..542a57337 100644 --- a/ui/v2.5/src/utils/bulkUpdate.ts +++ b/ui/v2.5/src/utils/bulkUpdate.ts @@ -1,5 +1,5 @@ import * as GQL from "src/core/generated-graphql"; -import _ from "lodash"; +import isEqual from "lodash-es/isEqual"; interface IHasRating { rating?: GQL.Maybe | undefined; @@ -63,7 +63,7 @@ export function getAggregatePerformerIds(state: IHasPerformers[]) { } else { const perfIds = o.performers ? o.performers.map((p) => p.id).sort() : []; - if (!_.isEqual(ret, perfIds)) { + if (isEqual(ret, perfIds)) { ret = []; } } @@ -87,7 +87,7 @@ export function getAggregateTagIds(state: IHasTags[]) { } else { const tIds = o.tags ? o.tags.map((t) => t.id).sort() : []; - if (!_.isEqual(ret, tIds)) { + if (isEqual(ret, tIds)) { ret = []; } } @@ -115,7 +115,7 @@ export function getAggregateMovieIds(state: IHasMovies[]) { } else { const mIds = o.movies ? o.movies.map((m) => m.movie.id).sort() : []; - if (!_.isEqual(ret, mIds)) { + if (isEqual(ret, mIds)) { ret = []; } } @@ -180,7 +180,7 @@ export function getAggregateState( newValue: T, first: boolean ) { - if (!first && !_.isEqual(currentValue, newValue)) { + if (!first && isEqual(currentValue, newValue)) { return undefined; } diff --git a/ui/v2.5/yarn.lock b/ui/v2.5/yarn.lock index a0a6a33fa..4a9dad558 100644 --- a/ui/v2.5/yarn.lock +++ b/ui/v2.5/yarn.lock @@ -1384,7 +1384,19 @@ dependencies: "@types/node" "*" -"@types/lodash@^4.14.165", "@types/lodash@^4.14.168": +"@types/lodash-es@^4.17.6": + version "4.17.6" + resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.6.tgz#c2ed4c8320ffa6f11b43eb89e9eaeec65966a0a0" + integrity sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.182" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" + integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== + +"@types/lodash@^4.14.165": version "4.14.168" resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz" integrity sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q== @@ -2397,6 +2409,11 @@ chardet@^0.7.0: optionalDependencies: fsevents "~2.3.1" +classnames@^2.2.5: + version "2.3.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" + integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== + classnames@^2.2.6: version "2.2.6" resolved "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz" @@ -4904,7 +4921,7 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" -lodash-es@^4.17.14, lodash-es@^4.17.15, lodash-es@^4.17.20: +lodash-es@^4.17.14, lodash-es@^4.17.15, lodash-es@^4.17.20, lodash-es@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== From 63e1bbf35ded85426c4b44824ce4004b35ca7ae9 Mon Sep 17 00:00:00 2001 From: DingDongSoLong4 <99329275+DingDongSoLong4@users.noreply.github.com> Date: Wed, 22 Jun 2022 07:45:47 +0200 Subject: [PATCH 27/34] UI and filter fixes (#2686) * Use primitive string in recommendation row props * Use unique keys in recommendation rows The keys for the cards used while loading clash with the ids of the actual cards, causing a list unique key warning. * List filter alignment tweaks * Rework list hook filtering * Internationalise checksum correctly --- .../FrontPage/RecommendationRow.tsx | 2 +- .../Galleries/GalleryRecommendationRow.tsx | 7 +- .../Images/ImageRecommendationRow.tsx | 4 +- ui/v2.5/src/components/List/ListFilter.tsx | 12 +- .../components/List/ListOperationButtons.tsx | 6 +- .../src/components/List/ListViewOptions.tsx | 8 +- .../Movies/MovieRecommendationRow.tsx | 4 +- .../Performers/PerformerRecommendationRow.tsx | 7 +- .../Scenes/SceneRecommendationRow.tsx | 4 +- .../Studios/StudioRecommendationRow.tsx | 7 +- .../components/Tags/TagRecommendationRow.tsx | 4 +- ui/v2.5/src/hooks/ListHook.tsx | 252 +++++++++--------- .../models/list-filter/criteria/factory.ts | 4 +- 13 files changed, 171 insertions(+), 150 deletions(-) diff --git a/ui/v2.5/src/components/FrontPage/RecommendationRow.tsx b/ui/v2.5/src/components/FrontPage/RecommendationRow.tsx index e6e58b10a..0b48434c0 100644 --- a/ui/v2.5/src/components/FrontPage/RecommendationRow.tsx +++ b/ui/v2.5/src/components/FrontPage/RecommendationRow.tsx @@ -2,7 +2,7 @@ import React, { PropsWithChildren } from "react"; interface IProps { className?: string; - header: String; + header: string; link: JSX.Element; } diff --git a/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx b/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx index d3ca6823e..437eaeb94 100644 --- a/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx @@ -10,7 +10,7 @@ import { FormattedMessage } from "react-intl"; interface IProps { isTouch: boolean; filter: ListFilterModel; - header: String; + header: string; } export const GalleryRecommendationRow: FunctionComponent = ( @@ -41,7 +41,10 @@ export const GalleryRecommendationRow: FunctionComponent = ( > {result.loading ? [...Array(props.filter.itemsPerPage)].map((i) => ( -
+
)) : result.data?.findGalleries.galleries.map((g) => ( diff --git a/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx b/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx index 6d76f3e17..55f7e7b78 100644 --- a/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx +++ b/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx @@ -10,7 +10,7 @@ import { ImageCard } from "./ImageCard"; interface IProps { isTouch: boolean; filter: ListFilterModel; - header: String; + header: string; } export const ImageRecommendationRow: FunctionComponent = ( @@ -41,7 +41,7 @@ export const ImageRecommendationRow: FunctionComponent = ( > {result.loading ? [...Array(props.filter.itemsPerPage)].map((i) => ( -
+
)) : result.data?.findImages.images.map((i) => ( diff --git a/ui/v2.5/src/components/List/ListFilter.tsx b/ui/v2.5/src/components/List/ListFilter.tsx index 56e0a8a76..9236a5561 100644 --- a/ui/v2.5/src/components/List/ListFilter.tsx +++ b/ui/v2.5/src/components/List/ListFilter.tsx @@ -217,8 +217,8 @@ export const ListFilter: React.FC = ({ return ( <> -
-
+
+
= ({
- + = ({ - + {currentSortBy @@ -322,13 +322,13 @@ export const ListFilter: React.FC = ({ )} -
+
onChangePageSize(e.target.value)} value={filter.itemsPerPage.toString()} - className="btn-secondary mx-1 mb-1" + className="btn-secondary" > {pageSizeOptions.map((s) => (