Merge branch 'develop' into serve-stream
This commit is contained in:
commit
4aaa200145
246 changed files with 5082 additions and 1919 deletions
29
packages/backend/migration/1672822262496-Flash.js
Normal file
29
packages/backend/migration/1672822262496-Flash.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
export class Flash1672822262496 {
|
||||
name = 'Flash1672822262496'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "flash" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "title" character varying(256) NOT NULL, "summary" character varying(1024) NOT NULL, "userId" character varying(32) NOT NULL, "script" character varying(16384) NOT NULL, "permissions" character varying(256) array NOT NULL DEFAULT '{}', "likedCount" integer NOT NULL DEFAULT '0', CONSTRAINT "PK_0c01a2c1c5f2266942dd1b3fdbc" PRIMARY KEY ("id")); COMMENT ON COLUMN "flash"."createdAt" IS 'The created date of the Flash.'; COMMENT ON COLUMN "flash"."updatedAt" IS 'The updated date of the Flash.'; COMMENT ON COLUMN "flash"."userId" IS 'The ID of author.'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_149d2e44785707548c82999b01" ON "flash" ("createdAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_3aa8ea9a8f15214ad91638c0a7" ON "flash" ("updatedAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_9b88250fc2fd009b8f1b5623ed" ON "flash" ("userId") `);
|
||||
await queryRunner.query(`CREATE TABLE "flash_like" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "flashId" character varying(32) NOT NULL, CONSTRAINT "PK_d110109ee310588d63d6183b233" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_60c4af1c19a7a75f1592f93b28" ON "flash_like" ("userId") `);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_cfbfeeccb0cbedcd660b17eb07" ON "flash_like" ("userId", "flashId") `);
|
||||
await queryRunner.query(`ALTER TABLE "flash" ADD CONSTRAINT "FK_9b88250fc2fd009b8f1b5623ed5" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "flash_like" ADD CONSTRAINT "FK_60c4af1c19a7a75f1592f93b287" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "flash_like" ADD CONSTRAINT "FK_6c16fe0e93b7a1951eca624b76a" FOREIGN KEY ("flashId") REFERENCES "flash"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "flash_like" DROP CONSTRAINT "FK_6c16fe0e93b7a1951eca624b76a"`);
|
||||
await queryRunner.query(`ALTER TABLE "flash_like" DROP CONSTRAINT "FK_60c4af1c19a7a75f1592f93b287"`);
|
||||
await queryRunner.query(`ALTER TABLE "flash" DROP CONSTRAINT "FK_9b88250fc2fd009b8f1b5623ed5"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_cfbfeeccb0cbedcd660b17eb07"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_60c4af1c19a7a75f1592f93b28"`);
|
||||
await queryRunner.query(`DROP TABLE "flash_like"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_9b88250fc2fd009b8f1b5623ed"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_3aa8ea9a8f15214ad91638c0a7"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_149d2e44785707548c82999b01"`);
|
||||
await queryRunner.query(`DROP TABLE "flash"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,9 +21,9 @@
|
|||
"@tensorflow/tfjs-node": "4.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^4.10.0",
|
||||
"@bull-board/fastify": "^4.10.0",
|
||||
"@bull-board/ui": "^4.10.0",
|
||||
"@bull-board/api": "^4.10.1",
|
||||
"@bull-board/fastify": "^4.10.1",
|
||||
"@bull-board/ui": "^4.10.1",
|
||||
"@discordapp/twemoji": "14.0.2",
|
||||
"@fastify/accepts": "4.1.0",
|
||||
"@fastify/cookie": "^8.3.0",
|
||||
|
|
@ -38,10 +38,10 @@
|
|||
"@peertube/http-signature": "1.7.0",
|
||||
"@sinonjs/fake-timers": "10.0.2",
|
||||
"accepts": "^1.3.8",
|
||||
"ajv": "8.11.2",
|
||||
"ajv": "8.12.0",
|
||||
"archiver": "5.3.1",
|
||||
"autwh": "0.1.0",
|
||||
"aws-sdk": "2.1286.0",
|
||||
"aws-sdk": "2.1289.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.4",
|
||||
"bull": "4.10.2",
|
||||
|
|
@ -109,8 +109,8 @@
|
|||
"stringz": "2.1.0",
|
||||
"summaly": "2.7.0",
|
||||
"syslog-pro": "git+https://github.com/misskey-dev/SyslogPro#0.2.9-misskey.2",
|
||||
"systeminformation": "5.16.9",
|
||||
"tinycolor2": "1.5.1",
|
||||
"systeminformation": "5.17.1",
|
||||
"tinycolor2": "1.5.2",
|
||||
"tmp": "0.2.1",
|
||||
"tsc-alias": "1.8.2",
|
||||
"tsconfig-paths": "4.1.2",
|
||||
|
|
@ -128,7 +128,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@redocly/openapi-core": "1.0.0-beta.117",
|
||||
"@swc/core": "1.3.24",
|
||||
"@swc/core": "1.3.25",
|
||||
"@swc/jest": "0.2.24",
|
||||
"@types/accepts": "1.3.5",
|
||||
"@types/archiver": "5.3.1",
|
||||
|
|
@ -172,8 +172,8 @@
|
|||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.47.1",
|
||||
"@typescript-eslint/parser": "5.47.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.48.0",
|
||||
"@typescript-eslint/parser": "5.48.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.31.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
|
|
|
|||
|
|
@ -95,6 +95,8 @@ import { UserEntityService } from './entities/UserEntityService.js';
|
|||
import { UserGroupEntityService } from './entities/UserGroupEntityService.js';
|
||||
import { UserGroupInvitationEntityService } from './entities/UserGroupInvitationEntityService.js';
|
||||
import { UserListEntityService } from './entities/UserListEntityService.js';
|
||||
import { FlashEntityService } from './entities/FlashEntityService.js';
|
||||
import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js';
|
||||
import { ApAudienceService } from './activitypub/ApAudienceService.js';
|
||||
import { ApDbResolverService } from './activitypub/ApDbResolverService.js';
|
||||
import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js';
|
||||
|
|
@ -216,6 +218,8 @@ const $UserEntityService: Provider = { provide: 'UserEntityService', useExisting
|
|||
const $UserGroupEntityService: Provider = { provide: 'UserGroupEntityService', useExisting: UserGroupEntityService };
|
||||
const $UserGroupInvitationEntityService: Provider = { provide: 'UserGroupInvitationEntityService', useExisting: UserGroupInvitationEntityService };
|
||||
const $UserListEntityService: Provider = { provide: 'UserListEntityService', useExisting: UserListEntityService };
|
||||
const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService };
|
||||
const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService };
|
||||
|
||||
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
|
||||
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
|
||||
|
|
@ -338,6 +342,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
UserGroupEntityService,
|
||||
UserGroupInvitationEntityService,
|
||||
UserListEntityService,
|
||||
FlashEntityService,
|
||||
FlashLikeEntityService,
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
ApDeliverManagerService,
|
||||
|
|
@ -455,6 +461,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$UserGroupEntityService,
|
||||
$UserGroupInvitationEntityService,
|
||||
$UserListEntityService,
|
||||
$FlashEntityService,
|
||||
$FlashLikeEntityService,
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
$ApDeliverManagerService,
|
||||
|
|
@ -572,6 +580,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
UserGroupEntityService,
|
||||
UserGroupInvitationEntityService,
|
||||
UserListEntityService,
|
||||
FlashEntityService,
|
||||
FlashLikeEntityService,
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
ApDeliverManagerService,
|
||||
|
|
@ -688,6 +698,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$UserGroupEntityService,
|
||||
$UserGroupInvitationEntityService,
|
||||
$UserListEntityService,
|
||||
$FlashEntityService,
|
||||
$FlashLikeEntityService,
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
$ApDeliverManagerService,
|
||||
|
|
|
|||
|
|
@ -47,26 +47,6 @@ function truncateBody<T extends keyof pushNotificationsTypes>(type: T, body: pus
|
|||
return body;
|
||||
}
|
||||
|
||||
function truncateUnreadAntennaNote(notification: pushNotificationsTypes['unreadAntennaNote']): pushNotificationsTypes['unreadAntennaNote'] {
|
||||
if (notification.note) {
|
||||
return {
|
||||
...notification,
|
||||
note: {
|
||||
...notification.note,
|
||||
// textをgetNoteSummaryしたものに置き換える
|
||||
text: getNoteSummary(('type' in notification && notification.type === 'renote') ? notification.note.renote as Packed<'Note'> : notification.note),
|
||||
|
||||
cw: undefined,
|
||||
reply: undefined,
|
||||
renote: undefined,
|
||||
user: undefined as any, // 通知を受け取ったユーザーである場合が多いのでこれも捨てる アンテナの場合も不要なのでいらない
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class PushNotificationService {
|
||||
constructor(
|
||||
|
|
|
|||
55
packages/backend/src/core/entities/FlashEntityService.ts
Normal file
55
packages/backend/src/core/entities/FlashEntityService.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { FlashsRepository, FlashLikesRepository } from '@/models/index.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { Packed } from '@/misc/schema.js';
|
||||
import type { } from '@/models/entities/Blocking.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import type { Flash } from '@/models/entities/Flash.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
||||
@Injectable()
|
||||
export class FlashEntityService {
|
||||
constructor(
|
||||
@Inject(DI.flashsRepository)
|
||||
private flashsRepository: FlashsRepository,
|
||||
|
||||
@Inject(DI.flashLikesRepository)
|
||||
private flashLikesRepository: FlashLikesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async pack(
|
||||
src: Flash['id'] | Flash,
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
): Promise<Packed<'Flash'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: flash.id,
|
||||
createdAt: flash.createdAt.toISOString(),
|
||||
updatedAt: flash.updatedAt.toISOString(),
|
||||
userId: flash.userId,
|
||||
user: this.userEntityService.pack(flash.user ?? flash.userId, me), // { detail: true } すると無限ループするので注意
|
||||
title: flash.title,
|
||||
summary: flash.summary,
|
||||
script: flash.script,
|
||||
likedCount: flash.likedCount,
|
||||
isLiked: meId ? await this.flashLikesRepository.findOneBy({ flashId: flash.id, userId: meId }).then(x => x != null) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packMany(
|
||||
flashs: Flash[],
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
) {
|
||||
return Promise.all(flashs.map(x => this.pack(x, me)));
|
||||
}
|
||||
}
|
||||
|
||||
44
packages/backend/src/core/entities/FlashLikeEntityService.ts
Normal file
44
packages/backend/src/core/entities/FlashLikeEntityService.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { FlashLikesRepository } from '@/models/index.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { Packed } from '@/misc/schema.js';
|
||||
import type { } from '@/models/entities/Blocking.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import type { FlashLike } from '@/models/entities/FlashLike.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
import { FlashEntityService } from './FlashEntityService.js';
|
||||
|
||||
@Injectable()
|
||||
export class FlashLikeEntityService {
|
||||
constructor(
|
||||
@Inject(DI.flashLikesRepository)
|
||||
private flashLikesRepository: FlashLikesRepository,
|
||||
|
||||
private flashEntityService: FlashEntityService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async pack(
|
||||
src: FlashLike['id'] | FlashLike,
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
) {
|
||||
const like = typeof src === 'object' ? src : await this.flashLikesRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: like.id,
|
||||
flash: await this.flashEntityService.pack(like.flash ?? like.flashId, me),
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packMany(
|
||||
likes: any[],
|
||||
me: { id: User['id'] },
|
||||
) {
|
||||
return Promise.all(likes.map(x => this.pack(x, me)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,5 +69,7 @@ export const DI = {
|
|||
adsRepository: Symbol('adsRepository'),
|
||||
passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'),
|
||||
retentionAggregationsRepository: Symbol('retentionAggregationsRepository'),
|
||||
flashsRepository: Symbol('flashsRepository'),
|
||||
flashLikesRepository: Symbol('flashLikesRepository'),
|
||||
//#endregion
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
|||
const dictionary = {
|
||||
'safe-file': FILE_TYPE_BROWSERSAFE,
|
||||
'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml'],
|
||||
'sharp-animation-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml'],
|
||||
};
|
||||
|
||||
export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
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, UserGroup, UserGroupJoining, UserGroupInvitation, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, MessagingMessage, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation } from './index.js';
|
||||
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserGroup, UserGroupJoining, UserGroupInvitation, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, MessagingMessage, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash } from './index.js';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
||||
|
|
@ -388,6 +388,18 @@ const $retentionAggregationsRepository: Provider = {
|
|||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $flashsRepository: Provider = {
|
||||
provide: DI.flashsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(Flash),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $flashLikesRepository: Provider = {
|
||||
provide: DI.flashLikesRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(FlashLike),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
],
|
||||
|
|
@ -456,6 +468,8 @@ const $retentionAggregationsRepository: Provider = {
|
|||
$adsRepository,
|
||||
$passwordResetRequestsRepository,
|
||||
$retentionAggregationsRepository,
|
||||
$flashsRepository,
|
||||
$flashLikesRepository,
|
||||
],
|
||||
exports: [
|
||||
$usersRepository,
|
||||
|
|
@ -522,6 +536,8 @@ const $retentionAggregationsRepository: Provider = {
|
|||
$adsRepository,
|
||||
$passwordResetRequestsRepository,
|
||||
$retentionAggregationsRepository,
|
||||
$flashsRepository,
|
||||
$flashLikesRepository,
|
||||
],
|
||||
})
|
||||
export class RepositoryModule {}
|
||||
|
|
|
|||
60
packages/backend/src/models/entities/Flash.ts
Normal file
60
packages/backend/src/models/entities/Flash.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
|
||||
import { id } from '../id.js';
|
||||
import { User } from './User.js';
|
||||
import { DriveFile } from './DriveFile.js';
|
||||
|
||||
@Entity()
|
||||
export class Flash {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The created date of the Flash.',
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The updated date of the Flash.',
|
||||
})
|
||||
public updatedAt: Date;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256,
|
||||
})
|
||||
public title: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
})
|
||||
public summary: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: 'The ID of author.',
|
||||
})
|
||||
public userId: User['id'];
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: User | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 16384,
|
||||
})
|
||||
public script: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256, array: true, default: '{}',
|
||||
})
|
||||
public permissions: string[];
|
||||
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
})
|
||||
public likedCount: number;
|
||||
}
|
||||
33
packages/backend/src/models/entities/FlashLike.ts
Normal file
33
packages/backend/src/models/entities/FlashLike.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 { Flash } from './Flash.js';
|
||||
|
||||
@Entity()
|
||||
@Index(['userId', 'flashId'], { unique: true })
|
||||
export class FlashLike {
|
||||
@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 flashId: Flash['id'];
|
||||
|
||||
@ManyToOne(type => Flash, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public flash: Flash | null;
|
||||
}
|
||||
|
|
@ -62,6 +62,8 @@ import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js';
|
|||
import { Webhook } from '@/models/entities/Webhook.js';
|
||||
import { Channel } from '@/models/entities/Channel.js';
|
||||
import { RetentionAggregation } from '@/models/entities/RetentionAggregation.js';
|
||||
import { Flash } from '@/models/entities/Flash.js';
|
||||
import { FlashLike } from '@/models/entities/FlashLike.js';
|
||||
import type { Repository } from 'typeorm';
|
||||
|
||||
export {
|
||||
|
|
@ -129,6 +131,8 @@ export {
|
|||
Webhook,
|
||||
Channel,
|
||||
RetentionAggregation,
|
||||
Flash,
|
||||
FlashLike,
|
||||
};
|
||||
|
||||
export type AbuseUserReportsRepository = Repository<AbuseUserReport>;
|
||||
|
|
@ -195,3 +199,5 @@ export type UserSecurityKeysRepository = Repository<UserSecurityKey>;
|
|||
export type WebhooksRepository = Repository<Webhook>;
|
||||
export type ChannelsRepository = Repository<Channel>;
|
||||
export type RetentionAggregationsRepository = Repository<RetentionAggregation>;
|
||||
export type FlashsRepository = Repository<Flash>;
|
||||
export type FlashLikesRepository = Repository<FlashLike>;
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js';
|
|||
import { Webhook } from '@/models/entities/Webhook.js';
|
||||
import { Channel } from '@/models/entities/Channel.js';
|
||||
import { RetentionAggregation } from '@/models/entities/RetentionAggregation.js';
|
||||
import { Flash } from '@/models/entities/Flash.js';
|
||||
import { FlashLike } from '@/models/entities/FlashLike.js';
|
||||
|
||||
import { Config } from '@/config.js';
|
||||
import MisskeyLogger from '@/logger.js';
|
||||
|
|
@ -184,6 +186,8 @@ export const entities = [
|
|||
Webhook,
|
||||
UserIp,
|
||||
RetentionAggregation,
|
||||
Flash,
|
||||
FlashLike,
|
||||
...charts,
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -89,24 +89,33 @@ export class MediaProxyServerService {
|
|||
}
|
||||
}
|
||||
const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image');
|
||||
const isAnimationConvertibleImage = isMimeImage(mime, 'sharp-animation-convertible-image');
|
||||
|
||||
let image: IImageStreamable | null = null;
|
||||
if ('emoji' in request.query && isConvertibleImage) {
|
||||
const data = pipeline(
|
||||
Readable.fromWeb(response.body),
|
||||
sharp({ animated: !('static' in request.query) })
|
||||
.resize({
|
||||
height: 128,
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.webp(webpDefault),
|
||||
);
|
||||
if (!isAnimationConvertibleImage && !('static' in request.query)) {
|
||||
image = {
|
||||
data: Readable.fromWeb(response.body),
|
||||
ext,
|
||||
type: mime,
|
||||
};
|
||||
} else {
|
||||
const data = pipeline(
|
||||
Readable.fromWeb(response.body),
|
||||
sharp({ animated: !('static' in request.query) })
|
||||
.resize({
|
||||
height: 128,
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.webp(webpDefault),
|
||||
);
|
||||
|
||||
image = {
|
||||
data,
|
||||
ext: 'webp',
|
||||
type: 'image/webp',
|
||||
};
|
||||
image = {
|
||||
data,
|
||||
ext: 'webp',
|
||||
type: 'image/webp',
|
||||
};
|
||||
}
|
||||
} else if ('static' in request.query && isConvertibleImage) {
|
||||
image = this.imageProcessingService.convertSharpToWebpStreamObj(Readable.fromWeb(response.body).pipe(sharp()), 498, 280);
|
||||
} else if ('preview' in request.query && isConvertibleImage) {
|
||||
|
|
|
|||
|
|
@ -266,6 +266,15 @@ import * as ep___pages_like from './endpoints/pages/like.js';
|
|||
import * as ep___pages_show from './endpoints/pages/show.js';
|
||||
import * as ep___pages_unlike from './endpoints/pages/unlike.js';
|
||||
import * as ep___pages_update from './endpoints/pages/update.js';
|
||||
import * as ep___flash_create from './endpoints/flash/create.js';
|
||||
import * as ep___flash_delete from './endpoints/flash/delete.js';
|
||||
import * as ep___flash_featured from './endpoints/flash/featured.js';
|
||||
import * as ep___flash_like from './endpoints/flash/like.js';
|
||||
import * as ep___flash_show from './endpoints/flash/show.js';
|
||||
import * as ep___flash_unlike from './endpoints/flash/unlike.js';
|
||||
import * as ep___flash_update from './endpoints/flash/update.js';
|
||||
import * as ep___flash_my from './endpoints/flash/my.js';
|
||||
import * as ep___flash_myLikes from './endpoints/flash/my-likes.js';
|
||||
import * as ep___ping from './endpoints/ping.js';
|
||||
import * as ep___pinnedUsers from './endpoints/pinned-users.js';
|
||||
import * as ep___promo_read from './endpoints/promo/read.js';
|
||||
|
|
@ -587,6 +596,15 @@ const $pages_like: Provider = { provide: 'ep:pages/like', useClass: ep___pages_l
|
|||
const $pages_show: Provider = { provide: 'ep:pages/show', useClass: ep___pages_show.default };
|
||||
const $pages_unlike: Provider = { provide: 'ep:pages/unlike', useClass: ep___pages_unlike.default };
|
||||
const $pages_update: Provider = { provide: 'ep:pages/update', useClass: ep___pages_update.default };
|
||||
const $flash_create: Provider = { provide: 'ep:flash/create', useClass: ep___flash_create.default };
|
||||
const $flash_delete: Provider = { provide: 'ep:flash/delete', useClass: ep___flash_delete.default };
|
||||
const $flash_featured: Provider = { provide: 'ep:flash/featured', useClass: ep___flash_featured.default };
|
||||
const $flash_like: Provider = { provide: 'ep:flash/like', useClass: ep___flash_like.default };
|
||||
const $flash_show: Provider = { provide: 'ep:flash/show', useClass: ep___flash_show.default };
|
||||
const $flash_unlike: Provider = { provide: 'ep:flash/unlike', useClass: ep___flash_unlike.default };
|
||||
const $flash_update: Provider = { provide: 'ep:flash/update', useClass: ep___flash_update.default };
|
||||
const $flash_my: Provider = { provide: 'ep:flash/my', useClass: ep___flash_my.default };
|
||||
const $flash_myLikes: Provider = { provide: 'ep:flash/my-likes', useClass: ep___flash_myLikes.default };
|
||||
const $ping: Provider = { provide: 'ep:ping', useClass: ep___ping.default };
|
||||
const $pinnedUsers: Provider = { provide: 'ep:pinned-users', useClass: ep___pinnedUsers.default };
|
||||
const $promo_read: Provider = { provide: 'ep:promo/read', useClass: ep___promo_read.default };
|
||||
|
|
@ -912,6 +930,15 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$pages_show,
|
||||
$pages_unlike,
|
||||
$pages_update,
|
||||
$flash_create,
|
||||
$flash_delete,
|
||||
$flash_featured,
|
||||
$flash_like,
|
||||
$flash_show,
|
||||
$flash_unlike,
|
||||
$flash_update,
|
||||
$flash_my,
|
||||
$flash_myLikes,
|
||||
$ping,
|
||||
$pinnedUsers,
|
||||
$promo_read,
|
||||
|
|
@ -1231,6 +1258,15 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$pages_show,
|
||||
$pages_unlike,
|
||||
$pages_update,
|
||||
$flash_create,
|
||||
$flash_delete,
|
||||
$flash_featured,
|
||||
$flash_like,
|
||||
$flash_show,
|
||||
$flash_unlike,
|
||||
$flash_update,
|
||||
$flash_my,
|
||||
$flash_myLikes,
|
||||
$ping,
|
||||
$pinnedUsers,
|
||||
$promo_read,
|
||||
|
|
|
|||
|
|
@ -265,6 +265,15 @@ import * as ep___pages_like from './endpoints/pages/like.js';
|
|||
import * as ep___pages_show from './endpoints/pages/show.js';
|
||||
import * as ep___pages_unlike from './endpoints/pages/unlike.js';
|
||||
import * as ep___pages_update from './endpoints/pages/update.js';
|
||||
import * as ep___flash_create from './endpoints/flash/create.js';
|
||||
import * as ep___flash_delete from './endpoints/flash/delete.js';
|
||||
import * as ep___flash_featured from './endpoints/flash/featured.js';
|
||||
import * as ep___flash_like from './endpoints/flash/like.js';
|
||||
import * as ep___flash_show from './endpoints/flash/show.js';
|
||||
import * as ep___flash_unlike from './endpoints/flash/unlike.js';
|
||||
import * as ep___flash_update from './endpoints/flash/update.js';
|
||||
import * as ep___flash_my from './endpoints/flash/my.js';
|
||||
import * as ep___flash_myLikes from './endpoints/flash/my-likes.js';
|
||||
import * as ep___ping from './endpoints/ping.js';
|
||||
import * as ep___pinnedUsers from './endpoints/pinned-users.js';
|
||||
import * as ep___promo_read from './endpoints/promo/read.js';
|
||||
|
|
@ -584,6 +593,15 @@ const eps = [
|
|||
['pages/show', ep___pages_show],
|
||||
['pages/unlike', ep___pages_unlike],
|
||||
['pages/update', ep___pages_update],
|
||||
['flash/create', ep___flash_create],
|
||||
['flash/delete', ep___flash_delete],
|
||||
['flash/featured', ep___flash_featured],
|
||||
['flash/like', ep___flash_like],
|
||||
['flash/show', ep___flash_show],
|
||||
['flash/unlike', ep___flash_unlike],
|
||||
['flash/update', ep___flash_update],
|
||||
['flash/my', ep___flash_my],
|
||||
['flash/my-likes', ep___flash_myLikes],
|
||||
['ping', ep___ping],
|
||||
['pinned-users', ep___pinnedUsers],
|
||||
['promo/read', ep___promo_read],
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j
|
|||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: false,
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
res: {
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
case '-followers': query.orderBy('instance.followersCount', 'ASC'); break;
|
||||
case '+caughtAt': query.orderBy('instance.caughtAt', 'DESC'); break;
|
||||
case '-caughtAt': query.orderBy('instance.caughtAt', 'ASC'); break;
|
||||
case '+latestRequestReceivedAt': query.orderBy('instance.latestRequestReceivedAt', 'DESC'); break;
|
||||
case '-latestRequestReceivedAt': query.orderBy('instance.latestRequestReceivedAt', 'ASC'); break;
|
||||
case '+latestRequestReceivedAt': query.orderBy('instance.latestRequestReceivedAt', 'DESC', 'NULLS LAST'); break;
|
||||
case '-latestRequestReceivedAt': query.orderBy('instance.latestRequestReceivedAt', 'ASC', 'NULLS FIRST'); break;
|
||||
|
||||
default: query.orderBy('instance.id', 'DESC'); break;
|
||||
}
|
||||
|
|
|
|||
66
packages/backend/src/server/api/endpoints/flash/create.ts
Normal file
66
packages/backend/src/server/api/endpoints/flash/create.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import ms from 'ms';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { DriveFilesRepository, FlashsRepository, PagesRepository } from '@/models/index.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { Page } from '@/models/entities/Page.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { PageEntityService } from '@/core/entities/PageEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['flash'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:flash',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 10,
|
||||
},
|
||||
|
||||
errors: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string' },
|
||||
summary: { type: 'string' },
|
||||
script: { type: 'string' },
|
||||
permissions: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
},
|
||||
required: ['title', 'summary', 'script', 'permissions'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.flashsRepository)
|
||||
private flashsRepository: FlashsRepository,
|
||||
|
||||
private flashEntityService: FlashEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const flash = await this.flashsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
userId: me.id,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
title: ps.title,
|
||||
summary: ps.summary,
|
||||
script: ps.script,
|
||||
permissions: ps.permissions,
|
||||
}).then(x => this.flashsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
return await this.flashEntityService.pack(flash);
|
||||
});
|
||||
}
|
||||
}
|
||||
56
packages/backend/src/server/api/endpoints/flash/delete.ts
Normal file
56
packages/backend/src/server/api/endpoints/flash/delete.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { FlashsRepository } 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: ['flashs'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:flash',
|
||||
|
||||
errors: {
|
||||
noSuchFlash: {
|
||||
message: 'No such flash.',
|
||||
code: 'NO_SUCH_FLASH',
|
||||
id: 'de1623ef-bbb3-4289-a71e-14cfa83d9740',
|
||||
},
|
||||
|
||||
accessDenied: {
|
||||
message: 'Access denied.',
|
||||
code: 'ACCESS_DENIED',
|
||||
id: '1036ad7b-9f92-4fff-89c3-0e50dc941704',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
flashId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['flashId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.flashsRepository)
|
||||
private flashsRepository: FlashsRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const flash = await this.flashsRepository.findOneBy({ id: ps.flashId });
|
||||
if (flash == null) {
|
||||
throw new ApiError(meta.errors.noSuchFlash);
|
||||
}
|
||||
if (flash.userId !== me.id) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
await this.flashsRepository.delete(flash.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
48
packages/backend/src/server/api/endpoints/flash/featured.ts
Normal file
48
packages/backend/src/server/api/endpoints/flash/featured.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { FlashsRepository } from '@/models/index.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['flash'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'Flash',
|
||||
},
|
||||
},
|
||||
} 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.flashsRepository)
|
||||
private flashsRepository: FlashsRepository,
|
||||
|
||||
private flashEntityService: FlashEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.flashsRepository.createQueryBuilder('flash')
|
||||
.andWhere('flash.likedCount > 0')
|
||||
.orderBy('flash.likedCount', 'DESC');
|
||||
|
||||
const flashs = await query.take(10).getMany();
|
||||
|
||||
return await this.flashEntityService.packMany(flashs, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
87
packages/backend/src/server/api/endpoints/flash/like.ts
Normal file
87
packages/backend/src/server/api/endpoints/flash/like.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { FlashsRepository, FlashLikesRepository } 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: ['flash'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:flash-likes',
|
||||
|
||||
errors: {
|
||||
noSuchFlash: {
|
||||
message: 'No such flash.',
|
||||
code: 'NO_SUCH_FLASH',
|
||||
id: 'c07c1491-9161-4c5c-9d75-01906f911f73',
|
||||
},
|
||||
|
||||
yourFlash: {
|
||||
message: 'You cannot like your flash.',
|
||||
code: 'YOUR_FLASH',
|
||||
id: '3fd8a0e7-5955-4ba9-85bb-bf3e0c30e13b',
|
||||
},
|
||||
|
||||
alreadyLiked: {
|
||||
message: 'The flash has already been liked.',
|
||||
code: 'ALREADY_LIKED',
|
||||
id: '010065cf-ad43-40df-8067-abff9f4686e3',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
flashId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['flashId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.flashsRepository)
|
||||
private flashsRepository: FlashsRepository,
|
||||
|
||||
@Inject(DI.flashLikesRepository)
|
||||
private flashLikesRepository: FlashLikesRepository,
|
||||
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const flash = await this.flashsRepository.findOneBy({ id: ps.flashId });
|
||||
if (flash == null) {
|
||||
throw new ApiError(meta.errors.noSuchFlash);
|
||||
}
|
||||
|
||||
if (flash.userId === me.id) {
|
||||
throw new ApiError(meta.errors.yourFlash);
|
||||
}
|
||||
|
||||
// if already liked
|
||||
const exist = await this.flashLikesRepository.findOneBy({
|
||||
flashId: flash.id,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
throw new ApiError(meta.errors.alreadyLiked);
|
||||
}
|
||||
|
||||
// Create like
|
||||
await this.flashLikesRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
flashId: flash.id,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
this.flashsRepository.increment({ id: flash.id }, 'likedCount', 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
68
packages/backend/src/server/api/endpoints/flash/my-likes.ts
Normal file
68
packages/backend/src/server/api/endpoints/flash/my-likes.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { FlashLikesRepository } from '@/models/index.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { FlashLikeEntityService } from '@/core/entities/FlashLikeEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account', 'flash'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:flash-likes',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
flash: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'Flash',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.flashLikesRepository)
|
||||
private flashLikesRepository: FlashLikesRepository,
|
||||
|
||||
private flashLikeEntityService: FlashLikeEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.flashLikesRepository.createQueryBuilder('like'), ps.sinceId, ps.untilId)
|
||||
.andWhere('like.userId = :meId', { meId: me.id })
|
||||
.leftJoinAndSelect('like.flash', 'flash');
|
||||
|
||||
const likes = await query
|
||||
.take(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return this.flashLikeEntityService.packMany(likes, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
57
packages/backend/src/server/api/endpoints/flash/my.ts
Normal file
57
packages/backend/src/server/api/endpoints/flash/my.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { FlashsRepository } from '@/models/index.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account', 'flash'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:flash',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'Flash',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.flashsRepository)
|
||||
private flashsRepository: FlashsRepository,
|
||||
|
||||
private flashEntityService: FlashEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.flashsRepository.createQueryBuilder('flash'), ps.sinceId, ps.untilId)
|
||||
.andWhere('flash.userId = :meId', { meId: me.id });
|
||||
|
||||
const flashs = await query
|
||||
.take(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await this.flashEntityService.packMany(flashs);
|
||||
});
|
||||
}
|
||||
}
|
||||
60
packages/backend/src/server/api/endpoints/flash/show.ts
Normal file
60
packages/backend/src/server/api/endpoints/flash/show.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { IsNull } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository, FlashsRepository } from '@/models/index.js';
|
||||
import type { Flash } from '@/models/entities/Flash.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['flashs'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'Flash',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchFlash: {
|
||||
message: 'No such flash.',
|
||||
code: 'NO_SUCH_FLASH',
|
||||
id: 'f0d34a1a-d29a-401d-90ba-1982122b5630',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
flashId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['flashId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.flashsRepository)
|
||||
private flashsRepository: FlashsRepository,
|
||||
|
||||
private flashEntityService: FlashEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const flash = await this.flashsRepository.findOneBy({ id: ps.flashId });
|
||||
|
||||
if (flash == null) {
|
||||
throw new ApiError(meta.errors.noSuchFlash);
|
||||
}
|
||||
|
||||
return await this.flashEntityService.pack(flash, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
68
packages/backend/src/server/api/endpoints/flash/unlike.ts
Normal file
68
packages/backend/src/server/api/endpoints/flash/unlike.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { FlashsRepository, FlashLikesRepository } 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: ['flash'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:flash-likes',
|
||||
|
||||
errors: {
|
||||
noSuchFlash: {
|
||||
message: 'No such flash.',
|
||||
code: 'NO_SUCH_FLASH',
|
||||
id: 'afe8424a-a69e-432d-a5f2-2f0740c62410',
|
||||
},
|
||||
|
||||
notLiked: {
|
||||
message: 'You have not liked that flash.',
|
||||
code: 'NOT_LIKED',
|
||||
id: '755f25a7-9871-4f65-9f34-51eaad9ae0ac',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
flashId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['flashId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.flashsRepository)
|
||||
private flashsRepository: FlashsRepository,
|
||||
|
||||
@Inject(DI.flashLikesRepository)
|
||||
private flashLikesRepository: FlashLikesRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const flash = await this.flashsRepository.findOneBy({ id: ps.flashId });
|
||||
if (flash == null) {
|
||||
throw new ApiError(meta.errors.noSuchFlash);
|
||||
}
|
||||
|
||||
const exist = await this.flashLikesRepository.findOneBy({
|
||||
flashId: flash.id,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
throw new ApiError(meta.errors.notLiked);
|
||||
}
|
||||
|
||||
// Delete like
|
||||
await this.flashLikesRepository.delete(exist.id);
|
||||
|
||||
this.flashsRepository.decrement({ id: flash.id }, 'likedCount', 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
78
packages/backend/src/server/api/endpoints/flash/update.ts
Normal file
78
packages/backend/src/server/api/endpoints/flash/update.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import ms from 'ms';
|
||||
import { Not } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { FlashsRepository, DriveFilesRepository } 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: ['flash'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:flash',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 300,
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchFlash: {
|
||||
message: 'No such flash.',
|
||||
code: 'NO_SUCH_FLASH',
|
||||
id: '611e13d2-309e-419a-a5e4-e0422da39b02',
|
||||
},
|
||||
|
||||
accessDenied: {
|
||||
message: 'Access denied.',
|
||||
code: 'ACCESS_DENIED',
|
||||
id: '08e60c88-5948-478e-a132-02ec701d67b2',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
flashId: { type: 'string', format: 'misskey:id' },
|
||||
title: { type: 'string' },
|
||||
summary: { type: 'string' },
|
||||
script: { type: 'string' },
|
||||
permissions: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
},
|
||||
required: ['flashId', 'title', 'summary', 'script', 'permissions'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.flashsRepository)
|
||||
private flashsRepository: FlashsRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const flash = await this.flashsRepository.findOneBy({ id: ps.flashId });
|
||||
if (flash == null) {
|
||||
throw new ApiError(meta.errors.noSuchFlash);
|
||||
}
|
||||
if (flash.userId !== me.id) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
await this.flashsRepository.update(flash.id, {
|
||||
updatedAt: new Date(),
|
||||
title: ps.title,
|
||||
summary: ps.summary,
|
||||
script: ps.script,
|
||||
permissions: ps.permissions,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ export const meta = {
|
|||
alreadyLiked: {
|
||||
message: 'The page has already been liked.',
|
||||
code: 'ALREADY_LIKED',
|
||||
id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3',
|
||||
id: 'd4c1edbe-7da2-4eae-8714-1acfd2d63941',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
updatedAt: new Date(),
|
||||
title: ps.title,
|
||||
name: ps.name === undefined ? page.name : ps.name,
|
||||
summary: ps.name === undefined ? page.summary : ps.summary,
|
||||
summary: ps.summary === undefined ? page.summary : ps.summary,
|
||||
content: ps.content,
|
||||
variables: ps.variables,
|
||||
script: ps.script,
|
||||
|
|
|
|||
|
|
@ -26,9 +26,10 @@ import { PageEntityService } from '@/core/entities/PageEntityService.js';
|
|||
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
||||
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
|
||||
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
||||
import type { ChannelsRepository, ClipsRepository, EmojisRepository, GalleryPostsRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { ChannelsRepository, ClipsRepository, EmojisRepository, FlashsRepository, GalleryPostsRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import { deepClone } from '@/misc/clone.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
||||
import manifest from './manifest.json' assert { type: 'json' };
|
||||
import { FeedService } from './FeedService.js';
|
||||
import { UrlPreviewService } from './UrlPreviewService.js';
|
||||
|
|
@ -73,6 +74,10 @@ export class ClientServerService {
|
|||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
@Inject(DI.flashsRepository)
|
||||
private flashsRepository: FlashsRepository,
|
||||
|
||||
private flashEntityService: FlashEntityService,
|
||||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private pageEntityService: PageEntityService,
|
||||
|
|
@ -352,7 +357,7 @@ export class ClientServerService {
|
|||
const name = meta.name || 'Misskey';
|
||||
let content = '';
|
||||
content += '<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">';
|
||||
content += `<ShortName>${name} Search</ShortName>`;
|
||||
content += `<ShortName>${name}</ShortName>`;
|
||||
content += `<Description>${name} Search</Description>`;
|
||||
content += '<InputEncoding>UTF-8</InputEncoding>';
|
||||
content += `<Image width="16" height="16" type="image/x-icon">${this.config.url}/favicon.ico</Image>`;
|
||||
|
|
@ -545,6 +550,30 @@ export class ClientServerService {
|
|||
}
|
||||
});
|
||||
|
||||
// Flash
|
||||
fastify.get<{ Params: { id: string; } }>('/play/:id', async (request, reply) => {
|
||||
const flash = await this.flashsRepository.findOneBy({
|
||||
id: request.params.id,
|
||||
});
|
||||
|
||||
if (flash) {
|
||||
const _flash = await this.flashEntityService.pack(flash);
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: flash.userId });
|
||||
const meta = await this.metaService.fetch();
|
||||
reply.header('Cache-Control', 'public, max-age=15');
|
||||
return await reply.view('flash', {
|
||||
flash: _flash,
|
||||
profile,
|
||||
avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: flash.userId })),
|
||||
instanceName: meta.name ?? 'Misskey',
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
});
|
||||
|
||||
// Clip
|
||||
// TODO: 非publicなclipのハンドリング
|
||||
fastify.get<{ Params: { clip: string; } }>('/clips/:clip', async (request, reply) => {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ html
|
|||
link(rel='icon' href= icon || '/favicon.ico')
|
||||
link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png')
|
||||
link(rel='manifest' href='/manifest.json')
|
||||
link(rel='search' type='application/opensearchdescription+xml' title=((title || "Misskey") + " Search") href=`${url}/opensearch.xml`)
|
||||
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`)
|
||||
link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
|
||||
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
|
||||
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
|
||||
|
|
|
|||
31
packages/backend/src/server/web/views/flash.pug
Normal file
31
packages/backend/src/server/web/views/flash.pug
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
extends ./base
|
||||
|
||||
block vars
|
||||
- const user = flash.user;
|
||||
- const title = flash.title;
|
||||
- const url = `${config.url}/play/${flash.id}`;
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
||||
block desc
|
||||
meta(name='description' content= flash.summary)
|
||||
|
||||
block og
|
||||
meta(property='og:type' content='article')
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content= flash.summary)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
|
||||
block meta
|
||||
if profile.noCrawle
|
||||
meta(name='robots' content='noindex')
|
||||
|
||||
meta(name='misskey:user-username' content=user.username)
|
||||
meta(name='misskey:user-id' content=user.id)
|
||||
meta(name='misskey:flash-id' content=flash.id)
|
||||
|
||||
// todo
|
||||
if user.twitter
|
||||
meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
"@rollup/plugin-alias": "4.0.2",
|
||||
"@rollup/plugin-json": "6.0.0",
|
||||
"@rollup/pluginutils": "5.0.2",
|
||||
"@syuilo/aiscript": "0.12.0",
|
||||
"@syuilo/aiscript": "0.12.1",
|
||||
"@tabler/icons": "^1.118.0",
|
||||
"@vitejs/plugin-vue": "4.0.0",
|
||||
"@vue/compiler-sfc": "3.2.45",
|
||||
|
|
@ -20,7 +20,8 @@
|
|||
"blurhash": "2.0.4",
|
||||
"broadcast-channel": "4.19.1",
|
||||
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
||||
"chart.js": "4.1.1",
|
||||
"canvas-confetti": "^1.6.0",
|
||||
"chart.js": "4.1.2",
|
||||
"chartjs-adapter-date-fns": "3.0.0",
|
||||
"chartjs-chart-matrix": "^1.3.0",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
|
|
@ -35,7 +36,7 @@
|
|||
"insert-text-at-cursor": "0.3.0",
|
||||
"is-file-animated": "1.0.2",
|
||||
"json5": "2.2.3",
|
||||
"katex": "0.15.6",
|
||||
"katex": "0.16.4",
|
||||
"matter-js": "0.18.0",
|
||||
"mfm-js": "0.23.0",
|
||||
"misskey-js": "0.0.14",
|
||||
|
|
@ -44,7 +45,7 @@
|
|||
"punycode": "2.1.1",
|
||||
"querystring": "0.2.1",
|
||||
"rndstr": "1.0.0",
|
||||
"rollup": "3.9.0",
|
||||
"rollup": "3.9.1",
|
||||
"s-age": "1.1.2",
|
||||
"sanitize-html": "^2.8.1",
|
||||
"sass": "1.57.1",
|
||||
|
|
@ -55,14 +56,14 @@
|
|||
"textarea-caret": "3.1.0",
|
||||
"three": "0.148.0",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.5.1",
|
||||
"tinycolor2": "1.5.2",
|
||||
"tsc-alias": "1.8.2",
|
||||
"tsconfig-paths": "4.1.2",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typescript": "4.9.4",
|
||||
"uuid": "9.0.0",
|
||||
"vanilla-tilt": "1.8.0",
|
||||
"vite": "4.0.3",
|
||||
"vite": "4.0.4",
|
||||
"vue": "3.2.45",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vuedraggable": "next"
|
||||
|
|
@ -72,7 +73,7 @@
|
|||
"@types/glob": "8.0.0",
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@types/katex": "0.14.0",
|
||||
"@types/katex": "0.16.0",
|
||||
"@types/matter-js": "0.18.2",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/sanitize-html": "^2.8.0",
|
||||
|
|
@ -82,16 +83,16 @@
|
|||
"@types/uuid": "9.0.0",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.47.1",
|
||||
"@typescript-eslint/parser": "5.47.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.48.0",
|
||||
"@typescript-eslint/parser": "5.48.0",
|
||||
"@vue/runtime-core": "3.2.45",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.2.0",
|
||||
"cypress": "12.3.0",
|
||||
"eslint": "8.31.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-vue": "9.8.0",
|
||||
"start-server-and-test": "1.15.2",
|
||||
"vue-eslint-parser": "^9.1.0",
|
||||
"vue-tsc": "^1.0.19"
|
||||
"vue-tsc": "^1.0.22"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ import { del, get, set } from '@/scripts/idb-proxy';
|
|||
import { apiUrl } from '@/config';
|
||||
import { waiting, api, popup, popupMenu, success, alert } from '@/os';
|
||||
import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
|
||||
import { miLocalStorage } from './local-storage';
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
type Account = misskey.entities.MeDetailed;
|
||||
|
||||
const accountData = localStorage.getItem('account');
|
||||
const accountData = miLocalStorage.getItem('account');
|
||||
|
||||
// TODO: 外部からはreadonlyに
|
||||
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
|
||||
|
|
@ -21,7 +22,7 @@ export const iAmAdmin = $i != null && $i.isAdmin;
|
|||
|
||||
export async function signout() {
|
||||
waiting();
|
||||
localStorage.removeItem('account');
|
||||
miLocalStorage.removeItem('account');
|
||||
|
||||
await removeAccount($i.id);
|
||||
|
||||
|
|
@ -119,7 +120,7 @@ export function updateAccount(accountData) {
|
|||
for (const [key, value] of Object.entries(accountData)) {
|
||||
$i[key] = value;
|
||||
}
|
||||
localStorage.setItem('account', JSON.stringify($i));
|
||||
miLocalStorage.setItem('account', JSON.stringify($i));
|
||||
}
|
||||
|
||||
export function refreshAccount() {
|
||||
|
|
@ -130,7 +131,7 @@ export async function login(token: Account['token'], redirect?: string) {
|
|||
waiting();
|
||||
if (_DEV_) console.log('logging as token ', token);
|
||||
const me = await fetchAccount(token);
|
||||
localStorage.setItem('account', JSON.stringify(me));
|
||||
miLocalStorage.setItem('account', JSON.stringify(me));
|
||||
document.cookie = `token=${token}; path=/; max-age=31536000`; // bull dashboardの認証とかで使う
|
||||
await addAccount(me.id, token);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="bcekxzvu _gap _panel">
|
||||
<div class="bcekxzvu _margin _panel">
|
||||
<div class="target">
|
||||
<MkA v-user-preview="report.targetUserId" class="info" :to="`/user-info/${report.targetUserId}`">
|
||||
<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true" :disable-link="true"/>
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
|
||||
</div>
|
||||
</MkA>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.registeredDate }}</template>
|
||||
<template #value>{{ dateString(report.targetUser.createdAt) }} (<MkTime :time="report.targetUser.createdAt"/>)</template>
|
||||
</MkKeyValue>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XWindow ref="uiWindow" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
|
||||
<MkWindow ref="uiWindow" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
|
||||
<template #header>
|
||||
<i class="ti ti-exclamation-circle" style="margin-right: 0.5em;"></i>
|
||||
<I18n :src="i18n.ts.reportAbuseOf" tag="span">
|
||||
|
|
@ -8,24 +8,26 @@
|
|||
</template>
|
||||
</I18n>
|
||||
</template>
|
||||
<div class="dpvffvvy _monolithic_">
|
||||
<div class="_section">
|
||||
<MkTextarea v-model="comment">
|
||||
<template #label>{{ i18n.ts.details }}</template>
|
||||
<template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template>
|
||||
</MkTextarea>
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<div class="dpvffvvy _gaps_m">
|
||||
<div class="">
|
||||
<MkTextarea v-model="comment">
|
||||
<template #label>{{ i18n.ts.details }}</template>
|
||||
<template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template>
|
||||
</MkTextarea>
|
||||
</div>
|
||||
<div class="">
|
||||
<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_section">
|
||||
<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</XWindow>
|
||||
</MkSpacer>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XWindow from '@/components/MkWindow.vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
|
|
@ -40,7 +42,7 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const uiWindow = shallowRef<InstanceType<typeof XWindow>>();
|
||||
const uiWindow = shallowRef<InstanceType<typeof MkWindow>>();
|
||||
const comment = ref(props.initialComment || '');
|
||||
|
||||
function send() {
|
||||
|
|
|
|||
114
packages/frontend/src/components/MkAsUi.vue
Normal file
114
packages/frontend/src/components/MkAsUi.vue
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="c.type === 'root'" :class="$style.root">
|
||||
<template v-for="child in c.children" :key="child">
|
||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
||||
</template>
|
||||
</div>
|
||||
<span v-else-if="c.type === 'text'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, fontWeight: c.bold ? 'bold' : null, color: c.color ?? null }">{{ c.text }}</span>
|
||||
<Mfm v-else-if="c.type === 'mfm'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, color: c.color ?? null }" :text="c.text"/>
|
||||
<MkButton v-else-if="c.type === 'button'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" @click="c.onClick">{{ c.text }}</MkButton>
|
||||
<div v-else-if="c.type === 'buttons'" class="_buttons">
|
||||
<MkButton v-for="button in c.buttons" :primary="button.primary" :rounded="button.rounded" :small="size === 'small'" @click="button.onClick">{{ button.text }}</MkButton>
|
||||
</div>
|
||||
<MkSwitch v-else-if="c.type === 'switch'" :model-value="valueForSwitch" @update:model-value="onSwitchUpdate">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
</MkSwitch>
|
||||
<MkTextarea v-else-if="c.type === 'textarea'" :model-value="c.default" @update:model-value="c.onInput">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
</MkTextarea>
|
||||
<MkInput v-else-if="c.type === 'textInput'" :small="size === 'small'" :model-value="c.default" @update:model-value="c.onInput">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-else-if="c.type === 'numberInput'" :small="size === 'small'" :model-value="c.default" type="number" @update:model-value="c.onInput">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
</MkInput>
|
||||
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :model-value="c.default" @update:model-value="c.onChange">
|
||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
|
||||
</MkSelect>
|
||||
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" @click="openPostForm">{{ c.text }}</MkButton>
|
||||
<FormFolder v-else-if="c.type === 'folder'" :default-open="c.opened">
|
||||
<template #label>{{ c.title }}</template>
|
||||
<template v-for="child in c.children" :key="child">
|
||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
||||
</template>
|
||||
</FormFolder>
|
||||
<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace', [$style.containerCenter]: c.align === 'center' }]" :style="{ backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
|
||||
<template v-for="child in c.children" :key="child">
|
||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, Ref } from 'vue';
|
||||
import * as os from '@/os';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import { AsUiComponent } from '@/scripts/aiscript/ui';
|
||||
import FormFolder from '@/components/form/folder.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
component: AsUiComponent;
|
||||
components: Ref<AsUiComponent>[];
|
||||
size: 'small' | 'medium' | 'large';
|
||||
}>(), {
|
||||
size: 'medium',
|
||||
});
|
||||
|
||||
const c = props.component;
|
||||
|
||||
function g(id) {
|
||||
return props.components.find(x => x.value.id === id).value;
|
||||
}
|
||||
|
||||
let valueForSwitch = $ref(c.default ?? false);
|
||||
|
||||
function onSwitchUpdate(v) {
|
||||
valueForSwitch = v;
|
||||
if (c.onChange) c.onChange(v);
|
||||
}
|
||||
|
||||
function openPostForm() {
|
||||
os.post({
|
||||
initialText: c.form.text,
|
||||
instant: true,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.containerCenter {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fontSerif {
|
||||
font-family: serif;
|
||||
}
|
||||
|
||||
.fontMonospace {
|
||||
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -46,6 +46,7 @@ import { defaultStore } from '@/store';
|
|||
import { emojilist } from '@/scripts/emojilist';
|
||||
import { instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
|
||||
type EmojiDef = {
|
||||
emoji: string;
|
||||
|
|
@ -208,7 +209,7 @@ function exec() {
|
|||
}
|
||||
} else if (props.type === 'hashtag') {
|
||||
if (!props.q || props.q === '') {
|
||||
hashtags.value = JSON.parse(localStorage.getItem('hashtags') || '[]');
|
||||
hashtags.value = JSON.parse(miLocalStorage.getItem('hashtags') || '[]');
|
||||
fetching.value = false;
|
||||
} else {
|
||||
const cacheKey = `autocomplete:hashtag:${props.q}`;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<button
|
||||
v-if="!link"
|
||||
ref="el" class="bghgjjyj _button"
|
||||
:class="{ inline, primary, gradate, danger, rounded, full, small }"
|
||||
:class="{ inline, primary, gradate, danger, rounded, full, small, large, asLike }"
|
||||
:type="type"
|
||||
@click="emit('click', $event)"
|
||||
@mousedown="onMousedown"
|
||||
|
|
@ -41,6 +41,8 @@ const props = defineProps<{
|
|||
danger?: boolean;
|
||||
full?: boolean;
|
||||
small?: boolean;
|
||||
large?: boolean;
|
||||
asLike?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -131,6 +133,11 @@ function onMousedown(evt: MouseEvent): void {
|
|||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: 100%;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
&.full {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -153,6 +160,37 @@ function onMousedown(evt: MouseEvent): void {
|
|||
}
|
||||
}
|
||||
|
||||
&.asLike {
|
||||
background: rgba(255, 86, 125, 0.07);
|
||||
color: #ff002f;
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background: rgba(255, 74, 116, 0.11);
|
||||
}
|
||||
|
||||
&:not(:disabled):active {
|
||||
background: rgba(224, 57, 96, 0.125);
|
||||
}
|
||||
|
||||
> .ripples {
|
||||
::v-deep(div) {
|
||||
background: rgba(255, 60, 106, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background: rgb(241 97 132);
|
||||
|
||||
&:not(:disabled):hover {
|
||||
background: rgb(241 92 128);
|
||||
}
|
||||
|
||||
&:not(:disabled):active {
|
||||
background: rgb(241 92 128);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.gradate {
|
||||
font-weight: bold;
|
||||
color: var(--fgOnAccent) !important;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
*/
|
||||
import { onMounted, ref, shallowRef, watch, PropType, onUnmounted } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import { enUS } from 'date-fns/locale';
|
||||
import gradient from 'chartjs-plugin-gradient';
|
||||
import * as os from '@/os';
|
||||
import { defaultStore } from '@/store';
|
||||
|
|
@ -186,6 +185,10 @@ const render = () => {
|
|||
time: {
|
||||
stepSize: 1,
|
||||
unit: props.span === 'day' ? 'month' : 'day',
|
||||
displayFormats: {
|
||||
day: 'M/d',
|
||||
month: 'Y/M',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
},
|
||||
|
|
@ -194,11 +197,6 @@ const render = () => {
|
|||
maxRotation: 0,
|
||||
autoSkipPadding: 16,
|
||||
},
|
||||
adapters: {
|
||||
date: {
|
||||
locale: enUS,
|
||||
},
|
||||
},
|
||||
min: getDate(props.limit).getTime(),
|
||||
},
|
||||
y: {
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ defineExpose({
|
|||
|
||||
&.disabled {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.6;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
> .box {
|
||||
|
|
@ -72,4 +72,11 @@ defineExpose({
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 500px) {
|
||||
.root {
|
||||
font-size: 90%;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XModalWindow
|
||||
<MkModalWindow
|
||||
ref="dialogEl"
|
||||
:width="800"
|
||||
:height="500"
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</XModalWindow>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -30,7 +30,7 @@ import { nextTick, onMounted } from 'vue';
|
|||
import * as misskey from 'misskey-js';
|
||||
import Cropper from 'cropperjs';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
import { defaultStore } from '@/store';
|
||||
|
|
@ -50,7 +50,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const imgUrl = getProxiedImageUrl(props.file.url);
|
||||
let dialogEl = $shallowRef<InstanceType<typeof XModalWindow>>();
|
||||
let dialogEl = $shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
let imgEl = $shallowRef<HTMLImageElement>();
|
||||
let cropper: Cropper | null = null;
|
||||
let loading = $ref(true);
|
||||
|
|
|
|||
109
packages/frontend/src/components/MkDonation.vue
Normal file
109
packages/frontend/src/components/MkDonation.vue
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<template>
|
||||
<div class="_panel _shadow" :class="$style.root">
|
||||
<!-- TODO: インスタンス運営者が任意のテキストとリンクを設定できるようにする -->
|
||||
<div :class="$style.icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-pig-money" width="40" height="40" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M15 11v.01"></path>
|
||||
<path d="M5.173 8.378a3 3 0 1 1 4.656 -1.377"></path>
|
||||
<path d="M16 4v3.803a6.019 6.019 0 0 1 2.658 3.197h1.341a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-1.342c-.336 .95 -.907 1.8 -1.658 2.473v2.027a1.5 1.5 0 0 1 -3 0v-.583a6.04 6.04 0 0 1 -1 .083h-4a6.04 6.04 0 0 1 -1 -.083v.583a1.5 1.5 0 0 1 -3 0v-2l.001 -.027a6 6 0 0 1 3.999 -10.473h2.5l4.5 -3h.001z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div :class="$style.main">
|
||||
<div :class="$style.title">{{ i18n.ts.didYouLikeMisskey }}</div>
|
||||
<div :class="$style.text">
|
||||
<I18n :src="i18n.ts.pleaseDonate" tag="span">
|
||||
<template #host>
|
||||
{{ $instance.name ?? host }}
|
||||
</template>
|
||||
</I18n>
|
||||
<div style="margin-top: 0.2em;">
|
||||
<MkLink target="_blank" url="https://misskey-hub.net/docs/donate.html">{{ i18n.ts.learnMore }}</MkLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_buttons">
|
||||
<MkButton @click="close">{{ i18n.ts.remindMeLater }}</MkButton>
|
||||
<MkButton @click="neverShow">{{ i18n.ts.neverShow }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<button class="_button" :class="$style.close" @click="close"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, shallowRef } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import { host } from '@/config';
|
||||
import { i18n } from '@/i18n';
|
||||
import * as os from '@/os';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const zIndex = os.claimZIndex('low');
|
||||
|
||||
function close() {
|
||||
miLocalStorage.setItem('latestDonationInfoShownAt', Date.now().toString());
|
||||
emit('closed');
|
||||
}
|
||||
|
||||
function neverShow() {
|
||||
miLocalStorage.setItem('neverShowDonationInfo', 'true')
|
||||
close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: fixed;
|
||||
z-index: v-bind(zIndex);
|
||||
bottom: var(--margin);
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
width: calc(100% - (var(--margin) * 2));
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.icon {
|
||||
text-align: center;
|
||||
padding-top: 25px;
|
||||
width: 100px;
|
||||
color: var(--accent);
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
.icon {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
.icon {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 25px 25px 25px 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 0.7em 0 1em 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XModalWindow
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="800"
|
||||
:height="500"
|
||||
|
|
@ -15,14 +15,14 @@
|
|||
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
|
||||
</template>
|
||||
<XDrive :multiple="multiple" :select="type" @change-selection="onChangeSelection" @selected="ok()"/>
|
||||
</XModalWindow>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XDrive from '@/components/MkDrive.vue';
|
||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import number from '@/filters/number';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const dialog = shallowRef<InstanceType<typeof XModalWindow>>();
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
|
||||
const selected = ref<Misskey.entities.DriveFile[]>([]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XWindow
|
||||
<MkWindow
|
||||
ref="window"
|
||||
:initial-width="800"
|
||||
:initial-height="500"
|
||||
|
|
@ -10,14 +10,14 @@
|
|||
{{ i18n.ts.drive }}
|
||||
</template>
|
||||
<XDrive :initial-folder="initialFolder"/>
|
||||
</XWindow>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XDrive from '@/components/MkDrive.vue';
|
||||
import XWindow from '@/components/MkWindow.vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
defineProps<{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XModalWindow
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
:height="450"
|
||||
|
|
@ -16,13 +16,13 @@
|
|||
<template #label>{{ i18n.ts.caption }}</template>
|
||||
</MkTextarea>
|
||||
</MkSpacer>
|
||||
</XModalWindow>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
|
@ -37,7 +37,7 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
|
||||
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
|
||||
let caption = $ref(props.default);
|
||||
|
||||
|
|
|
|||
|
|
@ -54,8 +54,6 @@ const props = defineProps<{
|
|||
}
|
||||
|
||||
.urempief {
|
||||
margin-top: var(--margin);
|
||||
|
||||
&.list {
|
||||
> .file {
|
||||
display: flex;
|
||||
|
|
@ -89,7 +87,6 @@ const props = defineProps<{
|
|||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||
grid-gap: 12px;
|
||||
margin: var(--margin) 0;
|
||||
|
||||
> .file {
|
||||
position: relative;
|
||||
|
|
|
|||
112
packages/frontend/src/components/MkFlashPreview.vue
Normal file
112
packages/frontend/src/components/MkFlashPreview.vue
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<template>
|
||||
<MkA :to="`/play/${flash.id}`" class="vhpxefrk _panel" tabindex="-1">
|
||||
<article>
|
||||
<header>
|
||||
<h1 :title="flash.title">{{ flash.title }}</h1>
|
||||
</header>
|
||||
<p v-if="flash.summary" :title="flash.summary">{{ flash.summary.length > 85 ? flash.summary.slice(0, 85) + '…' : flash.summary }}</p>
|
||||
<footer>
|
||||
<img class="icon" :src="flash.user.avatarUrl"/>
|
||||
<p>{{ userName(flash.user) }}</p>
|
||||
</footer>
|
||||
</article>
|
||||
</MkA>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import { userName } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
|
||||
const props = defineProps<{
|
||||
//flash: misskey.entities.Flash;
|
||||
flash: any;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vhpxefrk {
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
> article {
|
||||
padding: 16px;
|
||||
|
||||
> header {
|
||||
margin-bottom: 8px;
|
||||
|
||||
> h1 {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
color: var(--urlPreviewTitle);
|
||||
}
|
||||
}
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
color: var(--urlPreviewText);
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
> footer {
|
||||
margin-top: 8px;
|
||||
height: 16px;
|
||||
|
||||
> img {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
> p {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
color: var(--urlPreviewInfo);
|
||||
font-size: 0.8em;
|
||||
line-height: 16px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
font-size: 12px;
|
||||
|
||||
> article {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
font-size: 10px;
|
||||
|
||||
> article {
|
||||
padding: 8px;
|
||||
|
||||
> header {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
> footer {
|
||||
margin-top: 4px;
|
||||
|
||||
> img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -25,8 +25,9 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
|
||||
const localStoragePrefix = 'ui:folder:';
|
||||
const miLocalStoragePrefix = 'ui:folder:' as const;
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
|
|
@ -44,13 +45,13 @@ export default defineComponent({
|
|||
data() {
|
||||
return {
|
||||
bg: null,
|
||||
showBody: (this.persistKey && localStorage.getItem(localStoragePrefix + this.persistKey)) ? localStorage.getItem(localStoragePrefix + this.persistKey) === 't' : this.expanded,
|
||||
showBody: (this.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`) === 't') : this.expanded,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
showBody() {
|
||||
if (this.persistKey) {
|
||||
localStorage.setItem(localStoragePrefix + this.persistKey, this.showBody ? 't' : 'f');
|
||||
miLocalStorage.setItem(`${miLocalStoragePrefix}${this.persistKey}`, this.showBody ? 't' : 'f');
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<XModalWindow ref="dialog"
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="370"
|
||||
:height="400"
|
||||
@close="dialog.close()"
|
||||
|
|
@ -8,18 +9,18 @@
|
|||
<template #header>{{ i18n.ts.forgotPassword }}</template>
|
||||
|
||||
<form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
|
||||
<div class="main _formRoot">
|
||||
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required>
|
||||
<div class="main _gaps_m">
|
||||
<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required>
|
||||
<template #label>{{ i18n.ts.username }}</template>
|
||||
<template #prefix>@</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="email" class="_formBlock" type="email" :spellcheck="false" required>
|
||||
<MkInput v-model="email" type="email" :spellcheck="false" required>
|
||||
<template #label>{{ i18n.ts.emailAddress }}</template>
|
||||
<template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.ts.send }}</MkButton>
|
||||
<MkButton type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.ts.send }}</MkButton>
|
||||
</div>
|
||||
<div class="sub">
|
||||
<MkA to="/about" class="_link">{{ i18n.ts._forgotPassword.ifNoEmail }}</MkA>
|
||||
|
|
@ -28,12 +29,12 @@
|
|||
<div v-else class="bafecedb">
|
||||
{{ i18n.ts._forgotPassword.contactAdmin }}
|
||||
</div>
|
||||
</XModalWindow>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import * as os from '@/os';
|
||||
|
|
@ -45,7 +46,7 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
let dialog: InstanceType<typeof XModalWindow> = $ref();
|
||||
let dialog: InstanceType<typeof MkModalWindow> = $ref();
|
||||
|
||||
let username = $ref('');
|
||||
let email = $ref('');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XModalWindow
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="450"
|
||||
:can-close="false"
|
||||
|
|
@ -15,43 +15,43 @@
|
|||
</template>
|
||||
|
||||
<MkSpacer :margin-min="20" :margin-max="32">
|
||||
<div class="xkpnjxcv _formRoot">
|
||||
<div class="xkpnjxcv _gaps_m">
|
||||
<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
|
||||
<FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1" class="_formBlock">
|
||||
<FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text" class="_formBlock">
|
||||
<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text">
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||
</FormInput>
|
||||
<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]" class="_formBlock">
|
||||
<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]">
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||
</FormTextarea>
|
||||
<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]" class="_formBlock">
|
||||
<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
|
||||
<span v-text="form[item].label || item"></span>
|
||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||
</FormSwitch>
|
||||
<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]" class="_formBlock">
|
||||
<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||
<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
|
||||
</FormSelect>
|
||||
<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]" class="_formBlock">
|
||||
<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||
<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
|
||||
</FormRadios>
|
||||
<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter" class="_formBlock">
|
||||
<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter">
|
||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
|
||||
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
|
||||
</FormRange>
|
||||
<MkButton v-else-if="form[item].type === 'button'" class="_formBlock" @click="form[item].action($event, values)">
|
||||
<MkButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)">
|
||||
<span v-text="form[item].content || item"></span>
|
||||
</MkButton>
|
||||
</template>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</XModalWindow>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
@ -63,11 +63,11 @@ import FormSelect from './form/select.vue';
|
|||
import FormRange from './form/range.vue';
|
||||
import MkButton from './MkButton.vue';
|
||||
import FormRadios from './form/radios.vue';
|
||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XModalWindow,
|
||||
MkModalWindow,
|
||||
FormInput,
|
||||
FormTextarea,
|
||||
FormSwitch,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
<script lang="ts" setup>
|
||||
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import { enUS } from 'date-fns/locale';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
|
||||
import * as os from '@/os';
|
||||
|
|
@ -149,7 +148,9 @@ async function renderChart() {
|
|||
round: 'week',
|
||||
isoWeekday: 0,
|
||||
displayFormats: {
|
||||
week: 'MMM dd',
|
||||
day: 'M/d',
|
||||
month: 'Y/M',
|
||||
week: 'M/d',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const menu = defaultStore.state.menu;
|
|||
|
||||
const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k => navbarItemDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
|
||||
type: def.to ? 'link' : 'button',
|
||||
text: i18n.ts[def.title],
|
||||
text: def.title,
|
||||
icon: def.icon,
|
||||
to: def.to,
|
||||
action: def.action,
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@ onBeforeUnmount(() => {
|
|||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
|
|
|||
|
|
@ -383,7 +383,6 @@ defineExpose({
|
|||
mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%);
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
container-type: inline-size;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
padding: 16px;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
|
||||
<div ref="rootEl" class="hrmcaedk _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
|
||||
<div ref="rootEl" class="hrmcaedk" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
|
||||
<div class="header" @contextmenu="onContextmenu">
|
||||
<button v-if="history.length > 0" v-tooltip="$ts.goBack" class="_button" @click="back()"><i class="ti ti-arrow-left"></i></button>
|
||||
<span v-else style="display: inline-block; width: 20px"></span>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')">
|
||||
<div ref="rootEl" class="ebkgoccj _narrow_" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
|
||||
<div ref="rootEl" class="ebkgoccj" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
|
||||
<div ref="headerEl" class="header">
|
||||
<button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
|
||||
<span class="title">
|
||||
|
|
@ -89,6 +89,7 @@ defineExpose({
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
contain: content;
|
||||
container-type: inline-size;
|
||||
border-radius: var(--radius);
|
||||
|
||||
--root-margin: 24px;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
v-show="!isDeleted"
|
||||
ref="el"
|
||||
v-hotkey="keymap"
|
||||
class="lxwezrsl _block"
|
||||
class="lxwezrsl"
|
||||
:tabindex="!isDeleted ? '-1' : null"
|
||||
:class="{ renote: isRenote }"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XModalWindow
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
:height="450"
|
||||
|
|
@ -12,24 +12,24 @@
|
|||
<template #header>{{ i18n.ts.notificationSetting }}</template>
|
||||
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<div class="_formRoot">
|
||||
<div class="_gaps_m">
|
||||
<template v-if="showGlobalToggle">
|
||||
<MkSwitch v-model="useGlobalSetting" class="_formBlock">
|
||||
<MkSwitch v-model="useGlobalSetting">
|
||||
{{ i18n.ts.useGlobalSetting }}
|
||||
<template #caption>{{ i18n.ts.useGlobalSettingDesc }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
<template v-if="!useGlobalSetting">
|
||||
<MkInfo class="_formBlock">{{ i18n.ts.notificationSettingDesc }}</MkInfo>
|
||||
<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
|
||||
<MkInfo>{{ i18n.ts.notificationSettingDesc }}</MkInfo>
|
||||
<div class="_buttons">
|
||||
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
|
||||
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
|
||||
</div>
|
||||
<MkSwitch v-for="ntype in notificationTypes" class="_formBlock" :key="ntype" v-model="typesMap[ntype]">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch>
|
||||
<MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype]">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch>
|
||||
</template>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</XModalWindow>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -38,7 +38,7 @@ import { notificationTypes } from 'misskey-js';
|
|||
import MkSwitch from './form/switch.vue';
|
||||
import MkInfo from './MkInfo.vue';
|
||||
import MkButton from './MkButton.vue';
|
||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{
|
|||
|
||||
let includingTypes = $computed(() => props.includingTypes || []);
|
||||
|
||||
const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
|
||||
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
|
||||
let typesMap = $ref<Record<typeof notificationTypes[number], boolean>>({});
|
||||
let useGlobalSetting = $ref((includingTypes === null || includingTypes.length === 0) && props.showGlobalToggle);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj _block" tabindex="-1">
|
||||
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1">
|
||||
<div v-if="page.eyeCatchingImage" class="thumbnail" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
|
||||
<article>
|
||||
<header>
|
||||
|
|
@ -14,22 +14,15 @@
|
|||
</MkA>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import { userName } from '@/filters/user';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
page: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
userName,
|
||||
},
|
||||
});
|
||||
const props = defineProps<{
|
||||
page: misskey.entities.Page;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XWindow
|
||||
<MkWindow
|
||||
ref="windowEl"
|
||||
:initial-width="500"
|
||||
:initial-height="500"
|
||||
|
|
@ -20,13 +20,13 @@
|
|||
<div class="yrolvcoq" :style="{ background: pageMetadata?.value?.bg }" style="container-type: inline-size;">
|
||||
<RouterView :router="router"/>
|
||||
</div>
|
||||
</XWindow>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ComputedRef, inject, provide } from 'vue';
|
||||
import RouterView from '@/components/global/RouterView.vue';
|
||||
import XWindow from '@/components/MkWindow.vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import { popout as _popout } from '@/scripts/popout';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import { url } from '@/config';
|
||||
|
|
@ -47,7 +47,7 @@ defineEmits<{
|
|||
const router = new Router(routes, props.initialPath);
|
||||
|
||||
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
|
||||
let windowEl = $shallowRef<InstanceType<typeof XWindow>>();
|
||||
let windowEl = $shallowRef<InstanceType<typeof MkWindow>>();
|
||||
const history = $ref<{ path: string; key: any; }[]>([{
|
||||
path: router.getCurrentPath(),
|
||||
key: router.getCurrentKey(),
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@
|
|||
</div>
|
||||
|
||||
<div v-else ref="rootEl">
|
||||
<div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _gap">
|
||||
<div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _margin">
|
||||
<MkButton v-if="!moreFetching" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMoreAhead">
|
||||
{{ i18n.ts.loadMore }}
|
||||
</MkButton>
|
||||
<MkLoading v-else class="loading"/>
|
||||
</div>
|
||||
<slot :items="items"></slot>
|
||||
<div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _gap">
|
||||
<div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _margin">
|
||||
<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
|
||||
{{ i18n.ts.loadMore }}
|
||||
</MkButton>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')">
|
||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @close="emit('closing')" @closed="emit('closed')">
|
||||
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
|
@ -20,6 +20,7 @@ defineProps<{
|
|||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
(ev: 'closing'): void;
|
||||
}>();
|
||||
|
||||
let modal = $shallowRef<InstanceType<typeof MkModal>>();
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account'
|
|||
import { uploadFile } from '@/scripts/upload';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
|
||||
const modal = inject('modal');
|
||||
|
||||
|
|
@ -156,7 +157,7 @@ let autocomplete = $ref(null);
|
|||
let draghover = $ref(false);
|
||||
let quoteId = $ref(null);
|
||||
let hasNotSpecifiedMentions = $ref(false);
|
||||
let recentHashtags = $ref(JSON.parse(localStorage.getItem('hashtags') || '[]'));
|
||||
let recentHashtags = $ref(JSON.parse(miLocalStorage.getItem('hashtags') || '[]'));
|
||||
let imeText = $ref('');
|
||||
|
||||
const typing = throttle(3000, () => {
|
||||
|
|
@ -543,7 +544,7 @@ function onDrop(ev): void {
|
|||
}
|
||||
|
||||
function saveDraft() {
|
||||
const draftData = JSON.parse(localStorage.getItem('drafts') || '{}');
|
||||
const draftData = JSON.parse(miLocalStorage.getItem('drafts') || '{}');
|
||||
|
||||
draftData[draftKey] = {
|
||||
updatedAt: new Date(),
|
||||
|
|
@ -558,15 +559,15 @@ function saveDraft() {
|
|||
},
|
||||
};
|
||||
|
||||
localStorage.setItem('drafts', JSON.stringify(draftData));
|
||||
miLocalStorage.setItem('drafts', JSON.stringify(draftData));
|
||||
}
|
||||
|
||||
function deleteDraft() {
|
||||
const draftData = JSON.parse(localStorage.getItem('drafts') ?? '{}');
|
||||
const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
|
||||
|
||||
delete draftData[draftKey];
|
||||
|
||||
localStorage.setItem('drafts', JSON.stringify(draftData));
|
||||
miLocalStorage.setItem('drafts', JSON.stringify(draftData));
|
||||
}
|
||||
|
||||
async function post(ev?: MouseEvent) {
|
||||
|
|
@ -622,8 +623,8 @@ async function post(ev?: MouseEvent) {
|
|||
emit('posted');
|
||||
if (postData.text && postData.text !== '') {
|
||||
const hashtags_ = mfm.parse(postData.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
|
||||
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
|
||||
localStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
|
||||
const history = JSON.parse(miLocalStorage.getItem('hashtags') || '[]') as string[];
|
||||
miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
|
||||
}
|
||||
posting = false;
|
||||
postAccount = null;
|
||||
|
|
@ -698,7 +699,7 @@ onMounted(() => {
|
|||
nextTick(() => {
|
||||
// 書きかけの投稿を復元
|
||||
if (!props.instant && !props.mention && !props.specified) {
|
||||
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[draftKey];
|
||||
const draft = JSON.parse(miLocalStorage.getItem('drafts') || '{}')[draftKey];
|
||||
if (draft) {
|
||||
text = draft.data.text;
|
||||
useCw = draft.data.useCw;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="jmgmzlwq _block"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserCaution }}<a class="link" :href="href" rel="nofollow noopener" target="_blank">{{ i18n.ts.showOnRemote }}</a></div>
|
||||
<div class="jmgmzlwq"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserCaution }}<a class="link" :href="href" rel="nofollow noopener" target="_blank">{{ i18n.ts.showOnRemote }}</a></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
<script lang="ts" setup>
|
||||
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import { enUS } from 'date-fns/locale';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
|
||||
import * as os from '@/os';
|
||||
|
|
@ -40,7 +39,7 @@ async function renderChart() {
|
|||
const wide = rootEl.offsetWidth > 600;
|
||||
const narrow = rootEl.offsetWidth < 400;
|
||||
|
||||
const maxDays = wide ? 20 : narrow ? 7 : 14;
|
||||
const maxDays = wide ? 15 : narrow ? 5 : 10;
|
||||
|
||||
const raw = await os.api('retention', { });
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="_card">
|
||||
<div class="_content">
|
||||
<div class="">
|
||||
<div class="">
|
||||
<MkInput v-model="text">
|
||||
<template #label>Text</template>
|
||||
</MkInput>
|
||||
|
|
@ -15,10 +15,10 @@
|
|||
<MkButton inline>This is</MkButton>
|
||||
<MkButton inline primary>the button</MkButton>
|
||||
</div>
|
||||
<div class="_content" style="pointer-events: none;">
|
||||
<div class="" style="pointer-events: none;">
|
||||
<Mfm :text="mfm"/>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<div class="">
|
||||
<MkButton inline primary @click="openMenu">Open menu</MkButton>
|
||||
<MkButton inline primary @click="openDialog">Open dialog</MkButton>
|
||||
<MkButton inline primary @click="openForm">Open form</MkButton>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
<template>
|
||||
<form class="eppvobhk _monolithic_" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
|
||||
<div class="auth _section _formRoot">
|
||||
<form class="eppvobhk" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
|
||||
<div class="auth _gaps_m">
|
||||
<div v-show="withAvatar" class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null, marginBottom: message ? '1.5em' : null }"></div>
|
||||
<MkInfo v-if="message">
|
||||
{{ message }}
|
||||
</MkInfo>
|
||||
<div v-if="!totpLogin" class="normal-signin">
|
||||
<MkInput v-model="username" class="_formBlock" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required data-cy-signin-username @update:model-value="onUsernameChange">
|
||||
<div v-if="!totpLogin" class="normal-signin _gaps_m">
|
||||
<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required data-cy-signin-username @update:model-value="onUsernameChange">
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" class="_formBlock" :placeholder="i18n.ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password>
|
||||
<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
|
||||
</MkInput>
|
||||
<MkButton class="_formBlock" type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
||||
<MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
||||
</div>
|
||||
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
|
||||
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
|
||||
|
|
@ -40,10 +40,10 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="social _section">
|
||||
<a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/twitter`"><i class="ti ti-brand-twitter" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Twitter' }) }}</a>
|
||||
<a v-if="meta && meta.enableGithubIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/github`"><i class="ti ti-brand-github" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'GitHub' }) }}</a>
|
||||
<a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/discord`"><i class="ti ti-brand-discord" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Discord' }) }}</a>
|
||||
<div class="social">
|
||||
<a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _margin" :href="`${apiUrl}/signin/twitter`"><i class="ti ti-brand-twitter" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Twitter' }) }}</a>
|
||||
<a v-if="meta && meta.enableGithubIntegration" class="_borderButton _margin" :href="`${apiUrl}/signin/github`"><i class="ti ti-brand-github" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'GitHub' }) }}</a>
|
||||
<a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _margin" :href="`${apiUrl}/signin/discord`"><i class="ti ti-brand-discord" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Discord' }) }}</a>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XModalWindow
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="370"
|
||||
:height="400"
|
||||
|
|
@ -8,14 +8,16 @@
|
|||
>
|
||||
<template #header>{{ i18n.ts.login }}</template>
|
||||
|
||||
<MkSignin :auto-set="autoSet" :message="message" @login="onLogin"/>
|
||||
</XModalWindow>
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<MkSignin :auto-set="autoSet" :message="message" @login="onLogin"/>
|
||||
</MkSpacer>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkSignin from '@/components/MkSignin.vue';
|
||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
|
@ -32,7 +34,7 @@ const emit = defineEmits<{
|
|||
(ev: 'cancelled'): void;
|
||||
}>();
|
||||
|
||||
const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
|
||||
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
|
||||
function onClose() {
|
||||
emit('cancelled');
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
|
||||
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required>
|
||||
<form class="qlvuhzng _gaps_m" autocomplete="new-password" @submit.prevent="onSubmit">
|
||||
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required>
|
||||
<template #label>{{ i18n.ts.invitationCode }}</template>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
</MkInput>
|
||||
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:model-value="onChangeUsername">
|
||||
<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:model-value="onChangeUsername">
|
||||
<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-question-circle"></i></div></template>
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-if="instance.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:model-value="onChangeEmail">
|
||||
<MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:model-value="onChangeEmail">
|
||||
<template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ti ti-question-circle"></i></div></template>
|
||||
<template #prefix><i class="ti ti-mail"></i></template>
|
||||
<template #caption>
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="password" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password @update:model-value="onChangePassword">
|
||||
<MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:model-value="onChangePassword">
|
||||
<template #label>{{ i18n.ts.password }}</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="retypedPassword" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:model-value="onChangePasswordRetype">
|
||||
<MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:model-value="onChangePasswordRetype">
|
||||
<template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
|
|
@ -50,17 +50,17 @@
|
|||
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-if="instance.tosUrl" v-model="ToSAgreement" class="_formBlock tou">
|
||||
<MkSwitch v-if="instance.tosUrl" v-model="ToSAgreement" class="tou">
|
||||
<I18n :src="i18n.ts.agreeTo">
|
||||
<template #0>
|
||||
<a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.tos }}</a>
|
||||
</template>
|
||||
</I18n>
|
||||
</MkSwitch>
|
||||
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" class="_formBlock captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
||||
<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ i18n.ts.start }}</MkButton>
|
||||
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" class="captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
||||
<MkButton type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ i18n.ts.start }}</MkButton>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XModalWindow
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="366"
|
||||
:height="500"
|
||||
|
|
@ -8,18 +8,16 @@
|
|||
>
|
||||
<template #header>{{ i18n.ts.signup }}</template>
|
||||
|
||||
<div class="_monolithic_">
|
||||
<div class="_section">
|
||||
<XSignup :auto-set="autoSet" @signup="onSignup" @signup-email-pending="onSignupEmailPending"/>
|
||||
</div>
|
||||
</div>
|
||||
</XModalWindow>
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<XSignup :auto-set="autoSet" @signup="onSignup" @signup-email-pending="onSignupEmailPending"/>
|
||||
</MkSpacer>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XSignup from '@/components/MkSignup.vue';
|
||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
|
@ -33,7 +31,7 @@ const emit = defineEmits<{
|
|||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
|
||||
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
|
||||
function onSignup(res) {
|
||||
emit('done', res);
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export default defineComponent({
|
|||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 10px 16px 10px 8px;
|
||||
padding: 9px 16px 9px 8px;
|
||||
border-radius: 9px;
|
||||
font-size: 0.9em;
|
||||
|
||||
|
|
@ -141,8 +141,8 @@ export default defineComponent({
|
|||
margin-right: 0;
|
||||
margin-bottom: 6px;
|
||||
font-size: 1.5em;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
aspect-ratio: 1;
|
||||
background: var(--panel);
|
||||
border-radius: 100%;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XModalWindow
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
:height="450"
|
||||
|
|
@ -11,21 +11,26 @@
|
|||
@ok="ok()"
|
||||
>
|
||||
<template #header>{{ title || $ts.generateAccessToken }}</template>
|
||||
<div v-if="information" class="_section">
|
||||
<MkInfo warn>{{ information }}</MkInfo>
|
||||
</div>
|
||||
<div class="_section">
|
||||
<MkInput v-model="name">
|
||||
<template #label>{{ $ts.name }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
<div class="_section">
|
||||
<div style="margin-bottom: 16px;"><b>{{ $ts.permission }}</b></div>
|
||||
<MkButton inline @click="disableAll">{{ $ts.disableAll }}</MkButton>
|
||||
<MkButton inline @click="enableAll">{{ $ts.enableAll }}</MkButton>
|
||||
<MkSwitch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ $t(`_permissions.${kind}`) }}</MkSwitch>
|
||||
</div>
|
||||
</XModalWindow>
|
||||
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<div class="_gaps_m">
|
||||
<div v-if="information">
|
||||
<MkInfo warn>{{ information }}</MkInfo>
|
||||
</div>
|
||||
<div>
|
||||
<MkInput v-model="name">
|
||||
<template #label>{{ $ts.name }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
<div><b>{{ $ts.permission }}</b></div>
|
||||
<div class="_buttons">
|
||||
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
|
||||
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
|
||||
</div>
|
||||
<MkSwitch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ $t(`_permissions.${kind}`) }}</MkSwitch>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -35,7 +40,8 @@ import MkInput from './form/input.vue';
|
|||
import MkSwitch from './form/switch.vue';
|
||||
import MkButton from './MkButton.vue';
|
||||
import MkInfo from './MkInfo.vue';
|
||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
title?: string | null;
|
||||
|
|
@ -54,7 +60,7 @@ const emit = defineEmits<{
|
|||
(ev: 'done', result: { name: string | null, permissions: string[] }): void;
|
||||
}>();
|
||||
|
||||
const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
|
||||
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
let name = $ref(props.initialName);
|
||||
let permissions = $ref({});
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef } from 'vue';
|
||||
import { onMounted, shallowRef } from 'vue';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkSparkle from '@/components/MkSparkle.vue';
|
||||
import { version } from '@/config';
|
||||
import { i18n } from '@/i18n';
|
||||
import { confetti } from '@/scripts/confetti';
|
||||
|
||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||
|
||||
|
|
@ -23,6 +24,12 @@ const whatIsNew = () => {
|
|||
modal.value.close();
|
||||
window.open(`https://misskey-hub.net/docs/releases.html#_${version.replace(/\./g, '-')}`, '_blank');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
confetti({
|
||||
duration: 1000 * 3,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XModalWindow
|
||||
<MkModalWindow
|
||||
ref="dialogEl"
|
||||
:with-ok-button="true"
|
||||
:ok-button-disabled="selected == null"
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</XModalWindow>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
|
@ -56,7 +56,7 @@ import { nextTick, onMounted } from 'vue';
|
|||
import * as misskey from 'misskey-js';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import * as os from '@/os';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<Transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')">
|
||||
<div v-if="showing" ref="rootEl" class="ebkgocck" :class="{ maximized }">
|
||||
<div class="body _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
|
||||
<div class="body _shadow" @mousedown="onBodyMousedown" @keydown="onKeydown">
|
||||
<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
|
||||
<span class="left">
|
||||
<button v-for="button in buttonsLeft" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button>
|
||||
|
|
@ -489,6 +489,7 @@ defineExpose({
|
|||
flex: 1;
|
||||
overflow: auto;
|
||||
background: var(--panel);
|
||||
container-type: inline-size;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XWindow :initial-width="640" :initial-height="402" :can-resize="true" :close-button="true">
|
||||
<MkWindow :initial-width="640" :initial-height="402" :can-resize="true" :close-button="true">
|
||||
<template #header>
|
||||
<i class="icon ti ti-brand-youtube" style="margin-right: 0.5em;"></i>
|
||||
<span>{{ title ?? 'YouTube' }}</span>
|
||||
|
|
@ -14,11 +14,11 @@
|
|||
<MkLoading v-if="fetching"/>
|
||||
<MkError v-else-if="!player.url" @retry="ytFetch()"/>
|
||||
</div>
|
||||
</XWindow>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import XWindow from '@/components/MkWindow.vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import { versatileLang } from '@/scripts/intl-const';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="dwzlatin" :class="{ opened }">
|
||||
<div ref="rootEl" class="dwzlatin" :class="{ opened }">
|
||||
<div class="header _button" @click="toggle">
|
||||
<span class="icon"><slot name="icon"></slot></span>
|
||||
<span class="text"><slot name="label"></slot></span>
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
<i v-else class="ti ti-chevron-down icon"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="openedAtLeastOnce" class="body">
|
||||
<div v-if="openedAtLeastOnce" class="body" :class="{ bgSame }">
|
||||
<Transition
|
||||
:name="$store.state.animation ? 'folder-toggle' : ''"
|
||||
@enter="enter"
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick } from 'vue';
|
||||
import { nextTick, onMounted } from 'vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
defaultOpen: boolean;
|
||||
|
|
@ -38,6 +38,17 @@ const props = withDefaults(defineProps<{
|
|||
defaultOpen: false,
|
||||
});
|
||||
|
||||
const getBgColor = (el: HTMLElement) => {
|
||||
const style = window.getComputedStyle(el);
|
||||
if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
|
||||
return style.backgroundColor;
|
||||
} else {
|
||||
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
|
||||
}
|
||||
};
|
||||
|
||||
let rootEl = $ref<HTMLElement>();
|
||||
let bgSame = $ref(false);
|
||||
let opened = $ref(props.defaultOpen);
|
||||
let openedAtLeastOnce = $ref(props.defaultOpen);
|
||||
|
||||
|
|
@ -72,6 +83,13 @@ function toggle() {
|
|||
opened = !opened;
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
const parentBg = getBgColor(rootEl.parentElement);
|
||||
const myBg = computedStyle.getPropertyValue('--panel');
|
||||
bgSame = parentBg === myBg;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
@ -142,6 +160,10 @@ function toggle() {
|
|||
background: var(--panel);
|
||||
border-radius: 0 0 6px 6px;
|
||||
container-type: inline-size;
|
||||
|
||||
&.bgSame {
|
||||
background: var(--bg);
|
||||
}
|
||||
}
|
||||
|
||||
&.opened {
|
||||
|
|
|
|||
|
|
@ -78,8 +78,8 @@ const inputEl = shallowRef<HTMLElement>();
|
|||
const prefixEl = shallowRef<HTMLElement>();
|
||||
const suffixEl = shallowRef<HTMLElement>();
|
||||
const height =
|
||||
props.small ? 35 :
|
||||
props.large ? 39 :
|
||||
props.small ? 34 :
|
||||
props.large ? 40 :
|
||||
37;
|
||||
|
||||
const focus = () => inputEl.value.focus();
|
||||
|
|
|
|||
|
|
@ -1,35 +1,27 @@
|
|||
<template>
|
||||
<div class="vrtktovh _formBlock">
|
||||
<div class="vrtktovh" :class="{ first }">
|
||||
<div class="label"><slot name="label"></slot></div>
|
||||
<div class="main _formRoot">
|
||||
<div class="main">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
first?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vrtktovh {
|
||||
border-top: solid 0.5px var(--divider);
|
||||
border-bottom: solid 0.5px var(--divider);
|
||||
|
||||
& + .vrtktovh {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
//border-bottom: solid 0.5px var(--divider);
|
||||
|
||||
> .label {
|
||||
font-weight: bold;
|
||||
margin: 1.5em 0 16px 0;
|
||||
padding: 1.5em 0 0 0;
|
||||
margin: 0 0 16px 0;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
|
|
@ -37,7 +29,15 @@
|
|||
}
|
||||
|
||||
> .main {
|
||||
margin: 1.5em 0;
|
||||
margin: 1.5em 0 0 0;
|
||||
}
|
||||
|
||||
&.first {
|
||||
border-top: none;
|
||||
|
||||
> .label {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
>
|
||||
<slot></slot>
|
||||
</select>
|
||||
<div ref="suffixEl" class="suffix"><i class="ti ti-chevron-down"></i></div>
|
||||
<div ref="suffixEl" class="suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div>
|
||||
</div>
|
||||
<div class="caption"><slot name="caption"></slot></div>
|
||||
|
||||
|
|
@ -56,6 +56,7 @@ const slots = useSlots();
|
|||
const { modelValue, autofocus } = toRefs(props);
|
||||
const v = ref(modelValue.value);
|
||||
const focused = ref(false);
|
||||
const opening = ref(false);
|
||||
const changed = ref(false);
|
||||
const invalid = ref(false);
|
||||
const filled = computed(() => v.value !== '' && v.value != null);
|
||||
|
|
@ -64,8 +65,8 @@ const prefixEl = ref(null);
|
|||
const suffixEl = ref(null);
|
||||
const container = ref(null);
|
||||
const height =
|
||||
props.small ? 35 :
|
||||
props.large ? 39 :
|
||||
props.small ? 34 :
|
||||
props.large ? 40 :
|
||||
37;
|
||||
|
||||
const focus = () => inputEl.value.focus();
|
||||
|
|
@ -119,6 +120,7 @@ onMounted(() => {
|
|||
|
||||
const onClick = (ev: MouseEvent) => {
|
||||
focused.value = true;
|
||||
opening.value = true;
|
||||
|
||||
const menu = [];
|
||||
let options = slots.default!();
|
||||
|
|
@ -126,7 +128,7 @@ const onClick = (ev: MouseEvent) => {
|
|||
const pushOption = (option: VNode) => {
|
||||
menu.push({
|
||||
text: option.children,
|
||||
active: v.value === option.props.value,
|
||||
active: computed(() => v.value === option.props.value),
|
||||
action: () => {
|
||||
v.value = option.props.value;
|
||||
},
|
||||
|
|
@ -158,6 +160,9 @@ const onClick = (ev: MouseEvent) => {
|
|||
|
||||
os.popupMenu(menu, container.value, {
|
||||
width: container.value.offsetWidth,
|
||||
onClosing: () => {
|
||||
opening.value = false;
|
||||
},
|
||||
}).then(() => {
|
||||
focused.value = false;
|
||||
});
|
||||
|
|
@ -277,3 +282,13 @@ const onClick = (ev: MouseEvent) => {
|
|||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" module>
|
||||
.chevron {
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
.chevronOpening {
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="terlnhxf _formBlock">
|
||||
<div class="terlnhxf">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -38,13 +38,13 @@ const forceSpacerMin = inject('forceSpacerMin', false) || deviceKind === 'smartp
|
|||
container-type: inline-size;
|
||||
}
|
||||
|
||||
@container (max-width: 360px) {
|
||||
@container (max-width: 450px) {
|
||||
.root {
|
||||
padding: v-bind('props.marginMin + "px"');
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: 361px) {
|
||||
@container (min-width: 451px) {
|
||||
.root {
|
||||
padding: v-bind('props.marginMax + "px"');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,22 +36,21 @@ const relative = $computed(() => {
|
|||
i18n.ts._ago.future);
|
||||
});
|
||||
|
||||
function tick() {
|
||||
// TODO: パフォーマンス向上のため、このコンポーネントが画面内に表示されている場合のみ更新する
|
||||
now = new Date();
|
||||
|
||||
tickId = window.setTimeout(() => {
|
||||
window.requestAnimationFrame(tick);
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
let tickId: number;
|
||||
|
||||
function tick() {
|
||||
now = new Date();
|
||||
const ago = (now.getTime() - _time.getTime()) / 1000/*ms*/;
|
||||
const next = ago < 60 ? 10000 : ago < 3600 ? 60000 : 180000;
|
||||
|
||||
tickId = window.setTimeout(tick, next);
|
||||
}
|
||||
|
||||
if (props.mode === 'relative' || props.mode === 'detail') {
|
||||
tickId = window.requestAnimationFrame(tick);
|
||||
tick();
|
||||
|
||||
onUnmounted(() => {
|
||||
window.cancelAnimationFrame(tickId);
|
||||
window.clearTimeout(tickId);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { miLocalStorage } from "./local-storage";
|
||||
|
||||
const address = new URL(location.href);
|
||||
const siteName = (document.querySelector('meta[property="og:site_name"]') as HTMLMetaElement)?.content;
|
||||
|
||||
|
|
@ -6,10 +8,10 @@ export const hostname = address.hostname;
|
|||
export const url = address.origin;
|
||||
export const apiUrl = url + '/api';
|
||||
export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming';
|
||||
export const lang = localStorage.getItem('lang');
|
||||
export const lang = miLocalStorage.getItem('lang');
|
||||
export const langs = _LANGS_;
|
||||
export const locale = JSON.parse(localStorage.getItem('locale'));
|
||||
export const locale = JSON.parse(miLocalStorage.getItem('locale'));
|
||||
export const version = _VERSION_;
|
||||
export const instanceName = siteName === 'Misskey' ? host : siteName;
|
||||
export const ui = localStorage.getItem('ui');
|
||||
export const debug = localStorage.getItem('debug') === 'true';
|
||||
export const ui = miLocalStorage.getItem('ui');
|
||||
export const debug = miLocalStorage.getItem('debug') === 'true';
|
||||
|
|
|
|||
|
|
@ -9,9 +9,12 @@ import '@/style.scss';
|
|||
//#region account indexedDB migration
|
||||
import { set } from '@/scripts/idb-proxy';
|
||||
|
||||
if (localStorage.getItem('accounts') != null) {
|
||||
set('accounts', JSON.parse(localStorage.getItem('accounts')));
|
||||
localStorage.removeItem('accounts');
|
||||
{
|
||||
const accounts = miLocalStorage.getItem('accounts');
|
||||
if (accounts) {
|
||||
set('accounts', JSON.parse(accounts));
|
||||
miLocalStorage.removeItem('accounts');
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
|
@ -40,6 +43,7 @@ import { reloadChannel } from '@/scripts/unison-reload';
|
|||
import { reactionPicker } from '@/scripts/reaction-picker';
|
||||
import { getUrlWithoutLoginId } from '@/scripts/login-id';
|
||||
import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||
import { miLocalStorage } from './local-storage';
|
||||
|
||||
(async () => {
|
||||
console.info(`Misskey v${version}`);
|
||||
|
|
@ -154,7 +158,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
|
|||
const fetchInstanceMetaPromise = fetchInstance();
|
||||
|
||||
fetchInstanceMetaPromise.then(() => {
|
||||
localStorage.setItem('v', instance.version);
|
||||
miLocalStorage.setItem('v', instance.version);
|
||||
|
||||
// Init service worker
|
||||
initializeSw();
|
||||
|
|
@ -172,6 +176,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
|
|||
app.config.performance = true;
|
||||
}
|
||||
|
||||
// TODO: 廃止
|
||||
app.config.globalProperties = {
|
||||
$i,
|
||||
$store: defaultStore,
|
||||
|
|
@ -222,12 +227,12 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
|
|||
}
|
||||
|
||||
// クライアントが更新されたか?
|
||||
const lastVersion = localStorage.getItem('lastVersion');
|
||||
const lastVersion = miLocalStorage.getItem('lastVersion');
|
||||
if (lastVersion !== version) {
|
||||
localStorage.setItem('lastVersion', version);
|
||||
miLocalStorage.setItem('lastVersion', version);
|
||||
|
||||
// テーマリビルドするため
|
||||
localStorage.removeItem('theme');
|
||||
miLocalStorage.removeItem('theme');
|
||||
|
||||
try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
|
||||
if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
|
||||
|
|
@ -243,7 +248,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
|
|||
// NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため)
|
||||
watch(defaultStore.reactiveState.darkMode, (darkMode) => {
|
||||
applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
|
||||
}, { immediate: localStorage.theme == null });
|
||||
}, { immediate: miLocalStorage.getItem('theme') == null });
|
||||
|
||||
const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
|
||||
const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
|
||||
|
|
@ -340,7 +345,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
|
|||
});
|
||||
}
|
||||
|
||||
const lastUsed = localStorage.getItem('lastUsed');
|
||||
const lastUsed = miLocalStorage.getItem('lastUsed');
|
||||
if (lastUsed) {
|
||||
const lastUsedDate = parseInt(lastUsed, 10);
|
||||
// 二時間以上前なら
|
||||
|
|
@ -350,7 +355,15 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
|
|||
}));
|
||||
}
|
||||
}
|
||||
localStorage.setItem('lastUsed', Date.now().toString());
|
||||
miLocalStorage.setItem('lastUsed', Date.now().toString());
|
||||
|
||||
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
|
||||
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
|
||||
if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3)))) {
|
||||
if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
|
||||
}
|
||||
}
|
||||
|
||||
if ('Notification' in window) {
|
||||
// 許可を得ていなかったらリクエスト
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { computed, reactive } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { api } from './os';
|
||||
import { miLocalStorage } from './local-storage';
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
const instanceData = localStorage.getItem('instance');
|
||||
const instanceData = miLocalStorage.getItem('instance');
|
||||
|
||||
// TODO: instanceをリアクティブにするかは再考の余地あり
|
||||
|
||||
|
|
@ -21,7 +22,7 @@ export async function fetchInstance() {
|
|||
instance[k] = v;
|
||||
}
|
||||
|
||||
localStorage.setItem('instance', JSON.stringify(instance));
|
||||
miLocalStorage.setItem('instance', JSON.stringify(instance));
|
||||
}
|
||||
|
||||
export const emojiCategories = computed(() => {
|
||||
|
|
|
|||
33
packages/frontend/src/local-storage.ts
Normal file
33
packages/frontend/src/local-storage.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
type Keys =
|
||||
'v' |
|
||||
'lastVersion' |
|
||||
'instance' |
|
||||
'account' |
|
||||
'accounts' |
|
||||
'latestDonationInfoShownAt' |
|
||||
'neverShowDonationInfo' |
|
||||
'lastUsed' |
|
||||
'lang' |
|
||||
'drafts' |
|
||||
'hashtags' |
|
||||
'wallpaper' |
|
||||
'theme' |
|
||||
'colorSchema' |
|
||||
'useSystemFont' |
|
||||
'fontSize' |
|
||||
'ui' |
|
||||
'locale' |
|
||||
'theme' |
|
||||
'customCss' |
|
||||
'message_drafts' |
|
||||
'scratchpad' |
|
||||
`miux:${string}` |
|
||||
`ui:folder:${string}` |
|
||||
`themes:${string}` |
|
||||
`aiscript:${string}`;
|
||||
|
||||
export const miLocalStorage = {
|
||||
getItem: (key: Keys) => window.localStorage.getItem(key),
|
||||
setItem: (key: Keys, value: string) => window.localStorage.setItem(key, value),
|
||||
removeItem: (key: Keys) => window.localStorage.removeItem(key),
|
||||
};
|
||||
|
|
@ -5,128 +5,134 @@ import * as os from '@/os';
|
|||
import { i18n } from '@/i18n';
|
||||
import { ui } from '@/config';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import { miLocalStorage } from './local-storage';
|
||||
|
||||
export const navbarItemDef = reactive({
|
||||
notifications: {
|
||||
title: 'notifications',
|
||||
title: i18n.ts.notifications,
|
||||
icon: 'ti ti-bell',
|
||||
show: computed(() => $i != null),
|
||||
indicated: computed(() => $i != null && $i.hasUnreadNotification),
|
||||
to: '/my/notifications',
|
||||
},
|
||||
messaging: {
|
||||
title: 'messaging',
|
||||
title: i18n.ts.messaging,
|
||||
icon: 'ti ti-messages',
|
||||
show: computed(() => $i != null),
|
||||
indicated: computed(() => $i != null && $i.hasUnreadMessagingMessage),
|
||||
to: '/my/messaging',
|
||||
},
|
||||
drive: {
|
||||
title: 'drive',
|
||||
title: i18n.ts.drive,
|
||||
icon: 'ti ti-cloud',
|
||||
show: computed(() => $i != null),
|
||||
to: '/my/drive',
|
||||
},
|
||||
followRequests: {
|
||||
title: 'followRequests',
|
||||
title: i18n.ts.followRequests,
|
||||
icon: 'ti ti-user-plus',
|
||||
show: computed(() => $i != null && $i.isLocked),
|
||||
indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest),
|
||||
to: '/my/follow-requests',
|
||||
},
|
||||
explore: {
|
||||
title: 'explore',
|
||||
title: i18n.ts.explore,
|
||||
icon: 'ti ti-hash',
|
||||
to: '/explore',
|
||||
},
|
||||
announcements: {
|
||||
title: 'announcements',
|
||||
title: i18n.ts.announcements,
|
||||
icon: 'ti ti-speakerphone',
|
||||
indicated: computed(() => $i != null && $i.hasUnreadAnnouncement),
|
||||
to: '/announcements',
|
||||
},
|
||||
search: {
|
||||
title: 'search',
|
||||
title: i18n.ts.search,
|
||||
icon: 'ti ti-search',
|
||||
action: () => search(),
|
||||
},
|
||||
lists: {
|
||||
title: 'lists',
|
||||
title: i18n.ts.lists,
|
||||
icon: 'ti ti-list',
|
||||
show: computed(() => $i != null),
|
||||
to: '/my/lists',
|
||||
},
|
||||
/*
|
||||
groups: {
|
||||
title: 'groups',
|
||||
title: i18n.ts.groups,
|
||||
icon: 'ti ti-users',
|
||||
show: computed(() => $i != null),
|
||||
to: '/my/groups',
|
||||
},
|
||||
*/
|
||||
antennas: {
|
||||
title: 'antennas',
|
||||
title: i18n.ts.antennas,
|
||||
icon: 'ti ti-antenna',
|
||||
show: computed(() => $i != null),
|
||||
to: '/my/antennas',
|
||||
},
|
||||
favorites: {
|
||||
title: 'favorites',
|
||||
title: i18n.ts.favorites,
|
||||
icon: 'ti ti-star',
|
||||
show: computed(() => $i != null),
|
||||
to: '/my/favorites',
|
||||
},
|
||||
pages: {
|
||||
title: 'pages',
|
||||
title: i18n.ts.pages,
|
||||
icon: 'ti ti-news',
|
||||
to: '/pages',
|
||||
},
|
||||
play: {
|
||||
title: 'Play',
|
||||
icon: 'ti ti-player-play',
|
||||
to: '/play',
|
||||
},
|
||||
gallery: {
|
||||
title: 'gallery',
|
||||
title: i18n.ts.gallery,
|
||||
icon: 'ti ti-icons',
|
||||
to: '/gallery',
|
||||
},
|
||||
clips: {
|
||||
title: 'clip',
|
||||
title: i18n.ts.clip,
|
||||
icon: 'ti ti-paperclip',
|
||||
show: computed(() => $i != null),
|
||||
to: '/my/clips',
|
||||
},
|
||||
channels: {
|
||||
title: 'channel',
|
||||
title: i18n.ts.channel,
|
||||
icon: 'ti ti-device-tv',
|
||||
to: '/channels',
|
||||
},
|
||||
ui: {
|
||||
title: 'switchUi',
|
||||
title: i18n.ts.switchUi,
|
||||
icon: 'ti ti-devices',
|
||||
action: (ev) => {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.default,
|
||||
active: ui === 'default' || ui === null,
|
||||
action: () => {
|
||||
localStorage.setItem('ui', 'default');
|
||||
miLocalStorage.setItem('ui', 'default');
|
||||
unisonReload();
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.deck,
|
||||
active: ui === 'deck',
|
||||
action: () => {
|
||||
localStorage.setItem('ui', 'deck');
|
||||
miLocalStorage.setItem('ui', 'deck');
|
||||
unisonReload();
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.classic,
|
||||
active: ui === 'classic',
|
||||
action: () => {
|
||||
localStorage.setItem('ui', 'classic');
|
||||
miLocalStorage.setItem('ui', 'classic');
|
||||
unisonReload();
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
},
|
||||
},
|
||||
reload: {
|
||||
title: 'reload',
|
||||
title: i18n.ts.reload,
|
||||
icon: 'ti ti-refresh',
|
||||
action: (ev) => {
|
||||
location.reload();
|
||||
|
|
|
|||
|
|
@ -515,6 +515,7 @@ export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement
|
|||
align?: string;
|
||||
width?: number;
|
||||
viaKeyboard?: boolean;
|
||||
onClosing?: () => void;
|
||||
}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let dispose;
|
||||
|
|
@ -529,6 +530,9 @@ export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement
|
|||
resolve();
|
||||
dispose();
|
||||
},
|
||||
closing: () => {
|
||||
if (options?.onClosing) options.onClosing();
|
||||
},
|
||||
}).then(res => {
|
||||
dispose = res.dispose;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import * as os from '@/os';
|
|||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { miLocalStorage } from '@/local-storage';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
error?: Error;
|
||||
|
|
@ -42,7 +43,7 @@ os.api('meta', {
|
|||
loaded = true;
|
||||
serverIsDead = false;
|
||||
meta = res;
|
||||
localStorage.setItem('v', res.version);
|
||||
miLocalStorage.setItem('v', res.version);
|
||||
}, () => {
|
||||
loaded = true;
|
||||
serverIsDead = true;
|
||||
|
|
|
|||
|
|
@ -3,18 +3,17 @@
|
|||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<div style="overflow: clip;">
|
||||
<MkSpacer :content-max="600" :margin-min="20">
|
||||
<div class="_formRoot znqjceqz">
|
||||
<div id="debug"></div>
|
||||
<div ref="containerEl" v-panel class="_formBlock about" :class="{ playing: easterEggEngine != null }">
|
||||
<div class="_gaps_m znqjceqz">
|
||||
<div ref="containerEl" v-panel class="about" :class="{ playing: easterEggEngine != null }">
|
||||
<img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/>
|
||||
<div class="misskey">Misskey</div>
|
||||
<div class="version">v{{ version }}</div>
|
||||
<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :is-reaction="false" :normal="true" :no-style="true"/></span>
|
||||
</div>
|
||||
<div class="_formBlock" style="text-align: center;">
|
||||
<div style="text-align: center;">
|
||||
{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a>
|
||||
</div>
|
||||
<div class="_formBlock" style="text-align: center;">
|
||||
<div style="text-align: center;">
|
||||
<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton>
|
||||
</div>
|
||||
<FormSection>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer v-if="tab === 'overview'" :content-max="600" :margin-min="20">
|
||||
<div class="_formRoot">
|
||||
<div class="_formBlock fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }">
|
||||
<div class="_gaps_m">
|
||||
<div class="fwhjspax" :style="{ backgroundImage: `url(${ $instance.bannerUrl })` }">
|
||||
<div class="content">
|
||||
<img :src="$instance.iconUrl ?? $instance.faviconUrl ?? '/favicon.ico'" alt="" class="icon"/>
|
||||
<div class="name">
|
||||
|
|
@ -12,44 +12,48 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<MkKeyValue class="_formBlock">
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.description }}</template>
|
||||
<template #value><div v-html="$instance.description"></div></template>
|
||||
</MkKeyValue>
|
||||
|
||||
<FormSection>
|
||||
<MkKeyValue class="_formBlock" :copy="version">
|
||||
<template #key>Misskey</template>
|
||||
<template #value>{{ version }}</template>
|
||||
</MkKeyValue>
|
||||
<div class="_formBlock" v-html="i18n.t('poweredByMisskeyDescription', { name: $instance.name ?? host })">
|
||||
<div class="_gaps_m">
|
||||
<MkKeyValue :copy="version">
|
||||
<template #key>Misskey</template>
|
||||
<template #value>{{ version }}</template>
|
||||
</MkKeyValue>
|
||||
<div v-html="i18n.t('poweredByMisskeyDescription', { name: $instance.name ?? host })">
|
||||
</div>
|
||||
<FormLink to="/about-misskey">{{ i18n.ts.aboutMisskey }}</FormLink>
|
||||
</div>
|
||||
<FormLink to="/about-misskey">{{ i18n.ts.aboutMisskey }}</FormLink>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<FormSplit>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ i18n.ts.administrator }}</template>
|
||||
<template #value>{{ $instance.maintainerName }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ i18n.ts.contact }}</template>
|
||||
<template #value>{{ $instance.maintainerEmail }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" class="_formBlock" external>{{ i18n.ts.tos }}</FormLink>
|
||||
<div class="_gaps_m">
|
||||
<FormSplit>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.administrator }}</template>
|
||||
<template #value>{{ $instance.maintainerName }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.contact }}</template>
|
||||
<template #value>{{ $instance.maintainerEmail }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" external>{{ i18n.ts.tos }}</FormLink>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
<FormSuspense :p="initStats">
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.statistics }}</template>
|
||||
<FormSplit>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.users }}</template>
|
||||
<template #value>{{ number(stats.originalUsersCount) }}</template>
|
||||
</MkKeyValue>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.notes }}</template>
|
||||
<template #value>{{ number(stats.originalNotesCount) }}</template>
|
||||
</MkKeyValue>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer v-if="file" :content-max="600" :margin-min="16" :margin-max="32">
|
||||
<div v-if="tab === 'overview'" class="cxqhhsmd _formRoot">
|
||||
<a class="_formBlock thumbnail" :href="file.url" target="_blank">
|
||||
<div v-if="tab === 'overview'" class="cxqhhsmd _gaps_m">
|
||||
<a class="thumbnail" :href="file.url" target="_blank">
|
||||
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
||||
</a>
|
||||
<div class="_formBlock">
|
||||
<div>
|
||||
<MkKeyValue :copy="file.type" oneline style="margin: 1em 0;">
|
||||
<template #key>MIME Type</template>
|
||||
<template #value><span class="_monospace">{{ file.type }}</span></template>
|
||||
|
|
@ -31,29 +31,29 @@
|
|||
<MkA v-if="file.user" class="user" :to="`/user-info/${file.user.id}`">
|
||||
<MkUserCardMini :user="file.user"/>
|
||||
</MkA>
|
||||
<div class="_formBlock">
|
||||
<div>
|
||||
<MkSwitch v-model="isSensitive" @update:model-value="toggleIsSensitive">NSFW</MkSwitch>
|
||||
</div>
|
||||
|
||||
<div class="_formBlock">
|
||||
<div>
|
||||
<MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="tab === 'ip' && info" class="_formRoot">
|
||||
<div v-else-if="tab === 'ip' && info" class="_gaps_m">
|
||||
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
|
||||
<MkKeyValue v-if="info.requestIp" class="_formBlock _monospace" :copy="info.requestIp" oneline>
|
||||
<MkKeyValue v-if="info.requestIp" class="_monospace" :copy="info.requestIp" oneline>
|
||||
<template #key>IP</template>
|
||||
<template #value>{{ info.requestIp }}</template>
|
||||
</MkKeyValue>
|
||||
<FormSection v-if="info.requestHeaders">
|
||||
<template #label>Headers</template>
|
||||
<MkKeyValue v-for="(v, k) in info.requestHeaders" :key="k" class="_formBlock _monospace">
|
||||
<MkKeyValue v-for="(v, k) in info.requestHeaders" :key="k" class="_monospace">
|
||||
<template #key>{{ k }}</template>
|
||||
<template #value>{{ v }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSection>
|
||||
</div>
|
||||
<div v-else-if="tab === 'raw'" class="_formRoot">
|
||||
<div v-else-if="tab === 'raw'" class="_gaps_m">
|
||||
<MkObjectView v-if="info" tall :value="info">
|
||||
</MkObjectView>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="900">
|
||||
<div class="lcixvhis">
|
||||
<div class="_section reports">
|
||||
<div class="_content">
|
||||
<div class="reports">
|
||||
<div class="">
|
||||
<div class="inputs" style="display: flex;">
|
||||
<MkSelect v-model="state" style="margin: 0; flex: 1;">
|
||||
<template #label>{{ i18n.ts.state }}</template>
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@
|
|||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="900">
|
||||
<div class="uqshojas">
|
||||
<div v-for="ad in ads" class="_panel _formRoot ad">
|
||||
<div v-for="ad in ads" class="_panel _gaps_m ad">
|
||||
<MkAd v-if="ad.url" :specify="ad"/>
|
||||
<MkInput v-model="ad.url" type="url" class="_formBlock">
|
||||
<MkInput v-model="ad.url" type="url">
|
||||
<template #label>URL</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="ad.imageUrl" class="_formBlock">
|
||||
<MkInput v-model="ad.imageUrl">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<FormRadios v-model="ad.place" class="_formBlock">
|
||||
<FormRadios v-model="ad.place">
|
||||
<template #label>Form</template>
|
||||
<option value="square">square</option>
|
||||
<option value="horizontal">horizontal</option>
|
||||
|
|
@ -33,10 +33,10 @@
|
|||
<template #label>{{ i18n.ts.expiration }}</template>
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
<MkTextarea v-model="ad.memo" class="_formBlock">
|
||||
<MkTextarea v-model="ad.memo">
|
||||
<template #label>{{ i18n.ts.memo }}</template>
|
||||
</MkTextarea>
|
||||
<div class="buttons _formBlock">
|
||||
<div class="buttons">
|
||||
<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton class="button" inline danger @click="remove(ad)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
<MkStickyContainer>
|
||||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="900">
|
||||
<div class="ztgjmzrw">
|
||||
<section v-for="announcement in announcements" class="_card _gap announcements">
|
||||
<div class="_content announcement">
|
||||
<div class="ztgjmzrw _gaps_m">
|
||||
<section v-for="announcement in announcements" class="">
|
||||
<div class="_panel _gaps_m" style="padding: 24px;">
|
||||
<MkInput v-model="announcement.title">
|
||||
<template #label>{{ i18n.ts.title }}</template>
|
||||
</MkInput>
|
||||
|
|
@ -15,9 +15,9 @@
|
|||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p>
|
||||
<div class="buttons">
|
||||
<div class="buttons _buttons">
|
||||
<MkButton class="button" inline primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton class="button" inline @click="remove(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
|
||||
<MkButton class="button" inline danger @click="remove(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div>
|
||||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormRadios v-model="provider" class="_formBlock">
|
||||
<div class="_gaps_m">
|
||||
<FormRadios v-model="provider">
|
||||
<option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
|
||||
<option value="hcaptcha">hCaptcha</option>
|
||||
<option value="recaptcha">reCAPTCHA</option>
|
||||
|
|
@ -10,49 +10,49 @@
|
|||
</FormRadios>
|
||||
|
||||
<template v-if="provider === 'hcaptcha'">
|
||||
<FormInput v-model="hcaptchaSiteKey" class="_formBlock">
|
||||
<FormInput v-model="hcaptchaSiteKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="hcaptchaSecretKey" class="_formBlock">
|
||||
<FormInput v-model="hcaptchaSecretKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
|
||||
</FormInput>
|
||||
<FormSlot class="_formBlock">
|
||||
<FormSlot>
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
|
||||
</FormSlot>
|
||||
</template>
|
||||
<template v-else-if="provider === 'recaptcha'">
|
||||
<FormInput v-model="recaptchaSiteKey" class="_formBlock">
|
||||
<FormInput v-model="recaptchaSiteKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="recaptchaSecretKey" class="_formBlock">
|
||||
<FormInput v-model="recaptchaSecretKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
|
||||
</FormInput>
|
||||
<FormSlot v-if="recaptchaSiteKey" class="_formBlock">
|
||||
<FormSlot v-if="recaptchaSiteKey">
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/>
|
||||
</FormSlot>
|
||||
</template>
|
||||
<template v-else-if="provider === 'turnstile'">
|
||||
<FormInput v-model="turnstileSiteKey" class="_formBlock">
|
||||
<FormInput v-model="turnstileSiteKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.turnstileSiteKey }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="turnstileSecretKey" class="_formBlock">
|
||||
<FormInput v-model="turnstileSecretKey">
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.turnstileSecretKey }}</template>
|
||||
</FormInput>
|
||||
<FormSlot class="_formBlock">
|
||||
<FormSlot>
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha provider="turnstile" :sitekey="turnstileSiteKey || '1x00000000000000000000AA'"/>
|
||||
</FormSlot>
|
||||
</template>
|
||||
|
||||
<FormButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</FormButton>
|
||||
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</div>
|
||||
|
|
@ -62,7 +62,7 @@
|
|||
import { defineAsyncComponent } from 'vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormButton from '@/components/MkButton.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
import * as os from '@/os';
|
||||
|
|
|
|||
|
|
@ -3,40 +3,43 @@
|
|||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormSwitch v-model="enableEmail" class="_formBlock">
|
||||
<div class="_gaps_m">
|
||||
<FormSwitch v-model="enableEmail">
|
||||
<template #label>{{ i18n.ts.enableEmail }} ({{ i18n.ts.recommended }})</template>
|
||||
<template #caption>{{ i18n.ts.emailConfigInfo }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<template v-if="enableEmail">
|
||||
<FormInput v-model="email" type="email" class="_formBlock">
|
||||
<FormInput v-model="email" type="email">
|
||||
<template #label>{{ i18n.ts.emailAddress }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ i18n.ts.smtpConfig }}</template>
|
||||
<FormSplit :min-width="280">
|
||||
<FormInput v-model="smtpHost" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.smtpHost }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="smtpPort" type="number" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.smtpPort }}</template>
|
||||
</FormInput>
|
||||
</FormSplit>
|
||||
<FormSplit :min-width="280">
|
||||
<FormInput v-model="smtpUser" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.smtpUser }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="smtpPass" type="password" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.smtpPass }}</template>
|
||||
</FormInput>
|
||||
</FormSplit>
|
||||
<FormInfo class="_formBlock">{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
|
||||
<FormSwitch v-model="smtpSecure" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.smtpSecure }}</template>
|
||||
<template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<FormSplit :min-width="280">
|
||||
<FormInput v-model="smtpHost">
|
||||
<template #label>{{ i18n.ts.smtpHost }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="smtpPort" type="number">
|
||||
<template #label>{{ i18n.ts.smtpPort }}</template>
|
||||
</FormInput>
|
||||
</FormSplit>
|
||||
<FormSplit :min-width="280">
|
||||
<FormInput v-model="smtpUser">
|
||||
<template #label>{{ i18n.ts.smtpUser }}</template>
|
||||
</FormInput>
|
||||
<FormInput v-model="smtpPass" type="password">
|
||||
<template #label>{{ i18n.ts.smtpPass }}</template>
|
||||
</FormInput>
|
||||
</FormSplit>
|
||||
<FormInfo>{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
|
||||
<FormSwitch v-model="smtpSecure">
|
||||
<template #label>{{ i18n.ts.smtpSecure }}</template>
|
||||
<template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
|
||||
</FormSwitch>
|
||||
</div>
|
||||
</FormSection>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<XModalWindow
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="370"
|
||||
:with-ok-button="true"
|
||||
|
|
@ -9,28 +9,28 @@
|
|||
>
|
||||
<template #header>:{{ emoji.name }}:</template>
|
||||
|
||||
<div class="_monolithic_">
|
||||
<div class="yigymqpb _section">
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<div class="yigymqpb _gaps_m">
|
||||
<img :src="`/emoji/${emoji.name}.webp`" class="img"/>
|
||||
<MkInput v-model="name" class="_formBlock">
|
||||
<MkInput v-model="name">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="category" class="_formBlock" :datalist="categories">
|
||||
<MkInput v-model="category" :datalist="categories">
|
||||
<template #label>{{ i18n.ts.category }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="aliases" class="_formBlock">
|
||||
<MkInput v-model="aliases">
|
||||
<template #label>{{ i18n.ts.tags }}</template>
|
||||
<template #caption>{{ i18n.ts.setMultipleBySeparatingWithSpace }}</template>
|
||||
</MkInput>
|
||||
<MkButton danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</XModalWindow>
|
||||
</MkSpacer>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import * as os from '@/os';
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue