wip
This commit is contained in:
parent
4fce5d8066
commit
9d81d06853
26 changed files with 844 additions and 174 deletions
|
@ -1,7 +1,9 @@
|
|||
{
|
||||
"globals": {
|
||||
"_DEV_": false,
|
||||
"_LANG_": false,
|
||||
"_LANGS_": false,
|
||||
"_LOCALE_": false,
|
||||
"_VERSION_": false,
|
||||
"_ENV_": false,
|
||||
"_PERF_PREFIX_": false,
|
||||
|
|
4
src/client/@types/global.d.ts
vendored
4
src/client/@types/global.d.ts
vendored
|
@ -1,4 +1,6 @@
|
|||
declare const _LANGS_: string[];
|
||||
declare const _LANG_: string;
|
||||
declare const _LANGS_: string[][];
|
||||
declare const _LOCALE_: Record<string, any>;
|
||||
declare const _VERSION_: string;
|
||||
declare const _ENV_: string;
|
||||
declare const _DEV_: boolean;
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
<XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="$emit('closed')">
|
||||
<template #header>
|
||||
<Fa :icon="faExclamationCircle" style="margin-right: 0.5em;"/>
|
||||
<i18n-t keypath="reportAbuseOf" tag="span">
|
||||
<I18n src="reportAbuseOf" tag="span">
|
||||
<template #name>
|
||||
<b><MkAcct :user="user"/></b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
</template>
|
||||
<div class="dpvffvvy">
|
||||
<div class="_section">
|
||||
|
|
|
@ -6,19 +6,19 @@
|
|||
<div class="status">
|
||||
<div>
|
||||
<Fa :icon="faUsers" fixed-width/>
|
||||
<i18n-t keypath="_channel.usersCount" tag="span" style="margin-left: 4px;">
|
||||
<I18n src="_channel.usersCount" tag="span" style="margin-left: 4px;">
|
||||
<template #n>
|
||||
<b>{{ channel.usersCount }}</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
</div>
|
||||
<div>
|
||||
<Fa :icon="faPencilAlt" fixed-width/>
|
||||
<i18n-t keypath="_channel.notesCount" tag="span" style="margin-left: 4px;">
|
||||
<I18n src="_channel.notesCount" tag="span" style="margin-left: 4px;">
|
||||
<template #n>
|
||||
<b>{{ channel.notesCount }}</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
15
src/client/components/global/i18n.ts
Normal file
15
src/client/components/global/i18n.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { h, Fragment, defineComponent } from 'vue';
|
||||
import type { SetupContext, VNodeChild, RenderFunction } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
render() {
|
||||
// TODO
|
||||
return h('span', this.src);
|
||||
}
|
||||
});
|
|
@ -9,6 +9,7 @@ import userName from './global/user-name.vue';
|
|||
import ellipsis from './global/ellipsis.vue';
|
||||
import time from './global/time.vue';
|
||||
import url from './global/url.vue';
|
||||
import i18n from './global/i18n';
|
||||
import loading from './global/loading.vue';
|
||||
import error from './global/error.vue';
|
||||
|
||||
|
@ -24,4 +25,5 @@ export default function(app: App) {
|
|||
app.component('MkUrl', url);
|
||||
app.component('MkLoading', loading);
|
||||
app.component('MkError', error);
|
||||
app.component('I18n', i18n);
|
||||
}
|
||||
|
|
|
@ -16,13 +16,13 @@
|
|||
<div class="renote" v-if="isRenote">
|
||||
<MkAvatar class="avatar" :user="note.user"/>
|
||||
<Fa :icon="faRetweet"/>
|
||||
<i18n-t keypath="renotedBy" tag="span">
|
||||
<I18n src="renotedBy" tag="span">
|
||||
<template #user>
|
||||
<MkA class="name" :to="userPage(note.user)" v-user-preview="note.userId">
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
<div class="info">
|
||||
<button class="_button time" @click="showRenoteMenu()" ref="renoteTime">
|
||||
<Fa class="dropdownIcon" v-if="isMyRenote" :icon="faEllipsisH"/>
|
||||
|
@ -90,13 +90,13 @@
|
|||
<XSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
|
||||
</div>
|
||||
<div v-else class="_panel muted" @click="muted = false">
|
||||
<i18n-t keypath="userSaysSomething" tag="small">
|
||||
<I18n src="userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.userId">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -38,9 +38,9 @@
|
|||
</MkInput>
|
||||
<label v-if="meta.tosUrl" class="tou">
|
||||
<input type="checkbox" v-model="ToSAgreement">
|
||||
<i18n-t keypath="agreeTo">
|
||||
<I18n src="agreeTo">
|
||||
<a :href="meta.tosUrl" class="_link" target="_blank">{{ $t('tos') }}</a>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
</label>
|
||||
<captcha v-if="meta.enableHcaptcha" class="captcha" provider="hcaptcha" ref="hcaptcha" v-model:value="hCaptchaResponse" :sitekey="meta.hcaptchaSiteKey"/>
|
||||
<captcha v-if="meta.enableRecaptcha" class="captcha" provider="grecaptcha" ref="recaptcha" v-model:value="reCaptchaResponse" :sitekey="meta.recaptchaSiteKey"/>
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { clientDb, entries } from './db';
|
||||
|
||||
const address = new URL(location.href);
|
||||
const siteName = (document.querySelector('meta[property="og:site_name"]') as HTMLMetaElement)?.content;
|
||||
|
||||
|
@ -8,9 +6,9 @@ export const hostname = address.hostname;
|
|||
export const url = address.origin;
|
||||
export const apiUrl = url + '/api';
|
||||
export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming';
|
||||
export const lang = localStorage.getItem('lang');
|
||||
export const lang = _LANG_;
|
||||
export const langs = _LANGS_;
|
||||
export const getLocale = async () => Object.fromEntries((await entries(clientDb.i18n)) as [string, string][]);
|
||||
export const locale = _LOCALE_;
|
||||
export const version = _VERSION_;
|
||||
export const instanceName = siteName === 'Misskey' ? host : siteName;
|
||||
export const ui = localStorage.getItem('ui');
|
||||
|
|
|
@ -1,36 +1,49 @@
|
|||
import { createI18n } from 'vue-i18n';
|
||||
import { clientDb, get, count } from './db';
|
||||
import { setI18nContexts } from '@/scripts/set-i18n-contexts';
|
||||
import { version, langs, getLocale } from '@/config';
|
||||
import { markRaw } from 'vue';
|
||||
import { locale } from '@/config';
|
||||
|
||||
let _lang = localStorage.getItem('lang');
|
||||
export class I18n<T extends Record<string, any>> {
|
||||
public locale: T;
|
||||
|
||||
if (_lang == null) {
|
||||
if (langs.map(x => x[0]).includes(navigator.language)) {
|
||||
_lang = navigator.language;
|
||||
} else {
|
||||
_lang = langs.map(x => x[0]).find(x => x.split('-')[0] == navigator.language);
|
||||
constructor(locale: T) {
|
||||
this.locale = locale;
|
||||
|
||||
if (_lang == null) {
|
||||
// Fallback
|
||||
_lang = 'en-US';
|
||||
if (_DEV_) {
|
||||
console.log('i18n', this.locale);
|
||||
}
|
||||
|
||||
//#region BIND
|
||||
this.t = this.t.bind(this);
|
||||
//#endregion
|
||||
}
|
||||
|
||||
localStorage.setItem('lang', _lang);
|
||||
// string にしているのは、ドット区切りでのパス指定を許可するため
|
||||
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
|
||||
public t(key: string, args?: Record<string, any>): string {
|
||||
try {
|
||||
let str = key.split('.').reduce((o, i) => o[i], this.locale) as string;
|
||||
if (args) {
|
||||
for (const [k, v] of Object.entries(args)) {
|
||||
str = str.replace(`{${k}}`, v);
|
||||
}
|
||||
}
|
||||
return str;
|
||||
} catch (e) {
|
||||
if (_DEV_) {
|
||||
console.warn(`missing localization '${key}'`);
|
||||
return `⚠'${key}'⚠`;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const lang = _lang;
|
||||
export const i18n = markRaw(new I18n(locale));
|
||||
|
||||
export const locale = await count(clientDb.i18n).then(async n => {
|
||||
if (n === 0) return await setI18nContexts(_lang, version);
|
||||
if ((await get('_version_', clientDb.i18n) !== version)) return await setI18nContexts(_lang, version, true);
|
||||
|
||||
return await getLocale();
|
||||
});
|
||||
|
||||
export const i18n = createI18n({
|
||||
sync: false,
|
||||
locale: _lang,
|
||||
messages: { [_lang]: locale }
|
||||
});
|
||||
// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
|
||||
declare module '@vue/runtime-core' {
|
||||
interface ComponentCustomProperties {
|
||||
$t: typeof i18n['t'];
|
||||
$ts: typeof i18n['locale'];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,11 +40,11 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
|||
import widgets from '@/widgets';
|
||||
import directives from '@/directives';
|
||||
import components from '@/components';
|
||||
import { version, ui } from '@/config';
|
||||
import { version, ui, lang } from '@/config';
|
||||
import { router } from '@/router';
|
||||
import { applyTheme } from '@/scripts/theme';
|
||||
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
|
||||
import { i18n, lang } from '@/i18n';
|
||||
import { i18n } from '@/i18n';
|
||||
import { stream, isMobile, dialog } from '@/os';
|
||||
import * as sound from '@/scripts/sound';
|
||||
import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
|
||||
|
@ -53,6 +53,8 @@ import { fetchInstance, instance } from '@/instance';
|
|||
|
||||
console.info(`Misskey v${version}`);
|
||||
|
||||
window.clearTimeout(window.mkBootTimer);
|
||||
|
||||
if (_DEV_) {
|
||||
console.warn('Development mode!!!');
|
||||
|
||||
|
@ -175,10 +177,11 @@ app.config.globalProperties = {
|
|||
$i,
|
||||
$store: defaultStore,
|
||||
$instance: instance,
|
||||
$t: i18n.t,
|
||||
$ts: i18n.locale,
|
||||
};
|
||||
|
||||
app.use(router);
|
||||
app.use(i18n);
|
||||
// eslint-disable-next-line vue/component-definition-name-casing
|
||||
app.component('Fa', FontAwesomeIcon);
|
||||
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
</div>
|
||||
<div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : null }" class="banner">
|
||||
<div class="status">
|
||||
<div><Fa :icon="faUsers" fixed-width/><i18n-t keypath="_channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></i18n-t></div>
|
||||
<div><Fa :icon="faPencilAlt" fixed-width/><i18n-t keypath="_channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></i18n-t></div>
|
||||
<div><Fa :icon="faUsers" fixed-width/><I18n src="_channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
|
||||
<div><Fa :icon="faPencilAlt" fixed-width/><I18n src="_channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
|
||||
</div>
|
||||
<div class="fade"></div>
|
||||
</div>
|
||||
|
|
|
@ -35,18 +35,18 @@
|
|||
<div>
|
||||
<MkRadio v-model="game.bw" value="random" @update:modelValue="updateSettings('bw')">{{ $t('random') }}</MkRadio>
|
||||
<MkRadio v-model="game.bw" :value="'1'" @update:modelValue="updateSettings('bw')">
|
||||
<i18n-t keypath="_reversi.blackIs" tag="span">
|
||||
<I18n src="_reversi.blackIs" tag="span">
|
||||
<template #name>
|
||||
<b><MkUserName :user="game.user1"/></b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
</MkRadio>
|
||||
<MkRadio v-model="game.bw" :value="'2'" @update:modelValue="updateSettings('bw')">
|
||||
<i18n-t keypath="_reversi.blackIs" tag="span">
|
||||
<I18n src="_reversi.blackIs" tag="span">
|
||||
<template #name>
|
||||
<b><MkUserName :user="game.user2"/></b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
</MkRadio>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -46,11 +46,11 @@
|
|||
</div>
|
||||
<div class="sazhgisb" v-else>
|
||||
<h1>
|
||||
<i18n-t keypath="waitingFor" tag="span">
|
||||
<I18n src="waitingFor" tag="span">
|
||||
<template #x>
|
||||
<b><MkUserName :user="matching"/></b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
<MkEllipsis/>
|
||||
</h1>
|
||||
<div class="cancel">
|
||||
|
|
|
@ -45,14 +45,14 @@
|
|||
<div v-if="data && !$i.twoFactorEnabled">
|
||||
<ol style="margin: 0; padding: 0 0 0 1em;">
|
||||
<li>
|
||||
<i18n-t keypath="_2fa.step1" tag="span">
|
||||
<I18n src="_2fa.step1" tag="span">
|
||||
<template #a>
|
||||
<a href="https://authy.com/" rel="noopener" target="_blank" class="_link">Authy</a>
|
||||
</template>
|
||||
<template #b>
|
||||
<a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" class="_link">Google Authenticator</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
</li>
|
||||
<li>{{ $t('_2fa.step2') }}<br><img :src="data.qr"></li>
|
||||
<li>{{ $t('_2fa.step3') }}<br>
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
<template #label>{{ $t('uiLanguage') }}</template>
|
||||
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
|
||||
<template #caption>
|
||||
<i18n-t keypath="i18nInfo" tag="span">
|
||||
<I18n src="i18nInfo" tag="span">
|
||||
<template #link>
|
||||
<MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
</template>
|
||||
</FormSelect>
|
||||
|
||||
|
|
|
@ -23,14 +23,14 @@
|
|||
</div>
|
||||
<div class="_content" v-else-if="tutorial === 4">
|
||||
<div>{{ $t('_tutorial.step5_1') }}</div>
|
||||
<i18n-t keypath="_tutorial.step5_2" tag="div">
|
||||
<I18n src="_tutorial.step5_2" tag="div">
|
||||
<template #featured>
|
||||
<MkA class="_link" to="/featured">{{ $t('featured') }}</MkA>
|
||||
</template>
|
||||
<template #explore>
|
||||
<MkA class="_link" to="/explore">{{ $t('explore') }}</MkA>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
<div>{{ $t('_tutorial.step5_3') }}</div>
|
||||
<small>{{ $t('_tutorial.step5_4') }}</small>
|
||||
</div>
|
||||
|
@ -41,11 +41,11 @@
|
|||
</div>
|
||||
<div class="_content" v-else-if="tutorial === 6">
|
||||
<div>{{ $t('_tutorial.step7_1') }}</div>
|
||||
<i18n-t keypath="_tutorial.step7_2" tag="div">
|
||||
<I18n src="_tutorial.step7_2" tag="div">
|
||||
<template #help>
|
||||
<MkA class="_link" to="/docs">{{ $t('help') }}</MkA>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</I18n>
|
||||
<div>{{ $t('_tutorial.step7_3') }}</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import { clientDb, clear, bulkSet } from '../db';
|
||||
import { deepEntries, delimitEntry } from 'deep-entries';
|
||||
|
||||
export function setI18nContexts(lang: string, version: string, cleardb = false) {
|
||||
return Promise.all([
|
||||
cleardb ? clear(clientDb.i18n) : Promise.resolve(),
|
||||
fetch(`/assets/locales/${lang}.${version}.json`)
|
||||
])
|
||||
.then(([, response]) => response.json())
|
||||
.then(locale => {
|
||||
const flatLocaleEntries = deepEntries(locale, delimitEntry) as [string, string][];
|
||||
bulkSet(flatLocaleEntries, clientDb.i18n);
|
||||
return Object.fromEntries(flatLocaleEntries);
|
||||
});
|
||||
}
|
|
@ -177,7 +177,7 @@ export default defineComponent({
|
|||
// TODO: この値を設定で変えられるようにする?
|
||||
$columnMargin: 12px;
|
||||
|
||||
$deckMargin: 12px;
|
||||
$deckMargin: $columnMargin;
|
||||
|
||||
--margin: var(--marginHalf);
|
||||
|
||||
|
|
117
src/server/web/boot.js
Normal file
117
src/server/web/boot.js
Normal file
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* BOOT LOADER
|
||||
* サーバーからレスポンスされるHTMLに埋め込まれるスクリプトで、以下の役割を持ちます。
|
||||
* - バージョンやユーザーの言語に基づいて適切なメインスクリプトを読み込む。
|
||||
* - キャッシュされたコンパイル済みテーマを適用する。
|
||||
* - クライアントの設定値に基づいて対応するHTMLクラスやCSS変数を設定する。
|
||||
* テーマやCSS変数をこの段階で設定するのは、メインスクリプトが読み込まれる間もテーマを適用したいためです。
|
||||
* 注: webpackは介さないため、このファイルではrequireやimportは使えません。
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
//#region Script
|
||||
|
||||
//#region Detect language
|
||||
const supportedLangs = LANGS;
|
||||
let lang = localStorage.getItem('lang');
|
||||
if (lang == null || !supportedLangs.includes(lang)) {
|
||||
if (supportedLangs.includes(navigator.language)) {
|
||||
lang = navigator.language;
|
||||
} else {
|
||||
lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
|
||||
|
||||
// Fallback
|
||||
if (lang == null) lang = 'en-US';
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const ver = localStorage.getItem('v') || VERSION;
|
||||
|
||||
const salt = localStorage.getItem('salt')
|
||||
? `?salt=${localStorage.getItem('salt')}`
|
||||
: '';
|
||||
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.setAttribute('src', `/assets/app.${ver}.${lang}.js${salt}`);
|
||||
script.setAttribute('async', 'true');
|
||||
script.setAttribute('defer', 'true');
|
||||
head.appendChild(script);
|
||||
|
||||
// 3秒経ってもスクリプトがロードされない場合はバージョンが古くて
|
||||
// 404になっているせいかもしれないので、バージョンを確認して古ければ更新する
|
||||
//
|
||||
// 読み込まれたスクリプトからこのタイマーを解除できるように、
|
||||
// グローバルにタイマーIDを代入しておく
|
||||
window.mkBootTimer = window.setTimeout(async () => {
|
||||
const res = await fetch('/api/meta', {
|
||||
method: 'POST',
|
||||
cache: 'no-cache'
|
||||
});
|
||||
|
||||
const meta = await res.json();
|
||||
|
||||
if (meta.version != ver) {
|
||||
localStorage.setItem('v', meta.version);
|
||||
alert(
|
||||
'Misskeyの新しいバージョンがあります。ページを再度読み込みします。' +
|
||||
'\n\n' +
|
||||
'New version of Misskey available. The page will be reloaded.');
|
||||
refresh();
|
||||
}
|
||||
}, 3000);
|
||||
//#endregion
|
||||
|
||||
//#region Theme
|
||||
const theme = localStorage.getItem('theme');
|
||||
if (theme) {
|
||||
for (const [k, v] of Object.entries(JSON.parse(theme))) {
|
||||
document.documentElement.style.setProperty(`--${k}`, v.toString());
|
||||
|
||||
// HTMLの theme-color 適用
|
||||
if (k === 'htmlThemeColor') {
|
||||
for (const tag of document.head.children) {
|
||||
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
|
||||
tag.setAttribute('content', v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const fontSize = localStorage.getItem('fontSize');
|
||||
if (fontSize) {
|
||||
document.documentElement.classList.add('f-' + fontSize);
|
||||
}
|
||||
|
||||
const useSystemFont = localStorage.getItem('useSystemFont');
|
||||
if (useSystemFont) {
|
||||
document.documentElement.classList.add('useSystemFont');
|
||||
}
|
||||
|
||||
const wallpaper = localStorage.getItem('wallpaper');
|
||||
if (wallpaper) {
|
||||
document.documentElement.style.backgroundImage = `url(${wallpaper})`;
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
// Random
|
||||
localStorage.setItem('salt', Math.random().toString().substr(2, 8));
|
||||
|
||||
// Clear cache (service worker)
|
||||
try {
|
||||
navigator.serviceWorker.controller.postMessage('clear');
|
||||
navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||
registrations.forEach(registration => registration.unregister());
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
location.reload();
|
||||
}
|
37
src/server/web/style.css
Normal file
37
src/server/web/style.css
Normal file
|
@ -0,0 +1,37 @@
|
|||
html {
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
#ini {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
#ini > svg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
animation: ini 0.6s infinite linear;
|
||||
color: var(--accent);
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
@keyframes ini {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
|
@ -33,76 +33,11 @@ html
|
|||
block og
|
||||
meta(property='og:image' content=img)
|
||||
|
||||
style.
|
||||
html {
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
}
|
||||
style
|
||||
include ../style.css
|
||||
|
||||
#ini {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
#ini > svg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
animation: ini 0.6s infinite linear;
|
||||
color: var(--accent);
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
@keyframes ini {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
script(src=`/assets/app.${version}.js` async defer)
|
||||
script.
|
||||
const theme = localStorage.getItem('theme');
|
||||
if (theme) {
|
||||
for (const [k, v] of Object.entries(JSON.parse(theme))) {
|
||||
document.documentElement.style.setProperty(`--${k}`, v.toString());
|
||||
if (k === 'htmlThemeColor') {
|
||||
for (const tag of document.head.children) {
|
||||
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
|
||||
tag.setAttribute('content', v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fontSize = localStorage.getItem('fontSize');
|
||||
if (fontSize) {
|
||||
document.documentElement.classList.add('f-' + fontSize);
|
||||
}
|
||||
|
||||
const useSystemFont = localStorage.getItem('useSystemFont');
|
||||
if (useSystemFont) {
|
||||
document.documentElement.classList.add('useSystemFont');
|
||||
}
|
||||
|
||||
const wallpaper = localStorage.getItem('wallpaper');
|
||||
if (wallpaper) {
|
||||
document.documentElement.style.backgroundImage = `url(${wallpaper})`;
|
||||
}
|
||||
script
|
||||
include ../boot.js
|
||||
|
||||
body
|
||||
noscript: p
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue