diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index ab4b10549..8ae062847 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -519,6 +519,10 @@ fixedWidgetsPosition: "ウィジェットの位置を固定する"
enablePlayer: "プレイヤーを開く"
disablePlayer: "プレイヤーを閉じる"
expandTweet: "ツイートを展開する"
+themeEditor: "テーマエディター"
+description: "説明"
+author: "作者"
+leaveConfirm: "未保存の変更があります。破棄しますか?"
deck: "デッキ"
undeck: "デッキ解除"
@@ -530,6 +534,70 @@ _theme:
installed: "{name}をインストールしました"
alreadyInstalled: "そのテーマは既にインストールされています"
invalid: "テーマの形式が間違っています"
+ make: "テーマを作る"
+ base: "ベース"
+ addConstant: "定数を追加"
+ constant: "定数"
+ defaultValue: "デフォルト値"
+ color: "色"
+ refProp: "プロパティを参照"
+ refConst: "定数を参照"
+ key: "キー"
+ func: "関数"
+ funcKind: "関数の種類"
+ argument: "引数"
+ basedProp: "元にするプロパティの名前"
+ alpha: "不透明度"
+ darken: "暗さ"
+ lighten: "明るさ"
+ inputConstantName: "定数名を入力してください"
+ importInfo: "ここにテーマコードを貼り付けて、エディターにインポートできます"
+ deleteConstantConfirm: "定数 {const} を削除しても良いですか?"
+
+ keys:
+ accent: "アクセント"
+ bg: "背景"
+ fg: "文字"
+ focus: "フォーカス"
+ indicator: "インジケーター"
+ panel: "パネル"
+ shadow: "影"
+ header: "ヘッダー"
+ navBg: "サイドバーの背景"
+ navFg: "サイドバーの文字"
+ navHoverFg: "サイドバー文字(ホバー)"
+ navActive: "サイドバー文字(アクティブ)"
+ navIndicator: "サイドバーのインジケーター"
+ link: "リンク"
+ hashtag: "ハッシュタグ"
+ mention: "メンション"
+ mentionMe: "あなた宛てメンション"
+ renote: "Renote"
+ modalBg: "モーダルの背景"
+ divider: "分割線"
+ scrollbarHandle: "スクロールバーの取っ手"
+ scrollbarHandleHover: "スクロールバーの取っ手(ホバー)"
+ dateLabelFg: "日付ラベルの文字"
+ infoBg: "情報の背景"
+ infoFg: "情報の文字"
+ infoWarnBg: "警告の背景"
+ infoWarnFg: "警告の文字"
+ cwBg: "CW ボタンの背景"
+ cwFg: "CW ボタンの文字"
+ cwHoverBg: "CW ボタンの背景 (ホバー)"
+ toastBg: "通知トーストの背景"
+ toastFg: "通知トーストの文字"
+ buttonBg: "ボタンの背景"
+ buttonHoverBg: "ボタンの背景 (ホバー)"
+ inputBorder: "入力ボックスの縁取り"
+ listItemHoverBg: "リスト項目の背景 (ホバー)"
+ driveFolderBg: "ドライブフォルダーの背景"
+ wallpaperOverlay: "壁紙のオーバーレイ"
+ badge: "バッジ"
+ messageBg: "チャットの背景"
+ accentDarken: "アクセント (暗め)"
+ accentLighten: "アクセント (明るめ)"
+ fgHighlighted: "強調された文字"
_sfx:
note: "ノート"
diff --git a/src/client/app.vue b/src/client/app.vue
index 4f3918356..a751d5db4 100644
--- a/src/client/app.vue
+++ b/src/client/app.vue
@@ -131,6 +131,10 @@ export default Vue.extend({
computed: {
keymap(): any {
return {
+ 'd': () => {
+ if (this.$store.state.device.syncDeviceDarkMode) return;
+ this.$store.commit('device/set', { key: 'darkMode', value: !this.$store.state.device.darkMode });
+ },
'p': this.post,
'n': this.post,
's': this.search,
diff --git a/src/client/pages/preferences/theme.vue b/src/client/pages/preferences/theme.vue
index 246787fa5..173ccd709 100644
--- a/src/client/pages/preferences/theme.vue
+++ b/src/client/pages/preferences/theme.vue
@@ -22,6 +22,7 @@
+ {{ $t('syncDeviceDarkMode') }}
{{ $t('setWallpaper') }}
diff --git a/src/client/pages/room/room.vue b/src/client/pages/room/room.vue
index cf6850526..05b93c04e 100644
--- a/src/client/pages/room/room.vue
+++ b/src/client/pages/room/room.vue
@@ -143,7 +143,7 @@ export default Vue.extend({
if (this.changed) {
this.$root.dialog({
type: 'warning',
- text: this.$t('leave-confirm'),
+ text: this.$t('leaveConfirm'),
showCancelButton: true
}).then(({ canceled }) => {
if (canceled) {
diff --git a/src/client/pages/theme-editor.vue b/src/client/pages/theme-editor.vue
new file mode 100644
index 000000000..3a3fbfa2d
--- /dev/null
+++ b/src/client/pages/theme-editor.vue
@@ -0,0 +1,343 @@
+
+
+
+
{{ $t('themeEditor') }}
+
+
+
+
{{ $t('name') }}
+
{{ $t('author') }}
+
{{ $t('description') }}
+
+
+
+
+
+
{{ $t('_theme.addConstant') }}
+
+
+
+
+ {{ $t('_theme.importInfo') }}
+
+ {{ $t('import') }}
+
+
+
+
+
+
+
+
+
diff --git a/src/client/router.ts b/src/client/router.ts
index cf98c57bd..a741aeb95 100644
--- a/src/client/router.ts
+++ b/src/client/router.ts
@@ -24,6 +24,7 @@ export const router = new VueRouter({
{ path: '/about-misskey', component: page('about-misskey') },
{ path: '/featured', component: page('featured') },
{ path: '/docs', component: page('docs') },
+ { path: '/theme-editor', component: page('theme-editor') },
{ path: '/docs/:doc', component: page('doc'), props: true },
{ path: '/explore', component: page('explore') },
{ path: '/explore/tags/:tag', props: true, component: page('explore') },
diff --git a/src/client/scripts/theme-editor.ts b/src/client/scripts/theme-editor.ts
new file mode 100644
index 000000000..e0c3bc25b
--- /dev/null
+++ b/src/client/scripts/theme-editor.ts
@@ -0,0 +1,74 @@
+import { v4 as uuid} from 'uuid';
+
+import { themeProps, Theme } from './theme';
+
+export type Default = null;
+export type Color = string;
+export type FuncName = 'alpha' | 'darken' | 'lighten';
+export type Func = { type: 'func', name: FuncName, arg: number, value: string };
+export type RefProp = { type: 'refProp', key: string };
+export type RefConst = { type: 'refConst', key: string };
+
+export type ThemeValue = Color | Func | RefProp | RefConst | Default;
+
+export type ThemeViewModel = [ string, ThemeValue ][];
+
+export const fromThemeString = (str?: string) : ThemeValue => {
+ if (!str) return null;
+ if (str.startsWith(':')) {
+ const parts = str.slice(1).split('<');
+ const name = parts[0] as FuncName;
+ const arg = parseFloat(parts[1]);
+ const value = parts[2].startsWith('@') ? parts[2].slice(1) : '';
+ return { type: 'func', name, arg, value };
+ } else if (str.startsWith('@')) {
+ return {
+ type: 'refProp',
+ key: str.slice(1),
+ };
+ } else if (str.startsWith('$')) {
+ return {
+ type: 'refConst',
+ key: str.slice(1),
+ };
+ } else {
+ return str;
+ }
+};
+
+export const toThemeString = (value: Color | Func | RefProp | RefConst) => {
+ if (typeof value === 'string') return value;
+ switch (value.type) {
+ case 'func': return `:${value.name}<${value.arg}<@${value.value}`;
+ case 'refProp': return `@${value.key}`;
+ case 'refConst': return `$${value.key}`;
+ }
+};
+
+export const convertToMisskeyTheme = (vm: ThemeViewModel, name: string, desc: string, author: string, base: 'dark' | 'light'): Theme => {
+ const props = { } as { [key: string]: string };
+ for (const [ key, value ] of vm) {
+ if (value === null) continue;
+ props[key] = toThemeString(value);
+ }
+
+ return {
+ id: uuid(),
+ name, desc, author, props, base
+ };
+};
+
+export const convertToViewModel = (theme: Theme): ThemeViewModel => {
+ const vm: ThemeViewModel = [];
+ // プロパティの登録
+ vm.push(...themeProps.map(key => [ key, fromThemeString(theme.props[key])] as [ string, ThemeValue ]));
+
+ // 定数の登録
+ const consts = Object
+ .keys(theme.props)
+ .filter(k => k.startsWith('$'))
+ .map(k => [ k, fromThemeString(theme.props[k]) ] as [ string, ThemeValue ]);
+
+ vm.push(...consts);
+ return vm;
+};
diff --git a/src/client/scripts/theme.ts b/src/client/scripts/theme.ts
index d458df45f..30eaf77e0 100644
--- a/src/client/scripts/theme.ts
+++ b/src/client/scripts/theme.ts
@@ -12,6 +12,8 @@ export type Theme = {
export const lightTheme: Theme = require('../themes/_light.json5');
export const darkTheme: Theme = require('../themes/_dark.json5');
+export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
+
export const builtinThemes = [
require('../themes/white.json5'),
require('../themes/black.json5'),
diff --git a/src/client/themes/_dark.json5 b/src/client/themes/_dark.json5
index 4e5225db3..8d7a345fd 100644
--- a/src/client/themes/_dark.json5
+++ b/src/client/themes/_dark.json5
@@ -12,12 +12,10 @@
accent: '#86b300',
accentDarken: ':darken<10<@accent',
accentLighten: ':lighten<10<@accent',
- accentShadow: ':alpha<0.3<@accent',
focus: ':alpha<0.3<@accent',
bg: '#000',
fg: '#c7d1d8',
fgHighlighted: ':lighten<3<@fg',
- html: '@bg',
divider: 'rgba(255, 255, 255, 0.1)',
indicator: '@accent',
panel: '#000',
diff --git a/src/client/themes/_light.json5 b/src/client/themes/_light.json5
index 2317ddef6..4e499e178 100644
--- a/src/client/themes/_light.json5
+++ b/src/client/themes/_light.json5
@@ -12,12 +12,10 @@
accent: '#86b300',
accentDarken: ':darken<10<@accent',
accentLighten: ':lighten<10<@accent',
- accentShadow: ':alpha<0.4<@accent',
focus: ':alpha<0.3<@accent',
bg: '#fafafa',
fg: '#5c6a73',
fgHighlighted: ':darken<3<@fg',
- html: '@bg',
divider: 'rgba(0, 0, 0, 0.1)',
indicator: '@accent',
panel: '#fff',
diff --git a/src/client/themes/halloween.json5 b/src/client/themes/halloween.json5
index 7cabf01d1..1394c793e 100644
--- a/src/client/themes/halloween.json5
+++ b/src/client/themes/halloween.json5
@@ -12,7 +12,6 @@
panel: '#1f1d30',
bg: '#0f0e17',
fg: '#b1bee3',
- html: '@accent',
renote: '@accent',
},
}