From 6a82e94c5489d4879cbbf86091cd15c7d144f284 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Sat, 29 Sep 2018 00:01:11 +0900 Subject: [PATCH] wip --- locales/ja-JP.yml | 24 +++ package.json | 1 + src/client/app/app.vue | 12 +- .../app/common/views/components/avatar.vue | 4 +- .../app/common/views/components/index.ts | 2 + .../app/common/views/components/theme.vue | 179 ++++++++++++++++++ .../app/common/views/components/ui/button.vue | 12 +- .../app/desktop/views/components/settings.vue | 5 + src/client/app/init.ts | 35 +++- .../app/mobile/views/pages/settings.vue | 7 + src/client/app/store.ts | 3 + src/client/app/{common/scripts => }/theme.ts | 27 ++- src/client/theme/dark.json | 2 +- src/client/theme/halloween.json | 3 +- src/client/theme/light.json | 2 +- 15 files changed, 287 insertions(+), 31 deletions(-) create mode 100644 src/client/app/common/views/components/theme.vue rename src/client/app/{common/scripts => }/theme.ts (74%) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 0f83ba8419..46dea949d2 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -285,6 +285,28 @@ common/views/components/media-banner.vue: sensitive: "閲覧注æ„" click-to-show: "クリックã—ã¦è¡¨ç¤º" +common/views/components/theme.vue: + light-theme: "éžãƒ€ãƒ¼ã‚¯ãƒ¢ãƒ¼ãƒ‰æ™‚ã«ä½¿ç”¨ã™ã‚‹ãƒ†ãƒ¼ãƒž" + dark-theme: "ダークモード時ã«ä½¿ç”¨ã™ã‚‹ãƒ†ãƒ¼ãƒž" + install-a-theme: "テーマã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«" + theme-code: "テーマコード" + install: "インストール" + create-a-theme: "テーマã®ä½œæˆ" + save-created-theme: "テーマをä¿å˜" + primary-color: "プライマリ カラー" + secondary-color: "セカンダリ カラー" + text-color: "æ–‡å—色" + base-theme: "ベーステーマ" + base-theme-light: "Light" + base-theme-dark: "Dark" + theme-name: "テーマå" + preview-created-theme: "プレビュー" + invalid-theme: "テーマãŒæ£ã—ãã‚ã‚Šã¾ã›ã‚“。" + already-installed: "æ—¢ã«ãã®ãƒ†ãƒ¼ãƒžã¯ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•ã‚Œã¦ã„ã¾ã™ã€‚" + saved: "ä¿å˜ã—ã¾ã—ãŸ" + installed-themes: "インストールã•ã‚ŒãŸãƒ†ãƒ¼ãƒž" + select-theme: "テーマをé¸æŠžã—ã¦ãã ã•ã„" + common/views/components/cw-button.vue: hide: "éš ã™" show: "ã‚‚ã£ã¨è¦‹ã‚‹" @@ -762,6 +784,7 @@ desktop/views/components/settings.vue: 2fa: "二段階èªè¨¼" other: "ãã®ä»–" license: "ライセンス" + theme: "テーマ" behaviour: "動作" fetch-on-scroll: "スクãƒãƒ¼ãƒ«ã§è‡ªå‹•èªã¿è¾¼ã¿" @@ -1417,6 +1440,7 @@ mobile/views/pages/settings.vue: notification-position: "通知ã®è¡¨ç¤º" notification-position-bottom: "下" notification-position-top: "上" + theme: "テーマ" behavior: "動作" fetch-on-scroll: "スクãƒãƒ¼ãƒ«ã§è‡ªå‹•èªã¿è¾¼ã¿" note-visibility: "投稿ã®å…¬é–‹ç¯„囲" diff --git a/package.json b/package.json index 347e9d0c24..e19283cf64 100644 --- a/package.json +++ b/package.json @@ -208,6 +208,7 @@ "v-animate-css": "0.0.2", "vue": "2.5.17", "vue-chartjs": "3.4.0", + "vue-color": "2.6.0", "vue-cropperjs": "2.2.2", "vue-js-modal": "1.3.26", "vue-json-tree-view": "2.1.4", diff --git a/src/client/app/app.vue b/src/client/app/app.vue index 9b6af27ece..778e9f29cf 100644 --- a/src/client/app/app.vue +++ b/src/client/app/app.vue @@ -14,8 +14,7 @@ export default Vue.extend({ keymap(): any { return { 'h|slash': this.help, - 'd': this.dark, - 'x': this.test + 'd': this.dark }; } }, @@ -26,11 +25,10 @@ export default Vue.extend({ }, dark() { - applyTheme(darkTheme); - }, - - test() { - applyTheme(halloweenTheme); + this.$store.commit('device/set', { + key: 'darkmode', + value: !this.$store.state.device.darkmode + }); } } }); diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue index ca09af87de..ac018abcfc 100644 --- a/src/client/app/common/views/components/avatar.vue +++ b/src/client/app/common/views/components/avatar.vue @@ -59,7 +59,9 @@ export default Vue.extend({ } }, mounted() { - this.$el.style.color = `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`; + if (this.user.avatarColor) { + this.$el.style.color = `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`; + } }, methods: { onClick(e) { diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts index 4c1c0afa80..0dea38a7a1 100644 --- a/src/client/app/common/views/components/index.ts +++ b/src/client/app/common/views/components/index.ts @@ -1,5 +1,6 @@ import Vue from 'vue'; +import theme from './theme.vue'; import instance from './instance.vue'; import cwButton from './cw-button.vue'; import tagCloud from './tag-cloud.vue'; @@ -43,6 +44,7 @@ import uiSelect from './ui/select.vue'; import formButton from './ui/form/button.vue'; import formRadio from './ui/form/radio.vue'; +Vue.component('mk-theme', theme); Vue.component('mk-instance', instance); Vue.component('mk-cw-button', cwButton); Vue.component('mk-tag-cloud', tagCloud); diff --git a/src/client/app/common/views/components/theme.vue b/src/client/app/common/views/components/theme.vue new file mode 100644 index 0000000000..27888d1e85 --- /dev/null +++ b/src/client/app/common/views/components/theme.vue @@ -0,0 +1,179 @@ +<template> +<div class="nicnklzforebnpfgasiypmpdaaglujqm"> + <label> + <span>%i18n:@light-theme%</span> + <ui-select v-model="light" placeholder="%i18n:@light-theme%"> + <option v-for="x in themes" :value="x.meta.id" :key="x.meta.id">{{ x.meta.name }}</option> + </ui-select> + </label> + + <label> + <span>%i18n:@dark-theme%</span> + <ui-select v-model="dark" placeholder="%i18n:@dark-theme%"> + <option v-for="x in themes" :value="x.meta.id" :key="x.meta.id">{{ x.meta.name }}</option> + </ui-select> + </label> + + <details class="creator"> + <summary>%i18n:@create-a-theme%</summary> + <div> + <span>%i18n:@base-theme%:</span> + <ui-radio v-model="myThemeBase" value="light">%i18n:@base-theme-light%</ui-radio> + <ui-radio v-model="myThemeBase" value="dark">%i18n:@base-theme-dark%</ui-radio> + </div> + <div> + <ui-input v-model="myThemeName"> + <span>%i18n:@theme-name%</span> + </ui-input> + </div> + <div> + <div style="padding-bottom:8px;">%i18n:@primary-color%:</div> + <color-picker v-model="myThemePrimary"/> + </div> + <div> + <div style="padding-bottom:8px;">%i18n:@secondary-color%:</div> + <color-picker v-model="myThemeSecondary"/> + </div> + <div> + <div style="padding-bottom:8px;">%i18n:@text-color%:</div> + <color-picker v-model="myThemeText"/> + </div> + <ui-button @click="preview()">%i18n:@preview-created-theme%</ui-button> + <ui-button primary @click="gen()">%i18n:@save-created-theme%</ui-button> + </details> + + <details> + <summary>%i18n:@install-a-theme%</summary> + <ui-textarea v-model="installThemeCode"> + <span>%i18n:@theme-code%</span> + </ui-textarea> + <ui-button @click="install()">%i18n:@install%</ui-button> + </details> + + <details> + <summary>%i18n:@installed-themes%</summary> + <ui-select v-model="selectedInstalledTheme" placeholder="%i18n:@select-theme%"> + <option v-for="x in installedThemes" :value="x.meta.id" :key="x.meta.id">{{ x.meta.name }}</option> + </ui-select> + <ui-textarea readonly :value="selectedInstalledThemeCode"> + <span>%i18n:@theme-code%</span> + </ui-textarea> + </details> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { apiUrl, docsUrl } from '../../../config'; +import { lightTheme, darkTheme, builtinThemes, applyTheme } from '../../../theme'; +import { Chrome } from 'vue-color'; +import * as uuid from 'uuid'; +import * as tinycolor from 'tinycolor2'; + +export default Vue.extend({ + components: { + ColorPicker: Chrome + }, + + data() { + return { + installThemeCode: null, + selectedInstalledTheme: null, + myThemeBase: 'light', + myThemeName: '', + myThemePrimary: lightTheme.meta.vars.primary, + myThemeSecondary: lightTheme.meta.vars.secondary, + myThemeText: lightTheme.meta.vars.text + }; + }, + + computed: { + themes(): any { + return this.$store.state.device.themes.concat(builtinThemes); + }, + + installedThemes(): any { + 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 }); } + }, + + selectedInstalledThemeCode() { + if (this.selectedInstalledTheme == null) return null; + return JSON.stringify(this.installedThemes.find(x => x.meta.id == this.selectedInstalledTheme)); + }, + + myTheme(): any { + return { + meta: { + name: this.myThemeName, + author: this.$store.state.i.name, + 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.meta.vars.primary; + this.myThemeSecondary = theme.meta.vars.secondary; + this.myThemeText = theme.meta.vars.text; + } + }, + + methods: { + install() { + const theme = JSON.parse(this.installThemeCode); + if (theme.meta == null || theme.meta.id == null) { + alert('%i18n:@invalid-theme%'); + return; + } + if (this.$store.state.device.themes.some(t => t.meta.id == theme.meta.id)) { + alert('%i18n:@already-installed%'); + return; + } + const themes = this.$store.state.device.themes.concat(theme); + this.$store.commit('device/set', { + key: 'themes', value: themes + }); + }, + + preview() { + applyTheme(this.myTheme, false); + }, + + gen() { + const theme = this.myTheme; + theme.meta.id = uuid(); + const themes = this.$store.state.device.themes.concat(theme); + this.$store.commit('device/set', { + key: 'themes', value: themes + }); + alert('%i18n:@saved%'); + } + } +}); +</script> + +<style lang="stylus" scoped> +.nicnklzforebnpfgasiypmpdaaglujqm + > .creator + > div + padding 16px 0 + border-bottom solid 1px var(--faceDivider) +</style> diff --git a/src/client/app/common/views/components/ui/button.vue b/src/client/app/common/views/components/ui/button.vue index a165d100a4..47644b32b5 100644 --- a/src/client/app/common/views/components/ui/button.vue +++ b/src/client/app/common/views/components/ui/button.vue @@ -27,14 +27,6 @@ export default Vue.extend({ return { styl: 'fill' }; - }, - inject: { - isCardChild: { default: false } - }, - created() { - if (this.isCardChild) { - this.styl = 'line'; - } } }); </script> @@ -43,6 +35,7 @@ export default Vue.extend({ .dmtdnykelhudezerjlfpbhgovrgnqqgr display block width 100% + min-height 40px margin 0 padding 0 font-weight normal @@ -52,6 +45,9 @@ export default Vue.extend({ outline none box-shadow none + &:not(.inline) + .dmtdnykelhudezerjlfpbhgovrgnqqgr + margin-top 16px + &.inline display inline-block width auto diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue index c7d82590ea..1cb8d4d4c8 100644 --- a/src/client/app/desktop/views/components/settings.vue +++ b/src/client/app/desktop/views/components/settings.vue @@ -19,6 +19,11 @@ <x-profile/> </section> + <section class="web" v-show="page == 'web'"> + <h1>%i18n:@theme%</h1> + <mk-theme/> + </section> + <section class="web" v-show="page == 'web'"> <h1>%i18n:@behaviour%</h1> <ui-switch v-model="fetchOnScroll"> diff --git a/src/client/app/init.ts b/src/client/app/init.ts index 8d430ad7ff..802f7b42eb 100644 --- a/src/client/app/init.ts +++ b/src/client/app/init.ts @@ -14,11 +14,11 @@ import App from './app.vue'; import checkForUpdate from './common/scripts/check-for-update'; import MiOS, { API } from './mios'; import { version, codename, lang } from './config'; -import applyTheme from './common/scripts/theme'; -const defaultTheme = require('../theme/light.json'); +import { builtinThemes, applyTheme } from './theme'; +const lightTheme = require('../theme/light.json'); if (localStorage.getItem('theme') == null) { - applyTheme(defaultTheme); + applyTheme(lightTheme); } Vue.use(Vuex); @@ -92,6 +92,35 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API) const launch = (router: VueRouter, api?: (os: MiOS) => API) => { os.apis = api ? api(os) : null; + //#region theme + os.store.watch(s => { + return s.device.darkmode; + }, v => { + const themes = os.store.state.device.themes.concat(builtinThemes); + const dark = themes.find(t => t.meta.id == os.store.state.device.darkTheme); + const light = themes.find(t => t.meta.id == os.store.state.device.lightTheme); + applyTheme(v ? dark : light); + }); + os.store.watch(s => { + return s.device.lightTheme; + }, v => { + const themes = os.store.state.device.themes.concat(builtinThemes); + const theme = themes.find(t => t.meta.id == v); + if (!os.store.state.device.darkmode) { + applyTheme(theme); + } + }); + os.store.watch(s => { + return s.device.darkTheme; + }, v => { + const themes = os.store.state.device.themes.concat(builtinThemes); + const theme = themes.find(t => t.meta.id == v); + if (os.store.state.device.darkmode) { + applyTheme(theme); + } + }); + //#endregion + //#region shadow const shadow = '0 3px 8px rgba(0, 0, 0, 0.2)'; if (os.store.state.settings.useShadow) document.documentElement.style.setProperty('--shadow', shadow); diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue index b83eaf6d33..94fa38cec9 100644 --- a/src/client/app/mobile/views/pages/settings.vue +++ b/src/client/app/mobile/views/pages/settings.vue @@ -23,6 +23,13 @@ <ui-switch v-model="games_reversi_useContrastStones">%i18n:common.use-contrast-reversi-stones%</ui-switch> </section> + <section> + <header>%i18n:@theme%</header> + <div> + <mk-theme/> + </div> + </section> + <section> <header>%i18n:@timeline%</header> <div> diff --git a/src/client/app/store.ts b/src/client/app/store.ts index fbcc53d7be..545261225a 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -44,6 +44,9 @@ const defaultDeviceSettings = { apiViaStream: true, autoPopout: false, darkmode: false, + darkTheme: 'dark', + lightTheme: 'light', + themes: [], enableSounds: true, soundVolume: 0.5, lang: null, diff --git a/src/client/app/common/scripts/theme.ts b/src/client/app/theme.ts similarity index 74% rename from src/client/app/common/scripts/theme.ts rename to src/client/app/theme.ts index 7a1c6abb76..1147ff300d 100644 --- a/src/client/app/common/scripts/theme.ts +++ b/src/client/app/theme.ts @@ -1,22 +1,21 @@ import * as tinycolor from 'tinycolor2'; -const lightTheme = require('../../../theme/light'); -const darkTheme = require('../../../theme/dark'); type Theme = { meta: { id: string; name: string; - inherit: string; + author: string; + base?: string; vars: any; }; } & { [key: string]: string; }; -export default function(theme: Theme) { - if (theme.meta.inherit) { - const inherit = [lightTheme, darkTheme].find(x => x.meta.id == theme.meta.inherit); - theme = Object.assign({}, inherit, theme); +export function applyTheme(theme: Theme, persisted = true) { + if (theme.meta.base) { + const base = [lightTheme, darkTheme].find(x => x.meta.id == theme.meta.base); + theme = Object.assign({}, base, theme); } const props = compile(theme); @@ -26,7 +25,9 @@ export default function(theme: Theme) { document.documentElement.style.setProperty(`--${k}`, v.toString()); }); - localStorage.setItem('theme', JSON.stringify(props)); + if (persisted) { + localStorage.setItem('theme', JSON.stringify(props)); + } } function compile(theme: Theme): { [key: string]: string } { @@ -87,3 +88,13 @@ function compile(theme: Theme): { [key: string]: string } { function genValue(c: tinycolor.Instance): string { return c.toRgbString(); } + +export const lightTheme = require('../theme/light.json'); +export const darkTheme = require('../theme/dark.json'); +export const halloweenTheme = require('../theme/halloween.json'); + +export const builtinThemes = [ + lightTheme, + darkTheme, + halloweenTheme +]; diff --git a/src/client/theme/dark.json b/src/client/theme/dark.json index 015225ddab..74447b8f2f 100644 --- a/src/client/theme/dark.json +++ b/src/client/theme/dark.json @@ -1,6 +1,6 @@ { "meta": { - "id": "9978f7f9-5616-44fd-a704-cc5985efdd63", + "id": "dark", "name": "Dark", "author": "syuilo", "vars": { diff --git a/src/client/theme/halloween.json b/src/client/theme/halloween.json index 6e92db95ff..fb34db57a8 100644 --- a/src/client/theme/halloween.json +++ b/src/client/theme/halloween.json @@ -3,10 +3,9 @@ "id": "42e4f09b-67d5-498c-af7d-29faa54745b0", "name": "Halloween", "author": "syuilo", - "inherit": "9978f7f9-5616-44fd-a704-cc5985efdd63", + "base": "dark", "vars": { "primary": "#d67036", - "primaryForeground": "#fff", "secondary": "#1f1d30", "text": "#b1bee3" } diff --git a/src/client/theme/light.json b/src/client/theme/light.json index 3d131f066a..1b6604e642 100644 --- a/src/client/theme/light.json +++ b/src/client/theme/light.json @@ -1,6 +1,6 @@ { "meta": { - "id": "406cfea3-a4e7-486c-9057-30ede1353c3f", + "id": "light", "name": "Light", "author": "syuilo", "vars": { -- GitLab