Merge branch 'develop'
This commit is contained in:
		
						commit
						e967d9ded3
					
				
					 24 changed files with 125 additions and 23 deletions
				
			
		
							
								
								
									
										10
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								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 | ||||
|  |  | |||
|  | @ -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." | ||||
|  |  | |||
|  | @ -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." | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -778,6 +778,7 @@ whatIsNew: "更新情報を見る" | |||
| translate: "翻訳" | ||||
| translatedFrom: "{x}から翻訳" | ||||
| accountDeletionInProgress: "アカウントの削除が進行中です" | ||||
| usernameInfo: "サーバー上であなたのアカウントを一意に識別するための名前。アルファベット(a~z, A~Z)、数字(0~9)、およびアンダーバー(_)が使用できます。ユーザー名は後から変更することは出来ません。" | ||||
| 
 | ||||
| _accountDelete: | ||||
|   accountDelete: "アカウントの削除" | ||||
|  |  | |||
|  | @ -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: "기능" | ||||
|  |  | |||
|  | @ -777,6 +777,11 @@ misskeyUpdated: "Misskey更新完成!" | |||
| whatIsNew: "显示更新信息" | ||||
| translate: "翻译" | ||||
| translatedFrom: "从 {x} 翻译" | ||||
| accountDeletionInProgress: "正在删除账户" | ||||
| usernameInfo: "在服务器上唯一标识您的帐户的名称。您可以使用字母 (a ~ z, A ~ Z)、数字 (0 ~ 9) 和下划线 (_)。用户名以后不能更改。" | ||||
| _accountDelete: | ||||
|   accountDelete: "删除帐户" | ||||
|   inProgress: "正在删除" | ||||
| _docs: | ||||
|   continueReading: "继续阅读" | ||||
|   features: "特性" | ||||
|  |  | |||
							
								
								
									
										14
									
								
								migration/1629778475000-deepl-integration2.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								migration/1629778475000-deepl-integration2.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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"`); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -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", | ||||
|  |  | |||
|  | @ -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; | ||||
| 	} | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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 => { | ||||
|  |  | |||
|  | @ -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 = [ | ||||
|  |  | |||
|  | @ -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(); | ||||
| 			}); | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ Misskey Webクライアントのプラグイン機能を使うと、クライア | |||
| プラグインは、AiScriptのメタデータ埋め込み機能を使って、デフォルトとしてプラグインのメタデータを定義する必要があります。 メタデータは次のプロパティを含むオブジェクトです。 | ||||
| 
 | ||||
| ### name | ||||
| プラグイン名 | ||||
| Nomo de kromaĵo | ||||
| 
 | ||||
| ### author | ||||
| プラグイン作者 | ||||
|  |  | |||
|  | @ -319,6 +319,11 @@ export class Meta { | |||
| 	}) | ||||
| 	public deeplAuthKey: string | null; | ||||
| 
 | ||||
| 	@Column('boolean', { | ||||
| 		default: false, | ||||
| 	}) | ||||
| 	public deeplIsPro: boolean; | ||||
| 
 | ||||
| 	@Column('varchar', { | ||||
| 		length: 512, | ||||
| 		nullable: true | ||||
|  |  | |||
|  | @ -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: { | ||||
|  |  | |||
|  | @ -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; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -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', | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue