diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 6eb90c9d02..40772f9b1f 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -466,6 +466,17 @@ details: "詳細" chooseEmoji: "絵文字を選択" unableToProcess: "操作を完了できません" recentUsed: "最近使用" +install: "インストール" +uninstall: "アンインストール" + +_theme: + explore: "テーマを探す" + install: "テーマのインストール" + manage: "テーマの管理" + code: "テーマコード" + installed: "{name}をインストールしました" + alreadyInstalled: "そのテーマは既にインストールされています" + invalid: "テーマの形式が間違っています" _sfx: note: "ノート" diff --git a/src/client/components/error.vue b/src/client/components/error.vue index 7446a7cb5d..dd9de43c16 100644 --- a/src/client/components/error.vue +++ b/src/client/components/error.vue @@ -1,6 +1,6 @@ <template> <div class="mjndxjcg _panel"> - <img src="https://xn--931a.moe/assets/error.png" class="_ghost"/> + <img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/> <p><fa :icon="faExclamationTriangle"/> {{ $t('error') }}</p> <mk-button @click="() => $emit('retry')" class="button">{{ $t('retry') }}</mk-button> </div> diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue index bc2ae8472c..65dda17575 100644 --- a/src/client/components/notes.vue +++ b/src/client/components/notes.vue @@ -1,7 +1,7 @@ <template> <div class="mk-notes" v-size="[{ max: 500 }]"> <div class="empty" v-if="empty"> - <img src="https://xn--931a.moe/assets/info.png" class="_ghost"/> + <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <div>{{ $t('noNotes') }}</div> </div> diff --git a/src/client/init.ts b/src/client/init.ts index c7587afb8c..b9c6aedae4 100644 --- a/src/client/init.ts +++ b/src/client/init.ts @@ -178,6 +178,7 @@ os.init(async () => { }, watch: { '$store.state.device.darkMode'() { + // TODO: このファイルでbuiltinThemesを参照するとcode splittingが効かず、初回読み込み時に全てのテーマコードを読み込むことになってしまい無駄なので何とかする const themes = builtinThemes.concat(this.$store.state.device.themes); applyTheme(themes.find(x => x.id === (this.$store.state.device.darkMode ? this.$store.state.device.darkTheme : this.$store.state.device.lightTheme))); } diff --git a/src/client/pages/follow-requests.vue b/src/client/pages/follow-requests.vue index 14d60a12ec..a900bf735c 100644 --- a/src/client/pages/follow-requests.vue +++ b/src/client/pages/follow-requests.vue @@ -6,7 +6,7 @@ <mk-pagination :pagination="pagination" class="mk-follow-requests" ref="list"> <template #empty> <div class="tkdrhpxr"> - <img src="https://xn--931a.moe/assets/info.png" class="_ghost"/> + <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <div>{{ $t('noFollowRequests') }}</div> </div> </template> diff --git a/src/client/pages/messaging/index.vue b/src/client/pages/messaging/index.vue index 702979a098..ed24f8ef54 100644 --- a/src/client/pages/messaging/index.vue +++ b/src/client/pages/messaging/index.vue @@ -32,7 +32,7 @@ </router-link> </div> <div class="no-history" v-if="!fetching && messages.length == 0"> - <img src="https://xn--931a.moe/assets/info.png" class="_ghost"/> + <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <div>{{ $t('noHistory') }}</div> </div> <mk-loading v-if="fetching"/> diff --git a/src/client/pages/not-found.vue b/src/client/pages/not-found.vue index 6ddbd1932b..9608e07786 100644 --- a/src/client/pages/not-found.vue +++ b/src/client/pages/not-found.vue @@ -5,7 +5,7 @@ <section class="_card"> <div class="_content"> - <img src="https://xn--931a.moe/assets/not-found.png" class="_ghost"/> + <img src="https://xn--931a.moe/assets/not-found.jpg" class="_ghost"/> <div>{{ $t('notFoundDescription') }}</div> </div> </section> diff --git a/src/client/pages/preferences/theme.vue b/src/client/pages/preferences/theme.vue index 488935a0cd..fcea457396 100644 --- a/src/client/pages/preferences/theme.vue +++ b/src/client/pages/preferences/theme.vue @@ -42,6 +42,7 @@ <option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option> </optgroup> </mk-select> + <a href="https://assets.msky.cafe/theme/list" rel="noopener" target="_blank" class="_link">{{ $t('_theme.explore') }}</a> </div> <div class="_content"> <mk-switch v-model="syncDeviceDarkMode">{{ $t('syncDeviceDarkMode') }}</mk-switch> @@ -50,18 +51,43 @@ <mk-button primary v-if="wallpaper == null" @click="setWallpaper">{{ $t('setWallpaper') }}</mk-button> <mk-button primary v-else @click="wallpaper = null">{{ $t('removeWallpaper') }}</mk-button> </div> + <div class="_content"> + <details> + <summary><fa :icon="faDownload"/> {{ $t('_theme.install') }}</summary> + <mk-textarea v-model="installThemeCode"> + <span>{{ $t('_theme.code') }}</span> + </mk-textarea> + <mk-button @click="() => install(this.installThemeCode)" :disabled="installThemeCode == null"><fa :icon="faCheck"/> {{ $t('install') }}</mk-button> + </details> + </div> + <div class="_content"> + <details> + <summary><fa :icon="faFolderOpen"/> {{ $t('_theme.manage') }}</summary> + <mk-select v-model="selectedThemeId"> + <option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option> + </mk-select> + <template v-if="selectedTheme"> + <mk-textarea readonly tall :value="selectedThemeCode"> + <span>{{ $t('_theme.code') }}</span> + </mk-textarea> + <mk-button @click="uninstall()" v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</mk-button> + </template> + </details> + </div> </section> </template> <script lang="ts"> import Vue from 'vue'; -import { faPalette } from '@fortawesome/free-solid-svg-icons'; +import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; +import * as JSON5 from 'json5'; import MkInput from '../../components/ui/input.vue'; import MkButton from '../../components/ui/button.vue'; import MkSelect from '../../components/ui/select.vue'; import MkSwitch from '../../components/ui/switch.vue'; +import MkTextarea from '../../components/ui/textarea.vue'; import i18n from '../../i18n'; -import { Theme, builtinThemes, applyTheme } from '../../theme'; +import { Theme, builtinThemes, applyTheme, validateTheme } from '../../theme'; import { selectFile } from '../../scripts/select-file'; import { isDeviceDarkmode } from '../../scripts/is-device-darkmode'; @@ -73,12 +99,16 @@ export default Vue.extend({ MkButton, MkSelect, MkSwitch, + MkTextarea, }, data() { return { + builtinThemes, + installThemeCode: null, + selectedThemeId: null, wallpaper: localStorage.getItem('wallpaper'), - faPalette + faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt } }, @@ -118,6 +148,16 @@ export default Vue.extend({ get() { return this.$store.state.device.syncDeviceDarkMode; }, set(value) { this.$store.commit('device/set', { key: 'syncDeviceDarkMode', 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'); + }, }, watch: { @@ -155,6 +195,53 @@ export default Vue.extend({ this.wallpaper = file.url; }); }, + + install(code) { + let theme; + try { + theme = JSON5.parse(code); + } catch (e) { + this.$root.dialog({ + type: 'error', + text: this.$t('_theme.invalid') + }); + return; + } + if (!validateTheme(theme)) { + this.$root.dialog({ + type: 'error', + text: this.$t('_theme.invalid') + }); + return; + } + if (this.$store.state.device.themes.some(t => t.id === theme.id)) { + this.$root.dialog({ + type: 'info', + text: this.$t('_theme.alreadyInstalled') + }); + 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('_theme.installed', { name: 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', + iconOnly: true, autoClose: true + }); + }, } }); </script> @@ -179,7 +266,7 @@ export default Vue.extend({ top: 50%; left: 50%; overflow: hidden; - padding: 0 200px; + padding: 0 100px; transform: translate3d(-50%, -50%, 0); input { diff --git a/src/client/theme.ts b/src/client/theme.ts index 2a6adbffcc..e90c1f3a3b 100644 --- a/src/client/theme.ts +++ b/src/client/theme.ts @@ -102,3 +102,10 @@ function compile(theme: Theme): { [key: string]: string } { function genValue(c: tinycolor.Instance): string { return c.toRgbString(); } + +export function validateTheme(theme: Record<string, any>): boolean { + if (theme.id == null) return false; + if (theme.name == null) return false; + if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false; + return true; +} diff --git a/src/server/web/views/base.pug b/src/server/web/views/base.pug index e6751ecca2..76114e6f5a 100644 --- a/src/server/web/views/base.pug +++ b/src/server/web/views/base.pug @@ -16,9 +16,9 @@ html link(rel='icon' href= icon || '/favicon.ico') link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png') link(rel='manifest' href='/manifest.json') - link(rel='prefetch' href='https://xn--931a.moe/assets/info.png') - link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.png') - link(rel='prefetch' href='https://xn--931a.moe/assets/error.png') + link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg') + link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg') + link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg') title block title