diff options
Diffstat (limited to 'src/client/scripts')
48 files changed, 0 insertions, 4284 deletions
diff --git a/src/client/scripts/2fa.ts b/src/client/scripts/2fa.ts deleted file mode 100644 index 00363cffa6..0000000000 --- a/src/client/scripts/2fa.ts +++ /dev/null @@ -1,33 +0,0 @@ -export function byteify(data: string, encoding: 'ascii' | 'base64' | 'hex') { - switch (encoding) { - case 'ascii': - return Uint8Array.from(data, c => c.charCodeAt(0)); - case 'base64': - return Uint8Array.from( - atob( - data - .replace(/-/g, '+') - .replace(/_/g, '/') - ), - c => c.charCodeAt(0) - ); - case 'hex': - return new Uint8Array( - data - .match(/.{1,2}/g) - .map(byte => parseInt(byte, 16)) - ); - } -} - -export function hexify(buffer: ArrayBuffer) { - return Array.from(new Uint8Array(buffer)) - .reduce( - (str, byte) => str + byte.toString(16).padStart(2, '0'), - '' - ); -} - -export function stringify(buffer: ArrayBuffer) { - return String.fromCharCode(... new Uint8Array(buffer)); -} diff --git a/src/client/scripts/aiscript/api.ts b/src/client/scripts/aiscript/api.ts deleted file mode 100644 index 5dd08f34ac..0000000000 --- a/src/client/scripts/aiscript/api.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { utils, values } from '@syuilo/aiscript'; -import * as os from '@client/os'; -import { $i } from '@client/account'; - -export function createAiScriptEnv(opts) { - let apiRequests = 0; - return { - USER_ID: $i ? values.STR($i.id) : values.NULL, - USER_NAME: $i ? values.STR($i.name) : values.NULL, - USER_USERNAME: $i ? values.STR($i.username) : values.NULL, - 'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => { - await os.dialog({ - type: type ? type.value : 'info', - title: title.value, - text: text.value, - }); - }), - 'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => { - const confirm = await os.dialog({ - type: type ? type.value : 'question', - showCancelButton: true, - title: title.value, - text: text.value, - }); - return confirm.canceled ? values.FALSE : values.TRUE; - }), - 'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => { - if (token) utils.assertString(token); - apiRequests++; - if (apiRequests > 16) return values.NULL; - const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token || null)); - return utils.jsToVal(res); - }), - 'Mk:save': values.FN_NATIVE(([key, value]) => { - utils.assertString(key); - localStorage.setItem('aiscript:' + opts.storageKey + ':' + key.value, JSON.stringify(utils.valToJs(value))); - return values.NULL; - }), - 'Mk:load': values.FN_NATIVE(([key]) => { - utils.assertString(key); - return utils.jsToVal(JSON.parse(localStorage.getItem('aiscript:' + opts.storageKey + ':' + key.value))); - }), - }; -} diff --git a/src/client/scripts/autocomplete.ts b/src/client/scripts/autocomplete.ts deleted file mode 100644 index c0c33b2c7e..0000000000 --- a/src/client/scripts/autocomplete.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { Ref, ref } from 'vue'; -import * as getCaretCoordinates from 'textarea-caret'; -import { toASCII } from 'punycode/'; -import { popup } from '@client/os'; - -export class Autocomplete { - private suggestion: { - x: Ref<number>; - y: Ref<number>; - q: Ref<string | null>; - close: Function; - } | null; - private textarea: any; - private vm: any; - private currentType: string; - private opts: { - model: string; - }; - private opening: boolean; - - private get text(): string { - return this.vm[this.opts.model]; - } - - private set text(text: string) { - this.vm[this.opts.model] = text; - } - - /** - * 対象のテキストエリアを与えてインスタンスを初期化します。 - */ - constructor(textarea, vm, opts) { - //#region BIND - this.onInput = this.onInput.bind(this); - this.complete = this.complete.bind(this); - this.close = this.close.bind(this); - //#endregion - - this.suggestion = null; - this.textarea = textarea; - this.vm = vm; - this.opts = opts; - this.opening = false; - - this.attach(); - } - - /** - * このインスタンスにあるテキストエリアの入力のキャプチャを開始します。 - */ - public attach() { - this.textarea.addEventListener('input', this.onInput); - } - - /** - * このインスタンスにあるテキストエリアの入力のキャプチャを解除します。 - */ - public detach() { - this.textarea.removeEventListener('input', this.onInput); - this.close(); - } - - /** - * テキスト入力時 - */ - private onInput() { - const caretPos = this.textarea.selectionStart; - const text = this.text.substr(0, caretPos).split('\n').pop()!; - - const mentionIndex = text.lastIndexOf('@'); - const hashtagIndex = text.lastIndexOf('#'); - const emojiIndex = text.lastIndexOf(':'); - const mfmTagIndex = text.lastIndexOf('$'); - - const max = Math.max( - mentionIndex, - hashtagIndex, - emojiIndex, - mfmTagIndex); - - if (max == -1) { - this.close(); - return; - } - - const isMention = mentionIndex != -1; - const isHashtag = hashtagIndex != -1; - const isMfmTag = mfmTagIndex != -1; - const isEmoji = emojiIndex != -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':'); - - let opened = false; - - if (isMention) { - const username = text.substr(mentionIndex + 1); - if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) { - this.open('user', username); - opened = true; - } else if (username === '') { - this.open('user', null); - opened = true; - } - } - - if (isHashtag && !opened) { - const hashtag = text.substr(hashtagIndex + 1); - if (!hashtag.includes(' ')) { - this.open('hashtag', hashtag); - opened = true; - } - } - - if (isEmoji && !opened) { - const emoji = text.substr(emojiIndex + 1); - if (!emoji.includes(' ')) { - this.open('emoji', emoji); - opened = true; - } - } - - if (isMfmTag && !opened) { - const mfmTag = text.substr(mfmTagIndex + 1); - if (!mfmTag.includes(' ')) { - this.open('mfmTag', mfmTag.replace('[', '')); - opened = true; - } - } - - if (!opened) { - this.close(); - } - } - - /** - * サジェストを提示します。 - */ - private async open(type: string, q: string | null) { - if (type != this.currentType) { - this.close(); - } - if (this.opening) return; - this.opening = true; - this.currentType = type; - - //#region サジェストを表示すべき位置を計算 - const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart); - - const rect = this.textarea.getBoundingClientRect(); - - const x = rect.left + caretPosition.left - this.textarea.scrollLeft; - const y = rect.top + caretPosition.top - this.textarea.scrollTop; - //#endregion - - if (this.suggestion) { - this.suggestion.x.value = x; - this.suggestion.y.value = y; - this.suggestion.q.value = q; - - this.opening = false; - } else { - const _x = ref(x); - const _y = ref(y); - const _q = ref(q); - - const { dispose } = await popup(import('@client/components/autocomplete.vue'), { - textarea: this.textarea, - close: this.close, - type: type, - q: _q, - x: _x, - y: _y, - }, { - done: (res) => { - this.complete(res); - } - }); - - this.suggestion = { - q: _q, - x: _x, - y: _y, - close: () => dispose(), - }; - - this.opening = false; - } - } - - /** - * サジェストを閉じます。 - */ - private close() { - if (this.suggestion == null) return; - - this.suggestion.close(); - this.suggestion = null; - - this.textarea.focus(); - } - - /** - * オートコンプリートする - */ - private complete({ type, value }) { - this.close(); - - const caret = this.textarea.selectionStart; - - if (type == 'user') { - const source = this.text; - - const before = source.substr(0, caret); - const trimmedBefore = before.substring(0, before.lastIndexOf('@')); - const after = source.substr(caret); - - const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`; - - // 挿入 - this.text = `${trimmedBefore}@${acct} ${after}`; - - // キャレットを戻す - this.vm.$nextTick(() => { - this.textarea.focus(); - const pos = trimmedBefore.length + (acct.length + 2); - this.textarea.setSelectionRange(pos, pos); - }); - } else if (type == 'hashtag') { - const source = this.text; - - const before = source.substr(0, caret); - const trimmedBefore = before.substring(0, before.lastIndexOf('#')); - const after = source.substr(caret); - - // 挿入 - this.text = `${trimmedBefore}#${value} ${after}`; - - // キャレットを戻す - this.vm.$nextTick(() => { - this.textarea.focus(); - const pos = trimmedBefore.length + (value.length + 2); - this.textarea.setSelectionRange(pos, pos); - }); - } else if (type == 'emoji') { - const source = this.text; - - const before = source.substr(0, caret); - const trimmedBefore = before.substring(0, before.lastIndexOf(':')); - const after = source.substr(caret); - - // 挿入 - this.text = trimmedBefore + value + after; - - // キャレットを戻す - this.vm.$nextTick(() => { - this.textarea.focus(); - const pos = trimmedBefore.length + value.length; - this.textarea.setSelectionRange(pos, pos); - }); - } else if (type == 'mfmTag') { - const source = this.text; - - const before = source.substr(0, caret); - const trimmedBefore = before.substring(0, before.lastIndexOf('$')); - const after = source.substr(caret); - - // 挿入 - this.text = `${trimmedBefore}$[${value} ]${after}`; - - // キャレットを戻す - this.vm.$nextTick(() => { - this.textarea.focus(); - const pos = trimmedBefore.length + (value.length + 3); - this.textarea.setSelectionRange(pos, pos); - }); - } - } -} diff --git a/src/client/scripts/check-word-mute.ts b/src/client/scripts/check-word-mute.ts deleted file mode 100644 index 3b1fa75b1e..0000000000 --- a/src/client/scripts/check-word-mute.ts +++ /dev/null @@ -1,26 +0,0 @@ -export async function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: string[][]): Promise<boolean> { - // 自分自身 - if (me && (note.userId === me.id)) return false; - - const words = mutedWords - // Clean up - .map(xs => xs.filter(x => x !== '')) - .filter(xs => xs.length > 0); - - if (words.length > 0) { - if (note.text == null) return false; - - const matched = words.some(and => - and.every(keyword => { - const regexp = keyword.match(/^\/(.+)\/(.*)$/); - if (regexp) { - return new RegExp(regexp[1], regexp[2]).test(note.text!); - } - return note.text!.includes(keyword); - })); - - if (matched) return true; - } - - return false; -} diff --git a/src/client/scripts/collect-page-vars.ts b/src/client/scripts/collect-page-vars.ts deleted file mode 100644 index a4096fb2c2..0000000000 --- a/src/client/scripts/collect-page-vars.ts +++ /dev/null @@ -1,48 +0,0 @@ -export function collectPageVars(content) { - const pageVars = []; - const collect = (xs: any[]) => { - for (const x of xs) { - if (x.type === 'textInput') { - pageVars.push({ - name: x.name, - type: 'string', - value: x.default || '' - }); - } else if (x.type === 'textareaInput') { - pageVars.push({ - name: x.name, - type: 'string', - value: x.default || '' - }); - } else if (x.type === 'numberInput') { - pageVars.push({ - name: x.name, - type: 'number', - value: x.default || 0 - }); - } else if (x.type === 'switch') { - pageVars.push({ - name: x.name, - type: 'boolean', - value: x.default || false - }); - } else if (x.type === 'counter') { - pageVars.push({ - name: x.name, - type: 'number', - value: 0 - }); - } else if (x.type === 'radioButton') { - pageVars.push({ - name: x.name, - type: 'string', - value: x.default || '' - }); - } else if (x.children) { - collect(x.children); - } - } - }; - collect(content); - return pageVars; -} diff --git a/src/client/scripts/contains.ts b/src/client/scripts/contains.ts deleted file mode 100644 index 770bda63bb..0000000000 --- a/src/client/scripts/contains.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default (parent, child, checkSame = true) => { - if (checkSame && parent === child) return true; - let node = child.parentNode; - while (node) { - if (node == parent) return true; - node = node.parentNode; - } - return false; -}; diff --git a/src/client/scripts/copy-to-clipboard.ts b/src/client/scripts/copy-to-clipboard.ts deleted file mode 100644 index ab13cab970..0000000000 --- a/src/client/scripts/copy-to-clipboard.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Clipboardに値をコピー(TODO: 文字列以外も対応) - */ -export default val => { - // 空div 生成 - const tmp = document.createElement('div'); - // 選択用のタグ生成 - const pre = document.createElement('pre'); - - // 親要素のCSSで user-select: none だとコピーできないので書き換える - pre.style.webkitUserSelect = 'auto'; - pre.style.userSelect = 'auto'; - - tmp.appendChild(pre).textContent = val; - - // 要素を画面外へ - const s = tmp.style; - s.position = 'fixed'; - s.right = '200%'; - - // body に追加 - document.body.appendChild(tmp); - // 要素を選択 - document.getSelection().selectAllChildren(tmp); - - // クリップボードにコピー - const result = document.execCommand('copy'); - - // 要素削除 - document.body.removeChild(tmp); - - return result; -}; diff --git a/src/client/scripts/extract-avg-color-from-blurhash.ts b/src/client/scripts/extract-avg-color-from-blurhash.ts deleted file mode 100644 index 123ab7a06d..0000000000 --- a/src/client/scripts/extract-avg-color-from-blurhash.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function extractAvgColorFromBlurhash(hash: string) { - return typeof hash == 'string' - ? '#' + [...hash.slice(2, 6)] - .map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x)) - .reduce((a, c) => a * 83 + c, 0) - .toString(16) - .padStart(6, '0') - : undefined; -} diff --git a/src/client/scripts/focus.ts b/src/client/scripts/focus.ts deleted file mode 100644 index 0894877820..0000000000 --- a/src/client/scripts/focus.ts +++ /dev/null @@ -1,27 +0,0 @@ -export function focusPrev(el: Element | null, self = false, scroll = true) { - if (el == null) return; - if (!self) el = el.previousElementSibling; - if (el) { - if (el.hasAttribute('tabindex')) { - (el as HTMLElement).focus({ - preventScroll: !scroll - }); - } else { - focusPrev(el.previousElementSibling, true); - } - } -} - -export function focusNext(el: Element | null, self = false, scroll = true) { - if (el == null) return; - if (!self) el = el.nextElementSibling; - if (el) { - if (el.hasAttribute('tabindex')) { - (el as HTMLElement).focus({ - preventScroll: !scroll - }); - } else { - focusPrev(el.nextElementSibling, true); - } - } -} diff --git a/src/client/scripts/form.ts b/src/client/scripts/form.ts deleted file mode 100644 index 7bf6cec452..0000000000 --- a/src/client/scripts/form.ts +++ /dev/null @@ -1,31 +0,0 @@ -export type FormItem = { - label?: string; - type: 'string'; - default: string | null; - hidden?: boolean; - multiline?: boolean; -} | { - label?: string; - type: 'number'; - default: number | null; - hidden?: boolean; - step?: number; -} | { - label?: string; - type: 'boolean'; - default: boolean | null; - hidden?: boolean; -} | { - label?: string; - type: 'enum'; - default: string | null; - hidden?: boolean; - enum: string[]; -} | { - label?: string; - type: 'array'; - default: unknown[] | null; - hidden?: boolean; -}; - -export type Form = Record<string, FormItem>; diff --git a/src/client/scripts/gen-search-query.ts b/src/client/scripts/gen-search-query.ts deleted file mode 100644 index cafb3cccfe..0000000000 --- a/src/client/scripts/gen-search-query.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { parseAcct } from '@/misc/acct'; -import { host as localHost } from '@client/config'; - -export async function genSearchQuery(v: any, q: string) { - let host: string; - let userId: string; - if (q.split(' ').some(x => x.startsWith('@'))) { - for (const at of q.split(' ').filter(x => x.startsWith('@')).map(x => x.substr(1))) { - if (at.includes('.')) { - if (at === localHost || at === '.') { - host = null; - } else { - host = at; - } - } else { - const user = await v.os.api('users/show', parseAcct(at)).catch(x => null); - if (user) { - userId = user.id; - } else { - // todo: show error - } - } - } - - } - return { - query: q.split(' ').filter(x => !x.startsWith('/') && !x.startsWith('@')).join(' '), - host: host, - userId: userId - }; -} diff --git a/src/client/scripts/get-account-from-id.ts b/src/client/scripts/get-account-from-id.ts deleted file mode 100644 index 065b41118c..0000000000 --- a/src/client/scripts/get-account-from-id.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { get } from '@client/scripts/idb-proxy'; - -export async function getAccountFromId(id: string) { - const accounts = await get('accounts') as { token: string; id: string; }[]; - if (!accounts) console.log('Accounts are not recorded'); - return accounts.find(e => e.id === id); -} diff --git a/src/client/scripts/get-md5.ts b/src/client/scripts/get-md5.ts deleted file mode 100644 index b002d762b1..0000000000 --- a/src/client/scripts/get-md5.ts +++ /dev/null @@ -1,10 +0,0 @@ -// スクリプトサイズがデカい -//import * as crypto from 'crypto'; - -export default (data: ArrayBuffer) => { - //const buf = new Buffer(data); - //const hash = crypto.createHash('md5'); - //hash.update(buf); - //return hash.digest('hex'); - return ''; -}; diff --git a/src/client/scripts/get-static-image-url.ts b/src/client/scripts/get-static-image-url.ts deleted file mode 100644 index 92c31914c7..0000000000 --- a/src/client/scripts/get-static-image-url.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { url as instanceUrl } from '@client/config'; -import * as url from '../../prelude/url'; - -export function getStaticImageUrl(baseUrl: string): string { - const u = new URL(baseUrl); - if (u.href.startsWith(`${instanceUrl}/proxy/`)) { - // もう既にproxyっぽそうだったらsearchParams付けるだけ - u.searchParams.set('static', '1'); - return u.href; - } - const dummy = `${u.host}${u.pathname}`; // 拡張子がないとキャッシュしてくれないCDNがあるので - return `${instanceUrl}/proxy/${dummy}?${url.query({ - url: u.href, - static: '1' - })}`; -} diff --git a/src/client/scripts/get-user-menu.ts b/src/client/scripts/get-user-menu.ts deleted file mode 100644 index 3689a93b47..0000000000 --- a/src/client/scripts/get-user-menu.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { i18n } from '@client/i18n'; -import copyToClipboard from '@client/scripts/copy-to-clipboard'; -import { host } from '@client/config'; -import { getAcct } from '@/misc/acct'; -import * as os from '@client/os'; -import { userActions } from '@client/store'; -import { router } from '@client/router'; -import { $i } from '@client/account'; - -export function getUserMenu(user) { - const meId = $i ? $i.id : null; - - async function pushList() { - const t = i18n.locale.selectList; // なぜか後で参照すると null になるので最初にメモリに確保しておく - const lists = await os.api('users/lists/list'); - if (lists.length === 0) { - os.dialog({ - type: 'error', - text: i18n.locale.youHaveNoLists - }); - return; - } - const { canceled, result: listId } = await os.dialog({ - type: null, - title: t, - select: { - items: lists.map(list => ({ - value: list.id, text: list.name - })) - }, - showCancelButton: true - }); - if (canceled) return; - os.apiWithDialog('users/lists/push', { - listId: listId, - userId: user.id - }); - } - - async function inviteGroup() { - const groups = await os.api('users/groups/owned'); - if (groups.length === 0) { - os.dialog({ - type: 'error', - text: i18n.locale.youHaveNoGroups - }); - return; - } - const { canceled, result: groupId } = await os.dialog({ - type: null, - title: i18n.locale.group, - select: { - items: groups.map(group => ({ - value: group.id, text: group.name - })) - }, - showCancelButton: true - }); - if (canceled) return; - os.apiWithDialog('users/groups/invite', { - groupId: groupId, - userId: user.id - }); - } - - async function toggleMute() { - os.apiWithDialog(user.isMuted ? 'mute/delete' : 'mute/create', { - userId: user.id - }).then(() => { - user.isMuted = !user.isMuted; - }); - } - - async function toggleBlock() { - if (!await getConfirmed(user.isBlocking ? i18n.locale.unblockConfirm : i18n.locale.blockConfirm)) return; - - os.apiWithDialog(user.isBlocking ? 'blocking/delete' : 'blocking/create', { - userId: user.id - }).then(() => { - user.isBlocking = !user.isBlocking; - }); - } - - async function toggleSilence() { - if (!await getConfirmed(i18n.t(user.isSilenced ? 'unsilenceConfirm' : 'silenceConfirm'))) return; - - os.apiWithDialog(user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', { - userId: user.id - }).then(() => { - user.isSilenced = !user.isSilenced; - }); - } - - async function toggleSuspend() { - if (!await getConfirmed(i18n.t(user.isSuspended ? 'unsuspendConfirm' : 'suspendConfirm'))) return; - - os.apiWithDialog(user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', { - userId: user.id - }).then(() => { - user.isSuspended = !user.isSuspended; - }); - } - - function reportAbuse() { - os.popup(import('@client/components/abuse-report-window.vue'), { - user: user, - }, {}, 'closed'); - } - - async function getConfirmed(text: string): Promise<boolean> { - const confirm = await os.dialog({ - type: 'warning', - showCancelButton: true, - title: 'confirm', - text, - }); - - return !confirm.canceled; - } - - let menu = [{ - icon: 'fas fa-at', - text: i18n.locale.copyUsername, - action: () => { - copyToClipboard(`@${user.username}@${user.host || host}`); - } - }, { - icon: 'fas fa-info-circle', - text: i18n.locale.info, - action: () => { - os.pageWindow(`/user-info/${user.id}`); - } - }, { - icon: 'fas fa-envelope', - text: i18n.locale.sendMessage, - action: () => { - os.post({ specified: user }); - } - }, meId != user.id ? { - type: 'link', - icon: 'fas fa-comments', - text: i18n.locale.startMessaging, - to: '/my/messaging/' + getAcct(user), - } : undefined, null, { - icon: 'fas fa-list-ul', - text: i18n.locale.addToList, - action: pushList - }, meId != user.id ? { - icon: 'fas fa-users', - text: i18n.locale.inviteToGroup, - action: inviteGroup - } : undefined] as any; - - if ($i && meId != user.id) { - menu = menu.concat([null, { - icon: user.isMuted ? 'fas fa-eye' : 'fas fa-eye-slash', - text: user.isMuted ? i18n.locale.unmute : i18n.locale.mute, - action: toggleMute - }, { - icon: 'fas fa-ban', - text: user.isBlocking ? i18n.locale.unblock : i18n.locale.block, - action: toggleBlock - }]); - - menu = menu.concat([null, { - icon: 'fas fa-exclamation-circle', - text: i18n.locale.reportAbuse, - action: reportAbuse - }]); - - if ($i && ($i.isAdmin || $i.isModerator)) { - menu = menu.concat([null, { - icon: 'fas fa-microphone-slash', - text: user.isSilenced ? i18n.locale.unsilence : i18n.locale.silence, - action: toggleSilence - }, { - icon: 'fas fa-snowflake', - text: user.isSuspended ? i18n.locale.unsuspend : i18n.locale.suspend, - action: toggleSuspend - }]); - } - } - - if ($i && meId === user.id) { - menu = menu.concat([null, { - icon: 'fas fa-pencil-alt', - text: i18n.locale.editProfile, - action: () => { - router.push('/settings/profile'); - } - }]); - } - - if (userActions.length > 0) { - menu = menu.concat([null, ...userActions.map(action => ({ - icon: 'fas fa-plug', - text: action.title, - action: () => { - action.handler(user); - } - }))]); - } - - return menu; -} diff --git a/src/client/scripts/hotkey.ts b/src/client/scripts/hotkey.ts deleted file mode 100644 index 2b3f491fd8..0000000000 --- a/src/client/scripts/hotkey.ts +++ /dev/null @@ -1,88 +0,0 @@ -import keyCode from './keycode'; - -type Keymap = Record<string, Function>; - -type Pattern = { - which: string[]; - ctrl?: boolean; - shift?: boolean; - alt?: boolean; -}; - -type Action = { - patterns: Pattern[]; - callback: Function; - allowRepeat: boolean; -}; - -const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => { - const result = { - patterns: [], - callback: callback, - allowRepeat: true - } as Action; - - if (patterns.match(/^\(.*\)$/) !== null) { - result.allowRepeat = false; - patterns = patterns.slice(1, -1); - } - - result.patterns = patterns.split('|').map(part => { - const pattern = { - which: [], - ctrl: false, - alt: false, - shift: false - } as Pattern; - - const keys = part.trim().split('+').map(x => x.trim().toLowerCase()); - for (const key of keys) { - switch (key) { - case 'ctrl': pattern.ctrl = true; break; - case 'alt': pattern.alt = true; break; - case 'shift': pattern.shift = true; break; - default: pattern.which = keyCode(key).map(k => k.toLowerCase()); - } - } - - return pattern; - }); - - return result; -}); - -const ignoreElemens = ['input', 'textarea']; - -function match(e: KeyboardEvent, patterns: Action['patterns']): boolean { - const key = e.code.toLowerCase(); - return patterns.some(pattern => pattern.which.includes(key) && - pattern.ctrl === e.ctrlKey && - pattern.shift === e.shiftKey && - pattern.alt === e.altKey && - !e.metaKey - ); -} - -export const makeHotkey = (keymap: Keymap) => { - const actions = parseKeymap(keymap); - - return (e: KeyboardEvent) => { - if (document.activeElement) { - if (ignoreElemens.some(el => document.activeElement!.matches(el))) return; - if (document.activeElement.attributes['contenteditable']) return; - } - - for (const action of actions) { - const matched = match(e, action.patterns); - - if (matched) { - if (!action.allowRepeat && e.repeat) return; - - e.preventDefault(); - e.stopPropagation(); - action.callback(e); - break; - } - } - }; -}; diff --git a/src/client/scripts/hpml/block.ts b/src/client/scripts/hpml/block.ts deleted file mode 100644 index 804c5c1124..0000000000 --- a/src/client/scripts/hpml/block.ts +++ /dev/null @@ -1,109 +0,0 @@ -// blocks - -export type BlockBase = { - id: string; - type: string; -}; - -export type TextBlock = BlockBase & { - type: 'text'; - text: string; -}; - -export type SectionBlock = BlockBase & { - type: 'section'; - title: string; - children: (Block | VarBlock)[]; -}; - -export type ImageBlock = BlockBase & { - type: 'image'; - fileId: string | null; -}; - -export type ButtonBlock = BlockBase & { - type: 'button'; - text: any; - primary: boolean; - action: string; - content: string; - event: string; - message: string; - var: string; - fn: string; -}; - -export type IfBlock = BlockBase & { - type: 'if'; - var: string; - children: Block[]; -}; - -export type TextareaBlock = BlockBase & { - type: 'textarea'; - text: string; -}; - -export type PostBlock = BlockBase & { - type: 'post'; - text: string; - attachCanvasImage: boolean; - canvasId: string; -}; - -export type CanvasBlock = BlockBase & { - type: 'canvas'; - name: string; // canvas id - width: number; - height: number; -}; - -export type NoteBlock = BlockBase & { - type: 'note'; - detailed: boolean; - note: string | null; -}; - -export type Block = - TextBlock | SectionBlock | ImageBlock | ButtonBlock | IfBlock | TextareaBlock | PostBlock | CanvasBlock | NoteBlock | VarBlock; - -// variable blocks - -export type VarBlockBase = BlockBase & { - name: string; -}; - -export type NumberInputVarBlock = VarBlockBase & { - type: 'numberInput'; - text: string; -}; - -export type TextInputVarBlock = VarBlockBase & { - type: 'textInput'; - text: string; -}; - -export type SwitchVarBlock = VarBlockBase & { - type: 'switch'; - text: string; -}; - -export type RadioButtonVarBlock = VarBlockBase & { - type: 'radioButton'; - title: string; - values: string[]; -}; - -export type CounterVarBlock = VarBlockBase & { - type: 'counter'; - text: string; - inc: number; -}; - -export type VarBlock = - NumberInputVarBlock | TextInputVarBlock | SwitchVarBlock | RadioButtonVarBlock | CounterVarBlock; - -const varBlock = ['numberInput', 'textInput', 'switch', 'radioButton', 'counter']; -export function isVarBlock(block: Block): block is VarBlock { - return varBlock.includes(block.type); -} diff --git a/src/client/scripts/hpml/evaluator.ts b/src/client/scripts/hpml/evaluator.ts deleted file mode 100644 index 68d140ff50..0000000000 --- a/src/client/scripts/hpml/evaluator.ts +++ /dev/null @@ -1,234 +0,0 @@ -import autobind from 'autobind-decorator'; -import { PageVar, envVarsDef, Fn, HpmlScope, HpmlError } from '.'; -import { version } from '@client/config'; -import { AiScript, utils, values } from '@syuilo/aiscript'; -import { createAiScriptEnv } from '../aiscript/api'; -import { collectPageVars } from '../collect-page-vars'; -import { initHpmlLib, initAiLib } from './lib'; -import * as os from '@client/os'; -import { markRaw, ref, Ref, unref } from 'vue'; -import { Expr, isLiteralValue, Variable } from './expr'; - -/** - * Hpml evaluator - */ -export class Hpml { - private variables: Variable[]; - private pageVars: PageVar[]; - private envVars: Record<keyof typeof envVarsDef, any>; - public aiscript?: AiScript; - public pageVarUpdatedCallback?: values.VFn; - public canvases: Record<string, HTMLCanvasElement> = {}; - public vars: Ref<Record<string, any>> = ref({}); - public page: Record<string, any>; - - private opts: { - randomSeed: string; visitor?: any; url?: string; - enableAiScript: boolean; - }; - - constructor(page: Hpml['page'], opts: Hpml['opts']) { - this.page = page; - this.variables = this.page.variables; - this.pageVars = collectPageVars(this.page.content); - this.opts = opts; - - if (this.opts.enableAiScript) { - this.aiscript = markRaw(new AiScript({ ...createAiScriptEnv({ - storageKey: 'pages:' + this.page.id - }), ...initAiLib(this)}, { - in: (q) => { - return new Promise(ok => { - os.dialog({ - title: q, - input: {} - }).then(({ canceled, result: a }) => { - ok(a); - }); - }); - }, - out: (value) => { - console.log(value); - }, - log: (type, params) => { - }, - })); - - this.aiscript.scope.opts.onUpdated = (name, value) => { - this.eval(); - }; - } - - const date = new Date(); - - this.envVars = { - AI: 'kawaii', - VERSION: version, - URL: this.page ? `${opts.url}/@${this.page.user.username}/pages/${this.page.name}` : '', - LOGIN: opts.visitor != null, - NAME: opts.visitor ? opts.visitor.name || opts.visitor.username : '', - USERNAME: opts.visitor ? opts.visitor.username : '', - USERID: opts.visitor ? opts.visitor.id : '', - NOTES_COUNT: opts.visitor ? opts.visitor.notesCount : 0, - FOLLOWERS_COUNT: opts.visitor ? opts.visitor.followersCount : 0, - FOLLOWING_COUNT: opts.visitor ? opts.visitor.followingCount : 0, - IS_CAT: opts.visitor ? opts.visitor.isCat : false, - SEED: opts.randomSeed ? opts.randomSeed : '', - YMD: `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`, - AISCRIPT_DISABLED: !this.opts.enableAiScript, - NULL: null - }; - - this.eval(); - } - - @autobind - public eval() { - try { - this.vars.value = this.evaluateVars(); - } catch (e) { - //this.onError(e); - } - } - - @autobind - public interpolate(str: string) { - if (str == null) return null; - return str.replace(/{(.+?)}/g, match => { - const v = unref(this.vars)[match.slice(1, -1).trim()]; - return v == null ? 'NULL' : v.toString(); - }); - } - - @autobind - public callAiScript(fn: string) { - try { - if (this.aiscript) this.aiscript.execFn(this.aiscript.scope.get(fn), []); - } catch (e) {} - } - - @autobind - public registerCanvas(id: string, canvas: any) { - this.canvases[id] = canvas; - } - - @autobind - public updatePageVar(name: string, value: any) { - const pageVar = this.pageVars.find(v => v.name === name); - if (pageVar !== undefined) { - pageVar.value = value; - if (this.pageVarUpdatedCallback) { - if (this.aiscript) this.aiscript.execFn(this.pageVarUpdatedCallback, [values.STR(name), utils.jsToVal(value)]); - } - } else { - throw new HpmlError(`No such page var '${name}'`); - } - } - - @autobind - public updateRandomSeed(seed: string) { - this.opts.randomSeed = seed; - this.envVars.SEED = seed; - } - - @autobind - private _interpolateScope(str: string, scope: HpmlScope) { - return str.replace(/{(.+?)}/g, match => { - const v = scope.getState(match.slice(1, -1).trim()); - return v == null ? 'NULL' : v.toString(); - }); - } - - @autobind - public evaluateVars(): Record<string, any> { - const values: Record<string, any> = {}; - - for (const [k, v] of Object.entries(this.envVars)) { - values[k] = v; - } - - for (const v of this.pageVars) { - values[v.name] = v.value; - } - - for (const v of this.variables) { - values[v.name] = this.evaluate(v, new HpmlScope([values])); - } - - return values; - } - - @autobind - private evaluate(expr: Expr, scope: HpmlScope): any { - - if (isLiteralValue(expr)) { - if (expr.type === null) { - return null; - } - - if (expr.type === 'number') { - return parseInt((expr.value as any), 10); - } - - if (expr.type === 'text' || expr.type === 'multiLineText') { - return this._interpolateScope(expr.value || '', scope); - } - - if (expr.type === 'textList') { - return this._interpolateScope(expr.value || '', scope).trim().split('\n'); - } - - if (expr.type === 'ref') { - return scope.getState(expr.value); - } - - if (expr.type === 'aiScriptVar') { - if (this.aiscript) { - try { - return utils.valToJs(this.aiscript.scope.get(expr.value)); - } catch (e) { - return null; - } - } else { - return null; - } - } - - // Define user function - if (expr.type == 'fn') { - return { - slots: expr.value.slots.map(x => x.name), - exec: (slotArg: Record<string, any>) => { - return this.evaluate(expr.value.expression, scope.createChildScope(slotArg, expr.id)); - } - } as Fn; - } - return; - } - - // Call user function - if (expr.type.startsWith('fn:')) { - const fnName = expr.type.split(':')[1]; - const fn = scope.getState(fnName); - const args = {} as Record<string, any>; - for (let i = 0; i < fn.slots.length; i++) { - const name = fn.slots[i]; - args[name] = this.evaluate(expr.args[i], scope); - } - return fn.exec(args); - } - - if (expr.args === undefined) return null; - - const funcs = initHpmlLib(expr, scope, this.opts.randomSeed, this.opts.visitor); - - // Call function - const fnName = expr.type; - const fn = (funcs as any)[fnName]; - if (fn == null) { - throw new HpmlError(`No such function '${fnName}'`); - } else { - return fn(...expr.args.map(x => this.evaluate(x, scope))); - } - } -} diff --git a/src/client/scripts/hpml/expr.ts b/src/client/scripts/hpml/expr.ts deleted file mode 100644 index 00e3ed118b..0000000000 --- a/src/client/scripts/hpml/expr.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { literalDefs, Type } from '.'; - -export type ExprBase = { - id: string; -}; - -// value - -export type EmptyValue = ExprBase & { - type: null; - value: null; -}; - -export type TextValue = ExprBase & { - type: 'text'; - value: string; -}; - -export type MultiLineTextValue = ExprBase & { - type: 'multiLineText'; - value: string; -}; - -export type TextListValue = ExprBase & { - type: 'textList'; - value: string; -}; - -export type NumberValue = ExprBase & { - type: 'number'; - value: number; -}; - -export type RefValue = ExprBase & { - type: 'ref'; - value: string; // value is variable name -}; - -export type AiScriptRefValue = ExprBase & { - type: 'aiScriptVar'; - value: string; // value is variable name -}; - -export type UserFnValue = ExprBase & { - type: 'fn'; - value: UserFnInnerValue; -}; -type UserFnInnerValue = { - slots: { - name: string; - type: Type; - }[]; - expression: Expr; -}; - -export type Value = - EmptyValue | TextValue | MultiLineTextValue | TextListValue | NumberValue | RefValue | AiScriptRefValue | UserFnValue; - -export function isLiteralValue(expr: Expr): expr is Value { - if (expr.type == null) return true; - if (literalDefs[expr.type]) return true; - return false; -} - -// call function - -export type CallFn = ExprBase & { // "fn:hoge" or string - type: string; - args: Expr[]; - value: null; -}; - -// variable -export type Variable = (Value | CallFn) & { - name: string; -}; - -// expression -export type Expr = Variable | Value | CallFn; diff --git a/src/client/scripts/hpml/index.ts b/src/client/scripts/hpml/index.ts deleted file mode 100644 index ac81eac2d9..0000000000 --- a/src/client/scripts/hpml/index.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Hpml - */ - -import autobind from 'autobind-decorator'; -import { Hpml } from './evaluator'; -import { funcDefs } from './lib'; - -export type Fn = { - slots: string[]; - exec: (args: Record<string, any>) => ReturnType<Hpml['evaluate']>; -}; - -export type Type = 'string' | 'number' | 'boolean' | 'stringArray' | null; - -export const literalDefs: Record<string, { out: any; category: string; icon: any; }> = { - text: { out: 'string', category: 'value', icon: 'fas fa-quote-right', }, - multiLineText: { out: 'string', category: 'value', icon: 'fas fa-align-left', }, - textList: { out: 'stringArray', category: 'value', icon: 'fas fa-list', }, - number: { out: 'number', category: 'value', icon: 'fas fa-sort-numeric-up', }, - ref: { out: null, category: 'value', icon: 'fas fa-magic', }, - aiScriptVar: { out: null, category: 'value', icon: 'fas fa-magic', }, - fn: { out: 'function', category: 'value', icon: 'fas fa-square-root-alt', }, -}; - -export const blockDefs = [ - ...Object.entries(literalDefs).map(([k, v]) => ({ - type: k, out: v.out, category: v.category, icon: v.icon - })), - ...Object.entries(funcDefs).map(([k, v]) => ({ - type: k, out: v.out, category: v.category, icon: v.icon - })) -]; - -export type PageVar = { name: string; value: any; type: Type; }; - -export const envVarsDef: Record<string, Type> = { - AI: 'string', - URL: 'string', - VERSION: 'string', - LOGIN: 'boolean', - NAME: 'string', - USERNAME: 'string', - USERID: 'string', - NOTES_COUNT: 'number', - FOLLOWERS_COUNT: 'number', - FOLLOWING_COUNT: 'number', - IS_CAT: 'boolean', - SEED: null, - YMD: 'string', - AISCRIPT_DISABLED: 'boolean', - NULL: null, -}; - -export class HpmlScope { - private layerdStates: Record<string, any>[]; - public name: string; - - constructor(layerdStates: HpmlScope['layerdStates'], name?: HpmlScope['name']) { - this.layerdStates = layerdStates; - this.name = name || 'anonymous'; - } - - @autobind - public createChildScope(states: Record<string, any>, name?: HpmlScope['name']): HpmlScope { - const layer = [states, ...this.layerdStates]; - return new HpmlScope(layer, name); - } - - /** - * 指定した名前の変数の値を取得します - * @param name 変数名 - */ - @autobind - public getState(name: string): any { - for (const later of this.layerdStates) { - const state = later[name]; - if (state !== undefined) { - return state; - } - } - - throw new HpmlError( - `No such variable '${name}' in scope '${this.name}'`, { - scope: this.layerdStates - }); - } -} - -export class HpmlError extends Error { - public info?: any; - - constructor(message: string, info?: any) { - super(message); - - this.info = info; - - // Maintains proper stack trace for where our error was thrown (only available on V8) - if (Error.captureStackTrace) { - Error.captureStackTrace(this, HpmlError); - } - } -} diff --git a/src/client/scripts/hpml/lib.ts b/src/client/scripts/hpml/lib.ts deleted file mode 100644 index 2a1ac73a40..0000000000 --- a/src/client/scripts/hpml/lib.ts +++ /dev/null @@ -1,246 +0,0 @@ -import * as tinycolor from 'tinycolor2'; -import { Hpml } from './evaluator'; -import { values, utils } from '@syuilo/aiscript'; -import { Fn, HpmlScope } from '.'; -import { Expr } from './expr'; -import * as seedrandom from 'seedrandom'; - -/* TODO: https://www.chartjs.org/docs/latest/configuration/canvas-background.html#color -// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs -Chart.pluginService.register({ - beforeDraw: (chart, easing) => { - if (chart.config.options.chartArea && chart.config.options.chartArea.backgroundColor) { - const ctx = chart.chart.ctx; - ctx.save(); - ctx.fillStyle = chart.config.options.chartArea.backgroundColor; - ctx.fillRect(0, 0, chart.chart.width, chart.chart.height); - ctx.restore(); - } - } -}); -*/ - -export function initAiLib(hpml: Hpml) { - return { - 'MkPages:updated': values.FN_NATIVE(([callback]) => { - hpml.pageVarUpdatedCallback = (callback as values.VFn); - }), - 'MkPages:get_canvas': values.FN_NATIVE(([id]) => { - utils.assertString(id); - const canvas = hpml.canvases[id.value]; - const ctx = canvas.getContext('2d'); - return values.OBJ(new Map([ - ['clear_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.clearRect(x.value, y.value, width.value, height.value); })], - ['fill_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.fillRect(x.value, y.value, width.value, height.value); })], - ['stroke_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.strokeRect(x.value, y.value, width.value, height.value); })], - ['fill_text', values.FN_NATIVE(([text, x, y, width]) => { ctx.fillText(text.value, x.value, y.value, width ? width.value : undefined); })], - ['stroke_text', values.FN_NATIVE(([text, x, y, width]) => { ctx.strokeText(text.value, x.value, y.value, width ? width.value : undefined); })], - ['set_line_width', values.FN_NATIVE(([width]) => { ctx.lineWidth = width.value; })], - ['set_font', values.FN_NATIVE(([font]) => { ctx.font = font.value; })], - ['set_fill_style', values.FN_NATIVE(([style]) => { ctx.fillStyle = style.value; })], - ['set_stroke_style', values.FN_NATIVE(([style]) => { ctx.strokeStyle = style.value; })], - ['begin_path', values.FN_NATIVE(() => { ctx.beginPath(); })], - ['close_path', values.FN_NATIVE(() => { ctx.closePath(); })], - ['move_to', values.FN_NATIVE(([x, y]) => { ctx.moveTo(x.value, y.value); })], - ['line_to', values.FN_NATIVE(([x, y]) => { ctx.lineTo(x.value, y.value); })], - ['arc', values.FN_NATIVE(([x, y, radius, startAngle, endAngle]) => { ctx.arc(x.value, y.value, radius.value, startAngle.value, endAngle.value); })], - ['rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.rect(x.value, y.value, width.value, height.value); })], - ['fill', values.FN_NATIVE(() => { ctx.fill(); })], - ['stroke', values.FN_NATIVE(() => { ctx.stroke(); })], - ])); - }), - 'MkPages:chart': values.FN_NATIVE(([id, opts]) => { - /* TODO - utils.assertString(id); - utils.assertObject(opts); - const canvas = hpml.canvases[id.value]; - const color = getComputedStyle(document.documentElement).getPropertyValue('--accent'); - Chart.defaults.color = '#555'; - const chart = new Chart(canvas, { - type: opts.value.get('type').value, - data: { - labels: opts.value.get('labels').value.map(x => x.value), - datasets: opts.value.get('datasets').value.map(x => ({ - label: x.value.has('label') ? x.value.get('label').value : '', - data: x.value.get('data').value.map(x => x.value), - pointRadius: 0, - lineTension: 0, - borderWidth: 2, - borderColor: x.value.has('color') ? x.value.get('color') : color, - backgroundColor: tinycolor(x.value.has('color') ? x.value.get('color') : color).setAlpha(0.1).toRgbString(), - })) - }, - options: { - responsive: false, - devicePixelRatio: 1.5, - title: { - display: opts.value.has('title'), - text: opts.value.has('title') ? opts.value.get('title').value : '', - fontSize: 14, - }, - layout: { - padding: { - left: 32, - right: 32, - top: opts.value.has('title') ? 16 : 32, - bottom: 16 - } - }, - legend: { - display: opts.value.get('datasets').value.filter(x => x.value.has('label') && x.value.get('label').value).length === 0 ? false : true, - position: 'bottom', - labels: { - boxWidth: 16, - } - }, - tooltips: { - enabled: false, - }, - chartArea: { - backgroundColor: '#fff' - }, - ...(opts.value.get('type').value === 'radar' ? { - scale: { - ticks: { - display: opts.value.has('show_tick_label') ? opts.value.get('show_tick_label').value : false, - min: opts.value.has('min') ? opts.value.get('min').value : undefined, - max: opts.value.has('max') ? opts.value.get('max').value : undefined, - maxTicksLimit: 8, - }, - pointLabels: { - fontSize: 12 - } - } - } : { - scales: { - yAxes: [{ - ticks: { - display: opts.value.has('show_tick_label') ? opts.value.get('show_tick_label').value : true, - min: opts.value.has('min') ? opts.value.get('min').value : undefined, - max: opts.value.has('max') ? opts.value.get('max').value : undefined, - } - }] - } - }) - } - }); - */ - }) - }; -} - -export const funcDefs: Record<string, { in: any[]; out: any; category: string; icon: any; }> = { - if: { in: ['boolean', 0, 0], out: 0, category: 'flow', icon: 'fas fa-share-alt', }, - for: { in: ['number', 'function'], out: null, category: 'flow', icon: 'fas fa-recycle', }, - not: { in: ['boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag', }, - or: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag', }, - and: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag', }, - add: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-plus', }, - subtract: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-minus', }, - multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-times', }, - divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-divide', }, - mod: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-divide', }, - round: { in: ['number'], out: 'number', category: 'operation', icon: 'fas fa-calculator', }, - eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'fas fa-equals', }, - notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'fas fa-not-equal', }, - gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-greater-than', }, - lt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-less-than', }, - gtEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-greater-than-equal', }, - ltEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-less-than-equal', }, - strLen: { in: ['string'], out: 'number', category: 'text', icon: 'fas fa-quote-right', }, - strPick: { in: ['string', 'number'], out: 'string', category: 'text', icon: 'fas fa-quote-right', }, - strReplace: { in: ['string', 'string', 'string'], out: 'string', category: 'text', icon: 'fas fa-quote-right', }, - strReverse: { in: ['string'], out: 'string', category: 'text', icon: 'fas fa-quote-right', }, - join: { in: ['stringArray', 'string'], out: 'string', category: 'text', icon: 'fas fa-quote-right', }, - stringToNumber: { in: ['string'], out: 'number', category: 'convert', icon: 'fas fa-exchange-alt', }, - numberToString: { in: ['number'], out: 'string', category: 'convert', icon: 'fas fa-exchange-alt', }, - splitStrByLine: { in: ['string'], out: 'stringArray', category: 'convert', icon: 'fas fa-exchange-alt', }, - pick: { in: [null, 'number'], out: null, category: 'list', icon: 'fas fa-indent', }, - listLen: { in: [null], out: 'number', category: 'list', icon: 'fas fa-indent', }, - rannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice', }, - dailyRannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice', }, - seedRannum: { in: [null, 'number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice', }, - random: { in: ['number'], out: 'boolean', category: 'random', icon: 'fas fa-dice', }, - dailyRandom: { in: ['number'], out: 'boolean', category: 'random', icon: 'fas fa-dice', }, - seedRandom: { in: [null, 'number'], out: 'boolean', category: 'random', icon: 'fas fa-dice', }, - randomPick: { in: [0], out: 0, category: 'random', icon: 'fas fa-dice', }, - dailyRandomPick: { in: [0], out: 0, category: 'random', icon: 'fas fa-dice', }, - seedRandomPick: { in: [null, 0], out: 0, category: 'random', icon: 'fas fa-dice', }, - DRPWPM: { in: ['stringArray'], out: 'string', category: 'random', icon: 'fas fa-dice', }, // dailyRandomPickWithProbabilityMapping -}; - -export function initHpmlLib(expr: Expr, scope: HpmlScope, randomSeed: string, visitor?: any) { - - const date = new Date(); - const day = `${visitor ? visitor.id : ''} ${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; - - const funcs: Record<string, Function> = { - not: (a: boolean) => !a, - or: (a: boolean, b: boolean) => a || b, - and: (a: boolean, b: boolean) => a && b, - eq: (a: any, b: any) => a === b, - notEq: (a: any, b: any) => a !== b, - gt: (a: number, b: number) => a > b, - lt: (a: number, b: number) => a < b, - gtEq: (a: number, b: number) => a >= b, - ltEq: (a: number, b: number) => a <= b, - if: (bool: boolean, a: any, b: any) => bool ? a : b, - for: (times: number, fn: Fn) => { - const result: any[] = []; - for (let i = 0; i < times; i++) { - result.push(fn.exec({ - [fn.slots[0]]: i + 1 - })); - } - return result; - }, - add: (a: number, b: number) => a + b, - subtract: (a: number, b: number) => a - b, - multiply: (a: number, b: number) => a * b, - divide: (a: number, b: number) => a / b, - mod: (a: number, b: number) => a % b, - round: (a: number) => Math.round(a), - strLen: (a: string) => a.length, - strPick: (a: string, b: number) => a[b - 1], - strReplace: (a: string, b: string, c: string) => a.split(b).join(c), - strReverse: (a: string) => a.split('').reverse().join(''), - join: (texts: string[], separator: string) => texts.join(separator || ''), - stringToNumber: (a: string) => parseInt(a), - numberToString: (a: number) => a.toString(), - splitStrByLine: (a: string) => a.split('\n'), - pick: (list: any[], i: number) => list[i - 1], - listLen: (list: any[]) => list.length, - random: (probability: number) => Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * 100) < probability, - rannum: (min: number, max: number) => min + Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * (max - min + 1)), - randomPick: (list: any[]) => list[Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * list.length)], - dailyRandom: (probability: number) => Math.floor(seedrandom(`${day}:${expr.id}`)() * 100) < probability, - dailyRannum: (min: number, max: number) => min + Math.floor(seedrandom(`${day}:${expr.id}`)() * (max - min + 1)), - dailyRandomPick: (list: any[]) => list[Math.floor(seedrandom(`${day}:${expr.id}`)() * list.length)], - seedRandom: (seed: any, probability: number) => Math.floor(seedrandom(seed)() * 100) < probability, - seedRannum: (seed: any, min: number, max: number) => min + Math.floor(seedrandom(seed)() * (max - min + 1)), - seedRandomPick: (seed: any, list: any[]) => list[Math.floor(seedrandom(seed)() * list.length)], - DRPWPM: (list: string[]) => { - const xs: any[] = []; - let totalFactor = 0; - for (const x of list) { - const parts = x.split(' '); - const factor = parseInt(parts.pop()!, 10); - const text = parts.join(' '); - totalFactor += factor; - xs.push({ factor, text }); - } - const r = seedrandom(`${day}:${expr.id}`)() * totalFactor; - let stackedFactor = 0; - for (const x of xs) { - if (r >= stackedFactor && r <= stackedFactor + x.factor) { - return x.text; - } else { - stackedFactor += x.factor; - } - } - return xs[0].text; - }, - }; - - return funcs; -} diff --git a/src/client/scripts/hpml/type-checker.ts b/src/client/scripts/hpml/type-checker.ts deleted file mode 100644 index 9633b3cd01..0000000000 --- a/src/client/scripts/hpml/type-checker.ts +++ /dev/null @@ -1,189 +0,0 @@ -import autobind from 'autobind-decorator'; -import { Type, envVarsDef, PageVar } from '.'; -import { Expr, isLiteralValue, Variable } from './expr'; -import { funcDefs } from './lib'; - -type TypeError = { - arg: number; - expect: Type; - actual: Type; -}; - -/** - * Hpml type checker - */ -export class HpmlTypeChecker { - public variables: Variable[]; - public pageVars: PageVar[]; - - constructor(variables: HpmlTypeChecker['variables'] = [], pageVars: HpmlTypeChecker['pageVars'] = []) { - this.variables = variables; - this.pageVars = pageVars; - } - - @autobind - public typeCheck(v: Expr): TypeError | null { - if (isLiteralValue(v)) return null; - - const def = funcDefs[v.type || '']; - if (def == null) { - throw new Error('Unknown type: ' + v.type); - } - - const generic: Type[] = []; - - for (let i = 0; i < def.in.length; i++) { - const arg = def.in[i]; - const type = this.infer(v.args[i]); - if (type === null) continue; - - if (typeof arg === 'number') { - if (generic[arg] === undefined) { - generic[arg] = type; - } else if (type !== generic[arg]) { - return { - arg: i, - expect: generic[arg], - actual: type - }; - } - } else if (type !== arg) { - return { - arg: i, - expect: arg, - actual: type - }; - } - } - - return null; - } - - @autobind - public getExpectedType(v: Expr, slot: number): Type { - const def = funcDefs[v.type || '']; - if (def == null) { - throw new Error('Unknown type: ' + v.type); - } - - const generic: Type[] = []; - - for (let i = 0; i < def.in.length; i++) { - const arg = def.in[i]; - const type = this.infer(v.args[i]); - if (type === null) continue; - - if (typeof arg === 'number') { - if (generic[arg] === undefined) { - generic[arg] = type; - } - } - } - - if (typeof def.in[slot] === 'number') { - return generic[def.in[slot]] || null; - } else { - return def.in[slot]; - } - } - - @autobind - public infer(v: Expr): Type { - if (v.type === null) return null; - if (v.type === 'text') return 'string'; - if (v.type === 'multiLineText') return 'string'; - if (v.type === 'textList') return 'stringArray'; - if (v.type === 'number') return 'number'; - if (v.type === 'ref') { - const variable = this.variables.find(va => va.name === v.value); - if (variable) { - return this.infer(variable); - } - - const pageVar = this.pageVars.find(va => va.name === v.value); - if (pageVar) { - return pageVar.type; - } - - const envVar = envVarsDef[v.value || '']; - if (envVar !== undefined) { - return envVar; - } - - return null; - } - if (v.type === 'aiScriptVar') return null; - if (v.type === 'fn') return null; // todo - if (v.type.startsWith('fn:')) return null; // todo - - const generic: Type[] = []; - - const def = funcDefs[v.type]; - - for (let i = 0; i < def.in.length; i++) { - const arg = def.in[i]; - if (typeof arg === 'number') { - const type = this.infer(v.args[i]); - - if (generic[arg] === undefined) { - generic[arg] = type; - } else { - if (type !== generic[arg]) { - generic[arg] = null; - } - } - } - } - - if (typeof def.out === 'number') { - return generic[def.out]; - } else { - return def.out; - } - } - - @autobind - public getVarByName(name: string): Variable { - const v = this.variables.find(x => x.name === name); - if (v !== undefined) { - return v; - } else { - throw new Error(`No such variable '${name}'`); - } - } - - @autobind - public getVarsByType(type: Type): Variable[] { - if (type == null) return this.variables; - return this.variables.filter(x => (this.infer(x) === null) || (this.infer(x) === type)); - } - - @autobind - public getEnvVarsByType(type: Type): string[] { - if (type == null) return Object.keys(envVarsDef); - return Object.entries(envVarsDef).filter(([k, v]) => v === null || type === v).map(([k, v]) => k); - } - - @autobind - public getPageVarsByType(type: Type): string[] { - if (type == null) return this.pageVars.map(v => v.name); - return this.pageVars.filter(v => type === v.type).map(v => v.name); - } - - @autobind - public isUsedName(name: string) { - if (this.variables.some(v => v.name === name)) { - return true; - } - - if (this.pageVars.some(v => v.name === name)) { - return true; - } - - if (envVarsDef[name]) { - return true; - } - - return false; - } -} diff --git a/src/client/scripts/idb-proxy.ts b/src/client/scripts/idb-proxy.ts deleted file mode 100644 index 5f76ae30bb..0000000000 --- a/src/client/scripts/idb-proxy.ts +++ /dev/null @@ -1,37 +0,0 @@ -// FirefoxのプライベートモードなどではindexedDBが使用不可能なので、 -// indexedDBが使えない環境ではlocalStorageを使う -import { - get as iget, - set as iset, - del as idel, -} from 'idb-keyval'; - -const fallbackName = (key: string) => `idbfallback::${key}`; - -let idbAvailable = typeof window !== 'undefined' ? !!window.indexedDB : true; - -if (idbAvailable) { - try { - await iset('idb-test', 'test'); - } catch (e) { - console.error('idb error', e); - idbAvailable = false; - } -} - -if (!idbAvailable) console.error('indexedDB is unavailable. It will use localStorage.'); - -export async function get(key: string) { - if (idbAvailable) return iget(key); - return JSON.parse(localStorage.getItem(fallbackName(key))); -} - -export async function set(key: string, val: any) { - if (idbAvailable) return iset(key, val); - return localStorage.setItem(fallbackName(key), JSON.stringify(val)); -} - -export async function del(key: string) { - if (idbAvailable) return idel(key); - return localStorage.removeItem(fallbackName(key)); -} diff --git a/src/client/scripts/initialize-sw.ts b/src/client/scripts/initialize-sw.ts deleted file mode 100644 index 6f1874572a..0000000000 --- a/src/client/scripts/initialize-sw.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { instance } from '@client/instance'; -import { $i } from '@client/account'; -import { api } from '@client/os'; -import { lang } from '@client/config'; - -export async function initializeSw() { - if (instance.swPublickey && - ('serviceWorker' in navigator) && - ('PushManager' in window) && - $i && $i.token) { - navigator.serviceWorker.register(`/sw.js`); - - navigator.serviceWorker.ready.then(registration => { - registration.active?.postMessage({ - msg: 'initialize', - lang, - }); - // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters - registration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array(instance.swPublickey) - }).then(subscription => { - function encode(buffer: ArrayBuffer | null) { - return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer))); - } - - // Register - api('sw/register', { - endpoint: subscription.endpoint, - auth: encode(subscription.getKey('auth')), - publickey: encode(subscription.getKey('p256dh')) - }); - }) - // When subscribe failed - .catch(async (err: Error) => { - // 通知が許可されていなかったとき - if (err.name === 'NotAllowedError') { - return; - } - - // 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが - // 既に存在していることが原因でエラーになった可能性があるので、 - // そのサブスクリプションを解除しておく - const subscription = await registration.pushManager.getSubscription(); - if (subscription) subscription.unsubscribe(); - }); - }); - } -} - -/** - * Convert the URL safe base64 string to a Uint8Array - * @param base64String base64 string - */ -function urlBase64ToUint8Array(base64String: string): Uint8Array { - const padding = '='.repeat((4 - base64String.length % 4) % 4); - const base64 = (base64String + padding) - .replace(/-/g, '+') - .replace(/_/g, '/'); - - const rawData = window.atob(base64); - const outputArray = new Uint8Array(rawData.length); - - for (let i = 0; i < rawData.length; ++i) { - outputArray[i] = rawData.charCodeAt(i); - } - return outputArray; -} diff --git a/src/client/scripts/is-device-darkmode.ts b/src/client/scripts/is-device-darkmode.ts deleted file mode 100644 index 854f38e517..0000000000 --- a/src/client/scripts/is-device-darkmode.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function isDeviceDarkmode() { - return window.matchMedia('(prefers-color-scheme: dark)').matches; -} diff --git a/src/client/scripts/is-device-touch.ts b/src/client/scripts/is-device-touch.ts deleted file mode 100644 index 3f0bfefed2..0000000000 --- a/src/client/scripts/is-device-touch.ts +++ /dev/null @@ -1 +0,0 @@ -export const isDeviceTouch = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0; diff --git a/src/client/scripts/is-mobile.ts b/src/client/scripts/is-mobile.ts deleted file mode 100644 index 60cb59f91e..0000000000 --- a/src/client/scripts/is-mobile.ts +++ /dev/null @@ -1,2 +0,0 @@ -const ua = navigator.userAgent.toLowerCase(); -export const isMobile = /mobile|iphone|ipad|android/.test(ua); diff --git a/src/client/scripts/keycode.ts b/src/client/scripts/keycode.ts deleted file mode 100644 index c127d54bb2..0000000000 --- a/src/client/scripts/keycode.ts +++ /dev/null @@ -1,33 +0,0 @@ -export default (input: string): string[] => { - if (Object.keys(aliases).some(a => a.toLowerCase() === input.toLowerCase())) { - const codes = aliases[input]; - return Array.isArray(codes) ? codes : [codes]; - } else { - return [input]; - } -}; - -export const aliases = { - 'esc': 'Escape', - 'enter': ['Enter', 'NumpadEnter'], - 'up': 'ArrowUp', - 'down': 'ArrowDown', - 'left': 'ArrowLeft', - 'right': 'ArrowRight', - 'plus': ['NumpadAdd', 'Semicolon'], -}; - -/*! -* Programatically add the following -*/ - -// lower case chars -for (let i = 97; i < 123; i++) { - const char = String.fromCharCode(i); - aliases[char] = `Key${char.toUpperCase()}`; -} - -// numbers -for (let i = 0; i < 10; i++) { - aliases[i] = [`Numpad${i}`, `Digit${i}`]; -} diff --git a/src/client/scripts/loading.ts b/src/client/scripts/loading.ts deleted file mode 100644 index 4b0a560e34..0000000000 --- a/src/client/scripts/loading.ts +++ /dev/null @@ -1,11 +0,0 @@ -export default { - start: () => { - // TODO - }, - done: () => { - // TODO - }, - set: val => { - // TODO - } -}; diff --git a/src/client/scripts/login-id.ts b/src/client/scripts/login-id.ts deleted file mode 100644 index 0f9c6be4a9..0000000000 --- a/src/client/scripts/login-id.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function getUrlWithLoginId(url: string, loginId: string) { - const u = new URL(url, origin); - u.searchParams.append('loginId', loginId); - return u.toString(); -} - -export function getUrlWithoutLoginId(url: string) { - const u = new URL(url); - u.searchParams.delete('loginId'); - return u.toString(); -} diff --git a/src/client/scripts/lookup-user.ts b/src/client/scripts/lookup-user.ts deleted file mode 100644 index c393472ae8..0000000000 --- a/src/client/scripts/lookup-user.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { parseAcct } from '@/misc/acct'; -import { i18n } from '@client/i18n'; -import * as os from '@client/os'; - -export async function lookupUser() { - const { canceled, result } = await os.dialog({ - title: i18n.locale.usernameOrUserId, - input: true - }); - if (canceled) return; - - const show = (user) => { - os.pageWindow(`/user-info/${user.id}`); - }; - - const usernamePromise = os.api('users/show', parseAcct(result)); - const idPromise = os.api('users/show', { userId: result }); - let _notFound = false; - const notFound = () => { - if (_notFound) { - os.dialog({ - type: 'error', - text: i18n.locale.noSuchUser - }); - } else { - _notFound = true; - } - }; - usernamePromise.then(show).catch(e => { - if (e.code === 'NO_SUCH_USER') { - notFound(); - } - }); - idPromise.then(show).catch(e => { - notFound(); - }); -} diff --git a/src/client/scripts/paging.ts b/src/client/scripts/paging.ts deleted file mode 100644 index 1da518efa1..0000000000 --- a/src/client/scripts/paging.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { markRaw } from 'vue'; -import * as os from '@client/os'; -import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from './scroll'; - -const SECOND_FETCH_LIMIT = 30; - -// reversed: items 配列の中身を逆順にする(新しい方が最後) - -export default (opts) => ({ - emits: ['queue'], - - data() { - return { - items: [], - queue: [], - offset: 0, - fetching: true, - moreFetching: false, - inited: false, - more: false, - backed: false, // 遡り中か否か - isBackTop: false, - }; - }, - - computed: { - empty(): boolean { - return this.items.length === 0 && !this.fetching && this.inited; - }, - - error(): boolean { - return !this.fetching && !this.inited; - }, - }, - - watch: { - pagination: { - handler() { - this.init(); - }, - deep: true - }, - - queue: { - handler(a, b) { - if (a.length === 0 && b.length === 0) return; - this.$emit('queue', this.queue.length); - }, - deep: true - } - }, - - created() { - opts.displayLimit = opts.displayLimit || 30; - this.init(); - }, - - activated() { - this.isBackTop = false; - }, - - deactivated() { - this.isBackTop = window.scrollY === 0; - }, - - methods: { - reload() { - this.items = []; - this.init(); - }, - - replaceItem(finder, data) { - const i = this.items.findIndex(finder); - this.items[i] = data; - }, - - removeItem(finder) { - const i = this.items.findIndex(finder); - this.items.splice(i, 1); - }, - - async init() { - this.queue = []; - this.fetching = true; - if (opts.before) opts.before(this); - let params = typeof this.pagination.params === 'function' ? this.pagination.params(true) : this.pagination.params; - if (params && params.then) params = await params; - if (params === null) return; - const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint; - await os.api(endpoint, { - ...params, - limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1, - }).then(items => { - for (let i = 0; i < items.length; i++) { - const item = items[i]; - markRaw(item); - if (this.pagination.reversed) { - if (i === items.length - 2) item._shouldInsertAd_ = true; - } else { - if (i === 3) item._shouldInsertAd_ = true; - } - } - if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) { - items.pop(); - this.items = this.pagination.reversed ? [...items].reverse() : items; - this.more = true; - } else { - this.items = this.pagination.reversed ? [...items].reverse() : items; - this.more = false; - } - this.offset = items.length; - this.inited = true; - this.fetching = false; - if (opts.after) opts.after(this, null); - }, e => { - this.fetching = false; - if (opts.after) opts.after(this, e); - }); - }, - - async fetchMore() { - if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return; - this.moreFetching = true; - this.backed = true; - let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params; - if (params && params.then) params = await params; - const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint; - await os.api(endpoint, { - ...params, - limit: SECOND_FETCH_LIMIT + 1, - ...(this.pagination.offsetMode ? { - offset: this.offset, - } : { - untilId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id, - }), - }).then(items => { - for (let i = 0; i < items.length; i++) { - const item = items[i]; - markRaw(item); - if (this.pagination.reversed) { - if (i === items.length - 9) item._shouldInsertAd_ = true; - } else { - if (i === 10) item._shouldInsertAd_ = true; - } - } - if (items.length > SECOND_FETCH_LIMIT) { - items.pop(); - this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items); - this.more = true; - } else { - this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items); - this.more = false; - } - this.offset += items.length; - this.moreFetching = false; - }, e => { - this.moreFetching = false; - }); - }, - - async fetchMoreFeature() { - if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return; - this.moreFetching = true; - let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params; - if (params && params.then) params = await params; - const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint; - await os.api(endpoint, { - ...params, - limit: SECOND_FETCH_LIMIT + 1, - ...(this.pagination.offsetMode ? { - offset: this.offset, - } : { - sinceId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id, - }), - }).then(items => { - for (const item of items) { - markRaw(item); - } - if (items.length > SECOND_FETCH_LIMIT) { - items.pop(); - this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items); - this.more = true; - } else { - this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items); - this.more = false; - } - this.offset += items.length; - this.moreFetching = false; - }, e => { - this.moreFetching = false; - }); - }, - - prepend(item) { - if (this.pagination.reversed) { - const container = getScrollContainer(this.$el); - const pos = getScrollPosition(this.$el); - const viewHeight = container.clientHeight; - const height = container.scrollHeight; - const isBottom = (pos + viewHeight > height - 32); - if (isBottom) { - // オーバーフローしたら古いアイテムは捨てる - if (this.items.length >= opts.displayLimit) { - // このやり方だとVue 3.2以降アニメーションが動かなくなる - //this.items = this.items.slice(-opts.displayLimit); - while (this.items.length >= opts.displayLimit) { - this.items.shift(); - } - this.more = true; - } - } - this.items.push(item); - // TODO - } else { - const isTop = this.isBackTop || (document.body.contains(this.$el) && isTopVisible(this.$el)); - - if (isTop) { - // Prepend the item - this.items.unshift(item); - - // オーバーフローしたら古いアイテムは捨てる - if (this.items.length >= opts.displayLimit) { - // このやり方だとVue 3.2以降アニメーションが動かなくなる - //this.items = this.items.slice(0, opts.displayLimit); - while (this.items.length >= opts.displayLimit) { - this.items.pop(); - } - this.more = true; - } - } else { - this.queue.push(item); - onScrollTop(this.$el, () => { - for (const item of this.queue) { - this.prepend(item); - } - this.queue = []; - }); - } - } - }, - - append(item) { - this.items.push(item); - }, - } -}); diff --git a/src/client/scripts/physics.ts b/src/client/scripts/physics.ts deleted file mode 100644 index 445b6296eb..0000000000 --- a/src/client/scripts/physics.ts +++ /dev/null @@ -1,152 +0,0 @@ -import * as Matter from 'matter-js'; - -export function physics(container: HTMLElement) { - const containerWidth = container.offsetWidth; - const containerHeight = container.offsetHeight; - const containerCenterX = containerWidth / 2; - - // サイズ固定化(要らないかも?) - container.style.position = 'relative'; - container.style.boxSizing = 'border-box'; - container.style.width = `${containerWidth}px`; - container.style.height = `${containerHeight}px`; - - // create engine - const engine = Matter.Engine.create({ - constraintIterations: 4, - positionIterations: 8, - velocityIterations: 8, - }); - - const world = engine.world; - - // create renderer - const render = Matter.Render.create({ - engine: engine, - //element: document.getElementById('debug'), - options: { - width: containerWidth, - height: containerHeight, - background: 'transparent', // transparent to hide - wireframeBackground: 'transparent', // transparent to hide - } - }); - - // Disable to hide debug - Matter.Render.run(render); - - // create runner - const runner = Matter.Runner.create(); - Matter.Runner.run(runner, engine); - - const groundThickness = 1024; - const ground = Matter.Bodies.rectangle(containerCenterX, containerHeight + (groundThickness / 2), containerWidth, groundThickness, { - isStatic: true, - restitution: 0.1, - friction: 2 - }); - - //const wallRight = Matter.Bodies.rectangle(window.innerWidth+50, window.innerHeight/2, 100, window.innerHeight, wallopts); - //const wallLeft = Matter.Bodies.rectangle(-50, window.innerHeight/2, 100, window.innerHeight, wallopts); - - Matter.World.add(world, [ - ground, - //wallRight, - //wallLeft, - ]); - - const objEls = Array.from(container.children); - const objs = []; - for (const objEl of objEls) { - const left = objEl.dataset.physicsX ? parseInt(objEl.dataset.physicsX) : objEl.offsetLeft; - const top = objEl.dataset.physicsY ? parseInt(objEl.dataset.physicsY) : objEl.offsetTop; - - let obj; - if (objEl.classList.contains('_physics_circle_')) { - obj = Matter.Bodies.circle( - left + (objEl.offsetWidth / 2), - top + (objEl.offsetHeight / 2), - Math.max(objEl.offsetWidth, objEl.offsetHeight) / 2, - { - restitution: 0.5 - } - ); - } else { - const style = window.getComputedStyle(objEl); - obj = Matter.Bodies.rectangle( - left + (objEl.offsetWidth / 2), - top + (objEl.offsetHeight / 2), - objEl.offsetWidth, - objEl.offsetHeight, - { - chamfer: { radius: parseInt(style.borderRadius || '0', 10) }, - restitution: 0.5 - } - ); - } - objEl.id = obj.id; - objs.push(obj); - } - - Matter.World.add(engine.world, objs); - - // Add mouse control - - const mouse = Matter.Mouse.create(container); - const mouseConstraint = Matter.MouseConstraint.create(engine, { - mouse: mouse, - constraint: { - stiffness: 0.1, - render: { - visible: false - } - } - }); - - Matter.World.add(engine.world, mouseConstraint); - - // keep the mouse in sync with rendering - render.mouse = mouse; - - for (const objEl of objEls) { - objEl.style.position = `absolute`; - objEl.style.top = 0; - objEl.style.left = 0; - objEl.style.margin = 0; - } - - window.requestAnimationFrame(update); - - let stop = false; - - function update() { - for (const objEl of objEls) { - const obj = objs.find(obj => obj.id.toString() === objEl.id.toString()); - if (obj == null) continue; - - const x = (obj.position.x - objEl.offsetWidth / 2); - const y = (obj.position.y - objEl.offsetHeight / 2); - const angle = obj.angle; - objEl.style.transform = `translate(${x}px, ${y}px) rotate(${angle}rad)`; - } - - if (!stop) { - window.requestAnimationFrame(update); - } - } - - // 奈落に落ちたオブジェクトは消す - const intervalId = setInterval(() => { - for (const obj of objs) { - if (obj.position.y > (containerHeight + 1024)) Matter.World.remove(world, obj); - } - }, 1000 * 10); - - return { - stop: () => { - stop = true; - Matter.Runner.stop(runner); - clearInterval(intervalId); - } - }; -} diff --git a/src/client/scripts/please-login.ts b/src/client/scripts/please-login.ts deleted file mode 100644 index a584e9fa96..0000000000 --- a/src/client/scripts/please-login.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { $i } from '@client/account'; -import { i18n } from '@client/i18n'; -import { dialog } from '@client/os'; - -export function pleaseLogin() { - if ($i) return; - - dialog({ - title: i18n.locale.signinRequired, - text: null - }); - - throw new Error('signin required'); -} diff --git a/src/client/scripts/popout.ts b/src/client/scripts/popout.ts deleted file mode 100644 index 6d92af4a05..0000000000 --- a/src/client/scripts/popout.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as config from '@client/config'; - -export function popout(path: string, w?: HTMLElement) { - let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path; - url += '?zen'; // TODO: ちゃんとURLパースしてクエリ付ける - if (w) { - const position = w.getBoundingClientRect(); - const width = parseInt(getComputedStyle(w, '').width, 10); - const height = parseInt(getComputedStyle(w, '').height, 10); - const x = window.screenX + position.left; - const y = window.screenY + position.top; - window.open(url, url, - `width=${width}, height=${height}, top=${y}, left=${x}`); - } else { - const width = 400; - const height = 500; - const x = window.top.outerHeight / 2 + window.top.screenY - (height / 2); - const y = window.top.outerWidth / 2 + window.top.screenX - (width / 2); - window.open(url, url, - `width=${width}, height=${height}, top=${x}, left=${y}`); - } -} diff --git a/src/client/scripts/reaction-picker.ts b/src/client/scripts/reaction-picker.ts deleted file mode 100644 index 38699c0979..0000000000 --- a/src/client/scripts/reaction-picker.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Ref, ref } from 'vue'; -import { popup } from '@client/os'; - -class ReactionPicker { - private src: Ref<HTMLElement | null> = ref(null); - private manualShowing = ref(false); - private onChosen?: Function; - private onClosed?: Function; - - constructor() { - // nop - } - - public async init() { - await popup(import('@client/components/emoji-picker-dialog.vue'), { - src: this.src, - asReactionPicker: true, - manualShowing: this.manualShowing - }, { - done: reaction => { - this.onChosen!(reaction); - }, - close: () => { - this.manualShowing.value = false; - }, - closed: () => { - this.src.value = null; - this.onClosed!(); - } - }); - } - - public show(src: HTMLElement, onChosen: Function, onClosed: Function) { - this.src.value = src; - this.manualShowing.value = true; - this.onChosen = onChosen; - this.onClosed = onClosed; - } -} - -export const reactionPicker = new ReactionPicker(); diff --git a/src/client/scripts/room/furniture.ts b/src/client/scripts/room/furniture.ts deleted file mode 100644 index 7734e32668..0000000000 --- a/src/client/scripts/room/furniture.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type RoomInfo = { - roomType: string; - carpetColor: string; - furnitures: Furniture[]; -}; - -export type Furniture = { - id: string; // 同じ家具が複数ある場合にそれぞれを識別するためのIDであり、家具IDではない - type: string; // こっちが家具ID(chairとか) - position: { - x: number; - y: number; - z: number; - }; - rotation: { - x: number; - y: number; - z: number; - }; - props?: Record<string, any>; -}; diff --git a/src/client/scripts/room/furnitures.json5 b/src/client/scripts/room/furnitures.json5 deleted file mode 100644 index 4a40994107..0000000000 --- a/src/client/scripts/room/furnitures.json5 +++ /dev/null @@ -1,407 +0,0 @@ -// 家具メタデータ - -// 家具IDはglbファイル及びそのディレクトリ名と一致する必要があります - -// 家具にはユーザーが設定できるプロパティを設定可能です: -// -// props: { -// <propname>: <proptype> -// } -// -// proptype一覧: -// * image ... 画像選択ダイアログを出し、その画像のURLが格納されます -// * color ... 色選択コントロールを出し、選択された色が格納されます - -// 家具にカスタムテクスチャを適用できるようにするには、textureプロパティに以下の追加の情報を含めます: -// 便宜上そのUVのどの部分にカスタムテクスチャを貼り合わせるかのエリアをテクスチャエリアと呼びます。 -// UVは1024*1024だと仮定します。 -// -// <key>: { -// prop: <プロパティ名>, -// uv: { -// x: <テクスチャエリアX座標>, -// y: <テクスチャエリアY座標>, -// width: <テクスチャエリアの幅>, -// height: <テクスチャエリアの高さ>, -// }, -// } -// -// <key>には、カスタムテクスチャを適用したいメッシュ名を指定します -// <プロパティ名>には、カスタムテクスチャとして使用する画像を格納するプロパティ(前述)名を指定します - -// 家具にカスタムカラーを適用できるようにするには、colorプロパティに以下の追加の情報を含めます: -// -// <key>: <プロパティ名> -// -// <key>には、カスタムカラーを適用したいマテリアル名を指定します -// <プロパティ名>には、カスタムカラーとして使用する色を格納するプロパティ(前述)名を指定します - -[ - { - id: "milk", - place: "floor" - }, - { - id: "bed", - place: "floor" - }, - { - id: "low-table", - place: "floor", - props: { - color: 'color' - }, - color: { - Table: 'color' - } - }, - { - id: "desk", - place: "floor", - props: { - color: 'color' - }, - color: { - Board: 'color' - } - }, - { - id: "chair", - place: "floor", - props: { - color: 'color' - }, - color: { - Chair: 'color' - } - }, - { - id: "chair2", - place: "floor", - props: { - color1: 'color', - color2: 'color' - }, - color: { - Cushion: 'color1', - Leg: 'color2' - } - }, - { - id: "fan", - place: "wall" - }, - { - id: "pc", - place: "floor" - }, - { - id: "plant", - place: "floor" - }, - { - id: "plant2", - place: "floor" - }, - { - id: "eraser", - place: "floor" - }, - { - id: "pencil", - place: "floor" - }, - { - id: "pudding", - place: "floor" - }, - { - id: "cardboard-box", - place: "floor" - }, - { - id: "cardboard-box2", - place: "floor" - }, - { - id: "cardboard-box3", - place: "floor" - }, - { - id: "book", - place: "floor", - props: { - color: 'color' - }, - color: { - Cover: 'color' - } - }, - { - id: "book2", - place: "floor" - }, - { - id: "piano", - place: "floor" - }, - { - id: "facial-tissue", - place: "floor" - }, - { - id: "server", - place: "floor" - }, - { - id: "moon", - place: "floor" - }, - { - id: "corkboard", - place: "wall" - }, - { - id: "mousepad", - place: "floor", - props: { - color: 'color' - }, - color: { - Pad: 'color' - } - }, - { - id: "monitor", - place: "floor", - props: { - screen: 'image' - }, - texture: { - Screen: { - prop: 'screen', - uv: { - x: 0, - y: 434, - width: 1024, - height: 588, - }, - }, - }, - }, - { - id: "tv", - place: "floor", - props: { - screen: 'image' - }, - texture: { - Screen: { - prop: 'screen', - uv: { - x: 0, - y: 434, - width: 1024, - height: 588, - }, - }, - }, - }, - { - id: "keyboard", - place: "floor" - }, - { - id: "carpet-stripe", - place: "floor", - props: { - color1: 'color', - color2: 'color' - }, - color: { - CarpetAreaA: 'color1', - CarpetAreaB: 'color2' - }, - }, - { - id: "mat", - place: "floor", - props: { - color: 'color' - }, - color: { - Mat: 'color' - } - }, - { - id: "color-box", - place: "floor", - props: { - color: 'color' - }, - color: { - main: 'color' - } - }, - { - id: "wall-clock", - place: "wall" - }, - { - id: "cube", - place: "floor", - props: { - color: 'color' - }, - color: { - Cube: 'color' - } - }, - { - id: "photoframe", - place: "wall", - props: { - photo: 'image', - color: 'color' - }, - texture: { - Photo: { - prop: 'photo', - uv: { - x: 0, - y: 342, - width: 1024, - height: 683, - }, - }, - }, - color: { - Frame: 'color' - } - }, - { - id: "pinguin", - place: "floor", - props: { - body: 'color', - belly: 'color' - }, - color: { - Body: 'body', - Belly: 'belly', - } - }, - { - id: "rubik-cube", - place: "floor", - }, - { - id: "poster-h", - place: "wall", - props: { - picture: 'image' - }, - texture: { - Poster: { - prop: 'picture', - uv: { - x: 0, - y: 277, - width: 1024, - height: 745, - }, - }, - }, - }, - { - id: "poster-v", - place: "wall", - props: { - picture: 'image' - }, - texture: { - Poster: { - prop: 'picture', - uv: { - x: 0, - y: 0, - width: 745, - height: 1024, - }, - }, - }, - }, - { - id: "sofa", - place: "floor", - props: { - color: 'color' - }, - color: { - Sofa: 'color' - } - }, - { - id: "spiral", - place: "floor", - props: { - color: 'color' - }, - color: { - Step: 'color' - } - }, - { - id: "bin", - place: "floor", - props: { - color: 'color' - }, - color: { - Bin: 'color' - } - }, - { - id: "cup-noodle", - place: "floor" - }, - { - id: "holo-display", - place: "floor", - props: { - image: 'image' - }, - texture: { - Image_Front: { - prop: 'image', - uv: { - x: 0, - y: 0, - width: 1024, - height: 1024, - }, - }, - Image_Back: { - prop: 'image', - uv: { - x: 0, - y: 0, - width: 1024, - height: 1024, - }, - }, - }, - }, - { - id: 'energy-drink', - place: "floor", - }, - { - id: 'doll-ai', - place: "floor", - }, - { - id: 'banknote', - place: "floor", - }, -] diff --git a/src/client/scripts/room/room.ts b/src/client/scripts/room/room.ts deleted file mode 100644 index 4450210c6c..0000000000 --- a/src/client/scripts/room/room.ts +++ /dev/null @@ -1,775 +0,0 @@ -import autobind from 'autobind-decorator'; -import { v4 as uuid } from 'uuid'; -import * as THREE from 'three'; -import { GLTFLoader, GLTF } from 'three/examples/jsm/loaders/GLTFLoader'; -import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; -import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'; -import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'; -import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'; -import { BloomPass } from 'three/examples/jsm/postprocessing/BloomPass.js'; -import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader.js'; -import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js'; -import { Furniture, RoomInfo } from './furniture'; -import { query as urlQuery } from '../../../prelude/url'; -const furnitureDefs = require('./furnitures.json5'); - -THREE.ImageUtils.crossOrigin = ''; - -type Options = { - graphicsQuality: Room['graphicsQuality']; - onChangeSelect: Room['onChangeSelect']; - useOrthographicCamera: boolean; -}; - -/** - * MisskeyRoom Core Engine - */ -export class Room { - private clock: THREE.Clock; - private scene: THREE.Scene; - private renderer: THREE.WebGLRenderer; - private camera: THREE.PerspectiveCamera | THREE.OrthographicCamera; - private controls: OrbitControls; - private composer: EffectComposer; - private mixers: THREE.AnimationMixer[] = []; - private furnitureControl: TransformControls; - private roomInfo: RoomInfo; - private graphicsQuality: 'cheep' | 'low' | 'medium' | 'high' | 'ultra'; - private roomObj: THREE.Object3D; - private objects: THREE.Object3D[] = []; - private selectedObject: THREE.Object3D = null; - private onChangeSelect: Function; - private isTransformMode = false; - private renderFrameRequestId: number; - - private get canvas(): HTMLCanvasElement { - return this.renderer.domElement; - } - - private get furnitures(): Furniture[] { - return this.roomInfo.furnitures; - } - - private set furnitures(furnitures: Furniture[]) { - this.roomInfo.furnitures = furnitures; - } - - private get enableShadow() { - return this.graphicsQuality != 'cheep'; - } - - private get usePostFXs() { - return this.graphicsQuality !== 'cheep' && this.graphicsQuality !== 'low'; - } - - private get shadowQuality() { - return ( - this.graphicsQuality === 'ultra' ? 16384 : - this.graphicsQuality === 'high' ? 8192 : - this.graphicsQuality === 'medium' ? 4096 : - this.graphicsQuality === 'low' ? 1024 : - 0); // cheep - } - - constructor(user, isMyRoom, roomInfo: RoomInfo, container: Element, options: Options) { - this.roomInfo = roomInfo; - this.graphicsQuality = options.graphicsQuality; - this.onChangeSelect = options.onChangeSelect; - - this.clock = new THREE.Clock(true); - - //#region Init a scene - this.scene = new THREE.Scene(); - - const width = container.clientWidth; - const height = container.clientHeight; - - //#region Init a renderer - this.renderer = new THREE.WebGLRenderer({ - antialias: false, - stencil: false, - alpha: false, - powerPreference: - this.graphicsQuality === 'ultra' ? 'high-performance' : - this.graphicsQuality === 'high' ? 'high-performance' : - this.graphicsQuality === 'medium' ? 'default' : - this.graphicsQuality === 'low' ? 'low-power' : - 'low-power' // cheep - }); - - this.renderer.setPixelRatio(window.devicePixelRatio); - this.renderer.setSize(width, height); - this.renderer.autoClear = false; - this.renderer.setClearColor(new THREE.Color(0x051f2d)); - this.renderer.shadowMap.enabled = this.enableShadow; - this.renderer.shadowMap.type = - this.graphicsQuality === 'ultra' ? THREE.PCFSoftShadowMap : - this.graphicsQuality === 'high' ? THREE.PCFSoftShadowMap : - this.graphicsQuality === 'medium' ? THREE.PCFShadowMap : - this.graphicsQuality === 'low' ? THREE.BasicShadowMap : - THREE.BasicShadowMap; // cheep - - container.insertBefore(this.canvas, container.firstChild); - //#endregion - - //#region Init a camera - this.camera = options.useOrthographicCamera - ? new THREE.OrthographicCamera( - width / - 2, width / 2, height / 2, height / - 2, -10, 10) - : new THREE.PerspectiveCamera(45, width / height); - - if (options.useOrthographicCamera) { - this.camera.position.x = 2; - this.camera.position.y = 2; - this.camera.position.z = 2; - this.camera.zoom = 100; - this.camera.updateProjectionMatrix(); - } else { - this.camera.position.x = 5; - this.camera.position.y = 2; - this.camera.position.z = 5; - } - - this.scene.add(this.camera); - //#endregion - - //#region AmbientLight - const ambientLight = new THREE.AmbientLight(0xffffff, 1); - this.scene.add(ambientLight); - //#endregion - - if (this.graphicsQuality !== 'cheep') { - //#region Room light - const roomLight = new THREE.SpotLight(0xffffff, 0.1); - - roomLight.position.set(0, 8, 0); - roomLight.castShadow = this.enableShadow; - roomLight.shadow.bias = -0.0001; - roomLight.shadow.mapSize.width = this.shadowQuality; - roomLight.shadow.mapSize.height = this.shadowQuality; - roomLight.shadow.camera.near = 0.1; - roomLight.shadow.camera.far = 9; - roomLight.shadow.camera.fov = 45; - - this.scene.add(roomLight); - //#endregion - } - - //#region Out light - const outLight1 = new THREE.SpotLight(0xffffff, 0.4); - outLight1.position.set(9, 3, -2); - outLight1.castShadow = this.enableShadow; - outLight1.shadow.bias = -0.001; // アクネ、アーチファクト対策 その代わりピーターパンが発生する可能性がある - outLight1.shadow.mapSize.width = this.shadowQuality; - outLight1.shadow.mapSize.height = this.shadowQuality; - outLight1.shadow.camera.near = 6; - outLight1.shadow.camera.far = 15; - outLight1.shadow.camera.fov = 45; - this.scene.add(outLight1); - - const outLight2 = new THREE.SpotLight(0xffffff, 0.2); - outLight2.position.set(-2, 3, 9); - outLight2.castShadow = false; - outLight2.shadow.bias = -0.001; // アクネ、アーチファクト対策 その代わりピーターパンが発生する可能性がある - outLight2.shadow.camera.near = 6; - outLight2.shadow.camera.far = 15; - outLight2.shadow.camera.fov = 45; - this.scene.add(outLight2); - //#endregion - - //#region Init a controller - this.controls = new OrbitControls(this.camera, this.canvas); - - this.controls.target.set(0, 1, 0); - this.controls.enableZoom = true; - this.controls.enablePan = isMyRoom; - this.controls.minPolarAngle = 0; - this.controls.maxPolarAngle = Math.PI / 2; - this.controls.minAzimuthAngle = 0; - this.controls.maxAzimuthAngle = Math.PI / 2; - this.controls.enableDamping = true; - this.controls.dampingFactor = 0.2; - //#endregion - - //#region POST FXs - if (!this.usePostFXs) { - this.composer = null; - } else { - const renderTarget = new THREE.WebGLRenderTarget(width, height, { - minFilter: THREE.LinearFilter, - magFilter: THREE.LinearFilter, - format: THREE.RGBFormat, - stencilBuffer: false, - }); - - const fxaa = new ShaderPass(FXAAShader); - fxaa.uniforms['resolution'].value = new THREE.Vector2(1 / width, 1 / height); - fxaa.renderToScreen = true; - - this.composer = new EffectComposer(this.renderer, renderTarget); - this.composer.addPass(new RenderPass(this.scene, this.camera)); - if (this.graphicsQuality === 'ultra') { - this.composer.addPass(new BloomPass(0.25, 30, 128.0, 512)); - } - this.composer.addPass(fxaa); - } - //#endregion - //#endregion - - //#region Label - //#region Avatar - const avatarUrl = `/proxy/?${urlQuery({ url: user.avatarUrl })}`; - - const textureLoader = new THREE.TextureLoader(); - textureLoader.crossOrigin = 'anonymous'; - - const iconTexture = textureLoader.load(avatarUrl); - iconTexture.wrapS = THREE.RepeatWrapping; - iconTexture.wrapT = THREE.RepeatWrapping; - iconTexture.anisotropy = 16; - - const avatarMaterial = new THREE.MeshBasicMaterial({ - map: iconTexture, - side: THREE.DoubleSide, - alphaTest: 0.5 - }); - - const iconGeometry = new THREE.PlaneGeometry(1, 1); - - const avatarObject = new THREE.Mesh(iconGeometry, avatarMaterial); - avatarObject.position.set(-3, 2.5, 2); - avatarObject.rotation.y = Math.PI / 2; - avatarObject.castShadow = false; - - this.scene.add(avatarObject); - //#endregion - - //#region Username - const name = user.username; - - new THREE.FontLoader().load('/assets/fonts/helvetiker_regular.typeface.json', font => { - const nameGeometry = new THREE.TextGeometry(name, { - size: 0.5, - height: 0, - curveSegments: 8, - font: font, - bevelThickness: 0, - bevelSize: 0, - bevelEnabled: false - }); - - const nameMaterial = new THREE.MeshLambertMaterial({ - color: 0xffffff - }); - - const nameObject = new THREE.Mesh(nameGeometry, nameMaterial); - nameObject.position.set(-3, 2.25, 1.25); - nameObject.rotation.y = Math.PI / 2; - nameObject.castShadow = false; - - this.scene.add(nameObject); - }); - //#endregion - //#endregion - - //#region Interaction - if (isMyRoom) { - this.furnitureControl = new TransformControls(this.camera, this.canvas); - this.scene.add(this.furnitureControl); - - // Hover highlight - this.canvas.onmousemove = this.onmousemove; - - // Click - this.canvas.onmousedown = this.onmousedown; - } - //#endregion - - //#region Init room - this.loadRoom(); - //#endregion - - //#region Load furnitures - for (const furniture of this.furnitures) { - this.loadFurniture(furniture).then(obj => { - this.scene.add(obj.scene); - this.objects.push(obj.scene); - }); - } - //#endregion - - // Start render - if (this.usePostFXs) { - this.renderWithPostFXs(); - } else { - this.renderWithoutPostFXs(); - } - } - - @autobind - private renderWithoutPostFXs() { - this.renderFrameRequestId = - window.requestAnimationFrame(this.renderWithoutPostFXs); - - // Update animations - const clock = this.clock.getDelta(); - for (const mixer of this.mixers) { - mixer.update(clock); - } - - this.controls.update(); - this.renderer.render(this.scene, this.camera); - } - - @autobind - private renderWithPostFXs() { - this.renderFrameRequestId = - window.requestAnimationFrame(this.renderWithPostFXs); - - // Update animations - const clock = this.clock.getDelta(); - for (const mixer of this.mixers) { - mixer.update(clock); - } - - this.controls.update(); - this.renderer.clear(); - this.composer.render(); - } - - @autobind - private loadRoom() { - const type = this.roomInfo.roomType; - new GLTFLoader().load(`/static-assets/client/room/rooms/${type}/${type}.glb`, gltf => { - gltf.scene.traverse(child => { - if (!(child instanceof THREE.Mesh)) return; - - child.receiveShadow = this.enableShadow; - - child.material = new THREE.MeshLambertMaterial({ - color: (child.material as THREE.MeshStandardMaterial).color, - map: (child.material as THREE.MeshStandardMaterial).map, - name: (child.material as THREE.MeshStandardMaterial).name, - }); - - // 異方性フィルタリング - if ((child.material as THREE.MeshLambertMaterial).map && this.graphicsQuality !== 'cheep') { - (child.material as THREE.MeshLambertMaterial).map.minFilter = THREE.LinearMipMapLinearFilter; - (child.material as THREE.MeshLambertMaterial).map.magFilter = THREE.LinearMipMapLinearFilter; - (child.material as THREE.MeshLambertMaterial).map.anisotropy = 8; - } - }); - - gltf.scene.position.set(0, 0, 0); - - this.scene.add(gltf.scene); - this.roomObj = gltf.scene; - if (this.roomInfo.roomType === 'default') { - this.applyCarpetColor(); - } - }); - } - - @autobind - private loadFurniture(furniture: Furniture) { - const def = furnitureDefs.find(d => d.id === furniture.type); - return new Promise<GLTF>((res, rej) => { - const loader = new GLTFLoader(); - loader.load(`/static-assets/client/room/furnitures/${furniture.type}/${furniture.type}.glb`, gltf => { - const model = gltf.scene; - - // Load animation - if (gltf.animations.length > 0) { - const mixer = new THREE.AnimationMixer(model); - this.mixers.push(mixer); - for (const clip of gltf.animations) { - mixer.clipAction(clip).play(); - } - } - - model.name = furniture.id; - model.position.x = furniture.position.x; - model.position.y = furniture.position.y; - model.position.z = furniture.position.z; - model.rotation.x = furniture.rotation.x; - model.rotation.y = furniture.rotation.y; - model.rotation.z = furniture.rotation.z; - - model.traverse(child => { - if (!(child instanceof THREE.Mesh)) return; - child.castShadow = this.enableShadow; - child.receiveShadow = this.enableShadow; - (child.material as THREE.MeshStandardMaterial).metalness = 0; - - // 異方性フィルタリング - if ((child.material as THREE.MeshStandardMaterial).map && this.graphicsQuality !== 'cheep') { - (child.material as THREE.MeshStandardMaterial).map.minFilter = THREE.LinearMipMapLinearFilter; - (child.material as THREE.MeshStandardMaterial).map.magFilter = THREE.LinearMipMapLinearFilter; - (child.material as THREE.MeshStandardMaterial).map.anisotropy = 8; - } - }); - - if (def.color) { // カスタムカラー - this.applyCustomColor(model); - } - - if (def.texture) { // カスタムテクスチャ - this.applyCustomTexture(model); - } - - res(gltf); - }, null, rej); - }); - } - - @autobind - private applyCarpetColor() { - this.roomObj.traverse(child => { - if (!(child instanceof THREE.Mesh)) return; - if (child.material && - (child.material as THREE.MeshStandardMaterial).name && - (child.material as THREE.MeshStandardMaterial).name === 'Carpet' - ) { - const colorHex = parseInt(this.roomInfo.carpetColor.substr(1), 16); - (child.material as THREE.MeshStandardMaterial).color.setHex(colorHex); - } - }); - } - - @autobind - private applyCustomColor(model: THREE.Object3D) { - const furniture = this.furnitures.find(furniture => furniture.id === model.name); - const def = furnitureDefs.find(d => d.id === furniture.type); - if (def.color == null) return; - model.traverse(child => { - if (!(child instanceof THREE.Mesh)) return; - for (const t of Object.keys(def.color)) { - if (!child.material || - !(child.material as THREE.MeshStandardMaterial).name || - (child.material as THREE.MeshStandardMaterial).name !== t - ) continue; - - const prop = def.color[t]; - const val = furniture.props ? furniture.props[prop] : undefined; - - if (val == null) continue; - - const colorHex = parseInt(val.substr(1), 16); - (child.material as THREE.MeshStandardMaterial).color.setHex(colorHex); - } - }); - } - - @autobind - private applyCustomTexture(model: THREE.Object3D) { - const furniture = this.furnitures.find(furniture => furniture.id === model.name); - const def = furnitureDefs.find(d => d.id === furniture.type); - if (def.texture == null) return; - - model.traverse(child => { - if (!(child instanceof THREE.Mesh)) return; - for (const t of Object.keys(def.texture)) { - if (child.name !== t) continue; - - const prop = def.texture[t].prop; - const val = furniture.props ? furniture.props[prop] : undefined; - - if (val == null) continue; - - const canvas = document.createElement('canvas'); - canvas.height = 1024; - canvas.width = 1024; - - child.material = new THREE.MeshLambertMaterial({ - emissive: 0x111111, - side: THREE.DoubleSide, - alphaTest: 0.5, - }); - - const img = new Image(); - img.crossOrigin = 'anonymous'; - img.onload = () => { - const uvInfo = def.texture[t].uv; - - const ctx = canvas.getContext('2d'); - ctx.drawImage(img, - 0, 0, img.width, img.height, - uvInfo.x, uvInfo.y, uvInfo.width, uvInfo.height); - - const texture = new THREE.Texture(canvas); - texture.wrapS = THREE.RepeatWrapping; - texture.wrapT = THREE.RepeatWrapping; - texture.anisotropy = 16; - texture.flipY = false; - - (child.material as THREE.MeshLambertMaterial).map = texture; - (child.material as THREE.MeshLambertMaterial).needsUpdate = true; - (child.material as THREE.MeshLambertMaterial).map.needsUpdate = true; - }; - img.src = val; - } - }); - } - - @autobind - private onmousemove(ev: MouseEvent) { - if (this.isTransformMode) return; - - const rect = (ev.target as HTMLElement).getBoundingClientRect(); - const x = ((ev.clientX - rect.left) / rect.width) * 2 - 1; - const y = -((ev.clientY - rect.top) / rect.height) * 2 + 1; - const pos = new THREE.Vector2(x, y); - - this.camera.updateMatrixWorld(); - - const raycaster = new THREE.Raycaster(); - raycaster.setFromCamera(pos, this.camera); - - const intersects = raycaster.intersectObjects(this.objects, true); - - for (const object of this.objects) { - if (this.isSelectedObject(object)) continue; - object.traverse(child => { - if (child instanceof THREE.Mesh) { - (child.material as THREE.MeshStandardMaterial).emissive.setHex(0x000000); - } - }); - } - - if (intersects.length > 0) { - const intersected = this.getRoot(intersects[0].object); - if (this.isSelectedObject(intersected)) return; - intersected.traverse(child => { - if (child instanceof THREE.Mesh) { - (child.material as THREE.MeshStandardMaterial).emissive.setHex(0x191919); - } - }); - } - } - - @autobind - private onmousedown(ev: MouseEvent) { - if (this.isTransformMode) return; - if (ev.target !== this.canvas || ev.button !== 0) return; - - const rect = (ev.target as HTMLElement).getBoundingClientRect(); - const x = ((ev.clientX - rect.left) / rect.width) * 2 - 1; - const y = -((ev.clientY - rect.top) / rect.height) * 2 + 1; - const pos = new THREE.Vector2(x, y); - - this.camera.updateMatrixWorld(); - - const raycaster = new THREE.Raycaster(); - raycaster.setFromCamera(pos, this.camera); - - const intersects = raycaster.intersectObjects(this.objects, true); - - for (const object of this.objects) { - object.traverse(child => { - if (child instanceof THREE.Mesh) { - (child.material as THREE.MeshStandardMaterial).emissive.setHex(0x000000); - } - }); - } - - if (intersects.length > 0) { - const selectedObj = this.getRoot(intersects[0].object); - this.selectFurniture(selectedObj); - } else { - this.selectedObject = null; - this.onChangeSelect(null); - } - } - - @autobind - private getRoot(obj: THREE.Object3D): THREE.Object3D { - let found = false; - let x = obj.parent; - while (!found) { - if (x.parent.parent == null) { - found = true; - } else { - x = x.parent; - } - } - return x; - } - - @autobind - private isSelectedObject(obj: THREE.Object3D): boolean { - if (this.selectedObject == null) { - return false; - } else { - return obj.name === this.selectedObject.name; - } - } - - @autobind - private selectFurniture(obj: THREE.Object3D) { - this.selectedObject = obj; - this.onChangeSelect(obj); - obj.traverse(child => { - if (child instanceof THREE.Mesh) { - (child.material as THREE.MeshStandardMaterial).emissive.setHex(0xff0000); - } - }); - } - - /** - * 家具の移動/回転モードにします - * @param type 移動か回転か - */ - @autobind - public enterTransformMode(type: 'translate' | 'rotate') { - this.isTransformMode = true; - this.furnitureControl.setMode(type); - this.furnitureControl.attach(this.selectedObject); - this.controls.enableRotate = false; - } - - /** - * 家具の移動/回転モードを終了します - */ - @autobind - public exitTransformMode() { - this.isTransformMode = false; - this.furnitureControl.detach(); - this.controls.enableRotate = true; - } - - /** - * 家具プロパティを更新します - * @param key プロパティ名 - * @param value 値 - */ - @autobind - public updateProp(key: string, value: any) { - const furniture = this.furnitures.find(furniture => furniture.id === this.selectedObject.name); - if (furniture.props == null) furniture.props = {}; - furniture.props[key] = value; - this.applyCustomColor(this.selectedObject); - this.applyCustomTexture(this.selectedObject); - } - - /** - * 部屋に家具を追加します - * @param type 家具の種類 - */ - @autobind - public addFurniture(type: string) { - const furniture = { - id: uuid(), - type: type, - position: { - x: 0, - y: 0, - z: 0, - }, - rotation: { - x: 0, - y: 0, - z: 0, - }, - }; - - this.furnitures.push(furniture); - - this.loadFurniture(furniture).then(obj => { - this.scene.add(obj.scene); - this.objects.push(obj.scene); - }); - } - - /** - * 現在選択されている家具を部屋から削除します - */ - @autobind - public removeFurniture() { - this.exitTransformMode(); - const obj = this.selectedObject; - this.scene.remove(obj); - this.objects = this.objects.filter(object => object.name !== obj.name); - this.furnitures = this.furnitures.filter(furniture => furniture.id !== obj.name); - this.selectedObject = null; - this.onChangeSelect(null); - } - - /** - * 全ての家具を部屋から削除します - */ - @autobind - public removeAllFurnitures() { - this.exitTransformMode(); - for (const obj of this.objects) { - this.scene.remove(obj); - } - this.objects = []; - this.furnitures = []; - this.selectedObject = null; - this.onChangeSelect(null); - } - - /** - * 部屋の床の色を変更します - * @param color 色 - */ - @autobind - public updateCarpetColor(color: string) { - this.roomInfo.carpetColor = color; - this.applyCarpetColor(); - } - - /** - * 部屋の種類を変更します - * @param type 種類 - */ - @autobind - public changeRoomType(type: string) { - this.roomInfo.roomType = type; - this.scene.remove(this.roomObj); - this.loadRoom(); - } - - /** - * 部屋データを取得します - */ - @autobind - public getRoomInfo() { - for (const obj of this.objects) { - const furniture = this.furnitures.find(f => f.id === obj.name); - furniture.position.x = obj.position.x; - furniture.position.y = obj.position.y; - furniture.position.z = obj.position.z; - furniture.rotation.x = obj.rotation.x; - furniture.rotation.y = obj.rotation.y; - furniture.rotation.z = obj.rotation.z; - } - - return this.roomInfo; - } - - /** - * 選択されている家具を取得します - */ - @autobind - public getSelectedObject() { - return this.selectedObject; - } - - @autobind - public findFurnitureById(id: string) { - return this.furnitures.find(furniture => furniture.id === id); - } - - /** - * レンダリングを終了します - */ - @autobind - public destroy() { - // Stop render loop - window.cancelAnimationFrame(this.renderFrameRequestId); - - this.controls.dispose(); - this.scene.dispose(); - } -} diff --git a/src/client/scripts/scroll.ts b/src/client/scripts/scroll.ts deleted file mode 100644 index 621fe88105..0000000000 --- a/src/client/scripts/scroll.ts +++ /dev/null @@ -1,80 +0,0 @@ -type ScrollBehavior = 'auto' | 'smooth' | 'instant'; - -export function getScrollContainer(el: Element | null): Element | null { - if (el == null || el.tagName === 'BODY') return null; - const overflow = window.getComputedStyle(el).getPropertyValue('overflow'); - if (overflow.endsWith('auto')) { // xとyを個別に指定している場合、hidden auto みたいな値になる - return el; - } else { - return getScrollContainer(el.parentElement); - } -} - -export function getScrollPosition(el: Element | null): number { - const container = getScrollContainer(el); - return container == null ? window.scrollY : container.scrollTop; -} - -export function isTopVisible(el: Element | null): boolean { - const scrollTop = getScrollPosition(el); - const topPosition = el.offsetTop; // TODO: container内でのelの相対位置を取得できればより正確になる - - return scrollTop <= topPosition; -} - -export function onScrollTop(el: Element, cb) { - const container = getScrollContainer(el) || window; - const onScroll = ev => { - if (!document.body.contains(el)) return; - if (isTopVisible(el)) { - cb(); - container.removeEventListener('scroll', onScroll); - } - }; - container.addEventListener('scroll', onScroll, { passive: true }); -} - -export function onScrollBottom(el: Element, cb) { - const container = getScrollContainer(el) || window; - const onScroll = ev => { - if (!document.body.contains(el)) return; - const pos = getScrollPosition(el); - if (pos + el.clientHeight > el.scrollHeight - 1) { - cb(); - container.removeEventListener('scroll', onScroll); - } - }; - container.addEventListener('scroll', onScroll, { passive: true }); -} - -export function scroll(el: Element, options: { - top?: number; - left?: number; - behavior?: ScrollBehavior; -}) { - const container = getScrollContainer(el); - if (container == null) { - window.scroll(options); - } else { - container.scroll(options); - } -} - -export function scrollToTop(el: Element, options: { behavior?: ScrollBehavior; } = {}) { - scroll(el, { top: 0, ...options }); -} - -export function scrollToBottom(el: Element, options: { behavior?: ScrollBehavior; } = {}) { - scroll(el, { top: 99999, ...options }); // TODO: ちゃんと計算する -} - -export function isBottom(el: Element, asobi = 0) { - const container = getScrollContainer(el); - const current = container - ? el.scrollTop + el.offsetHeight - : window.scrollY + window.innerHeight; - const max = container - ? el.scrollHeight - : document.body.offsetHeight; - return current >= (max - asobi); -} diff --git a/src/client/scripts/search.ts b/src/client/scripts/search.ts deleted file mode 100644 index 2221f5f279..0000000000 --- a/src/client/scripts/search.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as os from '@client/os'; -import { i18n } from '@client/i18n'; -import { router } from '@client/router'; - -export async function search() { - const { canceled, result: query } = await os.dialog({ - title: i18n.locale.search, - input: true - }); - if (canceled || query == null || query === '') return; - - const q = query.trim(); - - if (q.startsWith('@') && !q.includes(' ')) { - router.push(`/${q}`); - return; - } - - if (q.startsWith('#')) { - router.push(`/tags/${encodeURIComponent(q.substr(1))}`); - return; - } - - // like 2018/03/12 - if (/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}/.test(q.replace(/-/g, '/'))) { - const date = new Date(q.replace(/-/g, '/')); - - // 日付しか指定されてない場合、例えば 2018/03/12 ならユーザーは - // 2018/03/12 のコンテンツを「含む」結果になることを期待するはずなので - // 23時間59分進める(そのままだと 2018/03/12 00:00:00 「まで」の - // 結果になってしまい、2018/03/12 のコンテンツは含まれない) - if (q.replace(/-/g, '/').match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}$/)) { - date.setHours(23, 59, 59, 999); - } - - // TODO - //v.$root.$emit('warp', date); - os.dialog({ - icon: 'fas fa-history', - iconOnly: true, autoClose: true - }); - return; - } - - if (q.startsWith('https://')) { - const promise = os.api('ap/show', { - uri: q - }); - - os.promiseDialog(promise, null, null, i18n.locale.fetchingAsApObject); - - const res = await promise; - - if (res.type === 'User') { - router.push(`/@${res.object.username}@${res.object.host}`); - } else if (res.type === 'Note') { - router.push(`/notes/${res.object.id}`); - } - - return; - } - - router.push(`/search?q=${encodeURIComponent(q)}`); -} diff --git a/src/client/scripts/select-file.ts b/src/client/scripts/select-file.ts deleted file mode 100644 index f7b971e113..0000000000 --- a/src/client/scripts/select-file.ts +++ /dev/null @@ -1,89 +0,0 @@ -import * as os from '@client/os'; -import { i18n } from '@client/i18n'; -import { defaultStore } from '@client/store'; - -export function selectFile(src: any, label: string | null, multiple = false) { - return new Promise((res, rej) => { - const chooseFileFromPc = () => { - const input = document.createElement('input'); - input.type = 'file'; - input.multiple = multiple; - input.onchange = () => { - const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder)); - - Promise.all(promises).then(driveFiles => { - res(multiple ? driveFiles : driveFiles[0]); - }).catch(e => { - os.dialog({ - type: 'error', - text: e - }); - }); - - // 一応廃棄 - (window as any).__misskey_input_ref__ = null; - }; - - // https://qiita.com/fukasawah/items/b9dc732d95d99551013d - // iOS Safari で正常に動かす為のおまじない - (window as any).__misskey_input_ref__ = input; - - input.click(); - }; - - const chooseFileFromDrive = () => { - os.selectDriveFile(multiple).then(files => { - res(files); - }); - }; - - const chooseFileFromUrl = () => { - os.dialog({ - title: i18n.locale.uploadFromUrl, - input: { - placeholder: i18n.locale.uploadFromUrlDescription - } - }).then(({ canceled, result: url }) => { - if (canceled) return; - - const marker = Math.random().toString(); // TODO: UUIDとか使う - - const connection = os.stream.useChannel('main'); - connection.on('urlUploadFinished', data => { - if (data.marker === marker) { - res(multiple ? [data.file] : data.file); - connection.dispose(); - } - }); - - os.api('drive/files/upload-from-url', { - url: url, - folderId: defaultStore.state.uploadFolder, - marker - }); - - os.dialog({ - title: i18n.locale.uploadFromUrlRequested, - text: i18n.locale.uploadFromUrlMayTakeTime - }); - }); - }; - - os.popupMenu([label ? { - text: label, - type: 'label' - } : undefined, { - text: i18n.locale.upload, - icon: 'fas fa-upload', - action: chooseFileFromPc - }, { - text: i18n.locale.fromDrive, - icon: 'fas fa-cloud', - action: chooseFileFromDrive - }, { - text: i18n.locale.fromUrl, - icon: 'fas fa-link', - action: chooseFileFromUrl - }], src); - }); -} diff --git a/src/client/scripts/show-suspended-dialog.ts b/src/client/scripts/show-suspended-dialog.ts deleted file mode 100644 index dde829cdae..0000000000 --- a/src/client/scripts/show-suspended-dialog.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as os from '@client/os'; -import { i18n } from '@client/i18n'; - -export function showSuspendedDialog() { - return os.dialog({ - type: 'error', - title: i18n.locale.yourAccountSuspendedTitle, - text: i18n.locale.yourAccountSuspendedDescription - }); -} diff --git a/src/client/scripts/sound.ts b/src/client/scripts/sound.ts deleted file mode 100644 index c51fa8f215..0000000000 --- a/src/client/scripts/sound.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ColdDeviceStorage } from '@client/store'; - -const cache = new Map<string, HTMLAudioElement>(); - -export function getAudio(file: string, useCache = true): HTMLAudioElement { - let audio: HTMLAudioElement; - if (useCache && cache.has(file)) { - audio = cache.get(file); - } else { - audio = new Audio(`/static-assets/client/sounds/${file}.mp3`); - if (useCache) cache.set(file, audio); - } - return audio; -} - -export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement { - const masterVolume = ColdDeviceStorage.get('sound_masterVolume'); - audio.volume = masterVolume - ((1 - volume) * masterVolume); - return audio; -} - -export function play(type: string) { - const sound = ColdDeviceStorage.get('sound_' + type as any); - if (sound.type == null) return; - playFile(sound.type, sound.volume); -} - -export function playFile(file: string, volume: number) { - const masterVolume = ColdDeviceStorage.get('sound_masterVolume'); - if (masterVolume === 0) return; - - const audio = setVolume(getAudio(file), volume); - audio.play(); -} diff --git a/src/client/scripts/sticky-sidebar.ts b/src/client/scripts/sticky-sidebar.ts deleted file mode 100644 index c67b8f37ac..0000000000 --- a/src/client/scripts/sticky-sidebar.ts +++ /dev/null @@ -1,50 +0,0 @@ -export class StickySidebar { - private lastScrollTop = 0; - private container: HTMLElement; - private el: HTMLElement; - private spacer: HTMLElement; - private marginTop: number; - private isTop = false; - private isBottom = false; - private offsetTop: number; - private globalHeaderHeight: number = 59; - - constructor(container: StickySidebar['container'], marginTop = 0, globalHeaderHeight = 0) { - this.container = container; - this.el = this.container.children[0] as HTMLElement; - this.el.style.position = 'sticky'; - this.spacer = document.createElement('div'); - this.container.prepend(this.spacer); - this.marginTop = marginTop; - this.offsetTop = this.container.getBoundingClientRect().top; - this.globalHeaderHeight = globalHeaderHeight; - } - - public calc(scrollTop: number) { - if (scrollTop > this.lastScrollTop) { // downscroll - const overflow = Math.max(0, this.globalHeaderHeight + (this.el.clientHeight + this.marginTop) - window.innerHeight); - this.el.style.bottom = null; - this.el.style.top = `${-overflow + this.marginTop + this.globalHeaderHeight}px`; - - this.isBottom = (scrollTop + window.innerHeight) >= (this.el.offsetTop + this.el.clientHeight); - - if (this.isTop) { - this.isTop = false; - this.spacer.style.marginTop = `${Math.max(0, this.globalHeaderHeight + this.lastScrollTop + this.marginTop - this.offsetTop)}px`; - } - } else { // upscroll - const overflow = this.globalHeaderHeight + (this.el.clientHeight + this.marginTop) - window.innerHeight; - this.el.style.top = null; - this.el.style.bottom = `${-overflow}px`; - - this.isTop = scrollTop + this.marginTop + this.globalHeaderHeight <= this.el.offsetTop; - - if (this.isBottom) { - this.isBottom = false; - this.spacer.style.marginTop = `${this.globalHeaderHeight + this.lastScrollTop + this.marginTop - this.offsetTop - overflow}px`; - } - } - - this.lastScrollTop = scrollTop <= 0 ? 0 : scrollTop; - } -} diff --git a/src/client/scripts/theme-editor.ts b/src/client/scripts/theme-editor.ts deleted file mode 100644 index 3d69d2836a..0000000000 --- a/src/client/scripts/theme-editor.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { v4 as uuid} from 'uuid'; - -import { themeProps, Theme } from './theme'; - -export type Default = null; -export type Color = string; -export type FuncName = 'alpha' | 'darken' | 'lighten'; -export type Func = { type: 'func'; name: FuncName; arg: number; value: string; }; -export type RefProp = { type: 'refProp'; key: string; }; -export type RefConst = { type: 'refConst'; key: string; }; -export type Css = { type: 'css'; value: string; }; - -export type ThemeValue = Color | Func | RefProp | RefConst | Css | Default; - -export type ThemeViewModel = [ string, ThemeValue ][]; - -export const fromThemeString = (str?: string) : ThemeValue => { - if (!str) return null; - if (str.startsWith(':')) { - const parts = str.slice(1).split('<'); - const name = parts[0] as FuncName; - const arg = parseFloat(parts[1]); - const value = parts[2].startsWith('@') ? parts[2].slice(1) : ''; - return { type: 'func', name, arg, value }; - } else if (str.startsWith('@')) { - return { - type: 'refProp', - key: str.slice(1), - }; - } else if (str.startsWith('$')) { - return { - type: 'refConst', - key: str.slice(1), - }; - } else if (str.startsWith('"')) { - return { - type: 'css', - value: str.substr(1).trim(), - }; - } else { - return str; - } -}; - -export const toThemeString = (value: Color | Func | RefProp | RefConst | Css) => { - if (typeof value === 'string') return value; - switch (value.type) { - case 'func': return `:${value.name}<${value.arg}<@${value.value}`; - case 'refProp': return `@${value.key}`; - case 'refConst': return `$${value.key}`; - case 'css': return `" ${value.value}`; - } -}; - -export const convertToMisskeyTheme = (vm: ThemeViewModel, name: string, desc: string, author: string, base: 'dark' | 'light'): Theme => { - const props = { } as { [key: string]: string }; - for (const [ key, value ] of vm) { - if (value === null) continue; - props[key] = toThemeString(value); - } - - return { - id: uuid(), - name, desc, author, props, base - }; -}; - -export const convertToViewModel = (theme: Theme): ThemeViewModel => { - const vm: ThemeViewModel = []; - // プロパティの登録 - vm.push(...themeProps.map(key => [ key, fromThemeString(theme.props[key])] as [ string, ThemeValue ])); - - // 定数の登録 - const consts = Object - .keys(theme.props) - .filter(k => k.startsWith('$')) - .map(k => [ k, fromThemeString(theme.props[k]) ] as [ string, ThemeValue ]); - - vm.push(...consts); - return vm; -}; diff --git a/src/client/scripts/theme.ts b/src/client/scripts/theme.ts deleted file mode 100644 index 8b63821293..0000000000 --- a/src/client/scripts/theme.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { globalEvents } from '@client/events'; -import * as tinycolor from 'tinycolor2'; - -export type Theme = { - id: string; - name: string; - author: string; - desc?: string; - base?: 'dark' | 'light'; - props: Record<string, string>; -}; - -export const lightTheme: Theme = require('@client/themes/_light.json5'); -export const darkTheme: Theme = require('@client/themes/_dark.json5'); - -export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X')); - -export const builtinThemes = [ - require('@client/themes/l-light.json5'), - require('@client/themes/l-apricot.json5'), - require('@client/themes/l-rainy.json5'), - require('@client/themes/l-vivid.json5'), - require('@client/themes/l-sushi.json5'), - - require('@client/themes/d-dark.json5'), - require('@client/themes/d-persimmon.json5'), - require('@client/themes/d-astro.json5'), - require('@client/themes/d-future.json5'), - require('@client/themes/d-botanical.json5'), - require('@client/themes/d-pumpkin.json5'), - require('@client/themes/d-black.json5'), -] as Theme[]; - -let timeout = null; - -export function applyTheme(theme: Theme, persist = true) { - if (timeout) clearTimeout(timeout); - - document.documentElement.classList.add('_themeChanging_'); - - timeout = setTimeout(() => { - document.documentElement.classList.remove('_themeChanging_'); - }, 1000); - - // Deep copy - const _theme = JSON.parse(JSON.stringify(theme)); - - if (_theme.base) { - const base = [lightTheme, darkTheme].find(x => x.id === _theme.base); - _theme.props = Object.assign({}, base.props, _theme.props); - } - - const props = compile(_theme); - - for (const tag of document.head.children) { - if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { - tag.setAttribute('content', props['html']); - break; - } - } - - for (const [k, v] of Object.entries(props)) { - document.documentElement.style.setProperty(`--${k}`, v.toString()); - } - - if (persist) { - localStorage.setItem('theme', JSON.stringify(props)); - } - - // 色計算など再度行えるようにクライアント全体に通知 - globalEvents.emit('themeChanged'); -} - -function compile(theme: Theme): Record<string, string> { - function getColor(val: string): tinycolor.Instance { - // ref (prop) - if (val[0] === '@') { - return getColor(theme.props[val.substr(1)]); - } - - // ref (const) - else if (val[0] === '$') { - return getColor(theme.props[val]); - } - - // func - else if (val[0] === ':') { - const parts = val.split('<'); - const func = parts.shift().substr(1); - const arg = parseFloat(parts.shift()); - const color = getColor(parts.join('<')); - - switch (func) { - case 'darken': return color.darken(arg); - case 'lighten': return color.lighten(arg); - case 'alpha': return color.setAlpha(arg); - case 'hue': return color.spin(arg); - case 'saturate': return color.saturate(arg); - } - } - - // other case - return tinycolor(val); - } - - const props = {}; - - for (const [k, v] of Object.entries(theme.props)) { - if (k.startsWith('$')) continue; // ignore const - - props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v)); - } - - return props; -} - -function genValue(c: tinycolor.Instance): string { - return c.toRgbString(); -} - -export function validateTheme(theme: Record<string, any>): boolean { - if (theme.id == null || typeof theme.id !== 'string') return false; - if (theme.name == null || typeof theme.name !== 'string') return false; - if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false; - if (theme.props == null || typeof theme.props !== 'object') return false; - return true; -} diff --git a/src/client/scripts/unison-reload.ts b/src/client/scripts/unison-reload.ts deleted file mode 100644 index 59af584c1b..0000000000 --- a/src/client/scripts/unison-reload.ts +++ /dev/null @@ -1,15 +0,0 @@ -// SafariがBroadcastChannel未実装なのでライブラリを使う -import { BroadcastChannel } from 'broadcast-channel'; - -export const reloadChannel = new BroadcastChannel<string | null>('reload'); - -// BroadcastChannelを用いて、クライアントが一斉にreloadするようにします。 -export function unisonReload(path?: string) { - if (path !== undefined) { - reloadChannel.postMessage(path); - location.href = path; - } else { - reloadChannel.postMessage(null); - location.reload(); - } -} |