1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-09 16:45:33 +02:00

Compare commits

...

6 Commits

Author SHA1 Message Date
かっこかり
a09a2c2eee enhance: 絵文字データの参照を自前ライブラリに変更 (#17381)
* wip

* enhance: 絵文字データの参照を自前ライブラリに変更

* fix

* update to v17.0.2

* fix assets handling

* fix

* update mfm-js

* update emoji library

* Update COPYING [ci skip]

* Update Changelog

* Update Changelog

* fix: 端末の絵文字にフォールバックできるように
2026-05-09 18:35:38 +09:00
4ster1sk
717931cfcb fix(frontend): ドライブのファイル更新が即座に反映されない問題の修正 (#17383) 2026-05-09 18:33:16 +09:00
syuilo
9027129b58 enhance(frontend): MkInputでthrottleできるように & delay設定できるように 2026-05-08 18:26:05 +09:00
かっこかり
b73ac26612 Update CHANGELOG.md 2026-05-07 13:37:36 +09:00
かっこかり
b528ff9c59 enhance(frontend): テーマの適用管理を改善 (#17376)
* wip

* add test

* use themeManager.currentCompiledTheme for obtaining theme variables / reduce getComputedStyle usage

* fix

* fix: better error handling on theme installation

* Update Changelog

* chore: remove frontend-shared builds as it is currently working as a stub package

* fix: broken lockfile

* fix

* fix lint

* fix
2026-05-07 11:42:45 +09:00
github-actions[bot]
a82ba0d775 [skip ci] Update CHANGELOG.md (prepend template) 2026-05-06 10:44:25 +00:00
72 changed files with 828 additions and 8276 deletions

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "fluent-emojis"]
path = fluent-emojis
url = https://github.com/misskey-dev/emojis.git

View File

@@ -1,3 +1,20 @@
## Unreleased
### General
- Enhance: Unicode 17.0 に収録されている絵文字の処理・表示に対応
- Fluent Emojiや端末ネイティブの絵文字を利用している場合は、最新の絵文字に対応しておらず正しく表示できない可能性があります。絵文字が表示できない場合は、表示に使用する絵文字をTwemojiに切り替えてご利用ください。
### Client
- Enhance: テーマのプレビュー時、リロードせずにもとのテーマに戻せるように
- Enhance: Fluent Emojiを更新し、Unicode 15+相当の絵文字の表示に対応
- Fix: テーマエディター使用時に、最初の変更のみ適用される問題を修正
- Fix: テーマのプレビュー時、既存のテーマとIDが被っている場合にプレビューできない問題を修正
- Fix: テーマのインストールエラーの表示を改善
### Server
-
## 2026.5.1
### General

View File

@@ -3,13 +3,6 @@ Copyright © 2014-2026 syuilo and contributors
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.
Misskey includes several third-party Open-Source softwares.
Emoji keywords for Unicode 11 and below by Mu-An Chiou
License: MIT
https://github.com/muan/emojilib/blob/master/LICENSE
RsaSignature2017 implementation by Transmute Industries Inc
License: MIT
https://github.com/transmute-industries/RsaSignature2017/blob/master/LICENSE

View File

@@ -103,7 +103,6 @@ COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-rev
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built
COPY --chown=misskey:misskey --from=native-builder /misskey/fluent-emojis /misskey/fluent-emojis
COPY --chown=misskey:misskey . ./
ENV LD_PRELOAD=/usr/local/lib/libjemalloc.so

Submodule fluent-emojis deleted from cae981eb4c

View File

@@ -1409,6 +1409,8 @@ presets: "プリセット"
zeroPadding: "ゼロ埋め"
nothingToConfigure: "設定項目はありません"
viewRenotedChannel: "リノート先のチャンネルを見る"
previewingTheme: "テーマのプレビュー中"
previewingThemeRestore: "元に戻す"
_imageEditing:
_vars:

View File

@@ -55,7 +55,6 @@
"dependencies": {
"@aws-sdk/client-s3": "3.1037.0",
"@aws-sdk/lib-storage": "3.1037.0",
"@discordapp/twemoji": "16.0.1",
"@fastify/accepts": "5.0.4",
"@fastify/cors": "11.2.0",
"@fastify/express": "4.0.5",
@@ -63,6 +62,8 @@
"@fastify/multipart": "10.0.0",
"@fastify/static": "9.1.3",
"@kitajs/html": "4.2.13",
"@misskey-dev/emoji-assets": "17.0.3",
"@misskey-dev/emoji-data": "17.0.3",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.3.0",
"@napi-rs/canvas": "0.1.100",
@@ -76,7 +77,6 @@
"@simplewebauthn/server": "13.3.0",
"@sinonjs/fake-timers": "15.3.2",
"@smithy/node-http-handler": "4.6.1",
"@twemoji/parser": "16.0.0",
"accepts": "1.3.8",
"ajv": "8.20.0",
"archiver": "7.0.1",
@@ -111,7 +111,7 @@
"jsonld": "9.0.0",
"juice": "11.1.1",
"meilisearch": "0.57.0",
"mfm-js": "0.25.0",
"mfm-js": "0.26.0",
"mime-types": "3.0.2",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",

File diff suppressed because one or more lines are too long

View File

@@ -71,7 +71,7 @@ export class ClientServerService {
private readonly clientAssets: string;
private readonly assets: string;
private readonly swAssets: string;
private readonly fluentEmojisDir: string;
private readonly fluentEmojiDir: string;
private readonly twemojiDir: string;
private readonly frontendViteOut: string;
private readonly frontendEmbedViteOut: string;
@@ -135,8 +135,8 @@ export class ClientServerService {
this.clientAssets = resolve(frontendRootdir, 'assets');
this.assets = resolve(this.config.rootDir, 'built/_frontend_dist_');
this.swAssets = resolve(this.config.rootDir, 'built/_sw_dist_');
this.fluentEmojisDir = resolve(this.config.rootDir, 'fluent-emojis/dist');
this.twemojiDir = resolve(backendRootdir, 'node_modules/@discordapp/twemoji/dist/svg');
this.fluentEmojiDir = resolve(backendRootdir, 'node_modules/@misskey-dev/emoji-assets/built/fluent-emoji');
this.twemojiDir = resolve(backendRootdir, 'node_modules/@misskey-dev/emoji-assets/built/twemoji');
this.frontendViteOut = resolve(this.config.rootDir, 'built/_frontend_vite_');
this.frontendEmbedViteOut = resolve(this.config.rootDir, 'built/_frontend_embed_vite_');
this.tarball = resolve(this.config.rootDir, 'built/tarball');
@@ -306,7 +306,7 @@ export class ClientServerService {
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
return reply.sendFile(path, this.fluentEmojisDir, {
return reply.sendFile(path, this.fluentEmojiDir, {
maxAge: ms('30 days'),
});
});

View File

@@ -10,10 +10,8 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"dependencies": {
"@discordapp/twemoji": "16.0.1",
"@rollup/plugin-json": "6.1.0",
"@rollup/pluginutils": "5.3.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.6",
"buraha": "0.0.1",
"estree-walker": "3.0.3",
@@ -21,7 +19,7 @@
"i18n": "workspace:*",
"icons-subsetter": "workspace:*",
"json5": "2.2.3",
"mfm-js": "0.25.0",
"mfm-js": "0.26.0",
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.60.2",
@@ -31,6 +29,7 @@
"vue": "3.5.33"
},
"devDependencies": {
"@misskey-dev/emoji-assets": "17.0.3",
"@misskey-dev/summaly": "5.3.0",
"@tabler/icons-webfont": "3.35.0",
"@testing-library/vue": "8.1.0",

View File

@@ -28,7 +28,7 @@ import { postMessageToParentWindow, setIframeId } from '@/post-message.js';
import { serverContext } from '@/server-context.js';
import { i18n } from '@/i18n.js';
import type { Theme } from '@/theme.js';
import type { Theme } from '@@/js/theme.js';
console.log('Misskey Embed');

View File

@@ -21,7 +21,7 @@ html {
accent-color: var(--MI_THEME-accent);
overflow: clip;
overflow-wrap: break-word;
font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 14px;
line-height: 1.35;
text-size-adjust: 100%;

View File

@@ -5,26 +5,10 @@
// TODO: (可能な部分を)sharedに抽出して frontend と共通化
import tinycolor from 'tinycolor2';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import type { BundledTheme } from 'shiki/themes';
export type Theme = {
id: string;
name: string;
author: string;
desc?: string;
base?: 'dark' | 'light';
props: Record<string, string>;
codeHighlighter?: {
base: BundledTheme;
overrides?: Record<string, any>;
} | {
base: '_none_';
overrides: Record<string, any>;
};
};
import { compile } from '@@/js/theme.js';
import type { Theme } from '@@/js/theme.js';
let timeout: number | null = null;
@@ -32,7 +16,7 @@ export function assertIsTheme(theme: Record<string, unknown>): theme is Theme {
return typeof theme === 'object' && theme !== null && 'id' in theme && 'name' in theme && 'author' in theme && 'props' in theme;
}
export function applyTheme(theme: Theme, persist = true) {
export function applyTheme(theme: Theme) {
if (timeout) window.clearTimeout(timeout);
window.document.documentElement.classList.add('_themeChanging_');
@@ -68,48 +52,3 @@ export function applyTheme(theme: Theme, persist = true) {
// iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照
}
function compile(theme: Theme): Record<string, string> {
function getColor(val: string): tinycolor.Instance {
if (val[0] === '@') { // ref (prop)
return getColor(theme.props[val.substring(1)]);
} else if (val[0] === '$') { // ref (const)
return getColor(theme.props[val]);
} else if (val[0] === ':') { // func
const parts = val.split('<');
const funcTxt = parts.shift();
const argTxt = parts.shift();
if (funcTxt && argTxt) {
const func = funcTxt.substring(1);
const arg = parseFloat(argTxt);
const color = getColor(parts.join('<'));
switch (func) {
case 'darken': return color.darken(arg);
case 'lighten': return color.lighten(arg);
case 'alpha': return color.setAlpha(arg);
case 'hue': return color.spin(arg);
case 'saturate': return color.saturate(arg);
}
}
}
// other case
return tinycolor(val);
}
const props = {};
for (const [k, v] of Object.entries(theme.props)) {
if (k.startsWith('$')) continue; // ignore const
props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
}
return props;
}
function genValue(c: tinycolor.Instance): string {
return c.toRgbString();
}

View File

@@ -98,7 +98,8 @@ export function getConfig(): UserConfig {
'@/': __dirname + '/src/',
'@@/': __dirname + '/../frontend-shared/',
'/client-assets/': __dirname + '/assets/',
'/static-assets/': __dirname + '/../backend/assets/'
'/static-assets/': __dirname + '/../backend/assets/',
'/fluent-emoji/': '@misskey-dev/emoji-assets/fluent-emoji/',
},
},

View File

@@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
declare module '@@/themes/*.json5' {
import { Theme } from '@@/js/theme.js';
const theme: Theme;
// eslint-disable-next-line import/no-default-export
export default theme;
}

View File

@@ -1,109 +0,0 @@
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import * as esbuild from 'esbuild';
import { build } from 'esbuild';
import { execa } from 'execa';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
const entryPoints = fs.globSync('./js/**/**.{ts,tsx}');
/** @type {import('esbuild').BuildOptions} */
const options = {
entryPoints,
minify: process.env.NODE_ENV === 'production',
outdir: './js-built',
target: 'es2022',
platform: 'browser',
format: 'esm',
sourcemap: 'linked',
};
const args = process.argv.slice(2).map(arg => arg.toLowerCase());
// js-built配下をすべて削除する
if (!args.includes('--no-clean')) {
fs.rmSync('./js-built', { recursive: true, force: true });
}
if (args.includes('--watch')) {
await watchSrc();
} else {
await buildSrc();
}
async function buildSrc() {
console.log(`[${_package.name}] start building...`);
await build(options)
.then(() => {
console.log(`[${_package.name}] build succeeded.`);
})
.catch((err) => {
process.stderr.write(err.stderr);
process.exit(1);
});
if (process.env.NODE_ENV === 'production') {
console.log(`[${_package.name}] skip building d.ts because NODE_ENV is production.`);
} else {
await buildDts();
}
fs.copyFileSync('./js/emojilist.json', './js-built/emojilist.json');
console.log(`[${_package.name}] finish building.`);
}
function buildDts() {
return execa(
'tsgo',
[
'--project', 'tsconfig.json',
'--outDir', 'js-built',
'--declaration', 'true',
'--emitDeclarationOnly', 'true',
],
{
stdout: process.stdout,
stderr: process.stderr,
},
);
}
async function watchSrc() {
const plugins = [{
name: 'gen-dts',
setup(build) {
build.onStart(() => {
console.log(`[${_package.name}] detect changed...`);
});
build.onEnd(async result => {
if (result.errors.length > 0) {
console.error(`[${_package.name}] watch build failed:`, result);
return;
}
await buildDts();
});
},
}];
console.log(`[${_package.name}] start watching...`);
const context = await esbuild.context({ ...options, plugins });
await context.watch();
await new Promise((resolve, reject) => {
process.on('SIGHUP', resolve);
process.on('SIGINT', resolve);
process.on('SIGTERM', resolve);
process.on('uncaughtException', reject);
process.on('exit', resolve);
}).finally(async () => {
await context.dispose();
console.log(`[${_package.name}] finish watching.`);
});
}

View File

@@ -19,7 +19,7 @@ export function char2fluentEmojiFilePath(char: string): string {
// Fluent Emojiは国旗非対応 https://github.com/microsoft/fluentui-emoji/issues/25
if (codes[0]?.startsWith('1f1')) return char2twemojiFilePath(char);
if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
codes = codes.filter(x => x != null && x.length > 0);
const fileName = (codes as string[]).map(x => x.padStart(4, '0')).join('-');
codes = codes.filter(x => x && x.length);
const fileName = codes.join('-');
return `${fluentEmojiPngBase}/${fileName}.png`;
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,8 +11,7 @@ export type UnicodeEmojiDef = {
category: typeof unicodeEmojiCategories[number];
};
// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
import _emojilist from './emojilist.json' with { type: 'json' };
import _emojilist from '@misskey-dev/emoji-data/emojilist.json';
export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({
name: x[1] as string,

View File

@@ -0,0 +1,126 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import tinycolor from 'tinycolor2';
import JSON5 from 'json5';
import lightTheme from '@@/themes/_light.json5';
import type { BundledTheme } from 'shiki/themes';
export type Theme = {
id: string;
name: string;
author: string;
desc?: string;
base?: 'dark' | 'light';
kind?: 'dark' | 'light'; // legacy
props: Record<string, string>;
codeHighlighter?: {
base: BundledTheme;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
overrides?: Record<string, any>;
} | {
base: '_none_';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
overrides: Record<string, any>;
};
};
export type CompiledTheme = Record<string, string>;
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
export const getBuiltinThemes = () => Promise.all(
[
'l-light',
'l-coffee',
'l-apricot',
'l-rainy',
'l-botanical',
'l-vivid',
'l-cherry',
'l-sushi',
'l-u0',
'd-dark',
'd-persimmon',
'd-astro',
'd-future',
'd-botanical',
'd-green-lime',
'd-green-orange',
'd-cherry',
'd-ice',
'd-u0',
].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
);
export function compile(theme: Theme): CompiledTheme {
function getColor(val: string): tinycolor.Instance {
if (val[0] === '@') { // ref (prop)
return getColor(theme.props[val.substring(1)]);
} else if (val[0] === '$') { // ref (const)
return getColor(theme.props[val]);
} else if (val[0] === ':') { // func
const parts = val.split('<');
const funcTxt = parts.shift();
const argTxt = parts.shift();
if (funcTxt && argTxt) {
const func = funcTxt.substring(1);
const arg = parseFloat(argTxt);
const color = getColor(parts.join('<'));
switch (func) {
case 'darken': return color.darken(arg);
case 'lighten': return color.lighten(arg);
case 'alpha': return color.setAlpha(arg);
case 'hue': return color.spin(arg);
case 'saturate': return color.saturate(arg);
}
}
}
// other case
return tinycolor(val);
}
const props = {} as CompiledTheme;
for (const [k, v] of Object.entries(theme.props)) {
if (k.startsWith('$')) continue; // ignore const
props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
}
return props;
}
function genValue(c: tinycolor.Instance): string {
return c.toRgbString();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function validateTheme(theme: Record<string, any>): boolean {
if (theme.id == null || typeof theme.id !== 'string') return false;
if (theme.name == null || typeof theme.name !== 'string') return false;
if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false;
if (theme.props == null || typeof theme.props !== 'object') return false;
return true;
}
export function parseThemeCode(code: string): Theme {
let theme;
try {
theme = JSON5.parse(code);
} catch (_) {
throw new Error('Failed to parse theme json');
}
if (!validateTheme(theme)) {
throw new Error('This theme is invaild');
}
return theme;
}

View File

@@ -1,40 +1,30 @@
{
"name": "frontend-shared",
"type": "module",
"main": "./js-built/index.js",
"types": "./js-built/index.d.ts",
"exports": {
".": {
"import": "./js-built/index.js",
"types": "./js-built/index.d.ts"
},
"./*": {
"import": "./js-built/*",
"types": "./js-built/*"
}
},
"private": true,
"scripts": {
"build": "node ./build.js",
"watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"",
"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
"typecheck": "tsgo --noEmit",
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "24.12.2",
"@types/tinycolor2": "1.4.6",
"@typescript-eslint/eslint-plugin": "8.59.0",
"@typescript-eslint/parser": "8.59.0",
"esbuild": "0.28.0",
"eslint-plugin-vue": "10.9.0",
"nodemon": "3.1.14",
"vue-eslint-parser": "10.4.0"
},
"files": [
"js-built"
],
"dependencies": {
"@misskey-dev/emoji-data": "17.0.3",
"i18n": "workspace:*",
"json5": "2.2.3",
"misskey-js": "workspace:*",
"shiki": "4.0.2",
"tinycolor2": "1.6.0",
"vue": "3.5.33"
}
}

View File

@@ -51,7 +51,6 @@ await fs.readFile(
if (
micromatch(Array.from(modules), [
'../../assets/**',
'../../fluent-emojis/**',
'../../locales/ja-JP.yml',
'assets/**',
'public/**',

View File

@@ -7,7 +7,7 @@ import { type SharedOptions, http, HttpResponse } from 'msw';
export const onUnhandledRequest = ((req, print) => {
const url = new URL(req.url);
if (url.hostname !== 'localhost' || /^\/(?:client-assets\/|fluent-emojis?\/|iframe.html$|node_modules\/|src\/|sb-|static-assets\/|vite\/)/.test(url.pathname)) {
if (url.hostname !== 'localhost' || /^\/(?:client-assets\/|fluent-emoji\/|iframe.html$|node_modules\/|src\/|sb-|static-assets\/|vite\/)/.test(url.pathname)) {
return
}
print.warning()
@@ -16,16 +16,7 @@ export const onUnhandledRequest = ((req, print) => {
export const commonHandlers = [
http.get('/fluent-emoji/:codepoints.png', async ({ params }) => {
const { codepoints } = params;
const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob());
return new HttpResponse(value, {
headers: {
'Content-Type': 'image/png',
},
});
}),
http.get('/fluent-emojis/:codepoints.png', async ({ params }) => {
const { codepoints } = params;
const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob());
const value = await fetch(`https://unpkg.com/@misskey-dev/emoji-assets@17.0.3/built/fluent-emoji/${codepoints}.png`).then((response) => response.blob());
return new HttpResponse(value, {
headers: {
'Content-Type': 'image/png',
@@ -34,7 +25,7 @@ export const commonHandlers = [
}),
http.get('/twemoji/:codepoints.svg', async ({ params }) => {
const { codepoints } = params;
const value = await fetch(`https://unpkg.com/@discordapp/twemoji@16.0.1/dist/svg/${codepoints}.svg`).then((response) => response.blob());
const value = await fetch(`https://unpkg.com/@misskey-dev/emoji-assets@17.0.3/built/twemoji/${codepoints}.svg`).then((response) => response.blob());
return new HttpResponse(value, {
headers: {
'Content-Type': 'image/svg+xml',

View File

@@ -20,13 +20,13 @@ let moduleInitialized = false;
let unobserve = () => {};
let misskeyOS = null;
function loadTheme(applyTheme: typeof import('../src/theme')['applyTheme']) {
function loadTheme(themeMaganer: typeof import('../src/theme')['themeManager']) {
unobserve();
const theme = themes[window.document.documentElement.dataset.misskeyTheme];
if (theme) {
applyTheme(themes[window.document.documentElement.dataset.misskeyTheme]);
themeMaganer.updateTheme(themes[window.document.documentElement.dataset.misskeyTheme]);
} else {
applyTheme(themes['l-light']);
themeMaganer.updateTheme(themes['l-light']);
}
const observer = new MutationObserver((entries) => {
for (const entry of entries) {
@@ -34,7 +34,7 @@ function loadTheme(applyTheme: typeof import('../src/theme')['applyTheme']) {
const target = entry.target as HTMLElement;
const theme = themes[target.dataset.misskeyTheme];
if (theme) {
applyTheme(themes[target.dataset.misskeyTheme]);
themeMaganer.updateTheme(themes[target.dataset.misskeyTheme]);
} else {
target.removeAttribute('style');
}

View File

@@ -17,14 +17,13 @@
},
"dependencies": {
"@analytics/google-analytics": "1.1.0",
"@discordapp/twemoji": "16.0.1",
"@mcaptcha/core-glue": "0.1.0-alpha-5",
"@misskey-dev/browser-image-resizer": "2024.1.0",
"@misskey-dev/emoji-data": "17.0.3",
"@sentry/vue": "10.50.0",
"@simplewebauthn/browser": "13.3.0",
"@syuilo/aiscript": "1.2.1",
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.6",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.16",
"analytics": "0.8.19",
@@ -53,7 +52,7 @@
"json5": "2.2.3",
"matter-js": "0.20.0",
"mediabunny": "1.41.0",
"mfm-js": "0.25.0",
"mfm-js": "0.26.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
@@ -72,6 +71,7 @@
"wanakana": "5.3.1"
},
"devDependencies": {
"@misskey-dev/emoji-assets": "17.0.3",
"@misskey-dev/summaly": "5.3.0",
"@rollup/plugin-json": "6.1.0",
"@rollup/pluginutils": "5.3.0",

View File

@@ -13,7 +13,7 @@ import type { App } from 'vue';
import widgets from '@/widgets/index.js';
import directives from '@/directives/index.js';
import components from '@/components/index.js';
import { applyTheme } from '@/theme.js';
import { themeManager } from '@/theme.js';
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
import { i18n } from '@/i18n.js';
import { refreshCurrentAccount, login } from '@/accounts.js';
@@ -161,7 +161,7 @@ export async function common(createVue: () => Promise<App<Element>>) {
// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
// NOTE: この処理は必ずダークモード判定処理より後に来ること(初回のテーマ適用のため)
// NOTE: この処理は必ずサーバーテーマ適用処理より後に来ること(二重applyTheme発火を防ぐため)
// NOTE: この処理は必ずサーバーテーマ適用処理より後に来ること(二重発火を防ぐため)
// see: https://github.com/misskey-dev/misskey/issues/16562
watch(store.r.darkMode, (darkMode) => {
const theme = (() => {
@@ -172,7 +172,7 @@ export async function common(createVue: () => Promise<App<Element>>) {
}
})();
applyTheme(theme);
themeManager.updateTheme(theme);
}, { immediate: true });
window.document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
@@ -180,13 +180,13 @@ export async function common(createVue: () => Promise<App<Element>>) {
if (!isSafeMode) {
watch(prefer.r.darkTheme, (theme) => {
if (store.s.darkMode) {
applyTheme(theme ?? defaultDarkTheme);
themeManager.updateTheme(theme ?? defaultDarkTheme);
}
});
watch(prefer.r.lightTheme, (theme) => {
if (!store.s.darkMode) {
applyTheme(theme ?? defaultLightTheme);
themeManager.updateTheme(theme ?? defaultLightTheme);
}
});
}

View File

@@ -81,7 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
import tinycolor from 'tinycolor2';
import { globalEvents } from '@/events.js';
import { themeManager } from '@/theme.js';
import { defaultIdlingRenderScheduler } from '@/utility/idle-render.js';
// https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles
@@ -192,13 +192,13 @@ function tick() {
tick();
function calcColors() {
const computedStyle = getComputedStyle(window.document.documentElement);
const dark = tinycolor(computedStyle.getPropertyValue('--MI_THEME-bg')).isDark();
const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
const themeValue = themeManager.currentCompiledTheme!;
const dark = tinycolor(themeValue.bg).isDark();
const accent = tinycolor(themeValue.accent).toHexString();
majorGraduationColor.value = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
//minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
sHandColor.value = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
mHandColor.value = tinycolor(computedStyle.getPropertyValue('--MI_THEME-fg')).toHexString();
mHandColor.value = tinycolor(themeValue.fg).toHexString();
hHandColor.value = accent;
nowColor.value = accent;
}
@@ -207,13 +207,13 @@ calcColors();
onMounted(() => {
defaultIdlingRenderScheduler.add(tick);
globalEvents.on('themeChanged', calcColors);
themeManager.on('themeChanged', calcColors);
});
onBeforeUnmount(() => {
enabled = false;
defaultIdlingRenderScheduler.delete(tick);
globalEvents.off('themeChanged', calcColors);
themeManager.off('themeChanged', calcColors);
});
</script>

View File

@@ -34,7 +34,7 @@ import * as Misskey from 'misskey-js';
import Cropper from 'cropperjs';
import tinycolor from 'tinycolor2';
import MkModalWindow from '@/components/MkModalWindow.vue';
import * as os from '@/os.js';
import { themeManager } from '@/theme.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
@@ -105,10 +105,10 @@ onMounted(() => {
cropper = new Cropper(imgEl.value, {
});
const computedStyle = getComputedStyle(window.document.documentElement);
const themeValue = themeManager.currentCompiledTheme!;
const selection = cropper.getCropperSelection()!;
selection.themeColor = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
selection.themeColor = tinycolor(themeValue.accent).toHexString();
if (props.aspectRatio != null) selection.aspectRatio = props.aspectRatio;
selection.initialAspectRatio = props.aspectRatio ?? 1;
selection.outlined = true;

View File

@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
import { miLocalStorage } from '@/local-storage.js';
import { prefer } from '@/preferences.js';
import { globalEvents } from '@/events.js';
import { themeManager } from '@/theme.js';
import { getBgColor } from '@/utility/get-bg-color.js';
const miLocalStoragePrefix = 'ui:folder:' as const;
@@ -92,11 +92,11 @@ function updateBgColor() {
onMounted(() => {
updateBgColor();
globalEvents.on('themeChanging', updateBgColor);
themeManager.on('themeChanging', updateBgColor);
});
onBeforeUnmount(() => {
globalEvents.off('themeChanging', updateBgColor);
themeManager.off('themeChanging', updateBgColor);
});
</script>

View File

@@ -100,6 +100,7 @@ import { nextTick, onMounted, ref, useTemplateRef, watch } from 'vue';
import { prefer } from '@/preferences.js';
import { getBgColor } from '@/utility/get-bg-color.js';
import { pageFolderTeleportCount, popup } from '@/os.js';
import { themeManager } from '@/theme.js';
import MkFolderPage from '@/components/MkFolderPage.vue';
import { deviceKind } from '@/utility/device-kind.js';
@@ -192,9 +193,9 @@ async function toggle(ev: PointerEvent) {
}
onMounted(() => {
const computedStyle = getComputedStyle(window.document.documentElement);
const themeValue = themeManager.currentCompiledTheme!;
const parentBg = getBgColor(rootEl.value?.parentElement) ?? 'transparent';
const myBg = computedStyle.getPropertyValue('--MI_THEME-panel');
const myBg = themeValue.panel;
bgSame.value = parentBg === myBg;
});

View File

@@ -53,7 +53,7 @@ type ModelValueType<T extends SupportedTypes> =
<script lang="ts" setup generic="T extends SupportedTypes = 'text'">
import { onMounted, onUnmounted, nextTick, ref, useTemplateRef, watch, computed, toRefs } from 'vue';
import { debounce } from 'throttle-debounce';
import { throttle, debounce } from 'throttle-debounce';
import { useInterval } from '@@/js/use-interval.js';
import type { InputHTMLAttributes } from 'vue';
import type { SuggestionType } from '@/utility/autocomplete.js';
@@ -81,7 +81,8 @@ const props = defineProps<{
min?: number;
max?: number;
inline?: boolean;
debounce?: boolean;
debounce?: boolean | number;
throttle?: boolean | number;
manualSave?: boolean;
small?: boolean;
large?: boolean;
@@ -135,7 +136,8 @@ const updated = () => {
}
};
const debouncedUpdated = debounce(1000, updated);
const throttledUpdated = throttle(typeof props.throttle === 'number' ? props.throttle : 1000, updated);
const debouncedUpdated = debounce(typeof props.debounce === 'number' ? props.debounce : 1000, updated);
watch(modelValue, newValue => {
v.value = newValue;
@@ -143,7 +145,9 @@ watch(modelValue, newValue => {
watch(v, () => {
if (!props.manualSave) {
if (props.debounce) {
if (props.throttle === true || typeof props.throttle === 'number') {
throttledUpdated();
} else if (props.debounce === true || typeof props.debounce === 'number') {
debouncedUpdated();
} else {
updated();

View File

@@ -73,6 +73,7 @@ import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
import { initChart } from '@/utility/init-chart.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
import { themeManager } from '@/theme.js';
initChart();
@@ -186,7 +187,7 @@ function createDoughnut(chartEl: HTMLCanvasElement, tooltip: ReturnType<typeof u
labels: data.map(x => x.name),
datasets: [{
backgroundColor: data.map(x => x.color),
borderColor: getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'),
borderColor: themeManager.currentCompiledTheme!.panel,
borderWidth: 2,
hoverOffset: 0,
data: data.map(x => x.value),

View File

@@ -33,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { watch, ref } from 'vue';
import { genId } from '@/utility/id.js';
import { themeManager } from '@/theme.js';
import tinycolor from 'tinycolor2';
import { useInterval } from '@@/js/use-interval.js';
@@ -47,8 +48,7 @@ const polylinePoints = ref('');
const polygonPoints = ref('');
const headX = ref<number | null>(null);
const headY = ref<number | null>(null);
const clock = ref<number | null>(null);
const accent = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-accent'));
const accent = tinycolor(themeManager.currentCompiledTheme!.accent);
const color = accent.toRgbString();
function draw(): void {

View File

@@ -13,6 +13,7 @@ import { Chart } from 'chart.js';
import type { ScatterDataPoint } from 'chart.js';
import tinycolor from 'tinycolor2';
import { store } from '@/store.js';
import { themeManager } from '@/theme.js';
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
import { chartVLine } from '@/utility/chart-vline.js';
import { alpha } from '@/utility/color.js';
@@ -51,7 +52,7 @@ onMounted(async () => {
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const accent = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-accent'));
const accent = tinycolor(themeManager.currentCompiledTheme!.accent);
const color = accent.toHex();
if (chartEl.value == null) return;

View File

@@ -16,11 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, watch, onBeforeUnmount, ref, useTemplateRef } from 'vue';
import { themeManager } from '@/theme.js';
import tinycolor from 'tinycolor2';
const loaded = !!window.TagCanvas;
const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz';
const computedStyle = getComputedStyle(window.document.documentElement);
const idForCanvas = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
const idForTags = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
const available = ref(false);
@@ -33,7 +33,7 @@ watch(available, () => {
try {
window.TagCanvas.Start(idForCanvas, idForTags, {
textColour: '#ffffff',
outlineColour: tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(),
outlineColour: tinycolor(themeManager.currentCompiledTheme!.accent).toHexString(),
outlineRadius: 10,
initial: [-0.030, -0.010],
frontSelect: true,

View File

@@ -43,8 +43,8 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, watch } from 'vue';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import type { Theme } from '@/theme.js';
import { compile } from '@/theme.js';
import type { Theme } from '@@/js/theme.js';
import { compile } from '@@/js/theme.js';
import { deepClone } from '@/utility/clone.js';
const props = defineProps<{

View File

@@ -15,9 +15,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, useTemplateRef, ref, nextTick } from 'vue';
import { Chart } from 'chart.js';
import gradient from 'chartjs-plugin-gradient';
import tinycolor from 'tinycolor2';
import { misskeyApi } from '@/utility/misskey-api.js';
import { themeManager } from '@/theme.js';
import { store } from '@/store.js';
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
import { chartVLine } from '@/utility/chart-vline.js';
@@ -61,8 +61,7 @@ async function renderChart() {
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const computedStyle = getComputedStyle(window.document.documentElement);
const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
const accent = tinycolor(themeManager.currentCompiledTheme!.accent).toHexString();
const colorRead = accent;
const colorWrite = '#2ecc71';

View File

@@ -324,8 +324,9 @@ function onTopHandlePointerdown(evt: PointerEvent) {
if (main == null) return;
const base = getPositionY(evt);
const height = parseInt(getComputedStyle(main, '').height, 10);
const top = parseInt(getComputedStyle(main, '').top, 10);
const computedStyle = getComputedStyle(main, '');
const height = parseInt(computedStyle.height, 10);
const top = parseInt(computedStyle.top, 10);
// 動かした時
dragListen(me => {
@@ -353,8 +354,9 @@ function onRightHandlePointerdown(evt: PointerEvent) {
if (main == null) return;
const base = getPositionX(evt);
const width = parseInt(getComputedStyle(main, '').width, 10);
const left = parseInt(getComputedStyle(main, '').left, 10);
const computedStyle = getComputedStyle(main, '');
const width = parseInt(computedStyle.width, 10);
const left = parseInt(computedStyle.left, 10);
const browserWidth = window.innerWidth;
// 動かした時
@@ -380,8 +382,9 @@ function onBottomHandlePointerdown(evt: PointerEvent) {
if (main == null) return;
const base = getPositionY(evt);
const height = parseInt(getComputedStyle(main, '').height, 10);
const top = parseInt(getComputedStyle(main, '').top, 10);
const computedStyle = getComputedStyle(main, '');
const height = parseInt(computedStyle.height, 10);
const top = parseInt(computedStyle.top, 10);
const browserHeight = window.innerHeight;
// 動かした時
@@ -407,8 +410,9 @@ function onLeftHandlePointerdown(evt: PointerEvent) {
if (main == null) return;
const base = getPositionX(evt);
const width = parseInt(getComputedStyle(main, '').width, 10);
const left = parseInt(getComputedStyle(main, '').left, 10);
const computedStyle = getComputedStyle(main, '');
const width = parseInt(computedStyle.width, 10);
const left = parseInt(computedStyle.left, 10);
// 動かした時
dragListen(me => {

View File

@@ -5,7 +5,7 @@
import type { Directive } from 'vue';
import { getBgColor } from '@/utility/get-bg-color.js';
import { globalEvents } from '@/events.js';
import { themeManager } from '@/theme.js';
const handlerMap = new WeakMap<HTMLElement, () => void>();
@@ -27,10 +27,10 @@ export const adaptiveBorderDirective = {
calc();
globalEvents.on('themeChanged', calc);
themeManager.on('themeChanged', calc);
},
unmounted(src) {
globalEvents.off('themeChanged', handlerMap.get(src));
themeManager.off('themeChanged', handlerMap.get(src));
},
} as Directive<HTMLElement>;

View File

@@ -4,13 +4,14 @@
*/
import type { Directive } from 'vue';
import { themeManager } from '@/theme.js';
import { getBgColor } from '@/utility/get-bg-color.js';
export const panelDirective = {
mounted(src) {
const parentBg = getBgColor(src.parentElement) ?? 'transparent';
const myBg = getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel');
const myBg = themeManager.currentCompiledTheme!.panel;
if (parentBg === myBg) {
src.style.backgroundColor = 'var(--MI_THEME-bg)';

View File

@@ -8,8 +8,6 @@ import * as Misskey from 'misskey-js';
import { onBeforeUnmount } from 'vue';
type Events = {
themeChanging: () => void;
themeChanged: () => void;
clientNotification: (notification: Misskey.entities.Notification) => void;
notePosted: (note: Misskey.entities.Note) => void;
noteDeleted: (noteId: Misskey.entities.Note['id']) => void;

View File

@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="version">v{{ version }}</div>
<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }">
<MkCustomEmoji v-if="emoji.emoji[0] === ':'" class="emoji" :name="emoji.emoji" :normal="true" :noStyle="true" :fallbackToImage="true"/>
<MkEmoji v-else class="emoji" :emoji="emoji.emoji" :normal="true" :noStyle="true"/>
<MkEmoji v-else class="emoji unicode" :emoji="emoji.emoji" :normal="true" :noStyle="true"/>
</span>
</div>
<button v-if="thereIsTreasure" class="_button treasure" @click="getTreasure"><img src="/fluent-emoji/1f3c6.png" class="treasureImg"></button>
@@ -560,6 +560,10 @@ definePage(() => ({
pointer-events: none;
font-size: 24px;
width: 24px;
&.unicode {
height: 24px;
}
}
}
}

View File

@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, useTemplateRef } from 'vue';
import { Chart } from 'chart.js';
import { themeManager } from '@/theme.js';
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
import { initChart } from '@/utility/init-chart.js';
@@ -43,7 +44,7 @@ onMounted(() => {
labels: props.data.map(x => x.name),
datasets: [{
backgroundColor: props.data.map(x => x.color ?? '#000'),
borderColor: getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'),
borderColor: themeManager.currentCompiledTheme!.panel,
borderWidth: 2,
hoverOffset: 0,
data: props.data.map(x => x.value),

View File

@@ -53,7 +53,8 @@ import FormSection from '@/components/form/section.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { parsePluginMeta, installPlugin } from '@/plugin.js';
import { parseThemeCode, installTheme } from '@/theme.js';
import { installTheme } from '@/theme.js';
import { parseThemeCode } from '@@/js/theme.js';
import { unisonReload } from '@/utility/unison-reload.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';

View File

@@ -1040,9 +1040,9 @@ function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) {
function download() {
switch (lang) {
case 'en-US': return import('../../unicode-emoji-indexes/en-US.json').then(x => x.default);
case 'ja-JP': return import('../../unicode-emoji-indexes/ja-JP.json').then(x => x.default);
case 'ja-JP_hira': return import('../../unicode-emoji-indexes/ja-JP_hira.json').then(x => x.default);
case 'en-US': return import('@misskey-dev/emoji-data/indexes/en-US.json').then(x => x.default);
case 'ja-JP': return import('@misskey-dev/emoji-data/indexes/ja-JP.json').then(x => x.default);
case 'ja-JP_hira': return import('@misskey-dev/emoji-data/indexes/ja-JP_hira.json').then(x => x.default);
default: throw new Error('unrecognized lang: ' + lang);
}
}

View File

@@ -20,7 +20,8 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, computed } from 'vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkButton from '@/components/MkButton.vue';
import { parseThemeCode, previewTheme, installTheme } from '@/theme.js';
import { themeManager, installTheme, handleThemeInstallError } from '@/theme.js';
import { parseThemeCode } from '@@/js/theme.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
@@ -29,6 +30,19 @@ import { useRouter } from '@/router.js';
const router = useRouter();
const installThemeCode = ref<string | null>(null);
function previewTheme(code: string): void {
try {
const theme = parseThemeCode(code);
themeManager.previewTheme(theme);
} catch (err) {
os.alert({
type: 'error',
text: i18n.ts._theme.invalid,
});
console.error(err);
}
}
async function install(code: string): Promise<void> {
try {
const theme = parseThemeCode(code);
@@ -40,22 +54,7 @@ async function install(code: string): Promise<void> {
installThemeCode.value = null;
router.push('/settings/theme');
} catch (err: any) {
switch (err.message.toLowerCase()) {
case 'this theme is already installed':
os.alert({
type: 'info',
text: i18n.ts._theme.alreadyInstalled,
});
break;
default:
os.alert({
type: 'error',
text: i18n.ts._theme.invalid,
});
break;
}
console.error(err);
handleThemeInstallError(err);
}
}

View File

@@ -27,21 +27,27 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, ref } from 'vue';
import JSON5 from 'json5';
import type { Theme } from '@/theme.js';
import type { Theme } from '@@/js/theme.js';
import MkTextarea from '@/components/MkTextarea.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import { getBuiltinThemesRef, getThemesRef, removeTheme } from '@/theme.js';
import { removeTheme } from '@/theme.js';
import { getBuiltinThemes } from '@@/js/theme.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
import type { MkSelectItem } from '@/components/MkSelect.vue';
import { prefer } from '@/preferences';
const installedThemes = prefer.r.themes;
const builtinThemes = ref<Theme[]>([]);
getBuiltinThemes().then(themes => {
builtinThemes.value = themes;
});
const installedThemes = getThemesRef();
const builtinThemes = getBuiltinThemesRef();
const {
model: selectedThemeId,
def: selectedThemeIdDef,

View File

@@ -210,7 +210,7 @@ import JSON5 from 'json5';
import defaultLightTheme from '@@/themes/l-light.json5';
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
import { isSafeMode } from '@@/js/config.js';
import type { Theme } from '@/theme.js';
import type { Theme } from '@@/js/theme.js';
import * as os from '@/os.js';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSection from '@/components/form/section.vue';
@@ -218,7 +218,8 @@ import FormLink from '@/components/form/link.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkThemePreview from '@/components/MkThemePreview.vue';
import MkInfo from '@/components/MkInfo.vue';
import { getBuiltinThemesRef, getThemesRef, installTheme, parseThemeCode, removeTheme } from '@/theme.js';
import { handleThemeInstallError, installTheme, removeTheme } from '@/theme.js';
import { getBuiltinThemes } from '@@/js/theme.js';
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
import { store } from '@/store.js';
import { i18n } from '@/i18n.js';
@@ -229,8 +230,11 @@ import { prefer } from '@/preferences.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { checkDragDataType, getDragData, getPlainDragData, setDragData, setPlainDragData } from '@/drag-and-drop.js';
const installedThemes = getThemesRef();
const builtinThemes = getBuiltinThemesRef();
const installedThemes = prefer.r.themes;
const builtinThemes = ref<Theme[]>([]);
getBuiltinThemes().then(themes => {
builtinThemes.value = themes;
});
const instanceDarkTheme = computed<Theme | null>(() => instance.defaultDarkTheme ? JSON5.parse(instance.defaultDarkTheme) : null);
const installedDarkThemes = computed(() => installedThemes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
@@ -353,7 +357,7 @@ async function onDrop(ev: DragEvent) {
try {
await installTheme(code);
} catch (err) {
// nop
handleThemeInstallError(err);
}
}
}

View File

@@ -79,14 +79,15 @@ import JSON5 from 'json5';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import { host } from '@@/js/config.js';
import type { Theme } from '@/theme.js';
import type { Theme } from '@@/js/theme.js';
import { genId } from '@/utility/id.js';
import MkButton from '@/components/MkButton.vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkFolder from '@/components/MkFolder.vue';
import { ensureSignin } from '@/i.js';
import { addTheme, applyTheme } from '@/theme.js';
import { addTheme, themeManager } from '@/theme.js';
import { deepClone } from '@/utility/clone.js';
import * as os from '@/os.js';
import { store } from '@/store.js';
import { i18n } from '@/i18n.js';
@@ -130,7 +131,7 @@ const theme = ref<Theme>({
name: 'untitled',
author: `@${$i.username}@${toUnicode(host)}`,
base: 'light',
props: lightTheme.props,
props: deepClone(lightTheme.props),
});
const description = ref<string | null>(null);
const themeCode = ref<string>('');
@@ -170,7 +171,7 @@ function setFgColor(color: typeof fgColors[number]) {
function apply() {
themeCode.value = JSON5.stringify(theme.value, null, '\t');
applyTheme(theme.value, false);
themeManager.previewTheme(theme.value);
changed.value = true;
}
@@ -201,7 +202,7 @@ async function saveAs() {
theme.value.name = name;
if (description.value) theme.value.desc = description.value;
await addTheme(theme.value);
applyTheme(theme.value);
themeManager.updateTheme(theme.value);
if (store.s.darkMode) {
prefer.commit('darkTheme', theme.value);
} else {

View File

@@ -8,7 +8,7 @@ import { hemisphere } from '@@/js/intl-const.js';
import { DEFAULT_EMOJIS } from '@@/js/const.js';
import { prefersReducedMotion } from '@@/js/config.js';
import { definePreferences } from './manager.js';
import type { Theme } from '@/theme.js';
import type { Theme } from '@@/js/theme.js';
import type { SoundType } from '@/utility/sound.js';
import type { Plugin } from '@/plugin.js';
import type { DeviceKind } from '@/utility/device-kind.js';

View File

@@ -25,7 +25,7 @@
html {
overflow: auto;
overflow-wrap: break-word;
font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 14px;
line-height: 1.35;
text-size-adjust: 100%;

View File

@@ -6,73 +6,183 @@
// TODO: (可能な部分を)sharedに抽出して frontend-embed と共通化
import { ref, nextTick } from 'vue';
import tinycolor from 'tinycolor2';
import { EventEmitter } from 'eventemitter3';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import JSON5 from 'json5';
import { version } from '@@/js/config.js';
import type { Ref } from 'vue';
import type { BundledTheme } from 'shiki/themes';
import { getBuiltinThemes, parseThemeCode, themeProps, compile } from '@@/js/theme.js';
import type { Theme, CompiledTheme } from '@@/js/theme.js';
import { deepClone } from '@/utility/clone.js';
import { globalEvents } from '@/events.js';
import { miLocalStorage } from '@/local-storage.js';
import { $i } from '@/i.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { prefer } from '@/preferences.js';
import { deepEqual } from '@/utility/deep-equal.js';
export type Theme = {
id: string;
name: string;
author: string;
desc?: string;
base?: 'dark' | 'light';
kind?: 'dark' | 'light'; // legacy
props: Record<string, string>;
codeHighlighter?: {
base: BundledTheme;
overrides?: Record<string, any>;
} | {
base: '_none_';
overrides: Record<string, any>;
};
type ThemeManagerEvents = {
'themeChanging': () => void;
'themeChanged': () => void;
'previewStateChanged': (isPreview: boolean) => void;
'requestUpdateThemeCache': (theme: Theme, compiled: CompiledTheme) => void;
};
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
class ThemeManager extends EventEmitter<ThemeManagerEvents> {
/** 現在常用しているテーマ */
private _theme: Theme | null = null;
get theme() { return this._theme; }
private _compiledTheme: CompiledTheme | null = null;
get compiledTheme() { return this._compiledTheme; }
export const getBuiltinThemes = () => Promise.all(
[
'l-light',
'l-coffee',
'l-apricot',
'l-rainy',
'l-botanical',
'l-vivid',
'l-cherry',
'l-sushi',
'l-u0',
/** 現在適用中のテーマ */
private _currentTheme: Theme | null = null;
get currentTheme() { return this._currentTheme; }
get currentThemeId() { return this._currentTheme?.id; }
private _currentCompiledTheme: CompiledTheme | null = null;
get currentCompiledTheme() { return this._currentCompiledTheme; }
'd-dark',
'd-persimmon',
'd-astro',
'd-future',
'd-botanical',
'd-green-lime',
'd-green-orange',
'd-cherry',
'd-ice',
'd-u0',
].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
);
/** プレビュー中かどうか */
private _isPreviewMode = false;
get isPreviewMode() { return this._isPreviewMode; }
set isPreviewMode(value: boolean) {
if (this._isPreviewMode !== value) {
this._isPreviewMode = value;
this.emit('previewStateChanged', value);
}
}
export function getBuiltinThemesRef() {
const builtinThemes = ref<Theme[]>([]);
getBuiltinThemes().then(themes => builtinThemes.value = themes);
return builtinThemes;
constructor() {
super();
}
/** テーマを更新し、同時に適用します。 */
public updateTheme(newTheme: Theme) {
if (newTheme.id === this.theme?.id && version === miLocalStorage.getItem('themeCachedVersion')) return; // 変更なし
this.isPreviewMode = false;
// テーマを更新
this._theme = deepClone(newTheme);
const compiled = this.compile(newTheme);
this._compiledTheme = compiled;
// 適用中のテーマも更新
this._currentTheme = deepClone(this.theme);
this._currentCompiledTheme = deepClone(compiled);
this.applyTheme();
}
/** プレビュー用のテーマを適用します。 */
public previewTheme(theme: Theme) {
this.isPreviewMode = true;
// 適用中のテーマを更新
this._currentTheme = deepClone(theme);
this._currentCompiledTheme = this.compile(theme);
this.applyTheme();
}
/** プレビュー状態を解除し、適用中のテーマを常用しているテーマに戻します。 */
public clearPreview() {
this.isPreviewMode = false;
// 適用中のテーマを常用しているテーマに戻す
this._currentTheme = deepClone(this.theme);
this._currentCompiledTheme = deepClone(this.compiledTheme);
this.applyTheme();
}
/** 通常のテーマのコンパイルに加え、ベースとなるテーマの値を解決し代入します。 */
private compile(theme: Theme) {
const _theme = deepClone(theme);
if (_theme.base != null) {
const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
if (base) _theme.props = Object.assign({}, base.props, _theme.props);
}
return compile(_theme);
}
/** currentThemeを適用します。 */
private applyTheme() {
if (this.currentTheme == null || this.currentCompiledTheme == null) return;
// visibilityStateがhiddenな状態でstartViewTransitionするとブラウザによってはエラーになる
// 通常hiddenな時に呼ばれることはないが、iOSのPWAだとアプリ切り替え時に(何故か)hiddenな状態で(何故か)一瞬デバイスのダークモード判定が変わりapplyThemeが呼ばれる場合がある
if (window.document.startViewTransition != null && window.document.visibilityState === 'visible') {
window.document.documentElement.classList.add('_themeChanging_');
try {
window.document.startViewTransition(async () => {
this.updateAttributes();
await nextTick();
}).finished.then(() => {
window.document.documentElement.classList.remove('_themeChanging_');
this.emit('themeChanged');
});
} catch (err) {
// 様々な理由により startViewTransition は失敗することがある
// ref. https://github.com/misskey-dev/misskey/issues/16562
// FIXME: viewTransitonエラーはtry~catch貫通してそうな気配がする
console.error(err);
window.document.documentElement.classList.remove('_themeChanging_');
this.updateAttributes();
this.emit('themeChanged');
}
} else {
this.updateAttributes();
this.emit('themeChanged');
}
if (!this.isPreviewMode) {
this.emit('requestUpdateThemeCache', this.currentTheme, this.currentCompiledTheme);
}
}
private updateAttributes() {
if (!this.currentTheme || !this.currentCompiledTheme) return;
const colorScheme = this.currentTheme.base === 'dark' ? 'dark' : 'light';
window.document.documentElement.dataset.colorScheme = colorScheme;
for (const tag of window.document.head.children) {
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
tag.setAttribute('content', this.currentCompiledTheme['htmlThemeColor']);
break;
}
}
for (const key of themeProps) {
const value = this.currentCompiledTheme[key];
if (value) {
window.document.documentElement.style.setProperty(`--MI_THEME-${key}`, value.toString());
} else {
window.document.documentElement.style.removeProperty(`--MI_THEME-${key}`);
}
}
window.document.documentElement.style.setProperty('color-scheme', colorScheme);
this.emit('themeChanging');
}
}
export function getThemesRef(): Ref<Theme[]> {
return prefer.r.themes;
}
export const themeManager = new ThemeManager();
export const isPreviewMode = ref(false);
themeManager.on('requestUpdateThemeCache', (theme, props) => {
miLocalStorage.setItem('theme', JSON.stringify(props));
miLocalStorage.setItem('themeId', theme.id);
miLocalStorage.setItem('themeCachedVersion', version);
});
themeManager.on('previewStateChanged', (preview) => {
isPreviewMode.value = preview;
});
export async function addTheme(theme: Theme): Promise<void> {
if ($i == null) return;
@@ -93,163 +203,6 @@ export async function removeTheme(theme: Theme): Promise<void> {
prefer.commit('themes', themes);
}
function applyThemeInternal(theme: Theme, persist: boolean) {
const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
window.document.documentElement.dataset.colorScheme = colorScheme;
// Deep copy
const _theme = deepClone(theme);
if (_theme.base) {
const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
if (base) _theme.props = Object.assign({}, base.props, _theme.props);
}
const props = compile(_theme);
for (const tag of window.document.head.children) {
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
tag.setAttribute('content', props['htmlThemeColor']);
break;
}
}
for (const [k, v] of Object.entries(props)) {
window.document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
}
window.document.documentElement.style.setProperty('color-scheme', colorScheme);
if (persist) {
miLocalStorage.setItem('theme', JSON.stringify(props));
miLocalStorage.setItem('themeId', theme.id);
miLocalStorage.setItem('themeCachedVersion', version);
miLocalStorage.setItem('colorScheme', colorScheme);
}
// 色計算など再度行えるようにクライアント全体に通知
globalEvents.emit('themeChanging');
}
let timeout: number | null = null;
let currentThemeId = miLocalStorage.getItem('themeId');
export function applyTheme(theme: Theme, persist = true) {
if (timeout) {
window.clearTimeout(timeout);
timeout = null;
}
if (theme.id === currentThemeId && miLocalStorage.getItem('themeCachedVersion') === version) return;
currentThemeId = theme.id;
// visibilityStateがhiddenな状態でstartViewTransitionするとブラウザによってはエラーになる
// 通常hiddenな時に呼ばれることはないが、iOSのPWAだとアプリ切り替え時に(何故か)hiddenな状態で(何故か)一瞬デバイスのダークモード判定が変わりapplyThemeが呼ばれる場合がある
if (window.document.startViewTransition != null && window.document.visibilityState === 'visible') {
window.document.documentElement.classList.add('_themeChanging_');
try {
window.document.startViewTransition(async () => {
applyThemeInternal(theme, persist);
await nextTick();
}).finished.then(() => {
window.document.documentElement.classList.remove('_themeChanging_');
globalEvents.emit('themeChanged');
});
} catch (err) {
// 様々な理由により startViewTransition は失敗することがある
// ref. https://github.com/misskey-dev/misskey/issues/16562
// FIXME: viewTransitonエラーはtry~catch貫通してそうな気配がする
console.error(err);
window.document.documentElement.classList.remove('_themeChanging_');
applyThemeInternal(theme, persist);
globalEvents.emit('themeChanged');
}
} else {
applyThemeInternal(theme, persist);
globalEvents.emit('themeChanged');
}
}
export function compile(theme: Theme): Record<string, string> {
function getColor(val: string): tinycolor.Instance {
if (val[0] === '@') { // ref (prop)
return getColor(theme.props[val.substring(1)]);
} else if (val[0] === '$') { // ref (const)
return getColor(theme.props[val]);
} else if (val[0] === ':') { // func
const parts = val.split('<');
const funcTxt = parts.shift();
const argTxt = parts.shift();
if (funcTxt && argTxt) {
const func = funcTxt.substring(1);
const arg = parseFloat(argTxt);
const color = getColor(parts.join('<'));
switch (func) {
case 'darken': return color.darken(arg);
case 'lighten': return color.lighten(arg);
case 'alpha': return color.setAlpha(arg);
case 'hue': return color.spin(arg);
case 'saturate': return color.saturate(arg);
}
}
}
// other case
return tinycolor(val);
}
const props = {} as Record<string, string>;
for (const [k, v] of Object.entries(theme.props)) {
if (k.startsWith('$')) continue; // ignore const
props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
}
return props;
}
function genValue(c: tinycolor.Instance): string {
return c.toRgbString();
}
export function validateTheme(theme: Record<string, any>): boolean {
if (theme.id == null || typeof theme.id !== 'string') return false;
if (theme.name == null || typeof theme.name !== 'string') return false;
if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false;
if (theme.props == null || typeof theme.props !== 'object') return false;
return true;
}
export function parseThemeCode(code: string): Theme {
let theme;
try {
theme = JSON5.parse(code);
} catch (_) {
throw new Error('Failed to parse theme json');
}
if (!validateTheme(theme)) {
throw new Error('This theme is invaild');
}
if (prefer.s.themes.some(t => t.id === theme.id)) {
throw new Error('This theme is already installed');
}
return theme;
}
export function previewTheme(code: string): void {
const theme = parseThemeCode(code);
if (theme != null) applyTheme(theme, false);
}
export async function installTheme(code: string): Promise<void> {
const theme = parseThemeCode(code);
if (theme == null) return;
@@ -261,3 +214,26 @@ export function clearAppliedThemeCache() {
miLocalStorage.removeItem('themeId');
miLocalStorage.removeItem('themeCachedVersion');
}
export function handleThemeInstallError(err: unknown) {
if (err instanceof Error) {
let message = '';
switch (err.message.toLowerCase()) {
case 'this theme is already installed':
case 'already exists':
case 'builtin theme':
message = i18n.ts._theme.alreadyInstalled;
break;
default:
message = i18n.ts._theme.invalid;
break;
}
os.alert({
type: 'error',
text: message,
});
}
console.error(err);
}

View File

@@ -0,0 +1,57 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<span :class="$style.icon">
<i class="ti ti-info-circle"></i>
</span>
<span :class="$style.title">{{ i18n.ts.previewingTheme }}</span>
<span :class="$style.body"><button class="_textButton" style="color: var(--MI_THEME-fgOnAccent);" @click="restore">{{ i18n.ts.previewingThemeRestore }}</button> | <MkA class="_textButton" style="color: var(--MI_THEME-fgOnAccent);" to="/settings/theme">{{ i18n.ts.settings }}</MkA></span>
</div>
</template>
<script lang="ts" setup>
import { i18n } from '@/i18n.js';
import { themeManager } from '@/theme.js';
function restore() {
themeManager.clearPreview();
}
</script>
<style lang="scss" module>
.root {
--height: 24px;
font-size: 0.85em;
display: flex;
vertical-align: bottom;
width: 100%;
line-height: var(--height);
height: var(--height);
overflow: clip;
contain: strict;
background: var(--MI_THEME-accent);
color: var(--MI_THEME-fgOnAccent);
}
.icon {
margin-left: 10px;
animation: blink 2s infinite;
}
.title {
padding: 0 10px;
font-weight: bold;
}
.body {
min-width: 0;
flex: 1;
overflow: clip;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>

View File

@@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XReloadSuggestion v-if="shouldSuggestReload"/>
<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
<XThemePreviewing v-if="isThemePreviewMode"/>
<XAnnouncements v-if="$i"/>
<XStatusBars/>
<div :class="$style.columnsWrapper">
@@ -94,12 +95,14 @@ import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
import XTitlebar from '@/ui/_common_/titlebar.vue';
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
import XReloadSuggestion from '@/ui/_common_/ReloadSuggestion.vue';
import XThemePreviewing from '@/ui/_common_/ThemePreviewing.vue';
import * as os from '@/os.js';
import { $i } from '@/i.js';
import { i18n } from '@/i18n.js';
import { deviceKind } from '@/utility/device-kind.js';
import { prefer } from '@/preferences.js';
import { store } from '@/store.js';
import { isPreviewMode as isThemePreviewMode } from '@/theme.js';
import XMainColumn from '@/ui/deck/main-column.vue';
import XTlColumn from '@/ui/deck/tl-column.vue';
import XAntennaColumn from '@/ui/deck/antenna-column.vue';

View File

@@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<XReloadSuggestion v-if="shouldSuggestReload"/>
<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
<XThemePreviewing v-if="isThemePreviewMode"/>
<XAnnouncements v-if="$i"/>
<XStatusBars :class="$style.statusbars"/>
</div>
@@ -40,8 +41,10 @@ import type { PageMetadata } from '@/page.js';
import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
import XReloadSuggestion from '@/ui/_common_/ReloadSuggestion.vue';
import XThemePreviewing from '@/ui/_common_/ThemePreviewing.vue';
import XTitlebar from '@/ui/_common_/titlebar.vue';
import XSidebar from '@/ui/_common_/navbar.vue';
import { isPreviewMode as isThemePreviewMode } from '@/theme.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/i.js';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -44,7 +44,7 @@ export async function getTheme(mode: 'light' | 'dark', getName = false): Promise
_res.type = mode;
if (getName) {
return _res.name;
return _res.name!;
}
return _res;
}

View File

@@ -24,6 +24,8 @@ function rename(file: Misskey.entities.DriveFile) {
misskeyApi('drive/files/update', {
fileId: file.id,
name: name,
}).then(updated => {
globalEvents.emit('driveFilesUpdated', [updated]);
});
});
}
@@ -37,6 +39,8 @@ async function describe(file: Misskey.entities.DriveFile) {
misskeyApi('drive/files/update', {
fileId: file.id,
comment: caption.length === 0 ? null : caption,
}).then(updated => {
globalEvents.emit('driveFilesUpdated', [updated]);
});
},
closed: () => dispose(),
@@ -49,6 +53,8 @@ function move(file: Misskey.entities.DriveFile) {
misskeyApi('drive/files/update', {
fileId: file.id,
folderId: folders[0] ? folders[0].id : null,
}).then(updated => {
globalEvents.emit('driveFilesUpdated', [updated]);
});
});
}
@@ -57,6 +63,8 @@ function toggleSensitive(file: Misskey.entities.DriveFile) {
misskeyApi('drive/files/update', {
fileId: file.id,
isSensitive: !file.isSensitive,
}).then(updated => {
globalEvents.emit('driveFilesUpdated', [updated]);
}).catch(err => {
os.alert({
type: 'error',

View File

@@ -24,6 +24,7 @@ import {
import gradient from 'chartjs-plugin-gradient';
import zoomPlugin from 'chartjs-plugin-zoom';
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
import { themeManager } from '@/theme.js';
import { store } from '@/store.js';
import 'chartjs-adapter-date-fns';
@@ -50,7 +51,7 @@ export function initChart() {
);
// フォントカラー
Chart.defaults.color = getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-fg');
Chart.defaults.color = themeManager.currentCompiledTheme!.fg;
Chart.defaults.borderColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';

View File

@@ -11,8 +11,9 @@ export function popout(path: string, w?: HTMLElement) {
url = appendQuery(url, 'zen');
if (w) {
const position = w.getBoundingClientRect();
const width = parseInt(getComputedStyle(w, '').width, 10);
const height = parseInt(getComputedStyle(w, '').height, 10);
const computedStyle = getComputedStyle(w, '');
const width = parseInt(computedStyle.width, 10);
const height = parseInt(computedStyle.height, 10);
const x = window.screenX + position.left;
const y = window.screenY + position.top;
window.open(url, url,

View File

@@ -5,8 +5,8 @@
import { genId } from '@/utility/id.js';
import type { Theme } from '@/theme.js';
import { themeProps } from '@/theme.js';
import type { Theme } from '@@/js/theme.js';
import { themeProps } from '@@/js/theme.js';
export type Default = null;
export type Color = string;

View File

@@ -0,0 +1,188 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { afterEach, assert, beforeEach, describe, test, vi } from 'vitest';
import type { Theme } from '@@/js/theme.js';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import './init';
vi.mock('@/i18n.js', () => ({
i18n: {
ts: {
_theme: {
alreadyInstalled: 'already installed',
invalid: 'invalid',
},
},
},
updateI18n: vi.fn(),
}));
vi.mock('@/os.js', () => ({
alert: vi.fn(),
}));
const cloneTheme = <T>(value: T): T => structuredClone(value);
const createTheme = (base: 'light' | 'dark', options: {
id: string;
name: string;
accent: string;
bg: string;
fg: string;
}): Theme => {
const builtin = base === 'dark' ? darkTheme : lightTheme;
return {
id: options.id,
name: options.name,
author: 'tester',
base,
props: {
...cloneTheme(builtin.props),
accent: options.accent,
bg: options.bg,
fg: options.fg,
},
};
};
const primaryTheme = createTheme('light', {
id: 'primary-theme',
name: 'Primary Theme',
accent: '#224488',
bg: '#faf7f2',
fg: '#1a1a1a',
});
const previewTheme = createTheme('dark', {
id: 'preview-theme',
name: 'Preview Theme',
accent: '#55aa33',
bg: '#101820',
fg: '#f4f4f4',
});
const replacementTheme = createTheme('dark', {
id: 'replacement-theme',
name: 'Replacement Theme',
accent: '#bb5500',
bg: '#18110f',
fg: '#f6e7df',
});
const loadThemeModule = async () => {
vi.resetModules();
return await import('@/theme.js');
};
const resetDocument = () => {
window.localStorage.clear();
document.head.innerHTML = '<meta name="theme-color" content="#000000">';
document.documentElement.className = '';
document.documentElement.removeAttribute('data-color-scheme');
document.documentElement.style.cssText = '';
Reflect.deleteProperty(document, 'startViewTransition');
Object.defineProperty(document, 'visibilityState', {
configurable: true,
value: 'visible',
});
};
describe('ThemeManager', () => {
beforeEach(() => {
resetDocument();
});
afterEach(() => {
window.localStorage.clear();
});
test('通常テーマ適用後のプレビューは現在テーマのみを切り替え、キャッシュは保持する', async () => {
const { themeManager, isPreviewMode } = await loadThemeModule();
themeManager.updateTheme(primaryTheme);
const cachedTheme = window.localStorage.getItem('theme');
const cachedThemeId = window.localStorage.getItem('themeId');
themeManager.previewTheme(previewTheme);
assert.strictEqual(themeManager.theme?.id, primaryTheme.id);
assert.strictEqual(themeManager.currentTheme?.id, previewTheme.id);
assert.strictEqual(themeManager.currentThemeId, previewTheme.id);
assert.strictEqual(themeManager.isPreviewMode, true);
assert.strictEqual(isPreviewMode.value, true);
assert.strictEqual(document.documentElement.dataset.colorScheme, 'dark');
assert.strictEqual(document.documentElement.style.getPropertyValue('--MI_THEME-accent'), themeManager.currentCompiledTheme?.accent);
assert.strictEqual(window.localStorage.getItem('theme'), cachedTheme);
assert.strictEqual(window.localStorage.getItem('themeId'), cachedThemeId);
});
test('プレビュー解除で元のテーマと DOM 状態が復元される', async () => {
const { themeManager, isPreviewMode } = await loadThemeModule();
themeManager.updateTheme(primaryTheme);
const originalCompiledThemeColor = themeManager.currentCompiledTheme?.htmlThemeColor;
themeManager.previewTheme(previewTheme);
const previewCompiledThemeColor = themeManager.currentCompiledTheme?.htmlThemeColor;
assert.strictEqual(themeManager.currentTheme?.id, previewTheme.id);
assert.notStrictEqual(previewCompiledThemeColor, originalCompiledThemeColor);
themeManager.clearPreview();
assert.strictEqual(themeManager.theme?.id, primaryTheme.id);
assert.strictEqual(themeManager.currentTheme?.id, primaryTheme.id);
assert.strictEqual(themeManager.currentCompiledTheme?.htmlThemeColor, originalCompiledThemeColor);
assert.strictEqual(themeManager.isPreviewMode, false);
assert.strictEqual(isPreviewMode.value, false);
assert.strictEqual(document.documentElement.dataset.colorScheme, 'light');
assert.strictEqual(document.documentElement.style.getPropertyValue('--MI_THEME-accent'), themeManager.currentCompiledTheme?.accent);
assert.strictEqual(document.head.querySelector('meta[name="theme-color"]')?.getAttribute('content'), originalCompiledThemeColor);
assert.strictEqual(window.localStorage.getItem('themeId'), primaryTheme.id);
});
test('プレビュー中に通常テーマを更新するとプレビューを抜けて新しい通常テーマが適用される', async () => {
const { themeManager, isPreviewMode } = await loadThemeModule();
themeManager.updateTheme(primaryTheme);
themeManager.previewTheme(previewTheme);
themeManager.updateTheme(replacementTheme);
assert.strictEqual(themeManager.theme?.id, replacementTheme.id);
assert.strictEqual(themeManager.currentTheme?.id, replacementTheme.id);
assert.strictEqual(themeManager.isPreviewMode, false);
assert.strictEqual(isPreviewMode.value, false);
assert.strictEqual(document.documentElement.dataset.colorScheme, 'dark');
assert.strictEqual(document.documentElement.style.getPropertyValue('--MI_THEME-accent'), themeManager.currentCompiledTheme?.accent);
assert.strictEqual(window.localStorage.getItem('themeId'), replacementTheme.id);
});
test('themeChanging と themeChanged はプレビュー適用と復帰のたびに発火する', async () => {
const { themeManager } = await loadThemeModule();
const events: string[] = [];
themeManager.on('themeChanging', () => {
events.push('themeChanging');
});
themeManager.on('themeChanged', () => {
events.push('themeChanged');
});
themeManager.updateTheme(primaryTheme);
themeManager.previewTheme(previewTheme);
themeManager.clearPreview();
assert.deepStrictEqual(events, [
'themeChanging',
'themeChanged',
'themeChanging',
'themeChanged',
'themeChanging',
'themeChanged',
]);
});
});

View File

@@ -23,7 +23,8 @@
"resolveJsonModule": true,
"isolatedModules": true,
"paths": {
"@/*": ["../src/*"]
"@/*": ["../src/*"],
"@@/*": ["../../frontend-shared/*"]
},
"typeRoots": [
"../node_modules/@types"
@@ -37,6 +38,7 @@
"compileOnSave": false,
"include": [
"./**/*.ts",
"../src/**/*.vue"
"../src/**/*.vue",
"../@types/**/*.d.ts"
]
}

View File

@@ -137,8 +137,7 @@ export function getConfig(): UserConfig {
'@@/': __dirname + '/../frontend-shared/',
'/client-assets/': __dirname + '/assets/',
'/static-assets/': __dirname + '/../backend/assets/',
'/fluent-emojis/': __dirname + '/../../fluent-emojis/dist/',
'/fluent-emoji/': __dirname + '/../../fluent-emojis/dist/',
'/fluent-emoji/': '@misskey-dev/emoji-assets/fluent-emoji/',
},
},

View File

@@ -5651,6 +5651,14 @@ export interface Locale extends ILocale {
* リノート先のチャンネルを見る
*/
"viewRenotedChannel": string;
/**
* テーマのプレビュー中
*/
"previewingTheme": string;
/**
* 元に戻す
*/
"previewingThemeRestore": string;
"_imageEditing": {
"_vars": {
/**

136
pnpm-lock.yaml generated
View File

@@ -96,9 +96,6 @@ importers:
'@aws-sdk/lib-storage':
specifier: 3.1037.0
version: 3.1037.0(@aws-sdk/client-s3@3.1037.0)
'@discordapp/twemoji':
specifier: 16.0.1
version: 16.0.1
'@fastify/accepts':
specifier: 5.0.4
version: 5.0.4
@@ -120,6 +117,12 @@ importers:
'@kitajs/html':
specifier: 4.2.13
version: 4.2.13
'@misskey-dev/emoji-assets':
specifier: 17.0.3
version: 17.0.3
'@misskey-dev/emoji-data':
specifier: 17.0.3
version: 17.0.3
'@misskey-dev/sharp-read-bmp':
specifier: 1.2.0
version: 1.2.0
@@ -159,9 +162,6 @@ importers:
'@smithy/node-http-handler':
specifier: 4.6.1
version: 4.6.1
'@twemoji/parser':
specifier: 16.0.0
version: 16.0.0
accepts:
specifier: 1.3.8
version: 1.3.8
@@ -265,8 +265,8 @@ importers:
specifier: 0.57.0
version: 0.57.0
mfm-js:
specifier: 0.25.0
version: 0.25.0
specifier: 0.26.0
version: 0.26.0
mime-types:
specifier: 3.0.2
version: 3.0.2
@@ -608,15 +608,15 @@ importers:
'@analytics/google-analytics':
specifier: 1.1.0
version: 1.1.0
'@discordapp/twemoji':
specifier: 16.0.1
version: 16.0.1
'@mcaptcha/core-glue':
specifier: 0.1.0-alpha-5
version: 0.1.0-alpha-5
'@misskey-dev/browser-image-resizer':
specifier: 2024.1.0
version: 2024.1.0
'@misskey-dev/emoji-data':
specifier: 17.0.3
version: 17.0.3
'@sentry/vue':
specifier: 10.50.0
version: 10.50.0(vue@3.5.33(typescript@5.9.3))
@@ -629,9 +629,6 @@ importers:
'@syuilo/aiscript-0-19-0':
specifier: npm:@syuilo/aiscript@^0.19.0
version: '@syuilo/aiscript@0.19.0'
'@twemoji/parser':
specifier: 16.0.0
version: 16.0.0
'@vitejs/plugin-vue':
specifier: 6.0.6
version: 6.0.6(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0))(vue@3.5.33(typescript@5.9.3))
@@ -717,8 +714,8 @@ importers:
specifier: 1.41.0
version: 1.41.0
mfm-js:
specifier: 0.25.0
version: 0.25.0
specifier: 0.26.0
version: 0.26.0
misskey-bubble-game:
specifier: workspace:*
version: link:../misskey-bubble-game
@@ -768,6 +765,9 @@ importers:
specifier: 5.3.1
version: 5.3.1
devDependencies:
'@misskey-dev/emoji-assets':
specifier: 17.0.3
version: 17.0.3
'@misskey-dev/summaly':
specifier: 5.3.0
version: 5.3.0
@@ -1015,18 +1015,12 @@ importers:
packages/frontend-embed:
dependencies:
'@discordapp/twemoji':
specifier: 16.0.1
version: 16.0.1
'@rollup/plugin-json':
specifier: 6.1.0
version: 6.1.0(rollup@4.60.2)
'@rollup/pluginutils':
specifier: 5.3.0
version: 5.3.0(rollup@4.60.2)
'@twemoji/parser':
specifier: 16.0.0
version: 16.0.0
'@vitejs/plugin-vue':
specifier: 6.0.6
version: 6.0.6(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0))(vue@3.5.33(typescript@5.9.3))
@@ -1049,8 +1043,8 @@ importers:
specifier: 2.2.3
version: 2.2.3
mfm-js:
specifier: 0.25.0
version: 0.25.0
specifier: 0.26.0
version: 0.26.0
misskey-js:
specifier: workspace:*
version: link:../misskey-js
@@ -1073,6 +1067,9 @@ importers:
specifier: 3.5.33
version: 3.5.33(typescript@5.9.3)
devDependencies:
'@misskey-dev/emoji-assets':
specifier: 17.0.3
version: 17.0.3
'@misskey-dev/summaly':
specifier: 5.3.0
version: 5.3.0
@@ -1166,12 +1163,24 @@ importers:
packages/frontend-shared:
dependencies:
'@misskey-dev/emoji-data':
specifier: 17.0.3
version: 17.0.3
i18n:
specifier: workspace:*
version: link:../i18n
json5:
specifier: 2.2.3
version: 2.2.3
misskey-js:
specifier: workspace:*
version: link:../misskey-js
shiki:
specifier: 4.0.2
version: 4.0.2
tinycolor2:
specifier: 1.6.0
version: 1.6.0
vue:
specifier: 3.5.33
version: 3.5.33(typescript@5.9.3)
@@ -1179,21 +1188,18 @@ importers:
'@types/node':
specifier: 24.12.2
version: 24.12.2
'@types/tinycolor2':
specifier: 1.4.6
version: 1.4.6
'@typescript-eslint/eslint-plugin':
specifier: 8.59.0
version: 8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
'@typescript-eslint/parser':
specifier: 8.59.0
version: 8.59.0(eslint@9.39.4)(typescript@5.9.3)
esbuild:
specifier: 0.28.0
version: 0.28.0
eslint-plugin-vue:
specifier: 10.9.0
version: 10.9.0(@stylistic/eslint-plugin@5.5.0(eslint@9.39.4))(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(vue-eslint-parser@10.4.0(eslint@9.39.4))
nodemon:
specifier: 3.1.14
version: 3.1.14
vue-eslint-parser:
specifier: 10.4.0
version: 10.4.0(eslint@9.39.4)
@@ -1803,9 +1809,6 @@ packages:
resolution: {integrity: sha512-OGju/GYp0V72qlZ/Pd4jGEwqBwT/Za/tw+Z3AC7lgMheGqsbhTZrtc5iLz9z59G/Q53QyE2fnjHV8N9wjBpiWA==}
engines: {node: '>=18.0'}
'@discordapp/twemoji@16.0.1':
resolution: {integrity: sha512-figLiBWzjS5cyrAjLaGjM8AAaowO3qvK8rg5bA2dElB4qsaPMvBVlFDMO2d3x+nC1igt7kgWH4dvNmvvUHUF8w==}
'@emnapi/core@1.10.0':
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
@@ -2560,6 +2563,15 @@ packages:
'@misskey-dev/browser-image-resizer@2024.1.0':
resolution: {integrity: sha512-4EnO0zLW5NDtng3Gaz5MuT761uiuoOuplwX18wBqgj8w56LTU5BjLn/vbHwDIIe0j2gwqDYhMb7bDjmr1/Fomg==}
'@misskey-dev/emoji-assets@17.0.3':
resolution: {integrity: sha512-Uiy+R4rghBML5k2nZFLr2w4RUwqDrDnTWZ6lXGYtnEr0DRcW41pQyS2RUUm/pFjYr/DjNntcoysdhqinpa9P/Q==}
'@misskey-dev/emoji-data@17.0.0':
resolution: {integrity: sha512-grMnsmTm7VSah5heS1fIfOfoQmIpzo6ZU5AV5yRxCy7ivtWhFU3fpMfXZyKsH9G9/HZiLRsClKtyjHKMlxjt7g==}
'@misskey-dev/emoji-data@17.0.3':
resolution: {integrity: sha512-RmW0C2A1Zxn7DTYVCOhMDyCyyXYJLBc5GIEuhcufaqF0Gxw9+gIwaitCI3OUWnR928XnDwvvaizhJSjvIEF+2Q==}
'@misskey-dev/eslint-plugin@2.1.0':
resolution: {integrity: sha512-f++Vv1r3BQyGqEE0SB5algUZwRoTMZIYfVtpcuQ2fLuYUm0cQ5BBTs/gwAHPajVB2YD8F33gzPIReTKtuJyCwQ==}
peerDependencies:
@@ -4267,9 +4279,6 @@ packages:
resolution: {integrity: sha512-JSSdNiS0wgd8GHhBwnMAI18Y8XPhLVN+dNelPfZCXFhy9Lb3NbnFyp9JKxxr54jSUkEJPk3cidvCoHducSaRMQ==}
engines: {node: '>=14.17'}
'@twemoji/parser@16.0.0':
resolution: {integrity: sha512-jmuIjkp3OIaEemwMy3sArBwZSuZkRqmueGwRe2Zk4cFzbUJISFBJSZLDUUBNIgq3c+nY49ideYN2OiII6JUqwA==}
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@@ -6490,10 +6499,6 @@ packages:
resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==}
engines: {node: '>=14.14'}
fs-extra@8.1.0:
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
engines: {node: '>=6 <7 || >=8'}
fs-extra@9.1.0:
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
engines: {node: '>=10'}
@@ -7252,12 +7257,6 @@ packages:
engines: {node: '>=6'}
hasBin: true
jsonfile@4.0.0:
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
jsonfile@5.0.0:
resolution: {integrity: sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==}
jsonfile@6.2.0:
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
@@ -7625,8 +7624,8 @@ packages:
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
engines: {node: '>= 0.6'}
mfm-js@0.25.0:
resolution: {integrity: sha512-JoK5TOtswXIvZSZ9hUEL+ZkcNV4onu/DtkaKeXK846+sJBBF8DvxYmPutt7nPaRDsUMmJGr64PNVMFpMGdk3hw==}
mfm-js@0.26.0:
resolution: {integrity: sha512-JEbaa1GBJBcpsk9/6tGaOgo1WewUhL9cIFl3VhIZ2a/89X83hzwxwVZOEebFaWdOufwFxnti44QMR/TWcAxSzg==}
micromark-core-commonmark@2.0.3:
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
@@ -10077,10 +10076,6 @@ packages:
unist-util-visit@5.0.0:
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
universalify@0.1.2:
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
engines: {node: '>= 4.0.0'}
universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
@@ -11390,13 +11385,6 @@ snapshots:
ky: 1.14.0
undici: 6.22.0
'@discordapp/twemoji@16.0.1':
dependencies:
'@twemoji/parser': 16.0.0
fs-extra: 8.1.0
jsonfile: 5.0.0
universalify: 0.1.2
'@emnapi/core@1.10.0':
dependencies:
'@emnapi/wasi-threads': 1.2.1
@@ -12031,6 +12019,12 @@ snapshots:
'@misskey-dev/browser-image-resizer@2024.1.0': {}
'@misskey-dev/emoji-assets@17.0.3': {}
'@misskey-dev/emoji-data@17.0.0': {}
'@misskey-dev/emoji-data@17.0.3': {}
'@misskey-dev/eslint-plugin@2.1.0(@eslint/compat@1.4.0(eslint@9.39.4))(@stylistic/eslint-plugin@5.5.0(eslint@9.39.4))(@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3))(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4))(eslint@9.39.4)(globals@17.5.0)':
dependencies:
'@eslint/compat': 1.4.0(eslint@9.39.4)
@@ -13938,8 +13932,6 @@ snapshots:
'@tsd/typescript@5.9.3': {}
'@twemoji/parser@16.0.0': {}
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
@@ -16598,12 +16590,6 @@ snapshots:
jsonfile: 6.2.0
universalify: 2.0.1
fs-extra@8.1.0:
dependencies:
graceful-fs: 4.2.11
jsonfile: 4.0.0
universalify: 0.1.2
fs-extra@9.1.0:
dependencies:
at-least-node: 1.0.0
@@ -17399,16 +17385,6 @@ snapshots:
json5@2.2.3: {}
jsonfile@4.0.0:
optionalDependencies:
graceful-fs: 4.2.11
jsonfile@5.0.0:
dependencies:
universalify: 0.1.2
optionalDependencies:
graceful-fs: 4.2.11
jsonfile@6.2.0:
dependencies:
universalify: 2.0.1
@@ -17833,9 +17809,9 @@ snapshots:
methods@1.1.2: {}
mfm-js@0.25.0:
mfm-js@0.26.0:
dependencies:
'@twemoji/parser': 16.0.0
'@misskey-dev/emoji-data': 17.0.0
micromark-core-commonmark@2.0.3:
dependencies:
@@ -20522,8 +20498,6 @@ snapshots:
unist-util-is: 6.0.1
unist-util-visit-parents: 6.0.2
universalify@0.1.2: {}
universalify@2.0.1: {}
unload@2.4.1: {}

View File

@@ -35,4 +35,5 @@ minimumReleaseAge: 10080 # delay 7days to mitigate supply-chain attack
minimumReleaseAgeExclude:
- '@syuilo/aiscript'
- '@misskey-dev/*'
- mfm-js
- '@typescript/native-preview*'

View File

@@ -13,7 +13,6 @@ const __dirname = import.meta.dirname;
fs.rmSync(__dirname + '/../packages/backend/src-js', { recursive: true, force: true });
fs.rmSync(__dirname + '/../packages/backend/node_modules', { recursive: true, force: true });
fs.rmSync(__dirname + '/../packages/frontend-shared/built', { recursive: true, force: true });
fs.rmSync(__dirname + '/../packages/frontend-shared/node_modules', { recursive: true, force: true });
fs.rmSync(__dirname + '/../packages/frontend-builder/node_modules', { recursive: true, force: true });

View File

@@ -10,7 +10,6 @@ const __dirname = import.meta.dirname;
(async () => {
fs.rmSync(__dirname + '/../packages/backend/built', { recursive: true, force: true });
fs.rmSync(__dirname + '/../packages/backend/src-js', { recursive: true, force: true });
fs.rmSync(__dirname + '/../packages/frontend-shared/built', { recursive: true, force: true });
fs.rmSync(__dirname + '/../packages/frontend/built', { recursive: true, force: true });
fs.rmSync(__dirname + '/../packages/frontend-embed/built', { recursive: true, force: true });
fs.rmSync(__dirname + '/../packages/icons-subsetter/built', { recursive: true, force: true });

View File

@@ -70,12 +70,6 @@ execa('pnpm', ['--filter', 'backend', 'dev'], {
stderr: process.stderr,
});
execa('pnpm', ['--filter', 'frontend-shared', 'watch', '--no-clean'], {
cwd: _dirname + '/../',
stdout: process.stdout,
stderr: process.stderr,
});
execa('pnpm', ['--filter', 'frontend', 'watch'], {
cwd: _dirname + '/../',
stdout: process.stdout,