mirror of
https://activitypub.software/TransFem-org/Sharkey
synced 2024-11-25 07:25:12 +00:00
parent
8ae9d2eaa8
commit
b644567735
25 changed files with 403 additions and 15 deletions
|
@ -15,6 +15,7 @@ You should also include the user name that made the change.
|
||||||
### Improvements
|
### Improvements
|
||||||
- ユーザーごとにRenoteをミュートできるように
|
- ユーザーごとにRenoteをミュートできるように
|
||||||
- ノートごとに絵文字リアクションを受け取るか設定できるように
|
- ノートごとに絵文字リアクションを受け取るか設定できるように
|
||||||
|
- クリップをお気に入りに登録できるように
|
||||||
- ノート検索の利用可否をロールで制御可能に(デフォルトでオフ)
|
- ノート検索の利用可否をロールで制御可能に(デフォルトでオフ)
|
||||||
- ロールの並び順を設定可能に
|
- ロールの並び順を設定可能に
|
||||||
- カスタム絵文字にライセンス情報を付与できるように
|
- カスタム絵文字にライセンス情報を付与できるように
|
||||||
|
|
|
@ -975,6 +975,8 @@ sensitiveWords: "センシティブワード"
|
||||||
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
|
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
|
||||||
notesSearchNotAvailable: "ノート検索は利用できません。"
|
notesSearchNotAvailable: "ノート検索は利用できません。"
|
||||||
license: "ライセンス"
|
license: "ライセンス"
|
||||||
|
unfavoriteConfirm: "お気に入り解除しますか?"
|
||||||
|
myClips: "自分のクリップ"
|
||||||
|
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "獲得日時"
|
earnedAt: "獲得日時"
|
||||||
|
|
23
packages/backend/migration/1678953978856-clip-favorite.js
Normal file
23
packages/backend/migration/1678953978856-clip-favorite.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
export class clipFavorite1678953978856 {
|
||||||
|
name = 'clipFavorite1678953978856'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE TABLE "clip_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "clipId" character varying(32) NOT NULL, CONSTRAINT "PK_1b539f43906f05ebcabe752a977" PRIMARY KEY ("id"))`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_25a31662b0b0cc9af6549a9d71" ON "clip_favorite" ("userId") `);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b1754a39d0b281e07ed7c078ec" ON "clip_favorite" ("userId", "clipId") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "clip" ADD "lastClippedAt" TIMESTAMP WITH TIME ZONE`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_a3eac04ae2aa9e221e7596114a" ON "clip" ("lastClippedAt") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "clip_favorite" ADD CONSTRAINT "FK_25a31662b0b0cc9af6549a9d711" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "clip_favorite" ADD CONSTRAINT "FK_fce61c7986cee54393e79f1d849" FOREIGN KEY ("clipId") REFERENCES "clip"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "clip_favorite" DROP CONSTRAINT "FK_fce61c7986cee54393e79f1d849"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "clip_favorite" DROP CONSTRAINT "FK_25a31662b0b0cc9af6549a9d711"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_a3eac04ae2aa9e221e7596114a"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "clip" DROP COLUMN "lastClippedAt"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_b1754a39d0b281e07ed7c078ec"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_25a31662b0b0cc9af6549a9d71"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "clip_favorite"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { ClipsRepository } from '@/models/index.js';
|
import type { ClipFavoritesRepository, ClipsRepository, User } from '@/models/index.js';
|
||||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { } from '@/models/entities/Blocking.js';
|
import type { } from '@/models/entities/Blocking.js';
|
||||||
|
@ -14,6 +14,9 @@ export class ClipEntityService {
|
||||||
@Inject(DI.clipsRepository)
|
@Inject(DI.clipsRepository)
|
||||||
private clipsRepository: ClipsRepository,
|
private clipsRepository: ClipsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.clipFavoritesRepository)
|
||||||
|
private clipFavoritesRepository: ClipFavoritesRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
@ -21,25 +24,31 @@ export class ClipEntityService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async pack(
|
public async pack(
|
||||||
src: Clip['id'] | Clip,
|
src: Clip['id'] | Clip,
|
||||||
|
me?: { id: User['id'] } | null | undefined,
|
||||||
): Promise<Packed<'Clip'>> {
|
): Promise<Packed<'Clip'>> {
|
||||||
|
const meId = me ? me.id : null;
|
||||||
const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src });
|
const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
return await awaitAll({
|
return await awaitAll({
|
||||||
id: clip.id,
|
id: clip.id,
|
||||||
createdAt: clip.createdAt.toISOString(),
|
createdAt: clip.createdAt.toISOString(),
|
||||||
|
lastClippedAt: clip.lastClippedAt ? clip.lastClippedAt.toISOString() : null,
|
||||||
userId: clip.userId,
|
userId: clip.userId,
|
||||||
user: this.userEntityService.pack(clip.user ?? clip.userId),
|
user: this.userEntityService.pack(clip.user ?? clip.userId),
|
||||||
name: clip.name,
|
name: clip.name,
|
||||||
description: clip.description,
|
description: clip.description,
|
||||||
isPublic: clip.isPublic,
|
isPublic: clip.isPublic,
|
||||||
|
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
|
||||||
|
isFavorited: meId ? await this.clipFavoritesRepository.findOneBy({ clipId: clip.id, userId: meId }).then(x => x != null) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public packMany(
|
public packMany(
|
||||||
clips: Clip[],
|
clips: Clip[],
|
||||||
|
me?: { id: User['id'] } | null | undefined,
|
||||||
) {
|
) {
|
||||||
return Promise.all(clips.map(x => this.pack(x)));
|
return Promise.all(clips.map(x => this.pack(x, me)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,7 @@ export const DI = {
|
||||||
moderationLogsRepository: Symbol('moderationLogsRepository'),
|
moderationLogsRepository: Symbol('moderationLogsRepository'),
|
||||||
clipsRepository: Symbol('clipsRepository'),
|
clipsRepository: Symbol('clipsRepository'),
|
||||||
clipNotesRepository: Symbol('clipNotesRepository'),
|
clipNotesRepository: Symbol('clipNotesRepository'),
|
||||||
|
clipFavoritesRepository: Symbol('clipFavoritesRepository'),
|
||||||
antennasRepository: Symbol('antennasRepository'),
|
antennasRepository: Symbol('antennasRepository'),
|
||||||
antennaNotesRepository: Symbol('antennaNotesRepository'),
|
antennaNotesRepository: Symbol('antennaNotesRepository'),
|
||||||
promoNotesRepository: Symbol('promoNotesRepository'),
|
promoNotesRepository: Symbol('promoNotesRepository'),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment } from './index.js';
|
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite } from './index.js';
|
||||||
import type { DataSource } from 'typeorm';
|
import type { DataSource } from 'typeorm';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
|
|
||||||
|
@ -286,6 +286,12 @@ const $clipNotesRepository: Provider = {
|
||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $clipFavoritesRepository: Provider = {
|
||||||
|
provide: DI.clipFavoritesRepository,
|
||||||
|
useFactory: (db: DataSource) => db.getRepository(ClipFavorite),
|
||||||
|
inject: [DI.db],
|
||||||
|
};
|
||||||
|
|
||||||
const $antennasRepository: Provider = {
|
const $antennasRepository: Provider = {
|
||||||
provide: DI.antennasRepository,
|
provide: DI.antennasRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(Antenna),
|
useFactory: (db: DataSource) => db.getRepository(Antenna),
|
||||||
|
@ -445,6 +451,7 @@ const $roleAssignmentsRepository: Provider = {
|
||||||
$moderationLogsRepository,
|
$moderationLogsRepository,
|
||||||
$clipsRepository,
|
$clipsRepository,
|
||||||
$clipNotesRepository,
|
$clipNotesRepository,
|
||||||
|
$clipFavoritesRepository,
|
||||||
$antennasRepository,
|
$antennasRepository,
|
||||||
$antennaNotesRepository,
|
$antennaNotesRepository,
|
||||||
$promoNotesRepository,
|
$promoNotesRepository,
|
||||||
|
@ -512,6 +519,7 @@ const $roleAssignmentsRepository: Provider = {
|
||||||
$moderationLogsRepository,
|
$moderationLogsRepository,
|
||||||
$clipsRepository,
|
$clipsRepository,
|
||||||
$clipNotesRepository,
|
$clipNotesRepository,
|
||||||
|
$clipFavoritesRepository,
|
||||||
$antennasRepository,
|
$antennasRepository,
|
||||||
$antennaNotesRepository,
|
$antennaNotesRepository,
|
||||||
$promoNotesRepository,
|
$promoNotesRepository,
|
||||||
|
|
|
@ -12,6 +12,12 @@ export class Clip {
|
||||||
})
|
})
|
||||||
public createdAt: Date;
|
public createdAt: Date;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column('timestamp with time zone', {
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public lastClippedAt: Date | null;
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
@Column({
|
@Column({
|
||||||
...id(),
|
...id(),
|
||||||
|
|
33
packages/backend/src/models/entities/ClipFavorite.ts
Normal file
33
packages/backend/src/models/entities/ClipFavorite.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||||
|
import { id } from '../id.js';
|
||||||
|
import { User } from './User.js';
|
||||||
|
import { Clip } from './Clip.js';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Index(['userId', 'clipId'], { unique: true })
|
||||||
|
export class ClipFavorite {
|
||||||
|
@PrimaryColumn(id())
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@Column('timestamp with time zone')
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column(id())
|
||||||
|
public userId: User['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => User, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public user: User | null;
|
||||||
|
|
||||||
|
@Column(id())
|
||||||
|
public clipId: Clip['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => Clip, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public clip: Clip | null;
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js';
|
||||||
import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js';
|
import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js';
|
||||||
import { Clip } from '@/models/entities/Clip.js';
|
import { Clip } from '@/models/entities/Clip.js';
|
||||||
import { ClipNote } from '@/models/entities/ClipNote.js';
|
import { ClipNote } from '@/models/entities/ClipNote.js';
|
||||||
|
import { ClipFavorite } from '@/models/entities/ClipFavorite.js';
|
||||||
import { DriveFile } from '@/models/entities/DriveFile.js';
|
import { DriveFile } from '@/models/entities/DriveFile.js';
|
||||||
import { DriveFolder } from '@/models/entities/DriveFolder.js';
|
import { DriveFolder } from '@/models/entities/DriveFolder.js';
|
||||||
import { Emoji } from '@/models/entities/Emoji.js';
|
import { Emoji } from '@/models/entities/Emoji.js';
|
||||||
|
@ -81,6 +82,7 @@ export {
|
||||||
ChannelNotePining,
|
ChannelNotePining,
|
||||||
Clip,
|
Clip,
|
||||||
ClipNote,
|
ClipNote,
|
||||||
|
ClipFavorite,
|
||||||
DriveFile,
|
DriveFile,
|
||||||
DriveFolder,
|
DriveFolder,
|
||||||
Emoji,
|
Emoji,
|
||||||
|
@ -148,6 +150,7 @@ export type ChannelFollowingsRepository = Repository<ChannelFollowing>;
|
||||||
export type ChannelNotePiningsRepository = Repository<ChannelNotePining>;
|
export type ChannelNotePiningsRepository = Repository<ChannelNotePining>;
|
||||||
export type ClipsRepository = Repository<Clip>;
|
export type ClipsRepository = Repository<Clip>;
|
||||||
export type ClipNotesRepository = Repository<ClipNote>;
|
export type ClipNotesRepository = Repository<ClipNote>;
|
||||||
|
export type ClipFavoritesRepository = Repository<ClipFavorite>;
|
||||||
export type DriveFilesRepository = Repository<DriveFile>;
|
export type DriveFilesRepository = Repository<DriveFile>;
|
||||||
export type DriveFoldersRepository = Repository<DriveFolder>;
|
export type DriveFoldersRepository = Repository<DriveFolder>;
|
||||||
export type EmojisRepository = Repository<Emoji>;
|
export type EmojisRepository = Repository<Emoji>;
|
||||||
|
|
|
@ -12,6 +12,11 @@ export const packedClipSchema = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
},
|
},
|
||||||
|
lastClippedAt: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
userId: {
|
userId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -34,5 +39,13 @@ export const packedClipSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
isFavorited: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
},
|
||||||
|
favoritedCount: {
|
||||||
|
type: 'number',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { ChannelFollowing } from '@/models/entities/ChannelFollowing.js';
|
||||||
import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js';
|
import { ChannelNotePining } from '@/models/entities/ChannelNotePining.js';
|
||||||
import { Clip } from '@/models/entities/Clip.js';
|
import { Clip } from '@/models/entities/Clip.js';
|
||||||
import { ClipNote } from '@/models/entities/ClipNote.js';
|
import { ClipNote } from '@/models/entities/ClipNote.js';
|
||||||
|
import { ClipFavorite } from '@/models/entities/ClipFavorite.js';
|
||||||
import { DriveFile } from '@/models/entities/DriveFile.js';
|
import { DriveFile } from '@/models/entities/DriveFile.js';
|
||||||
import { DriveFolder } from '@/models/entities/DriveFolder.js';
|
import { DriveFolder } from '@/models/entities/DriveFolder.js';
|
||||||
import { Emoji } from '@/models/entities/Emoji.js';
|
import { Emoji } from '@/models/entities/Emoji.js';
|
||||||
|
@ -165,6 +166,7 @@ export const entities = [
|
||||||
ModerationLog,
|
ModerationLog,
|
||||||
Clip,
|
Clip,
|
||||||
ClipNote,
|
ClipNote,
|
||||||
|
ClipFavorite,
|
||||||
Antenna,
|
Antenna,
|
||||||
AntennaNote,
|
AntennaNote,
|
||||||
PromoNote,
|
PromoNote,
|
||||||
|
|
|
@ -114,6 +114,9 @@ import * as ep___clips_list from './endpoints/clips/list.js';
|
||||||
import * as ep___clips_notes from './endpoints/clips/notes.js';
|
import * as ep___clips_notes from './endpoints/clips/notes.js';
|
||||||
import * as ep___clips_show from './endpoints/clips/show.js';
|
import * as ep___clips_show from './endpoints/clips/show.js';
|
||||||
import * as ep___clips_update from './endpoints/clips/update.js';
|
import * as ep___clips_update from './endpoints/clips/update.js';
|
||||||
|
import * as ep___clips_favorite from './endpoints/clips/favorite.js';
|
||||||
|
import * as ep___clips_unfavorite from './endpoints/clips/unfavorite.js';
|
||||||
|
import * as ep___clips_myFavorites from './endpoints/clips/my-favorites.js';
|
||||||
import * as ep___drive from './endpoints/drive.js';
|
import * as ep___drive from './endpoints/drive.js';
|
||||||
import * as ep___drive_files from './endpoints/drive/files.js';
|
import * as ep___drive_files from './endpoints/drive/files.js';
|
||||||
import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js';
|
import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js';
|
||||||
|
@ -438,6 +441,9 @@ const $clips_list: Provider = { provide: 'ep:clips/list', useClass: ep___clips_l
|
||||||
const $clips_notes: Provider = { provide: 'ep:clips/notes', useClass: ep___clips_notes.default };
|
const $clips_notes: Provider = { provide: 'ep:clips/notes', useClass: ep___clips_notes.default };
|
||||||
const $clips_show: Provider = { provide: 'ep:clips/show', useClass: ep___clips_show.default };
|
const $clips_show: Provider = { provide: 'ep:clips/show', useClass: ep___clips_show.default };
|
||||||
const $clips_update: Provider = { provide: 'ep:clips/update', useClass: ep___clips_update.default };
|
const $clips_update: Provider = { provide: 'ep:clips/update', useClass: ep___clips_update.default };
|
||||||
|
const $clips_favorite: Provider = { provide: 'ep:clips/favorite', useClass: ep___clips_favorite.default };
|
||||||
|
const $clips_unfavorite: Provider = { provide: 'ep:clips/unfavorite', useClass: ep___clips_unfavorite.default };
|
||||||
|
const $clips_myFavorites: Provider = { provide: 'ep:clips/my-favorites', useClass: ep___clips_myFavorites.default };
|
||||||
const $drive: Provider = { provide: 'ep:drive', useClass: ep___drive.default };
|
const $drive: Provider = { provide: 'ep:drive', useClass: ep___drive.default };
|
||||||
const $drive_files: Provider = { provide: 'ep:drive/files', useClass: ep___drive_files.default };
|
const $drive_files: Provider = { provide: 'ep:drive/files', useClass: ep___drive_files.default };
|
||||||
const $drive_files_attachedNotes: Provider = { provide: 'ep:drive/files/attached-notes', useClass: ep___drive_files_attachedNotes.default };
|
const $drive_files_attachedNotes: Provider = { provide: 'ep:drive/files/attached-notes', useClass: ep___drive_files_attachedNotes.default };
|
||||||
|
@ -766,6 +772,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$clips_notes,
|
$clips_notes,
|
||||||
$clips_show,
|
$clips_show,
|
||||||
$clips_update,
|
$clips_update,
|
||||||
|
$clips_favorite,
|
||||||
|
$clips_unfavorite,
|
||||||
|
$clips_myFavorites,
|
||||||
$drive,
|
$drive,
|
||||||
$drive_files,
|
$drive_files,
|
||||||
$drive_files_attachedNotes,
|
$drive_files_attachedNotes,
|
||||||
|
@ -1088,6 +1097,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$clips_notes,
|
$clips_notes,
|
||||||
$clips_show,
|
$clips_show,
|
||||||
$clips_update,
|
$clips_update,
|
||||||
|
$clips_favorite,
|
||||||
|
$clips_unfavorite,
|
||||||
|
$clips_myFavorites,
|
||||||
$drive,
|
$drive,
|
||||||
$drive_files,
|
$drive_files,
|
||||||
$drive_files_attachedNotes,
|
$drive_files_attachedNotes,
|
||||||
|
|
|
@ -114,6 +114,9 @@ import * as ep___clips_list from './endpoints/clips/list.js';
|
||||||
import * as ep___clips_notes from './endpoints/clips/notes.js';
|
import * as ep___clips_notes from './endpoints/clips/notes.js';
|
||||||
import * as ep___clips_show from './endpoints/clips/show.js';
|
import * as ep___clips_show from './endpoints/clips/show.js';
|
||||||
import * as ep___clips_update from './endpoints/clips/update.js';
|
import * as ep___clips_update from './endpoints/clips/update.js';
|
||||||
|
import * as ep___clips_favorite from './endpoints/clips/favorite.js';
|
||||||
|
import * as ep___clips_unfavorite from './endpoints/clips/unfavorite.js';
|
||||||
|
import * as ep___clips_myFavorites from './endpoints/clips/my-favorites.js';
|
||||||
import * as ep___drive from './endpoints/drive.js';
|
import * as ep___drive from './endpoints/drive.js';
|
||||||
import * as ep___drive_files from './endpoints/drive/files.js';
|
import * as ep___drive_files from './endpoints/drive/files.js';
|
||||||
import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js';
|
import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js';
|
||||||
|
@ -436,6 +439,9 @@ const eps = [
|
||||||
['clips/notes', ep___clips_notes],
|
['clips/notes', ep___clips_notes],
|
||||||
['clips/show', ep___clips_show],
|
['clips/show', ep___clips_show],
|
||||||
['clips/update', ep___clips_update],
|
['clips/update', ep___clips_update],
|
||||||
|
['clips/favorite', ep___clips_favorite],
|
||||||
|
['clips/unfavorite', ep___clips_unfavorite],
|
||||||
|
['clips/my-favorites', ep___clips_myFavorites],
|
||||||
['drive', ep___drive],
|
['drive', ep___drive],
|
||||||
['drive/files', ep___drive_files],
|
['drive/files', ep___drive_files],
|
||||||
['drive/files/attached-notes', ep___drive_files_attachedNotes],
|
['drive/files/attached-notes', ep___drive_files_attachedNotes],
|
||||||
|
|
|
@ -106,6 +106,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
clipId: clip.id,
|
clipId: clip.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.clipsRepository.update(clip.id, {
|
||||||
|
lastClippedAt: new Date(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
description: ps.description,
|
description: ps.description,
|
||||||
}).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
return await this.clipEntityService.pack(clip);
|
return await this.clipEntityService.pack(clip, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
76
packages/backend/src/server/api/endpoints/clips/favorite.ts
Normal file
76
packages/backend/src/server/api/endpoints/clips/favorite.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type { ClipsRepository, ClipFavoritesRepository } from '@/models/index.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['clip'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'write:clip-favorite',
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchClip: {
|
||||||
|
message: 'No such clip.',
|
||||||
|
code: 'NO_SUCH_CLIP',
|
||||||
|
id: '4c2aaeae-80d8-4250-9606-26cb1fdb77a5',
|
||||||
|
},
|
||||||
|
|
||||||
|
alreadyFavorited: {
|
||||||
|
message: 'The clip has already been favorited.',
|
||||||
|
code: 'ALREADY_FAVORITED',
|
||||||
|
id: '92658936-c625-4273-8326-2d790129256e',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
clipId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['clipId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.clipsRepository)
|
||||||
|
private clipsRepository: ClipsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.clipFavoritesRepository)
|
||||||
|
private clipFavoritesRepository: ClipFavoritesRepository,
|
||||||
|
|
||||||
|
private idService: IdService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const clip = await this.clipsRepository.findOneBy({ id: ps.clipId });
|
||||||
|
if (clip == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchClip);
|
||||||
|
}
|
||||||
|
if ((clip.userId !== me.id) && !clip.isPublic) {
|
||||||
|
throw new ApiError(meta.errors.noSuchClip);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exist = await this.clipFavoritesRepository.findOneBy({
|
||||||
|
clipId: clip.id,
|
||||||
|
userId: me.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exist != null) {
|
||||||
|
throw new ApiError(meta.errors.alreadyFavorited);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.clipFavoritesRepository.insert({
|
||||||
|
id: this.idService.genId(),
|
||||||
|
createdAt: new Date(),
|
||||||
|
clipId: clip.id,
|
||||||
|
userId: me.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,7 +42,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await Promise.all(clips.map(x => this.clipEntityService.pack(x)));
|
return await this.clipEntityService.packMany(clips, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import type { ClipFavoritesRepository } from '@/models/index.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['account', 'clip'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'read:clip-favorite',
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'Clip',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
},
|
||||||
|
required: [],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.clipFavoritesRepository)
|
||||||
|
private clipFavoritesRepository: ClipFavoritesRepository,
|
||||||
|
|
||||||
|
private clipEntityService: ClipEntityService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const query = this.clipFavoritesRepository.createQueryBuilder('favorite')
|
||||||
|
.andWhere('favorite.userId = :meId', { meId: me.id })
|
||||||
|
.leftJoinAndSelect('favorite.clip', 'clip');
|
||||||
|
|
||||||
|
const favorites = await query
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
return this.clipEntityService.packMany(favorites.map(x => x.clip!), me);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -58,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
throw new ApiError(meta.errors.noSuchClip);
|
throw new ApiError(meta.errors.noSuchClip);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.clipEntityService.pack(clip);
|
return await this.clipEntityService.pack(clip, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type { ClipsRepository, ClipFavoritesRepository } from '@/models/index.js';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['clip'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'write:clip-favorite',
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchClip: {
|
||||||
|
message: 'No such clip.',
|
||||||
|
code: 'NO_SUCH_CLIP',
|
||||||
|
id: '2603966e-b865-426c-94a7-af4a01241dc1',
|
||||||
|
},
|
||||||
|
|
||||||
|
notFavorited: {
|
||||||
|
message: 'You have not favorited the clip.',
|
||||||
|
code: 'NOT_FAVORITED',
|
||||||
|
id: '90c3a9e8-b321-4dae-bf57-2bf79bbcc187',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
clipId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['clipId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.clipsRepository)
|
||||||
|
private clipsRepository: ClipsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.clipFavoritesRepository)
|
||||||
|
private clipFavoritesRepository: ClipFavoritesRepository,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const clip = await this.clipsRepository.findOneBy({ id: ps.clipId });
|
||||||
|
if (clip == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchClip);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exist = await this.clipFavoritesRepository.findOneBy({
|
||||||
|
clipId: clip.id,
|
||||||
|
userId: me.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exist == null) {
|
||||||
|
throw new ApiError(meta.errors.notFavorited);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.clipFavoritesRepository.delete(exist.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -64,7 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
isPublic: ps.isPublic,
|
isPublic: ps.isPublic,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.clipEntityService.pack(clip.id);
|
return await this.clipEntityService.pack(clip.id, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@ import type { ClipNotesRepository, ClipsRepository } from '@/models/index.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
|
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../error.js';
|
|
||||||
import { GetterService } from '@/server/api/GetterService.js';
|
import { GetterService } from '@/server/api/GetterService.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['clips', 'notes'],
|
tags: ['clips', 'notes'],
|
||||||
|
@ -67,7 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await Promise.all(clips.map(x => this.clipEntityService.pack(x)));
|
return await this.clipEntityService.packMany(clips, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
.take(ps.limit)
|
.take(ps.limit)
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
return await this.clipEntityService.packMany(clips);
|
return await this.clipEntityService.packMany(clips, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
<div v-if="clip.description" class="description">
|
<div v-if="clip.description" class="description">
|
||||||
<Mfm :text="clip.description" :is-note="false" :i="$i"/>
|
<Mfm :text="clip.description" :is-note="false" :i="$i"/>
|
||||||
</div>
|
</div>
|
||||||
|
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" as-like class="button" rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
|
||||||
|
<MkButton v-else v-tooltip="i18n.ts.favorite" as-like class="button" rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
|
||||||
<div class="user">
|
<div class="user">
|
||||||
<MkAvatar :user="clip.user" class="avatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
|
<MkAvatar :user="clip.user" class="avatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,12 +29,14 @@ import { i18n } from '@/i18n';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
import { url } from '@/config';
|
import { url } from '@/config';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
clipId: string,
|
clipId: string,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let clip: misskey.entities.Clip = $ref<misskey.entities.Clip>();
|
let clip: misskey.entities.Clip = $ref<misskey.entities.Clip>();
|
||||||
|
let favorited = $ref(false);
|
||||||
const pagination = {
|
const pagination = {
|
||||||
endpoint: 'clips/notes' as const,
|
endpoint: 'clips/notes' as const,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
@ -47,12 +51,34 @@ watch(() => props.clipId, async () => {
|
||||||
clip = await os.api('clips/show', {
|
clip = await os.api('clips/show', {
|
||||||
clipId: props.clipId,
|
clipId: props.clipId,
|
||||||
});
|
});
|
||||||
|
favorited = clip.isFavorited;
|
||||||
}, {
|
}, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
provide('currentClipPage', $$(clip));
|
provide('currentClipPage', $$(clip));
|
||||||
|
|
||||||
|
function favorite() {
|
||||||
|
os.apiWithDialog('clips/favorite', {
|
||||||
|
clipId: props.clipId,
|
||||||
|
}).then(() => {
|
||||||
|
favorited = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unfavorite() {
|
||||||
|
const confirm = await os.confirm({
|
||||||
|
type: 'warning',
|
||||||
|
text: i18n.ts.unfavoriteConfirm,
|
||||||
|
});
|
||||||
|
if (confirm.canceled) return;
|
||||||
|
os.apiWithDialog('clips/unfavorite', {
|
||||||
|
clipId: props.clipId,
|
||||||
|
}).then(() => {
|
||||||
|
favorited = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const headerActions = $computed(() => clip && isOwned ? [{
|
const headerActions = $computed(() => clip && isOwned ? [{
|
||||||
icon: 'ti ti-pencil',
|
icon: 'ti ti-pencil',
|
||||||
text: i18n.ts.edit,
|
text: i18n.ts.edit,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
<MkSpacer :content-max="700">
|
<MkSpacer :content-max="700">
|
||||||
<div class="qtcaoidl">
|
<div v-if="tab === 'my'" class="qtcaoidl">
|
||||||
<MkButton primary class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
<MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||||
|
|
||||||
<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="list">
|
<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="list">
|
||||||
<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _margin">
|
<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _margin">
|
||||||
|
@ -12,12 +12,22 @@
|
||||||
</MkA>
|
</MkA>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="tab === 'favorites'" class="_gaps">
|
||||||
|
<MkA v-for="item in favorites" :key="item.id" :to="`/clips/${item.id}`" :class="$style.clip" class="_panel">
|
||||||
|
<b>{{ item.name }}</b>
|
||||||
|
<div v-if="item.description" :class="$style.clipDescription">{{ item.description }}</div>
|
||||||
|
<div v-if="item.lastClippedAt">{{ i18n.ts.updatedAt }}: <MkTime :time="item.lastClippedAt" mode="detail"/></div>
|
||||||
|
<div :class="$style.clipUser">
|
||||||
|
<MkAvatar :user="item.user" :class="$style.clipUserAvatar" indicator link preview/> <MkUserName :user="item.user" :nowrap="false"/>
|
||||||
|
</div>
|
||||||
|
</MkA>
|
||||||
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</MkStickyContainer>
|
</MkStickyContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { watch } from 'vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from '@/components/MkPagination.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
@ -29,8 +39,15 @@ const pagination = {
|
||||||
limit: 10,
|
limit: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let tab = $ref('my');
|
||||||
|
let favorites = $ref();
|
||||||
|
|
||||||
const pagingComponent = $shallowRef<InstanceType<typeof MkPagination>>();
|
const pagingComponent = $shallowRef<InstanceType<typeof MkPagination>>();
|
||||||
|
|
||||||
|
watch($$(tab), async () => {
|
||||||
|
favorites = await os.api('clips/my-favorites');
|
||||||
|
});
|
||||||
|
|
||||||
async function create() {
|
async function create() {
|
||||||
const { canceled, result } = await os.form(i18n.ts.createNewClip, {
|
const { canceled, result } = await os.form(i18n.ts.createNewClip, {
|
||||||
name: {
|
name: {
|
||||||
|
@ -66,7 +83,15 @@ function onClipDeleted() {
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
const headerActions = $computed(() => []);
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
const headerTabs = $computed(() => [{
|
||||||
|
key: 'my',
|
||||||
|
title: i18n.ts.myClips,
|
||||||
|
icon: 'ti ti-paperclip',
|
||||||
|
}, {
|
||||||
|
key: 'favorites',
|
||||||
|
title: i18n.ts.favorites,
|
||||||
|
icon: 'ti ti-heart',
|
||||||
|
}]);
|
||||||
|
|
||||||
definePageMetadata({
|
definePageMetadata({
|
||||||
title: i18n.ts.clip,
|
title: i18n.ts.clip,
|
||||||
|
@ -98,3 +123,24 @@ definePageMetadata({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.clip {
|
||||||
|
display: block;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipDescription {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipUser {
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: solid 0.5px var(--divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clipUserAvatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
Loading…
Reference in a new issue