mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-05-09 22:35:36 +02:00
Compare commits
5 Commits
clean-pref
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a09a2c2eee | ||
|
|
717931cfcb | ||
|
|
9027129b58 | ||
|
|
b73ac26612 | ||
|
|
b528ff9c59 |
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "fluent-emojis"]
|
||||
path = fluent-emojis
|
||||
url = https://github.com/misskey-dev/emojis.git
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
## Unreleased
|
||||
|
||||
### General
|
||||
-
|
||||
- Enhance: Unicode 17.0 に収録されている絵文字の処理・表示に対応
|
||||
- Fluent Emojiや端末ネイティブの絵文字を利用している場合は、最新の絵文字に対応しておらず正しく表示できない可能性があります。絵文字が表示できない場合は、表示に使用する絵文字をTwemojiに切り替えてご利用ください。
|
||||
|
||||
### Client
|
||||
-
|
||||
- Enhance: テーマのプレビュー時、リロードせずにもとのテーマに戻せるように
|
||||
- Enhance: Fluent Emojiを更新し、Unicode 15+相当の絵文字の表示に対応
|
||||
- Fix: テーマエディター使用時に、最初の変更のみ適用される問題を修正
|
||||
- Fix: テーマのプレビュー時、既存のテーマとIDが被っている場合にプレビューできない問題を修正
|
||||
- Fix: テーマのインストールエラーの表示を改善
|
||||
|
||||
### Server
|
||||
-
|
||||
|
||||
7
COPYING
7
COPYING
@@ -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
|
||||
|
||||
@@ -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
@@ -1409,6 +1409,8 @@ presets: "プリセット"
|
||||
zeroPadding: "ゼロ埋め"
|
||||
nothingToConfigure: "設定項目はありません"
|
||||
viewRenotedChannel: "リノート先のチャンネルを見る"
|
||||
previewingTheme: "テーマのプレビュー中"
|
||||
previewingThemeRestore: "元に戻す"
|
||||
|
||||
_imageEditing:
|
||||
_vars:
|
||||
|
||||
@@ -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
@@ -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'),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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/',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
13
packages/frontend-shared/@types/theme.d.ts
vendored
Normal file
13
packages/frontend-shared/@types/theme.d.ts
vendored
Normal 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;
|
||||
}
|
||||
@@ -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.`);
|
||||
});
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
|
||||
126
packages/frontend-shared/js/theme.ts
Normal file
126
packages/frontend-shared/js/theme.ts
Normal 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;
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ await fs.readFile(
|
||||
if (
|
||||
micromatch(Array.from(modules), [
|
||||
'../../assets/**',
|
||||
'../../fluent-emojis/**',
|
||||
'../../locales/ja-JP.yml',
|
||||
'assets/**',
|
||||
'public/**',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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)';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
57
packages/frontend/src/ui/_common_/ThemePreviewing.vue
Normal file
57
packages/frontend/src/ui/_common_/ThemePreviewing.vue
Normal 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>
|
||||
@@ -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';
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)';
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
188
packages/frontend/test/theme.test.ts
Normal file
188
packages/frontend/test/theme.test.ts
Normal 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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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/',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -5651,6 +5651,14 @@ export interface Locale extends ILocale {
|
||||
* リノート先のチャンネルを見る
|
||||
*/
|
||||
"viewRenotedChannel": string;
|
||||
/**
|
||||
* テーマのプレビュー中
|
||||
*/
|
||||
"previewingTheme": string;
|
||||
/**
|
||||
* 元に戻す
|
||||
*/
|
||||
"previewingThemeRestore": string;
|
||||
"_imageEditing": {
|
||||
"_vars": {
|
||||
/**
|
||||
|
||||
136
pnpm-lock.yaml
generated
136
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
||||
@@ -35,4 +35,5 @@ minimumReleaseAge: 10080 # delay 7days to mitigate supply-chain attack
|
||||
minimumReleaseAgeExclude:
|
||||
- '@syuilo/aiscript'
|
||||
- '@misskey-dev/*'
|
||||
- mfm-js
|
||||
- '@typescript/native-preview*'
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user