-
{{ $t("breadcrumb.user_profile", { handle: handle }) }}
-
-
+
+ loading...
+
+
+
+
{{ $t("breadcrumb.user_profile", { handle: handle }) }}
+
+
+
+
+
+
-
-
{{ $t(tab.i18n) }}
+
+ {{ profileError }}
-
-
-
+
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 @@
+
+
+
+
+
+ {{ props.profile.handle }}
+
+
+ @{{ props.profile.handle }}
+
+
+
+
+
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 @@
+
+
+
+
+
+ #{{ props.tag.tag }}
+
+
+ {{ props.tag.count }} posts under this tag.
+
+
+
+
+
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/layouts/PanelLayout.vue b/PinkSea.Frontend/src/layouts/PanelLayout.vue
index 092f025..dceb294 100644
--- a/PinkSea.Frontend/src/layouts/PanelLayout.vue
+++ b/PinkSea.Frontend/src/layouts/PanelLayout.vue
@@ -58,15 +58,13 @@ 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", {
@@ -110,7 +108,7 @@ const getCreateSomethingButtonName = computed(() => {
{{ $t("menu.search") }}
-
+
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/SearchView.vue b/PinkSea.Frontend/src/views/SearchView.vue
new file mode 100644
index 0000000..cda689e
--- /dev/null
+++ b/PinkSea.Frontend/src/views/SearchView.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From 857f181aff7ab27024c95d3f41a6338383d466a7 Mon Sep 17 00:00:00 2001
From: naomiEve <0xlunaric@gmail.com>
Date: Wed, 7 May 2025 10:44:51 +0200
Subject: [PATCH 09/15] refactor: move the tombstone card into a common error
card component.
---
.../PostViewOekakiTombstoneCard.vue => ErrorCard.vue} | 8 ++++++--
PinkSea.Frontend/src/views/PostView.vue | 5 +++--
PinkSea.Frontend/src/views/UserView.vue | 3 ++-
3 files changed, 11 insertions(+), 5 deletions(-)
rename PinkSea.Frontend/src/components/{oekaki/PostViewOekakiTombstoneCard.vue => ErrorCard.vue} (84%)
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 @@
-

+
- {{ $t("post.this_post_no_longer_exists") }}
+ {{ $t(props.i18nKey) }}
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/UserView.vue b/PinkSea.Frontend/src/views/UserView.vue
index 5d04fa4..3223eff 100644
--- a/PinkSea.Frontend/src/views/UserView.vue
+++ b/PinkSea.Frontend/src/views/UserView.vue
@@ -7,6 +7,7 @@ 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 = [
{
@@ -73,7 +74,7 @@ const domainUrl = computed(() => {
- {{ profileError }}
+
From 3843382f612286faec02a4ccb013466a265fecbf Mon Sep 17 00:00:00 2001
From: purifetchi <0xlunaric@gmail.com>
Date: Mon, 19 May 2025 21:57:12 +0200
Subject: [PATCH 10/15] feat: add healthchecks to docker.
---
Dockerfile | 4 ++++
Dockerfile.Gateway | 4 ++++
PinkSea.AtProto.Server/Xrpc/ServiceCollectionExtensions.cs | 7 +++++++
PinkSea.Gateway/Program.cs | 7 +++++++
4 files changed, 22 insertions(+)
diff --git a/Dockerfile b/Dockerfile
index a26548c..77970eb 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,4 +12,8 @@ RUN dotnet publish -c Release -o out
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /App
COPY --from=build-env /App/out .
+
+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..a7e65d7 100644
--- a/Dockerfile.Gateway
+++ b/Dockerfile.Gateway
@@ -22,4 +22,8 @@ COPY --from=build-env /App/out .
RUN mkdir wwwroot
COPY --from=frontend-build-env /App/dist ./wwwroot/
+
+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.Gateway/Program.cs b/PinkSea.Gateway/Program.cs
index 53ec450..1886b12 100644
--- a/PinkSea.Gateway/Program.cs
+++ b/PinkSea.Gateway/Program.cs
@@ -28,6 +28,13 @@
return Results.Text(file, contentType: "text/html");
});
+app.MapGet(
+ "/xrpc/_health",
+ () => new
+ {
+ version = "PinkSea.Gateway"
+ });
+
app.MapFallback(async ([FromServices] MetaGeneratorService metaGenerator) =>
{
var file = await File.ReadAllTextAsync($"./wwwroot/index.html");
From 993cf3322227ba70d9661994f1c931c148cc8fb2 Mon Sep 17 00:00:00 2001
From: purifetchi <0xlunaric@gmail.com>
Date: Mon, 19 May 2025 23:41:48 +0200
Subject: [PATCH 11/15] chore: fix health checks.
---
Dockerfile | 2 ++
Dockerfile.Gateway | 2 ++
2 files changed, 4 insertions(+)
diff --git a/Dockerfile b/Dockerfile
index 77970eb..594900e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,6 +13,8 @@ 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
diff --git a/Dockerfile.Gateway b/Dockerfile.Gateway
index a7e65d7..deb88c8 100644
--- a/Dockerfile.Gateway
+++ b/Dockerfile.Gateway
@@ -23,6 +23,8 @@ 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
From 76e68b68432ce94453524fbfebc0884d15a8cd3c Mon Sep 17 00:00:00 2001
From: naomiEve <0xlunaric@gmail.com>
Date: Thu, 22 May 2025 13:44:23 +0200
Subject: [PATCH 12/15] feat: prepare for profile editing.
---
PinkSea.Frontend/src/views/SearchView.vue | 22 +-
PinkSea/Database/Models/OekakiModel.cs | 2 +-
PinkSea/Database/Models/UserLinkModel.cs | 38 +++
PinkSea/Database/Models/UserModel.cs | 32 ++
...21_Prepare for profile editing.Designer.cs | 285 ++++++++++++++++++
...50522112421_Prepare for profile editing.cs | 98 ++++++
.../PinkSeaDbContextModelSnapshot.cs | 67 +++-
PinkSea/Services/SearchService.cs | 2 +
8 files changed, 541 insertions(+), 5 deletions(-)
create mode 100644 PinkSea/Database/Models/UserLinkModel.cs
create mode 100644 PinkSea/Migrations/20250522112421_Prepare for profile editing.Designer.cs
create mode 100644 PinkSea/Migrations/20250522112421_Prepare for profile editing.cs
diff --git a/PinkSea.Frontend/src/views/SearchView.vue b/PinkSea.Frontend/src/views/SearchView.vue
index cda689e..503dd14 100644
--- a/PinkSea.Frontend/src/views/SearchView.vue
+++ b/PinkSea.Frontend/src/views/SearchView.vue
@@ -6,7 +6,7 @@ import TimeLine from '@/components/TimeLine.vue'
import type { TagSearchResult } from '@/models/tag-search-result'
import type { Author } from '@/models/author'
import { xrpc } from '@/api/atproto/client'
-import { useRoute } from 'vue-router'
+import { useRoute, useRouter } from 'vue-router'
import Intersector from '@/components/Intersector.vue'
import SearchTag from '@/components/search/SearchTag.vue'
import SearchProfileCard from '@/components/search/SearchProfileCard.vue'
@@ -32,6 +32,7 @@ const tags = ref
([]);
const profiles = ref([]);
const route = useRoute();
+const router = useRouter();
const applyNew = (resp: {
tags?: TagSearchResult[] | null
@@ -44,6 +45,17 @@ const applyNew = (resp: {
}
}
+const open = async (id: string) => {
+ let url = ""
+ if (currentTab.value == SearchType.Tags) {
+ url = `/tag/${id}`;
+ } else if (currentTab.value == SearchType.Profiles) {
+ url = `/${id}`;
+ }
+
+ await router.push(url);
+};
+
const loadMore = async () => {
const opts = { params: { query: route.params.value, type: currentTab.value } };
@@ -64,13 +76,13 @@ const loadMore = async () => {
@@ -111,4 +123,8 @@ const loadMore = async () => {
.search-result-list {
padding: 10px;
}
+
+.search-result-list > div {
+ cursor: pointer;
+}
diff --git a/PinkSea/Database/Models/OekakiModel.cs b/PinkSea/Database/Models/OekakiModel.cs
index b957ded..bade112 100644
--- a/PinkSea/Database/Models/OekakiModel.cs
+++ b/PinkSea/Database/Models/OekakiModel.cs
@@ -78,7 +78,7 @@ public class OekakiModel
/// The Bluesky crosspost record TID.
///
public string? BlueskyCrosspostRecordTid { get; set; }
-
+
///
/// The tag-oekaki relations.
///
diff --git a/PinkSea/Database/Models/UserLinkModel.cs b/PinkSea/Database/Models/UserLinkModel.cs
new file mode 100644
index 0000000..25adb9d
--- /dev/null
+++ b/PinkSea/Database/Models/UserLinkModel.cs
@@ -0,0 +1,38 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace PinkSea.Database.Models;
+
+///
+/// A link on a user's page.
+///
+public class UserLinkModel
+{
+ ///
+ /// The key of the link.
+ ///
+ [Key]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; set; }
+
+ ///
+ /// The DID of the user that owns this link.
+ ///
+ [ForeignKey(nameof(User))]
+ public required string UserDid { get; set; }
+
+ ///
+ /// The user that owns this link.
+ ///
+ public required UserModel User { get; set; }
+
+ ///
+ /// The name of the link.
+ ///
+ public required string Name { get; set; }
+
+ ///
+ /// The URL of the link.
+ ///
+ public required string Url { get; set; }
+}
\ No newline at end of file
diff --git a/PinkSea/Database/Models/UserModel.cs b/PinkSea/Database/Models/UserModel.cs
index d7a6b32..fdf2149 100644
--- a/PinkSea/Database/Models/UserModel.cs
+++ b/PinkSea/Database/Models/UserModel.cs
@@ -15,6 +15,27 @@ public class UserModel
[Key]
public required string Did { get; set; }
+ ///
+ /// The nickname of this user.
+ ///
+ public string? Nickname { get; set; }
+
+ ///
+ /// The description of this user.
+ ///
+ public string? Description { get; set; }
+
+ ///
+ /// The ID of the oekaki that server as this user's avatar.
+ ///
+ [ForeignKey(nameof(Avatar))]
+ public string? AvatarId { get; set; }
+
+ ///
+ /// The oekaki which serves as this user's avatar.
+ ///
+ public OekakiModel? Avatar { get; set; }
+
///
/// The cached handle of the user. May be out of date if an account event was skipped.
///
@@ -34,6 +55,17 @@ public class UserModel
/// When was this user created at?
///
public required DateTimeOffset CreatedAt { get; set; }
+
+ ///
+ /// The links this user owns.
+ ///
+ public virtual ICollection
? Links { get; set; }
+
+ ///
+ /// The oekaki this user has made.
+ ///
+ [InverseProperty(nameof(OekakiModel.Author))]
+ public virtual ICollection Oekaki { get; set; }
///
/// Returns an expression that describes this user as not being deleted.
diff --git a/PinkSea/Migrations/20250522112421_Prepare for profile editing.Designer.cs b/PinkSea/Migrations/20250522112421_Prepare for profile editing.Designer.cs
new file mode 100644
index 0000000..9fd0002
--- /dev/null
+++ b/PinkSea/Migrations/20250522112421_Prepare for profile editing.Designer.cs
@@ -0,0 +1,285 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using PinkSea.Database;
+
+#nullable disable
+
+namespace PinkSea.Migrations
+{
+ [DbContext(typeof(PinkSeaDbContext))]
+ [Migration("20250522112421_Prepare for profile editing")]
+ partial class Prepareforprofileediting
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.10")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("PinkSea.Database.Models.ConfigurationModel", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClientPrivateKey")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ClientPublicKey")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("KeyId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("SynchronizedAccountStates")
+ .HasColumnType("boolean");
+
+ b.HasKey("Id");
+
+ b.ToTable("Configuration");
+ });
+
+ modelBuilder.Entity("PinkSea.Database.Models.OAuthStateModel", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("Json")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("OAuthStates");
+ });
+
+ modelBuilder.Entity("PinkSea.Database.Models.OekakiModel", b =>
+ {
+ b.Property("Key")
+ .HasColumnType("text");
+
+ b.Property("AltText")
+ .HasColumnType("text");
+
+ b.Property("AuthorDid")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("BlobCid")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("BlueskyCrosspostRecordTid")
+ .HasColumnType("text");
+
+ b.Property("IndexedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IsNsfw")
+ .HasColumnType("boolean");
+
+ b.Property("OekakiTid")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ParentId")
+ .HasColumnType("text");
+
+ b.Property("RecordCid")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Tombstone")
+ .HasColumnType("boolean");
+
+ b.HasKey("Key");
+
+ b.HasIndex("ParentId");
+
+ b.HasIndex("Tombstone");
+
+ b.HasIndex("AuthorDid", "OekakiTid");
+
+ b.ToTable("Oekaki");
+ });
+
+ modelBuilder.Entity("PinkSea.Database.Models.TagModel", b =>
+ {
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.HasKey("Name");
+
+ b.ToTable("Tags");
+ });
+
+ modelBuilder.Entity("PinkSea.Database.Models.TagOekakiRelationModel", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("OekakiId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("TagId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OekakiId");
+
+ b.HasIndex("TagId");
+
+ b.ToTable("TagOekakiRelations");
+ });
+
+ modelBuilder.Entity("PinkSea.Database.Models.UserLinkModel", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Url")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UserDid")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserDid");
+
+ b.ToTable("UserLinkModel");
+ });
+
+ modelBuilder.Entity("PinkSea.Database.Models.UserModel", b =>
+ {
+ b.Property("Did")
+ .HasColumnType("text");
+
+ b.Property("AppViewBlocked")
+ .HasColumnType("boolean");
+
+ b.Property("AvatarId")
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasColumnType("text");
+
+ b.Property("Handle")
+ .HasColumnType("text");
+
+ b.Property("Nickname")
+ .HasColumnType("text");
+
+ b.Property("RepoStatus")
+ .HasColumnType("integer");
+
+ b.HasKey("Did");
+
+ b.HasIndex("AvatarId");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("PinkSea.Database.Models.OekakiModel", b =>
+ {
+ b.HasOne("PinkSea.Database.Models.UserModel", "Author")
+ .WithMany("Oekaki")
+ .HasForeignKey("AuthorDid")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("PinkSea.Database.Models.OekakiModel", "Parent")
+ .WithMany()
+ .HasForeignKey("ParentId");
+
+ b.Navigation("Author");
+
+ b.Navigation("Parent");
+ });
+
+ modelBuilder.Entity("PinkSea.Database.Models.TagOekakiRelationModel", b =>
+ {
+ b.HasOne("PinkSea.Database.Models.OekakiModel", "Oekaki")
+ .WithMany("TagOekakiRelations")
+ .HasForeignKey("OekakiId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("PinkSea.Database.Models.TagModel", "Tag")
+ .WithMany()
+ .HasForeignKey("TagId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Oekaki");
+
+ b.Navigation("Tag");
+ });
+
+ modelBuilder.Entity("PinkSea.Database.Models.UserLinkModel", b =>
+ {
+ b.HasOne("PinkSea.Database.Models.UserModel", "User")
+ .WithMany("Links")
+ .HasForeignKey("UserDid")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("PinkSea.Database.Models.UserModel", b =>
+ {
+ b.HasOne("PinkSea.Database.Models.OekakiModel", "Avatar")
+ .WithMany()
+ .HasForeignKey("AvatarId");
+
+ b.Navigation("Avatar");
+ });
+
+ modelBuilder.Entity("PinkSea.Database.Models.OekakiModel", b =>
+ {
+ b.Navigation("TagOekakiRelations");
+ });
+
+ modelBuilder.Entity("PinkSea.Database.Models.UserModel", b =>
+ {
+ b.Navigation("Links");
+
+ b.Navigation("Oekaki");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/PinkSea/Migrations/20250522112421_Prepare for profile editing.cs b/PinkSea/Migrations/20250522112421_Prepare for profile editing.cs
new file mode 100644
index 0000000..1d51590
--- /dev/null
+++ b/PinkSea/Migrations/20250522112421_Prepare for profile editing.cs
@@ -0,0 +1,98 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace PinkSea.Migrations
+{
+ ///
+ public partial class Prepareforprofileediting : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "AvatarId",
+ table: "Users",
+ type: "text",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "Description",
+ table: "Users",
+ type: "text",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "Nickname",
+ table: "Users",
+ type: "text",
+ nullable: true);
+
+ migrationBuilder.CreateTable(
+ name: "UserLinkModel",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ UserDid = table.Column(type: "text", nullable: false),
+ Name = table.Column(type: "text", nullable: false),
+ Url = table.Column(type: "text", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_UserLinkModel", x => x.Id);
+ table.ForeignKey(
+ name: "FK_UserLinkModel_Users_UserDid",
+ column: x => x.UserDid,
+ principalTable: "Users",
+ principalColumn: "Did",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Users_AvatarId",
+ table: "Users",
+ column: "AvatarId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_UserLinkModel_UserDid",
+ table: "UserLinkModel",
+ column: "UserDid");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Users_Oekaki_AvatarId",
+ table: "Users",
+ column: "AvatarId",
+ principalTable: "Oekaki",
+ principalColumn: "Key");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_Users_Oekaki_AvatarId",
+ table: "Users");
+
+ migrationBuilder.DropTable(
+ name: "UserLinkModel");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Users_AvatarId",
+ table: "Users");
+
+ migrationBuilder.DropColumn(
+ name: "AvatarId",
+ table: "Users");
+
+ migrationBuilder.DropColumn(
+ name: "Description",
+ table: "Users");
+
+ migrationBuilder.DropColumn(
+ name: "Nickname",
+ table: "Users");
+ }
+ }
+}
diff --git a/PinkSea/Migrations/PinkSeaDbContextModelSnapshot.cs b/PinkSea/Migrations/PinkSeaDbContextModelSnapshot.cs
index 753d80d..c6efeb5 100644
--- a/PinkSea/Migrations/PinkSeaDbContextModelSnapshot.cs
+++ b/PinkSea/Migrations/PinkSeaDbContextModelSnapshot.cs
@@ -149,6 +149,33 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.ToTable("TagOekakiRelations");
});
+ modelBuilder.Entity("PinkSea.Database.Models.UserLinkModel", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Url")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UserDid")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserDid");
+
+ b.ToTable("UserLinkModel");
+ });
+
modelBuilder.Entity("PinkSea.Database.Models.UserModel", b =>
{
b.Property("Did")
@@ -157,24 +184,35 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Property("AppViewBlocked")
.HasColumnType("boolean");
+ b.Property("AvatarId")
+ .HasColumnType("text");
+
b.Property("CreatedAt")
.HasColumnType("timestamp with time zone");
+ b.Property("Description")
+ .HasColumnType("text");
+
b.Property("Handle")
.HasColumnType("text");
+ b.Property("Nickname")
+ .HasColumnType("text");
+
b.Property("RepoStatus")
.HasColumnType("integer");
b.HasKey("Did");
+ b.HasIndex("AvatarId");
+
b.ToTable("Users");
});
modelBuilder.Entity("PinkSea.Database.Models.OekakiModel", b =>
{
b.HasOne("PinkSea.Database.Models.UserModel", "Author")
- .WithMany()
+ .WithMany("Oekaki")
.HasForeignKey("AuthorDid")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -207,10 +245,37 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Navigation("Tag");
});
+ modelBuilder.Entity("PinkSea.Database.Models.UserLinkModel", b =>
+ {
+ b.HasOne("PinkSea.Database.Models.UserModel", "User")
+ .WithMany("Links")
+ .HasForeignKey("UserDid")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("PinkSea.Database.Models.UserModel", b =>
+ {
+ b.HasOne("PinkSea.Database.Models.OekakiModel", "Avatar")
+ .WithMany()
+ .HasForeignKey("AvatarId");
+
+ b.Navigation("Avatar");
+ });
+
modelBuilder.Entity("PinkSea.Database.Models.OekakiModel", b =>
{
b.Navigation("TagOekakiRelations");
});
+
+ modelBuilder.Entity("PinkSea.Database.Models.UserModel", b =>
+ {
+ b.Navigation("Links");
+
+ b.Navigation("Oekaki");
+ });
#pragma warning restore 612, 618
}
}
diff --git a/PinkSea/Services/SearchService.cs b/PinkSea/Services/SearchService.cs
index 3615345..2ffe71d 100644
--- a/PinkSea/Services/SearchService.cs
+++ b/PinkSea/Services/SearchService.cs
@@ -22,6 +22,7 @@ public async Task> SearchPosts(string query, int limit, DateTi
.Where(o => !o.Tombstone)
.Where(o => o.AltText!.ToLower().Contains(query.ToLower()) ||
o.TagOekakiRelations!.Any(to => to.TagId.ToLower().Contains(query.ToLower()))) // TODO: Author
+ .Distinct()
.OrderByDescending(o => o.IndexedAt)
.Where(o => o.IndexedAt < since)
.Take(limit)
@@ -36,6 +37,7 @@ public async Task> SearchTags(string query, int limit, Dat
.Where(t => t.Name.ToLower().Contains(query.ToLower()))
.Join(dbContext.TagOekakiRelations, t => t.Name, to => to.TagId, (t, to) => new { t, to })
.Join(dbContext.Oekaki, c => c.to.OekakiId, o => o.Key, (c, o) => new { c.t, c.to, o })
+ .Distinct()
.GroupBy(c => c.t.Name)
.Take(limit)
.Select(c => new
From 4b3a26392a4cb60a013d5422d4a8958b928da05d Mon Sep 17 00:00:00 2001
From: purifetchi <0xlunaric@gmail.com>
Date: Fri, 27 Jun 2025 17:23:58 +0200
Subject: [PATCH 13/15] fix: unbreak enter on search box.
---
PinkSea.Frontend/src/layouts/PanelLayout.vue | 24 +++++++++++++-------
1 file changed, 16 insertions(+), 8 deletions(-)
diff --git a/PinkSea.Frontend/src/layouts/PanelLayout.vue b/PinkSea.Frontend/src/layouts/PanelLayout.vue
index dceb294..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;
@@ -71,7 +72,8 @@ const logout = async () => {
data: {},
headers: {
"Authorization": `Bearer ${persistedStore.token}`
- }});
+ }
+ });
} finally {
persistedStore.token = null;
}
@@ -107,8 +109,10 @@ const getCreateSomethingButtonName = computed(() => {
{{ $t("menu.search") }}
-
-
+
+
@@ -118,13 +122,15 @@ const getCreateSomethingButtonName = computed(() => {
{{ $t("menu.greeting", { name: identityStore.handle }) }}
-
+
@@ -212,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;
}
From 29bf8a8c5a42f8b91d5da69462e6bb63c3f0aeb7 Mon Sep 17 00:00:00 2001
From: purifetchi <0xlunaric@gmail.com>
Date: Fri, 27 Jun 2025 17:28:11 +0200
Subject: [PATCH 14/15] chore: update translations from weblate.
---
.../src/intl/translations/fr.json | 262 +++++++++---------
.../src/intl/translations/it.json | 188 +++++++++----
.../src/intl/translations/sv.json | 119 ++++----
3 files changed, 324 insertions(+), 245 deletions(-)
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"
+ }
}
From aa38cb2bc1aa1a14878fcfffc771ca3420120342 Mon Sep 17 00:00:00 2001
From: purifetchi <0xlunaric@gmail.com>
Date: Fri, 27 Jun 2025 17:52:12 +0200
Subject: [PATCH 15/15] fix: improve counting.
---
PinkSea.Frontend/src/views/SearchView.vue | 35 +++++++++++++++++++----
PinkSea/Services/SearchService.cs | 12 ++++----
2 files changed, 36 insertions(+), 11 deletions(-)
diff --git a/PinkSea.Frontend/src/views/SearchView.vue b/PinkSea.Frontend/src/views/SearchView.vue
index 503dd14..8445351 100644
--- a/PinkSea.Frontend/src/views/SearchView.vue
+++ b/PinkSea.Frontend/src/views/SearchView.vue
@@ -64,26 +64,44 @@ const loadMore = async () => {
applyNew(data);
};
+
+const setTab = (tab: SearchType) => {
+ currentTab.value = tab;
+
+ if (currentTab.value == SearchType.Tags) {
+ tags.value = [];
+ } else if (currentTab.value == SearchType.Profiles) {
+ profiles.value = [];
+ }
+};
-
+
-
+
+
+ {{ $t("timeline.nothing_here") }}
+
-
+
+
+ {{ $t("timeline.nothing_here") }}
+
@@ -112,7 +130,8 @@ const loadMore = async () => {
}
#tabs a.selected {
- background: #ffb6c1; color: #263b48;
+ background: #ffb6c1;
+ color: #263b48;
font-weight: bold;
}
@@ -124,7 +143,11 @@ const loadMore = async () => {
padding: 10px;
}
-.search-result-list > div {
+.search-result-list>div {
cursor: pointer;
}
+
+.search-centered {
+ text-align: center;
+}
diff --git a/PinkSea/Services/SearchService.cs b/PinkSea/Services/SearchService.cs
index 2ffe71d..cd0f9b3 100644
--- a/PinkSea/Services/SearchService.cs
+++ b/PinkSea/Services/SearchService.cs
@@ -16,12 +16,14 @@ public class SearchService(
{
public async Task
> SearchPosts(string query, int limit, DateTimeOffset since)
{
+ var lowerQuery = query.ToLower();
var list = await dbContext.Oekaki
.Include(o => o.TagOekakiRelations)
.Include(o => o.Author)
- .Where(o => !o.Tombstone)
- .Where(o => o.AltText!.ToLower().Contains(query.ToLower()) ||
- o.TagOekakiRelations!.Any(to => to.TagId.ToLower().Contains(query.ToLower()))) // TODO: Author
+ .Where(o => !o.Tombstone && o.ParentId == null)
+ .Where(o => o.AltText!.ToLower().Contains(lowerQuery) ||
+ o.TagOekakiRelations!.Any(to => to.TagId.ToLower().Contains(lowerQuery)) ||
+ o.Author.Handle!.ToLower().Contains(lowerQuery))
.Distinct()
.OrderByDescending(o => o.IndexedAt)
.Where(o => o.IndexedAt < since)
@@ -37,14 +39,14 @@ public async Task> SearchTags(string query, int limit, Dat
.Where(t => t.Name.ToLower().Contains(query.ToLower()))
.Join(dbContext.TagOekakiRelations, t => t.Name, to => to.TagId, (t, to) => new { t, to })
.Join(dbContext.Oekaki, c => c.to.OekakiId, o => o.Key, (c, o) => new { c.t, c.to, o })
- .Distinct()
+ .Where(c => !c.o.Tombstone && c.o.ParentId == null)
.GroupBy(c => c.t.Name)
.Take(limit)
.Select(c => new
{
Tag = c.Key,
Oekaki = c.First().o,
- Count = c.Count()
+ Count = c.Select(p => p.o.Key).Distinct().Count()
})
.ToListAsync();