summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-08-24 13:20:30 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-08-24 13:20:30 +0900
commite967d9ded35cbdf937bbc232e05a5fa0966458a7 (patch)
treef1fd95de0378a07565f4fe00192fdc8414d7d985
parentMerge branch 'develop' (diff)
parent12.89.1 (diff)
downloadmisskey-e967d9ded35cbdf937bbc232e05a5fa0966458a7.tar.gz
misskey-e967d9ded35cbdf937bbc232e05a5fa0966458a7.tar.bz2
misskey-e967d9ded35cbdf937bbc232e05a5fa0966458a7.zip
Merge branch 'develop'
-rw-r--r--CHANGELOG.md10
-rw-r--r--locales/de-DE.yml1
-rw-r--r--locales/en-US.yml1
-rw-r--r--locales/eo-UY.yml22
-rw-r--r--locales/ja-JP.yml1
-rw-r--r--locales/ko-KR.yml9
-rw-r--r--locales/zh-CN.yml5
-rw-r--r--migration/1629778475000-deepl-integration2.ts14
-rw-r--r--package.json2
-rw-r--r--src/client/components/signup.vue10
-rw-r--r--src/client/directives/tooltip.ts14
-rw-r--r--src/client/init.ts5
-rw-r--r--src/client/os.ts6
-rw-r--r--src/client/pages/about-misskey.vue2
-rw-r--r--src/client/pages/instance/other-settings.vue6
-rw-r--r--src/client/style.scss7
-rw-r--r--src/docs/eo-UY/advanced/create-plugin.md2
-rw-r--r--src/models/entities/meta.ts5
-rw-r--r--src/server/api/endpoints/admin/update-meta.ts8
-rw-r--r--src/server/api/endpoints/meta.ts2
-rw-r--r--src/server/api/endpoints/notes/translate.ts4
-rw-r--r--src/server/file/index.ts4
-rw-r--r--src/server/proxy/index.ts4
-rw-r--r--src/server/web/boot.js4
24 files changed, 125 insertions, 23 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fddba34870..7e8add521b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,16 @@
-->
+## 12.89.1 (2021/08/24)
+
+### Improvements
+- クライアントのデザインの調整
+
+### Bugfixes
+- 翻訳でDeepLのProアカウントに対応していない問題を修正
+- インスタンス設定でDeepLのAuth Keyが空で表示される問題を修正
+- セキュリティの向上
+
## 12.89.0 (2021/08/21)
### Improvements
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index 0fcfc9b46a..1afc8fab48 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -778,6 +778,7 @@ whatIsNew: "Änderungen anzeigen"
translate: "Übersetzen"
translatedFrom: "Aus {x} übersetzt"
accountDeletionInProgress: "Löschung des Benutzerkontos momentan in Bearbeitung"
+usernameInfo: "Ein Name, durch den dein Benutzerkonto auf diesem Server identifiziert werden kann. Du kannst das Alphabet (a~z, A~Z), Ziffern (0~9) oder Unterstriche (_) verwenden. Benutzernamen können später nicht geändert werden."
_accountDelete:
accountDelete: "Benutzerkonto löschen"
mayTakeTime: "Da die Löschung eines Benutzerkontos ein aufwendiger Prozess ist, kann dessen Dauer davon abhängen, wie viel Inhalt in diesem erstellt wurde oder wie viele Dateien hochgeladen wurden."
diff --git a/locales/en-US.yml b/locales/en-US.yml
index cf82a8cd73..9565e62038 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -778,6 +778,7 @@ whatIsNew: "Show changes"
translate: "Translate"
translatedFrom: "Translated from {x}"
accountDeletionInProgress: "Account deletion is currently in progress"
+usernameInfo: "A name that identifies your account from others on this server. You can use the alphabet (a~z, A~Z), digits (0~9) or underscores (_). Usernames can not be changed later."
_accountDelete:
accountDelete: "Delete Account"
mayTakeTime: "As account deletion is a resource-heavy process, it may take some time to complete depending on how much content you have created and how many files you have uploaded."
diff --git a/locales/eo-UY.yml b/locales/eo-UY.yml
index bb141a692c..4d435512b8 100644
--- a/locales/eo-UY.yml
+++ b/locales/eo-UY.yml
@@ -73,6 +73,7 @@ following: "Sekvatoj"
followers: "Sekvantoj"
followsYou: "Sekvas vin"
createList: "Kreii liston"
+manageLists: "Administri liston"
error: "Eraro"
somethingHappened: "Problemo okazis."
retry: "Reprovi"
@@ -188,7 +189,7 @@ fromUrl: "De URL"
uploadFromUrl: "Alŝuti de URL"
uploadFromUrlDescription: "URL de la dosiero kiun vi volas alŝuti"
explore: "Esplori"
-games: "Ludoj sur Misskey"
+games: "Miskiaj Ludoj"
messageRead: "Legita"
startMessaging: "Komenci babiladon"
nUsersRead: "Legita de {n} homoj"
@@ -245,6 +246,7 @@ yearX: "La jaro {year}"
pages: "Paĝoj"
connectService: "Konekti"
disconnectService: "Farkonektiĝi"
+enableLocalTimeline: "Ebligi lokan templinion"
enableGlobalTimeline: "Ebligi mallokan templinion"
registration: "Registri"
driveCapacityPerLocalAccount: "Volumo de disko po unu loka uzanto"
@@ -257,6 +259,7 @@ pinnedUsers: "Alpinglita uzanto"
pinnedPages: "Alpinglitaj paĝoj"
pinnedNotes: "Pinglita noto"
antennas: "Antenoj"
+manageAntennas: "Administri antenojn"
name: "Nomo"
withFileAntenna: "Nur kun aldonaĵo"
withReplies: "Inkluzive respondoj"
@@ -342,6 +345,7 @@ poll: "Balotujo"
useCw: "Kaŝi enhavo"
themeEditor: "Redaktilo de koloraroj"
author: "Aŭtoro"
+manage: "Administro"
plugins: "Kromaĵoj"
deck: "Kartaro"
medium: "Meza"
@@ -424,6 +428,7 @@ offline: "Forkonektita"
instanceBlocking: "Blokado de ekzemplo"
selectAccount: "Elekti konton"
user: "Uzanto"
+administration: "Administro"
accounts: "Kontoj"
high: "Alta"
middle: "Meza"
@@ -440,6 +445,7 @@ translatedFrom: "Tradukita el {x}"
_docs:
continueReading: "Legi plu"
features: "Funkcioj"
+ admin: "Administro"
_gallery:
liked: "Ŝatitaj notoj"
like: "Ŝati"
@@ -448,6 +454,9 @@ _email:
title: "Vi estas eksekvita"
_receiveFollowRequest:
title: "Vi ricevis peton de sekvado"
+_plugin:
+ install: "Instali kromaĵon"
+ manage: "Administri kromaĵojn"
_registry:
key: "Ŝlosilo"
keys: "Ŝlosiloj"
@@ -461,6 +470,7 @@ _aboutMisskey:
translation: "Traduki Misskey'on"
patrons: "Mecenatoj"
_mfm:
+ dummy: "Misskey vastigas la mondon de Fediverso"
mention: "Mencioj"
hashtag: "Kradvorto"
url: "URL"
@@ -488,6 +498,7 @@ _instanceTicker:
_channel:
create: "Krei kanalon"
edit: "Redakti kanalon"
+ owned: "Posedaĵo"
following: "Sekvante"
usersCount: "{n} partoprenanto(j)"
_menuDisplay:
@@ -496,6 +507,7 @@ _wordMute:
muteWords: "Kaŝigitaj vortoj"
mutedNotes: "Silentigataj notoj"
_theme:
+ manage: "Administri kolorarojn"
code: "Kodo de koloraro"
darken: "Malbrileco"
lighten: "Brileco"
@@ -612,10 +624,10 @@ _charts:
federationInstancesTotal: "Tuta numero de kunfederantaj ekzemploj"
filesTotal: "Tuta numero de dosieroj"
_timelines:
- home: "Hejmo"
- local: "Loka"
- social: "Sociala"
- global: "Malloka"
+ home: "HEJMO"
+ local: "LOKA"
+ social: "SOCIALA"
+ global: "MALLOKA"
_rooms:
translate: "Movi"
chooseImage: "Elekti bildon"
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 35739d2760..0c78ea1492 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -778,6 +778,7 @@ whatIsNew: "更新情報を見る"
translate: "翻訳"
translatedFrom: "{x}から翻訳"
accountDeletionInProgress: "アカウントの削除が進行中です"
+usernameInfo: "サーバー上であなたのアカウントを一意に識別するための名前。アルファベット(a~z, A~Z)、数字(0~9)、およびアンダーバー(_)が使用できます。ユーザー名は後から変更することは出来ません。"
_accountDelete:
accountDelete: "アカウントの削除"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index 99fc0ba6f0..43e01fc2c4 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -777,6 +777,15 @@ misskeyUpdated: "Misskey가 업데이트 되었습니다!"
whatIsNew: "패치 정보 보기"
translate: "번역"
translatedFrom: "{x}에서 번역"
+accountDeletionInProgress: "계정 삭제 작업을 진행하고 있습니다"
+usernameInfo: "서버상에서 계정을 식별하기 위한 이름. 알파벳(a~z, A~Z), 숫자(0~9) 및 언더바(_)를 사용할 수 있습니다. 사용자명은 나중에 변경할 수 없습니다."
+_accountDelete:
+ accountDelete: "계정 삭제"
+ mayTakeTime: "계정 삭제는 서버에 부하를 가하기 때문에, 작성한 콘텐츠나 업로드한 파일의 수가 많으면 완료까지 시간이 걸릴 수 있습니다."
+ sendEmail: "계정 삭제가 완료되면 등록된 이메일 주소로 알림을 보냅니다."
+ requestAccountDelete: "계정 삭제 요청"
+ started: "삭제 작업이 시작되었습니다."
+ inProgress: "삭제 진행 중"
_docs:
continueReading: "계속 읽기"
features: "기능"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index aad08d1b4b..7dad6232be 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -777,6 +777,11 @@ misskeyUpdated: "Misskey更新完成!"
whatIsNew: "显示更新信息"
translate: "翻译"
translatedFrom: "从 {x} 翻译"
+accountDeletionInProgress: "正在删除账户"
+usernameInfo: "在服务器上唯一标识您的帐户的名称。您可以使用字母 (a ~ z, A ~ Z)、数字 (0 ~ 9) 和下划线 (_)。用户名以后不能更改。"
+_accountDelete:
+ accountDelete: "删除帐户"
+ inProgress: "正在删除"
_docs:
continueReading: "继续阅读"
features: "特性"
diff --git a/migration/1629778475000-deepl-integration2.ts b/migration/1629778475000-deepl-integration2.ts
new file mode 100644
index 0000000000..67e5ea02ae
--- /dev/null
+++ b/migration/1629778475000-deepl-integration2.ts
@@ -0,0 +1,14 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class deeplIntegration21629778475000 implements MigrationInterface {
+ name = 'deeplIntegration21629778475000'
+
+ public async up(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.query(`ALTER TABLE "meta" ADD "deeplIsPro" boolean NOT NULL DEFAULT false`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "deeplIsPro"`);
+ }
+
+}
diff --git a/package.json b/package.json
index 7754fb9281..b085bf1c8c 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>",
- "version": "12.89.0",
+ "version": "12.89.1",
"codename": "indigo",
"repository": {
"type": "git",
diff --git a/src/client/components/signup.vue b/src/client/components/signup.vue
index b0b0c2ad4d..d584b97209 100644
--- a/src/client/components/signup.vue
+++ b/src/client/components/signup.vue
@@ -1,12 +1,12 @@
<template>
-<form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()">
+<form class="qlvuhzng" @submit.prevent="onSubmit" :autocomplete="Math.random()">
<template v-if="meta">
- <MkInput v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required>
+ <MkInput class="_inputNoTopMargin" v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required>
<template #label>{{ $ts.invitationCode }}</template>
<template #prefix><i class="fas fa-key"></i></template>
</MkInput>
- <MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @update:modelValue="onChangeUsername" data-cy-signup-username>
- <template #label>{{ $ts.username }}</template>
+ <MkInput class="_inputNoTopMargin" v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @update:modelValue="onChangeUsername" data-cy-signup-username>
+ <template #label>{{ $ts.username }} <div class="_button _help" v-tooltip:dialog="$ts.usernameInfo"><i class="far fa-question-circle"></i></div></template>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
<template #caption>
@@ -204,7 +204,7 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
-.mk-signup {
+.qlvuhzng {
.captcha {
margin: 16px 0;
}
diff --git a/src/client/directives/tooltip.ts b/src/client/directives/tooltip.ts
index 90247eacb2..ee690558af 100644
--- a/src/client/directives/tooltip.ts
+++ b/src/client/directives/tooltip.ts
@@ -1,6 +1,6 @@
import { Directive, ref } from 'vue';
import { isDeviceTouch } from '@client/scripts/is-device-touch';
-import { popup } from '@client/os';
+import { popup, dialog } from '@client/os';
const start = isDeviceTouch ? 'touchstart' : 'mouseover';
const end = isDeviceTouch ? 'touchend' : 'mouseleave';
@@ -24,6 +24,18 @@ export default {
}
};
+ if (binding.arg === 'dialog') {
+ el.addEventListener('click', (ev) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+ dialog({
+ type: 'info',
+ text: binding.value,
+ });
+ return false;
+ });
+ }
+
const show = e => {
if (!document.body.contains(el)) return;
if (self._close) return;
diff --git a/src/client/init.ts b/src/client/init.ts
index 194ece886b..4d2170e03f 100644
--- a/src/client/init.ts
+++ b/src/client/init.ts
@@ -101,15 +101,12 @@ window.addEventListener('resize', () => {
});
//#endregion
-// Get the <head> element
-const head = document.getElementsByTagName('head')[0];
-
// If mobile, insert the viewport meta tag
if (isMobile || window.innerWidth <= 1024) {
const viewport = document.getElementsByName('viewport').item(0);
viewport.setAttribute('content',
`${viewport.getAttribute('content')},minimum-scale=1,maximum-scale=1,user-scalable=no`);
- head.appendChild(viewport);
+ document.head.appendChild(viewport);
}
//#region Set lang attr
diff --git a/src/client/os.ts b/src/client/os.ts
index 284f982f06..8125332798 100644
--- a/src/client/os.ts
+++ b/src/client/os.ts
@@ -214,7 +214,11 @@ export function modalPageWindow(path: string) {
}, {}, 'closed');
}
-export function dialog(props: Record<string, any>) {
+export function dialog(props: {
+ type: 'error' | 'info' | 'success' | 'warning' | 'waiting';
+ title?: string | null;
+ text?: string | null;
+}) {
return new Promise((resolve, reject) => {
popup(import('@client/components/dialog.vue'), props, {
done: result => {
diff --git a/src/client/pages/about-misskey.vue b/src/client/pages/about-misskey.vue
index a324ebce5c..384c7e8ccb 100644
--- a/src/client/pages/about-misskey.vue
+++ b/src/client/pages/about-misskey.vue
@@ -60,7 +60,7 @@ import FormBase from '@client/components/form/base.vue';
import FormGroup from '@client/components/form/group.vue';
import FormKeyValueView from '@client/components/form/key-value-view.vue';
import MkLink from '@client/components/link.vue';
-import { physics } from '@client/scripts/physics.ts';
+import { physics } from '@client/scripts/physics';
import * as symbols from '@client/symbols';
const patrons = [
diff --git a/src/client/pages/instance/other-settings.vue b/src/client/pages/instance/other-settings.vue
index 8002528931..4fa80b2b2c 100644
--- a/src/client/pages/instance/other-settings.vue
+++ b/src/client/pages/instance/other-settings.vue
@@ -12,6 +12,9 @@
<template #prefix><i class="fas fa-key"></i></template>
DeepL Auth Key
</FormInput>
+ <FormSwitch v-model:value="deeplIsPro">
+ Pro account
+ </FormSwitch>
</FormGroup>
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
</FormSuspense>
@@ -50,6 +53,7 @@ export default defineComponent({
},
summalyProxy: '',
deeplAuthKey: '',
+ deeplIsPro: false,
}
},
@@ -62,11 +66,13 @@ export default defineComponent({
const meta = await os.api('meta', { detail: true });
this.summalyProxy = meta.summalyProxy;
this.deeplAuthKey = meta.deeplAuthKey;
+ this.deeplIsPro = meta.deeplIsPro;
},
save() {
os.apiWithDialog('admin/update-meta', {
summalyProxy: this.summalyProxy,
deeplAuthKey: this.deeplAuthKey,
+ deeplIsPro: this.deeplIsPro,
}).then(() => {
fetchInstance();
});
diff --git a/src/client/style.scss b/src/client/style.scss
index a6c027745a..269cf88939 100644
--- a/src/client/style.scss
+++ b/src/client/style.scss
@@ -156,8 +156,10 @@ hr {
._button {
appearance: none;
+ display: inline-block;
padding: 0;
margin: 0; // for Safari
+ width: max-content;
background: none;
border: none;
cursor: pointer;
@@ -201,6 +203,11 @@ hr {
}
}
+._help {
+ color: var(--accent);
+ cursor: help
+}
+
._textButton {
@extend ._button;
color: var(--accent);
diff --git a/src/docs/eo-UY/advanced/create-plugin.md b/src/docs/eo-UY/advanced/create-plugin.md
index 0d2fa19178..2a5af72917 100644
--- a/src/docs/eo-UY/advanced/create-plugin.md
+++ b/src/docs/eo-UY/advanced/create-plugin.md
@@ -5,7 +5,7 @@ Misskey Webクライアントのプラグイン機能を使うと、クライア
プラグインは、AiScriptのメタデータ埋め込み機能を使って、デフォルトとしてプラグインのメタデータを定義する必要があります。 メタデータは次のプロパティを含むオブジェクトです。
### name
-プラグイン名
+Nomo de kromaĵo
### author
プラグイン作者
diff --git a/src/models/entities/meta.ts b/src/models/entities/meta.ts
index 2a0632c87c..6428aacdf1 100644
--- a/src/models/entities/meta.ts
+++ b/src/models/entities/meta.ts
@@ -319,6 +319,11 @@ export class Meta {
})
public deeplAuthKey: string | null;
+ @Column('boolean', {
+ default: false,
+ })
+ public deeplIsPro: boolean;
+
@Column('varchar', {
length: 512,
nullable: true
diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts
index 5962dba98a..46f30fef7d 100644
--- a/src/server/api/endpoints/admin/update-meta.ts
+++ b/src/server/api/endpoints/admin/update-meta.ts
@@ -149,6 +149,10 @@ export const meta = {
validator: $.optional.nullable.str,
},
+ deeplIsPro: {
+ validator: $.optional.bool,
+ },
+
enableTwitterIntegration: {
validator: $.optional.bool,
},
@@ -574,6 +578,10 @@ export default define(meta, async (ps, me) => {
}
}
+ if (ps.deeplIsPro !== undefined) {
+ set.deeplIsPro = ps.deeplIsPro;
+ }
+
await getConnection().transaction(async transactionalEntityManager => {
const meta = await transactionalEntityManager.findOne(Meta, {
order: {
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index 1c87952f6a..3f422dff07 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -583,6 +583,8 @@ export default define(meta, async (ps, me) => {
response.objectStorageUseProxy = instance.objectStorageUseProxy;
response.objectStorageSetPublicRead = instance.objectStorageSetPublicRead;
response.objectStorageS3ForcePathStyle = instance.objectStorageS3ForcePathStyle;
+ response.deeplAuthKey = instance.deeplAuthKey;
+ response.deeplIsPro = instance.deeplIsPro;
}
}
diff --git a/src/server/api/endpoints/notes/translate.ts b/src/server/api/endpoints/notes/translate.ts
index a5fdf70ce6..e4bc6bb060 100644
--- a/src/server/api/endpoints/notes/translate.ts
+++ b/src/server/api/endpoints/notes/translate.ts
@@ -61,7 +61,9 @@ export default define(meta, async (ps, user) => {
params.append('text', note.text);
params.append('target_lang', targetLang);
- const res = await fetch('https://api-free.deepl.com/v2/translate', {
+ const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
+
+ const res = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
diff --git a/src/server/file/index.ts b/src/server/file/index.ts
index 9b5d8f7267..a455acd1cf 100644
--- a/src/server/file/index.ts
+++ b/src/server/file/index.ts
@@ -17,6 +17,10 @@ const _dirname = dirname(_filename);
// Init app
const app = new Koa();
app.use(cors());
+app.use(async (ctx, next) => {
+ ctx.set('Content-Security-Policy', `default-src 'none'; style-src 'unsafe-inline'`);
+ await next();
+});
// Init router
const router = new Router();
diff --git a/src/server/proxy/index.ts b/src/server/proxy/index.ts
index 9ef198d31b..b8993f19f8 100644
--- a/src/server/proxy/index.ts
+++ b/src/server/proxy/index.ts
@@ -10,6 +10,10 @@ import { proxyMedia } from './proxy-media';
// Init app
const app = new Koa();
app.use(cors());
+app.use(async (ctx, next) => {
+ ctx.set('Content-Security-Policy', `default-src 'none'; style-src 'unsafe-inline'`);
+ await next();
+});
// Init router
const router = new Router();
diff --git a/src/server/web/boot.js b/src/server/web/boot.js
index e2fd137f95..7a41ae2555 100644
--- a/src/server/web/boot.js
+++ b/src/server/web/boot.js
@@ -60,8 +60,6 @@
? `?salt=${localStorage.getItem('salt')}`
: '';
- const head = document.getElementsByTagName('head')[0];
-
const script = document.createElement('script');
script.setAttribute('src', `/assets/app.${v}.js${salt}`);
script.setAttribute('async', 'true');
@@ -70,7 +68,7 @@
renderError('APP_FETCH_FAILED');
checkUpdate();
});
- head.appendChild(script);
+ document.head.appendChild(script);
//#endregion
//#region Theme