summaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorsyuilo <syuilotan@yahoo.co.jp>2021-01-09 17:18:45 +0900
committersyuilo <syuilotan@yahoo.co.jp>2021-01-09 17:18:45 +0900
commitedbaa0786738fe91a24dd22bb6e1f296792fb72a (patch)
tree743cc9f4391fcea7d9a57f1aaff7179e40e55196 /src/client
parentAdd ping api (diff)
downloadsharkey-edbaa0786738fe91a24dd22bb6e1f296792fb72a.tar.gz
sharkey-edbaa0786738fe91a24dd22bb6e1f296792fb72a.tar.bz2
sharkey-edbaa0786738fe91a24dd22bb6e1f296792fb72a.zip
簡易テーマエディタ実装
Diffstat (limited to 'src/client')
-rw-r--r--src/client/components/sample.vue4
-rw-r--r--src/client/pages/advanced-theme-editor.vue353
-rw-r--r--src/client/pages/settings/theme.vue13
-rw-r--r--src/client/pages/theme-editor.vue438
-rw-r--r--src/client/router.ts1
-rw-r--r--src/client/themes/_dark.json54
-rw-r--r--src/client/themes/_light.json54
7 files changed, 521 insertions, 296 deletions
diff --git a/src/client/components/sample.vue b/src/client/components/sample.vue
index b6300ba446..8fd79ceec9 100644
--- a/src/client/components/sample.vue
+++ b/src/client/components/sample.vue
@@ -15,7 +15,7 @@
<MkButton inline>This is</MkButton>
<MkButton inline primary>the button</MkButton>
</div>
- <div class="_content">
+ <div class="_content" style="pointer-events: none;">
<Mfm :text="mfm"/>
</div>
<div class="_content">
@@ -49,7 +49,7 @@ export default defineComponent({
data() {
return {
text: '',
- flag: false,
+ flag: true,
radio: 'misskey',
mfm: `Hello world! This is an @example mention. BTW you are @${this.$i.username}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`
}
diff --git a/src/client/pages/advanced-theme-editor.vue b/src/client/pages/advanced-theme-editor.vue
new file mode 100644
index 0000000000..1f5e260379
--- /dev/null
+++ b/src/client/pages/advanced-theme-editor.vue
@@ -0,0 +1,353 @@
+<template>
+<div class="t9makv94">
+ <section class="_section">
+ <div class="_content">
+ <details>
+ <summary>{{ $ts.import }}</summary>
+ <MkTextarea v-model:value="themeToImport">
+ {{ $ts._theme.importInfo }}
+ </MkTextarea>
+ <MkButton :disabled="!themeToImport.trim()" @click="importTheme">{{ $ts.import }}</MkButton>
+ </details>
+ </div>
+ </section>
+ <section class="_section">
+ <div class="_content _card _vMargin">
+ <div class="_content">
+ <MkInput v-model:value="name" required><span>{{ $ts.name }}</span></MkInput>
+ <MkInput v-model:value="author" required><span>{{ $ts.author }}</span></MkInput>
+ <MkTextarea v-model:value="description"><span>{{ $ts.description }}</span></MkTextarea>
+ <div class="_inputs">
+ <div v-text="$ts._theme.base" />
+ <MkRadio v-model="baseTheme" value="light">{{ $ts.light }}</MkRadio>
+ <MkRadio v-model="baseTheme" value="dark">{{ $ts.dark }}</MkRadio>
+ </div>
+ </div>
+ </div>
+ <div class="_content _card _vMargin">
+ <div class="list-view _content">
+ <div class="item" v-for="([ k, v ], i) in theme" :key="k">
+ <div class="_inputs">
+ <div>
+ {{ k.startsWith('$') ? `${k} (${$ts._theme.constant})` : $t('_theme.keys.' + k) }}
+ <button v-if="k.startsWith('$')" class="_button _link" @click="del(i)" v-text="$ts.delete" />
+ </div>
+ <div>
+ <div class="type" @click="chooseType($event, i)">
+ {{ getTypeOf(v) }} <Fa :icon="faChevronDown"/>
+ </div>
+ <!-- default -->
+ <div v-if="v === null" v-text="baseProps[k]" class="default-value" />
+ <!-- color -->
+ <div v-else-if="typeof v === 'string'" class="color">
+ <input type="color" :value="v" @input="colorChanged($event.target.value, i)"/>
+ <MkInput class="select" :value="v" @update:value="colorChanged($event, i)"/>
+ </div>
+ <!-- ref const -->
+ <MkInput v-else-if="v.type === 'refConst'" v-model:value="v.key">
+ <template #prefix>$</template>
+ <span>{{ $ts.name }}</span>
+ </MkInput>
+ <!-- ref props -->
+ <MkSelect class="select" v-else-if="v.type === 'refProp'" v-model:value="v.key">
+ <option v-for="key in themeProps" :value="key" :key="key">{{ $t('_theme.keys.' + key) }}</option>
+ </MkSelect>
+ <!-- func -->
+ <template v-else-if="v.type === 'func'">
+ <MkSelect class="select" v-model:value="v.name">
+ <template #label>{{ $ts._theme.funcKind }}</template>
+ <option v-for="n in ['alpha', 'darken', 'lighten']" :value="n" :key="n">{{ $t('_theme.' + n) }}</option>
+ </MkSelect>
+ <MkInput type="number" v-model:value="v.arg"><span>{{ $ts._theme.argument }}</span></MkInput>
+ <MkSelect class="select" v-model:value="v.value">
+ <template #label>{{ $ts._theme.basedProp }}</template>
+ <option v-for="key in themeProps" :value="key" :key="key">{{ $t('_theme.keys.' + key) }}</option>
+ </MkSelect>
+ </template>
+ <!-- CSS -->
+ <MkInput v-else-if="v.type === 'css'" v-model:value="v.value">
+ <span>CSS</span>
+ </MkInput>
+ </div>
+ </div>
+ </div>
+ <MkButton primary @click="addConst">{{ $ts._theme.addConstant }}</MkButton>
+ </div>
+ </div>
+ </section>
+ <section class="_section">
+ <details class="_content">
+ <summary>{{ $ts.sample }}</summary>
+ <MkSample/>
+ </details>
+ </section>
+ <section class="_section">
+ <div class="_content">
+ <MkButton inline @click="preview">{{ $ts.preview }}</MkButton>
+ <MkButton inline primary :disabled="!name || !author" @click="save">{{ $ts.save }}</MkButton>
+ </div>
+ </section>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import { faPalette, faChevronDown, faKeyboard } from '@fortawesome/free-solid-svg-icons';
+import * as JSON5 from 'json5';
+import { toUnicode } from 'punycode';
+
+import MkRadio from '@/components/ui/radio.vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInput from '@/components/ui/input.vue';
+import MkTextarea from '@/components/ui/textarea.vue';
+import MkSelect from '@/components/ui/select.vue';
+import MkSample from '@/components/sample.vue';
+
+import { convertToMisskeyTheme, ThemeValue, convertToViewModel, ThemeViewModel } from '@/scripts/theme-editor';
+import { Theme, applyTheme, lightTheme, darkTheme, themeProps, validateTheme } from '@/scripts/theme';
+import { host } from '@/config';
+import * as os from '@/os';
+import { ColdDeviceStorage } from '@/store';
+
+export default defineComponent({
+ components: {
+ MkRadio,
+ MkButton,
+ MkInput,
+ MkTextarea,
+ MkSelect,
+ MkSample,
+ },
+
+ data() {
+ return {
+ INFO: {
+ title: this.$ts.themeEditor,
+ icon: faPalette,
+ },
+ theme: [] as ThemeViewModel,
+ name: '',
+ description: '',
+ baseTheme: 'light' as 'dark' | 'light',
+ author: `@${this.$i.username}@${toUnicode(host)}`,
+ themeToImport: '',
+ changed: false,
+ lightTheme, darkTheme, themeProps,
+ faPalette, faChevronDown, faKeyboard,
+ }
+ },
+
+ computed: {
+ baseProps() {
+ return this.baseTheme === 'light' ? this.lightTheme.props : this.darkTheme.props;
+ },
+ },
+
+ beforeUnmount() {
+ window.removeEventListener('beforeunload', this.beforeunload);
+ },
+
+ async beforeRouteLeave(to, from, next) {
+ if (this.changed && !(await this.confirm())) {
+ next(false);
+ } else {
+ next();
+ }
+ },
+
+ mounted() {
+ this.init();
+ window.addEventListener('beforeunload', this.beforeunload);
+ const changed = () => this.changed = true;
+ this.$watch('name', changed);
+ this.$watch('description', changed);
+ this.$watch('baseTheme', changed);
+ this.$watch('author', changed);
+ this.$watch('theme', changed);
+ },
+
+ methods: {
+ beforeunload(e: BeforeUnloadEvent) {
+ if (this.changed) {
+ e.preventDefault();
+ e.returnValue = '';
+ }
+ },
+
+ async confirm(): Promise<boolean> {
+ const { canceled } = await os.dialog({
+ type: 'warning',
+ text: this.$ts.leaveConfirm,
+ showCancelButton: true
+ });
+ return !canceled;
+ },
+
+ init() {
+ const t: ThemeViewModel = [];
+ for (const key of themeProps) {
+ t.push([ key, null ]);
+ }
+ this.theme = t;
+ },
+
+ async del(i: number) {
+ const { canceled } = await os.dialog({
+ type: 'warning',
+ showCancelButton: true,
+ text: this.$t('_theme.deleteConstantConfirm', { const: this.theme[i][0] }),
+ });
+ if (canceled) return;
+ Vue.delete(this.theme, i);
+ },
+
+ async addConst() {
+ const { canceled, result } = await os.dialog({
+ title: this.$ts._theme.inputConstantName,
+ input: true
+ });
+ if (canceled) return;
+ this.theme.push([ '$' + result, '#000000']);
+ },
+
+ save() {
+ const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme);
+ const themes = ColdDeviceStorage.get('themes').concat(theme);
+ ColdDeviceStorage.set('themes', themes);
+ os.dialog({
+ type: 'success',
+ text: this.$t('_theme.installed', { name: theme.name })
+ });
+ this.changed = false;
+ },
+
+ preview() {
+ const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme);
+ try {
+ applyTheme(theme, false);
+ } catch (e) {
+ os.dialog({
+ type: 'error',
+ text: e.message
+ });
+ }
+ },
+
+ async importTheme() {
+ if (this.changed && (!await this.confirm())) return;
+
+ try {
+ const theme = JSON5.parse(this.themeToImport) as Theme;
+ if (!validateTheme(theme)) throw new Error(this.$ts._theme.invalid);
+
+ this.name = theme.name;
+ this.description = theme.desc || '';
+ this.author = theme.author;
+ this.baseTheme = theme.base || 'light';
+ this.theme = convertToViewModel(theme);
+ this.themeToImport = '';
+ } catch (e) {
+ os.dialog({
+ type: 'error',
+ text: e.message
+ });
+ }
+ },
+
+ colorChanged(color: string, i: number) {
+ this.theme[i] = [this.theme[i][0], color];
+ },
+
+ getTypeOf(v: ThemeValue) {
+ return v === null
+ ? this.$ts._theme.defaultValue
+ : typeof v === 'string'
+ ? this.$ts._theme.color
+ : this.$t('_theme.' + v.type);
+ },
+
+ async chooseType(e: MouseEvent, i: number) {
+ const newValue = await this.showTypeMenu(e);
+ this.theme[i] = [ this.theme[i][0], newValue ];
+ },
+
+ showTypeMenu(e: MouseEvent) {
+ return new Promise<ThemeValue>((resolve) => {
+ os.modalMenu([{
+ text: this.$ts._theme.defaultValue,
+ action: () => resolve(null),
+ }, {
+ text: this.$ts._theme.color,
+ action: () => resolve('#000000'),
+ }, {
+ text: this.$ts._theme.func,
+ action: () => resolve({
+ type: 'func', name: 'alpha', arg: 1, value: 'accent'
+ }),
+ }, {
+ text: this.$ts._theme.refProp,
+ action: () => resolve({
+ type: 'refProp', key: 'accent',
+ }),
+ }, {
+ text: this.$ts._theme.refConst,
+ action: () => resolve({
+ type: 'refConst', key: '',
+ }),
+ }, {
+ text: 'CSS',
+ action: () => resolve({
+ type: 'css', value: '',
+ }),
+ }], e.currentTarget || e.target);
+ });
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.t9makv94 {
+ > ._section {
+ > ._content {
+ > .list-view {
+ > .item {
+ min-height: 48px;
+ word-break: break-all;
+
+ &:not(:last-child) {
+ margin-bottom: 8px;
+ }
+
+ .select {
+ margin: 24px 0;
+ }
+
+ .type {
+ cursor: pointer;
+ }
+
+ .default-value {
+ opacity: 0.6;
+ pointer-events: none;
+ user-select: none;
+ }
+
+ .color {
+ > input {
+ display: inline-block;
+ width: 1.5em;
+ height: 1.5em;
+ }
+
+ > div {
+ margin-left: 8px;
+ display: inline-block;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/pages/settings/theme.vue b/src/client/pages/settings/theme.vue
index dd7911ce34..da1ad618b5 100644
--- a/src/client/pages/settings/theme.vue
+++ b/src/client/pages/settings/theme.vue
@@ -49,11 +49,14 @@
<FormButton primary v-else @click="wallpaper = null">{{ $ts.removeWallpaper }}</FormButton>
<FormGroup>
- <FormLink to="https://assets.msky.cafe/theme/list" external>{{ $ts._theme.explore }}</FormLink>
- <FormLink to="/theme-editor">{{ $ts._theme.make }}</FormLink>
+ <FormLink to="https://assets.msky.cafe/theme/list" external><template #icon><Fa :icon="faGlobe"/></template>{{ $ts._theme.explore }}</FormLink>
+ <FormLink to="/settings/theme/install"><template #icon><Fa :icon="faDownload"/></template>{{ $ts._theme.install }}</FormLink>
</FormGroup>
- <FormLink to="/settings/theme/install"><template #icon><Fa :icon="faDownload"/></template>{{ $ts._theme.install }}</FormLink>
+ <FormGroup>
+ <FormLink to="/theme-editor"><template #icon><Fa :icon="faPaintRoller"/></template>{{ $ts._theme.make }}</FormLink>
+ <FormLink to="/advanced-theme-editor"><template #icon><Fa :icon="faPaintRoller"/></template>{{ $ts._theme.make }} ({{ $ts.advanced }})</FormLink>
+ </FormGroup>
<FormLink to="/settings/theme/manage"><template #icon><Fa :icon="faFolderOpen"/></template>{{ $ts._theme.manage }}</FormLink>
</FormBase>
@@ -61,7 +64,7 @@
<script lang="ts">
import { computed, defineComponent, onMounted, ref, watch } from 'vue';
-import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons';
+import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye, faGlobe, faPaintRoller } from '@fortawesome/free-solid-svg-icons';
import FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue';
import FormBase from '@/components/form/base.vue';
@@ -148,7 +151,7 @@ export default defineComponent({
wallpaper.value = file.url;
});
},
- faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
+ faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye, faGlobe, faPaintRoller,
};
}
});
diff --git a/src/client/pages/theme-editor.vue b/src/client/pages/theme-editor.vue
index 1f5e260379..6ee0c11bdb 100644
--- a/src/client/pages/theme-editor.vue
+++ b/src/client/pages/theme-editor.vue
@@ -1,121 +1,59 @@
<template>
-<div class="t9makv94">
- <section class="_section">
- <div class="_content">
- <details>
- <summary>{{ $ts.import }}</summary>
- <MkTextarea v-model:value="themeToImport">
- {{ $ts._theme.importInfo }}
- </MkTextarea>
- <MkButton :disabled="!themeToImport.trim()" @click="importTheme">{{ $ts.import }}</MkButton>
- </details>
+<FormBase class="cwepdizn">
+ <div class="_formItem colorPicker">
+ <div class="_formLabel">{{ $ts.backgroundColor }}</div>
+ <div class="_formPanel colors">
+ <button v-for="color in bgColors" :key="color.color" @click="bgColor = color" class="color _button" :class="{ active: bgColor.color === color.color }">
+ <div class="preview" :style="{ background: color.forPreview }"></div>
+ </button>
</div>
- </section>
- <section class="_section">
- <div class="_content _card _vMargin">
- <div class="_content">
- <MkInput v-model:value="name" required><span>{{ $ts.name }}</span></MkInput>
- <MkInput v-model:value="author" required><span>{{ $ts.author }}</span></MkInput>
- <MkTextarea v-model:value="description"><span>{{ $ts.description }}</span></MkTextarea>
- <div class="_inputs">
- <div v-text="$ts._theme.base" />
- <MkRadio v-model="baseTheme" value="light">{{ $ts.light }}</MkRadio>
- <MkRadio v-model="baseTheme" value="dark">{{ $ts.dark }}</MkRadio>
- </div>
- </div>
+ </div>
+ <div class="_formItem colorPicker">
+ <div class="_formLabel">{{ $ts.accentColor }}</div>
+ <div class="_formPanel colors">
+ <button v-for="color in accentColors" :key="color" @click="accentColor = color" class="color rounded _button" :class="{ active: accentColor === color }">
+ <div class="preview" :style="{ background: color }"></div>
+ </button>
</div>
- <div class="_content _card _vMargin">
- <div class="list-view _content">
- <div class="item" v-for="([ k, v ], i) in theme" :key="k">
- <div class="_inputs">
- <div>
- {{ k.startsWith('$') ? `${k} (${$ts._theme.constant})` : $t('_theme.keys.' + k) }}
- <button v-if="k.startsWith('$')" class="_button _link" @click="del(i)" v-text="$ts.delete" />
- </div>
- <div>
- <div class="type" @click="chooseType($event, i)">
- {{ getTypeOf(v) }} <Fa :icon="faChevronDown"/>
- </div>
- <!-- default -->
- <div v-if="v === null" v-text="baseProps[k]" class="default-value" />
- <!-- color -->
- <div v-else-if="typeof v === 'string'" class="color">
- <input type="color" :value="v" @input="colorChanged($event.target.value, i)"/>
- <MkInput class="select" :value="v" @update:value="colorChanged($event, i)"/>
- </div>
- <!-- ref const -->
- <MkInput v-else-if="v.type === 'refConst'" v-model:value="v.key">
- <template #prefix>$</template>
- <span>{{ $ts.name }}</span>
- </MkInput>
- <!-- ref props -->
- <MkSelect class="select" v-else-if="v.type === 'refProp'" v-model:value="v.key">
- <option v-for="key in themeProps" :value="key" :key="key">{{ $t('_theme.keys.' + key) }}</option>
- </MkSelect>
- <!-- func -->
- <template v-else-if="v.type === 'func'">
- <MkSelect class="select" v-model:value="v.name">
- <template #label>{{ $ts._theme.funcKind }}</template>
- <option v-for="n in ['alpha', 'darken', 'lighten']" :value="n" :key="n">{{ $t('_theme.' + n) }}</option>
- </MkSelect>
- <MkInput type="number" v-model:value="v.arg"><span>{{ $ts._theme.argument }}</span></MkInput>
- <MkSelect class="select" v-model:value="v.value">
- <template #label>{{ $ts._theme.basedProp }}</template>
- <option v-for="key in themeProps" :value="key" :key="key">{{ $t('_theme.keys.' + key) }}</option>
- </MkSelect>
- </template>
- <!-- CSS -->
- <MkInput v-else-if="v.type === 'css'" v-model:value="v.value">
- <span>CSS</span>
- </MkInput>
- </div>
- </div>
- </div>
- <MkButton primary @click="addConst">{{ $ts._theme.addConstant }}</MkButton>
- </div>
+ </div>
+ <div class="_formItem colorPicker">
+ <div class="_formLabel">{{ $ts.textColor }}</div>
+ <div class="_formPanel colors">
+ <button v-for="color in fgColors" :key="color" @click="fgColor = color" class="color char _button" :class="{ active: fgColor === color }">
+ <div class="preview" :style="{ color: color.forPreview ? color.forPreview : bgColor.kind === 'light' ? '#5f5f5f' : '#dadada' }">A</div>
+ </button>
</div>
- </section>
- <section class="_section">
- <details class="_content">
- <summary>{{ $ts.sample }}</summary>
- <MkSample/>
- </details>
- </section>
- <section class="_section">
- <div class="_content">
- <MkButton inline @click="preview">{{ $ts.preview }}</MkButton>
- <MkButton inline primary :disabled="!name || !author" @click="save">{{ $ts.save }}</MkButton>
+ </div>
+ <div class="_formItem preview">
+ <div class="_formLabel">{{ $ts.preview }}</div>
+ <div class="_formPanel preview">
+ <MkSample class="preview"/>
</div>
- </section>
-</div>
+ </div>
+ <FormButton @click="saveAs">{{ $ts.saveAs }}</FormButton>
+</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faPalette, faChevronDown, faKeyboard } from '@fortawesome/free-solid-svg-icons';
-import * as JSON5 from 'json5';
import { toUnicode } from 'punycode';
+import * as tinycolor from 'tinycolor2';
+import { v4 as uuid} from 'uuid';
-import MkRadio from '@/components/ui/radio.vue';
-import MkButton from '@/components/ui/button.vue';
-import MkInput from '@/components/ui/input.vue';
-import MkTextarea from '@/components/ui/textarea.vue';
-import MkSelect from '@/components/ui/select.vue';
+import FormBase from '@/components/form/base.vue';
+import FormButton from '@/components/form/button.vue';
import MkSample from '@/components/sample.vue';
-import { convertToMisskeyTheme, ThemeValue, convertToViewModel, ThemeViewModel } from '@/scripts/theme-editor';
-import { Theme, applyTheme, lightTheme, darkTheme, themeProps, validateTheme } from '@/scripts/theme';
+import { Theme, applyTheme, validateTheme } from '@/scripts/theme';
import { host } from '@/config';
import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
export default defineComponent({
components: {
- MkRadio,
- MkButton,
- MkInput,
- MkTextarea,
- MkSelect,
+ FormBase,
+ FormButton,
MkSample,
},
@@ -125,229 +63,159 @@ export default defineComponent({
title: this.$ts.themeEditor,
icon: faPalette,
},
- theme: [] as ThemeViewModel,
- name: '',
- description: '',
- baseTheme: 'light' as 'dark' | 'light',
- author: `@${this.$i.username}@${toUnicode(host)}`,
- themeToImport: '',
- changed: false,
- lightTheme, darkTheme, themeProps,
- faPalette, faChevronDown, faKeyboard,
+ bgColors: [
+ { color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' },
+ { color: '#f0eee9', kind: 'light', forPreview: '#f3e2b9' },
+ { color: '#e9eff0', kind: 'light', forPreview: '#bfe3e8' },
+ { color: '#f0e9ee', kind: 'light', forPreview: '#f1d1e8' },
+ { color: '#2b2b2b', kind: 'dark', forPreview: '#2b2b2b' },
+ { color: '#362e29', kind: 'dark', forPreview: '#735c4d' },
+ { color: '#303629', kind: 'dark', forPreview: '#506d2f' },
+ { color: '#293436', kind: 'dark', forPreview: '#258192' },
+ ],
+ bgColor: null,
+ accentColors: ['#e36749', '#f29924', '#98c934', '#34c9a9', '#34a1c9', '#606df7', '#8d34c9', '#e84d83'],
+ accentColor: null,
+ fgColors: [
+ { color: 'none', forLight: '#5f5f5f', forDark: '#dadada', forPreview: null },
+ { color: 'red', forLight: '#7f6666', forDark: '#e4d1d1', forPreview: '#ca4343' },
+ { color: 'yellow', forLight: '#736955', forDark: '#e0d5c0', forPreview: '#d49923' },
+ { color: 'green', forLight: '#586d5b', forDark: '#d1e4d4', forPreview: '#4cbd5c' },
+ { color: 'cyan', forLight: '#5d7475', forDark: '#d1e3e4', forPreview: '#2abdc3' },
+ { color: 'blue', forLight: '#676880', forDark: '#d1d2e4', forPreview: '#3035b5' },
+ { color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' },
+ ],
+ fgColor: null,
+ faPalette,
}
},
- computed: {
- baseProps() {
- return this.baseTheme === 'light' ? this.lightTheme.props : this.darkTheme.props;
- },
- },
-
- beforeUnmount() {
- window.removeEventListener('beforeunload', this.beforeunload);
- },
+ created() {
+ const currentBgColor = getComputedStyle(document.documentElement).getPropertyValue('--bg');
+ const matchedBgColor = this.bgColors.find(x => tinycolor(x.color).toRgbString() === tinycolor(currentBgColor).toRgbString());
+ this.bgColor = matchedBgColor ? matchedBgColor : this.bgColors[0];
+ const currentAccentColor = getComputedStyle(document.documentElement).getPropertyValue('--accent');
+ const matchedAccentColor = this.accentColors.find(x => tinycolor(x).toRgbString() === tinycolor(currentAccentColor).toRgbString());
+ this.accentColor = matchedAccentColor ? matchedAccentColor : this.accentColors[0];
+ const currentFgColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+ const matchedFgColor = this.fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(currentFgColor).toRgbString()));
+ this.fgColor = matchedFgColor ? matchedFgColor : this.fgColors[0];
- async beforeRouteLeave(to, from, next) {
- if (this.changed && !(await this.confirm())) {
- next(false);
- } else {
- next();
- }
- },
-
- mounted() {
- this.init();
- window.addEventListener('beforeunload', this.beforeunload);
- const changed = () => this.changed = true;
- this.$watch('name', changed);
- this.$watch('description', changed);
- this.$watch('baseTheme', changed);
- this.$watch('author', changed);
- this.$watch('theme', changed);
+ this.$watch('bgColor', this.apply);
+ this.$watch('accentColor', this.apply);
+ this.$watch('fgColor', this.apply);
+ this.apply();
},
methods: {
- beforeunload(e: BeforeUnloadEvent) {
- if (this.changed) {
- e.preventDefault();
- e.returnValue = '';
- }
+ convert() {
+ return {
+ id: '#MY_THEME#',
+ name: this.$ts.myTheme,
+ base: this.bgColor.kind,
+ props: {
+ bg: this.bgColor.color,
+ fg: this.bgColor.kind === 'light' ? this.fgColor.forLight : this.fgColor.forDark,
+ accent: this.accentColor,
+ }
+ };
},
- async confirm(): Promise<boolean> {
- const { canceled } = await os.dialog({
- type: 'warning',
- text: this.$ts.leaveConfirm,
- showCancelButton: true
- });
- return !canceled;
- },
+ apply() {
+ const theme = this.convert();
+ applyTheme(theme, true);
- init() {
- const t: ThemeViewModel = [];
- for (const key of themeProps) {
- t.push([ key, null ]);
- }
- this.theme = t;
- },
-
- async del(i: number) {
- const { canceled } = await os.dialog({
- type: 'warning',
- showCancelButton: true,
- text: this.$t('_theme.deleteConstantConfirm', { const: this.theme[i][0] }),
- });
- if (canceled) return;
- Vue.delete(this.theme, i);
+ const themes = ColdDeviceStorage.get('themes').filter(t => t.id != '#MY_THEME#').concat(theme);
+ ColdDeviceStorage.set('themes', themes);
+ ColdDeviceStorage.set('lightTheme', theme.id);
+ ColdDeviceStorage.set('darkTheme', theme.id);
},
-
- async addConst() {
- const { canceled, result } = await os.dialog({
- title: this.$ts._theme.inputConstantName,
- input: true
+
+ async saveAs() {
+ const { canceled, result: name } = await os.dialog({
+ title: this.$ts.name,
+ input: {
+ allowEmpty: false
+ }
});
if (canceled) return;
- this.theme.push([ '$' + result, '#000000']);
- },
-
- save() {
- const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme);
+
+ const theme = this.convert();
+ theme.id = uuid();
+ theme.name = name;
+ theme.author = `@${this.$i.username}@${toUnicode(host)}`;
const themes = ColdDeviceStorage.get('themes').concat(theme);
ColdDeviceStorage.set('themes', themes);
+ ColdDeviceStorage.set('lightTheme', theme.id);
+ ColdDeviceStorage.set('darkTheme', theme.id);
os.dialog({
type: 'success',
text: this.$t('_theme.installed', { name: theme.name })
});
- this.changed = false;
- },
-
- preview() {
- const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme);
- try {
- applyTheme(theme, false);
- } catch (e) {
- os.dialog({
- type: 'error',
- text: e.message
- });
- }
- },
-
- async importTheme() {
- if (this.changed && (!await this.confirm())) return;
-
- try {
- const theme = JSON5.parse(this.themeToImport) as Theme;
- if (!validateTheme(theme)) throw new Error(this.$ts._theme.invalid);
-
- this.name = theme.name;
- this.description = theme.desc || '';
- this.author = theme.author;
- this.baseTheme = theme.base || 'light';
- this.theme = convertToViewModel(theme);
- this.themeToImport = '';
- } catch (e) {
- os.dialog({
- type: 'error',
- text: e.message
- });
- }
- },
-
- colorChanged(color: string, i: number) {
- this.theme[i] = [this.theme[i][0], color];
- },
-
- getTypeOf(v: ThemeValue) {
- return v === null
- ? this.$ts._theme.defaultValue
- : typeof v === 'string'
- ? this.$ts._theme.color
- : this.$t('_theme.' + v.type);
- },
-
- async chooseType(e: MouseEvent, i: number) {
- const newValue = await this.showTypeMenu(e);
- this.theme[i] = [ this.theme[i][0], newValue ];
- },
-
- showTypeMenu(e: MouseEvent) {
- return new Promise<ThemeValue>((resolve) => {
- os.modalMenu([{
- text: this.$ts._theme.defaultValue,
- action: () => resolve(null),
- }, {
- text: this.$ts._theme.color,
- action: () => resolve('#000000'),
- }, {
- text: this.$ts._theme.func,
- action: () => resolve({
- type: 'func', name: 'alpha', arg: 1, value: 'accent'
- }),
- }, {
- text: this.$ts._theme.refProp,
- action: () => resolve({
- type: 'refProp', key: 'accent',
- }),
- }, {
- text: this.$ts._theme.refConst,
- action: () => resolve({
- type: 'refConst', key: '',
- }),
- }, {
- text: 'CSS',
- action: () => resolve({
- type: 'css', value: '',
- }),
- }], e.currentTarget || e.target);
- });
}
}
});
</script>
<style lang="scss" scoped>
-.t9makv94 {
- > ._section {
- > ._content {
- > .list-view {
- > .item {
- min-height: 48px;
- word-break: break-all;
+.cwepdizn {
+ max-width: 800px;
+ margin: 0 auto;
- &:not(:last-child) {
- margin-bottom: 8px;
- }
+ > .colorPicker {
+ > .colors {
+ padding: 32px;
+ text-align: center;
- .select {
- margin: 24px 0;
- }
+ > .color {
+ display: inline-block;
+ position: relative;
+ width: 64px;
+ height: 64px;
+ border-radius: 8px;
- .type {
- cursor: pointer;
+ &:hover {
+ > .preview {
+ transform: scale(1.1);
}
+ }
- .default-value {
- opacity: 0.6;
- pointer-events: none;
- user-select: none;
- }
+ > .preview {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ margin: auto;
+ width: 42px;
+ height: 42px;
+ border-radius: 4px;
+ box-shadow: 0 2px 4px rgb(0 0 0 / 30%);
+ transition: transform 0.15s ease;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 2px var(--divider) inset;
+ }
- .color {
- > input {
- display: inline-block;
- width: 1.5em;
- height: 1.5em;
- }
+ &.rounded {
+ border-radius: 999px;
- > div {
- margin-left: 8px;
- display: inline-block;
- }
+ > .preview {
+ border-radius: 999px;
}
}
+
+ &.char {
+ line-height: 42px;
+ }
}
}
}
+
+ > .preview > .preview > .preview {
+ box-shadow: none;
+ background: transparent;
+ }
}
</style>
diff --git a/src/client/router.ts b/src/client/router.ts
index 2826f4ac14..5753a47024 100644
--- a/src/client/router.ts
+++ b/src/client/router.ts
@@ -29,6 +29,7 @@ export const router = createRouter({
{ path: '/featured', component: page('featured') },
{ path: '/docs', component: page('docs') },
{ path: '/theme-editor', component: page('theme-editor') },
+ { path: '/advanced-theme-editor', component: page('advanced-theme-editor') },
{ path: '/docs/:doc', component: page('doc'), props: route => ({ doc: route.params.doc }) },
{ path: '/explore', component: page('explore') },
{ path: '/explore/tags/:tag', props: true, component: page('explore') },
diff --git a/src/client/themes/_dark.json5 b/src/client/themes/_dark.json5
index 18075ac322..847c0b4ec4 100644
--- a/src/client/themes/_dark.json5
+++ b/src/client/themes/_dark.json5
@@ -15,11 +15,11 @@
focus: ':alpha<0.3<@accent',
bg: '#000',
acrylicBg: ':alpha<0.5<@bg',
- fg: '#c7d1d8',
+ fg: '#dadada',
fgHighlighted: ':lighten<3<@fg',
divider: 'rgba(255, 255, 255, 0.1)',
indicator: '@accent',
- panel: '#000',
+ panel: ':lighten<3<@bg',
panelHighlight: ':lighten<3<@panel',
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
diff --git a/src/client/themes/_light.json5 b/src/client/themes/_light.json5
index 2b9bbdd5fd..d75e94afd6 100644
--- a/src/client/themes/_light.json5
+++ b/src/client/themes/_light.json5
@@ -15,11 +15,11 @@
focus: ':alpha<0.3<@accent',
bg: '#fff',
acrylicBg: ':alpha<0.5<@bg',
- fg: '#5c6a73',
+ fg: '#5f5f5f',
fgHighlighted: ':darken<3<@fg',
divider: 'rgba(0, 0, 0, 0.1)',
indicator: '@accent',
- panel: '#fff',
+ panel: ':lighten<3<@bg',
panelHighlight: ':darken<3<@panel',
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',