Merge branch 'develop' into bh-worker

This commit is contained in:
syuilo 2023-05-17 14:12:40 +09:00
commit c83c015357
145 changed files with 2804 additions and 4420 deletions

View file

@ -12,6 +12,16 @@
-->
## 13.x.x (unreleased)
### General
- タイムラインにフォロイーの行った他人へのリプライを含めるかどうかの設定をアカウントに保存するのをやめるように
- 今後はAPI呼び出し時およびストリーミング接続時に設定するようになります
### Client
- 開発者モードを追加
- AiScriptを0.13.3に更新
## 13.12.2
## NOTE

View file

@ -52,6 +52,8 @@ addToList: "リストに追加"
sendMessage: "メッセージを送信"
copyRSS: "RSSをコピー"
copyUsername: "ユーザー名をコピー"
copyUserId: "ユーザーIDをコピー"
copyNoteId: "ートIDをコピー"
searchUser: "ユーザーを検索"
reply: "返信"
loadMore: "もっと見る"
@ -823,6 +825,7 @@ translatedFrom: "{x}から翻訳"
accountDeletionInProgress: "アカウントの削除が進行中です"
usernameInfo: "サーバー上であなたのアカウントを一意に識別するための名前。アルファベット(a~z, A~Z)、数字(0~9)、およびアンダーバー(_)が使用できます。ユーザー名は後から変更することは出来ません。"
aiChanMode: "藍モード"
devMode: "開発者モード"
keepCw: "CWを維持する"
pubSub: "Pub/Subのアカウント"
lastCommunication: "直近の通信"

View file

@ -0,0 +1,11 @@
export class RemoveShowTimelineReplies1684206886988 {
name = 'RemoveShowTimelineReplies1684206886988'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "showTimelineReplies"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "showTimelineReplies" boolean NOT NULL DEFAULT false`);
}
}

View file

@ -144,7 +144,7 @@ export function loadConfig() {
const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json');
const clientManifest = clientManifestExists ?
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8'))
: { 'src/init.ts': { file: 'src/init.ts' } };
: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
const mixin = {} as Mixin;
@ -165,7 +165,7 @@ export function loadConfig() {
mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
mixin.userAgent = `Misskey/${meta.version} (${config.url})`;
mixin.clientEntry = clientManifest['src/init.ts'];
mixin.clientEntry = clientManifest['src/_boot_.ts'];
mixin.clientManifestExists = clientManifestExists;
const externalMediaProxy = config.mediaProxy ?

View file

@ -208,7 +208,7 @@ export class QueryService {
}
@bindThis
public generateRepliesQuery(q: SelectQueryBuilder<any>, me?: Pick<User, 'id' | 'showTimelineReplies'> | null): void {
public generateRepliesQuery(q: SelectQueryBuilder<any>, withReplies: boolean, me?: Pick<User, 'id'> | null): void {
if (me == null) {
q.andWhere(new Brackets(qb => { qb
.where('note.replyId IS NULL') // 返信ではない
@ -217,7 +217,7 @@ export class QueryService {
.andWhere('note.replyUserId = note.userId');
}));
}));
} else if (!me.showTimelineReplies) {
} else if (!withReplies) {
q.andWhere(new Brackets(qb => { qb
.where('note.replyId IS NULL') // 返信ではない
.orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信

View file

@ -32,6 +32,8 @@ import type { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { AccountMoveService } from '@/core/AccountMoveService.js';
import { checkHttps } from '@/misc/check-https.js';
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
import { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common';
@ -42,8 +44,6 @@ import type { ApLoggerService } from '../ApLoggerService.js';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import type { ApImageService } from './ApImageService.js';
import type { IActor, IObject } from '../type.js';
import type { AccountMoveService } from '@/core/AccountMoveService.js';
import { checkHttps } from '@/misc/check-https.js';
const nameLength = 128;
const summaryLength = 2048;
@ -306,7 +306,6 @@ export class ApPersonService implements OnModuleInit {
tags,
isBot,
isCat: (person as any).isCat === true,
showTimelineReplies: false,
})) as RemoteUser;
await transactionalEntityManager.save(new UserProfile({
@ -696,7 +695,7 @@ export class ApPersonService implements OnModuleInit {
if (!dst.alsoKnownAs || dst.alsoKnownAs.length === 0) {
return 'skip: dst.alsoKnownAs is empty';
}
if (!dst.alsoKnownAs?.includes(src.uri)) {
if (!dst.alsoKnownAs.includes(src.uri)) {
return 'skip: alsoKnownAs does not include from.uri';
}

View file

@ -466,7 +466,6 @@ export class UserEntityService implements OnModuleInit {
mutedInstances: profile!.mutedInstances,
mutingNotificationTypes: profile!.mutingNotificationTypes,
emailNotificationTypes: profile!.emailNotificationTypes,
showTimelineReplies: user.showTimelineReplies ?? falsy,
achievements: profile!.achievements,
loggedInDays: profile!.loggedInDates.length,
policies: this.roleService.getUserPolicies(user.id),

View file

@ -232,12 +232,6 @@ export class User {
})
public followersUri: string | null;
@Column('boolean', {
default: false,
comment: 'Whether to show users replying to other users in the timeline.',
})
public showTimelineReplies: boolean;
@Index({ unique: true })
@Column('char', {
length: 16, nullable: true, unique: true,

View file

@ -141,7 +141,6 @@ export const paramDef = {
preventAiLearning: { type: 'boolean' },
isBot: { type: 'boolean' },
isCat: { type: 'boolean' },
showTimelineReplies: { type: 'boolean' },
injectFeaturedNote: { type: 'boolean' },
receiveAnnouncementEmail: { type: 'boolean' },
alwaysMarkNsfw: { type: 'boolean' },
@ -239,7 +238,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus;
if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions;
if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
if (typeof ps.showTimelineReplies === 'boolean') updates.showTimelineReplies = ps.showTimelineReplies;
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;

View file

@ -34,11 +34,8 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
withFiles: {
type: 'boolean',
default: false,
description: 'Only show notes that have attached files.',
},
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
@ -78,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateRepliesQuery(query, me);
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
if (me) {
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);

View file

@ -46,11 +46,8 @@ export const paramDef = {
includeMyRenotes: { type: 'boolean', default: true },
includeRenotedMyNotes: { type: 'boolean', default: true },
includeLocalRenotes: { type: 'boolean', default: true },
withFiles: {
type: 'boolean',
default: false,
description: 'Only show notes that have attached files.',
},
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
},
required: [],
} as const;
@ -98,7 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.setParameters(followingQuery.getParameters());
this.queryService.generateChannelQuery(query, me);
this.queryService.generateRepliesQuery(query, me);
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);

View file

@ -36,11 +36,8 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
withFiles: {
type: 'boolean',
default: false,
description: 'Only show notes that have attached files.',
},
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
fileType: { type: 'array', items: {
type: 'string',
} },
@ -86,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateChannelQuery(query, me);
this.queryService.generateRepliesQuery(query, me);
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateMutedNoteQuery(query, me);

View file

@ -35,11 +35,8 @@ export const paramDef = {
includeMyRenotes: { type: 'boolean', default: true },
includeRenotedMyNotes: { type: 'boolean', default: true },
includeLocalRenotes: { type: 'boolean', default: true },
withFiles: {
type: 'boolean',
default: false,
description: 'Only show notes that have attached files.',
},
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
},
required: [],
} as const;
@ -84,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
this.queryService.generateChannelQuery(query, me);
this.queryService.generateRepliesQuery(query, me);
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateMutedNoteQuery(query, me);

View file

@ -13,6 +13,7 @@ class GlobalTimelineChannel extends Channel {
public readonly chName = 'globalTimeline';
public static shouldShare = true;
public static requireCredential = false;
private withReplies: boolean;
constructor(
private metaService: MetaService,
@ -31,6 +32,8 @@ class GlobalTimelineChannel extends Channel {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.gtlAvailable) return;
this.withReplies = params.withReplies as boolean;
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
}
@ -54,7 +57,7 @@ class GlobalTimelineChannel extends Channel {
}
// 関係ない返信は除外
if (note.reply && !this.user!.showTimelineReplies) {
if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;

View file

@ -11,6 +11,7 @@ class HomeTimelineChannel extends Channel {
public readonly chName = 'homeTimeline';
public static shouldShare = true;
public static requireCredential = true;
private withReplies: boolean;
constructor(
private noteEntityService: NoteEntityService,
@ -24,6 +25,8 @@ class HomeTimelineChannel extends Channel {
@bindThis
public async init(params: any) {
this.withReplies = params.withReplies as boolean;
this.subscriber.on('notesStream', this.onNote);
}
@ -63,7 +66,7 @@ class HomeTimelineChannel extends Channel {
}
// 関係ない返信は除外
if (note.reply && !this.user!.showTimelineReplies) {
if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;

View file

@ -13,6 +13,7 @@ class HybridTimelineChannel extends Channel {
public readonly chName = 'hybridTimeline';
public static shouldShare = true;
public static requireCredential = true;
private withReplies: boolean;
constructor(
private metaService: MetaService,
@ -31,6 +32,8 @@ class HybridTimelineChannel extends Channel {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return;
this.withReplies = params.withReplies as boolean;
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
}
@ -75,7 +78,7 @@ class HybridTimelineChannel extends Channel {
if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances ?? []))) return;
// 関係ない返信は除外
if (note.reply && !this.user!.showTimelineReplies) {
if (note.reply && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;

View file

@ -12,6 +12,7 @@ class LocalTimelineChannel extends Channel {
public readonly chName = 'localTimeline';
public static shouldShare = true;
public static requireCredential = false;
private withReplies: boolean;
constructor(
private metaService: MetaService,
@ -30,6 +31,8 @@ class LocalTimelineChannel extends Channel {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return;
this.withReplies = params.withReplies as boolean;
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
}
@ -54,7 +57,7 @@ class LocalTimelineChannel extends Channel {
}
// 関係ない返信は除外
if (note.reply && this.user && !this.user.showTimelineReplies) {
if (note.reply && this.user && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;

View file

@ -246,7 +246,7 @@ export default class Connection {
const ch: Channel = channelService.create(id, this);
this.channels.push(ch);
ch.init(params);
ch.init(params ?? {});
if (pong) {
this.sendMessageToWs('connected', {

View file

@ -43,7 +43,6 @@ describe('ユーザー', () => {
type MeDetailed = UserDetailedNotMe &
misskey.entities.MeDetailed & {
showTimelineReplies: boolean,
achievements: object[],
loggedInDays: number,
policies: object,
@ -160,7 +159,6 @@ describe('ユーザー', () => {
mutedInstances: user.mutedInstances,
mutingNotificationTypes: user.mutingNotificationTypes,
emailNotificationTypes: user.emailNotificationTypes,
showTimelineReplies: user.showTimelineReplies,
achievements: user.achievements,
loggedInDays: user.loggedInDays,
policies: user.policies,
@ -406,7 +404,6 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response.mutedInstances, []);
assert.deepStrictEqual(response.mutingNotificationTypes, []);
assert.deepStrictEqual(response.emailNotificationTypes, ['follow', 'receiveFollowRequest']);
assert.strictEqual(response.showTimelineReplies, false);
assert.deepStrictEqual(response.achievements, []);
assert.deepStrictEqual(response.loggedInDays, 0);
assert.deepStrictEqual(response.policies, DEFAULT_POLICIES);
@ -470,8 +467,6 @@ describe('ユーザー', () => {
{ parameters: (): object => ({ isBot: false }) },
{ parameters: (): object => ({ isCat: true }) },
{ parameters: (): object => ({ isCat: false }) },
{ parameters: (): object => ({ showTimelineReplies: true }) },
{ parameters: (): object => ({ showTimelineReplies: false }) },
{ parameters: (): object => ({ injectFeaturedNote: true }) },
{ parameters: (): object => ({ injectFeaturedNote: false }) },
{ parameters: (): object => ({ receiveAnnouncementEmail: true }) },

View file

@ -64,6 +64,7 @@ module.exports = {
'vue/singleline-html-element-content-newline': 'off',
// (vue/vue3-recommended disabled the autofix for Vue 2 compatibility)
'vue/v-on-event-hyphenation': ['warn', 'always', { autofix: true }],
'vue/attribute-hyphenation': ['warn', 'never'],
},
globals: {
// Node.js

View file

@ -19,12 +19,12 @@
"@rollup/plugin-json": "6.0.0",
"@rollup/plugin-replace": "5.0.2",
"@rollup/pluginutils": "5.0.2",
"@syuilo/aiscript": "0.13.2",
"@syuilo/aiscript": "0.13.3",
"@tabler/icons-webfont": "2.17.0",
"@vitejs/plugin-vue": "4.2.2",
"@vue-macros/reactivity-transform": "0.3.6",
"@vue/compiler-sfc": "3.3.1",
"autosize": "5.0.2",
"@vitejs/plugin-vue": "4.2.3",
"@vue-macros/reactivity-transform": "0.3.7",
"@vue/compiler-sfc": "3.3.2",
"autosize": "6.0.1",
"broadcast-channel": "4.20.2",
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
"buraha": "github:misskey-dev/buraha",
@ -53,7 +53,7 @@
"punycode": "2.3.0",
"querystring": "0.2.1",
"rndstr": "1.0.0",
"rollup": "3.21.6",
"rollup": "3.22.0",
"s-age": "1.1.2",
"sanitize-html": "2.10.0",
"sass": "1.62.1",
@ -70,31 +70,31 @@
"typescript": "5.0.4",
"uuid": "9.0.0",
"vanilla-tilt": "1.8.0",
"vite": "4.3.5",
"vue": "3.3.1",
"vite": "4.3.7",
"vue": "3.3.2",
"vue-plyr": "7.0.0",
"vue-prism-editor": "2.0.0-alpha.2",
"vuedraggable": "next"
},
"devDependencies": {
"@storybook/addon-actions": "7.0.10",
"@storybook/addon-essentials": "7.0.10",
"@storybook/addon-interactions": "7.0.10",
"@storybook/addon-links": "7.0.10",
"@storybook/addon-storysource": "7.0.10",
"@storybook/addons": "7.0.10",
"@storybook/blocks": "7.0.10",
"@storybook/core-events": "7.0.10",
"@storybook/addon-actions": "7.0.12",
"@storybook/addon-essentials": "7.0.12",
"@storybook/addon-interactions": "7.0.12",
"@storybook/addon-links": "7.0.12",
"@storybook/addon-storysource": "7.0.12",
"@storybook/addons": "7.0.12",
"@storybook/blocks": "7.0.12",
"@storybook/core-events": "7.0.12",
"@storybook/jest": "0.1.0",
"@storybook/manager-api": "7.0.10",
"@storybook/preview-api": "7.0.10",
"@storybook/react": "7.0.10",
"@storybook/react-vite": "7.0.10",
"@storybook/manager-api": "7.0.12",
"@storybook/preview-api": "7.0.12",
"@storybook/react": "7.0.12",
"@storybook/react-vite": "7.0.12",
"@storybook/testing-library": "0.1.0",
"@storybook/theming": "7.0.10",
"@storybook/types": "7.0.10",
"@storybook/vue3": "7.0.10",
"@storybook/vue3-vite": "7.0.10",
"@storybook/theming": "7.0.12",
"@storybook/types": "7.0.12",
"@storybook/vue3": "7.0.12",
"@storybook/vue3-vite": "7.0.12",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/vue": "7.0.0",
"@types/escape-regexp": "0.0.1",
@ -103,7 +103,7 @@
"@types/gulp-rename": "2.0.2",
"@types/matter-js": "0.18.3",
"@types/micromatch": "4.0.2",
"@types/node": "20.1.3",
"@types/node": "20.1.7",
"@types/punycode": "2.1.0",
"@types/sanitize-html": "2.9.0",
"@types/seedrandom": "3.0.5",
@ -116,16 +116,16 @@
"@typescript-eslint/eslint-plugin": "5.59.5",
"@typescript-eslint/parser": "5.59.5",
"@vitest/coverage-c8": "0.31.0",
"@vue/runtime-core": "3.3.1",
"@vue/runtime-core": "3.3.2",
"astring": "1.8.4",
"chokidar-cli": "3.0.0",
"cross-env": "7.0.3",
"cypress": "12.12.0",
"eslint": "8.40.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-vue": "9.12.0",
"eslint-plugin-vue": "9.13.0",
"fast-glob": "3.2.12",
"happy-dom": "9.16.0",
"happy-dom": "9.18.3",
"micromatch": "3.1.10",
"msw": "1.2.1",
"msw-storybook-addon": "1.8.0",
@ -133,13 +133,13 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"start-server-and-test": "2.0.0",
"storybook": "7.0.10",
"storybook": "7.0.12",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"summaly": "github:misskey-dev/summaly",
"vite-plugin-turbosnap": "1.0.2",
"vitest": "0.31.0",
"vitest-fetch-mock": "0.2.2",
"vue-eslint-parser": "9.2.1",
"vue-tsc": "1.6.4"
"vue-eslint-parser": "9.3.0",
"vue-tsc": "1.6.5"
}
}

View file

@ -0,0 +1,12 @@
// https://vitejs.dev/config/build-options.html#build-modulepreload
import 'vite/modulepreload-polyfill';
import '@/style.scss';
import { mainBoot } from './boot/main-boot';
import { subBoot } from './boot/sub-boot';
if (['/share', '/auth', '/miauth'].includes(location.pathname)) {
subBoot();
} else {
mainBoot();
}

View file

@ -0,0 +1,263 @@
import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent, App } from 'vue';
import { compareVersions } from 'compare-versions';
import JSON5 from 'json5';
import widgets from '@/widgets';
import directives from '@/directives';
import components from '@/components';
import { version, ui, lang, updateLocale } from '@/config';
import { applyTheme } from '@/scripts/theme';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
import { i18n, updateI18n } from '@/i18n';
import { confirm, alert, post, popup, toast } from '@/os';
import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
import { defaultStore, ColdDeviceStorage } from '@/store';
import { fetchInstance, instance } from '@/instance';
import { deviceKind } from '@/scripts/device-kind';
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 { deckStore } from '@/ui/deck/deck-store';
import { miLocalStorage } from '@/local-storage';
import { fetchCustomEmojis } from '@/custom-emojis';
import { mainRouter } from '@/router';
export async function common(createVue: () => App<Element>) {
console.info(`Misskey v${version}`);
if (_DEV_) {
console.warn('Development mode!!!');
console.info(`vue ${vueVersion}`);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).$i = $i;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).$store = defaultStore;
window.addEventListener('error', event => {
console.error(event);
/*
alert({
type: 'error',
title: 'DEV: Unhandled error',
text: event.message
});
*/
});
window.addEventListener('unhandledrejection', event => {
console.error(event);
/*
alert({
type: 'error',
title: 'DEV: Unhandled promise rejection',
text: event.reason
});
*/
});
}
const splash = document.getElementById('splash');
// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
if (splash) splash.addEventListener('transitionend', () => {
splash.remove();
});
let isClientUpdated = false;
//#region クライアントが更新されたかチェック
const lastVersion = miLocalStorage.getItem('lastVersion');
if (lastVersion !== version) {
miLocalStorage.setItem('lastVersion', version);
// テーマリビルドするため
miLocalStorage.removeItem('theme');
try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
isClientUpdated = true;
}
} catch (err) { /* empty */ }
}
//#endregion
//#region Detect language & fetch translations
const localeVersion = miLocalStorage.getItem('localeVersion');
const localeOutdated = (localeVersion == null || localeVersion !== version);
if (localeOutdated) {
const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
if (res.status === 200) {
const newLocale = await res.text();
const parsedNewLocale = JSON.parse(newLocale);
miLocalStorage.setItem('locale', newLocale);
miLocalStorage.setItem('localeVersion', version);
updateLocale(parsedNewLocale);
updateI18n(parsedNewLocale);
}
}
//#endregion
// タッチデバイスでCSSの:hoverを機能させる
document.addEventListener('touchend', () => {}, { passive: true });
// 一斉リロード
reloadChannel.addEventListener('message', path => {
if (path !== null) location.href = path;
else location.reload();
});
// If mobile, insert the viewport meta tag
if (['smartphone', 'tablet'].includes(deviceKind)) {
const viewport = document.getElementsByName('viewport').item(0);
viewport.setAttribute('content',
`${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`);
}
//#region Set lang attr
const html = document.documentElement;
html.setAttribute('lang', lang);
//#endregion
await defaultStore.ready;
await deckStore.ready;
const fetchInstanceMetaPromise = fetchInstance();
fetchInstanceMetaPromise.then(() => {
miLocalStorage.setItem('v', instance.version);
});
//#region loginId
const params = new URLSearchParams(location.search);
const loginId = params.get('loginId');
if (loginId) {
const target = getUrlWithoutLoginId(location.href);
if (!$i || $i.id !== loginId) {
const account = await getAccountFromId(loginId);
if (account) {
await login(account.token, target);
}
}
history.replaceState({ misskey: 'loginId' }, '', target);
}
//#endregion
// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
watch(defaultStore.reactiveState.darkMode, (darkMode) => {
applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
}, { immediate: miLocalStorage.getItem('theme') == null });
const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
watch(darkTheme, (theme) => {
if (defaultStore.state.darkMode) {
applyTheme(theme);
}
});
watch(lightTheme, (theme) => {
if (!defaultStore.state.darkMode) {
applyTheme(theme);
}
});
//#region Sync dark mode
if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
defaultStore.set('darkMode', isDeviceDarkmode());
}
window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
defaultStore.set('darkMode', mql.matches);
}
});
//#endregion
fetchInstanceMetaPromise.then(() => {
if (defaultStore.state.themeInitial) {
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme));
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme));
defaultStore.set('themeInitial', false);
}
});
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
}, { immediate: true });
watch(defaultStore.reactiveState.useBlurEffect, v => {
if (v) {
document.documentElement.style.removeProperty('--blur');
} else {
document.documentElement.style.setProperty('--blur', 'none');
}
}, { immediate: true });
//#region Fetch user
if ($i && $i.token) {
if (_DEV_) {
console.log('account cache found. refreshing...');
}
refreshAccount();
}
//#endregion
try {
await fetchCustomEmojis();
} catch (err) { /* empty */ }
const app = createVue();
if (_DEV_) {
app.config.performance = true;
}
widgets(app);
directives(app);
components(app);
// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210
// なぜか2回実行されることがあるため、mountするdivを1つに制限する
const rootEl = ((): HTMLElement => {
const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID);
if (currentRoot) {
console.warn('multiple import detected');
return currentRoot;
}
const root = document.createElement('div');
root.id = MISSKEY_MOUNT_DIV_ID;
document.body.appendChild(root);
return root;
})();
app.mount(rootEl);
// boot.jsのやつを解除
window.onerror = null;
window.onunhandledrejection = null;
removeSplash();
return {
isClientUpdated,
app,
};
}
function removeSplash() {
const splash = document.getElementById('splash');
if (splash) {
splash.style.opacity = '0';
splash.style.pointerEvents = 'none';
}
}

View file

@ -0,0 +1,254 @@
import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue';
import { common } from './common';
import { version, ui, lang, updateLocale } from '@/config';
import { i18n, updateI18n } from '@/i18n';
import { confirm, alert, post, popup, toast } from '@/os';
import { useStream } from '@/stream';
import * as sound from '@/scripts/sound';
import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
import { defaultStore, ColdDeviceStorage } from '@/store';
import { makeHotkey } from '@/scripts/hotkey';
import { reactionPicker } from '@/scripts/reaction-picker';
import { miLocalStorage } from '@/local-storage';
import { claimAchievement, claimedAchievements } from '@/scripts/achievements';
import { mainRouter } from '@/router';
import { initializeSw } from '@/scripts/initialize-sw';
export async function mainBoot() {
const { isClientUpdated } = await common(() => createApp(
new URLSearchParams(window.location.search).has('zen') ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
defineAsyncComponent(() => import('@/ui/universal.vue')),
));
reactionPicker.init();
if (isClientUpdated && $i) {
popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed');
}
const stream = useStream();
let reloadDialogShowing = false;
stream.on('_disconnected_', async () => {
if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
location.reload();
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
if (reloadDialogShowing) return;
reloadDialogShowing = true;
const { canceled } = await confirm({
type: 'warning',
title: i18n.ts.disconnectedFromServer,
text: i18n.ts.reloadConfirm,
});
reloadDialogShowing = false;
if (!canceled) {
location.reload();
}
}
});
for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
import('../plugin').then(async ({ install }) => {
// Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740
await new Promise(r => setTimeout(r, 0));
install(plugin);
});
}
const hotkeys = {
'd': (): void => {
defaultStore.set('darkMode', !defaultStore.state.darkMode);
},
's': (): void => {
mainRouter.push('/search');
},
};
if ($i) {
// only add post shortcuts if logged in
hotkeys['p|n'] = post;
defaultStore.loaded.then(() => {
if (defaultStore.state.accountSetupWizard !== -1) {
popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed');
}
});
if ($i.isDeleted) {
alert({
type: 'warning',
text: i18n.ts.accountDeletionInProgress,
});
}
const now = new Date();
const m = now.getMonth() + 1;
const d = now.getDate();
if ($i.birthday) {
const bm = parseInt($i.birthday.split('-')[1]);
const bd = parseInt($i.birthday.split('-')[2]);
if (m === bm && d === bd) {
claimAchievement('loggedInOnBirthday');
}
}
if (m === 1 && d === 1) {
claimAchievement('loggedInOnNewYearsDay');
}
if ($i.loggedInDays >= 3) claimAchievement('login3');
if ($i.loggedInDays >= 7) claimAchievement('login7');
if ($i.loggedInDays >= 15) claimAchievement('login15');
if ($i.loggedInDays >= 30) claimAchievement('login30');
if ($i.loggedInDays >= 60) claimAchievement('login60');
if ($i.loggedInDays >= 100) claimAchievement('login100');
if ($i.loggedInDays >= 200) claimAchievement('login200');
if ($i.loggedInDays >= 300) claimAchievement('login300');
if ($i.loggedInDays >= 400) claimAchievement('login400');
if ($i.loggedInDays >= 500) claimAchievement('login500');
if ($i.loggedInDays >= 600) claimAchievement('login600');
if ($i.loggedInDays >= 700) claimAchievement('login700');
if ($i.loggedInDays >= 800) claimAchievement('login800');
if ($i.loggedInDays >= 900) claimAchievement('login900');
if ($i.loggedInDays >= 1000) claimAchievement('login1000');
if ($i.notesCount > 0) claimAchievement('notes1');
if ($i.notesCount >= 10) claimAchievement('notes10');
if ($i.notesCount >= 100) claimAchievement('notes100');
if ($i.notesCount >= 500) claimAchievement('notes500');
if ($i.notesCount >= 1000) claimAchievement('notes1000');
if ($i.notesCount >= 5000) claimAchievement('notes5000');
if ($i.notesCount >= 10000) claimAchievement('notes10000');
if ($i.notesCount >= 20000) claimAchievement('notes20000');
if ($i.notesCount >= 30000) claimAchievement('notes30000');
if ($i.notesCount >= 40000) claimAchievement('notes40000');
if ($i.notesCount >= 50000) claimAchievement('notes50000');
if ($i.notesCount >= 60000) claimAchievement('notes60000');
if ($i.notesCount >= 70000) claimAchievement('notes70000');
if ($i.notesCount >= 80000) claimAchievement('notes80000');
if ($i.notesCount >= 90000) claimAchievement('notes90000');
if ($i.notesCount >= 100000) claimAchievement('notes100000');
if ($i.followersCount > 0) claimAchievement('followers1');
if ($i.followersCount >= 10) claimAchievement('followers10');
if ($i.followersCount >= 50) claimAchievement('followers50');
if ($i.followersCount >= 100) claimAchievement('followers100');
if ($i.followersCount >= 300) claimAchievement('followers300');
if ($i.followersCount >= 500) claimAchievement('followers500');
if ($i.followersCount >= 1000) claimAchievement('followers1000');
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) {
claimAchievement('passedSinceAccountCreated1');
}
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) {
claimAchievement('passedSinceAccountCreated2');
}
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) {
claimAchievement('passedSinceAccountCreated3');
}
if (claimedAchievements.length >= 30) {
claimAchievement('collectAchievements30');
}
window.setInterval(() => {
if (Math.floor(Math.random() * 20000) === 0) {
claimAchievement('justPlainLucky');
}
}, 1000 * 10);
window.setTimeout(() => {
claimAchievement('client30min');
}, 1000 * 60 * 30);
window.setTimeout(() => {
claimAchievement('client60min');
}, 1000 * 60 * 60);
const lastUsed = miLocalStorage.getItem('lastUsed');
if (lastUsed) {
const lastUsedDate = parseInt(lastUsed, 10);
// 二時間以上前なら
if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
toast(i18n.t('welcomeBackWithName', {
name: $i.name || $i.username,
}));
}
}
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))) && !location.pathname.startsWith('/miauth')) {
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) {
// 許可を得ていなかったらリクエスト
if (Notification.permission === 'default') {
Notification.requestPermission();
}
}
const main = markRaw(stream.useChannel('main', null, 'System'));
// 自分の情報が更新されたとき
main.on('meUpdated', i => {
updateAccount(i);
});
main.on('readAllNotifications', () => {
updateAccount({ hasUnreadNotification: false });
});
main.on('unreadNotification', () => {
updateAccount({ hasUnreadNotification: true });
});
main.on('unreadMention', () => {
updateAccount({ hasUnreadMentions: true });
});
main.on('readAllUnreadMentions', () => {
updateAccount({ hasUnreadMentions: false });
});
main.on('unreadSpecifiedNote', () => {
updateAccount({ hasUnreadSpecifiedNotes: true });
});
main.on('readAllUnreadSpecifiedNotes', () => {
updateAccount({ hasUnreadSpecifiedNotes: false });
});
main.on('readAllAntennas', () => {
updateAccount({ hasUnreadAntenna: false });
});
main.on('unreadAntenna', () => {
updateAccount({ hasUnreadAntenna: true });
sound.play('antenna');
});
main.on('readAllAnnouncements', () => {
updateAccount({ hasUnreadAnnouncement: false });
});
// トークンが再生成されたとき
// このままではMisskeyが利用できないので強制的にサインアウトさせる
main.on('myTokenRegenerated', () => {
signout();
});
}
// shortcut
document.addEventListener('keydown', makeHotkey(hotkeys));
initializeSw();
}

View file

@ -0,0 +1,8 @@
import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue';
import { common } from './common';
export async function subBoot() {
const { isClientUpdated } = await common(() => createApp(
defineAsyncComponent(() => import('@/ui/minimum.vue')),
));
}

View file

@ -9,7 +9,7 @@
</I18n>
</template>
<MkSpacer :margin-min="20" :margin-max="28">
<div class="dpvffvvy _gaps_m">
<div class="_gaps_m" :class="$style.root">
<div class="">
<MkTextarea v-model="comment">
<template #label>{{ i18n.ts.details }}</template>
@ -60,8 +60,8 @@ function send() {
}
</script>
<style lang="scss" scoped>
.dpvffvvy {
<style lang="scss" module>
.root {
--root-margin: 16px;
}
</style>

View file

@ -0,0 +1,243 @@
<template>
<canvas ref="canvasEl" style="width: 100%; height: 100%; pointer-events: none;"></canvas>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, shallowRef } from 'vue';
import isChromatic from 'chromatic/isChromatic';
const canvasEl = shallowRef<HTMLCanvasElement>();
const props = withDefaults(defineProps<{
scale?: number;
focus?: number;
}>(), {
scale: 1.0,
focus: 1.0,
});
function loadShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert(
`falied to compile shader: ${gl.getShaderInfoLog(shader)}`,
);
gl.deleteShader(shader);
return null;
}
return shader;
}
function initShaderProgram(gl, vsSource, fsSource) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert(
`failed to init shader: ${gl.getProgramInfoLog(
shaderProgram,
)}`,
);
return null;
}
return shaderProgram;
}
let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
onMounted(() => {
const canvas = canvasEl.value!;
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
const gl = canvas.getContext('webgl', { premultipliedAlpha: true });
if (gl == null) return;
gl.clearColor(0.0, 0.0, 0.0, 0.0);
gl.clear(gl.COLOR_BUFFER_BIT);
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const shaderProgram = initShaderProgram(gl, `
attribute vec2 vertex;
uniform vec2 u_scale;
varying vec2 v_pos;
void main() {
gl_Position = vec4(vertex, 0.0, 1.0);
v_pos = vertex / u_scale;
}
`, `
precision mediump float;
vec3 mod289(vec3 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec2 mod289(vec2 x) {
return x - floor(x * (1.0 / 289.0)) * 289.0;
}
vec3 permute(vec3 x) {
return mod289(((x*34.0)+1.0)*x);
}
float snoise(vec2 v) {
const vec4 C = vec4(0.211324865405187,
0.366025403784439,
-0.577350269189626,
0.024390243902439);
vec2 i = floor(v + dot(v, C.yy) );
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod289(i);
vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
+ i.x + vec3(0.0, i1.x, 1.0 ));
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
m = m*m ;
m = m*m ;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
uniform float u_time;
uniform vec2 u_resolution;
uniform float u_spread;
uniform float u_speed;
uniform float u_warp;
uniform float u_focus;
uniform float u_itensity;
varying vec2 v_pos;
float circle( in vec2 _pos, in vec2 _origin, in float _radius ) {
float SPREAD = 0.7 * u_spread;
float SPEED = 0.00055 * u_speed;
float WARP = 1.5 * u_warp;
float FOCUS = 1.15 * u_focus;
vec2 dist = _pos - _origin;
float distortion = snoise( vec2(
_pos.x * 1.587 * WARP + u_time * SPEED * 0.5,
_pos.y * 1.192 * WARP + u_time * SPEED * 0.3
) ) * 0.5 + 0.5;
float feather = 0.01 + SPREAD * pow( distortion, FOCUS );
return 1.0 - smoothstep(
_radius - ( _radius * feather ),
_radius + ( _radius * feather ),
dot( dist, dist ) * 4.0
);
}
void main() {
vec3 green = vec3( 1.0 ) - vec3( 153.0 / 255.0, 211.0 / 255.0, 221.0 / 255.0 );
vec3 purple = vec3( 1.0 ) - vec3( 195.0 / 255.0, 165.0 / 255.0, 242.0 / 255.0 );
vec3 orange = vec3( 1.0 ) - vec3( 255.0 / 255.0, 156.0 / 255.0, 136.0 / 255.0 );
float ratio = u_resolution.x / u_resolution.y;
vec2 uv = vec2( v_pos.x, v_pos.y / ratio ) * 0.5 + 0.5;
vec3 color = vec3( 0.0 );
float greenMix = snoise( v_pos * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5;
float purpleMix = snoise( v_pos * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5;
float orangeMix = snoise( v_pos * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5;
float alphaOne = 0.35 + 0.65 * pow( snoise( vec2( u_time * 0.00012, uv.x ) ) * 0.5 + 0.5, 1.2 );
float alphaTwo = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 1561.0 ) * 0.00014, uv.x ) ) * 0.5 + 0.5, 1.2 );
float alphaThree = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 3917.0 ) * 0.00013, uv.x ) ) * 0.5 + 0.5, 1.2 );
color += vec3( circle( uv, vec2( 0.22 + sin( u_time * 0.000201 ) * 0.06, 0.80 + cos( u_time * 0.000151 ) * 0.06 ), 0.15 ) ) * alphaOne * ( purple * purpleMix + orange * orangeMix );
color += vec3( circle( uv, vec2( 0.90 + cos( u_time * 0.000166 ) * 0.06, 0.42 + sin( u_time * 0.000138 ) * 0.06 ), 0.18 ) ) * alphaTwo * ( green * greenMix + purple * purpleMix );
color += vec3( circle( uv, vec2( 0.19 + sin( u_time * 0.000112 ) * 0.06, 0.25 + sin( u_time * 0.000192 ) * 0.06 ), 0.09 ) ) * alphaThree * ( orange * orangeMix );
color *= u_itensity + 1.0 * pow( snoise( vec2( v_pos.y + u_time * 0.00013, v_pos.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 );
vec3 inverted = vec3( 1.0 ) - color;
gl_FragColor = vec4( color, max(max(color.x, color.y), color.z) );
}
`);
gl.useProgram(shaderProgram);
const u_resolution = gl.getUniformLocation(shaderProgram, 'u_resolution');
const u_time = gl.getUniformLocation(shaderProgram, 'u_time');
const u_spread = gl.getUniformLocation(shaderProgram, 'u_spread');
const u_speed = gl.getUniformLocation(shaderProgram, 'u_speed');
const u_warp = gl.getUniformLocation(shaderProgram, 'u_warp');
const u_focus = gl.getUniformLocation(shaderProgram, 'u_focus');
const u_itensity = gl.getUniformLocation(shaderProgram, 'u_itensity');
const u_scale = gl.getUniformLocation(shaderProgram, 'u_scale');
gl.uniform2fv(u_resolution, [canvas.width, canvas.height]);
gl.uniform1f(u_spread, 1.0);
gl.uniform1f(u_speed, 1.0);
gl.uniform1f(u_warp, 1.0);
gl.uniform1f(u_focus, props.focus);
gl.uniform1f(u_itensity, 0.5);
gl.uniform2fv(u_scale, [props.scale, props.scale]);
const vertex = gl.getAttribLocation(shaderProgram, 'vertex');
gl.enableVertexAttribArray(vertex);
gl.vertexAttribPointer(vertex, 2, gl.FLOAT, false, 0, 0);
const vertices = [1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.DYNAMIC_DRAW);
if (isChromatic()) {
gl!.uniform1f(u_time, 0);
gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);
} else {
function render(timeStamp) {
gl!.uniform1f(u_time, timeStamp);
gl!.drawArrays(gl!.TRIANGLE_STRIP, 0, 4);
handle = window.requestAnimationFrame(render);
}
handle = window.requestAnimationFrame(render);
}
});
onUnmounted(() => {
if (handle) {
window.cancelAnimationFrame(handle);
}
});
</script>
<style lang="scss" module>
</style>

View file

@ -26,6 +26,3 @@ const props = withDefaults(defineProps<{
extractor: (item) => item,
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -95,7 +95,7 @@ import XNavFolder from '@/components/MkDrive.navFolder.vue';
import XFolder from '@/components/MkDrive.folder.vue';
import XFile from '@/components/MkDrive.file.vue';
import * as os from '@/os';
import { stream } from '@/stream';
import { useStream } from '@/stream';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
import { uploadFile, uploads } from '@/scripts/upload';
@ -131,7 +131,7 @@ const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]);
const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
const uploadings = uploads;
const connection = stream.useChannel('drive');
const connection = useStream().useChannel('drive');
const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // $ref使
//

View file

@ -1,5 +1,5 @@
<template>
<div class="ssazuxis">
<div ref="el" class="ssazuxis">
<header class="_button" :style="{ background: bg }" @click="showBody = !showBody">
<div class="title"><div><slot name="header"></slot></div></div>
<div class="divider"></div>
@ -22,80 +22,67 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { onMounted, ref, shallowRef, watch } from 'vue';
import tinycolor from 'tinycolor2';
import { miLocalStorage } from '@/local-storage';
import { defaultStore } from '@/store';
const miLocalStoragePrefix = 'ui:folder:' as const;
export default defineComponent({
props: {
expanded: {
type: Boolean,
required: false,
default: true,
},
persistKey: {
type: String,
required: false,
default: null,
},
},
data() {
return {
defaultStore,
bg: null,
showBody: (this.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`) === 't') : this.expanded,
};
},
watch: {
showBody() {
if (this.persistKey) {
miLocalStorage.setItem(`${miLocalStoragePrefix}${this.persistKey}`, this.showBody ? 't' : 'f');
}
},
},
mounted() {
function getParentBg(el: Element | null): string {
if (el == null || el.tagName === 'BODY') return 'var(--bg)';
const bg = el.style.background || el.style.backgroundColor;
if (bg) {
return bg;
} else {
return getParentBg(el.parentElement);
}
}
const rawBg = getParentBg(this.$el);
const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
bg.setAlpha(0.85);
this.bg = bg.toRgbString();
},
methods: {
toggleContent(show: boolean) {
this.showBody = show;
},
const props = withDefaults(defineProps<{
expanded?: boolean;
persistKey?: string;
}>(), {
expanded: true,
});
enter(el) {
const elementHeight = el.getBoundingClientRect().height;
el.style.height = 0;
el.offsetHeight; // reflow
el.style.height = elementHeight + 'px';
},
afterEnter(el) {
el.style.height = null;
},
leave(el) {
const elementHeight = el.getBoundingClientRect().height;
el.style.height = elementHeight + 'px';
el.offsetHeight; // reflow
el.style.height = 0;
},
afterLeave(el) {
el.style.height = null;
},
},
const el = shallowRef<HTMLDivElement>();
const bg = ref<string | null>(null);
const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded);
watch(showBody, () => {
if (props.persistKey) {
miLocalStorage.setItem(`${miLocalStoragePrefix}${props.persistKey}`, showBody.value ? 't' : 'f');
}
});
function enter(el: Element) {
const elementHeight = el.getBoundingClientRect().height;
el.style.height = 0;
el.offsetHeight; // reflow
el.style.height = elementHeight + 'px';
}
function afterEnter(el: Element) {
el.style.height = null;
}
function leave(el: Element) {
const elementHeight = el.getBoundingClientRect().height;
el.style.height = elementHeight + 'px';
el.offsetHeight; // reflow
el.style.height = 0;
}
function afterLeave(el: Element) {
el.style.height = null;
}
onMounted(() => {
function getParentBg(el: HTMLElement | null): string {
if (el == null || el.tagName === 'BODY') return 'var(--bg)';
const bg = el.style.background || el.style.backgroundColor;
if (bg) {
return bg;
} else {
return getParentBg(el.parentElement);
}
}
const rawBg = getParentBg(el.value);
const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
_bg.setAlpha(0.85);
bg.value = _bg.toRgbString();
});
</script>

View file

@ -6,7 +6,7 @@
<div :class="$style.headerIcon"><slot name="icon"></slot></div>
<div :class="$style.headerText">
<div :class="$style.headerTextMain">
<slot name="label"></slot>
<MkCondensedLine :min-scale="2 / 3"><slot name="label"></slot></MkCondensedLine>
</div>
<div :class="$style.headerTextSub">
<slot name="caption"></slot>

View file

@ -33,7 +33,7 @@
import { onBeforeUnmount, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os';
import { stream } from '@/stream';
import { useStream } from '@/stream';
import { i18n } from '@/i18n';
import { claimAchievement } from '@/scripts/achievements';
import { $i } from '@/account';
@ -50,7 +50,7 @@ const props = withDefaults(defineProps<{
let isFollowing = $ref(props.user.isFollowing);
let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou);
let wait = $ref(false);
const connection = stream.useChannel('main');
const connection = useStream().useChannel('main');
if (props.user.isFollowing == null) {
os.api('users/show', {

View file

@ -54,8 +54,8 @@
</MkModalWindow>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { reactive, shallowRef } from 'vue';
import MkInput from './MkInput.vue';
import MkTextarea from './MkTextarea.vue';
import MkSwitch from './MkSwitch.vue';
@ -66,58 +66,36 @@ import MkRadios from './MkRadios.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkModalWindow,
MkInput,
MkTextarea,
MkSwitch,
MkSelect,
MkRange,
MkButton,
MkRadios,
},
const props = defineProps<{
title: string;
form: any;
}>();
props: {
title: {
type: String,
required: true,
},
form: {
type: Object,
required: true,
},
},
const emit = defineEmits<{
(ev: 'done', v: {
canceled?: boolean;
result?: any;
}): void;
}>();
emits: ['done'],
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const values = reactive({});
data() {
return {
values: {},
i18n,
};
},
for (const item in props.form) {
values[item] = props.form[item].default ?? null;
}
created() {
for (const item in this.form) {
this.values[item] = this.form[item].default ?? null;
}
},
function ok() {
emit('done', {
result: values,
});
dialog.value.close();
}
methods: {
ok() {
this.$emit('done', {
result: this.values,
});
this.$refs.dialog.close();
},
cancel() {
this.$emit('done', {
canceled: true,
});
this.$refs.dialog.close();
},
},
});
function cancel() {
emit('done', {
canceled: true,
});
dialog.value.close();
}
</script>

View file

@ -22,7 +22,7 @@ import MkPagination, { Paging } from '@/components/MkPagination.vue';
import XNotification from '@/components/MkNotification.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import MkNote from '@/components/MkNote.vue';
import { stream } from '@/stream';
import { useStream } from '@/stream';
import { $i } from '@/account';
import { i18n } from '@/i18n';
import { notificationTypes } from '@/const';
@ -45,7 +45,7 @@ const pagination: Paging = {
const onNotification = (notification) => {
const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type);
if (isMuted || document.visibilityState === 'visible') {
stream.send('readNotification');
useStream().send('readNotification');
}
if (!isMuted) {
@ -56,7 +56,7 @@ const onNotification = (notification) => {
let connection;
onMounted(() => {
connection = stream.useChannel('main');
connection = useStream().useChannel('main');
connection.on('notification', onNotification);
});

View file

@ -28,54 +28,38 @@
</div>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue';
<script lang="ts" setup>
import { reactive } from 'vue';
import number from '@/filters/number';
import XValue from '@/components/MkObjectView.value.vue';
export default defineComponent({
name: 'XValue',
const props = defineProps<{
value: any;
}>();
props: {
value: {
required: true,
},
},
const collapsed = reactive({});
setup(props) {
const collapsed = reactive({});
if (isObject(props.value)) {
for (const key in props.value) {
collapsed[key] = collapsable(props.value[key]);
}
}
if (isObject(props.value)) {
for (const key in props.value) {
collapsed[key] = collapsable(props.value[key]);
}
}
function isObject(v): boolean {
return typeof v === 'object' && !Array.isArray(v) && v !== null;
}
function isObject(v): boolean {
return typeof v === 'object' && !Array.isArray(v) && v !== null;
}
function isArray(v): boolean {
return Array.isArray(v);
}
function isArray(v): boolean {
return Array.isArray(v);
}
function isEmpty(v): boolean {
return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
}
function isEmpty(v): boolean {
return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
}
function collapsable(v): boolean {
return (isObject(v) || isArray(v)) && !isEmpty(v);
}
return {
number,
collapsed,
isObject,
isArray,
isEmpty,
collapsable,
};
},
});
function collapsable(v): boolean {
return (isObject(v) || isArray(v)) && !isEmpty(v);
}
</script>
<style lang="scss" scoped>

View file

@ -1,5 +1,5 @@
<template>
<div class="zhyxdalp">
<div>
<XValue :value="value" :collapsed="false"/>
</div>
</template>
@ -12,9 +12,3 @@ const props = defineProps<{
value: Record<string, unknown>;
}>();
</script>
<style lang="scss" scoped>
.zhyxdalp {
}
</style>

View file

@ -12,12 +12,12 @@
>
<template #header>
<template v-if="pageMetadata?.value">
<i v-if="pageMetadata.value.icon" class="icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i>
<i v-if="pageMetadata.value.icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i>
<span>{{ pageMetadata.value.title }}</span>
</template>
</template>
<div :class="$style.root" :style="{ background: pageMetadata?.value?.bg }" style="container-type: inline-size;">
<div :class="$style.root" style="container-type: inline-size;">
<RouterView :key="reloadCount" :router="router"/>
</div>
</MkWindow>

View file

@ -1,37 +1,27 @@
<script lang="ts">
import { VNode, defineComponent, h } from 'vue';
import { VNode, defineComponent, h, ref, watch } from 'vue';
import MkRadio from './MkRadio.vue';
export default defineComponent({
components: {
MkRadio,
},
props: {
modelValue: {
required: false,
},
},
data() {
return {
value: this.modelValue,
};
},
watch: {
value() {
this.$emit('update:modelValue', this.value);
},
},
render() {
console.log(this.$slots, this.$slots.label && this.$slots.label());
if (!this.$slots.default) return null;
let options = this.$slots.default();
const label = this.$slots.label && this.$slots.label();
const caption = this.$slots.caption && this.$slots.caption();
setup(props, context) {
const value = ref(props.modelValue);
watch(value, () => {
context.emit('update:modelValue', value.value);
});
if (!context.slots.default) return null;
let options = context.slots.default();
const label = context.slots.label && context.slots.label();
const caption = context.slots.caption && context.slots.caption();
// Fragment
if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
return h('div', {
return () => h('div', {
class: 'novjtcto',
}, [
...(label ? [h('div', {
@ -42,8 +32,8 @@ export default defineComponent({
}, options.map(option => h(MkRadio, {
key: option.key,
value: option.props?.value,
modelValue: this.value,
'onUpdate:modelValue': value => this.value = value,
modelValue: value.value,
'onUpdate:modelValue': _v => value.value = _v,
}, () => option.children)),
),
...(caption ? [h('div', {

View file

@ -124,7 +124,3 @@ onMounted(async () => {
});
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,7 +1,7 @@
<template>
<div class="vswabwbm" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<circle fill="none" cx="64" cy="64">
<circle fill="none" cx="64" cy="64" style="stroke: var(--accent);">
<animate
attributeName="r"
begin="0s" dur="0.5s"
@ -22,7 +22,7 @@
/>
</circle>
<g fill="none" fill-rule="evenodd">
<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color">
<circle v-for="(particle, i) in particles" :key="i" :fill="particle.color" style="stroke: var(--accent);">
<animate
attributeName="r"
begin="0s" dur="0.8s"
@ -100,17 +100,11 @@ onMounted(() => {
});
</script>
<style lang="scss" scoped>
.vswabwbm {
<style lang="scss" module>
.root {
pointer-events: none;
position: fixed;
width: 128px;
height: 128px;
> svg {
> circle {
stroke: var(--accent);
}
}
}
</style>

View file

@ -1,118 +0,0 @@
<template>
<div class="">
<div class="">
<MkInput v-model="text">
<template #label>Text</template>
</MkInput>
<MkSwitch v-model="flag">
<span>Switch is now {{ flag ? 'on' : 'off' }}</span>
</MkSwitch>
<div style="margin: 32px 0;">
<MkRadio v-model="radio" value="misskey">Misskey</MkRadio>
<MkRadio v-model="radio" value="mastodon">Mastodon</MkRadio>
<MkRadio v-model="radio" value="pleroma">Pleroma</MkRadio>
</div>
<MkButton inline>This is</MkButton>
<MkButton inline primary>the button</MkButton>
</div>
<div class="" style="pointer-events: none;">
<Mfm :text="mfm"/>
</div>
<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>
<MkButton inline primary @click="openDrive">Open drive</MkButton>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkRadio from '@/components/MkRadio.vue';
import * as os from '@/os';
import * as config from '@/config';
import { $i } from '@/account';
export default defineComponent({
components: {
MkButton,
MkInput,
MkSwitch,
MkTextarea,
MkRadio,
},
data() {
return {
text: '',
flag: true,
radio: 'misskey',
$i,
mfm: `Hello world! This is an @example mention. BTW you are @${this.$i ? this.$i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`,
};
},
methods: {
async openDialog() {
os.alert({
type: 'warning',
title: 'Oh my Aichan',
text: 'Lorem ipsum dolor sit amet, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
});
},
async openForm() {
os.form('Example form', {
foo: {
type: 'boolean',
default: true,
label: 'This is a boolean property',
},
bar: {
type: 'number',
default: 300,
label: 'This is a number property',
},
baz: {
type: 'string',
default: 'Misskey makes you happy.',
label: 'This is a string property',
},
});
},
async openDrive() {
os.selectDriveFile(false);
},
async selectUser() {
os.selectUser();
},
async openMenu(ev) {
os.popupMenu([{
type: 'label',
text: 'Fruits',
}, {
text: 'Create some apples',
action: () => {},
}, {
text: 'Read some oranges',
action: () => {},
}, {
text: 'Update some melons',
action: () => {},
}, null, {
text: 'Delete some bananas',
danger: true,
action: () => {},
}], ev.currentTarget ?? ev.target);
},
},
});
</script>

View file

@ -1,7 +1,7 @@
<template>
<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>
<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
<div class="_gaps_m">
<div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null, marginBottom: message ? '1.5em' : null }"></div>
<MkInfo v-if="message">
{{ message }}
</MkInfo>
@ -236,18 +236,14 @@ function resetPassword() {
}
</script>
<style lang="scss" scoped>
.eppvobhk {
> .auth {
> .avatar {
margin: 0 auto 0 auto;
width: 64px;
height: 64px;
background: #ddd;
background-position: center;
background-size: cover;
border-radius: 100%;
}
}
<style lang="scss" module>
.avatar {
margin: 0 auto 0 auto;
width: 64px;
height: 64px;
background: #ddd;
background-position: center;
background-size: cover;
border-radius: 100%;
}
</style>

View file

@ -23,22 +23,13 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
export default defineComponent({
props: {
def: {
type: Array,
required: true,
},
grid: {
type: Boolean,
required: false,
default: false,
},
},
});
defineProps<{
def: any[];
grid?: boolean;
}>();
</script>
<style lang="scss" scoped>

View file

@ -7,17 +7,17 @@ export default defineComponent({
required: true,
},
},
render() {
const options = this.$slots.default();
setup(props, { emit, slots }) {
const options = slots.default();
return h('div', {
return () => h('div', {
class: 'pxhvhrfw',
}, options.map(option => withDirectives(h('button', {
class: ['_button', { active: this.modelValue === option.props.value }],
class: ['_button', { active: props.modelValue === option.props.value }],
key: option.key,
disabled: this.modelValue === option.props.value,
disabled: props.modelValue === option.props.value,
onClick: () => {
this.$emit('update:modelValue', option.props.value);
emit('update:modelValue', option.props.value);
},
}, option.children), [
[resolveDirective('click-anime')],

View file

@ -26,153 +26,88 @@
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, nextTick, ref, watch, computed, toRefs } from 'vue';
<script lang="ts" setup>
import { onMounted, nextTick, ref, watch, computed, toRefs, shallowRef } from 'vue';
import { debounce } from 'throttle-debounce';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
},
const props = defineProps<{
modelValue: string | null;
required?: boolean;
readonly?: boolean;
disabled?: boolean;
pattern?: string;
placeholder?: string;
autofocus?: boolean;
autocomplete?: string;
spellcheck?: boolean;
debounce?: boolean;
manualSave?: boolean;
code?: boolean;
tall?: boolean;
pre?: boolean;
}>();
props: {
modelValue: {
required: true,
},
type: {
type: String,
required: false,
},
required: {
type: Boolean,
required: false,
},
readonly: {
type: Boolean,
required: false,
},
disabled: {
type: Boolean,
required: false,
},
pattern: {
type: String,
required: false,
},
placeholder: {
type: String,
required: false,
},
autofocus: {
type: Boolean,
required: false,
default: false,
},
autocomplete: {
required: false,
},
spellcheck: {
required: false,
},
code: {
type: Boolean,
required: false,
},
tall: {
type: Boolean,
required: false,
default: false,
},
pre: {
type: Boolean,
required: false,
default: false,
},
debounce: {
type: Boolean,
required: false,
default: false,
},
manualSave: {
type: Boolean,
required: false,
default: false,
},
},
const emit = defineEmits<{
(ev: 'change', _ev: KeyboardEvent): void;
(ev: 'keydown', _ev: KeyboardEvent): void;
(ev: 'enter'): void;
(ev: 'update:modelValue', value: string): void;
}>();
emits: ['change', 'keydown', 'enter', 'update:modelValue'],
const { modelValue, autofocus } = toRefs(props);
const v = ref<string>(modelValue.value ?? '');
const focused = ref(false);
const changed = ref(false);
const invalid = ref(false);
const filled = computed(() => v.value !== '' && v.value != null);
const inputEl = shallowRef<HTMLTextAreaElement>();
setup(props, context) {
const { modelValue, autofocus } = toRefs(props);
const v = ref(modelValue.value);
const focused = ref(false);
const changed = ref(false);
const invalid = ref(false);
const filled = computed(() => v.value !== '' && v.value != null);
const inputEl = ref(null);
const focus = () => inputEl.value.focus();
const onInput = (ev) => {
changed.value = true;
emit('change', ev);
};
const onKeydown = (ev: KeyboardEvent) => {
if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
const focus = () => inputEl.value.focus();
const onInput = (ev) => {
changed.value = true;
context.emit('change', ev);
};
const onKeydown = (ev: KeyboardEvent) => {
if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
emit('keydown', ev);
context.emit('keydown', ev);
if (ev.code === 'Enter') {
emit('enter');
}
};
if (ev.code === 'Enter') {
context.emit('enter');
}
};
const updated = () => {
changed.value = false;
emit('update:modelValue', v.value ?? '');
};
const updated = () => {
changed.value = false;
context.emit('update:modelValue', v.value);
};
const debouncedUpdated = debounce(1000, updated);
const debouncedUpdated = debounce(1000, updated);
watch(modelValue, newValue => {
v.value = newValue;
});
watch(modelValue, newValue => {
v.value = newValue;
});
watch(v, newValue => {
if (!props.manualSave) {
if (props.debounce) {
debouncedUpdated();
} else {
updated();
}
}
watch(v, newValue => {
if (!props.manualSave) {
if (props.debounce) {
debouncedUpdated();
} else {
updated();
}
}
invalid.value = inputEl.value.validity.badInput;
});
invalid.value = inputEl.value.validity.badInput;
});
onMounted(() => {
nextTick(() => {
if (autofocus.value) {
focus();
}
});
});
return {
v,
focused,
invalid,
changed,
filled,
inputEl,
focus,
onInput,
onKeydown,
updated,
i18n,
};
},
onMounted(() => {
nextTick(() => {
if (autofocus.value) {
focus();
}
});
});
</script>

View file

@ -5,7 +5,7 @@
<script lang="ts" setup>
import { computed, provide, onUnmounted } from 'vue';
import MkNotes from '@/components/MkNotes.vue';
import { stream } from '@/stream';
import { useStream } from '@/stream';
import * as sound from '@/scripts/sound';
import { $i } from '@/account';
import { defaultStore } from '@/store';
@ -57,6 +57,8 @@ let query;
let connection;
let connection2;
const stream = useStream();
if (props.src === 'antenna') {
endpoint = 'antennas/notes';
query = {
@ -68,7 +70,12 @@ if (props.src === 'antenna') {
connection.on('note', prepend);
} else if (props.src === 'home') {
endpoint = 'notes/timeline';
connection = stream.useChannel('homeTimeline');
query = {
withReplies: defaultStore.state.showTimelineReplies,
};
connection = stream.useChannel('homeTimeline', {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on('note', prepend);
connection2 = stream.useChannel('main');
@ -76,15 +83,30 @@ if (props.src === 'antenna') {
connection2.on('unfollow', onChangeFollowing);
} else if (props.src === 'local') {
endpoint = 'notes/local-timeline';
connection = stream.useChannel('localTimeline');
query = {
withReplies: defaultStore.state.showTimelineReplies,
};
connection = stream.useChannel('localTimeline', {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on('note', prepend);
} else if (props.src === 'social') {
endpoint = 'notes/hybrid-timeline';
connection = stream.useChannel('hybridTimeline');
query = {
withReplies: defaultStore.state.showTimelineReplies,
};
connection = stream.useChannel('hybridTimeline', {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on('note', prepend);
} else if (props.src === 'global') {
endpoint = 'notes/global-timeline';
connection = stream.useChannel('globalTimeline');
query = {
withReplies: defaultStore.state.showTimelineReplies,
};
connection = stream.useChannel('globalTimeline', {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on('note', prepend);
} else if (props.src === 'mentions') {
endpoint = 'notes/mentions';

View file

@ -41,6 +41,9 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
// showing = false closed DOM
if (!props.showing) emit('closed');
const el = shallowRef<HTMLElement>();
const zIndex = os.claimZIndex('high');
@ -66,10 +69,8 @@ onMounted(() => {
setPosition();
const loop = () => {
loopHandler = window.requestAnimationFrame(() => {
setPosition();
loop();
});
setPosition();
loopHandler = window.requestAnimationFrame(loop);
};
loop();

View file

@ -1,5 +1,5 @@
<template>
<div class="fgmtyycl" :style="{ zIndex, top: top + 'px', left: left + 'px' }">
<div :class="$style.root" :style="{ zIndex, top: top + 'px', left: left + 'px' }">
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" @after-leave="emit('closed')">
<MkUrlPreview v-if="showing" class="_popup _shadow" :url="url"/>
</Transition>
@ -36,8 +36,8 @@ onMounted(() => {
});
</script>
<style lang="scss" scoped>
.fgmtyycl {
<style lang="scss" module>
.root {
position: absolute;
width: 500px;
max-width: calc(90vw - 12px);

View file

@ -4,6 +4,7 @@
<MkFolder>
<template #label>{{ i18n.ts.makeFollowManuallyApprove }}</template>
<template #icon><i class="ti ti-lock"></i></template>
<template #suffix>{{ isLocked ? i18n.ts.on : i18n.ts.off }}</template>
<MkSwitch v-model="isLocked">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch>
@ -11,6 +12,7 @@
<MkFolder>
<template #label>{{ i18n.ts.hideOnlineStatus }}</template>
<template #icon><i class="ti ti-eye-off"></i></template>
<template #suffix>{{ hideOnlineStatus ? i18n.ts.on : i18n.ts.off }}</template>
<MkSwitch v-model="hideOnlineStatus">{{ i18n.ts.hideOnlineStatus }}<template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template></MkSwitch>
@ -18,6 +20,7 @@
<MkFolder>
<template #label>{{ i18n.ts.noCrawle }}</template>
<template #icon><i class="ti ti-world-x"></i></template>
<template #suffix>{{ noCrawle ? i18n.ts.on : i18n.ts.off }}</template>
<MkSwitch v-model="noCrawle">{{ i18n.ts.noCrawle }}<template #caption>{{ i18n.ts.noCrawleDescription }}</template></MkSwitch>
@ -25,6 +28,7 @@
<MkFolder>
<template #label>{{ i18n.ts.preventAiLearning }}</template>
<template #icon><i class="ti ti-photo-shield"></i></template>
<template #suffix>{{ preventAiLearning ? i18n.ts.on : i18n.ts.off }}</template>
<MkSwitch v-model="preventAiLearning">{{ i18n.ts.preventAiLearning }}<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template></MkSwitch>

View file

@ -37,8 +37,8 @@ import { chooseFileFromPc } from '@/scripts/select-file';
import * as os from '@/os';
import { $i } from '@/account';
const name = ref('');
const description = ref('');
const name = ref($i.name ?? '');
const description = ref($i.description ?? '');
watch(name, () => {
os.apiWithDialog('i/update', {

View file

@ -7,10 +7,10 @@
@close="close(true)"
@closed="emit('closed')"
>
<template v-if="page === 1" #header>{{ i18n.ts._initialAccountSetting.profileSetting }}</template>
<template v-else-if="page === 2" #header>{{ i18n.ts._initialAccountSetting.privacySetting }}</template>
<template v-else-if="page === 3" #header>{{ i18n.ts.follow }}</template>
<template v-else-if="page === 4" #header>{{ i18n.ts.pushNotification }}</template>
<template v-if="page === 1" #header><i class="ti ti-user-edit"></i> {{ i18n.ts._initialAccountSetting.profileSetting }}</template>
<template v-else-if="page === 2" #header><i class="ti ti-lock"></i> {{ i18n.ts._initialAccountSetting.privacySetting }}</template>
<template v-else-if="page === 3" #header><i class="ti ti-user-plus"></i> {{ i18n.ts.follow }}</template>
<template v-else-if="page === 4" #header><i class="ti ti-bell-plus"></i> {{ i18n.ts.pushNotification }}</template>
<template v-else-if="page === 5" #header>{{ i18n.ts.done }}</template>
<template v-else #header>{{ i18n.ts.initialAccountSetting }}</template>
@ -27,6 +27,7 @@
>
<template v-if="page === 0">
<div :class="$style.centerPage">
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
<MkSpacer :margin-min="20" :margin-max="28">
<div class="_gaps" style="text-align: center;">
<i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
@ -41,7 +42,9 @@
<div style="height: 100cqh; overflow: auto;">
<MkSpacer :margin-min="20" :margin-max="28">
<XProfile/>
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
<div class="_buttonsCenter" style="margin-top: 16px;">
<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</MkSpacer>
</div>
</template>
@ -49,7 +52,9 @@
<div style="height: 100cqh; overflow: auto;">
<MkSpacer :margin-min="20" :margin-max="28">
<XPrivacy/>
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
<div class="_buttonsCenter" style="margin-top: 16px;">
<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</MkSpacer>
</div>
</template>
@ -78,6 +83,7 @@
</template>
<template v-else-if="page === 5">
<div :class="$style.centerPage">
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
<MkSpacer :margin-min="20" :margin-max="28">
<div class="_gaps" style="text-align: center;">
<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
@ -106,6 +112,7 @@ import MkButton from '@/components/MkButton.vue';
import XProfile from '@/components/MkUserSetupDialog.Profile.vue';
import XFollow from '@/components/MkUserSetupDialog.Follow.vue';
import XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue';
import MkAnimBg from '@/components/MkAnimBg.vue';
import { i18n } from '@/i18n';
import { instance } from '@/instance';
import { host } from '@/config';

View file

@ -15,70 +15,49 @@
</Transition>
</template>
<script lang="ts">
import { defineComponent, PropType, ref, watch } from 'vue';
<script lang="ts" setup>
import { ref, watch } from 'vue';
import MkButton from '@/components/MkButton.vue';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
},
const props = defineProps<{
p: () => Promise<any>;
}>();
props: {
p: {
type: Function as PropType<() => Promise<any>>,
required: true,
},
},
const pending = ref(true);
const resolved = ref(false);
const rejected = ref(false);
const result = ref(null);
setup(props, context) {
const pending = ref(true);
const resolved = ref(false);
const rejected = ref(false);
const result = ref(null);
const process = () => {
if (props.p == null) {
return;
}
const promise = props.p();
pending.value = true;
resolved.value = false;
rejected.value = false;
promise.then((_result) => {
pending.value = false;
resolved.value = true;
result.value = _result;
});
promise.catch(() => {
pending.value = false;
rejected.value = true;
});
};
const process = () => {
if (props.p == null) {
return;
}
const promise = props.p();
pending.value = true;
resolved.value = false;
rejected.value = false;
promise.then((_result) => {
pending.value = false;
resolved.value = true;
result.value = _result;
});
promise.catch(() => {
pending.value = false;
rejected.value = true;
});
};
watch(() => props.p, () => {
process();
}, {
immediate: true,
});
const retry = () => {
process();
};
return {
pending,
resolved,
rejected,
result,
retry,
defaultStore,
i18n,
};
},
watch(() => props.p, () => {
process();
}, {
immediate: true,
});
const retry = () => {
process();
};
</script>
<style lang="scss" scoped>

View file

@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.vue';
import { within } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.ts';
export const Default = {
render(args) {
return {

View file

@ -0,0 +1,367 @@
import { VNode, h } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import MkUrl from '@/components/global/MkUrl.vue';
import MkLink from '@/components/MkLink.vue';
import MkMention from '@/components/MkMention.vue';
import MkEmoji from '@/components/global/MkEmoji.vue';
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
import MkCode from '@/components/MkCode.vue';
import MkGoogle from '@/components/MkGoogle.vue';
import MkSparkle from '@/components/MkSparkle.vue';
import MkA from '@/components/global/MkA.vue';
import { host } from '@/config';
import { defaultStore } from '@/store';
const QUOTE_STYLE = `
display: block;
margin: 8px;
padding: 6px 0 6px 12px;
color: var(--fg);
border-left: solid 3px var(--fg);
opacity: 0.7;
`.split('\n').join(' ');
export default function(props: {
text: string;
plain?: boolean;
nowrap?: boolean;
author?: Misskey.entities.UserLite;
i?: Misskey.entities.UserLite;
isNote?: boolean;
emojiUrls?: string[];
rootScale?: number;
}) {
const isNote = props.isNote !== undefined ? props.isNote : true;
if (props.text == null || props.text === '') return;
const ast = (props.plain ? mfm.parseSimple : mfm.parse)(props.text);
const validTime = (t: string | null | undefined) => {
if (t == null) return null;
return t.match(/^[0-9.]+s$/) ? t : null;
};
const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm;
/**
* Gen Vue Elements from MFM AST
* @param ast MFM AST
* @param scale How times large the text is
*/
const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => {
switch (token.type) {
case 'text': {
const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
if (!props.plain) {
const res: (VNode | string)[] = [];
for (const t of text.split('\n')) {
res.push(h('br'));
res.push(t);
}
res.shift();
return res;
} else {
return [text.replace(/\n/g, ' ')];
}
}
case 'bold': {
return [h('b', genEl(token.children, scale))];
}
case 'strike': {
return [h('del', genEl(token.children, scale))];
}
case 'italic': {
return h('i', {
style: 'font-style: oblique;',
}, genEl(token.children, scale));
}
case 'fn': {
// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
let style;
switch (token.props.name) {
case 'tada': {
const speed = validTime(token.props.args.speed) ?? '1s';
style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both;` : '');
break;
}
case 'jelly': {
const speed = validTime(token.props.args.speed) ?? '1s';
style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
break;
}
case 'twitch': {
const speed = validTime(token.props.args.speed) ?? '0.5s';
style = useAnim ? `animation: mfm-twitch ${speed} ease infinite;` : '';
break;
}
case 'shake': {
const speed = validTime(token.props.args.speed) ?? '0.5s';
style = useAnim ? `animation: mfm-shake ${speed} ease infinite;` : '';
break;
}
case 'spin': {
const direction =
token.props.args.left ? 'reverse' :
token.props.args.alternate ? 'alternate' :
'normal';
const anime =
token.props.args.x ? 'mfm-spinX' :
token.props.args.y ? 'mfm-spinY' :
'mfm-spin';
const speed = validTime(token.props.args.speed) ?? '1.5s';
style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
break;
}
case 'jump': {
const speed = validTime(token.props.args.speed) ?? '0.75s';
style = useAnim ? `animation: mfm-jump ${speed} linear infinite;` : '';
break;
}
case 'bounce': {
const speed = validTime(token.props.args.speed) ?? '0.75s';
style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : '';
break;
}
case 'flip': {
const transform =
(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
token.props.args.v ? 'scaleY(-1)' :
'scaleX(-1)';
style = `transform: ${transform};`;
break;
}
case 'x2': {
return h('span', {
class: defaultStore.state.advancedMfm ? 'mfm-x2' : '',
}, genEl(token.children, scale * 2));
}
case 'x3': {
return h('span', {
class: defaultStore.state.advancedMfm ? 'mfm-x3' : '',
}, genEl(token.children, scale * 3));
}
case 'x4': {
return h('span', {
class: defaultStore.state.advancedMfm ? 'mfm-x4' : '',
}, genEl(token.children, scale * 4));
}
case 'font': {
const family =
token.props.args.serif ? 'serif' :
token.props.args.monospace ? 'monospace' :
token.props.args.cursive ? 'cursive' :
token.props.args.fantasy ? 'fantasy' :
token.props.args.emoji ? 'emoji' :
token.props.args.math ? 'math' :
null;
if (family) style = `font-family: ${family};`;
break;
}
case 'blur': {
return h('span', {
class: '_mfm_blur_',
}, genEl(token.children, scale));
}
case 'rainbow': {
const speed = validTime(token.props.args.speed) ?? '1s';
style = useAnim ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
break;
}
case 'sparkle': {
if (!useAnim) {
return genEl(token.children, scale);
}
return h(MkSparkle, {}, genEl(token.children, scale));
}
case 'rotate': {
const degrees = parseFloat(token.props.args.deg ?? '90');
style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
break;
}
case 'position': {
if (!defaultStore.state.advancedMfm) break;
const x = parseFloat(token.props.args.x ?? '0');
const y = parseFloat(token.props.args.y ?? '0');
style = `transform: translateX(${x}em) translateY(${y}em);`;
break;
}
case 'scale': {
if (!defaultStore.state.advancedMfm) {
style = '';
break;
}
const x = Math.min(parseFloat(token.props.args.x ?? '1'), 5);
const y = Math.min(parseFloat(token.props.args.y ?? '1'), 5);
style = `transform: scale(${x}, ${y});`;
scale = scale * Math.max(x, y);
break;
}
case 'fg': {
let color = token.props.args.color;
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
style = `color: #${color};`;
break;
}
case 'bg': {
let color = token.props.args.color;
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
style = `background-color: #${color};`;
break;
}
}
if (style == null) {
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
} else {
return h('span', {
style: 'display: inline-block; ' + style,
}, genEl(token.children, scale));
}
}
case 'small': {
return [h('small', {
style: 'opacity: 0.7;',
}, genEl(token.children, scale))];
}
case 'center': {
return [h('div', {
style: 'text-align:center;',
}, genEl(token.children, scale))];
}
case 'url': {
return [h(MkUrl, {
key: Math.random(),
url: token.props.url,
rel: 'nofollow noopener',
})];
}
case 'link': {
return [h(MkLink, {
key: Math.random(),
url: token.props.url,
rel: 'nofollow noopener',
}, genEl(token.children, scale))];
}
case 'mention': {
return [h(MkMention, {
key: Math.random(),
host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) || host,
username: token.props.username,
})];
}
case 'hashtag': {
return [h(MkA, {
key: Math.random(),
to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
style: 'color:var(--hashtag);',
}, `#${token.props.hashtag}`)];
}
case 'blockCode': {
return [h(MkCode, {
key: Math.random(),
code: token.props.code,
lang: token.props.lang,
})];
}
case 'inlineCode': {
return [h(MkCode, {
key: Math.random(),
code: token.props.code,
inline: true,
})];
}
case 'quote': {
if (!props.nowrap) {
return [h('div', {
style: QUOTE_STYLE,
}, genEl(token.children, scale))];
} else {
return [h('span', {
style: QUOTE_STYLE,
}, genEl(token.children, scale))];
}
}
case 'emojiCode': {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (props.author?.host == null) {
return [h(MkCustomEmoji, {
key: Math.random(),
name: token.props.name,
normal: props.plain,
host: null,
useOriginalSize: scale >= 2.5,
})];
} else {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (props.emojiUrls && (props.emojiUrls[token.props.name] == null)) {
return [h('span', `:${token.props.name}:`)];
} else {
return [h(MkCustomEmoji, {
key: Math.random(),
name: token.props.name,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
url: props.emojiUrls ? props.emojiUrls[token.props.name] : null,
normal: props.plain,
host: props.author.host,
useOriginalSize: scale >= 2.5,
})];
}
}
}
case 'unicodeEmoji': {
return [h(MkEmoji, {
key: Math.random(),
emoji: token.props.emoji,
})];
}
case 'mathInline': {
return [h('code', token.props.formula)];
}
case 'mathBlock': {
return [h('code', token.props.formula)];
}
case 'search': {
return [h(MkGoogle, {
key: Math.random(),
q: token.props.query,
})];
}
case 'plain': {
return [h('span', genEl(token.children, scale))];
}
default: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
console.error('unrecognized ast type:', (token as any).type);
return [];
}
}
}).flat(Infinity) as (VNode | string)[];
return h('span', {
// https://codeday.me/jp/qa/20190424/690106.html
style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;',
}, genEl(ast, props.rootScale ?? 1));
}

View file

@ -1,171 +0,0 @@
<template>
<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :is-note="isNote" :class="[$style.root, { [$style.nowrap]: nowrap }]"/>
</template>
<script lang="ts" setup>
import { } from 'vue';
import MfmCore from '@/components/mfm';
const props = withDefaults(defineProps<{
text: string;
plain?: boolean;
nowrap?: boolean;
author?: any;
isNote?: boolean;
}>(), {
plain: false,
nowrap: false,
author: null,
isNote: true,
});
</script>
<style lang="scss">
._mfm_blur_ {
filter: blur(6px);
transition: filter 0.3s;
&:hover {
filter: blur(0px);
}
}
.mfm-x2 {
--mfm-zoom-size: 200%;
}
.mfm-x3 {
--mfm-zoom-size: 400%;
}
.mfm-x4 {
--mfm-zoom-size: 600%;
}
.mfm-x2, .mfm-x3, .mfm-x4 {
font-size: var(--mfm-zoom-size);
.mfm-x2, .mfm-x3, .mfm-x4 {
/* only half effective */
font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
.mfm-x2, .mfm-x3, .mfm-x4 {
/* disabled */
font-size: 100%;
}
}
}
@keyframes mfm-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes mfm-spinX {
0% { transform: perspective(128px) rotateX(0deg); }
100% { transform: perspective(128px) rotateX(360deg); }
}
@keyframes mfm-spinY {
0% { transform: perspective(128px) rotateY(0deg); }
100% { transform: perspective(128px) rotateY(360deg); }
}
@keyframes mfm-jump {
0% { transform: translateY(0); }
25% { transform: translateY(-16px); }
50% { transform: translateY(0); }
75% { transform: translateY(-8px); }
100% { transform: translateY(0); }
}
@keyframes mfm-bounce {
0% { transform: translateY(0) scale(1, 1); }
25% { transform: translateY(-16px) scale(1, 1); }
50% { transform: translateY(0) scale(1, 1); }
75% { transform: translateY(0) scale(1.5, 0.75); }
100% { transform: translateY(0) scale(1, 1); }
}
// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`;
// let css = '';
// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
@keyframes mfm-twitch {
0% { transform: translate(7px, -2px) }
5% { transform: translate(-3px, 1px) }
10% { transform: translate(-7px, -1px) }
15% { transform: translate(0px, -1px) }
20% { transform: translate(-8px, 6px) }
25% { transform: translate(-4px, -3px) }
30% { transform: translate(-4px, -6px) }
35% { transform: translate(-8px, -8px) }
40% { transform: translate(4px, 6px) }
45% { transform: translate(-3px, 1px) }
50% { transform: translate(2px, -10px) }
55% { transform: translate(-7px, 0px) }
60% { transform: translate(-2px, 4px) }
65% { transform: translate(3px, -8px) }
70% { transform: translate(6px, 7px) }
75% { transform: translate(-7px, -2px) }
80% { transform: translate(-7px, -8px) }
85% { transform: translate(9px, 3px) }
90% { transform: translate(-3px, -2px) }
95% { transform: translate(-10px, 2px) }
100% { transform: translate(-2px, -6px) }
}
// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`;
// let css = '';
// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
@keyframes mfm-shake {
0% { transform: translate(-3px, -1px) rotate(-8deg) }
5% { transform: translate(0px, -1px) rotate(-10deg) }
10% { transform: translate(1px, -3px) rotate(0deg) }
15% { transform: translate(1px, 1px) rotate(11deg) }
20% { transform: translate(-2px, 1px) rotate(1deg) }
25% { transform: translate(-1px, -2px) rotate(-2deg) }
30% { transform: translate(-1px, 2px) rotate(-3deg) }
35% { transform: translate(2px, 1px) rotate(6deg) }
40% { transform: translate(-2px, -3px) rotate(-9deg) }
45% { transform: translate(0px, -1px) rotate(-12deg) }
50% { transform: translate(1px, 2px) rotate(10deg) }
55% { transform: translate(0px, -3px) rotate(8deg) }
60% { transform: translate(1px, -1px) rotate(8deg) }
65% { transform: translate(0px, -1px) rotate(-7deg) }
70% { transform: translate(-1px, -3px) rotate(6deg) }
75% { transform: translate(0px, -2px) rotate(4deg) }
80% { transform: translate(-2px, -1px) rotate(3deg) }
85% { transform: translate(1px, -3px) rotate(-10deg) }
90% { transform: translate(1px, 0px) rotate(3deg) }
95% { transform: translate(-2px, 0px) rotate(-3deg) }
100% { transform: translate(2px, 1px) rotate(2deg) }
}
@keyframes mfm-rubberBand {
from { transform: scale3d(1, 1, 1); }
30% { transform: scale3d(1.25, 0.75, 1); }
40% { transform: scale3d(0.75, 1.25, 1); }
50% { transform: scale3d(1.15, 0.85, 1); }
65% { transform: scale3d(0.95, 1.05, 1); }
75% { transform: scale3d(1.05, 0.95, 1); }
to { transform: scale3d(1, 1, 1); }
}
@keyframes mfm-rainbow {
0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
}
</style>
<style lang="scss" module>
.root {
white-space: pre-wrap;
&.nowrap {
white-space: pre;
word-wrap: normal; // https://codeday.me/jp/qa/20190424/690106.html
overflow: hidden;
text-overflow: ellipsis;
}
}
</style>

View file

@ -1,42 +1,24 @@
import { h, defineComponent } from 'vue';
import { h } from 'vue';
export default defineComponent({
props: {
src: {
type: String,
required: true,
},
tag: {
type: String,
required: false,
default: 'span',
},
textTag: {
type: String,
required: false,
default: null,
},
},
render() {
let str = this.src;
const parsed = [] as (string | { arg: string; })[];
while (true) {
const nextBracketOpen = str.indexOf('{');
const nextBracketClose = str.indexOf('}');
export default function(props: { src: string; tag?: string; textTag?: string; }, { slots }) {
let str = props.src;
const parsed = [] as (string | { arg: string; })[];
while (true) {
const nextBracketOpen = str.indexOf('{');
const nextBracketClose = str.indexOf('}');
if (nextBracketOpen === -1) {
parsed.push(str);
break;
} else {
if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen));
parsed.push({
arg: str.substring(nextBracketOpen + 1, nextBracketClose),
});
}
str = str.substr(nextBracketClose + 1);
if (nextBracketOpen === -1) {
parsed.push(str);
break;
} else {
if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen));
parsed.push({
arg: str.substring(nextBracketOpen + 1, nextBracketClose),
});
}
return h(this.tag, parsed.map(x => typeof x === 'string' ? (this.textTag ? h(this.textTag, x) : x) : this.$slots[x.arg]()));
},
});
str = str.substr(nextBracketClose + 1);
}
return h(props.tag ?? 'span', parsed.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]()));
}

View file

@ -1,6 +1,6 @@
import { App } from 'vue';
import Mfm from './global/MkMisskeyFlavoredMarkdown.vue';
import Mfm from './global/MkMisskeyFlavoredMarkdown.ts';
import MkA from './global/MkA.vue';
import MkAcct from './global/MkAcct.vue';
import MkAvatar from './global/MkAvatar.vue';

View file

@ -1,390 +0,0 @@
import { VNode, defineComponent, h } from 'vue';
import * as mfm from 'mfm-js';
import MkUrl from '@/components/global/MkUrl.vue';
import MkLink from '@/components/MkLink.vue';
import MkMention from '@/components/MkMention.vue';
import MkEmoji from '@/components/global/MkEmoji.vue';
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
import MkCode from '@/components/MkCode.vue';
import MkGoogle from '@/components/MkGoogle.vue';
import MkSparkle from '@/components/MkSparkle.vue';
import MkA from '@/components/global/MkA.vue';
import { host } from '@/config';
import { defaultStore } from '@/store';
const QUOTE_STYLE = `
display: block;
margin: 8px;
padding: 6px 0 6px 12px;
color: var(--fg);
border-left: solid 3px var(--fg);
opacity: 0.7;
`.split('\n').join(' ');
export default defineComponent({
props: {
text: {
type: String,
required: true,
},
plain: {
type: Boolean,
default: false,
},
nowrap: {
type: Boolean,
default: false,
},
author: {
type: Object,
default: null,
},
i: {
type: Object,
default: null,
},
isNote: {
type: Boolean,
default: true,
},
emojiUrls: {
type: Object,
default: null,
},
rootScale: {
type: Number,
default: 1,
}
},
render() {
if (this.text == null || this.text === '') return;
const ast = (this.plain ? mfm.parseSimple : mfm.parse)(this.text);
const validTime = (t: string | null | undefined) => {
if (t == null) return null;
return t.match(/^[0-9.]+s$/) ? t : null;
};
const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm;
/**
* Gen Vue Elements from MFM AST
* @param ast MFM AST
* @param scale How times large the text is
*/
const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => {
switch (token.type) {
case 'text': {
const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
if (!this.plain) {
const res: (VNode | string)[] = [];
for (const t of text.split('\n')) {
res.push(h('br'));
res.push(t);
}
res.shift();
return res;
} else {
return [text.replace(/\n/g, ' ')];
}
}
case 'bold': {
return [h('b', genEl(token.children, scale))];
}
case 'strike': {
return [h('del', genEl(token.children, scale))];
}
case 'italic': {
return h('i', {
style: 'font-style: oblique;',
}, genEl(token.children, scale));
}
case 'fn': {
// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
let style;
switch (token.props.name) {
case 'tada': {
const speed = validTime(token.props.args.speed) ?? '1s';
style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both;` : '');
break;
}
case 'jelly': {
const speed = validTime(token.props.args.speed) ?? '1s';
style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
break;
}
case 'twitch': {
const speed = validTime(token.props.args.speed) ?? '0.5s';
style = useAnim ? `animation: mfm-twitch ${speed} ease infinite;` : '';
break;
}
case 'shake': {
const speed = validTime(token.props.args.speed) ?? '0.5s';
style = useAnim ? `animation: mfm-shake ${speed} ease infinite;` : '';
break;
}
case 'spin': {
const direction =
token.props.args.left ? 'reverse' :
token.props.args.alternate ? 'alternate' :
'normal';
const anime =
token.props.args.x ? 'mfm-spinX' :
token.props.args.y ? 'mfm-spinY' :
'mfm-spin';
const speed = validTime(token.props.args.speed) ?? '1.5s';
style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
break;
}
case 'jump': {
const speed = validTime(token.props.args.speed) ?? '0.75s';
style = useAnim ? `animation: mfm-jump ${speed} linear infinite;` : '';
break;
}
case 'bounce': {
const speed = validTime(token.props.args.speed) ?? '0.75s';
style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : '';
break;
}
case 'flip': {
const transform =
(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
token.props.args.v ? 'scaleY(-1)' :
'scaleX(-1)';
style = `transform: ${transform};`;
break;
}
case 'x2': {
return h('span', {
class: defaultStore.state.advancedMfm ? 'mfm-x2' : '',
}, genEl(token.children, scale * 2));
}
case 'x3': {
return h('span', {
class: defaultStore.state.advancedMfm ? 'mfm-x3' : '',
}, genEl(token.children, scale * 3));
}
case 'x4': {
return h('span', {
class: defaultStore.state.advancedMfm ? 'mfm-x4' : '',
}, genEl(token.children, scale * 4));
}
case 'font': {
const family =
token.props.args.serif ? 'serif' :
token.props.args.monospace ? 'monospace' :
token.props.args.cursive ? 'cursive' :
token.props.args.fantasy ? 'fantasy' :
token.props.args.emoji ? 'emoji' :
token.props.args.math ? 'math' :
null;
if (family) style = `font-family: ${family};`;
break;
}
case 'blur': {
return h('span', {
class: '_mfm_blur_',
}, genEl(token.children, scale));
}
case 'rainbow': {
const speed = validTime(token.props.args.speed) ?? '1s';
style = useAnim ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
break;
}
case 'sparkle': {
if (!useAnim) {
return genEl(token.children, scale);
}
return h(MkSparkle, {}, genEl(token.children, scale));
}
case 'rotate': {
const degrees = parseFloat(token.props.args.deg ?? '90');
style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
break;
}
case 'position': {
if (!defaultStore.state.advancedMfm) break;
const x = parseFloat(token.props.args.x ?? '0');
const y = parseFloat(token.props.args.y ?? '0');
style = `transform: translateX(${x}em) translateY(${y}em);`;
break;
}
case 'scale': {
if (!defaultStore.state.advancedMfm) {
style = '';
break;
}
const x = Math.min(parseFloat(token.props.args.x ?? '1'), 5);
const y = Math.min(parseFloat(token.props.args.y ?? '1'), 5);
style = `transform: scale(${x}, ${y});`;
scale = scale * Math.max(x, y);
break;
}
case 'fg': {
let color = token.props.args.color;
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
style = `color: #${color};`;
break;
}
case 'bg': {
let color = token.props.args.color;
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
style = `background-color: #${color};`;
break;
}
}
if (style == null) {
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
} else {
return h('span', {
style: 'display: inline-block; ' + style,
}, genEl(token.children, scale));
}
}
case 'small': {
return [h('small', {
style: 'opacity: 0.7;',
}, genEl(token.children, scale))];
}
case 'center': {
return [h('div', {
style: 'text-align:center;',
}, genEl(token.children, scale))];
}
case 'url': {
return [h(MkUrl, {
key: Math.random(),
url: token.props.url,
rel: 'nofollow noopener',
})];
}
case 'link': {
return [h(MkLink, {
key: Math.random(),
url: token.props.url,
rel: 'nofollow noopener',
}, genEl(token.children, scale))];
}
case 'mention': {
return [h(MkMention, {
key: Math.random(),
host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host,
username: token.props.username,
})];
}
case 'hashtag': {
return [h(MkA, {
key: Math.random(),
to: this.isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
style: 'color:var(--hashtag);',
}, `#${token.props.hashtag}`)];
}
case 'blockCode': {
return [h(MkCode, {
key: Math.random(),
code: token.props.code,
lang: token.props.lang,
})];
}
case 'inlineCode': {
return [h(MkCode, {
key: Math.random(),
code: token.props.code,
inline: true,
})];
}
case 'quote': {
if (!this.nowrap) {
return [h('div', {
style: QUOTE_STYLE,
}, genEl(token.children, scale))];
} else {
return [h('span', {
style: QUOTE_STYLE,
}, genEl(token.children, scale))];
}
}
case 'emojiCode': {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (this.author?.host == null) {
return [h(MkCustomEmoji, {
key: Math.random(),
name: token.props.name,
normal: this.plain,
host: null,
useOriginalSize: scale >= 2.5,
})];
} else {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (this.emojiUrls && (this.emojiUrls[token.props.name] == null)) {
return [h('span', `:${token.props.name}:`)];
} else {
return [h(MkCustomEmoji, {
key: Math.random(),
name: token.props.name,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
url: this.emojiUrls ? this.emojiUrls[token.props.name] : null,
normal: this.plain,
host: this.author.host,
useOriginalSize: scale >= 2.5,
})];
}
}
}
case 'unicodeEmoji': {
return [h(MkEmoji, {
key: Math.random(),
emoji: token.props.emoji,
})];
}
case 'mathInline': {
return [h('code', token.props.formula)];
}
case 'mathBlock': {
return [h('code', token.props.formula)];
}
case 'search': {
return [h(MkGoogle, {
key: Math.random(),
q: token.props.query,
})];
}
case 'plain': {
return [h('span', genEl(token.children, scale))];
}
default: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
console.error('unrecognized ast type:', (token as any).type);
return [];
}
}
}).flat(Infinity) as (VNode | string)[];
// Parse ast to DOM
return h('span', genEl(ast, this.rootScale));
},
});

View file

@ -0,0 +1,29 @@
export type BlockBase = {
id: string;
type: string;
};
export type TextBlock = BlockBase & {
type: 'text';
text: string;
};
export type SectionBlock = BlockBase & {
type: 'section';
title: string;
children: Block[];
};
export type ImageBlock = BlockBase & {
type: 'image';
fileId: string | null;
};
export type NoteBlock = BlockBase & {
type: 'note';
detailed: boolean;
note: string | null;
};
export type Block =
TextBlock | SectionBlock | ImageBlock | NoteBlock;

View file

@ -1,44 +1,19 @@
<template>
<component :is="'x-' + block.type" :key="block.id" :block="block" :hpml="hpml" :h="h"/>
<component :is="'x-' + block.type" :key="block.id" :page="page" :block="block" :h="h"/>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import * as Misskey from 'misskey-js';
import XText from './page.text.vue';
import XSection from './page.section.vue';
import XImage from './page.image.vue';
import XButton from './page.button.vue';
import XNumberInput from './page.number-input.vue';
import XTextInput from './page.text-input.vue';
import XTextareaInput from './page.textarea-input.vue';
import XSwitch from './page.switch.vue';
import XIf from './page.if.vue';
import XTextarea from './page.textarea.vue';
import XPost from './page.post.vue';
import XCounter from './page.counter.vue';
import XRadioButton from './page.radio-button.vue';
import XCanvas from './page.canvas.vue';
import XNote from './page.note.vue';
import { Hpml } from '@/scripts/hpml/evaluator';
import { Block } from '@/scripts/hpml/block';
import { Block } from './block.type';
export default defineComponent({
components: {
XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton, XCanvas, XNote,
},
props: {
block: {
type: Object as PropType<Block>,
required: true,
},
hpml: {
type: Object as PropType<Hpml>,
required: true,
},
h: {
type: Number,
required: true,
},
},
});
defineProps<{
block: Block,
h: number,
page: Misskey.entities.Page,
}>();
</script>

View file

@ -1,66 +0,0 @@
<template>
<div>
<MkButton class="kudkigyw" :primary="block.primary" @click="click()">{{ hpml.interpolate(block.text) }}</MkButton>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, unref } from 'vue';
import MkButton from '../MkButton.vue';
import * as os from '@/os';
import { ButtonBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
export default defineComponent({
components: {
MkButton,
},
props: {
block: {
type: Object as PropType<ButtonBlock>,
required: true,
},
hpml: {
type: Object as PropType<Hpml>,
required: true,
},
},
methods: {
click() {
if (this.block.action === 'dialog') {
this.hpml.eval();
os.alert({
text: this.hpml.interpolate(this.block.content),
});
} else if (this.block.action === 'resetRandom') {
this.hpml.updateRandomSeed(Math.random());
this.hpml.eval();
} else if (this.block.action === 'pushEvent') {
os.api('page-push', {
pageId: this.hpml.page.id,
event: this.block.event,
...(this.block.var ? {
var: unref(this.hpml.vars)[this.block.var],
} : {}),
});
os.alert({
type: 'success',
text: this.hpml.interpolate(this.block.message),
});
} else if (this.block.action === 'callAiScript') {
this.hpml.callAiScript(this.block.fn);
}
},
},
});
</script>
<style lang="scss" scoped>
.kudkigyw {
display: inline-block;
min-width: 200px;
max-width: 450px;
margin: 8px 0;
}
</style>

View file

@ -1,48 +0,0 @@
<template>
<div class="ysrxegms">
<canvas ref="canvas" :width="block.width" :height="block.height"/>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, PropType, Ref, ref } from 'vue';
import { CanvasBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
export default defineComponent({
props: {
block: {
type: Object as PropType<CanvasBlock>,
required: true,
},
hpml: {
type: Object as PropType<Hpml>,
required: true,
},
},
setup(props, ctx) {
const canvas: Ref<any> = ref(null);
onMounted(() => {
props.hpml.registerCanvas(props.block.name, canvas.value);
});
return {
canvas,
};
},
});
</script>
<style lang="scss" scoped>
.ysrxegms {
display: inline-block;
vertical-align: bottom;
overflow: auto;
max-width: 100%;
> canvas {
display: block;
}
}
</style>

View file

@ -1,51 +0,0 @@
<template>
<div>
<MkButton class="llumlmnx" @click="click()">{{ hpml.interpolate(block.text) }}</MkButton>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
import MkButton from '../MkButton.vue';
import { CounterVarBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
export default defineComponent({
components: {
MkButton,
},
props: {
block: {
type: Object as PropType<CounterVarBlock>,
required: true,
},
hpml: {
type: Object as PropType<Hpml>,
required: true,
},
},
setup(props, ctx) {
const value = computed(() => {
return props.hpml.vars.value[props.block.name];
});
function click() {
props.hpml.updatePageVar(props.block.name, value.value + (props.block.inc || 1));
props.hpml.eval();
}
return {
click,
};
},
});
</script>
<style lang="scss" scoped>
.llumlmnx {
display: inline-block;
min-width: 300px;
max-width: 450px;
margin: 8px 0;
}
</style>

View file

@ -1,31 +0,0 @@
<template>
<div v-show="hpml.vars.value[block.var]">
<XBlock v-for="child in block.children" :key="child.id" :block="child" :hpml="hpml" :h="h"/>
</div>
</template>
<script lang="ts">
import { IfBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
import { defineComponent, defineAsyncComponent, PropType } from 'vue';
export default defineComponent({
components: {
XBlock: defineAsyncComponent(() => import('./page.block.vue')),
},
props: {
block: {
type: Object as PropType<IfBlock>,
required: true,
},
hpml: {
type: Object as PropType<Hpml>,
required: true,
},
h: {
type: Number,
required: true,
},
},
});
</script>

View file

@ -5,15 +5,15 @@
</template>
<script lang="ts" setup>
import { PropType } from 'vue';
import { } from 'vue';
import * as Misskey from 'misskey-js';
import { ImageBlock } from './block.type';
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
import { ImageBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
const props = defineProps<{
block: PropType<ImageBlock>,
hpml: PropType<Hpml>,
block: ImageBlock,
page: Misskey.entities.Page,
}>();
const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
const image = props.page.attachedFiles.find(x => x.id === props.block.fileId);
</script>

View file

@ -1,47 +1,29 @@
<template>
<div class="voxdxuby">
<div style="margin: 1em 0;">
<MkNote v-if="note && !block.detailed" :key="note.id + ':normal'" v-model:note="note"/>
<MkNoteDetailed v-if="note && block.detailed" :key="note.id + ':detail'" v-model:note="note"/>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, PropType, Ref, ref } from 'vue';
<script lang="ts" setup>
import { onMounted, Ref, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { NoteBlock } from './block.type';
import MkNote from '@/components/MkNote.vue';
import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
import * as os from '@/os';
import { NoteBlock } from '@/scripts/hpml/block';
export default defineComponent({
components: {
MkNote,
MkNoteDetailed,
},
props: {
block: {
type: Object as PropType<NoteBlock>,
required: true,
},
},
setup(props, ctx) {
const note: Ref<Record<string, any> | null> = ref(null);
const props = defineProps<{
block: NoteBlock,
page: Misskey.entities.Page,
}>();
onMounted(() => {
os.api('notes/show', { noteId: props.block.note })
.then(result => {
note.value = result;
});
const note: Ref<Misskey.entities.Note | null> = ref(null);
onMounted(() => {
os.api('notes/show', { noteId: props.block.note })
.then(result => {
note.value = result;
});
return {
note,
};
},
});
</script>
<style lang="scss" scoped>
.voxdxuby {
margin: 1em 0;
}
</style>

View file

@ -1,54 +0,0 @@
<template>
<div>
<MkInput class="kudkigyw" :model-value="value" type="number" @update:model-value="updateValue($event)">
<template #label>{{ hpml.interpolate(block.text) }}</template>
</MkInput>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
import MkInput from '../MkInput.vue';
import { Hpml } from '@/scripts/hpml/evaluator';
import { NumberInputVarBlock } from '@/scripts/hpml/block';
export default defineComponent({
components: {
MkInput,
},
props: {
block: {
type: Object as PropType<NumberInputVarBlock>,
required: true,
},
hpml: {
type: Object as PropType<Hpml>,
required: true,
},
},
setup(props, ctx) {
const value = computed(() => {
return props.hpml.vars.value[props.block.name];
});
function updateValue(newValue) {
props.hpml.updatePageVar(props.block.name, newValue);
props.hpml.eval();
}
return {
value,
updateValue,
};
},
});
</script>
<style lang="scss" scoped>
.kudkigyw {
display: inline-block;
min-width: 300px;
max-width: 450px;
margin: 8px 0;
}
</style>

View file

@ -1,111 +0,0 @@
<template>
<div class="ngbfujlo">
<MkTextarea :model-value="text" readonly style="margin: 0;"></MkTextarea>
<MkButton class="button" primary :disabled="posting || posted" @click="post()">
<i v-if="posted" class="ti ti-check"></i>
<i v-else class="ti ti-send"></i>
</MkButton>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import MkTextarea from '../MkTextarea.vue';
import MkButton from '../MkButton.vue';
import { apiUrl } from '@/config';
import * as os from '@/os';
import { PostBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
import { defaultStore } from '@/store';
import { $i } from '@/account';
export default defineComponent({
components: {
MkTextarea,
MkButton,
},
props: {
block: {
type: Object as PropType<PostBlock>,
required: true,
},
hpml: {
type: Object as PropType<Hpml>,
required: true,
},
},
data() {
return {
text: this.hpml.interpolate(this.block.text),
posted: false,
posting: false,
};
},
watch: {
'hpml.vars': {
handler() {
this.text = this.hpml.interpolate(this.block.text);
},
deep: true,
},
},
methods: {
upload() {
const promise = new Promise((ok) => {
const canvas = this.hpml.canvases[this.block.canvasId];
canvas.toBlob(blob => {
const formData = new FormData();
formData.append('file', blob);
formData.append('i', $i.token);
if (defaultStore.state.uploadFolder) {
formData.append('folderId', defaultStore.state.uploadFolder);
}
window.fetch(apiUrl + '/drive/files/create', {
method: 'POST',
body: formData,
})
.then(response => response.json())
.then(f => {
ok(f);
});
});
});
os.promiseDialog(promise);
return promise;
},
async post() {
this.posting = true;
const file = this.block.attachCanvasImage ? await this.upload() : null;
os.apiWithDialog('notes/create', {
text: this.text === '' ? null : this.text,
fileIds: file ? [file.id] : undefined,
}).then(() => {
this.posted = true;
});
},
},
});
</script>
<style lang="scss" scoped>
.ngbfujlo {
position: relative;
padding: 32px;
border-radius: 6px;
box-shadow: 0 2px 8px var(--shadow);
z-index: 1;
> .button {
margin-top: 32px;
}
@media (max-width: 600px) {
padding: 16px;
> .button {
margin-top: 16px;
}
}
}
</style>

View file

@ -1,44 +0,0 @@
<template>
<div>
<div>{{ hpml.interpolate(block.title) }}</div>
<MkRadio v-for="item in block.values" :key="item" :modelValue="value" :value="item" @update:model-value="updateValue($event)">{{ item }}</MkRadio>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
import MkRadio from '../MkRadio.vue';
import { Hpml } from '@/scripts/hpml/evaluator';
import { RadioButtonVarBlock } from '@/scripts/hpml/block';
export default defineComponent({
components: {
MkRadio,
},
props: {
block: {
type: Object as PropType<RadioButtonVarBlock>,
required: true,
},
hpml: {
type: Object as PropType<Hpml>,
required: true,
},
},
setup(props, ctx) {
const value = computed(() => {
return props.hpml.vars.value[props.block.name];
});
function updateValue(newValue: string) {
props.hpml.updatePageVar(props.block.name, newValue);
props.hpml.eval();
}
return {
value,
updateValue,
};
},
});
</script>

View file

@ -3,34 +3,23 @@
<component :is="'h' + h">{{ block.title }}</component>
<div class="children">
<XBlock v-for="child in block.children" :key="child.id" :block="child" :hpml="hpml" :h="h + 1"/>
<XBlock v-for="child in block.children" :key="child.id" :page="page" :block="child" :h="h + 1"/>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent, PropType } from 'vue';
import { SectionBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import { SectionBlock } from './block.type';
export default defineComponent({
components: {
XBlock: defineAsyncComponent(() => import('./page.block.vue')),
},
props: {
block: {
type: Object as PropType<SectionBlock>,
required: true,
},
hpml: {
type: Object as PropType<Hpml>,
required: true,
},
h: {
required: true,
},
},
});
const XBlock = defineAsyncComponent(() => import('./page.block.vue'));
defineProps<{
block: SectionBlock,
h: number,
page: Misskey.entities.Page,
}>();
</script>
<style lang="scss" scoped>

View file

@ -1,54 +0,0 @@
<template>
<div class="hkcxmtwj">
<MkSwitch :model-value="value" @update:model-value="updateValue($event)">{{ hpml.interpolate(block.text) }}</MkSwitch>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
import MkSwitch from '../MkSwitch.vue';
import { Hpml } from '@/scripts/hpml/evaluator';
import { SwitchVarBlock } from '@/scripts/hpml/block';
export default defineComponent({
components: {
MkSwitch,
},
props: {
block: {
type: Object as PropType<SwitchVarBlock>,
required: true,
},
hpml: {
type: Object as PropType<Hpml>,
required: true,
},
},
setup(props, ctx) {
const value = computed(() => {
return props.hpml.vars.value[props.block.name];
});
function updateValue(newValue: boolean) {
props.hpml.updatePageVar(props.block.name, newValue);
props.hpml.eval();
}
return {
value,
updateValue,
};
},
});
</script>
<style lang="scss" scoped>
.hkcxmtwj {
display: inline-block;
margin: 16px auto;
& + .hkcxmtwj {
margin-left: 16px;
}
}
</style>

View file

@ -1,54 +0,0 @@
<template>
<div>
<MkInput class="kudkigyw" :model-value="value" type="text" @update:model-value="updateValue($event)">
<template #label>{{ hpml.interpolate(block.text) }}</template>
</MkInput>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
import MkInput from '../MkInput.vue';
import { Hpml } from '@/scripts/hpml/evaluator';
import { TextInputVarBlock } from '@/scripts/hpml/block';
export default defineComponent({
components: {
MkInput,
},
props: {
block: {
type: Object as PropType<TextInputVarBlock>,
required: true,
},
hpml: {
type: Object as PropType<Hpml>,
required: true,
},
},
setup(props, ctx) {
const value = computed(() => {
return props.hpml.vars.value[props.block.name];
});
function updateValue(newValue) {
props.hpml.updatePageVar(props.block.name, newValue);
props.hpml.eval();
}
return {
value,
updateValue,
};
},
});
</script>
<style lang="scss" scoped>
.kudkigyw {
display: inline-block;
min-width: 300px;
max-width: 450px;
margin: 8px 0;
}
</style>

View file

@ -1,56 +1,26 @@
<template>
<div class="mrdgzndn">
<Mfm :key="text" :text="text" :is-note="false" :i="$i"/>
<Mfm :text="block.text" :isNote="false" :i="$i"/>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" class="url"/>
</div>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent, PropType } from 'vue';
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import * as mfm from 'mfm-js';
import { TextBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
import * as Misskey from 'misskey-js';
import { TextBlock } from './block.type';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
import { $i } from '@/account';
export default defineComponent({
components: {
MkUrlPreview: defineAsyncComponent(() => import('@/components/MkUrlPreview.vue')),
},
props: {
block: {
type: Object as PropType<TextBlock>,
required: true,
},
hpml: {
type: Object as PropType<Hpml>,
required: true,
},
},
data() {
return {
text: this.hpml.interpolate(this.block.text),
$i,
};
},
computed: {
urls(): string[] {
if (this.text) {
return extractUrlFromMfm(mfm.parse(this.text));
} else {
return [];
}
},
},
watch: {
'hpml.vars': {
handler() {
this.text = this.hpml.interpolate(this.block.text);
},
deep: true,
},
},
});
const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue'));
const props = defineProps<{
block: TextBlock,
page: Misskey.entities.Page,
}>();
const urls = props.block.text ? extractUrlFromMfm(mfm.parse(props.block.text)) : [];
</script>
<style lang="scss" scoped>

View file

@ -1,45 +0,0 @@
<template>
<div>
<MkTextarea :model-value="value" @update:model-value="updateValue($event)">
<template #label>{{ hpml.interpolate(block.text) }}</template>
</MkTextarea>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
import MkTextarea from '../MkTextarea.vue';
import { Hpml } from '@/scripts/hpml/evaluator';
import { TextInputVarBlock } from '@/scripts/hpml/block';
export default defineComponent({
components: {
MkTextarea,
},
props: {
block: {
type: Object as PropType<TextInputVarBlock>,
required: true,
},
hpml: {
type: Object as PropType<Hpml>,
required: true,
},
},
setup(props, ctx) {
const value = computed(() => {
return props.hpml.vars.value[props.block.name];
});
function updateValue(newValue) {
props.hpml.updatePageVar(props.block.name, newValue);
props.hpml.eval();
}
return {
value,
updateValue,
};
},
});
</script>

View file

@ -1,39 +0,0 @@
<template>
<MkTextarea :model-value="text" readonly></MkTextarea>
</template>
<script lang="ts">
import { TextBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
import { defineComponent, PropType } from 'vue';
import MkTextarea from '../MkTextarea.vue';
export default defineComponent({
components: {
MkTextarea,
},
props: {
block: {
type: Object as PropType<TextBlock>,
required: true,
},
hpml: {
type: Object as PropType<Hpml>,
required: true,
},
},
data() {
return {
text: this.hpml.interpolate(this.block.text),
};
},
watch: {
'hpml.vars': {
handler() {
this.text = this.hpml.interpolate(this.block.text);
},
deep: true,
},
},
});
</script>

View file

@ -1,44 +1,17 @@
<template>
<div v-if="hpml" class="iroscrza" :class="{ center: page.alignCenter, serif: page.font === 'serif' }">
<XBlock v-for="child in page.content" :key="child.id" :block="child" :hpml="hpml" :h="2"/>
<div class="iroscrza" :class="{ center: page.alignCenter, serif: page.font === 'serif' }">
<XBlock v-for="child in page.content" :key="child.id" :block="child" :h="2"/>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, nextTick, PropType } from 'vue';
<script lang="ts" setup>
import { onMounted, nextTick } from 'vue';
import * as Misskey from 'misskey-js';
import XBlock from './page.block.vue';
import { Hpml } from '@/scripts/hpml/evaluator';
import { url } from '@/config';
import { $i } from '@/account';
export default defineComponent({
components: {
XBlock,
},
props: {
page: {
type: Object as PropType<Record<string, any>>,
required: true,
},
},
setup(props, ctx) {
const hpml = new Hpml(props.page, {
randomSeed: Math.random(),
visitor: $i,
url: url,
});
onMounted(() => {
nextTick(() => {
hpml.eval();
});
});
return {
hpml,
};
},
});
defineProps<{
page: Misskey.entities.Page,
}>();
</script>
<style lang="scss" scoped>

View file

@ -1,8 +1,7 @@
import { shallowRef, computed, markRaw } from 'vue';
import * as Misskey from 'misskey-js';
import { api, apiGet } from './os';
import { miLocalStorage } from './local-storage';
import { stream } from '@/stream';
import { useStream } from '@/stream';
import { get, set } from '@/scripts/idb-proxy';
const storageCache = await get('emojis');
@ -17,6 +16,9 @@ export const customEmojiCategories = computed<[ ...string[], null ]>(() => {
return markRaw([...Array.from(categories), null]);
});
// TODO: ここら辺副作用なのでいい感じにする
const stream = useStream();
stream.on('emojiAdded', emojiData => {
customEmojis.value = [emojiData.emoji, ...customEmojis.value];
set('emojis', customEmojis.value);
@ -34,10 +36,9 @@ stream.on('emojiDeleted', emojiData => {
export async function fetchCustomEmojis(force = false) {
const now = Date.now();
const needsMigration = miLocalStorage.getItem('emojis') != null;
let res;
if (force || needsMigration) {
if (force) {
res = await api('emojis', {});
} else {
const lastFetchedAt = await get('lastEmojisFetchedAt');
@ -48,10 +49,6 @@ export async function fetchCustomEmojis(force = false) {
customEmojis.value = res.emojis;
set('emojis', res.emojis);
set('lastEmojisFetchedAt', now);
if (needsMigration) {
miLocalStorage.removeItem('emojis');
miLocalStorage.removeItem('lastEmojisFetchedAt');
}
}
let cachedTags;

View file

@ -5,7 +5,7 @@ import { defineAsyncComponent, Directive, ref } from 'vue';
import { isTouchUsing } from '@/scripts/touch';
import { popup, alert } from '@/os';
const start = isTouchUsing ? 'touchstart' : 'mouseover';
const start = isTouchUsing ? 'touchstart' : 'mouseenter';
const end = isTouchUsing ? 'touchend' : 'mouseleave';
export default {
@ -63,16 +63,24 @@ export default {
ev.preventDefault();
});
el.addEventListener(start, () => {
el.addEventListener(start, (ev) => {
window.clearTimeout(self.showTimer);
window.clearTimeout(self.hideTimer);
self.showTimer = window.setTimeout(self.show, delay);
if (delay === 0) {
self.show();
} else {
self.showTimer = window.setTimeout(self.show, delay);
}
}, { passive: true });
el.addEventListener(end, () => {
window.clearTimeout(self.showTimer);
window.clearTimeout(self.hideTimer);
self.hideTimer = window.setTimeout(self.close, delay);
if (delay === 0) {
self.close();
} else {
self.hideTimer = window.setTimeout(self.close, delay);
}
}, { passive: true });
el.addEventListener('click', () => {

View file

@ -1,527 +0,0 @@
/**
* Client entry point
*/
// https://vitejs.dev/config/build-options.html#build-modulepreload
import 'vite/modulepreload-polyfill';
import '@/style.scss';
import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue';
import { compareVersions } from 'compare-versions';
import JSON5 from 'json5';
import widgets from '@/widgets';
import directives from '@/directives';
import components from '@/components';
import { version, ui, lang, updateLocale } from '@/config';
import { applyTheme } from '@/scripts/theme';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
import { i18n, updateI18n } from '@/i18n';
import { confirm, alert, post, popup, toast } from '@/os';
import { stream } from '@/stream';
import * as sound from '@/scripts/sound';
import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
import { defaultStore, ColdDeviceStorage } from '@/store';
import { fetchInstance, instance } from '@/instance';
import { makeHotkey } from '@/scripts/hotkey';
import { deviceKind } from '@/scripts/device-kind';
import { initializeSw } from '@/scripts/initialize-sw';
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 { deckStore } from '@/ui/deck/deck-store';
import { miLocalStorage } from '@/local-storage';
import { claimAchievement, claimedAchievements } from '@/scripts/achievements';
import { fetchCustomEmojis } from '@/custom-emojis';
import { mainRouter } from '@/router';
console.info(`Misskey v${version}`);
if (_DEV_) {
console.warn('Development mode!!!');
console.info(`vue ${vueVersion}`);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).$i = $i;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).$store = defaultStore;
window.addEventListener('error', event => {
console.error(event);
/*
alert({
type: 'error',
title: 'DEV: Unhandled error',
text: event.message
});
*/
});
window.addEventListener('unhandledrejection', event => {
console.error(event);
/*
alert({
type: 'error',
title: 'DEV: Unhandled promise rejection',
text: event.reason
});
*/
});
}
//#region Detect language & fetch translations
const localeVersion = miLocalStorage.getItem('localeVersion');
const localeOutdated = (localeVersion == null || localeVersion !== version);
if (localeOutdated) {
const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
if (res.status === 200) {
const newLocale = await res.text();
const parsedNewLocale = JSON.parse(newLocale);
miLocalStorage.setItem('locale', newLocale);
miLocalStorage.setItem('localeVersion', version);
updateLocale(parsedNewLocale);
updateI18n(parsedNewLocale);
}
}
//#endregion
// タッチデバイスでCSSの:hoverを機能させる
document.addEventListener('touchend', () => {}, { passive: true });
// 一斉リロード
reloadChannel.addEventListener('message', path => {
if (path !== null) location.href = path;
else location.reload();
});
// If mobile, insert the viewport meta tag
if (['smartphone', 'tablet'].includes(deviceKind)) {
const viewport = document.getElementsByName('viewport').item(0);
viewport.setAttribute('content',
`${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`);
}
//#region Set lang attr
const html = document.documentElement;
html.setAttribute('lang', lang);
//#endregion
//#region loginId
const params = new URLSearchParams(location.search);
const loginId = params.get('loginId');
if (loginId) {
const target = getUrlWithoutLoginId(location.href);
if (!$i || $i.id !== loginId) {
const account = await getAccountFromId(loginId);
if (account) {
await login(account.token, target);
}
}
history.replaceState({ misskey: 'loginId' }, '', target);
}
//#endregion
//#region Fetch user
if ($i && $i.token) {
if (_DEV_) {
console.log('account cache found. refreshing...');
}
refreshAccount();
} else {
if (_DEV_) {
console.log('no account cache found.');
}
// 連携ログインの場合用にCookieを参照する
const i = (document.cookie.match(/igi=(\w+)/) ?? [null, null])[1];
if (i != null && i !== 'null') {
if (_DEV_) {
console.log('signing...');
}
try {
document.body.innerHTML = '<div>Please wait...</div>';
await login(i);
} catch (err) {
// Render the error screen
// TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな)
document.body.innerHTML = '<div id="err">Oops!</div>';
}
} else {
if (_DEV_) {
console.log('not signed in');
}
}
}
//#endregion
const fetchInstanceMetaPromise = fetchInstance();
fetchInstanceMetaPromise.then(() => {
miLocalStorage.setItem('v', instance.version);
// Init service worker
initializeSw();
});
try {
await fetchCustomEmojis();
} catch (err) { /* empty */ }
const app = createApp(
new URLSearchParams(window.location.search).has('zen') ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
defineAsyncComponent(() => import('@/ui/universal.vue')),
);
if (_DEV_) {
app.config.performance = true;
}
widgets(app);
directives(app);
components(app);
const splash = document.getElementById('splash');
// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
if (splash) splash.addEventListener('transitionend', () => {
splash.remove();
});
await deckStore.ready;
// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210
// なぜかinit.tsの内容が2回実行されることがあるため、mountするdivを1つに制限する
const rootEl = ((): HTMLElement => {
const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID);
if (currentRoot) {
console.warn('multiple import detected');
return currentRoot;
}
const root = document.createElement('div');
root.id = MISSKEY_MOUNT_DIV_ID;
document.body.appendChild(root);
return root;
})();
app.mount(rootEl);
// boot.jsのやつを解除
window.onerror = null;
window.onunhandledrejection = null;
reactionPicker.init();
if (splash) {
splash.style.opacity = '0';
splash.style.pointerEvents = 'none';
}
// クライアントが更新されたか?
const lastVersion = miLocalStorage.getItem('lastVersion');
if (lastVersion !== version) {
miLocalStorage.setItem('lastVersion', version);
// テーマリビルドするため
miLocalStorage.removeItem('theme');
try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため
if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
// ログインしてる場合だけ
if ($i) {
popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed');
}
}
} catch (err) { /* empty */ }
}
await defaultStore.ready;
// NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため)
watch(defaultStore.reactiveState.darkMode, (darkMode) => {
applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
}, { immediate: miLocalStorage.getItem('theme') == null });
const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
watch(darkTheme, (theme) => {
if (defaultStore.state.darkMode) {
applyTheme(theme);
}
});
watch(lightTheme, (theme) => {
if (!defaultStore.state.darkMode) {
applyTheme(theme);
}
});
//#region Sync dark mode
if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
defaultStore.set('darkMode', isDeviceDarkmode());
}
window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
defaultStore.set('darkMode', mql.matches);
}
});
//#endregion
fetchInstanceMetaPromise.then(() => {
if (defaultStore.state.themeInitial) {
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme));
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme));
defaultStore.set('themeInitial', false);
}
});
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
}, { immediate: true });
watch(defaultStore.reactiveState.useBlurEffect, v => {
if (v) {
document.documentElement.style.removeProperty('--blur');
} else {
document.documentElement.style.setProperty('--blur', 'none');
}
}, { immediate: true });
let reloadDialogShowing = false;
stream.on('_disconnected_', async () => {
if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
location.reload();
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
if (reloadDialogShowing) return;
reloadDialogShowing = true;
const { canceled } = await confirm({
type: 'warning',
title: i18n.ts.disconnectedFromServer,
text: i18n.ts.reloadConfirm,
});
reloadDialogShowing = false;
if (!canceled) {
location.reload();
}
}
});
for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
import('./plugin').then(async ({ install }) => {
// Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740
await new Promise(r => setTimeout(r, 0));
install(plugin);
});
}
const hotkeys = {
'd': (): void => {
defaultStore.set('darkMode', !defaultStore.state.darkMode);
},
's': (): void => {
mainRouter.push('/search');
},
};
if ($i) {
// only add post shortcuts if logged in
hotkeys['p|n'] = post;
if (defaultStore.state.accountSetupWizard !== -1) {
// このウィザードが実装される前に登録したユーザーには表示させないため
// TODO: そのうち消す
if (Date.now() - new Date($i.createdAt).getTime() < 1000 * 60 * 60 * 24) {
popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed');
} else {
defaultStore.set('accountSetupWizard', -1);
}
}
if ($i.isDeleted) {
alert({
type: 'warning',
text: i18n.ts.accountDeletionInProgress,
});
}
const now = new Date();
const m = now.getMonth() + 1;
const d = now.getDate();
if ($i.birthday) {
const bm = parseInt($i.birthday.split('-')[1]);
const bd = parseInt($i.birthday.split('-')[2]);
if (m === bm && d === bd) {
claimAchievement('loggedInOnBirthday');
}
}
if (m === 1 && d === 1) {
claimAchievement('loggedInOnNewYearsDay');
}
if ($i.loggedInDays >= 3) claimAchievement('login3');
if ($i.loggedInDays >= 7) claimAchievement('login7');
if ($i.loggedInDays >= 15) claimAchievement('login15');
if ($i.loggedInDays >= 30) claimAchievement('login30');
if ($i.loggedInDays >= 60) claimAchievement('login60');
if ($i.loggedInDays >= 100) claimAchievement('login100');
if ($i.loggedInDays >= 200) claimAchievement('login200');
if ($i.loggedInDays >= 300) claimAchievement('login300');
if ($i.loggedInDays >= 400) claimAchievement('login400');
if ($i.loggedInDays >= 500) claimAchievement('login500');
if ($i.loggedInDays >= 600) claimAchievement('login600');
if ($i.loggedInDays >= 700) claimAchievement('login700');
if ($i.loggedInDays >= 800) claimAchievement('login800');
if ($i.loggedInDays >= 900) claimAchievement('login900');
if ($i.loggedInDays >= 1000) claimAchievement('login1000');
if ($i.notesCount > 0) claimAchievement('notes1');
if ($i.notesCount >= 10) claimAchievement('notes10');
if ($i.notesCount >= 100) claimAchievement('notes100');
if ($i.notesCount >= 500) claimAchievement('notes500');
if ($i.notesCount >= 1000) claimAchievement('notes1000');
if ($i.notesCount >= 5000) claimAchievement('notes5000');
if ($i.notesCount >= 10000) claimAchievement('notes10000');
if ($i.notesCount >= 20000) claimAchievement('notes20000');
if ($i.notesCount >= 30000) claimAchievement('notes30000');
if ($i.notesCount >= 40000) claimAchievement('notes40000');
if ($i.notesCount >= 50000) claimAchievement('notes50000');
if ($i.notesCount >= 60000) claimAchievement('notes60000');
if ($i.notesCount >= 70000) claimAchievement('notes70000');
if ($i.notesCount >= 80000) claimAchievement('notes80000');
if ($i.notesCount >= 90000) claimAchievement('notes90000');
if ($i.notesCount >= 100000) claimAchievement('notes100000');
if ($i.followersCount > 0) claimAchievement('followers1');
if ($i.followersCount >= 10) claimAchievement('followers10');
if ($i.followersCount >= 50) claimAchievement('followers50');
if ($i.followersCount >= 100) claimAchievement('followers100');
if ($i.followersCount >= 300) claimAchievement('followers300');
if ($i.followersCount >= 500) claimAchievement('followers500');
if ($i.followersCount >= 1000) claimAchievement('followers1000');
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) {
claimAchievement('passedSinceAccountCreated1');
}
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) {
claimAchievement('passedSinceAccountCreated2');
}
if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) {
claimAchievement('passedSinceAccountCreated3');
}
if (claimedAchievements.length >= 30) {
claimAchievement('collectAchievements30');
}
window.setInterval(() => {
if (Math.floor(Math.random() * 20000) === 0) {
claimAchievement('justPlainLucky');
}
}, 1000 * 10);
window.setTimeout(() => {
claimAchievement('client30min');
}, 1000 * 60 * 30);
window.setTimeout(() => {
claimAchievement('client60min');
}, 1000 * 60 * 60);
const lastUsed = miLocalStorage.getItem('lastUsed');
if (lastUsed) {
const lastUsedDate = parseInt(lastUsed, 10);
// 二時間以上前なら
if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
toast(i18n.t('welcomeBackWithName', {
name: $i.name || $i.username,
}));
}
}
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))) && !location.pathname.startsWith('/miauth')) {
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) {
// 許可を得ていなかったらリクエスト
if (Notification.permission === 'default') {
Notification.requestPermission();
}
}
const main = markRaw(stream.useChannel('main', null, 'System'));
// 自分の情報が更新されたとき
main.on('meUpdated', i => {
updateAccount(i);
});
main.on('readAllNotifications', () => {
updateAccount({ hasUnreadNotification: false });
});
main.on('unreadNotification', () => {
updateAccount({ hasUnreadNotification: true });
});
main.on('unreadMention', () => {
updateAccount({ hasUnreadMentions: true });
});
main.on('readAllUnreadMentions', () => {
updateAccount({ hasUnreadMentions: false });
});
main.on('unreadSpecifiedNote', () => {
updateAccount({ hasUnreadSpecifiedNotes: true });
});
main.on('readAllUnreadSpecifiedNotes', () => {
updateAccount({ hasUnreadSpecifiedNotes: false });
});
main.on('readAllAntennas', () => {
updateAccount({ hasUnreadAntenna: false });
});
main.on('unreadAntenna', () => {
updateAccount({ hasUnreadAntenna: true });
sound.play('antenna');
});
main.on('readAllAnnouncements', () => {
updateAccount({ hasUnreadAnnouncement: false });
});
// トークンが再生成されたとき
// このままではMisskeyが利用できないので強制的にサインアウトさせる
main.on('myTokenRegenerated', () => {
signout();
});
}
// shortcut
document.addEventListener('keydown', makeHotkey(hotkeys));

View file

@ -86,8 +86,13 @@
</FormSection>
<FormSection>
<template #label>Special thanks</template>
<div style="text-align: center;">
<a style="display: inline-block;" class="dcadvirth" title="DC Advirth" href="https://www.dotchain.ltd/advirth" target="_blank"><img width="200" src="https://misskey-hub.net/sponsors/dcadvirth.png" alt="DC Advirth"></a>
<div class="_gaps" style="text-align: center;">
<div>
<a style="display: inline-block;" class="masknetwork" title="Mask Network" href="https://mask.io/" target="_blank"><img width="200" src="https://misskey-hub.net/sponsors/masknetwork.png" alt="Mask Network"></a>
</div>
<div>
<a style="display: inline-block;" class="dcadvirth" title="DC Advirth" href="https://www.dotchain.ltd/advirth" target="_blank"><img width="200" src="https://misskey-hub.net/sponsors/dcadvirth.png" alt="DC Advirth"></a>
</div>
</div>
</FormSection>
</div>

View file

@ -2,7 +2,7 @@
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="900">
<div class="lcixvhis">
<div>
<div class="reports">
<div class="">
<div class="inputs" style="display: flex;">
@ -87,9 +87,3 @@ definePageMetadata({
icon: 'ti ti-exclamation-circle',
});
</script>
<style lang="scss" scoped>
.lcixvhis {
margin: var(--margin);
}
</style>

View file

@ -2,7 +2,7 @@
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="900">
<div class="ztgjmzrw _gaps_m">
<div class="_gaps_m">
<section v-for="announcement in announcements" class="">
<div class="_panel _gaps_m" style="padding: 24px;">
<MkInput v-model="announcement.title">
@ -113,9 +113,3 @@ definePageMetadata({
icon: 'ti ti-speakerphone',
});
</script>
<style lang="scss" scoped>
.ztgjmzrw {
margin: var(--margin);
}
</style>

View file

@ -3,7 +3,7 @@
<MkStickyContainer>
<template #header><XHeader :actions="headerActions"/></template>
<MkSpacer :content-max="900">
<div class="xrmjdkdw">
<div>
<div>
<div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
<MkSelect v-model="origin" style="margin: 0; flex: 1;">
@ -109,9 +109,3 @@ definePageMetadata(computed(() => ({
icon: 'ti ti-cloud',
})));
</script>
<style lang="scss" scoped>
.xrmjdkdw {
margin: var(--margin);
}
</style>

View file

@ -67,7 +67,3 @@ onMounted(() => {
});
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -132,7 +132,3 @@ defineExpose({
pushData,
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -33,9 +33,9 @@
import { markRaw, onMounted, onUnmounted, ref } from 'vue';
import XChart from './overview.queue.chart.vue';
import number from '@/filters/number';
import { stream } from '@/stream';
import { useStream } from '@/stream';
const connection = markRaw(stream.useChannel('queueStats'));
const connection = markRaw(useStream().useChannel('queueStats'));
const activeSincePrevTick = ref(0);
const active = ref(0);

View file

@ -1,6 +1,6 @@
<template>
<MkSpacer :content-max="1000">
<div ref="rootEl" class="edbbcaef">
<div ref="rootEl" :class="$style.root">
<MkFoldableSection class="item">
<template #header>Stats</template>
<XStats/>
@ -72,7 +72,7 @@ import XRetention from './overview.retention.vue';
import XModerators from './overview.moderators.vue';
import XHeatmap from './overview.heatmap.vue';
import * as os from '@/os';
import { stream } from '@/stream';
import { useStream } from '@/stream';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
@ -87,7 +87,7 @@ let federationSubActive = $ref<number | null>(null);
let federationSubActiveDiff = $ref<number | null>(null);
let newUsers = $ref(null);
let activeInstances = $shallowRef(null);
const queueStatsConnection = markRaw(stream.useChannel('queueStats'));
const queueStatsConnection = markRaw(useStream().useChannel('queueStats'));
const now = new Date();
const filesPagination = {
endpoint: 'admin/drive/files' as const,
@ -176,8 +176,8 @@ definePageMetadata({
});
</script>
<style lang="scss" scoped>
.edbbcaef {
<style lang="scss" module>
.root {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
grid-gap: 16px;

View file

@ -132,7 +132,3 @@ defineExpose({
pushData,
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -47,11 +47,11 @@ import { markRaw, onMounted, onUnmounted, ref } from 'vue';
import XChart from './queue.chart.chart.vue';
import number from '@/filters/number';
import * as os from '@/os';
import { stream } from '@/stream';
import { useStream } from '@/stream';
import { i18n } from '@/i18n';
import MkFolder from '@/components/MkFolder.vue';
const connection = markRaw(stream.useChannel('queueStats'));
const connection = markRaw(useStream().useChannel('queueStats'));
const activeSincePrevTick = ref(0);
const active = ref(0);

View file

@ -5,6 +5,8 @@
<div v-if="channel && tab === 'overview'" class="_gaps">
<div class="_panel" :class="$style.bannerContainer">
<XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/>
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" as-like class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ti ti-star"></i></MkButton>
<MkButton v-else v-tooltip="i18n.ts.favorite" as-like class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ti ti-star"></i></MkButton>
<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" :class="$style.banner">
<div :class="$style.bannerStatus">
<div><i class="ti ti-users ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
@ -13,13 +15,10 @@
<div :class="$style.bannerFade"></div>
</div>
<div v-if="channel.description" :class="$style.description">
<Mfm :text="channel.description" :is-note="false" :i="$i"/>
<Mfm :text="channel.description" :isNote="false" :i="$i"/>
</div>
</div>
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" as-like class="button" rounded primary @click="unfavorite()"><i class="ti ti-star"></i></MkButton>
<MkButton v-else v-tooltip="i18n.ts.favorite" as-like class="button" rounded @click="favorite()"><i class="ti ti-star"></i></MkButton>
<MkFoldableSection>
<template #header><i class="ti ti-pin ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template>
<div v-if="channel.pinnedNotes.length > 0" class="_gaps">
@ -229,6 +228,13 @@ definePageMetadata(computed(() => channel ? {
left: 16px;
}
.favorite {
position: absolute;
z-index: 1;
top: 16px;
right: 16px;
}
.banner {
position: relative;
height: 200px;

View file

@ -5,7 +5,7 @@
<div v-if="clip">
<div class="okzinsic _panel">
<div v-if="clip.description" class="description">
<Mfm :text="clip.description" :is-note="false" :i="$i"/>
<Mfm :text="clip.description" :isNote="false" :i="$i"/>
</div>
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" as-like class="button" rounded primary @click="unfavorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>
<MkButton v-else v-tooltip="i18n.ts.favorite" as-like class="button" rounded @click="favorite()"><i class="ti ti-heart"></i><span v-if="clip.favoritedCount > 0" style="margin-left: 6px;">{{ clip.favoritedCount }}</span></MkButton>

View file

@ -10,8 +10,8 @@
<template #header>:{{ emoji.name }}:</template>
<MkSpacer :margin-min="20" :margin-max="28">
<div class="yigymqpb _gaps_m">
<img :src="`/emoji/${emoji.name}.webp`" class="img"/>
<div class="_gaps_m">
<img :src="`/emoji/${emoji.name}.webp`" :class="$style.img"/>
<MkInput v-model="name">
<template #label>{{ i18n.ts.name }}</template>
</MkInput>
@ -99,12 +99,10 @@ async function del() {
}
</script>
<style lang="scss" scoped>
.yigymqpb {
> .img {
display: block;
height: 64px;
margin: 0 auto;
}
<style lang="scss" module>
.img {
display: block;
height: 64px;
margin: 0 auto;
}
</style>

View file

@ -33,7 +33,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
import MkInput from '@/components/MkInput.vue';
import { useRouter } from '@/router';
const PRESET_DEFAULT = `/// @ 0.13.2
const PRESET_DEFAULT = `/// @ 0.13.3
var name = ""
@ -51,7 +51,7 @@ Ui:render([
])
`;
const PRESET_OMIKUJI = `/// @ 0.13.2
const PRESET_OMIKUJI = `/// @ 0.13.3
//
//
@ -94,7 +94,7 @@ Ui:render([
])
`;
const PRESET_SHUFFLE = `/// @ 0.13.2
const PRESET_SHUFFLE = `/// @ 0.13.3
//
let string = "ペペロンチーノ"
@ -173,7 +173,7 @@ var cursor = 0
do()
`;
const PRESET_QUIZ = `/// @ 0.13.2
const PRESET_QUIZ = `/// @ 0.13.3
let title = '地理クイズ'
let qas = [{
@ -286,7 +286,7 @@ qaEls.push(Ui:C:container({
Ui:render(qaEls)
`;
const PRESET_TIMELINE = `/// @ 0.13.2
const PRESET_TIMELINE = `/// @ 0.13.3
// API
@fetch() {
@ -442,7 +442,3 @@ definePageMetadata(computed(() => flash ? {
title: i18n.ts._play.new,
}));
</script>
<style lang="scss" scoped>
</style>

View file

@ -7,7 +7,7 @@
<MkFoldableSection class="_margin">
<template #header><i class="ti ti-clock"></i>{{ i18n.ts.recentPosts }}</template>
<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disable-auto-load="true">
<div class="vfpdbgtk">
<div :class="$style.items">
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
</div>
</MkPagination>
@ -15,7 +15,7 @@
<MkFoldableSection class="_margin">
<template #header><i class="ti ti-comet"></i>{{ i18n.ts.popularPosts }}</template>
<MkPagination v-slot="{items}" :pagination="popularPostsPagination" :disable-auto-load="true">
<div class="vfpdbgtk">
<div :class="$style.items">
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
</div>
</MkPagination>
@ -23,7 +23,7 @@
</div>
<div v-else-if="tab === 'liked'">
<MkPagination v-slot="{items}" :pagination="likedPostsPagination">
<div class="vfpdbgtk">
<div :class="$style.items">
<MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/>
</div>
</MkPagination>
@ -31,7 +31,7 @@
<div v-else-if="tab === 'my'">
<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="ti ti-plus"></i> {{ i18n.ts.postToGallery }}</MkA>
<MkPagination v-slot="{items}" :pagination="myPostsPagination">
<div class="vfpdbgtk">
<div :class="$style.items">
<MkGalleryPostPreview v-for="post in items" :key="post.id" :post="post" class="post"/>
</div>
</MkPagination>
@ -119,15 +119,11 @@ definePageMetadata({
});
</script>
<style lang="scss" scoped>
.vfpdbgtk {
<style lang="scss" module>
.items {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-gap: 12px;
margin: 0 var(--margin);
> .post {
}
}
</style>

View file

@ -38,7 +38,3 @@ definePageMetadata({
icon: 'ti ti-antenna',
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -36,7 +36,3 @@ definePageMetadata({
icon: 'ti ti-antenna',
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,6 +1,6 @@
<template>
<MkSpacer :content-max="700">
<div class="shaynizk">
<div>
<div class="_gaps_m">
<MkInput v-model="name">
<template #label>{{ i18n.ts.name }}</template>
@ -33,7 +33,7 @@
<MkSwitch v-model="withFile">{{ i18n.ts.withFileAntenna }}</MkSwitch>
<MkSwitch v-model="notify">{{ i18n.ts.notifyAntenna }}</MkSwitch>
</div>
<div class="actions">
<div :class="$style.actions">
<MkButton inline primary @click="saveAntenna()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
<MkButton v-if="antenna.id != null" inline danger @click="deleteAntenna()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
@ -128,12 +128,10 @@ function addUser() {
}
</script>
<style lang="scss" scoped>
.shaynizk {
> .actions {
margin-top: 16px;
padding: 24px 0;
border-top: solid 0.5px var(--divider);
}
<style lang="scss" module>
.actions {
margin-top: 16px;
padding: 24px 0;
border-top: solid 0.5px var(--divider);
}
</style>

View file

@ -8,8 +8,8 @@
</button>
</template>
<section class="oyyftmcf">
<MkDriveFileThumbnail v-if="file" class="preview" :file="file" fit="contain" @click="choose()"/>
<section>
<MkDriveFileThumbnail v-if="file" style="height: 150px;" :file="file" fit="contain" @click="choose()"/>
</section>
</XContainer>
</template>
@ -54,11 +54,3 @@ onMounted(async () => {
}
});
</script>
<style lang="scss" scoped>
.oyyftmcf {
> .preview {
height: 150px;
}
}
</style>

View file

@ -3,8 +3,8 @@
<XContainer :draggable="true" @remove="() => $emit('remove')">
<template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template>
<section class="vckmsadr">
<textarea v-model="text"></textarea>
<section>
<textarea v-model="text" :class="$style.textarea"></textarea>
</section>
</XContainer>
</template>
@ -33,23 +33,21 @@ watch($$(text), () => {
});
</script>
<style lang="scss" scoped>
.vckmsadr {
> textarea {
display: block;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: 100%;
min-width: 100%;
min-height: 150px;
border: none;
box-shadow: none;
padding: 16px;
background: transparent;
color: var(--fg);
font-size: 14px;
box-sizing: border-box;
}
<style lang="scss" module>
.textarea {
display: block;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: 100%;
min-width: 100%;
min-height: 150px;
border: none;
box-shadow: none;
padding: 16px;
background: transparent;
color: var(--fg);
font-size: 14px;
box-sizing: border-box;
}
</style>

View file

@ -9,49 +9,41 @@
</Sortable>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import XSection from './els/page-editor.el.section.vue';
import XText from './els/page-editor.el.text.vue';
import XImage from './els/page-editor.el.image.vue';
import XNote from './els/page-editor.el.note.vue';
export default defineComponent({
components: {
Sortable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
XSection, XText, XImage, XNote,
},
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
props: {
modelValue: {
type: Array,
required: true,
},
},
const props = defineProps<{
modelValue: any[];
}>();
emits: ['update:modelValue'],
const emit = defineEmits<{
(ev: 'update:modelValue', value: any[]): void;
}>();
methods: {
updateItem(v) {
const i = this.modelValue.findIndex(x => x.id === v.id);
const newValue = [
...this.modelValue.slice(0, i),
v,
...this.modelValue.slice(i + 1),
];
this.$emit('update:modelValue', newValue);
},
function updateItem(v) {
const i = props.modelValue.findIndex(x => x.id === v.id);
const newValue = [
...props.modelValue.slice(0, i),
v,
...props.modelValue.slice(i + 1),
];
emit('update:modelValue', newValue);
}
removeItem(el) {
const i = this.modelValue.findIndex(x => x.id === el.id);
const newValue = [
...this.modelValue.slice(0, i),
...this.modelValue.slice(i + 1),
];
this.$emit('update:modelValue', newValue);
},
},
});
function removeItem(el) {
const i = props.modelValue.findIndex(x => x.id === el.id);
const newValue = [
...props.modelValue.slice(0, i),
...props.modelValue.slice(i + 1),
];
emit('update:modelValue', newValue);
}
</script>
<style lang="scss" module>

Some files were not shown because too many files have changed in this diff Show more