クライアントの設定コンポーネントを整理
* デスクトップとモバイルで統一 * いくつかの設定を廃止
This commit is contained in:
357
src/client/app/common/views/components/settings/theme.vue
Normal file
357
src/client/app/common/views/components/settings/theme.vue
Normal file
@ -0,0 +1,357 @@
|
||||
<template>
|
||||
<ui-card>
|
||||
<template #title><fa icon="palette"/> {{ $t('theme') }}</template>
|
||||
<section class="nicnklzforebnpfgasiypmpdaaglujqm fit-top">
|
||||
<label>
|
||||
<ui-select v-model="light" :placeholder="$t('light-theme')">
|
||||
<template #label><fa :icon="faSun"/> {{ $t('light-theme') }}</template>
|
||||
<optgroup :label="$t('light-themes')">
|
||||
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$t('dark-themes')">
|
||||
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
</ui-select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<ui-select v-model="dark" :placeholder="$t('dark-theme')">
|
||||
<template #label><fa :icon="faMoon"/> {{ $t('dark-theme') }}</template>
|
||||
<optgroup :label="$t('dark-themes')">
|
||||
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$t('light-themes')">
|
||||
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
</ui-select>
|
||||
</label>
|
||||
|
||||
<a href="https://assets.msky.cafe/theme/list" target="_blank">{{ $t('find-more-theme') }}</a>
|
||||
|
||||
<details class="creator">
|
||||
<summary><fa icon="palette"/> {{ $t('create-a-theme') }}</summary>
|
||||
<div>
|
||||
<span>{{ $t('base-theme') }}:</span>
|
||||
<ui-radio v-model="myThemeBase" value="light">{{ $t('base-theme-light') }}</ui-radio>
|
||||
<ui-radio v-model="myThemeBase" value="dark">{{ $t('base-theme-dark') }}</ui-radio>
|
||||
</div>
|
||||
<div>
|
||||
<ui-input v-model="myThemeName">
|
||||
<span>{{ $t('theme-name') }}</span>
|
||||
</ui-input>
|
||||
<ui-textarea v-model="myThemeDesc">
|
||||
<span>{{ $t('desc') }}</span>
|
||||
</ui-textarea>
|
||||
</div>
|
||||
<div>
|
||||
<div style="padding-bottom:8px;">{{ $t('primary-color') }}:</div>
|
||||
<color-picker v-model="myThemePrimary"/>
|
||||
</div>
|
||||
<div>
|
||||
<div style="padding-bottom:8px;">{{ $t('secondary-color') }}:</div>
|
||||
<color-picker v-model="myThemeSecondary"/>
|
||||
</div>
|
||||
<div>
|
||||
<div style="padding-bottom:8px;">{{ $t('text-color') }}:</div>
|
||||
<color-picker v-model="myThemeText"/>
|
||||
</div>
|
||||
<ui-button @click="preview()"><fa icon="eye"/> {{ $t('preview-created-theme') }}</ui-button>
|
||||
<ui-button primary @click="gen()"><fa :icon="['far', 'save']"/> {{ $t('save-created-theme') }}</ui-button>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><fa icon="download"/> {{ $t('install-a-theme') }}</summary>
|
||||
<ui-button @click="import_()"><fa icon="file-import"/> {{ $t('import') }}</ui-button>
|
||||
<input ref="file" type="file" accept=".misskeytheme" style="display:none;" @change="onUpdateImportFile"/>
|
||||
<p>{{ $t('import-by-code') }}:</p>
|
||||
<ui-textarea v-model="installThemeCode">
|
||||
<span>{{ $t('theme-code') }}</span>
|
||||
</ui-textarea>
|
||||
<ui-button @click="() => install(this.installThemeCode)"><fa icon="check"/> {{ $t('install') }}</ui-button>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><fa icon="folder-open"/> {{ $t('manage-themes') }}</summary>
|
||||
<ui-select v-model="selectedThemeId" :placeholder="$t('select-theme')">
|
||||
<optgroup :label="$t('builtin-themes')">
|
||||
<option v-for="x in builtinThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$t('my-themes')">
|
||||
<option v-for="x in installedThemes.filter(t => t.author == this.$store.state.i.username)" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
<optgroup :label="$t('installed-themes')">
|
||||
<option v-for="x in installedThemes.filter(t => t.author != this.$store.state.i.username)" :value="x.id" :key="x.id">{{ x.name }}</option>
|
||||
</optgroup>
|
||||
</ui-select>
|
||||
<template v-if="selectedTheme">
|
||||
<ui-input readonly :value="selectedTheme.author">
|
||||
<span>{{ $t('author') }}</span>
|
||||
</ui-input>
|
||||
<ui-textarea v-if="selectedTheme.desc" readonly :value="selectedTheme.desc">
|
||||
<span>{{ $t('desc') }}</span>
|
||||
</ui-textarea>
|
||||
<ui-textarea readonly tall :value="selectedThemeCode">
|
||||
<span>{{ $t('theme-code') }}</span>
|
||||
</ui-textarea>
|
||||
<ui-button @click="export_()" link :download="`${selectedTheme.name}.misskeytheme`" ref="export"><fa icon="box"/> {{ $t('export') }}</ui-button>
|
||||
<ui-button @click="uninstall()" v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><fa :icon="['far', 'trash-alt']"/> {{ $t('uninstall') }}</ui-button>
|
||||
</template>
|
||||
</details>
|
||||
</section>
|
||||
</ui-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import { lightTheme, darkTheme, builtinThemes, applyTheme, Theme } from '../../../../theme';
|
||||
import { Chrome } from 'vue-color';
|
||||
import * as uuid from 'uuid';
|
||||
import * as tinycolor from 'tinycolor2';
|
||||
import * as JSON5 from 'json5';
|
||||
import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
// 後方互換性のため
|
||||
function convertOldThemedefinition(t) {
|
||||
const t2 = {
|
||||
id: t.meta.id,
|
||||
name: t.meta.name,
|
||||
author: t.meta.author,
|
||||
base: t.meta.base,
|
||||
vars: t.meta.vars,
|
||||
props: t
|
||||
};
|
||||
delete t2.props.meta;
|
||||
return t2;
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/theme.vue'),
|
||||
components: {
|
||||
ColorPicker: Chrome
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
builtinThemes: builtinThemes,
|
||||
installThemeCode: null,
|
||||
selectedThemeId: null,
|
||||
myThemeBase: 'light',
|
||||
myThemeName: '',
|
||||
myThemeDesc: '',
|
||||
myThemePrimary: lightTheme.vars.primary,
|
||||
myThemeSecondary: lightTheme.vars.secondary,
|
||||
myThemeText: lightTheme.vars.text,
|
||||
faMoon, faSun
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
themes(): Theme[] {
|
||||
return builtinThemes.concat(this.$store.state.device.themes);
|
||||
},
|
||||
|
||||
darkThemes(): Theme[] {
|
||||
return this.themes.filter(t => t.base == 'dark' || t.kind == 'dark');
|
||||
},
|
||||
|
||||
lightThemes(): Theme[] {
|
||||
return this.themes.filter(t => t.base == 'light' || t.kind == 'light');
|
||||
},
|
||||
|
||||
installedThemes(): Theme[] {
|
||||
return this.$store.state.device.themes;
|
||||
},
|
||||
|
||||
light: {
|
||||
get() { return this.$store.state.device.lightTheme; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'lightTheme', value }); }
|
||||
},
|
||||
|
||||
dark: {
|
||||
get() { return this.$store.state.device.darkTheme; },
|
||||
set(value) { this.$store.commit('device/set', { key: 'darkTheme', value }); }
|
||||
},
|
||||
|
||||
selectedTheme() {
|
||||
if (this.selectedThemeId == null) return null;
|
||||
return this.themes.find(x => x.id == this.selectedThemeId);
|
||||
},
|
||||
|
||||
selectedThemeCode() {
|
||||
if (this.selectedTheme == null) return null;
|
||||
return JSON5.stringify(this.selectedTheme, null, '\t');
|
||||
},
|
||||
|
||||
myTheme(): any {
|
||||
return {
|
||||
name: this.myThemeName,
|
||||
author: this.$store.state.i.username,
|
||||
desc: this.myThemeDesc,
|
||||
base: this.myThemeBase,
|
||||
vars: {
|
||||
primary: tinycolor(typeof this.myThemePrimary == 'string' ? this.myThemePrimary : this.myThemePrimary.rgba).toRgbString(),
|
||||
secondary: tinycolor(typeof this.myThemeSecondary == 'string' ? this.myThemeSecondary : this.myThemeSecondary.rgba).toRgbString(),
|
||||
text: tinycolor(typeof this.myThemeText == 'string' ? this.myThemeText : this.myThemeText.rgba).toRgbString()
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
myThemeBase(v) {
|
||||
const theme = v == 'light' ? lightTheme : darkTheme;
|
||||
this.myThemePrimary = theme.vars.primary;
|
||||
this.myThemeSecondary = theme.vars.secondary;
|
||||
this.myThemeText = theme.vars.text;
|
||||
}
|
||||
},
|
||||
|
||||
beforeCreate() {
|
||||
// migrate old theme definitions
|
||||
// 後方互換性のため
|
||||
this.$store.commit('device/set', {
|
||||
key: 'themes', value: this.$store.state.device.themes.map(t => {
|
||||
if (t.id == null) {
|
||||
return convertOldThemedefinition(t);
|
||||
} else {
|
||||
return t;
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
install(code) {
|
||||
let theme;
|
||||
|
||||
try {
|
||||
theme = JSON5.parse(code);
|
||||
} catch (e) {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('invalid-theme')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 後方互換性のため
|
||||
if (theme.id == null && theme.meta != null) {
|
||||
theme = convertOldThemedefinition(theme);
|
||||
}
|
||||
|
||||
if (theme.id == null) {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('invalid-theme')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.$store.state.device.themes.some(t => t.id == theme.id)) {
|
||||
this.$root.dialog({
|
||||
type: 'info',
|
||||
text: this.$t('already-installed')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const themes = this.$store.state.device.themes.concat(theme);
|
||||
this.$store.commit('device/set', {
|
||||
key: 'themes', value: themes
|
||||
});
|
||||
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('installed').replace('{}', theme.name)
|
||||
});
|
||||
},
|
||||
|
||||
uninstall() {
|
||||
const theme = this.selectedTheme;
|
||||
const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
|
||||
this.$store.commit('device/set', {
|
||||
key: 'themes', value: themes
|
||||
});
|
||||
|
||||
this.$root.dialog({
|
||||
type: 'info',
|
||||
text: this.$t('uninstalled').replace('{}', theme.name)
|
||||
});
|
||||
},
|
||||
|
||||
import_() {
|
||||
(this.$refs.file as any).click();
|
||||
}
|
||||
|
||||
export_() {
|
||||
const blob = new Blob([this.selectedThemeCode], {
|
||||
type: 'application/json5'
|
||||
});
|
||||
this.$refs.export.$el.href = window.URL.createObjectURL(blob);
|
||||
},
|
||||
|
||||
onUpdateImportFile() {
|
||||
const f = (this.$refs.file as any).files[0];
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = e => {
|
||||
this.install(e.target.result);
|
||||
};
|
||||
|
||||
reader.readAsText(f);
|
||||
},
|
||||
|
||||
preview() {
|
||||
applyTheme(this.myTheme, false);
|
||||
},
|
||||
|
||||
gen() {
|
||||
const theme = this.myTheme;
|
||||
|
||||
if (theme.name == null || theme.name.trim() == '') {
|
||||
this.$root.dialog({
|
||||
type: 'warning',
|
||||
text: this.$t('theme-name-required')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
theme.id = uuid();
|
||||
|
||||
const themes = this.$store.state.device.themes.concat(theme);
|
||||
this.$store.commit('device/set', {
|
||||
key: 'themes', value: themes
|
||||
});
|
||||
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('saved')
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.nicnklzforebnpfgasiypmpdaaglujqm
|
||||
> a
|
||||
display block
|
||||
margin-top -16px
|
||||
margin-bottom 16px
|
||||
|
||||
> details
|
||||
border-top solid var(--lineWidth) var(--faceDivider)
|
||||
|
||||
> summary
|
||||
padding 16px 0
|
||||
|
||||
> *:last-child
|
||||
margin-bottom 16px
|
||||
|
||||
> .creator
|
||||
> div
|
||||
padding 16px 0
|
||||
border-bottom solid var(--lineWidth) var(--faceDivider)
|
||||
</style>
|
Reference in New Issue
Block a user