Compare commits

..

No commits in common. "ff00dfebb632d5f677900ce7fc6a17da35c86784" and "789852211b37015cf7e0bcb536402f0fbfb0ae73" have entirely different histories.

32 changed files with 1964 additions and 900 deletions

1288
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,7 @@ edition = "2021"
async-trait = "0.1" async-trait = "0.1"
axum = "0.7" axum = "0.7"
axum-extra = "0.9" axum-extra = "0.9"
cached = "0.47" cached = "0.46"
cfg-if = "1" cfg-if = "1"
chrono = "0.4" chrono = "0.4"
compact_str = "0.7" compact_str = "0.7"

View File

@ -26,3 +26,4 @@ strum = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
once_cell = "1.18.0"

View File

@ -65,6 +65,8 @@ pub enum Relation {
on_delete = "SetNull" on_delete = "SetNull"
)] )]
DriveFolder, DriveFolder,
#[sea_orm(has_many = "super::page::Entity")]
Page,
#[sea_orm( #[sea_orm(
belongs_to = "super::user::Entity", belongs_to = "super::user::Entity",
from = "Column::UserId", from = "Column::UserId",
@ -81,6 +83,12 @@ impl Related<super::drive_folder::Entity> for Entity {
} }
} }
impl Related<super::page::Entity> for Entity {
fn to() -> RelationDef {
Relation::Page.def()
}
}
impl Related<super::user::Entity> for Entity { impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::User.def() Relation::User.def()

View File

@ -0,0 +1,51 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "gallery_like")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
#[sea_orm(column_name = "postId")]
pub post_id: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::gallery_post::Entity",
from = "Column::PostId",
to = "super::gallery_post::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
GalleryPost,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
User,
}
impl Related<super::gallery_post::Entity> for Entity {
fn to() -> RelationDef {
Relation::GalleryPost.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,54 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "gallery_post")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "updatedAt")]
pub updated_at: DateTimeWithTimeZone,
pub title: String,
pub description: Option<String>,
#[sea_orm(column_name = "userId")]
pub user_id: String,
#[sea_orm(column_name = "fileIds")]
pub file_ids: Vec<String>,
#[sea_orm(column_name = "isSensitive")]
pub is_sensitive: bool,
#[sea_orm(column_name = "likedCount")]
pub liked_count: i32,
pub tags: Vec<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::gallery_like::Entity")]
GalleryLike,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
User,
}
impl Related<super::gallery_like::Entity> for Entity {
fn to() -> RelationDef {
Relation::GalleryLike.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -20,6 +20,8 @@ pub mod drive_folder;
pub mod emoji; pub mod emoji;
pub mod follow_request; pub mod follow_request;
pub mod following; pub mod following;
pub mod gallery_like;
pub mod gallery_post;
pub mod hashtag; pub mod hashtag;
pub mod instance; pub mod instance;
pub mod meta; pub mod meta;
@ -34,6 +36,8 @@ pub mod note_thread_muting;
pub mod note_unread; pub mod note_unread;
pub mod note_watching; pub mod note_watching;
pub mod notification; pub mod notification;
pub mod page;
pub mod page_like;
pub mod password_reset_request; pub mod password_reset_request;
pub mod poll; pub mod poll;
pub mod poll_vote; pub mod poll_vote;

View File

@ -0,0 +1,90 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
use super::sea_orm_active_enums::PageVisibilityEnum;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "page")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "updatedAt")]
pub updated_at: DateTimeWithTimeZone,
pub title: String,
pub name: String,
pub summary: Option<String>,
#[sea_orm(column_name = "alignCenter")]
pub align_center: bool,
pub font: String,
#[sea_orm(column_name = "userId")]
pub user_id: String,
#[sea_orm(column_name = "eyeCatchingImageId")]
pub eye_catching_image_id: Option<String>,
#[sea_orm(column_type = "JsonBinary")]
pub content: Json,
#[sea_orm(column_type = "JsonBinary")]
pub variables: Json,
pub visibility: PageVisibilityEnum,
#[sea_orm(column_name = "visibleUserIds")]
pub visible_user_ids: Vec<String>,
#[sea_orm(column_name = "likedCount")]
pub liked_count: i32,
#[sea_orm(column_name = "hideTitleWhenPinned")]
pub hide_title_when_pinned: bool,
pub script: String,
#[sea_orm(column_name = "isPublic")]
pub is_public: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::drive_file::Entity",
from = "Column::EyeCatchingImageId",
to = "super::drive_file::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
DriveFile,
#[sea_orm(has_many = "super::page_like::Entity")]
PageLike,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
User,
#[sea_orm(has_one = "super::user_profile::Entity")]
UserProfile,
}
impl Related<super::drive_file::Entity> for Entity {
fn to() -> RelationDef {
Relation::DriveFile.def()
}
}
impl Related<super::page_like::Entity> for Entity {
fn to() -> RelationDef {
Relation::PageLike.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl Related<super::user_profile::Entity> for Entity {
fn to() -> RelationDef {
Relation::UserProfile.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,51 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "page_like")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_name = "createdAt")]
pub created_at: DateTimeWithTimeZone,
#[sea_orm(column_name = "userId")]
pub user_id: String,
#[sea_orm(column_name = "pageId")]
pub page_id: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::page::Entity",
from = "Column::PageId",
to = "super::page::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
Page,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
User,
}
impl Related<super::page::Entity> for Entity {
fn to() -> RelationDef {
Relation::Page.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -18,6 +18,8 @@ pub use super::drive_folder::Entity as DriveFolder;
pub use super::emoji::Entity as Emoji; pub use super::emoji::Entity as Emoji;
pub use super::follow_request::Entity as FollowRequest; pub use super::follow_request::Entity as FollowRequest;
pub use super::following::Entity as Following; pub use super::following::Entity as Following;
pub use super::gallery_like::Entity as GalleryLike;
pub use super::gallery_post::Entity as GalleryPost;
pub use super::hashtag::Entity as Hashtag; pub use super::hashtag::Entity as Hashtag;
pub use super::instance::Entity as Instance; pub use super::instance::Entity as Instance;
pub use super::meta::Entity as Meta; pub use super::meta::Entity as Meta;
@ -32,6 +34,8 @@ pub use super::note_thread_muting::Entity as NoteThreadMuting;
pub use super::note_unread::Entity as NoteUnread; pub use super::note_unread::Entity as NoteUnread;
pub use super::note_watching::Entity as NoteWatching; pub use super::note_watching::Entity as NoteWatching;
pub use super::notification::Entity as Notification; pub use super::notification::Entity as Notification;
pub use super::page::Entity as Page;
pub use super::page_like::Entity as PageLike;
pub use super::password_reset_request::Entity as PasswordResetRequest; pub use super::password_reset_request::Entity as PasswordResetRequest;
pub use super::poll::Entity as Poll; pub use super::poll::Entity as Poll;
pub use super::poll_vote::Entity as PollVote; pub use super::poll_vote::Entity as PollVote;

View File

@ -118,6 +118,20 @@ pub enum NotificationTypeEnum {
Reply, Reply,
} }
#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy, Serialize, Deserialize)]
#[sea_orm(
rs_type = "String",
db_type = "Enum",
enum_name = "page_visibility_enum"
)]
pub enum PageVisibilityEnum {
#[sea_orm(string_value = "followers")]
Followers,
#[sea_orm(string_value = "public")]
Public,
#[sea_orm(string_value = "specified")]
Specified,
}
#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy, Serialize, Deserialize)]
#[sea_orm( #[sea_orm(
rs_type = "String", rs_type = "String",
db_type = "Enum", db_type = "Enum",

View File

@ -106,6 +106,10 @@ pub enum Relation {
DriveFile1, DriveFile1,
#[sea_orm(has_many = "super::drive_folder::Entity")] #[sea_orm(has_many = "super::drive_folder::Entity")]
DriveFolder, DriveFolder,
#[sea_orm(has_many = "super::gallery_like::Entity")]
GalleryLike,
#[sea_orm(has_many = "super::gallery_post::Entity")]
GalleryPost,
#[sea_orm(has_many = "super::meta::Entity")] #[sea_orm(has_many = "super::meta::Entity")]
Meta, Meta,
#[sea_orm(has_many = "super::moderation_log::Entity")] #[sea_orm(has_many = "super::moderation_log::Entity")]
@ -124,6 +128,10 @@ pub enum Relation {
NoteUnread, NoteUnread,
#[sea_orm(has_many = "super::note_watching::Entity")] #[sea_orm(has_many = "super::note_watching::Entity")]
NoteWatching, NoteWatching,
#[sea_orm(has_many = "super::page::Entity")]
Page,
#[sea_orm(has_many = "super::page_like::Entity")]
PageLike,
#[sea_orm(has_many = "super::password_reset_request::Entity")] #[sea_orm(has_many = "super::password_reset_request::Entity")]
PasswordResetRequest, PasswordResetRequest,
#[sea_orm(has_many = "super::poll_vote::Entity")] #[sea_orm(has_many = "super::poll_vote::Entity")]
@ -202,6 +210,18 @@ impl Related<super::drive_folder::Entity> for Entity {
} }
} }
impl Related<super::gallery_like::Entity> for Entity {
fn to() -> RelationDef {
Relation::GalleryLike.def()
}
}
impl Related<super::gallery_post::Entity> for Entity {
fn to() -> RelationDef {
Relation::GalleryPost.def()
}
}
impl Related<super::meta::Entity> for Entity { impl Related<super::meta::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::Meta.def() Relation::Meta.def()
@ -256,6 +276,18 @@ impl Related<super::note_watching::Entity> for Entity {
} }
} }
impl Related<super::page::Entity> for Entity {
fn to() -> RelationDef {
Relation::Page.def()
}
}
impl Related<super::page_like::Entity> for Entity {
fn to() -> RelationDef {
Relation::PageLike.def()
}
}
impl Related<super::password_reset_request::Entity> for Entity { impl Related<super::password_reset_request::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::PasswordResetRequest.def() Relation::PasswordResetRequest.def()

View File

@ -42,6 +42,8 @@ pub struct Model {
pub security_keys_available: bool, pub security_keys_available: bool,
#[sea_orm(column_name = "usePasswordLessLogin")] #[sea_orm(column_name = "usePasswordLessLogin")]
pub use_password_less_login: bool, pub use_password_less_login: bool,
#[sea_orm(column_name = "pinnedPageId", unique)]
pub pinned_page_id: Option<String>,
#[sea_orm(column_type = "JsonBinary")] #[sea_orm(column_type = "JsonBinary")]
pub room: Json, pub room: Json,
#[sea_orm(column_type = "JsonBinary")] #[sea_orm(column_type = "JsonBinary")]
@ -75,6 +77,14 @@ pub struct Model {
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation { pub enum Relation {
#[sea_orm(
belongs_to = "super::page::Entity",
from = "Column::PinnedPageId",
to = "super::page::Column::Id",
on_update = "NoAction",
on_delete = "SetNull"
)]
Page,
#[sea_orm( #[sea_orm(
belongs_to = "super::user::Entity", belongs_to = "super::user::Entity",
from = "Column::UserId", from = "Column::UserId",
@ -85,6 +95,12 @@ pub enum Relation {
User, User,
} }
impl Related<super::page::Entity> for Entity {
fn to() -> RelationDef {
Relation::Page.def()
}
}
impl Related<super::user::Entity> for Entity { impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef { fn to() -> RelationDef {
Relation::User.def() Relation::User.def()

View File

@ -7,8 +7,6 @@ mod m20230806_142918_drop_featured_note_option;
mod m20240107_005747_remove_user_groups; mod m20240107_005747_remove_user_groups;
mod m20240107_220523_generated_is_quote; mod m20240107_220523_generated_is_quote;
mod m20240107_224446_generated_is_renote; mod m20240107_224446_generated_is_renote;
mod m20240112_215106_remove_pages;
mod m20240112_234759_remove_gallery;
pub struct Migrator; pub struct Migrator;
@ -23,8 +21,6 @@ impl MigratorTrait for Migrator {
Box::new(m20240107_005747_remove_user_groups::Migration), Box::new(m20240107_005747_remove_user_groups::Migration),
Box::new(m20240107_220523_generated_is_quote::Migration), Box::new(m20240107_220523_generated_is_quote::Migration),
Box::new(m20240107_224446_generated_is_renote::Migration), Box::new(m20240107_224446_generated_is_renote::Migration),
Box::new(m20240112_215106_remove_pages::Migration),
Box::new(m20240112_234759_remove_gallery::Migration),
] ]
} }
} }

View File

@ -1,118 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(
r#"
ALTER TABLE "user_profile" DROP COLUMN "pinnedPageId";
DROP TABLE "page_like";
DROP TABLE "page";
"#,
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(
r#"
create table page
(
id varchar(32) not null
constraint "PK_742f4117e065c5b6ad21b37ba1f"
primary key,
"createdAt" timestamp with time zone not null,
"updatedAt" timestamp with time zone not null,
title varchar(256) not null,
name varchar(256) not null,
summary varchar(256),
"alignCenter" boolean not null,
font varchar(32) not null,
"userId" varchar(32) not null
constraint "FK_ae1d917992dd0c9d9bbdad06c4a"
references "user"
on delete cascade,
"eyeCatchingImageId" varchar(32)
constraint "FK_a9ca79ad939bf06066b81c9d3aa"
references drive_file
on delete cascade,
content jsonb default '[]'::jsonb not null,
variables jsonb default '[]'::jsonb not null,
visibility page_visibility_enum not null,
"visibleUserIds" varchar(32)[] default '{}'::character varying[] not null,
"likedCount" integer default 0 not null,
"hideTitleWhenPinned" boolean default false not null,
script varchar(16384) default ''::character varying not null,
"isPublic" boolean default true not null
);
comment on column page."createdAt" is 'The created date of the Page.';
comment on column page."updatedAt" is 'The updated date of the Page.';
comment on column page."userId" is 'The ID of author.';
create index "IDX_fbb4297c927a9b85e9cefa2eb1"
on page ("createdAt");
create index "IDX_af639b066dfbca78b01a920f8a"
on page ("updatedAt");
create index "IDX_b82c19c08afb292de4600d99e4"
on page (name);
create index "IDX_ae1d917992dd0c9d9bbdad06c4"
on page ("userId");
create index "IDX_90148bbc2bf0854428786bfc15"
on page ("visibleUserIds");
create unique index "IDX_2133ef8317e4bdb839c0dcbf13"
on page ("userId", name);
create table page_like
(
id varchar(32) not null
constraint "PK_813f034843af992d3ae0f43c64c"
primary key,
"createdAt" timestamp with time zone not null,
"userId" varchar(32) not null
constraint "FK_0e61efab7f88dbb79c9166dbb48"
references "user"
on delete cascade,
"pageId" varchar(32) not null
constraint "FK_cf8782626dced3176038176a847"
references page
on delete cascade
);
create index "IDX_0e61efab7f88dbb79c9166dbb4"
on page_like ("userId");
create unique index "IDX_4ce6fb9c70529b4c8ac46c9bfa"
on page_like ("userId", "pageId");
alter table user_profile
add "pinnedPageId" varchar(32)
constraint "UQ_6dc44f1ceb65b1e72bacef2ca27"
unique
constraint "FK_6dc44f1ceb65b1e72bacef2ca27"
references page
on delete set null;
"#,
)
.await?;
Ok(())
}
}

View File

@ -1,102 +0,0 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(
r#"
DROP TABLE "gallery_like";
DROP TABLE "gallery_post";
"#,
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(
r#"
create table gallery_post
(
id varchar(32) not null
constraint "PK_8e90d7b6015f2c4518881b14753"
primary key,
"createdAt" timestamp with time zone not null,
"updatedAt" timestamp with time zone not null,
title varchar(256) not null,
description varchar(2048),
"userId" varchar(32) not null
constraint "FK_985b836dddd8615e432d7043ddb"
references "user"
on delete cascade,
"fileIds" varchar(32)[] default '{}'::character varying[] not null,
"isSensitive" boolean default false not null,
"likedCount" integer default 0 not null,
tags varchar(128)[] default '{}'::character varying[] not null
);
comment on column gallery_post."createdAt" is 'The created date of the GalleryPost.';
comment on column gallery_post."updatedAt" is 'The updated date of the GalleryPost.';
comment on column gallery_post."userId" is 'The ID of author.';
comment on column gallery_post."isSensitive" is 'Whether the post is sensitive.';
create index "IDX_8f1a239bd077c8864a20c62c2c"
on gallery_post ("createdAt");
create index "IDX_f631d37835adb04792e361807c"
on gallery_post ("updatedAt");
create index "IDX_985b836dddd8615e432d7043dd"
on gallery_post ("userId");
create index "IDX_3ca50563facd913c425e7a89ee"
on gallery_post ("fileIds");
create index "IDX_f2d744d9a14d0dfb8b96cb7fc5"
on gallery_post ("isSensitive");
create index "IDX_1a165c68a49d08f11caffbd206"
on gallery_post ("likedCount");
create index "IDX_05cca34b985d1b8edc1d1e28df"
on gallery_post (tags);
create table gallery_like
(
id varchar(32) not null
constraint "PK_853ab02be39b8de45cd720cc15f"
primary key,
"createdAt" timestamp with time zone not null,
"userId" varchar(32) not null
constraint "FK_8fd5215095473061855ceb948cf"
references "user"
on delete cascade,
"postId" varchar(32) not null
constraint "FK_b1cb568bfe569e47b7051699fc8"
references gallery_post
on delete cascade
);
create index "IDX_8fd5215095473061855ceb948c"
on gallery_like ("userId");
create unique index "IDX_df1b5f4099e99fb0bc5eae53b6"
on gallery_like ("userId", "postId");
"#,
)
.await?;
Ok(())
}
}

View File

@ -4,9 +4,9 @@ use ext_calckey_model_migration::{
}; };
use magnetar_sdk::types::SpanFilter; use magnetar_sdk::types::SpanFilter;
use sea_orm::{ use sea_orm::{
ColumnTrait, Condition, ConnectionTrait, Cursor, DbErr, DynIden, EntityTrait, FromQueryResult, ColumnTrait, Condition, ConnectionTrait, Cursor, CursorTrait, DbErr, DynIden, EntityTrait,
Iden, IntoIdentity, Iterable, JoinType, RelationDef, RelationTrait, Select, SelectModel, FromQueryResult, Iden, IntoIdentity, Iterable, JoinType, RelationDef, RelationTrait, Select,
SelectorTrait, SelectModel, SelectorTrait,
}; };
use std::fmt::Write; use std::fmt::Write;

View File

@ -312,7 +312,7 @@ impl NoteResolver {
&mut select, &mut select,
&note_tbl, &note_tbl,
options.with_reply_target.then_some(1).unwrap_or_default(), options.with_reply_target.then_some(1).unwrap_or_default(),
options.with_renote_target.then_some(2).unwrap_or_default(), options.with_renote_target.then_some(1).unwrap_or_default(),
options, options,
&self.user_resolver, &self.user_resolver,
); );

View File

@ -14,6 +14,7 @@ import {
FollowingFolloweePopulated, FollowingFolloweePopulated,
FollowingFollowerPopulated, FollowingFollowerPopulated,
FollowRequest, FollowRequest,
GalleryPost,
Instance, Instance,
LiteInstanceMetadata, LiteInstanceMetadata,
MeDetailed, MeDetailed,
@ -562,6 +563,25 @@ export type Endpoints = {
"following/requests/list": { req: NoParams; res: FollowRequest[] }; "following/requests/list": { req: NoParams; res: FollowRequest[] };
"following/requests/reject": { req: { userId: User["id"] }; res: null }; "following/requests/reject": { req: { userId: User["id"] }; res: null };
// gallery
"gallery/featured": { req: TODO; res: TODO };
"gallery/popular": { req: TODO; res: TODO };
"gallery/posts": { req: TODO; res: TODO };
"gallery/posts/create": { req: TODO; res: TODO };
"gallery/posts/delete": { req: { postId: GalleryPost["id"] }; res: null };
"gallery/posts/like": { req: TODO; res: TODO };
"gallery/posts/show": { req: TODO; res: TODO };
"gallery/posts/unlike": { req: TODO; res: TODO };
"gallery/posts/update": { req: TODO; res: TODO };
// games
"games/reversi/games": { req: TODO; res: TODO };
"games/reversi/games/show": { req: TODO; res: TODO };
"games/reversi/games/surrender": { req: TODO; res: TODO };
"games/reversi/invitations": { req: TODO; res: TODO };
"games/reversi/match": { req: TODO; res: TODO };
"games/reversi/match/cancel": { req: TODO; res: TODO };
// get-online-users-count // get-online-users-count
"get-online-users-count": { req: NoParams; res: { count: number } }; "get-online-users-count": { req: NoParams; res: { count: number } };
@ -591,6 +611,8 @@ export type Endpoints = {
}; };
res: NoteFavorite[]; res: NoteFavorite[];
}; };
"i/gallery/likes": { req: TODO; res: TODO };
"i/gallery/posts": { req: TODO; res: TODO };
"i/get-word-muted-notes-count": { req: TODO; res: TODO }; "i/get-word-muted-notes-count": { req: TODO; res: TODO };
"i/import-following": { req: TODO; res: TODO }; "i/import-following": { req: TODO; res: TODO };
"i/import-user-lists": { req: TODO; res: TODO }; "i/import-user-lists": { req: TODO; res: TODO };
@ -928,6 +950,7 @@ export type Endpoints = {
}; };
res: FollowingFolloweePopulated[]; res: FollowingFolloweePopulated[];
}; };
"users/gallery/posts": { req: TODO; res: TODO };
"users/get-frequently-replied-users": { req: TODO; res: TODO }; "users/get-frequently-replied-users": { req: TODO; res: TODO };
"users/lists/create": { req: { name: string }; res: UserList }; "users/lists/create": { req: { name: string }; res: UserList };
"users/lists/delete": { req: { listId: UserList["id"] }; res: null }; "users/lists/delete": { req: { listId: UserList["id"] }; res: null };
@ -957,6 +980,7 @@ export type Endpoints = {
res: Note[]; res: Note[];
}; };
"users/recommendation": { req: TODO; res: TODO }; "users/recommendation": { req: TODO; res: TODO };
"users/relation": { req: TODO; res: TODO };
"users/report-abuse": { req: TODO; res: TODO }; "users/report-abuse": { req: TODO; res: TODO };
"users/search-by-username-and-host": { req: TODO; res: TODO }; "users/search-by-username-and-host": { req: TODO; res: TODO };
"users/search": { req: TODO; res: TODO }; "users/search": { req: TODO; res: TODO };

View File

@ -44,4 +44,12 @@ export const permissions = [
"read:reactions", "read:reactions",
"write:reactions", "write:reactions",
"write:votes", "write:votes",
"read:pages",
"write:pages",
"write:page-likes",
"read:page-likes",
"read:gallery",
"write:gallery",
"read:gallery-likes",
"write:gallery-likes",
]; ];

View File

@ -121,6 +121,8 @@ export type DriveFile = {
export type DriveFolder = TODO; export type DriveFolder = TODO;
export type GalleryPost = TODO;
export type Note = { export type Note = {
id: ID; id: ID;
createdAt: DateString; createdAt: DateString;

View File

@ -0,0 +1,118 @@
<template>
<MkA :to="`/gallery/${post.id}`" class="ttasepnz _panel">
<div class="thumbnail">
<ImgWithBlurhash
class="img"
:src="post.files[0].thumbnailUrl"
:hash="post.files[0].blurhash"
/>
</div>
<article>
<header>
<MagAvatarResolvingProxy :user="post.user" class="avatar" />
</header>
<footer>
<span class="title">{{ post.title }}</span>
</footer>
</article>
</MkA>
</template>
<script lang="ts" setup>
import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
const props = defineProps<{
post: any;
}>();
</script>
<style lang="scss" scoped>
.ttasepnz {
display: block;
position: relative;
height: 200px;
&:hover,
&:focus {
text-decoration: none;
color: var(--accent);
> .thumbnail {
transform: scale(1.1);
}
> article {
> footer {
&:before {
opacity: 1;
}
}
}
}
> .thumbnail {
width: 100%;
height: 100%;
position: absolute;
transition: all 0.5s ease;
> .img {
position: relative;
width: 100%;
height: 100%;
object-fit: cover;
}
}
> article {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
> header {
position: absolute;
top: 0;
width: 100%;
padding: 12px;
box-sizing: border-box;
display: flex;
> .avatar {
margin-left: auto;
width: 32px;
height: 32px;
}
}
> footer {
position: absolute;
bottom: 0;
width: 100%;
padding: 16px;
box-sizing: border-box;
color: #fff;
text-shadow: 0 0 8px var(--shadow);
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
&:before {
content: "";
display: block;
position: absolute;
z-index: -1;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(rgba(0, 0, 0, 0.4), transparent);
opacity: 0;
transition: opacity 0.5s ease;
}
> .title {
font-weight: bold;
}
}
}
}
</style>

View File

@ -49,7 +49,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from "vue"; import { onMounted, ref } from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import PhotoSwipeLightbox from "photoswipe/lightbox"; import PhotoSwipeLightbox from "photoswipe/lightbox";
import PhotoSwipe from "photoswipe"; import PhotoSwipe from "photoswipe";
@ -68,6 +68,7 @@ const props = defineProps<{
inDm?: boolean; inDm?: boolean;
}>(); }>();
const gallery = ref(null);
const pswpZIndex = os.claimZIndex("middle"); const pswpZIndex = os.claimZIndex("middle");
onMounted(() => { onMounted(() => {
@ -103,6 +104,7 @@ onMounted(() => {
} }
return item; return item;
}), }),
gallery: gallery.value,
children: ".image", children: ".image",
thumbSelector: ".image", thumbSelector: ".image",
loop: false, loop: false,

View File

@ -63,6 +63,11 @@ export const navbarItemDef = reactive({
show: computed(() => $i != null), show: computed(() => $i != null),
to: "/my/favorites", to: "/my/favorites",
}, },
gallery: {
title: "gallery",
icon: "ph-image-square ph-bold ph-lg",
to: "/gallery",
},
clips: { clips: {
title: "clips", title: "clips",
icon: "ph-paperclip ph-bold ph-lg", icon: "ph-paperclip ph-bold ph-lg",

View File

@ -0,0 +1,195 @@
<template>
<MkStickyContainer>
<template #header
><MkPageHeader :actions="headerActions" :tabs="headerTabs"
/></template>
<MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<FormInput v-model="title">
<template #label>{{ i18n.ts.title }}</template>
</FormInput>
<FormTextarea v-model="description" :max="500">
<template #label>{{ i18n.ts.description }}</template>
</FormTextarea>
<div class="">
<div
v-for="file in files"
:key="file.id"
class="wqugxsfx"
:style="{
backgroundImage: file
? `url(${file.thumbnailUrl})`
: null,
}"
>
<div class="name">{{ file.name }}</div>
<button
v-tooltip="i18n.ts.remove"
class="remove _button"
@click="remove(file)"
>
<i class="ph-x ph-bold ph-lg"></i>
</button>
</div>
<FormButton primary @click="selectFile"
><i class="ph-plus ph-bold ph-lg"></i>
{{ i18n.ts.attachFile }}</FormButton
>
</div>
<FormSwitch v-model="isSensitive">{{
i18n.ts.markAsSensitive
}}</FormSwitch>
<FormButton v-if="postId" primary @click="save"
><i class="ph-floppy-disk-back ph-bold ph-lg"></i>
{{ i18n.ts.save }}</FormButton
>
<FormButton v-else primary @click="save"
><i class="ph-floppy-disk-back ph-bold ph-lg"></i>
{{ i18n.ts.publish }}</FormButton
>
<FormButton v-if="postId" danger @click="del"
><i class="ph-trash ph-bold ph-lg"></i>
{{ i18n.ts.delete }}</FormButton
>
</FormSuspense>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed, inject, watch } from "vue";
import FormButton from "@/components/MkButton.vue";
import FormInput from "@/components/form/input.vue";
import FormTextarea from "@/components/form/textarea.vue";
import FormSwitch from "@/components/form/switch.vue";
import FormSuspense from "@/components/form/suspense.vue";
import { selectFiles } from "@/scripts/select-file";
import * as os from "@/os";
import { useRouter } from "@/router";
import { definePageMetadata } from "@/scripts/page-metadata";
import { i18n } from "@/i18n";
const router = useRouter();
const props = defineProps<{
postId?: string;
}>();
let init = $ref(null);
let files = $ref([]);
let description = $ref(null);
let title = $ref(null);
let isSensitive = $ref(false);
function selectFile(evt) {
selectFiles(evt.currentTarget ?? evt.target, null).then((selected) => {
files = files.concat(selected);
});
}
function remove(file) {
files = files.filter((f) => f.id !== file.id);
}
async function save() {
if (props.postId) {
await os.apiWithDialog("gallery/posts/update", {
postId: props.postId,
title: title,
description: description,
fileIds: files.map((file) => file.id),
isSensitive: isSensitive,
});
router.push(`/gallery/${props.postId}`);
} else {
const created = await os.apiWithDialog("gallery/posts/create", {
title: title,
description: description,
fileIds: files.map((file) => file.id),
isSensitive: isSensitive,
});
router.push(`/gallery/${created.id}`);
}
}
async function del() {
const { canceled } = await os.confirm({
type: "warning",
text: i18n.ts.deleteConfirm,
});
if (canceled) return;
await os.apiWithDialog("gallery/posts/delete", {
postId: props.postId,
});
router.push("/gallery");
}
watch(
() => props.postId,
() => {
init = () =>
props.postId
? os
.api("gallery/posts/show", {
postId: props.postId,
})
.then((post) => {
files = post.files;
title = post.title;
description = post.description;
isSensitive = post.isSensitive;
})
: Promise.resolve(null);
},
{ immediate: true }
);
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata(
computed(() =>
props.postId
? {
title: i18n.ts.edit,
icon: "ph-pencil ph-bold ph-lg",
}
: {
title: i18n.ts.postToGallery,
icon: "ph-pencil ph-bold ph-lg",
}
)
);
</script>
<style lang="scss" scoped>
.wqugxsfx {
height: 200px;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
position: relative;
> .name {
position: absolute;
top: 8px;
left: 9px;
padding: 8px;
background: var(--panel);
}
> .remove {
position: absolute;
top: 8px;
right: 9px;
padding: 8px;
background: var(--panel);
}
}
</style>

View File

@ -0,0 +1,223 @@
<template>
<MkStickyContainer>
<template #header
><MkPageHeader
v-model:tab="tab"
:actions="headerActions"
:tabs="headerTabs"
:display-back-button="true"
/></template>
<MkSpacer :content-max="1200">
<swiper
:round-lengths="true"
:touch-angle="25"
:threshold="10"
:centeredSlides="true"
:modules="[Virtual]"
:space-between="20"
:virtual="true"
:allow-touch-move="
!(
deviceKind === 'desktop' &&
!defaultStore.state.swipeOnDesktop
)
"
@swiper="setSwiperRef"
@slide-change="onSlideChange"
>
<swiper-slide>
<MkFolder class="_gap">
<template #header
><i class="ph-clock ph-bold ph-lg"></i>
{{ i18n.ts.recentPosts }}</template
>
<MkPagination
v-slot="{ items }"
:pagination="recentPostsPagination"
:disable-auto-load="true"
>
<div class="vfpdbgtk">
<MkGalleryPostPreview
v-for="post in items"
:key="post.id"
:post="post"
class="post"
/>
</div>
</MkPagination>
</MkFolder>
<MkFolder class="_gap">
<template #header
><i class="ph-fire-simple ph-bold ph-lg"></i>
{{ i18n.ts.popularPosts }}</template
>
<MkPagination
v-slot="{ items }"
:pagination="popularPostsPagination"
:disable-auto-load="true"
>
<div class="vfpdbgtk">
<MkGalleryPostPreview
v-for="post in items"
:key="post.id"
:post="post"
class="post"
/>
</div>
</MkPagination>
</MkFolder>
</swiper-slide>
<swiper-slide>
<MkPagination
v-slot="{ items }"
:pagination="likedPostsPagination"
>
<div class="vfpdbgtk">
<MkGalleryPostPreview
v-for="like in items"
:key="like.id"
:post="like.post"
class="post"
/>
</div>
</MkPagination>
</swiper-slide>
<swiper-slide>
<MkA to="/gallery/new" class="_link" style="margin: 16px"
><i class="ph-plus ph-bold ph-lg"></i>
{{ i18n.ts.postToGallery }}</MkA
>
<MkPagination
v-slot="{ items }"
:pagination="myPostsPagination"
>
<div class="vfpdbgtk">
<MkGalleryPostPreview
v-for="post in items"
:key="post.id"
:post="post"
class="post"
/>
</div>
</MkPagination>
</swiper-slide>
</swiper>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed, defineComponent, watch, onMounted } from "vue";
import { Virtual } from "swiper";
import { Swiper, SwiperSlide } from "swiper/vue";
import MkFolder from "@/components/MkFolder.vue";
import MkPagination from "@/components/MkPagination.vue";
import MkGalleryPostPreview from "@/components/MkGalleryPostPreview.vue";
import { definePageMetadata } from "@/scripts/page-metadata";
import { deviceKind } from "@/scripts/device-kind";
import { i18n } from "@/i18n";
import { useRouter } from "@/router";
import { defaultStore } from "@/store";
import "swiper/scss";
import "swiper/scss/virtual";
const router = useRouter();
const props = defineProps<{
tag?: string;
}>();
const tabs = ["explore", "liked", "my"];
let tab = $ref(tabs[0]);
watch($$(tab), () => syncSlide(tabs.indexOf(tab)));
let tagsRef = $ref();
const recentPostsPagination = {
endpoint: "gallery/posts" as const,
limit: 6,
};
const popularPostsPagination = {
endpoint: "gallery/featured" as const,
limit: 5,
};
const myPostsPagination = {
endpoint: "i/gallery/posts" as const,
limit: 5,
};
const likedPostsPagination = {
endpoint: "i/gallery/likes" as const,
limit: 5,
};
watch(
() => props.tag,
() => {
if (tagsRef) tagsRef.tags.toggleContent(props.tag == null);
}
);
const headerActions = $computed(() => [
{
icon: "ph-plus ph-bold ph-lg",
text: i18n.ts.create,
handler: () => {
router.push("/gallery/new");
},
},
]);
const headerTabs = $computed(() => [
{
key: "explore",
title: i18n.ts.gallery,
icon: "ph-image-square ph-bold ph-lg",
},
{
key: "liked",
title: i18n.ts._gallery.liked,
icon: "ph-heart ph-bold ph-lg",
},
{
key: "my",
title: i18n.ts._gallery.my,
icon: "ph-crown-simple ph-bold ph-lg",
},
]);
definePageMetadata({
title: i18n.ts.gallery,
icon: "ph-image-square ph-bold ph-lg",
});
let swiperRef = null;
function setSwiperRef(swiper) {
swiperRef = swiper;
syncSlide(tabs.indexOf(tab));
}
function onSlideChange() {
tab = tabs[swiperRef.activeIndex];
}
function syncSlide(index) {
swiperRef.slideTo(index);
}
onMounted(() => {
syncSlide(tabs.indexOf(swiperRef.activeIndex));
});
</script>
<style lang="scss" scoped>
.vfpdbgtk {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-gap: 12px;
margin: 0 var(--margin);
> .post {
}
}
</style>

View File

@ -0,0 +1,360 @@
<template>
<MkStickyContainer>
<template #header
><MkPageHeader :actions="headerActions" :tabs="headerTabs"
/></template>
<MkSpacer :content-max="1000" :margin-min="16" :margin-max="32">
<div class="_root">
<transition
:name="$store.state.animation ? 'fade' : ''"
mode="out-in"
>
<div v-if="post" class="rkxwuolj">
<div class="files">
<div
v-for="file in post.files"
:key="file.id"
class="file"
>
<img :src="file.url" />
</div>
</div>
<div class="body _block">
<div class="title">{{ post.title }}</div>
<div class="description">
<Mfm :text="post.description" />
</div>
<div class="info">
<i class="ph-clock ph-bold ph-lg"></i>
<MkTime :time="post.createdAt" mode="detail" />
</div>
<div class="actions">
<div class="like">
<MkButton
v-if="post.isLiked"
v-tooltip="i18n.ts._gallery.unlike"
class="button"
primary
@click="unlike()"
><i class="ph-heart ph-fill ph-lg"></i
><span
v-if="post.likedCount > 0"
class="count"
>{{ post.likedCount }}</span
></MkButton
>
<MkButton
v-else
v-tooltip="i18n.ts._gallery.like"
class="button"
@click="like()"
><i class="ph-heart ph-bold"></i
><span
v-if="post.likedCount > 0"
class="count"
>{{ post.likedCount }}</span
></MkButton
>
</div>
<div class="other">
<button
v-if="$i && $i.id === post.user.id"
v-tooltip="i18n.ts.edit"
class="_button"
@click="edit"
>
<i
class="ph-pencil ph-bold ph-lg ph-fw ph-lg"
></i>
</button>
<button
v-tooltip="i18n.ts.shareWithNote"
class="_button"
@click="shareWithNote"
>
<i
class="ph-repeat ph-bold ph-lg ph-fw ph-lg"
></i>
</button>
<button
v-if="shareAvailable()"
v-tooltip="i18n.ts.share"
class="_button"
@click="share"
>
<i
class="ph-share-network ph-bold ph-lg ph-fw ph-lg"
></i>
</button>
</div>
</div>
<div class="user">
<MagAvatarResolvingProxy
:user="post.user"
class="avatar"
/>
<div class="name">
<MkUserName
:user="post.user"
style="display: block"
/>
<MkAcct :user="post.user" />
</div>
<MkFollowButton
v-if="!$i || $i.id != post.user.id"
:user="post.user"
:inline="true"
:transparent="false"
:full="true"
large
class="koudoku"
/>
</div>
</div>
<MkContainer
:max-height="300"
:foldable="true"
class="other"
>
<template #header
><i class="ph-clock ph-bold ph-lg"></i>
{{ i18n.ts.recentPosts }}</template
>
<MkPagination
v-slot="{ items }"
:pagination="otherPostsPagination"
>
<div class="sdrarzaf">
<MkGalleryPostPreview
v-for="post in items"
:key="post.id"
:post="post"
class="post"
/>
</div>
</MkPagination>
</MkContainer>
</div>
<MkError v-else-if="error" @retry="fetch()" />
<MkLoading v-else />
</transition>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed, watch } from "vue";
import MkButton from "@/components/MkButton.vue";
import * as os from "@/os";
import MkContainer from "@/components/MkContainer.vue";
import MkPagination from "@/components/MkPagination.vue";
import MkGalleryPostPreview from "@/components/MkGalleryPostPreview.vue";
import MkFollowButton from "@/components/MkFollowButton.vue";
import { url } from "@/config";
import { useRouter } from "@/router";
import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata";
import { shareAvailable } from "@/scripts/share-available";
import { $i } from "@/account";
const router = useRouter();
const props = defineProps<{
postId: string;
}>();
let post = $ref(null);
let error = $ref(null);
const otherPostsPagination = {
endpoint: "users/gallery/posts" as const,
limit: 6,
params: computed(() => ({
userId: post.user.id,
})),
};
function fetchPost() {
post = null;
os.api("gallery/posts/show", {
postId: props.postId,
})
.then((_post) => {
post = _post;
})
.catch((_error) => {
error = _error;
});
}
function share() {
navigator.share({
title: post.title,
text: post.description,
url: `${url}/gallery/${post.id}`,
});
}
function shareWithNote() {
os.post({
initialText: `${post.title} ${url}/gallery/${post.id}`,
});
}
function like() {
os.api("gallery/posts/like", {
postId: props.postId,
}).then(() => {
post.isLiked = true;
post.likedCount++;
});
}
async function unlike() {
os.api("gallery/posts/unlike", {
postId: props.postId,
}).then(() => {
post.isLiked = false;
post.likedCount--;
});
}
function edit() {
router.push(`/gallery/${post.id}/edit`);
}
watch(() => props.postId, fetchPost, { immediate: true });
const headerActions = $computed(() => [
{
icon: "ph-pencil ph-bold ph-lg",
text: i18n.ts.edit,
handler: edit,
},
]);
const headerTabs = $computed(() => []);
definePageMetadata(
computed(() =>
post
? {
title: post.title,
avatar: post.user,
}
: null
)
);
</script>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.125s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.rkxwuolj {
> .files {
> .file {
> img {
display: block;
max-width: 100%;
max-height: 500px;
margin: 0 auto;
border-radius: 10px;
}
& + .file {
margin-top: 16px;
}
}
}
> .body {
padding: 32px;
> .title {
font-weight: bold;
font-size: 1.2em;
margin-bottom: 16px;
}
> .info {
margin-top: 16px;
font-size: 90%;
opacity: 0.7;
}
> .actions {
display: flex;
align-items: center;
margin-top: 16px;
padding: 16px 0 0 0;
border-top: solid 0.5px var(--divider);
> .like {
> .button {
--accent: #eb6f92;
--X8: #eb6f92;
--buttonBg: rgb(216 71 106 / 5%);
--buttonHoverBg: rgb(216 71 106 / 10%);
color: #eb6f92;
::v-deep(.count) {
margin-left: 0.5em;
}
}
}
> .other {
margin-left: auto;
> button {
padding: 8px;
margin: 0 8px;
&:hover {
color: var(--fgHighlighted);
}
}
}
}
> .user {
margin-top: 16px;
padding: 16px 0 0 0;
border-top: solid 0.5px var(--divider);
display: flex;
align-items: center;
> .avatar {
width: 52px;
height: 52px;
}
> .name {
margin: 0 0 0 12px;
font-size: 90%;
}
> .koudoku {
margin-left: auto;
}
}
}
}
.sdrarzaf {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-gap: 12px;
margin: var(--margin);
> .post {
}
}
</style>

View File

@ -0,0 +1,45 @@
<template>
<MkSpacer :content-max="800">
<MkPagination v-slot="{ items }" :pagination="pagination">
<div class="jrnovfpt">
<MkGalleryPostPreview
v-for="post in items"
:key="post.id"
:post="post"
class="post"
/>
</div>
</MkPagination>
</MkSpacer>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import MkGalleryPostPreview from "@/components/MkGalleryPostPreview.vue";
import MkPagination from "@/components/MkPagination.vue";
import { packed } from "magnetar-common";
const props = withDefaults(
defineProps<{
user: packed.PackUserBase;
}>(),
{}
);
const pagination = {
endpoint: "users/gallery/posts" as const,
limit: 6,
params: computed(() => ({
userId: props.user.id,
})),
};
</script>
<style lang="scss" scoped>
.jrnovfpt {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-gap: 12px;
margin: var(--margin);
}
</style>

View File

@ -18,6 +18,7 @@
/> />
<XReactions v-else-if="tab === 'reactions'" :user="user" /> <XReactions v-else-if="tab === 'reactions'" :user="user" />
<XClips v-else-if="tab === 'clips'" :user="user" /> <XClips v-else-if="tab === 'clips'" :user="user" />
<XGallery v-else-if="tab === 'gallery'" :user="user" />
</div> </div>
<MkError v-else-if="error" @retry="fetchUser()" /> <MkError v-else-if="error" @retry="fetchUser()" />
<MkLoading v-else /> <MkLoading v-else />
@ -40,6 +41,7 @@ import * as Acct from "calckey-js/built/acct";
const XHome = defineAsyncComponent(() => import("./home.vue")); const XHome = defineAsyncComponent(() => import("./home.vue"));
const XReactions = defineAsyncComponent(() => import("./reactions.vue")); const XReactions = defineAsyncComponent(() => import("./reactions.vue"));
const XClips = defineAsyncComponent(() => import("./clips.vue")); const XClips = defineAsyncComponent(() => import("./clips.vue"));
const XGallery = defineAsyncComponent(() => import("./gallery.vue"));
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -140,6 +142,11 @@ const headerTabs = $computed(() =>
title: i18n.ts.clips, title: i18n.ts.clips,
icon: "ph-paperclip ph-bold ph-lg", icon: "ph-paperclip ph-bold ph-lg",
}, },
{
key: "gallery",
title: i18n.ts.gallery,
icon: "ph-image-square ph-bold ph-lg",
},
] ]
: []), : []),
] ]

View File

@ -364,6 +364,24 @@ export const routes = [
path: "/tags/:tag", path: "/tags/:tag",
component: page(() => import("./pages/tag.vue")), component: page(() => import("./pages/tag.vue")),
}, },
{
path: "/gallery/:postId/edit",
component: page(() => import("./pages/gallery/edit.vue")),
loginRequired: true,
},
{
path: "/gallery/new",
component: page(() => import("./pages/gallery/edit.vue")),
loginRequired: true,
},
{
path: "/gallery/:postId",
component: page(() => import("./pages/gallery/post.vue")),
},
{
path: "/gallery",
component: page(() => import("./pages/gallery/index.vue")),
},
{ {
path: "/registry/keys/system/:path(*)?", path: "/registry/keys/system/:path(*)?",
component: page(() => import("./pages/registry.keys.vue")), component: page(() => import("./pages/registry.keys.vue")),

View File

@ -47,6 +47,10 @@
><i class="ph-compass ph-bold ph-lg icon"></i ><i class="ph-compass ph-bold ph-lg icon"></i
>{{ i18n.ts.explore }}</MkA >{{ i18n.ts.explore }}</MkA
> >
<MkA to="/gallery" class="link" active-class="active"
><i class="ph-image-square ph-bold ph-lg icon"></i
>{{ i18n.ts.gallery }}</MkA
>
<div class="action"> <div class="action">
<button class="_buttonPrimary" @click="signup()"> <button class="_buttonPrimary" @click="signup()">
{{ i18n.ts.signup }} {{ i18n.ts.signup }}

View File

@ -18,6 +18,10 @@
><i class="ph-compass ph-bold ph-lg icon"></i ><i class="ph-compass ph-bold ph-lg icon"></i
>{{ i18n.ts.explore }}</MkA >{{ i18n.ts.explore }}</MkA
> >
<MkA to="/gallery" class="link" active-class="active"
><i class="ph-image-square ph-bold ph-lg icon"></i
>{{ i18n.ts.gallery }}</MkA
>
<div v-if="info" class="page active link"> <div v-if="info" class="page active link">
<div class="title"> <div class="title">
<i v-if="info.icon" class="icon" :class="info.icon"></i> <i v-if="info.icon" class="icon" :class="info.icon"></i>