jormungandr-bite/src/client/app/common/views/components/settings/theme.vue

592 lines
14 KiB
Vue
Raw Normal View History

2018-09-28 09:01:11 -06:00
<template>
2018-12-29 21:02:06 -07:00
<ui-card>
2019-02-17 19:13:56 -07:00
<template #title><fa icon="palette"/> {{ $t('theme') }}</template>
2018-12-29 21:02:06 -07:00
<section class="nicnklzforebnpfgasiypmpdaaglujqm fit-top">
<div class="dark">
<div class="toggleWrapper">
<input type="checkbox" class="dn" id="dn" v-model="darkmode"/>
<label for="dn" class="toggle">
<span class="toggle__handler">
<span class="crater crater--1"></span>
<span class="crater crater--2"></span>
<span class="crater crater--3"></span>
</span>
<span class="star star--1"></span>
<span class="star star--2"></span>
<span class="star star--3"></span>
<span class="star star--4"></span>
<span class="star star--5"></span>
<span class="star star--6"></span>
</label>
</div>
</div>
2018-12-29 21:02:06 -07:00
<label>
<ui-select v-model="light" :placeholder="$t('light-theme')">
2019-02-17 19:13:56 -07:00
<template #label><fa :icon="faSun"/> {{ $t('light-theme') }}</template>
2018-12-29 21:02:06 -07:00
<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')">
2019-02-17 19:13:56 -07:00
<template #label><fa :icon="faMoon"/> {{ $t('dark-theme') }}</template>
2018-12-29 21:02:06 -07:00
<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>
2018-10-02 01:10:45 -06:00
</ui-textarea>
2018-12-29 21:02:06 -07:00
<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>
2018-09-28 09:01:11 -06:00
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../../i18n';
import { lightTheme, darkTheme, builtinThemes, applyTheme, Theme } from '../../../../theme';
2018-09-28 09:01:11 -06:00
import { Chrome } from 'vue-color';
import * as uuid from 'uuid';
import * as tinycolor from 'tinycolor2';
2018-10-02 01:04:31 -06:00
import * as JSON5 from 'json5';
2018-11-14 09:43:06 -07:00
import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons';
2018-10-02 01:04:31 -06:00
// 後方互換性のため
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;
}
2018-09-28 09:01:11 -06:00
export default Vue.extend({
i18n: i18n('common/views/components/theme.vue'),
2018-09-28 09:01:11 -06:00
components: {
ColorPicker: Chrome
},
data() {
return {
2018-10-08 15:46:52 -06:00
builtinThemes: builtinThemes,
2018-09-28 09:01:11 -06:00
installThemeCode: null,
2018-10-08 15:46:52 -06:00
selectedThemeId: null,
2018-09-28 09:01:11 -06:00
myThemeBase: 'light',
myThemeName: '',
2018-10-02 12:07:46 -06:00
myThemeDesc: '',
2018-10-02 01:04:31 -06:00
myThemePrimary: lightTheme.vars.primary,
myThemeSecondary: lightTheme.vars.secondary,
2018-11-14 09:43:06 -07:00
myThemeText: lightTheme.vars.text,
faMoon, faSun
2018-09-28 09:01:11 -06:00
};
},
computed: {
2018-10-02 01:04:31 -06:00
themes(): Theme[] {
2018-10-08 00:23:10 -06:00
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');
2018-09-28 09:01:11 -06:00
},
2018-10-02 01:04:31 -06:00
installedThemes(): Theme[] {
2018-09-28 09:01:11 -06:00
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 }); }
},
2018-10-08 15:46:52 -06:00
selectedTheme() {
if (this.selectedThemeId == null) return null;
return this.themes.find(x => x.id == this.selectedThemeId);
2018-10-02 01:10:45 -06:00
},
2018-10-08 15:46:52 -06:00
selectedThemeCode() {
if (this.selectedTheme == null) return null;
return JSON5.stringify(this.selectedTheme, null, '\t');
2018-09-28 09:01:11 -06:00
},
myTheme(): any {
return {
2018-10-02 01:04:31 -06:00
name: this.myThemeName,
2018-10-02 01:10:45 -06:00
author: this.$store.state.i.username,
2018-10-02 12:07:46 -06:00
desc: this.myThemeDesc,
2018-10-02 01:04:31 -06:00
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()
2018-09-28 09:01:11 -06:00
}
};
},
darkmode: {
get() { return this.$store.state.device.darkmode; },
set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); }
},
2018-09-28 09:01:11 -06:00
},
watch: {
myThemeBase(v) {
const theme = v == 'light' ? lightTheme : darkTheme;
2018-10-02 01:04:31 -06:00
this.myThemePrimary = theme.vars.primary;
this.myThemeSecondary = theme.vars.secondary;
this.myThemeText = theme.vars.text;
2018-09-28 09:01:11 -06:00
}
},
2018-10-02 01:04:31 -06:00
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;
}
})
});
},
2018-09-28 09:01:11 -06:00
methods: {
2018-10-02 11:57:31 -06:00
install(code) {
2018-10-02 01:04:31 -06:00
let theme;
try {
2018-10-02 11:57:31 -06:00
theme = JSON5.parse(code);
2018-10-02 01:04:31 -06:00
} catch (e) {
2018-12-01 23:28:52 -07:00
this.$root.dialog({
2018-10-10 06:23:38 -06:00
type: 'error',
text: this.$t('invalid-theme')
2018-10-10 06:23:38 -06:00
});
2018-09-28 09:01:11 -06:00
return;
}
2018-10-02 01:04:31 -06:00
// 後方互換性のため
if (theme.id == null && theme.meta != null) {
theme = convertOldThemedefinition(theme);
}
if (theme.id == null) {
2018-12-01 23:28:52 -07:00
this.$root.dialog({
2018-10-10 06:23:38 -06:00
type: 'error',
text: this.$t('invalid-theme')
2018-10-10 06:23:38 -06:00
});
2018-10-02 01:04:31 -06:00
return;
}
if (this.$store.state.device.themes.some(t => t.id == theme.id)) {
2018-12-01 23:28:52 -07:00
this.$root.dialog({
2018-10-10 06:23:38 -06:00
type: 'info',
text: this.$t('already-installed')
2018-10-10 06:23:38 -06:00
});
2018-09-28 09:01:11 -06:00
return;
}
2018-10-02 01:04:31 -06:00
2018-09-28 09:01:11 -06:00
const themes = this.$store.state.device.themes.concat(theme);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
2018-10-02 01:04:31 -06:00
2018-12-01 23:28:52 -07:00
this.$root.dialog({
2018-10-10 06:23:38 -06:00
type: 'success',
text: this.$t('installed').replace('{}', theme.name)
2018-10-10 06:23:38 -06:00
});
},
uninstall() {
2018-10-08 15:46:52 -06:00
const theme = this.selectedTheme;
2018-10-02 01:04:31 -06:00
const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
2018-10-10 06:23:38 -06:00
2018-12-01 23:28:52 -07:00
this.$root.dialog({
2018-10-10 06:23:38 -06:00
type: 'info',
text: this.$t('uninstalled').replace('{}', theme.name)
2018-10-10 06:23:38 -06:00
});
2018-09-28 09:01:11 -06:00
},
2018-10-02 11:57:31 -06:00
import_() {
(this.$refs.file as any).click();
}
export_() {
2018-10-08 15:46:52 -06:00
const blob = new Blob([this.selectedThemeCode], {
2018-10-02 11:57:31 -06:00
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);
},
2018-09-28 09:01:11 -06:00
preview() {
applyTheme(this.myTheme, false);
},
gen() {
const theme = this.myTheme;
2018-10-10 06:23:38 -06:00
2018-10-02 12:07:46 -06:00
if (theme.name == null || theme.name.trim() == '') {
2018-12-01 23:28:52 -07:00
this.$root.dialog({
2018-10-10 06:23:38 -06:00
type: 'warning',
text: this.$t('theme-name-required')
2018-10-10 06:23:38 -06:00
});
2018-10-02 12:07:46 -06:00
return;
}
2018-10-10 06:23:38 -06:00
2018-10-02 01:04:31 -06:00
theme.id = uuid();
2018-10-10 06:23:38 -06:00
2018-09-28 09:01:11 -06:00
const themes = this.$store.state.device.themes.concat(theme);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
2018-10-10 06:23:38 -06:00
2018-12-01 23:28:52 -07:00
this.$root.dialog({
2018-10-10 06:23:38 -06:00
type: 'success',
text: this.$t('saved')
2018-10-10 06:23:38 -06:00
});
2018-09-28 09:01:11 -06:00
}
}
});
</script>
<style lang="stylus" scoped>
.nicnklzforebnpfgasiypmpdaaglujqm
> .dark
margin-top 48px
margin-bottom 110px
.toggleWrapper {
position: absolute;
top: 50%;
left: 50%;
overflow: hidden;
padding: 0 200px;
transform: translate3d(-50%, -50%, 0);
input {
position: absolute;
left: -99em;
}
}
.toggle {
cursor: pointer;
display: inline-block;
position: relative;
width: 90px;
height: 50px;
background-color: #83D8FF;
border-radius: 90px - 6;
transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
&:before {
content: 'Light';
position: absolute;
left: -60px;
top: 15px;
font-size: 18px;
color: var(--primary);
}
&:after {
content: 'Dark';
position: absolute;
right: -58px;
top: 15px;
font-size: 18px;
color: var(--text);
}
}
.toggle__handler {
display: inline-block;
position: relative;
z-index: 1;
top: 3px;
left: 3px;
width: 50px - 6;
height: 50px - 6;
background-color: #FFCF96;
border-radius: 50px;
box-shadow: 0 2px 6px rgba(0,0,0,.3);
transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55);
transform: rotate(-45deg);
.crater {
position: absolute;
background-color: #E8CDA5;
opacity: 0;
transition: opacity 200ms ease-in-out;
border-radius: 100%;
}
.crater--1 {
top: 18px;
left: 10px;
width: 4px;
height: 4px;
}
.crater--2 {
top: 28px;
left: 22px;
width: 6px;
height: 6px;
}
.crater--3 {
top: 10px;
left: 25px;
width: 8px;
height: 8px;
}
}
.star {
position: absolute;
background-color: #ffffff;
transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
border-radius: 50%;
}
.star--1 {
top: 10px;
left: 35px;
z-index: 0;
width: 30px;
height: 3px;
}
.star--2 {
top: 18px;
left: 28px;
z-index: 1;
width: 30px;
height: 3px;
}
.star--3 {
top: 27px;
left: 40px;
z-index: 0;
width: 30px;
height: 3px;
}
.star--4,
.star--5,
.star--6 {
opacity: 0;
transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95);
}
.star--4 {
top: 16px;
left: 11px;
z-index: 0;
width: 2px;
height: 2px;
transform: translate3d(3px,0,0);
}
.star--5 {
top: 32px;
left: 17px;
z-index: 0;
width: 3px;
height: 3px;
transform: translate3d(3px,0,0);
}
.star--6 {
top: 36px;
left: 28px;
z-index: 0;
width: 2px;
height: 2px;
transform: translate3d(3px,0,0);
}
input:checked {
+ .toggle {
background-color: #749DD6;
&:before {
color: var(--text);
}
&:after {
color: var(--primary);
}
.toggle__handler {
background-color: #FFE5B5;
transform: translate3d(40px, 0, 0) rotate(0);
.crater { opacity: 1; }
}
.star--1 {
width: 2px;
height: 2px;
}
.star--2 {
width: 4px;
height: 4px;
transform: translate3d(-5px, 0, 0);
}
.star--3 {
width: 2px;
height: 2px;
transform: translate3d(-7px, 0, 0);
}
.star--4,
.star--5,
.star--6 {
opacity: 1;
transform: translate3d(0,0,0);
}
.star--4 {
transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
}
.star--5 {
transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
}
.star--6 {
transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95);
}
}
}
2018-12-29 21:02:06 -07:00
> a
display block
margin-top -16px
margin-bottom 16px
2018-10-02 01:23:55 -06:00
> details
2018-12-29 22:00:57 -07:00
border-top solid var(--lineWidth) var(--faceDivider)
2018-10-02 01:23:55 -06:00
2018-10-02 11:57:31 -06:00
> summary
padding 16px 0
> *:last-child
margin-bottom 16px
2018-09-28 09:01:11 -06:00
> .creator
> div
padding 16px 0
2018-12-29 22:00:57 -07:00
border-bottom solid var(--lineWidth) var(--faceDivider)
2018-09-28 09:01:11 -06:00
</style>