diff --git a/Dockerfile b/Dockerfile index a26548c..594900e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,4 +12,10 @@ RUN dotnet publish -c Release -o out FROM mcr.microsoft.com/dotnet/aspnet:8.0 WORKDIR /App COPY --from=build-env /App/out . + +RUN apt-get update && apt-get install -y curl + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl --fail http://localhost:8080/xrpc/_health || exit 1 + ENTRYPOINT ["dotnet", "PinkSea.dll"] diff --git a/Dockerfile.Gateway b/Dockerfile.Gateway index da11086..deb88c8 100644 --- a/Dockerfile.Gateway +++ b/Dockerfile.Gateway @@ -22,4 +22,10 @@ COPY --from=build-env /App/out . RUN mkdir wwwroot COPY --from=frontend-build-env /App/dist ./wwwroot/ + +RUN apt-get update && apt-get install -y curl + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl --fail http://localhost:8080/xrpc/_health || exit 1 + ENTRYPOINT ["dotnet", "PinkSea.Gateway.dll"] diff --git a/PinkSea.AtProto.Server/Xrpc/ServiceCollectionExtensions.cs b/PinkSea.AtProto.Server/Xrpc/ServiceCollectionExtensions.cs index 11ce555..5ec36b6 100644 --- a/PinkSea.AtProto.Server/Xrpc/ServiceCollectionExtensions.cs +++ b/PinkSea.AtProto.Server/Xrpc/ServiceCollectionExtensions.cs @@ -43,6 +43,13 @@ public static IEndpointRouteBuilder UseXrpcHandler(this IEndpointRouteBuilder ro "/xrpc/{nsid}", HandleXrpc); + routeBuilder.MapGet( + "/xrpc/_health", + () => new + { + version = "PinkSea" + }); + return routeBuilder; } diff --git a/PinkSea.AtProto.Shared/Lexicons/AtProto/GetRepoStatusRequest.cs b/PinkSea.AtProto.Shared/Lexicons/AtProto/GetRepoStatusRequest.cs new file mode 100644 index 0000000..7aace22 --- /dev/null +++ b/PinkSea.AtProto.Shared/Lexicons/AtProto/GetRepoStatusRequest.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace PinkSea.AtProto.Shared.Lexicons.AtProto; + +/// +/// The request for the "com.atproto.sync.getRepoStatus" XRPC call. +/// +public class GetRepoStatusRequest +{ + /// + /// The DID we're querying. + /// + [JsonPropertyName("did")] + public required string Did { get; set; } +} \ No newline at end of file diff --git a/PinkSea.AtProto.Shared/Lexicons/AtProto/GetRepoStatusResponse.cs b/PinkSea.AtProto.Shared/Lexicons/AtProto/GetRepoStatusResponse.cs new file mode 100644 index 0000000..e1b612e --- /dev/null +++ b/PinkSea.AtProto.Shared/Lexicons/AtProto/GetRepoStatusResponse.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace PinkSea.AtProto.Shared.Lexicons.AtProto; + +/// +/// A response for the "com.atproto.sync.getRepoStatus" XRPC call. +/// +public class GetRepoStatusResponse +{ + /// + /// The DID for this repo. + /// + [JsonPropertyName("did")] + public required string Did { get; set; } + + /// + /// Whether this repo is active. + /// + [JsonPropertyName("active")] + public required bool Active { get; set; } + + /// + /// The status of the repo. + /// + [JsonPropertyName("status")] + public string? Status { get; set; } +} \ No newline at end of file diff --git a/PinkSea.AtProto/Xrpc/Extensions/HttpResponseMessageExtensions.cs b/PinkSea.AtProto/Xrpc/Extensions/HttpResponseMessageExtensions.cs index 60a483f..7a80f16 100644 --- a/PinkSea.AtProto/Xrpc/Extensions/HttpResponseMessageExtensions.cs +++ b/PinkSea.AtProto/Xrpc/Extensions/HttpResponseMessageExtensions.cs @@ -23,7 +23,17 @@ public static async Task> ReadXrpcResponse( var body = await message.Content.ReadAsStringAsync(); if (!message.IsSuccessStatusCode) { - var error = JsonSerializer.Deserialize(body) ?? new XrpcError + XrpcError? error = null; + try + { + error = JsonSerializer.Deserialize(body); + } + catch (Exception e) + { + logger?.LogError(e, "Failed to deserialize JSON when parsing XRPC result."); + } + + error ??= new XrpcError { Error = "UnknownError", Message = body @@ -43,7 +53,21 @@ public static async Task> ReadXrpcResponse( if (message is TResponse typedResponse) return XrpcErrorOr.Ok(typedResponse); - var result = JsonSerializer.Deserialize(body); - return XrpcErrorOr.Ok(result!); + try + { + var result = JsonSerializer.Deserialize(body); + return XrpcErrorOr.Ok(result!); + } + catch (Exception e) + { + logger?.LogError(e, "Failed to deserialize JSON when parsing XRPC result."); + var error = new XrpcError + { + Error = "UnknownError", + Message = body + }; + + return XrpcErrorOr.Fail(error.Error, error.Message); + } } } \ No newline at end of file diff --git a/PinkSea.Frontend/src/api/atproto/lexicons.ts b/PinkSea.Frontend/src/api/atproto/lexicons.ts index 76bc36c..d470bf4 100644 --- a/PinkSea.Frontend/src/api/atproto/lexicons.ts +++ b/PinkSea.Frontend/src/api/atproto/lexicons.ts @@ -1,4 +1,7 @@ import type { Oekaki } from '@/models/oekaki' +import type { SearchType } from '@/models/search-type' +import type { Author } from '@/models/author' +import type { TagSearchResult } from '@/models/tag-search-result' declare module '@atcute/client/lexicons' { type EmptyParams = object @@ -101,6 +104,41 @@ declare module '@atcute/client/lexicons' { } } + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace ComShinolabsPinkseaUnspeccedGetProfile { + interface Params { + did: string + } + + interface Output { + did: string, + handle: string, + nick: string, + description: string, + links: [{ + name: string, + url: string + }], + avatar: string + } + } + + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace ComShinolabsPinkseaGetSearchResults { + interface Params { + query: string, + type: SearchType, + since?: Date | null, + limit?: number | null + } + + interface Output { + oekaki?: Oekaki[] | null, + tags?: TagSearchResult[] | null, + profiles?: Author[] | null + } + } + interface Queries { 'com.shinolabs.pinksea.getRecent': { params: GenericTimelineQueryRequest, @@ -133,6 +171,14 @@ declare module '@atcute/client/lexicons' { 'com.shinolabs.pinksea.getParentForReply': { params: ComShinolabsPinkseaGetParentForReply.Params, output: ComShinolabsPinkseaGetParentForReply.Params + }, + 'com.shinolabs.pinksea.unspecced.getProfile': { + params: ComShinolabsPinkseaUnspeccedGetProfile.Params, + output: ComShinolabsPinkseaUnspeccedGetProfile.Output + }, + 'com.shinolabs.pinksea.getSearchResults': { + params: ComShinolabsPinkseaGetSearchResults.Params, + output: ComShinolabsPinkseaGetSearchResults.Output } } diff --git a/PinkSea.Frontend/src/components/CardWithImage.vue b/PinkSea.Frontend/src/components/CardWithImage.vue new file mode 100644 index 0000000..c142894 --- /dev/null +++ b/PinkSea.Frontend/src/components/CardWithImage.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/PinkSea.Frontend/src/components/oekaki/PostViewOekakiTombstoneCard.vue b/PinkSea.Frontend/src/components/ErrorCard.vue similarity index 84% rename from PinkSea.Frontend/src/components/oekaki/PostViewOekakiTombstoneCard.vue rename to PinkSea.Frontend/src/components/ErrorCard.vue index cbd5a9c..d7c388f 100644 --- a/PinkSea.Frontend/src/components/oekaki/PostViewOekakiTombstoneCard.vue +++ b/PinkSea.Frontend/src/components/ErrorCard.vue @@ -1,11 +1,15 @@ diff --git a/PinkSea.Frontend/src/components/search/SearchProfileCard.vue b/PinkSea.Frontend/src/components/search/SearchProfileCard.vue new file mode 100644 index 0000000..69e6b7d --- /dev/null +++ b/PinkSea.Frontend/src/components/search/SearchProfileCard.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/PinkSea.Frontend/src/components/search/SearchTag.vue b/PinkSea.Frontend/src/components/search/SearchTag.vue new file mode 100644 index 0000000..6b4d304 --- /dev/null +++ b/PinkSea.Frontend/src/components/search/SearchTag.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/PinkSea.Frontend/src/intl/translations/en.json b/PinkSea.Frontend/src/intl/translations/en.json index bc6037d..17a3437 100644 --- a/PinkSea.Frontend/src/intl/translations/en.json +++ b/PinkSea.Frontend/src/intl/translations/en.json @@ -27,7 +27,13 @@ "settings": "your settings", "user_profile": "{{handle}}'s profile", "user_post": "{{handle}}'s post", - "tagged": "posts tagged #{{tag}}" + "tagged": "posts tagged #{{tag}}", + "search": "searching for {{value}}" + }, + "search": { + "posts_tab": "Posts", + "tags_tab": "Tags", + "profiles_tab": "Profiles" }, "timeline": { "by_before_handle": "By ", diff --git a/PinkSea.Frontend/src/intl/translations/fr.json b/PinkSea.Frontend/src/intl/translations/fr.json index 6f3a8e3..b5da309 100644 --- a/PinkSea.Frontend/src/intl/translations/fr.json +++ b/PinkSea.Frontend/src/intl/translations/fr.json @@ -1,133 +1,133 @@ { - "sidebar": { - "title": "PinkSea", - "tag": "oekaki BBS", - "shinolabs": "Un projet des laboratoires Shinonome" - }, - "menu": { - "greeting": "Salut @{{name}}!", - "invitation": "Connecte toi pour dessiner!", - "input_placeholder": "@your.cool.tld", - "atp_login": "@ Connexion", - "my_oekaki": "Mon oekaki", - "recent": "Récent", - "settings": "Paramètres", - "logout": "Déconnexion", - "create_something": "Créer quelque chose", - "search": "Rechercher", - "search_placeholder": "Recherche d'un tag", - "password": "Mot de passe (Optionnel)", - "oauth2_info": "Si tu laisse le champ de mot de passe vide, PinkSea va utiliser OAuth2 pour se connecter à ton PDS. C'est généralement plus sécurisé qu'une connexion par mot de passe.", - "search_go": "Aller" - }, - "breadcrumb": { - "recent": "récent", - "painter": "dessinateur", - "settings": "tes paramètres", - "user_profile": "profil de {{handle}}", - "user_post": "publications de {{handle}}", - "tagged": "publications taguées #{{tag}}" - }, - "timeline": { - "by_before_handle": "Par ", - "by_after_handle": "", - "nothing_here": "Rien ici pour l'instant... (╥﹏╥)" - }, - "post": { - "response_from_before_handle": "Réponse de ", - "response_from_after_handle": "", - "response_from_at_date": " à ", - "this_post_no_longer_exists": "Ce post n'existe plus." - }, - "response_box": { - "login_to_respond": "Connecte toi pour répondre!", - "click_to_respond": "Clique pour ouvrir le dessinateur", - "open_painter": "Ouvrir le dessinateur", - "reply": "Répondre!", - "cancel": "Annuler" - }, - "settings": { - "category_general": "général", - "general_language": "Langage", - "category_sensitive": "Médias sensibles", - "sensitive_blur_nsfw": "Flouter les publications NSFW", - "sensitive_hide_nsfw": "Ne pas afficher les publications NSFW" - }, - "painter": { - "do_you_want_to_restore": "La dernière tentative de publication a échoué et ton image a été sauvegardée. Veux-tu la restaurer?", - "could_not_send_post": "Il y a eu un problème lors de l'envoi de la publication. Veuillez réessayer plus tard. Votre publication a été sauvegardé dans votre navigateur.", - "add_a_description": "Ajoute une description!", - "tag": "Tag", - "crosspost_to_bluesky": "Cross-post sur Bluesky", - "upload_description": "Description", - "upload_tags": "Tags", - "upload_confirm": "Confirmer", - "hint_description": "Rajouter une courte description permet de donner du contexte à votre dessin. Optionnel.", - "hint_confirm": "Une fois que vous êtes prêts, cliquez sur le bouton au dessus pour publier votre image!", - "upload_social": "Accessibilité", - "hint_xpost": "Si coché, nous allons créer automatiquement un post pour vous sur Bluesky avec l'image et un lien vers PinkSea.", - "upload": "Publier!", - "hint_tags": "Donne à ton post jusqu'à cinq tags pour qu'il puisse être découvert facilement ! Par exemple: personnages (koiwai_yotsuba), licences (yotsubato! /oc) ou information generale (portrait). Optionnel.", - "hint_nsfw": "Coche s'il te plaît si ton post contient du contenu pour adulte tel que de la nudité ou des éléments suggestifs." - }, - "profile": { - "bluesky_profile": "Profil Bluesky", - "domain": "Site web", - "posts_tab": "Posts", - "replies_tab": "Réponses" - }, - "tegakijs": { - "badDimensions": "Dimensions invalides.", - "confirmDelLayers": "Supprimer les calques sélectionnés?", - "confirmMergeLayers": "Fusionner les calques sélectionnés?", - "tooManyLayers": "Limite de calque atteinte.", - "noActiveLayer": "Pas de calque actif.", - "hiddenActiveLayer": "Le calque actif n'est pas visible.", - "blur": "Flou", - "eraser": "Gomme", - "bucket": "Seau", - "pause": "Pause", - "slower": "Ralentir", - "faster": "Accélérer", - "preserveAlpha": "Conserver L'Alpha", - "pen": "Stylo", - "pencil": "Crayon", - "airbrush": "Aérographe", - "pipette": "Pipette", - "play": "Jouer", - "loadingReplay": "Chargement du replay…", - "recordingEnabled": "Enregistrer un replay", - "rewind": "Rembobiner", - "tone": "Tone", - "promptWidth": "Largeur de la toile en pixels", - "promptHeight": "Hauteur de la toile en pixels", - "errorLoadImage": "L'image n'a pas pu être chargée.", - "confirmCancel": "Es-tu sûre? Ton travail sera perdu.", - "size": "Taille", - "alpha": "Opacité", - "zoom": "Zoom", - "layers": "Calques", - "addLayer": "Ajouter un calque", - "moveLayerUp": "Monter", - "moveLayerDown": "Descendre", - "newCanvas": "Nouveau", - "open": "Ouvrir", - "save": "Sauvegarder", - "export": "Exporter", - "redo": "Rétablir", - "undo": "Annuler", - "close": "Fermer", - "pressure": "Pression", - "toggleVisibility": "Activer/désactiver la visibilité", - "switchPalette": "Change de palette", - "paletteSlotReplace": "Clic droit pour remplacer avec la couleur sélectionnée", - "finish": "Finir", - "delLayers": "Supprimer les calques", - "mergeLayers": "Fusionner les calques", - "layer": "Calque", - "confirmChangeCanvas": "Es-tu sûre? Changer la toile va effacer tout les calques et l'historique et désactiver l'enregistrement du replay.", - "color": "Couleur", - "errorLoadReplay": "Le replay n'a pas pu être chargé: ", - "saveAs": "Sauvegarder Sous" - } + "sidebar": { + "title": "PinkSea", + "tag": "oekaki BBS", + "shinolabs": "Un projet des laboratoires Shinonome" + }, + "menu": { + "greeting": "Salut @{{name}}!", + "invitation": "Connecte toi pour dessiner!", + "input_placeholder": "@your.cool.tld", + "atp_login": "@ Connexion", + "my_oekaki": "Mon oekaki", + "recent": "Récent", + "settings": "Paramètres", + "logout": "Déconnexion", + "create_something": "Créer quelque chose", + "search": "Rechercher", + "search_placeholder": "Recherche d'un tag", + "password": "Mot de passe (Optionnel)", + "oauth2_info": "Si tu laisse le champ de mot de passe vide, PinkSea va utiliser OAuth2 pour se connecter à ton PDS. C'est généralement plus sécurisé qu'une connexion par mot de passe.", + "search_go": "Aller" + }, + "breadcrumb": { + "recent": "récent", + "painter": "dessinateur", + "settings": "tes paramètres", + "user_profile": "profil de {{handle}}", + "user_post": "publications de {{handle}}", + "tagged": "publications taguées #{{tag}}" + }, + "timeline": { + "by_before_handle": "Par ", + "by_after_handle": "", + "nothing_here": "Rien ici pour l'instant... (╥﹏╥)" + }, + "post": { + "response_from_before_handle": "Réponse de ", + "response_from_after_handle": "", + "response_from_at_date": " à ", + "this_post_no_longer_exists": "Ce post n'existe plus." + }, + "response_box": { + "login_to_respond": "Connecte toi pour répondre!", + "click_to_respond": "Clique pour ouvrir le dessinateur", + "open_painter": "Ouvrir le dessinateur", + "reply": "Répondre!", + "cancel": "Annuler" + }, + "settings": { + "category_general": "général", + "general_language": "Langage", + "category_sensitive": "Médias sensibles", + "sensitive_blur_nsfw": "Flouter les publications NSFW", + "sensitive_hide_nsfw": "Ne pas afficher les publications NSFW" + }, + "painter": { + "do_you_want_to_restore": "La dernière tentative de publication a échoué et ton image a été sauvegardée. Veux-tu la restaurer?", + "could_not_send_post": "Il y a eu un problème lors de l'envoi de la publication. Veuillez réessayer plus tard. Votre publication a été sauvegardé dans votre navigateur.", + "add_a_description": "Ajoute une description!", + "tag": "Tag", + "crosspost_to_bluesky": "Cross-post sur Bluesky", + "upload_description": "Description", + "upload_tags": "Tags", + "upload_confirm": "Confirmer", + "hint_description": "Rajouter une courte description permet de donner du contexte à votre dessin. Optionnel.", + "hint_confirm": "Une fois que vous êtes prêts, cliquez sur le bouton au dessus pour publier votre image!", + "upload_social": "Accessibilité", + "hint_xpost": "Si coché, nous allons créer automatiquement un post pour vous sur Bluesky avec l'image et un lien vers PinkSea.", + "upload": "Publier!", + "hint_tags": "Donne à ton post jusqu'à cinq tags pour qu'il puisse être découvert facilement ! Par exemple: personnages (koiwai_yotsuba), licences (yotsubato! /oc) ou information generale (portrait). Optionnel.", + "hint_nsfw": "Coche s'il te plaît si ton post contient du contenu pour adulte tel que de la nudité ou des éléments suggestifs." + }, + "profile": { + "bluesky_profile": "Profil Bluesky", + "domain": "Site web", + "posts_tab": "Posts", + "replies_tab": "Réponses" + }, + "tegakijs": { + "badDimensions": "Dimensions invalides.", + "confirmDelLayers": "Supprimer les calques sélectionnés?", + "confirmMergeLayers": "Fusionner les calques sélectionnés?", + "tooManyLayers": "Limite de calque atteinte.", + "noActiveLayer": "Pas de calque actif.", + "hiddenActiveLayer": "Le calque actif n'est pas visible.", + "blur": "Flou", + "eraser": "Gomme", + "bucket": "Seau", + "pause": "Pause", + "slower": "Ralentir", + "faster": "Accélérer", + "preserveAlpha": "Conserver L'Alpha", + "pen": "Stylo", + "pencil": "Crayon", + "airbrush": "Aérographe", + "pipette": "Pipette", + "play": "Jouer", + "loadingReplay": "Chargement du replay…", + "recordingEnabled": "Enregistrer un replay", + "rewind": "Rembobiner", + "tone": "Tone", + "promptWidth": "Largeur de la toile en pixels", + "promptHeight": "Hauteur de la toile en pixels", + "errorLoadImage": "L'image n'a pas pu être chargée.", + "confirmCancel": "Es-tu sûre? Ton travail sera perdu.", + "size": "Taille", + "alpha": "Opacité", + "zoom": "Zoom", + "layers": "Calques", + "addLayer": "Ajouter un calque", + "moveLayerUp": "Monter", + "moveLayerDown": "Descendre", + "newCanvas": "Nouveau", + "open": "Ouvrir", + "save": "Sauvegarder", + "export": "Exporter", + "redo": "Rétablir", + "undo": "Annuler", + "close": "Fermer", + "pressure": "Pression", + "toggleVisibility": "Activer/désactiver la visibilité", + "switchPalette": "Change de palette", + "paletteSlotReplace": "Clic droit pour remplacer avec la couleur sélectionnée", + "finish": "Finir", + "delLayers": "Supprimer les calques", + "mergeLayers": "Fusionner les calques", + "layer": "Calque", + "confirmChangeCanvas": "Es-tu sûre? Changer la toile va effacer tout les calques et l'historique et désactiver l'enregistrement du replay.", + "color": "Couleur", + "errorLoadReplay": "Le replay n'a pas pu être chargé: ", + "saveAs": "Sauvegarder Sous" + } } diff --git a/PinkSea.Frontend/src/intl/translations/it.json b/PinkSea.Frontend/src/intl/translations/it.json index 52a1886..f0c6fbd 100644 --- a/PinkSea.Frontend/src/intl/translations/it.json +++ b/PinkSea.Frontend/src/intl/translations/it.json @@ -1,56 +1,136 @@ { - "sidebar": { - "title": "PinkSea", - "tag": "oekaki BBS", - "shinolabs": "Una produzione Shinonome Laboratories" - }, - "menu": { - "greeting": "Salve @{{name}}!", - "invitation": "Accedi per iniziare a creare!", - "input_placeholder": "@il-tuo-handle.bsky.social", - "atp_login": "@ Accedi", - "my_oekaki": "Il Mio Oekaki", - "recent": "Recente", - "settings": "Impostazioni", - "logout": "Disconnetti", - "create_something": "Crea Qualcosa" - }, - "breadcrumb": { - "recent": "recente", - "painter": "bozzetto", - "settings": "le tue impostazioni", - "user_profile": "profilo di {{handle}}", - "user_post": "pubblicazione di {{handle}}", - "tagged": "pubblicazioni taggate #{{tag}}" - }, - "timeline": { - "by_before_handle": "Fatto Da ", - "by_after_handle": "" - }, - "post": { - "response_from_before_handle": "Risposta Fatta Da ", - "response_from_after_handle": "", - "response_from_at_date": " alle " - }, - "response_box": { - "login_to_respond": "Accedi per rispondere!", - "click_to_respond": "Clicca per aprire il pannello da disegno", - "open_painter": "Apri Bozzetto", - "reply": "Rispondi!" - }, - "settings": { - "category_general": "generale", - "general_language": "Lingua", - - "category_sensitive": "materiale sensibile", - "sensitive_blur_nsfw": "Sfoca pubblicazioni NSFW", - "sensitive_hide_nsfw": "Non mostrare pubblicazioni NSFW" - }, - "painter": { - "do_you_want_to_restore": "L'ultimo caricamento ha avuto un' errore e la tua immagine è stata salvata. Vuoi ripristinarlo?", - "could_not_send_post": "C'è stato un problema con il caricamento della tua pubblicazione. Riprova dopo, perfavore. La tua pubblicazione è stata salvata nel tuo browser." - }, - "profile": { - "bluesky_profile": "Profilo Bluesky" - } + "sidebar": { + "title": "PinkSea", + "tag": "oekaki BBS", + "shinolabs": "un progetto shinonome laboratories" + }, + "menu": { + "greeting": "Ciao @{{name}}!", + "invitation": "Accedi per iniziare a creare!", + "input_placeholder": "@alice.bsky.social", + "atp_login": "@ Accedi", + "my_oekaki": "Il mio oekaki", + "recent": "Recenti", + "settings": "Impostazioni", + "logout": "Esci", + "create_something": "Crea qualcosa", + "search_placeholder": "Cerca un tag", + "search": "Cerca", + "search_go": "Vai", + "password": "Password (Opzionale)", + "oauth2_info": "Se lasci il campo password vuoto, PinkSea userà OAuth2 per connettersi al tuo PDS. In genere è più sicuro di accedere tramite password." + }, + "breadcrumb": { + "recent": "recenti", + "painter": "bozzetto", + "settings": "le tue impostazioni", + "user_profile": "profilo di {{handle}}", + "user_post": "post di {{handle}}", + "tagged": "post taggati #{{tag}}" + }, + "timeline": { + "by_before_handle": "Di ", + "by_after_handle": "", + "nothing_here": "Non c'è nulla qui per ora... (╥﹏╥)" + }, + "post": { + "response_from_before_handle": "Risposta da ", + "response_from_after_handle": "", + "response_from_at_date": " il ", + "this_post_no_longer_exists": "Questo post non esiste più." + }, + "response_box": { + "login_to_respond": "Accedi per rispondere!", + "click_to_respond": "Clicca per aprire il pannello di disegno", + "open_painter": "Apri bozzetto", + "reply": "Rispondi!", + "cancel": "Annulla" + }, + "settings": { + "category_general": "generale", + "general_language": "Lingua", + "category_sensitive": "materiale sensibile", + "sensitive_blur_nsfw": "Sfoca post NSFW", + "sensitive_hide_nsfw": "Non mostrare post NSFW" + }, + "painter": { + "do_you_want_to_restore": "L'ultimo caricamento è risultato in un errore e la tua immagine è stata salvata. Vuoi ripristinarla?", + "could_not_send_post": "C'è stato un problema con il caricamento del tuo post. Riprova dopo, per favore. Il tuo post è stato salvato nel tuo browser.", + "hint_description": "Allegare una breve descrizione aiuta a dare contesto al tuo disegno. Opzionale.", + "hint_tags": "Dai fino a cinque tag al tuo post per aiutare gli altri a scoprirlo! Per esempio: personaggi (koiwai_yotsuba), copyright (yotsubato! / oc) o informazioni generali (ritratto). Opzionale.", + "add_a_description": "Aggiungi una descrizione!", + "tag": "Tag", + "crosspost_to_bluesky": "Cross-posta su Bluesky", + "upload": "Carica!", + "upload_description": "Descrizione", + "upload_tags": "Tag", + "upload_social": "Sociale", + "upload_confirm": "Conferma", + "hint_nsfw": "Per piacere controlla se il tuo post contiene contenuti da adulti come nudo o temi altamente suggestivi.", + "hint_xpost": "Se spuntato, creeremo automaticamente un post su Bluesky per te con allegati l'immagine e un link a PinkSea.", + "hint_confirm": "Quando sei pronto, clicca sul bottone sopra per pubblicare la tua immagine!" + }, + "profile": { + "bluesky_profile": "Profilo Bluesky", + "domain": "Sito web", + "posts_tab": "Post", + "replies_tab": "Risposte" + }, + "tegakijs": { + "confirmCancel": "Sei sicuro? Il tuo lavoro andrà perso.", + "paletteSlotReplace": "Click destro per sostituire con il colore attuale", + "badDimensions": "Dimensioni non valide.", + "promptWidth": "Larghezza della tela in pixel", + "promptHeight": "Altezza della tela in pixel", + "confirmDelLayers": "Cancella layer selezionati?", + "confirmMergeLayers": "Unisci layer selezionati?", + "tooManyLayers": "Limite di layer raggiunto.", + "noActiveLayer": "Nessun layer attivo.", + "hiddenActiveLayer": "Il layer attivo non è visibile.", + "color": "Colore", + "size": "Dimensione", + "alpha": "Opacità", + "flow": "Flusso", + "zoom": "Zoom", + "layers": "Layer", + "switchPalette": "Cambia tavolozza", + "layer": "Layer", + "addLayer": "Aggiungi layer", + "gapless": "Ininterrotto", + "mergeLayers": "Unisci layer", + "moveLayerUp": "Sposta sopra", + "moveLayerDown": "Sposta sotto", + "newCanvas": "Nuovo", + "open": "Apri", + "save": "Salva", + "saveAs": "Salva Come", + "export": "Esporta", + "undo": "Annulla", + "redo": "Ripeti", + "close": "Chiudi", + "finish": "Termina", + "pressure": "Pressione", + "preserveAlpha": "Mantieni Alfa", + "pen": "Penna", + "pencil": "Matita", + "airbrush": "Aerografo", + "pipette": "Pipetta", + "blur": "Sfoca", + "eraser": "Gomma", + "tone": "Tonalità", + "play": "Riproduci", + "pause": "Pausa", + "rewind": "Riavvolgi", + "slower": "Più lento", + "faster": "Più veloce", + "errorLoadReplay": "Non è stato possibile caricare il replay: ", + "loadingReplay": "Caricamento replay…", + "confirmChangeCanvas": "Sei sicuro? Cambiare la tela rimuoverà tutti i layer e lo storico e disattiverà la registrazione del replay.", + "delLayers": "Elimina layer", + "recordingEnabled": "Registrazione replay", + "toggleVisibility": "Inverti visibilità", + "errorLoadImage": "Non è stato possibile caricare l'immagine.", + "bucket": "Secchiello", + "tip": "Punta" + } } diff --git a/PinkSea.Frontend/src/intl/translations/sv.json b/PinkSea.Frontend/src/intl/translations/sv.json index 41d2fdb..8c77a6f 100644 --- a/PinkSea.Frontend/src/intl/translations/sv.json +++ b/PinkSea.Frontend/src/intl/translations/sv.json @@ -1,62 +1,61 @@ { - "sidebar": { - "title": "PinkSea", - "tag": "oekaki BBS", - "shinolabs": "ett shinonome laboratories project" - }, - "menu": { - "greeting": "Hej @{{name}}!", - "invitation": "Logga in och börja skapa", - "input_placeholder": "@alice.bsky.social", - "atp_login": "@ Login", - "my_oekaki": "Min oekaki", - "recent": "Senaste", - "settings": "Inställningar", - "logout": "Logga ut", - "create_something": "Skapa något" - }, - "breadcrumb": { - "recent": "senaste", - "painter": "tecknare", - "settings": "dina inställningar", - "user_profile": "{{handle}}'s profil", - "user_post": "{{handle}}'s inlägg", - "tagged": "inlägg märkta #{{tag}}" - }, - "timeline": { - "by_before_handle": "Av ", - "by_after_handle": "" - }, - "post": { - "response_from_before_handle": "Svar från ", - "response_from_after_handle": "", - "response_from_at_date": " vid " - }, - "response_box": { - "login_to_respond": "Logga in för att svara!", - "click_to_respond": "Klicka för att öppna ritpanelen", - "open_painter": "Öppna tecknare", - "reply": "Svara!", - "cancel": "Avbryt" - }, - "settings": { - "category_general": "allmänt", - "general_language": "Språk", - - "category_sensitive": "sensitive media", - "sensitive_blur_nsfw": "Gör NSFW inlägg suddiga", - "sensitive_hide_nsfw": "Visa inte NSFW inlägg" - }, - "painter": { - "do_you_want_to_restore": "Den senaste uppladdningen har gett ett felmeddelande och din bild har sparats. Vill du återställa det?", - "could_not_send_post": "Det uppstod problem vid publiceringen av inlägget. Försök igen senare. Ditt inlägg har sparats i din webbläsare.", - "add_a_description": "Lägg till beskrivning!", - "tag": "Tagg", - "crosspost_to_bluesky": "Kors-publicera till BlueSky", - "upload": "Ladda up!" - }, - "profile": { - "bluesky_profile": "Bluesky profil", - "domain": "Hemsida" - } + "sidebar": { + "title": "PinkSea", + "tag": "oekaki BBS", + "shinolabs": "ett shinonome laboratories project" + }, + "menu": { + "greeting": "Hej @{{name}}!", + "invitation": "Logga in och börja skapa!", + "input_placeholder": "@alice.bsky.social", + "atp_login": "@ Login", + "my_oekaki": "Min oekaki", + "recent": "Senaste", + "settings": "Inställningar", + "logout": "Logga ut", + "create_something": "Skapa något" + }, + "breadcrumb": { + "recent": "senaste", + "painter": "tecknare", + "settings": "dina inställningar", + "user_profile": "{{handle}}'s profil", + "user_post": "{{handle}}'s inlägg", + "tagged": "inlägg märkta #{{tag}}" + }, + "timeline": { + "by_before_handle": "Av ", + "by_after_handle": "" + }, + "post": { + "response_from_before_handle": "Svar från ", + "response_from_after_handle": "", + "response_from_at_date": " vid " + }, + "response_box": { + "login_to_respond": "Logga in för att svara!", + "click_to_respond": "Klicka för att öppna ritpanelen", + "open_painter": "Öppna tecknare", + "reply": "Svara!", + "cancel": "Avbryt" + }, + "settings": { + "category_general": "allmänt", + "general_language": "Språk", + "category_sensitive": "sensitive media", + "sensitive_blur_nsfw": "Gör NSFW inlägg suddiga", + "sensitive_hide_nsfw": "Visa inte NSFW inlägg" + }, + "painter": { + "do_you_want_to_restore": "Den senaste uppladdningen har gett ett felmeddelande och din bild har sparats. Vill du återställa det?", + "could_not_send_post": "Det uppstod problem vid publiceringen av inlägget. Försök igen senare. Ditt inlägg har sparats i din webbläsare.", + "add_a_description": "Lägg till beskrivning!", + "tag": "Tagg", + "crosspost_to_bluesky": "Kors-publicera till BlueSky", + "upload": "Ladda up!" + }, + "profile": { + "bluesky_profile": "Bluesky profil", + "domain": "Hemsida" + } } diff --git a/PinkSea.Frontend/src/layouts/PanelLayout.vue b/PinkSea.Frontend/src/layouts/PanelLayout.vue index 092f025..cee9732 100644 --- a/PinkSea.Frontend/src/layouts/PanelLayout.vue +++ b/PinkSea.Frontend/src/layouts/PanelLayout.vue @@ -42,7 +42,8 @@ onBeforeMount(async () => { params: {}, headers: { "Authorization": `Bearer ${persistedStore.token}` - }}); + } + }); identityStore.did = data.did; identityStore.handle = data.handle; @@ -58,22 +59,21 @@ const openPainter = async () => { }; const navigateTo = async (url: string) => { - if(url == "/tag/") return; // prevent using the search box with no query + if (url == "/search/") { + return; // prevent using the search box with no query + } await router.push(url); }; -const queryToTag = (q: string) => { - return q.replace(/\s/g, "_"); -} - const logout = async () => { try { await xrpc.call("com.shinolabs.pinksea.invalidateSession", { data: {}, headers: { "Authorization": `Bearer ${persistedStore.token}` - }}); + } + }); } finally { persistedStore.token = null; } @@ -109,8 +109,10 @@ const getCreateSomethingButtonName = computed(() => {
{{ $t("menu.search") }}
- - + +

@@ -120,13 +122,15 @@ const getCreateSomethingButtonName = computed(() => {
{{ $t("menu.greeting", { name: identityStore.handle }) }}
    -
  • {{ $t("menu.my_oekaki") }}
  • +
  • {{ + $t("menu.my_oekaki") }}
  • {{ $t("menu.recent") }}
  • {{ $t("menu.settings") }}
  • {{ $t("menu.logout") }}
- +
@@ -214,12 +218,14 @@ h1 { padding-bottom: 6px; } -h1, .title h2 { +h1, +.title h2 { padding-right: 4px; padding-left: 4px; } -.title, .aside-box { +.title, +.aside-box { box-shadow: 0px 1px #FFB6C1, 0px -1px #FFB6C1; } diff --git a/PinkSea.Frontend/src/models/search-type.ts b/PinkSea.Frontend/src/models/search-type.ts new file mode 100644 index 0000000..86c4931 --- /dev/null +++ b/PinkSea.Frontend/src/models/search-type.ts @@ -0,0 +1,5 @@ +export enum SearchType { + Posts, + Profiles, + Tags +} diff --git a/PinkSea.Frontend/src/models/tag-search-result.ts b/PinkSea.Frontend/src/models/tag-search-result.ts new file mode 100644 index 0000000..789f1c8 --- /dev/null +++ b/PinkSea.Frontend/src/models/tag-search-result.ts @@ -0,0 +1,7 @@ +import type { Oekaki } from '@/models/oekaki' + +export interface TagSearchResult { + tag: string, + oekaki: Oekaki, + count: number +} diff --git a/PinkSea.Frontend/src/router/index.ts b/PinkSea.Frontend/src/router/index.ts index c35c585..561b486 100644 --- a/PinkSea.Frontend/src/router/index.ts +++ b/PinkSea.Frontend/src/router/index.ts @@ -10,6 +10,7 @@ import TagView from '@/views/TagView.vue' import SettingsView from '@/views/SettingsView.vue' import i18next from 'i18next' import { withTegakiViewBackProtection } from '@/api/tegaki/tegaki-view-helper' +import SearchView from '@/views/SearchView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -80,6 +81,16 @@ const router = createRouter({ return { name: 'breadcrumb.settings' }; } } + }, + { + path: '/search/:value', + name: 'search', + component: SearchView, + meta: { + resolveBreadcrumb: async (route: RouteParamsGeneric) => { + return { name: "breadcrumb.search", params: { value: route.value } }; + } + } } ] }); diff --git a/PinkSea.Frontend/src/views/PostView.vue b/PinkSea.Frontend/src/views/PostView.vue index 896a4b2..84a2f6f 100644 --- a/PinkSea.Frontend/src/views/PostView.vue +++ b/PinkSea.Frontend/src/views/PostView.vue @@ -9,8 +9,9 @@ import PostViewOekakiParentCard from '@/components/oekaki/PostViewOekakiParentCa import PostViewOekakiChildCard from '@/components/oekaki/PostViewOekakiChildCard.vue' import RespondBox from '@/components/RespondBox.vue' import Loader from '@/components/Loader.vue' -import PostViewOekakiTombstoneCard from '@/components/oekaki/PostViewOekakiTombstoneCard.vue' +import PostViewOekakiTombstoneCard from '@/components/ErrorCard.vue' import type { OekakiTombstone } from '@/models/oekaki-tombstone' +import ErrorCard from '@/components/ErrorCard.vue' const route = useRoute(); @@ -47,7 +48,7 @@ onBeforeMount(async () => {
- +
diff --git a/PinkSea.Frontend/src/views/SearchView.vue b/PinkSea.Frontend/src/views/SearchView.vue new file mode 100644 index 0000000..8445351 --- /dev/null +++ b/PinkSea.Frontend/src/views/SearchView.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/PinkSea.Frontend/src/views/UserView.vue b/PinkSea.Frontend/src/views/UserView.vue index f6e76d8..3223eff 100644 --- a/PinkSea.Frontend/src/views/UserView.vue +++ b/PinkSea.Frontend/src/views/UserView.vue @@ -6,6 +6,8 @@ import { computed, ref, watch } from 'vue' import { xrpc } from '@/api/atproto/client' import { useRoute } from 'vue-router' import { UserProfileTab } from '@/models/user-profile-tab' +import { XRPCError } from '@atcute/client' +import ErrorCard from '@/components/ErrorCard.vue' const tabs = [ { @@ -21,11 +23,27 @@ const tabs = [ const handle = ref(""); const route = useRoute(); +const exists = ref(null); +const profileError = ref(""); + const currentTab = ref(UserProfileTab.Posts); watch(() => route.params.did, async () => { - const { data } = await xrpc.get("com.shinolabs.pinksea.getHandleFromDid", { params: { did: route.params.did as string }}); - handle.value = data.handle; + try { + const { data } = await xrpc.get("com.shinolabs.pinksea.unspecced.getProfile", { params: { did: route.params.did as string }}); + handle.value = data.handle; + exists.value = true; + } catch (e) { + if (e instanceof XRPCError) { + const xrpcError = e as XRPCError; + profileError.value = xrpcError.description ?? "An unknown error has occurred."; + } else { + profileError.value = "Failed to load the profile."; + } + + exists.value = false; + } + }, { immediate: true }); const bskyUrl = computed(() => { @@ -40,17 +58,25 @@ const domainUrl = computed(() => {