Merge branch 'develop' into serve-stream

This commit is contained in:
tamaina 2023-01-07 04:25:37 +00:00
commit 4aaa200145
246 changed files with 5082 additions and 1919 deletions

View 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"`);
}
}

View file

@ -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",

View file

@ -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,

View file

@ -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(

View 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)));
}
}

View 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)));
}
}

View file

@ -69,5 +69,7 @@ export const DI = {
adsRepository: Symbol('adsRepository'),
passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'),
retentionAggregationsRepository: Symbol('retentionAggregationsRepository'),
flashsRepository: Symbol('flashsRepository'),
flashLikesRepository: Symbol('flashLikesRepository'),
//#endregion
};

View file

@ -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);

View file

@ -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 {}

View 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;
}

View 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;
}

View file

@ -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>;

View file

@ -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,
];

View file

@ -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) {

View file

@ -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,

View file

@ -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],

View file

@ -8,7 +8,7 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j
export const meta = {
tags: ['admin'],
requireCredential: false,
requireCredential: true,
requireModerator: true,
res: {

View file

@ -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;
}

View 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);
});
}
}

View 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);
});
}
}

View 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);
});
}
}

View 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);
});
}
}

View 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);
});
}
}

View 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);
});
}
}

View 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);
});
}
}

View 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);
});
}
}

View 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,
});
});
}
}

View file

@ -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;

View file

@ -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,

View file

@ -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) => {

View file

@ -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')

View 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}`)

View file

@ -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"
}
}

View file

@ -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);

View file

@ -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>

View file

@ -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() {

View 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>

View file

@ -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}`;

View file

@ -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;

View file

@ -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: {

View file

@ -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>

View file

@ -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);

View 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>

View file

@ -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[]>([]);

View file

@ -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<{

View file

@ -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);

View file

@ -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;

View 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>

View file

@ -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');
}
},
},

View file

@ -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('');

View file

@ -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,

View file

@ -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: {

View file

@ -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,

View file

@ -217,6 +217,7 @@ onBeforeUnmount(() => {
content: "";
display: block;
position: absolute;
z-index: -1;
top: 0;
left: 0;
right: 0;

View file

@ -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;

View file

@ -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>

View file

@ -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;

View file

@ -4,7 +4,7 @@
v-show="!isDeleted"
ref="el"
v-hotkey="keymap"
class="lxwezrsl _block"
class="lxwezrsl"
:tabindex="!isDeleted ? '-1' : null"
:class="{ renote: isRenote }"
>

View file

@ -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);

View file

@ -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>

View file

@ -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(),

View file

@ -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>

View file

@ -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>>();

View file

@ -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;

View file

@ -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>

View file

@ -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', { });

View file

@ -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>

View file

@ -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>

View file

@ -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');

View file

@ -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>

View file

@ -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);

View file

@ -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%;

View file

@ -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({});

View file

@ -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>

View file

@ -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';

View file

@ -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;
}
}

View file

@ -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<{

View file

@ -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 {

View file

@ -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();

View file

@ -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>

View file

@ -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>

View file

@ -1,5 +1,5 @@
<template>
<div class="terlnhxf _formBlock">
<div class="terlnhxf">
<slot></slot>
</div>
</template>

View file

@ -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"');
}

View file

@ -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>

View file

@ -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';

View file

@ -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) {
// 許可を得ていなかったらリクエスト

View file

@ -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(() => {

View 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),
};

View file

@ -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();

View file

@ -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;
});

View file

@ -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;

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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';

View file

@ -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>

View file

@ -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