diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-12-27 14:36:33 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2022-12-27 14:36:33 +0900 |
| commit | 9384f5399da39e53855beb8e7f8ded1aa56bf72e (patch) | |
| tree | ce5959571a981b9c4047da3c7b3fd080aa44222c /packages/client/src/scripts | |
| parent | wip: retention for dashboard (diff) | |
| download | sharkey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.gz sharkey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.tar.bz2 sharkey-9384f5399da39e53855beb8e7f8ded1aa56bf72e.zip | |
rename: client -> frontend
Diffstat (limited to 'packages/client/src/scripts')
71 files changed, 0 insertions, 5199 deletions
diff --git a/packages/client/src/scripts/2fa.ts b/packages/client/src/scripts/2fa.ts deleted file mode 100644 index 62a38ff02a..0000000000 --- a/packages/client/src/scripts/2fa.ts +++ /dev/null @@ -1,33 +0,0 @@ -export function byteify(string: string, encoding: 'ascii' | 'base64' | 'hex') { - switch (encoding) { - case 'ascii': - return Uint8Array.from(string, c => c.charCodeAt(0)); - case 'base64': - return Uint8Array.from( - atob( - string - .replace(/-/g, '+') - .replace(/_/g, '/'), - ), - c => c.charCodeAt(0), - ); - case 'hex': - return new Uint8Array( - string - .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/packages/client/src/scripts/aiscript/api.ts b/packages/client/src/scripts/aiscript/api.ts deleted file mode 100644 index 6debcb8a13..0000000000 --- a/packages/client/src/scripts/aiscript/api.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { utils, values } from '@syuilo/aiscript'; -import * as os from '@/os'; -import { $i } from '@/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.alert({ - type: type ? type.value : 'info', - title: title.value, - text: text.value, - }); - }), - 'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => { - const confirm = await os.confirm({ - type: type ? type.value : 'question', - 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/packages/client/src/scripts/array.ts b/packages/client/src/scripts/array.ts deleted file mode 100644 index 4620c8b735..0000000000 --- a/packages/client/src/scripts/array.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { EndoRelation, Predicate } from './relation'; - -/** - * Count the number of elements that satisfy the predicate - */ - -export function countIf<T>(f: Predicate<T>, xs: T[]): number { - return xs.filter(f).length; -} - -/** - * Count the number of elements that is equal to the element - */ -export function count<T>(a: T, xs: T[]): number { - return countIf(x => x === a, xs); -} - -/** - * Concatenate an array of arrays - */ -export function concat<T>(xss: T[][]): T[] { - return ([] as T[]).concat(...xss); -} - -/** - * Intersperse the element between the elements of the array - * @param sep The element to be interspersed - */ -export function intersperse<T>(sep: T, xs: T[]): T[] { - return concat(xs.map(x => [sep, x])).slice(1); -} - -/** - * Returns the array of elements that is not equal to the element - */ -export function erase<T>(a: T, xs: T[]): T[] { - return xs.filter(x => x !== a); -} - -/** - * Finds the array of all elements in the first array not contained in the second array. - * The order of result values are determined by the first array. - */ -export function difference<T>(xs: T[], ys: T[]): T[] { - return xs.filter(x => !ys.includes(x)); -} - -/** - * Remove all but the first element from every group of equivalent elements - */ -export function unique<T>(xs: T[]): T[] { - return [...new Set(xs)]; -} - -export function uniqueBy<TValue, TKey>(values: TValue[], keySelector: (value: TValue) => TKey): TValue[] { - const map = new Map<TKey, TValue>(); - - for (const value of values) { - const key = keySelector(value); - if (!map.has(key)) map.set(key, value); - } - - return [...map.values()]; -} - -export function sum(xs: number[]): number { - return xs.reduce((a, b) => a + b, 0); -} - -export function maximum(xs: number[]): number { - return Math.max(...xs); -} - -/** - * Splits an array based on the equivalence relation. - * The concatenation of the result is equal to the argument. - */ -export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] { - const groups = [] as T[][]; - for (const x of xs) { - if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) { - groups[groups.length - 1].push(x); - } else { - groups.push([x]); - } - } - return groups; -} - -/** - * Splits an array based on the equivalence relation induced by the function. - * The concatenation of the result is equal to the argument. - */ -export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] { - return groupBy((a, b) => f(a) === f(b), xs); -} - -export function groupByX<T>(collections: T[], keySelector: (x: T) => string) { - return collections.reduce((obj: Record<string, T[]>, item: T) => { - const key = keySelector(item); - if (typeof obj[key] === 'undefined') { - obj[key] = []; - } - - obj[key].push(item); - - return obj; - }, {}); -} - -/** - * Compare two arrays by lexicographical order - */ -export function lessThan(xs: number[], ys: number[]): boolean { - for (let i = 0; i < Math.min(xs.length, ys.length); i++) { - if (xs[i] < ys[i]) return true; - if (xs[i] > ys[i]) return false; - } - return xs.length < ys.length; -} - -/** - * Returns the longest prefix of elements that satisfy the predicate - */ -export function takeWhile<T>(f: Predicate<T>, xs: T[]): T[] { - const ys: T[] = []; - for (const x of xs) { - if (f(x)) { - ys.push(x); - } else { - break; - } - } - return ys; -} - -export function cumulativeSum(xs: number[]): number[] { - const ys = Array.from(xs); // deep copy - for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1]; - return ys; -} - -export function toArray<T>(x: T | T[] | undefined): T[] { - return Array.isArray(x) ? x : x != null ? [x] : []; -} - -export function toSingle<T>(x: T | T[] | undefined): T | undefined { - return Array.isArray(x) ? x[0] : x; -} diff --git a/packages/client/src/scripts/autocomplete.ts b/packages/client/src/scripts/autocomplete.ts deleted file mode 100644 index 1bae3790f5..0000000000 --- a/packages/client/src/scripts/autocomplete.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { nextTick, Ref, ref, defineAsyncComponent } from 'vue'; -import getCaretCoordinates from 'textarea-caret'; -import { toASCII } from 'punycode/'; -import { popup } from '@/os'; - -export class Autocomplete { - private suggestion: { - x: Ref<number>; - y: Ref<number>; - q: Ref<string | null>; - close: () => void; - } | null; - private textarea: HTMLInputElement | HTMLTextAreaElement; - private currentType: string; - private textRef: Ref<string>; - private opening: boolean; - - private get text(): string { - // Use raw .value to get the latest value - // (Because v-model does not update while composition) - return this.textarea.value; - } - - private set text(text: string) { - // Use ref value to notify other watchers - // (Because .value setter never fires input/change events) - this.textRef.value = text; - } - - /** - * 対象のテキストエリアを与えてインスタンスを初期化します。 - */ - constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) { - //#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.textRef = textRef; - 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(defineAsyncComponent(() => import('@/components/MkAutocomplete.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}`; - - // キャレットを戻す - 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}`; - - // キャレットを戻す - 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; - - // キャレットを戻す - 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}`; - - // キャレットを戻す - nextTick(() => { - this.textarea.focus(); - const pos = trimmedBefore.length + (value.length + 3); - this.textarea.setSelectionRange(pos, pos); - }); - } - } -} diff --git a/packages/client/src/scripts/chart-vline.ts b/packages/client/src/scripts/chart-vline.ts deleted file mode 100644 index 8e3c4436b2..0000000000 --- a/packages/client/src/scripts/chart-vline.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const chartVLine = (vLineColor: string) => ({ - id: 'vLine', - beforeDraw(chart, args, options) { - if (chart.tooltip?._active?.length) { - const activePoint = chart.tooltip._active[0]; - const ctx = chart.ctx; - const x = activePoint.element.x; - const topY = chart.scales.y.top; - const bottomY = chart.scales.y.bottom; - - ctx.save(); - ctx.beginPath(); - ctx.moveTo(x, bottomY); - ctx.lineTo(x, topY); - ctx.lineWidth = 1; - ctx.strokeStyle = vLineColor; - ctx.stroke(); - ctx.restore(); - } - }, -}); diff --git a/packages/client/src/scripts/check-word-mute.ts b/packages/client/src/scripts/check-word-mute.ts deleted file mode 100644 index 35d40a6e08..0000000000 --- a/packages/client/src/scripts/check-word-mute.ts +++ /dev/null @@ -1,37 +0,0 @@ -export function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: Array<string | string[]>): boolean { - // 自分自身 - if (me && (note.userId === me.id)) return false; - - if (mutedWords.length > 0) { - const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim(); - - if (text === '') return false; - - const matched = mutedWords.some(filter => { - if (Array.isArray(filter)) { - // Clean up - const filteredFilter = filter.filter(keyword => keyword !== ''); - if (filteredFilter.length === 0) return false; - - return filteredFilter.every(keyword => text.includes(keyword)); - } else { - // represents RegExp - const regexp = filter.match(/^\/(.+)\/(.*)$/); - - // This should never happen due to input sanitisation. - if (!regexp) return false; - - try { - return new RegExp(regexp[1], regexp[2]).test(text); - } catch (err) { - // This should never happen due to input sanitisation. - return false; - } - } - }); - - if (matched) return true; - } - - return false; -} diff --git a/packages/client/src/scripts/clone.ts b/packages/client/src/scripts/clone.ts deleted file mode 100644 index 16fad24129..0000000000 --- a/packages/client/src/scripts/clone.ts +++ /dev/null @@ -1,18 +0,0 @@ -// structredCloneが遅いため -// SEE: http://var.blog.jp/archives/86038606.html - -type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[]; - -export function deepClone<T extends Cloneable>(x: T): T { - if (typeof x === 'object') { - if (x === null) return x; - if (Array.isArray(x)) return x.map(deepClone) as T; - const obj = {} as Record<string, Cloneable>; - for (const [k, v] of Object.entries(x)) { - obj[k] = deepClone(v); - } - return obj as T; - } else { - return x; - } -} diff --git a/packages/client/src/scripts/collect-page-vars.ts b/packages/client/src/scripts/collect-page-vars.ts deleted file mode 100644 index 76b68beaf6..0000000000 --- a/packages/client/src/scripts/collect-page-vars.ts +++ /dev/null @@ -1,68 +0,0 @@ -interface StringPageVar { - name: string, - type: 'string', - value: string -} - -interface NumberPageVar { - name: string, - type: 'number', - value: number -} - -interface BooleanPageVar { - name: string, - type: 'boolean', - value: boolean -} - -type PageVar = StringPageVar | NumberPageVar | BooleanPageVar; - -export function collectPageVars(content): PageVar[] { - const pageVars: PageVar[] = []; - const collect = (xs: any[]): void => { - 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/packages/client/src/scripts/contains.ts b/packages/client/src/scripts/contains.ts deleted file mode 100644 index 256e09d293..0000000000 --- a/packages/client/src/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/packages/client/src/scripts/copy-to-clipboard.ts b/packages/client/src/scripts/copy-to-clipboard.ts deleted file mode 100644 index ab13cab970..0000000000 --- a/packages/client/src/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/packages/client/src/scripts/device-kind.ts b/packages/client/src/scripts/device-kind.ts deleted file mode 100644 index 544cac0604..0000000000 --- a/packages/client/src/scripts/device-kind.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defaultStore } from '@/store'; - -const ua = navigator.userAgent.toLowerCase(); -const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700); -const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua); - -export const deviceKind = defaultStore.state.overridedDeviceKind ? defaultStore.state.overridedDeviceKind - : isSmartphone ? 'smartphone' - : isTablet ? 'tablet' - : 'desktop'; diff --git a/packages/client/src/scripts/emoji-base.ts b/packages/client/src/scripts/emoji-base.ts deleted file mode 100644 index 3f05642d57..0000000000 --- a/packages/client/src/scripts/emoji-base.ts +++ /dev/null @@ -1,20 +0,0 @@ -const twemojiSvgBase = '/twemoji'; -const fluentEmojiPngBase = '/fluent-emoji'; - -export function char2twemojiFilePath(char: string): string { - let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16)); - if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); - codes = codes.filter(x => x && x.length); - const fileName = codes.join('-'); - return `${twemojiSvgBase}/${fileName}.svg`; -} - -export function char2fluentEmojiFilePath(char: string): string { - let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16)); - // Fluent Emojiは国旗非対応 https://github.com/microsoft/fluentui-emoji/issues/25 - if (codes[0]?.startsWith('1f1')) return char2twemojiFilePath(char); - if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); - codes = codes.filter(x => x && x.length); - const fileName = codes.map(x => x!.padStart(4, '0')).join('-'); - return `${fluentEmojiPngBase}/${fileName}.png`; -} diff --git a/packages/client/src/scripts/emojilist.ts b/packages/client/src/scripts/emojilist.ts deleted file mode 100644 index bc52fa7a43..0000000000 --- a/packages/client/src/scripts/emojilist.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const unicodeEmojiCategories = ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'] as const; - -export type UnicodeEmojiDef = { - name: string; - keywords: string[]; - char: string; - category: typeof unicodeEmojiCategories[number]; -} - -// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb -import _emojilist from '../emojilist.json'; - -export const emojilist = _emojilist as UnicodeEmojiDef[]; - -export function getEmojiName(char: string): string | undefined { - return emojilist.find(emo => emo.char === char)?.name; -} diff --git a/packages/client/src/scripts/extract-avg-color-from-blurhash.ts b/packages/client/src/scripts/extract-avg-color-from-blurhash.ts deleted file mode 100644 index af517f2672..0000000000 --- a/packages/client/src/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/packages/client/src/scripts/extract-mentions.ts b/packages/client/src/scripts/extract-mentions.ts deleted file mode 100644 index cc19b161a8..0000000000 --- a/packages/client/src/scripts/extract-mentions.ts +++ /dev/null @@ -1,11 +0,0 @@ -// test is located in test/extract-mentions - -import * as mfm from 'mfm-js'; - -export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] { - // TODO: 重複を削除 - const mentionNodes = mfm.extract(nodes, (node) => node.type === 'mention'); - const mentions = mentionNodes.map(x => x.props); - - return mentions; -} diff --git a/packages/client/src/scripts/extract-url-from-mfm.ts b/packages/client/src/scripts/extract-url-from-mfm.ts deleted file mode 100644 index 34e3eb6c19..0000000000 --- a/packages/client/src/scripts/extract-url-from-mfm.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as mfm from 'mfm-js'; -import { unique } from '@/scripts/array'; - -// unique without hash -// [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ] -const removeHash = (x: string) => x.replace(/#[^#]*$/, ''); - -export function extractUrlFromMfm(nodes: mfm.MfmNode[], respectSilentFlag = true): string[] { - const urlNodes = mfm.extract(nodes, (node) => { - return (node.type === 'url') || (node.type === 'link' && (!respectSilentFlag || !node.props.silent)); - }); - const urls: string[] = unique(urlNodes.map(x => x.props.url)); - - return urls.reduce((array, url) => { - const urlWithoutHash = removeHash(url); - if (!array.map(x => removeHash(x)).includes(urlWithoutHash)) array.push(url); - return array; - }, [] as string[]); -} diff --git a/packages/client/src/scripts/focus.ts b/packages/client/src/scripts/focus.ts deleted file mode 100644 index d6802fa322..0000000000 --- a/packages/client/src/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/packages/client/src/scripts/form.ts b/packages/client/src/scripts/form.ts deleted file mode 100644 index 7f321cc0ae..0000000000 --- a/packages/client/src/scripts/form.ts +++ /dev/null @@ -1,59 +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: 'radio'; - default: unknown | null; - hidden?: boolean; - options: { - label: string; - value: unknown; - }[]; -} | { - label?: string; - type: 'object'; - default: Record<string, unknown> | null; - hidden: true; -} | { - label?: string; - type: 'array'; - default: unknown[] | null; - hidden: true; -}; - -export type Form = Record<string, FormItem>; - -type GetItemType<Item extends FormItem> = - Item['type'] extends 'string' ? string : - Item['type'] extends 'number' ? number : - Item['type'] extends 'boolean' ? boolean : - Item['type'] extends 'radio' ? unknown : - Item['type'] extends 'enum' ? string : - Item['type'] extends 'array' ? unknown[] : - Item['type'] extends 'object' ? Record<string, unknown> - : never; - -export type GetFormResultType<F extends Form> = { - [P in keyof F]: GetItemType<F[P]>; -}; diff --git a/packages/client/src/scripts/format-time-string.ts b/packages/client/src/scripts/format-time-string.ts deleted file mode 100644 index c20db5e827..0000000000 --- a/packages/client/src/scripts/format-time-string.ts +++ /dev/null @@ -1,50 +0,0 @@ -const defaultLocaleStringFormats: {[index: string]: string} = { - 'weekday': 'narrow', - 'era': 'narrow', - 'year': 'numeric', - 'month': 'numeric', - 'day': 'numeric', - 'hour': 'numeric', - 'minute': 'numeric', - 'second': 'numeric', - 'timeZoneName': 'short', -}; - -function formatLocaleString(date: Date, format: string): string { - return format.replace(/\{\{(\w+)(:(\w+))?\}\}/g, (match: string, kind: string, unused?, option?: string) => { - if (['weekday', 'era', 'year', 'month', 'day', 'hour', 'minute', 'second', 'timeZoneName'].includes(kind)) { - return date.toLocaleString(window.navigator.language, { [kind]: option ? option : defaultLocaleStringFormats[kind] }); - } else { - return match; - } - }); -} - -export function formatDateTimeString(date: Date, format: string): string { - return format - .replace(/yyyy/g, date.getFullYear().toString()) - .replace(/yy/g, date.getFullYear().toString().slice(-2)) - .replace(/MMMM/g, date.toLocaleString(window.navigator.language, { month: 'long' })) - .replace(/MMM/g, date.toLocaleString(window.navigator.language, { month: 'short' })) - .replace(/MM/g, (`0${date.getMonth() + 1}`).slice(-2)) - .replace(/M/g, (date.getMonth() + 1).toString()) - .replace(/dd/g, (`0${date.getDate()}`).slice(-2)) - .replace(/d/g, date.getDate().toString()) - .replace(/HH/g, (`0${date.getHours()}`).slice(-2)) - .replace(/H/g, date.getHours().toString()) - .replace(/hh/g, (`0${(date.getHours() % 12) || 12}`).slice(-2)) - .replace(/h/g, ((date.getHours() % 12) || 12).toString()) - .replace(/mm/g, (`0${date.getMinutes()}`).slice(-2)) - .replace(/m/g, date.getMinutes().toString()) - .replace(/ss/g, (`0${date.getSeconds()}`).slice(-2)) - .replace(/s/g, date.getSeconds().toString()) - .replace(/tt/g, date.getHours() >= 12 ? 'PM' : 'AM'); -} - -export function formatTimeString(date: Date, format: string): string { - return format.replace(/\[(([^\[]|\[\])*)\]|(([yMdHhmst])\4{0,3})/g, (match: string, localeformat?: string, unused?, datetimeformat?: string) => { - if (localeformat) return formatLocaleString(date, localeformat); - if (datetimeformat) return formatDateTimeString(date, datetimeformat); - return match; - }); -} diff --git a/packages/client/src/scripts/gen-search-query.ts b/packages/client/src/scripts/gen-search-query.ts deleted file mode 100644 index da7d622632..0000000000 --- a/packages/client/src/scripts/gen-search-query.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as Acct from 'misskey-js/built/acct'; -import { host as localHost } from '@/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', Acct.parse(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/packages/client/src/scripts/get-account-from-id.ts b/packages/client/src/scripts/get-account-from-id.ts deleted file mode 100644 index 1da897f176..0000000000 --- a/packages/client/src/scripts/get-account-from-id.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { get } from '@/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(account => account.id === id); -} diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts deleted file mode 100644 index 7656770894..0000000000 --- a/packages/client/src/scripts/get-note-menu.ts +++ /dev/null @@ -1,341 +0,0 @@ -import { defineAsyncComponent, Ref, inject } from 'vue'; -import * as misskey from 'misskey-js'; -import { pleaseLogin } from './please-login'; -import { $i } from '@/account'; -import { i18n } from '@/i18n'; -import { instance } from '@/instance'; -import * as os from '@/os'; -import copyToClipboard from '@/scripts/copy-to-clipboard'; -import { url } from '@/config'; -import { noteActions } from '@/store'; -import { notePage } from '@/filters/note'; - -export function getNoteMenu(props: { - note: misskey.entities.Note; - menuButton: Ref<HTMLElement>; - translation: Ref<any>; - translating: Ref<boolean>; - isDeleted: Ref<boolean>; - currentClipPage?: Ref<misskey.entities.Clip>; -}) { - const isRenote = ( - props.note.renote != null && - props.note.text == null && - props.note.fileIds.length === 0 && - props.note.poll == null - ); - - const appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note; - - function del(): void { - os.confirm({ - type: 'warning', - text: i18n.ts.noteDeleteConfirm, - }).then(({ canceled }) => { - if (canceled) return; - - os.api('notes/delete', { - noteId: appearNote.id, - }); - }); - } - - function delEdit(): void { - os.confirm({ - type: 'warning', - text: i18n.ts.deleteAndEditConfirm, - }).then(({ canceled }) => { - if (canceled) return; - - os.api('notes/delete', { - noteId: appearNote.id, - }); - - os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel }); - }); - } - - function toggleFavorite(favorite: boolean): void { - os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', { - noteId: appearNote.id, - }); - } - - function toggleThreadMute(mute: boolean): void { - os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', { - noteId: appearNote.id, - }); - } - - function copyContent(): void { - copyToClipboard(appearNote.text); - os.success(); - } - - function copyLink(): void { - copyToClipboard(`${url}/notes/${appearNote.id}`); - os.success(); - } - - function togglePin(pin: boolean): void { - os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', { - noteId: appearNote.id, - }, undefined, null, res => { - if (res.id === '72dab508-c64d-498f-8740-a8eec1ba385a') { - os.alert({ - type: 'error', - text: i18n.ts.pinLimitExceeded, - }); - } - }); - } - - async function clip(): Promise<void> { - const clips = await os.api('clips/list'); - os.popupMenu([{ - icon: 'ti ti-plus', - text: i18n.ts.createNew, - action: async () => { - const { canceled, result } = await os.form(i18n.ts.createNewClip, { - name: { - type: 'string', - label: i18n.ts.name, - }, - description: { - type: 'string', - required: false, - multiline: true, - label: i18n.ts.description, - }, - isPublic: { - type: 'boolean', - label: i18n.ts.public, - default: false, - }, - }); - if (canceled) return; - - const clip = await os.apiWithDialog('clips/create', result); - - os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id }); - }, - }, null, ...clips.map(clip => ({ - text: clip.name, - action: () => { - os.promiseDialog( - os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }), - null, - async (err) => { - if (err.id === '734806c4-542c-463a-9311-15c512803965') { - const confirm = await os.confirm({ - type: 'warning', - text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }), - }); - if (!confirm.canceled) { - os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }); - if (props.currentClipPage?.value.id === clip.id) props.isDeleted.value = true; - } - } else { - os.alert({ - type: 'error', - text: err.message + '\n' + err.id, - }); - } - }, - ); - }, - }))], props.menuButton.value, { - }).then(focus); - } - - async function unclip(): Promise<void> { - os.apiWithDialog('clips/remove-note', { clipId: props.currentClipPage.value.id, noteId: appearNote.id }); - props.isDeleted.value = true; - } - - async function promote(): Promise<void> { - const { canceled, result: days } = await os.inputNumber({ - title: i18n.ts.numberOfDays, - }); - - if (canceled) return; - - os.apiWithDialog('admin/promo/create', { - noteId: appearNote.id, - expiresAt: Date.now() + (86400000 * days), - }); - } - - function share(): void { - navigator.share({ - title: i18n.t('noteOf', { user: appearNote.user.name }), - text: appearNote.text, - url: `${url}/notes/${appearNote.id}`, - }); - } - function notedetails(): void { - os.pageWindow(`/notes/${appearNote.id}`); - } - async function translate(): Promise<void> { - if (props.translation.value != null) return; - props.translating.value = true; - const res = await os.api('notes/translate', { - noteId: appearNote.id, - targetLang: localStorage.getItem('lang') || navigator.language, - }); - props.translating.value = false; - props.translation.value = res; - } - - let menu; - if ($i) { - const statePromise = os.api('notes/state', { - noteId: appearNote.id, - }); - - menu = [ - ...( - props.currentClipPage?.value.userId === $i.id ? [{ - icon: 'ti ti-backspace', - text: i18n.ts.unclip, - danger: true, - action: unclip, - }, null] : [] - ), { - icon: 'ti ti-external-link', - text: i18n.ts.details, - action: notedetails, - }, { - icon: 'ti ti-copy', - text: i18n.ts.copyContent, - action: copyContent, - }, { - icon: 'ti ti-link', - text: i18n.ts.copyLink, - action: copyLink, - }, (appearNote.url || appearNote.uri) ? { - icon: 'ti ti-external-link', - text: i18n.ts.showOnRemote, - action: () => { - window.open(appearNote.url || appearNote.uri, '_blank'); - }, - } : undefined, - { - icon: 'ti ti-share', - text: i18n.ts.share, - action: share, - }, - instance.translatorAvailable ? { - icon: 'ti ti-language-hiragana', - text: i18n.ts.translate, - action: translate, - } : undefined, - null, - statePromise.then(state => state.isFavorited ? { - icon: 'ti ti-star-off', - text: i18n.ts.unfavorite, - action: () => toggleFavorite(false), - } : { - icon: 'ti ti-star', - text: i18n.ts.favorite, - action: () => toggleFavorite(true), - }), - { - icon: 'ti ti-paperclip', - text: i18n.ts.clip, - action: () => clip(), - }, - statePromise.then(state => state.isMutedThread ? { - icon: 'ti ti-message-off', - text: i18n.ts.unmuteThread, - action: () => toggleThreadMute(false), - } : { - icon: 'ti ti-message-off', - text: i18n.ts.muteThread, - action: () => toggleThreadMute(true), - }), - appearNote.userId === $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? { - icon: 'ti ti-pinned-off', - text: i18n.ts.unpin, - action: () => togglePin(false), - } : { - icon: 'ti ti-pin', - text: i18n.ts.pin, - action: () => togglePin(true), - } : undefined, - /* - ...($i.isModerator || $i.isAdmin ? [ - null, - { - icon: 'fas fa-bullhorn', - text: i18n.ts.promote, - action: promote - }] - : [] - ),*/ - ...(appearNote.userId !== $i.id ? [ - null, - { - icon: 'ti ti-exclamation-circle', - text: i18n.ts.reportAbuse, - action: () => { - const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`; - os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { - user: appearNote.user, - initialComment: `Note: ${u}\n-----\n`, - }, {}, 'closed'); - }, - }] - : [] - ), - ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [ - null, - appearNote.userId === $i.id ? { - icon: 'ti ti-edit', - text: i18n.ts.deleteAndEdit, - action: delEdit, - } : undefined, - { - icon: 'ti ti-trash', - text: i18n.ts.delete, - danger: true, - action: del, - }] - : [] - )] - .filter(x => x !== undefined); - } else { - menu = [{ - icon: 'ti ti-external-link', - text: i18n.ts.detailed, - action: openDetail, - }, { - icon: 'ti ti-copy', - text: i18n.ts.copyContent, - action: copyContent, - }, { - icon: 'ti ti-link', - text: i18n.ts.copyLink, - action: copyLink, - }, (appearNote.url || appearNote.uri) ? { - icon: 'ti ti-external-link', - text: i18n.ts.showOnRemote, - action: () => { - window.open(appearNote.url || appearNote.uri, '_blank'); - }, - } : undefined] - .filter(x => x !== undefined); - } - - if (noteActions.length > 0) { - menu = menu.concat([null, ...noteActions.map(action => ({ - icon: 'ti ti-plug', - text: action.title, - action: () => { - action.handler(appearNote); - }, - }))]); - } - - return menu; -} diff --git a/packages/client/src/scripts/get-note-summary.ts b/packages/client/src/scripts/get-note-summary.ts deleted file mode 100644 index d57e1c3029..0000000000 --- a/packages/client/src/scripts/get-note-summary.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as misskey from 'misskey-js'; -import { i18n } from '@/i18n'; - -/** - * 投稿を表す文字列を取得します。 - * @param {*} note (packされた)投稿 - */ -export const getNoteSummary = (note: misskey.entities.Note): string => { - if (note.deletedAt) { - return `(${i18n.ts.deletedNote})`; - } - - if (note.isHidden) { - return `(${i18n.ts.invisibleNote})`; - } - - let summary = ''; - - // 本文 - if (note.cw != null) { - summary += note.cw; - } else { - summary += note.text ? note.text : ''; - } - - // ファイルが添付されているとき - if ((note.files || []).length !== 0) { - summary += ` (${i18n.t('withNFiles', { n: note.files.length })})`; - } - - // 投票が添付されているとき - if (note.poll) { - summary += ` (${i18n.ts.poll})`; - } - - // 返信のとき - if (note.replyId) { - if (note.reply) { - summary += `\n\nRE: ${getNoteSummary(note.reply)}`; - } else { - summary += '\n\nRE: ...'; - } - } - - // Renoteのとき - if (note.renoteId) { - if (note.renote) { - summary += `\n\nRN: ${getNoteSummary(note.renote)}`; - } else { - summary += '\n\nRN: ...'; - } - } - - return summary.trim(); -}; diff --git a/packages/client/src/scripts/get-static-image-url.ts b/packages/client/src/scripts/get-static-image-url.ts deleted file mode 100644 index cbd1761983..0000000000 --- a/packages/client/src/scripts/get-static-image-url.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { url as instanceUrl } from '@/config'; -import * as url from '@/scripts/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; - } - - // 拡張子がないとキャッシュしてくれないCDNがあるのでダミーの名前を指定する - const dummy = `${encodeURIComponent(`${u.host}${u.pathname}`)}.webp`; - - return `${instanceUrl}/proxy/${dummy}?${url.query({ - url: u.href, - static: '1', - })}`; -} diff --git a/packages/client/src/scripts/get-user-menu.ts b/packages/client/src/scripts/get-user-menu.ts deleted file mode 100644 index 2faacffdfc..0000000000 --- a/packages/client/src/scripts/get-user-menu.ts +++ /dev/null @@ -1,253 +0,0 @@ -import * as Acct from 'misskey-js/built/acct'; -import { defineAsyncComponent } from 'vue'; -import { i18n } from '@/i18n'; -import copyToClipboard from '@/scripts/copy-to-clipboard'; -import { host } from '@/config'; -import * as os from '@/os'; -import { userActions } from '@/store'; -import { $i, iAmModerator } from '@/account'; -import { mainRouter } from '@/router'; -import { Router } from '@/nirax'; - -export function getUserMenu(user, router: Router = mainRouter) { - const meId = $i ? $i.id : null; - - async function pushList() { - const t = i18n.ts.selectList; // なぜか後で参照すると null になるので最初にメモリに確保しておく - const lists = await os.api('users/lists/list'); - if (lists.length === 0) { - os.alert({ - type: 'error', - text: i18n.ts.youHaveNoLists, - }); - return; - } - const { canceled, result: listId } = await os.select({ - title: t, - items: lists.map(list => ({ - value: list.id, text: list.name, - })), - }); - 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.alert({ - type: 'error', - text: i18n.ts.youHaveNoGroups, - }); - return; - } - const { canceled, result: groupId } = await os.select({ - title: i18n.ts.group, - items: groups.map(group => ({ - value: group.id, text: group.name, - })), - }); - if (canceled) return; - os.apiWithDialog('users/groups/invite', { - groupId: groupId, - userId: user.id, - }); - } - - async function toggleMute() { - if (user.isMuted) { - os.apiWithDialog('mute/delete', { - userId: user.id, - }).then(() => { - user.isMuted = false; - }); - } else { - const { canceled, result: period } = await os.select({ - title: i18n.ts.mutePeriod, - items: [{ - value: 'indefinitely', text: i18n.ts.indefinitely, - }, { - value: 'tenMinutes', text: i18n.ts.tenMinutes, - }, { - value: 'oneHour', text: i18n.ts.oneHour, - }, { - value: 'oneDay', text: i18n.ts.oneDay, - }, { - value: 'oneWeek', text: i18n.ts.oneWeek, - }], - default: 'indefinitely', - }); - if (canceled) return; - - const expiresAt = period === 'indefinitely' ? null - : period === 'tenMinutes' ? Date.now() + (1000 * 60 * 10) - : period === 'oneHour' ? Date.now() + (1000 * 60 * 60) - : period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24) - : period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7) - : null; - - os.apiWithDialog('mute/create', { - userId: user.id, - expiresAt, - }).then(() => { - user.isMuted = true; - }); - } - } - - async function toggleBlock() { - if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.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(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { - user: user, - }, {}, 'closed'); - } - - async function getConfirmed(text: string): Promise<boolean> { - const confirm = await os.confirm({ - type: 'warning', - title: 'confirm', - text, - }); - - return !confirm.canceled; - } - - async function invalidateFollow() { - os.apiWithDialog('following/invalidate', { - userId: user.id, - }).then(() => { - user.isFollowed = !user.isFollowed; - }); - } - - let menu = [{ - icon: 'ti ti-at', - text: i18n.ts.copyUsername, - action: () => { - copyToClipboard(`@${user.username}@${user.host || host}`); - }, - }, { - icon: 'ti ti-rss', - text: i18n.ts.copyRSS, - action: () => { - copyToClipboard(`${user.host || host}/@${user.username}.atom`); - } - }, { - icon: 'ti ti-info-circle', - text: i18n.ts.info, - action: () => { - router.push(`/user-info/${user.id}`); - }, - }, { - icon: 'ti ti-mail', - text: i18n.ts.sendMessage, - action: () => { - os.post({ specified: user }); - }, - }, meId !== user.id ? { - type: 'link', - icon: 'ti ti-messages', - text: i18n.ts.startMessaging, - to: '/my/messaging/' + Acct.toString(user), - } : undefined, null, { - icon: 'ti ti-list', - text: i18n.ts.addToList, - action: pushList, - }, meId !== user.id ? { - icon: 'ti ti-users', - text: i18n.ts.inviteToGroup, - action: inviteGroup, - } : undefined] as any; - - if ($i && meId !== user.id) { - menu = menu.concat([null, { - icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off', - text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, - action: toggleMute, - }, { - icon: 'ti ti-ban', - text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block, - action: toggleBlock, - }]); - - if (user.isFollowed) { - menu = menu.concat([{ - icon: 'ti ti-link-off', - text: i18n.ts.breakFollow, - action: invalidateFollow, - }]); - } - - menu = menu.concat([null, { - icon: 'ti ti-exclamation-circle', - text: i18n.ts.reportAbuse, - action: reportAbuse, - }]); - - if (iAmModerator) { - menu = menu.concat([null, { - icon: 'ti ti-microphone-2-off', - text: user.isSilenced ? i18n.ts.unsilence : i18n.ts.silence, - action: toggleSilence, - }, { - icon: 'ti ti-snowflake', - text: user.isSuspended ? i18n.ts.unsuspend : i18n.ts.suspend, - action: toggleSuspend, - }]); - } - } - - if ($i && meId === user.id) { - menu = menu.concat([null, { - icon: 'ti ti-pencil', - text: i18n.ts.editProfile, - action: () => { - router.push('/settings/profile'); - }, - }]); - } - - if (userActions.length > 0) { - menu = menu.concat([null, ...userActions.map(action => ({ - icon: 'ti ti-plug', - text: action.title, - action: () => { - action.handler(user); - }, - }))]); - } - - return menu; -} diff --git a/packages/client/src/scripts/get-user-name.ts b/packages/client/src/scripts/get-user-name.ts deleted file mode 100644 index d499ea0203..0000000000 --- a/packages/client/src/scripts/get-user-name.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function(user: { name?: string | null, username: string }): string { - return user.name || user.username; -} diff --git a/packages/client/src/scripts/hotkey.ts b/packages/client/src/scripts/hotkey.ts deleted file mode 100644 index 4a0ded637d..0000000000 --- a/packages/client/src/scripts/hotkey.ts +++ /dev/null @@ -1,90 +0,0 @@ -import keyCode from './keycode'; - -type Callback = (ev: KeyboardEvent) => void; - -type Keymap = Record<string, Callback>; - -type Pattern = { - which: string[]; - ctrl?: boolean; - shift?: boolean; - alt?: boolean; -}; - -type Action = { - patterns: Pattern[]; - callback: Callback; - allowRepeat: boolean; -}; - -const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => { - const result = { - patterns: [], - 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(ev: KeyboardEvent, patterns: Action['patterns']): boolean { - const key = ev.code.toLowerCase(); - return patterns.some(pattern => pattern.which.includes(key) && - pattern.ctrl === ev.ctrlKey && - pattern.shift === ev.shiftKey && - pattern.alt === ev.altKey && - !ev.metaKey, - ); -} - -export const makeHotkey = (keymap: Keymap) => { - const actions = parseKeymap(keymap); - - return (ev: 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(ev, action.patterns); - - if (matched) { - if (!action.allowRepeat && ev.repeat) return; - - ev.preventDefault(); - ev.stopPropagation(); - action.callback(ev); - break; - } - } - }; -}; diff --git a/packages/client/src/scripts/hpml/block.ts b/packages/client/src/scripts/hpml/block.ts deleted file mode 100644 index 804c5c1124..0000000000 --- a/packages/client/src/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/packages/client/src/scripts/hpml/evaluator.ts b/packages/client/src/scripts/hpml/evaluator.ts deleted file mode 100644 index 196b3142a1..0000000000 --- a/packages/client/src/scripts/hpml/evaluator.ts +++ /dev/null @@ -1,232 +0,0 @@ -import autobind from 'autobind-decorator'; -import { PageVar, envVarsDef, Fn, HpmlScope, HpmlError } from '.'; -import { version } from '@/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 '@/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.inputText({ - title: q, - }).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 (err) { - //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 (err) {} - } - - @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 (err) { - 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/packages/client/src/scripts/hpml/expr.ts b/packages/client/src/scripts/hpml/expr.ts deleted file mode 100644 index 18c7c2a14b..0000000000 --- a/packages/client/src/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/packages/client/src/scripts/hpml/index.ts b/packages/client/src/scripts/hpml/index.ts deleted file mode 100644 index 9a55a5c286..0000000000 --- a/packages/client/src/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: 'ti ti-quote' }, - 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/packages/client/src/scripts/hpml/lib.ts b/packages/client/src/scripts/hpml/lib.ts deleted file mode 100644 index b684876a7f..0000000000 --- a/packages/client/src/scripts/hpml/lib.ts +++ /dev/null @@ -1,247 +0,0 @@ -import tinycolor from 'tinycolor2'; -import { Hpml } from './evaluator'; -import { values, utils } from '@syuilo/aiscript'; -import { Fn, HpmlScope } from '.'; -import { Expr } from './expr'; -import 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: 'ti ti-share' }, - 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: 'ti ti-plus' }, - subtract: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-minus' }, - multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-x' }, - 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: 'ti ti-quote' }, - strPick: { in: ['string', 'number'], out: 'string', category: 'text', icon: 'ti ti-quote' }, - strReplace: { in: ['string', 'string', 'string'], out: 'string', category: 'text', icon: 'ti ti-quote' }, - strReverse: { in: ['string'], out: 'string', category: 'text', icon: 'ti ti-quote' }, - join: { in: ['stringArray', 'string'], out: 'string', category: 'text', icon: 'ti ti-quote' }, - 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()}`; - - // SHOULD be fine to ignore since it's intended + function shape isn't defined - // eslint-disable-next-line @typescript-eslint/ban-types - 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/packages/client/src/scripts/hpml/type-checker.ts b/packages/client/src/scripts/hpml/type-checker.ts deleted file mode 100644 index 24c9ed8bcb..0000000000 --- a/packages/client/src/scripts/hpml/type-checker.ts +++ /dev/null @@ -1,191 +0,0 @@ -import autobind from 'autobind-decorator'; -import { isLiteralValue } from './expr'; -import { funcDefs } from './lib'; -import { envVarsDef } from '.'; -import type { Type, PageVar } from '.'; -import type { Expr, Variable } from './expr'; - -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/packages/client/src/scripts/i18n.ts b/packages/client/src/scripts/i18n.ts deleted file mode 100644 index 54184386da..0000000000 --- a/packages/client/src/scripts/i18n.ts +++ /dev/null @@ -1,29 +0,0 @@ -export class I18n<T extends Record<string, any>> { - public ts: T; - - constructor(locale: T) { - this.ts = locale; - - //#region BIND - this.t = this.t.bind(this); - //#endregion - } - - // string にしているのは、ドット区切りでのパス指定を許可するため - // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも - public t(key: string, args?: Record<string, string | number>): string { - try { - let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string; - - if (args) { - for (const [k, v] of Object.entries(args)) { - str = str.replace(`{${k}}`, v.toString()); - } - } - return str; - } catch (err) { - console.warn(`missing localization '${key}'`); - return key; - } - } -} diff --git a/packages/client/src/scripts/idb-proxy.ts b/packages/client/src/scripts/idb-proxy.ts deleted file mode 100644 index 77bb84463c..0000000000 --- a/packages/client/src/scripts/idb-proxy.ts +++ /dev/null @@ -1,36 +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) { - iset('idb-test', 'test').catch(err => { - console.error('idb error', err); - console.error('indexedDB is unavailable. It will use localStorage.'); - idbAvailable = false; - }); -} else { - 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/packages/client/src/scripts/initialize-sw.ts b/packages/client/src/scripts/initialize-sw.ts deleted file mode 100644 index de52f30523..0000000000 --- a/packages/client/src/scripts/initialize-sw.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { lang } from '@/config'; - -export async function initializeSw() { - if (!('serviceWorker' in navigator)) return; - - navigator.serviceWorker.register(`/sw.js`, { scope: '/', type: 'classic' }); - navigator.serviceWorker.ready.then(registration => { - registration.active?.postMessage({ - msg: 'initialize', - lang, - }); - }); -} diff --git a/packages/client/src/scripts/is-device-darkmode.ts b/packages/client/src/scripts/is-device-darkmode.ts deleted file mode 100644 index 854f38e517..0000000000 --- a/packages/client/src/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/packages/client/src/scripts/keycode.ts b/packages/client/src/scripts/keycode.ts deleted file mode 100644 index 69f6a82803..0000000000 --- a/packages/client/src/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'], -}; - -/*! -* Programmatically 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/packages/client/src/scripts/langmap.ts b/packages/client/src/scripts/langmap.ts deleted file mode 100644 index 25f5b366c8..0000000000 --- a/packages/client/src/scripts/langmap.ts +++ /dev/null @@ -1,666 +0,0 @@ -// TODO: sharedに置いてバックエンドのと統合したい -export const langmap = { - 'ach': { - nativeName: 'Lwo', - }, - 'ady': { - nativeName: 'Адыгэбзэ', - }, - 'af': { - nativeName: 'Afrikaans', - }, - 'af-NA': { - nativeName: 'Afrikaans (Namibia)', - }, - 'af-ZA': { - nativeName: 'Afrikaans (South Africa)', - }, - 'ak': { - nativeName: 'Tɕɥi', - }, - 'ar': { - nativeName: 'العربية', - }, - 'ar-AR': { - nativeName: 'العربية', - }, - 'ar-MA': { - nativeName: 'العربية', - }, - 'ar-SA': { - nativeName: 'العربية (السعودية)', - }, - 'ay-BO': { - nativeName: 'Aymar aru', - }, - 'az': { - nativeName: 'Azərbaycan dili', - }, - 'az-AZ': { - nativeName: 'Azərbaycan dili', - }, - 'be-BY': { - nativeName: 'Беларуская', - }, - 'bg': { - nativeName: 'Български', - }, - 'bg-BG': { - nativeName: 'Български', - }, - 'bn': { - nativeName: 'বাংলা', - }, - 'bn-IN': { - nativeName: 'বাংলা (ভারত)', - }, - 'bn-BD': { - nativeName: 'বাংলা(বাংলাদেশ)', - }, - 'br': { - nativeName: 'Brezhoneg', - }, - 'bs-BA': { - nativeName: 'Bosanski', - }, - 'ca': { - nativeName: 'Català', - }, - 'ca-ES': { - nativeName: 'Català', - }, - 'cak': { - nativeName: 'Maya Kaqchikel', - }, - 'ck-US': { - nativeName: 'ᏣᎳᎩ (tsalagi)', - }, - 'cs': { - nativeName: 'Čeština', - }, - 'cs-CZ': { - nativeName: 'Čeština', - }, - 'cy': { - nativeName: 'Cymraeg', - }, - 'cy-GB': { - nativeName: 'Cymraeg', - }, - 'da': { - nativeName: 'Dansk', - }, - 'da-DK': { - nativeName: 'Dansk', - }, - 'de': { - nativeName: 'Deutsch', - }, - 'de-AT': { - nativeName: 'Deutsch (Österreich)', - }, - 'de-DE': { - nativeName: 'Deutsch (Deutschland)', - }, - 'de-CH': { - nativeName: 'Deutsch (Schweiz)', - }, - 'dsb': { - nativeName: 'Dolnoserbšćina', - }, - 'el': { - nativeName: 'Ελληνικά', - }, - 'el-GR': { - nativeName: 'Ελληνικά', - }, - 'en': { - nativeName: 'English', - }, - 'en-GB': { - nativeName: 'English (UK)', - }, - 'en-AU': { - nativeName: 'English (Australia)', - }, - 'en-CA': { - nativeName: 'English (Canada)', - }, - 'en-IE': { - nativeName: 'English (Ireland)', - }, - 'en-IN': { - nativeName: 'English (India)', - }, - 'en-PI': { - nativeName: 'English (Pirate)', - }, - 'en-SG': { - nativeName: 'English (Singapore)', - }, - 'en-UD': { - nativeName: 'English (Upside Down)', - }, - 'en-US': { - nativeName: 'English (US)', - }, - 'en-ZA': { - nativeName: 'English (South Africa)', - }, - 'en@pirate': { - nativeName: 'English (Pirate)', - }, - 'eo': { - nativeName: 'Esperanto', - }, - 'eo-EO': { - nativeName: 'Esperanto', - }, - 'es': { - nativeName: 'Español', - }, - 'es-AR': { - nativeName: 'Español (Argentine)', - }, - 'es-419': { - nativeName: 'Español (Latinoamérica)', - }, - 'es-CL': { - nativeName: 'Español (Chile)', - }, - 'es-CO': { - nativeName: 'Español (Colombia)', - }, - 'es-EC': { - nativeName: 'Español (Ecuador)', - }, - 'es-ES': { - nativeName: 'Español (España)', - }, - 'es-LA': { - nativeName: 'Español (Latinoamérica)', - }, - 'es-NI': { - nativeName: 'Español (Nicaragua)', - }, - 'es-MX': { - nativeName: 'Español (México)', - }, - 'es-US': { - nativeName: 'Español (Estados Unidos)', - }, - 'es-VE': { - nativeName: 'Español (Venezuela)', - }, - 'et': { - nativeName: 'eesti keel', - }, - 'et-EE': { - nativeName: 'Eesti (Estonia)', - }, - 'eu': { - nativeName: 'Euskara', - }, - 'eu-ES': { - nativeName: 'Euskara', - }, - 'fa': { - nativeName: 'فارسی', - }, - 'fa-IR': { - nativeName: 'فارسی', - }, - 'fb-LT': { - nativeName: 'Leet Speak', - }, - 'ff': { - nativeName: 'Fulah', - }, - 'fi': { - nativeName: 'Suomi', - }, - 'fi-FI': { - nativeName: 'Suomi', - }, - 'fo': { - nativeName: 'Føroyskt', - }, - 'fo-FO': { - nativeName: 'Føroyskt (Færeyjar)', - }, - 'fr': { - nativeName: 'Français', - }, - 'fr-CA': { - nativeName: 'Français (Canada)', - }, - 'fr-FR': { - nativeName: 'Français (France)', - }, - 'fr-BE': { - nativeName: 'Français (Belgique)', - }, - 'fr-CH': { - nativeName: 'Français (Suisse)', - }, - 'fy-NL': { - nativeName: 'Frysk', - }, - 'ga': { - nativeName: 'Gaeilge', - }, - 'ga-IE': { - nativeName: 'Gaeilge', - }, - 'gd': { - nativeName: 'Gàidhlig', - }, - 'gl': { - nativeName: 'Galego', - }, - 'gl-ES': { - nativeName: 'Galego', - }, - 'gn-PY': { - nativeName: 'Avañe\'ẽ', - }, - 'gu-IN': { - nativeName: 'ગુજરાતી', - }, - 'gv': { - nativeName: 'Gaelg', - }, - 'gx-GR': { - nativeName: 'Ἑλληνική ἀρχαία', - }, - 'he': { - nativeName: 'עברית', - }, - 'he-IL': { - nativeName: 'עברית', - }, - 'hi': { - nativeName: 'हिन्दी', - }, - 'hi-IN': { - nativeName: 'हिन्दी', - }, - 'hr': { - nativeName: 'Hrvatski', - }, - 'hr-HR': { - nativeName: 'Hrvatski', - }, - 'hsb': { - nativeName: 'Hornjoserbšćina', - }, - 'ht': { - nativeName: 'Kreyòl', - }, - 'hu': { - nativeName: 'Magyar', - }, - 'hu-HU': { - nativeName: 'Magyar', - }, - 'hy': { - nativeName: 'Հայերեն', - }, - 'hy-AM': { - nativeName: 'Հայերեն (Հայաստան)', - }, - 'id': { - nativeName: 'Bahasa Indonesia', - }, - 'id-ID': { - nativeName: 'Bahasa Indonesia', - }, - 'is': { - nativeName: 'Íslenska', - }, - 'is-IS': { - nativeName: 'Íslenska (Iceland)', - }, - 'it': { - nativeName: 'Italiano', - }, - 'it-IT': { - nativeName: 'Italiano', - }, - 'ja': { - nativeName: '日本語', - }, - 'ja-JP': { - nativeName: '日本語 (日本)', - }, - 'jv-ID': { - nativeName: 'Basa Jawa', - }, - 'ka-GE': { - nativeName: 'ქართული', - }, - 'kk-KZ': { - nativeName: 'Қазақша', - }, - 'km': { - nativeName: 'ភាសាខ្មែរ', - }, - 'kl': { - nativeName: 'kalaallisut', - }, - 'km-KH': { - nativeName: 'ភាសាខ្មែរ', - }, - 'kab': { - nativeName: 'Taqbaylit', - }, - 'kn': { - nativeName: 'ಕನ್ನಡ', - }, - 'kn-IN': { - nativeName: 'ಕನ್ನಡ (India)', - }, - 'ko': { - nativeName: '한국어', - }, - 'ko-KR': { - nativeName: '한국어 (한국)', - }, - 'ku-TR': { - nativeName: 'Kurdî', - }, - 'kw': { - nativeName: 'Kernewek', - }, - 'la': { - nativeName: 'Latin', - }, - 'la-VA': { - nativeName: 'Latin', - }, - 'lb': { - nativeName: 'Lëtzebuergesch', - }, - 'li-NL': { - nativeName: 'Lèmbörgs', - }, - 'lt': { - nativeName: 'Lietuvių', - }, - 'lt-LT': { - nativeName: 'Lietuvių', - }, - 'lv': { - nativeName: 'Latviešu', - }, - 'lv-LV': { - nativeName: 'Latviešu', - }, - 'mai': { - nativeName: 'मैथिली, মৈথিলী', - }, - 'mg-MG': { - nativeName: 'Malagasy', - }, - 'mk': { - nativeName: 'Македонски', - }, - 'mk-MK': { - nativeName: 'Македонски (Македонски)', - }, - 'ml': { - nativeName: 'മലയാളം', - }, - 'ml-IN': { - nativeName: 'മലയാളം', - }, - 'mn-MN': { - nativeName: 'Монгол', - }, - 'mr': { - nativeName: 'मराठी', - }, - 'mr-IN': { - nativeName: 'मराठी', - }, - 'ms': { - nativeName: 'Bahasa Melayu', - }, - 'ms-MY': { - nativeName: 'Bahasa Melayu', - }, - 'mt': { - nativeName: 'Malti', - }, - 'mt-MT': { - nativeName: 'Malti', - }, - 'my': { - nativeName: 'ဗမာစကာ', - }, - 'no': { - nativeName: 'Norsk', - }, - 'nb': { - nativeName: 'Norsk (bokmål)', - }, - 'nb-NO': { - nativeName: 'Norsk (bokmål)', - }, - 'ne': { - nativeName: 'नेपाली', - }, - 'ne-NP': { - nativeName: 'नेपाली', - }, - 'nl': { - nativeName: 'Nederlands', - }, - 'nl-BE': { - nativeName: 'Nederlands (België)', - }, - 'nl-NL': { - nativeName: 'Nederlands (Nederland)', - }, - 'nn-NO': { - nativeName: 'Norsk (nynorsk)', - }, - 'oc': { - nativeName: 'Occitan', - }, - 'or-IN': { - nativeName: 'ଓଡ଼ିଆ', - }, - 'pa': { - nativeName: 'ਪੰਜਾਬੀ', - }, - 'pa-IN': { - nativeName: 'ਪੰਜਾਬੀ (ਭਾਰਤ ਨੂੰ)', - }, - 'pl': { - nativeName: 'Polski', - }, - 'pl-PL': { - nativeName: 'Polski', - }, - 'ps-AF': { - nativeName: 'پښتو', - }, - 'pt': { - nativeName: 'Português', - }, - 'pt-BR': { - nativeName: 'Português (Brasil)', - }, - 'pt-PT': { - nativeName: 'Português (Portugal)', - }, - 'qu-PE': { - nativeName: 'Qhichwa', - }, - 'rm-CH': { - nativeName: 'Rumantsch', - }, - 'ro': { - nativeName: 'Română', - }, - 'ro-RO': { - nativeName: 'Română', - }, - 'ru': { - nativeName: 'Русский', - }, - 'ru-RU': { - nativeName: 'Русский', - }, - 'sa-IN': { - nativeName: 'संस्कृतम्', - }, - 'se-NO': { - nativeName: 'Davvisámegiella', - }, - 'sh': { - nativeName: 'српскохрватски', - }, - 'si-LK': { - nativeName: 'සිංහල', - }, - 'sk': { - nativeName: 'Slovenčina', - }, - 'sk-SK': { - nativeName: 'Slovenčina (Slovakia)', - }, - 'sl': { - nativeName: 'Slovenščina', - }, - 'sl-SI': { - nativeName: 'Slovenščina', - }, - 'so-SO': { - nativeName: 'Soomaaliga', - }, - 'sq': { - nativeName: 'Shqip', - }, - 'sq-AL': { - nativeName: 'Shqip', - }, - 'sr': { - nativeName: 'Српски', - }, - 'sr-RS': { - nativeName: 'Српски (Serbia)', - }, - 'su': { - nativeName: 'Basa Sunda', - }, - 'sv': { - nativeName: 'Svenska', - }, - 'sv-SE': { - nativeName: 'Svenska', - }, - 'sw': { - nativeName: 'Kiswahili', - }, - 'sw-KE': { - nativeName: 'Kiswahili', - }, - 'ta': { - nativeName: 'தமிழ்', - }, - 'ta-IN': { - nativeName: 'தமிழ்', - }, - 'te': { - nativeName: 'తెలుగు', - }, - 'te-IN': { - nativeName: 'తెలుగు', - }, - 'tg': { - nativeName: 'забо́ни тоҷикӣ́', - }, - 'tg-TJ': { - nativeName: 'тоҷикӣ', - }, - 'th': { - nativeName: 'ภาษาไทย', - }, - 'th-TH': { - nativeName: 'ภาษาไทย (ประเทศไทย)', - }, - 'fil': { - nativeName: 'Filipino', - }, - 'tlh': { - nativeName: 'tlhIngan-Hol', - }, - 'tr': { - nativeName: 'Türkçe', - }, - 'tr-TR': { - nativeName: 'Türkçe', - }, - 'tt-RU': { - nativeName: 'татарча', - }, - 'uk': { - nativeName: 'Українська', - }, - 'uk-UA': { - nativeName: 'Українська', - }, - 'ur': { - nativeName: 'اردو', - }, - 'ur-PK': { - nativeName: 'اردو', - }, - 'uz': { - nativeName: 'O\'zbek', - }, - 'uz-UZ': { - nativeName: 'O\'zbek', - }, - 'vi': { - nativeName: 'Tiếng Việt', - }, - 'vi-VN': { - nativeName: 'Tiếng Việt', - }, - 'xh-ZA': { - nativeName: 'isiXhosa', - }, - 'yi': { - nativeName: 'ייִדיש', - }, - 'yi-DE': { - nativeName: 'ייִדיש (German)', - }, - 'zh': { - nativeName: '中文', - }, - 'zh-Hans': { - nativeName: '中文简体', - }, - 'zh-Hant': { - nativeName: '中文繁體', - }, - 'zh-CN': { - nativeName: '中文(中国大陆)', - }, - 'zh-HK': { - nativeName: '中文(香港)', - }, - 'zh-SG': { - nativeName: '中文(新加坡)', - }, - 'zh-TW': { - nativeName: '中文(台灣)', - }, - 'zu-ZA': { - nativeName: 'isiZulu', - }, -}; diff --git a/packages/client/src/scripts/login-id.ts b/packages/client/src/scripts/login-id.ts deleted file mode 100644 index 0f9c6be4a9..0000000000 --- a/packages/client/src/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/packages/client/src/scripts/lookup-user.ts b/packages/client/src/scripts/lookup-user.ts deleted file mode 100644 index 3ab9d55300..0000000000 --- a/packages/client/src/scripts/lookup-user.ts +++ /dev/null @@ -1,36 +0,0 @@ -import * as Acct from 'misskey-js/built/acct'; -import { i18n } from '@/i18n'; -import * as os from '@/os'; - -export async function lookupUser() { - const { canceled, result } = await os.inputText({ - title: i18n.ts.usernameOrUserId, - }); - if (canceled) return; - - const show = (user) => { - os.pageWindow(`/user-info/${user.id}`); - }; - - const usernamePromise = os.api('users/show', Acct.parse(result)); - const idPromise = os.api('users/show', { userId: result }); - let _notFound = false; - const notFound = () => { - if (_notFound) { - os.alert({ - type: 'error', - text: i18n.ts.noSuchUser, - }); - } else { - _notFound = true; - } - }; - usernamePromise.then(show).catch(err => { - if (err.code === 'NO_SUCH_USER') { - notFound(); - } - }); - idPromise.then(show).catch(err => { - notFound(); - }); -} diff --git a/packages/client/src/scripts/media-proxy.ts b/packages/client/src/scripts/media-proxy.ts deleted file mode 100644 index aaf7f9e610..0000000000 --- a/packages/client/src/scripts/media-proxy.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { query } from '@/scripts/url'; -import { url } from '@/config'; - -export function getProxiedImageUrl(imageUrl: string, type?: 'preview'): string { - return `${url}/proxy/image.webp?${query({ - url: imageUrl, - fallback: '1', - ...(type ? { [type]: '1' } : {}), - })}`; -} - -export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null { - if (imageUrl == null) return null; - return getProxiedImageUrl(imageUrl, type); -} diff --git a/packages/client/src/scripts/mfm-tags.ts b/packages/client/src/scripts/mfm-tags.ts deleted file mode 100644 index 18e8d7038a..0000000000 --- a/packages/client/src/scripts/mfm-tags.ts +++ /dev/null @@ -1 +0,0 @@ -export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'font', 'blur', 'rainbow', 'sparkle', 'rotate']; diff --git a/packages/client/src/scripts/page-metadata.ts b/packages/client/src/scripts/page-metadata.ts deleted file mode 100644 index 0db8369f9d..0000000000 --- a/packages/client/src/scripts/page-metadata.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as misskey from 'misskey-js'; -import { ComputedRef, inject, isRef, onActivated, onMounted, provide, ref, Ref } from 'vue'; - -export const setPageMetadata = Symbol('setPageMetadata'); -export const pageMetadataProvider = Symbol('pageMetadataProvider'); - -export type PageMetadata = { - title: string; - subtitle?: string; - icon?: string | null; - avatar?: misskey.entities.User | null; - userName?: misskey.entities.User | null; - bg?: string; -}; - -export function definePageMetadata(metadata: PageMetadata | null | Ref<PageMetadata | null> | ComputedRef<PageMetadata | null>): void { - const _metadata = isRef(metadata) ? metadata : ref(metadata); - - provide(pageMetadataProvider, _metadata); - - const set = inject(setPageMetadata) as any; - if (set) { - set(_metadata); - - onMounted(() => { - set(_metadata); - }); - - onActivated(() => { - set(_metadata); - }); - } -} - -export function provideMetadataReceiver(callback: (info: ComputedRef<PageMetadata>) => void): void { - provide(setPageMetadata, callback); -} - -export function injectPageMetadata(): PageMetadata | undefined { - return inject(pageMetadataProvider); -} diff --git a/packages/client/src/scripts/physics.ts b/packages/client/src/scripts/physics.ts deleted file mode 100644 index efda80f074..0000000000 --- a/packages/client/src/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) as HTMLElement[]; - const objs: Matter.Body[] = []; - 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: Matter.Body; - 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.toString(); - 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 = window.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); - window.clearInterval(intervalId); - }, - }; -} diff --git a/packages/client/src/scripts/please-login.ts b/packages/client/src/scripts/please-login.ts deleted file mode 100644 index b8fb853cc1..0000000000 --- a/packages/client/src/scripts/please-login.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { defineAsyncComponent } from 'vue'; -import { $i } from '@/account'; -import { i18n } from '@/i18n'; -import { popup } from '@/os'; - -export function pleaseLogin(path?: string) { - if ($i) return; - - popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), { - autoSet: true, - message: i18n.ts.signinRequired, - }, { - cancelled: () => { - if (path) { - window.location.href = path; - } - }, - }, 'closed'); - - if (!path) throw new Error('signin required'); -} diff --git a/packages/client/src/scripts/popout.ts b/packages/client/src/scripts/popout.ts deleted file mode 100644 index 580031d0a3..0000000000 --- a/packages/client/src/scripts/popout.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as config from '@/config'; -import { appendQuery } from './url'; - -export function popout(path: string, w?: HTMLElement) { - let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path; - url = appendQuery(url, 'zen'); - 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/packages/client/src/scripts/popup-position.ts b/packages/client/src/scripts/popup-position.ts deleted file mode 100644 index e84eebf103..0000000000 --- a/packages/client/src/scripts/popup-position.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { Ref } from 'vue'; - -export function calcPopupPosition(el: HTMLElement, props: { - anchorElement: HTMLElement | null; - innerMargin: number; - direction: 'top' | 'bottom' | 'left' | 'right'; - align: 'top' | 'bottom' | 'left' | 'right' | 'center'; - alignOffset?: number; - x?: number; - y?: number; -}): { top: number; left: number; transformOrigin: string; } { - const contentWidth = el.offsetWidth; - const contentHeight = el.offsetHeight; - - let rect: DOMRect; - - if (props.anchorElement) { - rect = props.anchorElement.getBoundingClientRect(); - } - - const calcPosWhenTop = () => { - let left: number; - let top: number; - - if (props.anchorElement) { - left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2); - top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin; - } else { - left = props.x; - top = (props.y - contentHeight) - props.innerMargin; - } - - left -= (el.offsetWidth / 2); - - if (left + contentWidth - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - contentWidth + window.pageXOffset - 1; - } - - return [left, top]; - }; - - const calcPosWhenBottom = () => { - let left: number; - let top: number; - - if (props.anchorElement) { - left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2); - top = (rect.top + window.pageYOffset + props.anchorElement.offsetHeight) + props.innerMargin; - } else { - left = props.x; - top = (props.y) + props.innerMargin; - } - - left -= (el.offsetWidth / 2); - - if (left + contentWidth - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - contentWidth + window.pageXOffset - 1; - } - - return [left, top]; - }; - - const calcPosWhenLeft = () => { - let left: number; - let top: number; - - if (props.anchorElement) { - left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin; - top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2); - } else { - left = (props.x - contentWidth) - props.innerMargin; - top = props.y; - } - - top -= (el.offsetHeight / 2); - - if (top + contentHeight - window.pageYOffset > window.innerHeight) { - top = window.innerHeight - contentHeight + window.pageYOffset - 1; - } - - return [left, top]; - }; - - const calcPosWhenRight = () => { - let left: number; - let top: number; - - if (props.anchorElement) { - left = (rect.left + props.anchorElement.offsetWidth + window.pageXOffset) + props.innerMargin; - - if (props.align === 'top') { - top = rect.top + window.pageYOffset; - if (props.alignOffset != null) top += props.alignOffset; - } else if (props.align === 'bottom') { - // TODO - } else { // center - top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2); - top -= (el.offsetHeight / 2); - } - } else { - left = props.x + props.innerMargin; - top = props.y; - top -= (el.offsetHeight / 2); - } - - if (top + contentHeight - window.pageYOffset > window.innerHeight) { - top = window.innerHeight - contentHeight + window.pageYOffset - 1; - } - - return [left, top]; - }; - - const calc = (): { - left: number; - top: number; - transformOrigin: string; - } => { - switch (props.direction) { - case 'top': { - const [left, top] = calcPosWhenTop(); - - // ツールチップを上に向かって表示するスペースがなければ下に向かって出す - if (top - window.pageYOffset < 0) { - const [left, top] = calcPosWhenBottom(); - return { left, top, transformOrigin: 'center top' }; - } - - return { left, top, transformOrigin: 'center bottom' }; - } - - case 'bottom': { - const [left, top] = calcPosWhenBottom(); - // TODO: ツールチップを下に向かって表示するスペースがなければ上に向かって出す - return { left, top, transformOrigin: 'center top' }; - } - - case 'left': { - const [left, top] = calcPosWhenLeft(); - - // ツールチップを左に向かって表示するスペースがなければ右に向かって出す - if (left - window.pageXOffset < 0) { - const [left, top] = calcPosWhenRight(); - return { left, top, transformOrigin: 'left center' }; - } - - return { left, top, transformOrigin: 'right center' }; - } - - case 'right': { - const [left, top] = calcPosWhenRight(); - // TODO: ツールチップを右に向かって表示するスペースがなければ左に向かって出す - return { left, top, transformOrigin: 'left center' }; - } - } - }; - - return calc(); -} diff --git a/packages/client/src/scripts/reaction-picker.ts b/packages/client/src/scripts/reaction-picker.ts deleted file mode 100644 index fe32e719da..0000000000 --- a/packages/client/src/scripts/reaction-picker.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { defineAsyncComponent, Ref, ref } from 'vue'; -import { popup } from '@/os'; - -class ReactionPicker { - private src: Ref<HTMLElement | null> = ref(null); - private manualShowing = ref(false); - private onChosen?: (reaction: string) => void; - private onClosed?: () => void; - - constructor() { - // nop - } - - public async init() { - await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.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: ReactionPicker['onChosen'], onClosed: ReactionPicker['onClosed']) { - this.src.value = src; - this.manualShowing.value = true; - this.onChosen = onChosen; - this.onClosed = onClosed; - } -} - -export const reactionPicker = new ReactionPicker(); diff --git a/packages/client/src/scripts/safe-uri-decode.ts b/packages/client/src/scripts/safe-uri-decode.ts deleted file mode 100644 index 301b56d7fd..0000000000 --- a/packages/client/src/scripts/safe-uri-decode.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function safeURIDecode(str: string): string { - try { - return decodeURIComponent(str); - } catch { - return str; - } -} diff --git a/packages/client/src/scripts/scroll.ts b/packages/client/src/scripts/scroll.ts deleted file mode 100644 index f5bc6bf9ce..0000000000 --- a/packages/client/src/scripts/scroll.ts +++ /dev/null @@ -1,85 +0,0 @@ -type ScrollBehavior = 'auto' | 'smooth' | 'instant'; - -export function getScrollContainer(el: HTMLElement | null): HTMLElement | null { - if (el == null || el.tagName === 'HTML') return null; - const overflow = window.getComputedStyle(el).getPropertyValue('overflow-y'); - if (overflow === 'scroll' || overflow === '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 isBottomVisible(el: HTMLElement, tolerance = 1, container = getScrollContainer(el)) { - if (container) return el.scrollHeight <= container.clientHeight + Math.abs(container.scrollTop) + tolerance; - return el.scrollHeight <= window.innerHeight + window.scrollY + tolerance; -} - -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/packages/client/src/scripts/search.ts b/packages/client/src/scripts/search.ts deleted file mode 100644 index 64914d3d65..0000000000 --- a/packages/client/src/scripts/search.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as os from '@/os'; -import { i18n } from '@/i18n'; -import { mainRouter } from '@/router'; - -export async function search() { - const { canceled, result: query } = await os.inputText({ - title: i18n.ts.search, - }); - if (canceled || query == null || query === '') return; - - const q = query.trim(); - - if (q.startsWith('@') && !q.includes(' ')) { - mainRouter.push(`/${q}`); - return; - } - - if (q.startsWith('#')) { - mainRouter.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.alert({ - 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.ts.fetchingAsApObject); - - const res = await promise; - - if (res.type === 'User') { - mainRouter.push(`/@${res.object.username}@${res.object.host}`); - } else if (res.type === 'Note') { - mainRouter.push(`/notes/${res.object.id}`); - } - - return; - } - - mainRouter.push(`/search?q=${encodeURIComponent(q)}`); -} diff --git a/packages/client/src/scripts/select-file.ts b/packages/client/src/scripts/select-file.ts deleted file mode 100644 index ec5f8f65e9..0000000000 --- a/packages/client/src/scripts/select-file.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { ref } from 'vue'; -import { DriveFile } from 'misskey-js/built/entities'; -import * as os from '@/os'; -import { stream } from '@/stream'; -import { i18n } from '@/i18n'; -import { defaultStore } from '@/store'; -import { uploadFile } from '@/scripts/upload'; - -function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> { - return new Promise((res, rej) => { - const keepOriginal = ref(defaultStore.state.keepOriginalUploading); - - const chooseFileFromPc = () => { - const input = document.createElement('input'); - input.type = 'file'; - input.multiple = multiple; - input.onchange = () => { - const promises = Array.from(input.files).map(file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal.value)); - - Promise.all(promises).then(driveFiles => { - res(multiple ? driveFiles : driveFiles[0]); - }).catch(err => { - // アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない - }); - - // 一応廃棄 - (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.inputText({ - title: i18n.ts.uploadFromUrl, - type: 'url', - placeholder: i18n.ts.uploadFromUrlDescription, - }).then(({ canceled, result: url }) => { - if (canceled) return; - - const marker = Math.random().toString(); // TODO: UUIDとか使う - - const connection = stream.useChannel('main'); - connection.on('urlUploadFinished', urlResponse => { - if (urlResponse.marker === marker) { - res(multiple ? [urlResponse.file] : urlResponse.file); - connection.dispose(); - } - }); - - os.api('drive/files/upload-from-url', { - url: url, - folderId: defaultStore.state.uploadFolder, - marker, - }); - - os.alert({ - title: i18n.ts.uploadFromUrlRequested, - text: i18n.ts.uploadFromUrlMayTakeTime, - }); - }); - }; - - os.popupMenu([label ? { - text: label, - type: 'label', - } : undefined, { - type: 'switch', - text: i18n.ts.keepOriginalUploading, - ref: keepOriginal, - }, { - text: i18n.ts.upload, - icon: 'ti ti-upload', - action: chooseFileFromPc, - }, { - text: i18n.ts.fromDrive, - icon: 'ti ti-cloud', - action: chooseFileFromDrive, - }, { - text: i18n.ts.fromUrl, - icon: 'ti ti-link', - action: chooseFileFromUrl, - }], src); - }); -} - -export function selectFile(src: any, label: string | null = null): Promise<DriveFile> { - return select(src, label, false) as Promise<DriveFile>; -} - -export function selectFiles(src: any, label: string | null = null): Promise<DriveFile[]> { - return select(src, label, true) as Promise<DriveFile[]>; -} diff --git a/packages/client/src/scripts/show-suspended-dialog.ts b/packages/client/src/scripts/show-suspended-dialog.ts deleted file mode 100644 index e11569ecd4..0000000000 --- a/packages/client/src/scripts/show-suspended-dialog.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as os from '@/os'; -import { i18n } from '@/i18n'; - -export function showSuspendedDialog() { - return os.alert({ - type: 'error', - title: i18n.ts.yourAccountSuspendedTitle, - text: i18n.ts.yourAccountSuspendedDescription, - }); -} diff --git a/packages/client/src/scripts/shuffle.ts b/packages/client/src/scripts/shuffle.ts deleted file mode 100644 index 05e6cdfbcf..0000000000 --- a/packages/client/src/scripts/shuffle.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * 配列をシャッフル (破壊的) - */ -export function shuffle<T extends any[]>(array: T): T { - let currentIndex = array.length, randomIndex; - - // While there remain elements to shuffle. - while (currentIndex !== 0) { - // Pick a remaining element. - randomIndex = Math.floor(Math.random() * currentIndex); - currentIndex--; - - // And swap it with the current element. - [array[currentIndex], array[randomIndex]] = [ - array[randomIndex], array[currentIndex]]; - } - - return array; -} diff --git a/packages/client/src/scripts/sound.ts b/packages/client/src/scripts/sound.ts deleted file mode 100644 index 9d1f603235..0000000000 --- a/packages/client/src/scripts/sound.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { ColdDeviceStorage } from '@/store'; - -const cache = new Map<string, HTMLAudioElement>(); - -export const soundsTypes = [ - null, - 'syuilo/up', - 'syuilo/down', - 'syuilo/pope1', - 'syuilo/pope2', - 'syuilo/waon', - 'syuilo/popo', - 'syuilo/triple', - 'syuilo/poi1', - 'syuilo/poi2', - 'syuilo/pirori', - 'syuilo/pirori-wet', - 'syuilo/pirori-square-wet', - 'syuilo/square-pico', - 'syuilo/reverved', - 'syuilo/ryukyu', - 'syuilo/kick', - 'syuilo/snare', - 'syuilo/queue-jammed', - 'aisha/1', - 'aisha/2', - 'aisha/3', - 'noizenecio/kick_gaba1', - 'noizenecio/kick_gaba2', - 'noizenecio/kick_gaba3', - 'noizenecio/kick_gaba4', - 'noizenecio/kick_gaba5', - 'noizenecio/kick_gaba6', - 'noizenecio/kick_gaba7', -] as const; - -export function getAudio(file: string, useCache = true): HTMLAudioElement { - let audio: HTMLAudioElement; - if (useCache && cache.has(file)) { - audio = cache.get(file); - } else { - audio = new Audio(`/client-assets/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/packages/client/src/scripts/sticky-sidebar.ts b/packages/client/src/scripts/sticky-sidebar.ts deleted file mode 100644 index c67b8f37ac..0000000000 --- a/packages/client/src/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/packages/client/src/scripts/theme-editor.ts b/packages/client/src/scripts/theme-editor.ts deleted file mode 100644 index 944875ff15..0000000000 --- a/packages/client/src/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/packages/client/src/scripts/theme.ts b/packages/client/src/scripts/theme.ts deleted file mode 100644 index 62a2b9459a..0000000000 --- a/packages/client/src/scripts/theme.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { ref } from 'vue'; -import tinycolor from 'tinycolor2'; -import { globalEvents } from '@/events'; - -export type Theme = { - id: string; - name: string; - author: string; - desc?: string; - base?: 'dark' | 'light'; - props: Record<string, string>; -}; - -import lightTheme from '@/themes/_light.json5'; -import darkTheme from '@/themes/_dark.json5'; -import { deepClone } from './clone'; - -export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X')); - -export const getBuiltinThemes = () => Promise.all( - [ - 'l-light', - 'l-coffee', - 'l-apricot', - 'l-rainy', - 'l-vivid', - 'l-cherry', - 'l-sushi', - 'l-u0', - - 'd-dark', - 'd-persimmon', - 'd-astro', - 'd-future', - 'd-botanical', - 'd-green-lime', - 'd-green-orange', - 'd-cherry', - 'd-ice', - 'd-u0', - ].map(name => import(`../themes/${name}.json5`).then(({ default: _default }): Theme => _default)), -); - -export const getBuiltinThemesRef = () => { - const builtinThemes = ref<Theme[]>([]); - getBuiltinThemes().then(themes => builtinThemes.value = themes); - return builtinThemes; -}; - -let timeout = null; - -export function applyTheme(theme: Theme, persist = true) { - if (timeout) window.clearTimeout(timeout); - - document.documentElement.classList.add('_themeChanging_'); - - timeout = window.setTimeout(() => { - document.documentElement.classList.remove('_themeChanging_'); - }, 1000); - - const colorSchema = theme.base === 'dark' ? 'dark' : 'light'; - - // Deep copy - const _theme = deepClone(theme); - - if (_theme.base) { - const base = [lightTheme, darkTheme].find(x => x.id === _theme.base); - if (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['htmlThemeColor']); - break; - } - } - - for (const [k, v] of Object.entries(props)) { - document.documentElement.style.setProperty(`--${k}`, v.toString()); - } - - document.documentElement.style.setProperty('color-schema', colorSchema); - - if (persist) { - localStorage.setItem('theme', JSON.stringify(props)); - localStorage.setItem('colorSchema', colorSchema); - } - - // 色計算など再度行えるようにクライアント全体に通知 - 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/packages/client/src/scripts/time.ts b/packages/client/src/scripts/time.ts deleted file mode 100644 index 34e8b6b17c..0000000000 --- a/packages/client/src/scripts/time.ts +++ /dev/null @@ -1,39 +0,0 @@ -const dateTimeIntervals = { - 'day': 86400000, - 'hour': 3600000, - 'ms': 1, -}; - -export function dateUTC(time: number[]): Date { - const d = time.length === 2 ? Date.UTC(time[0], time[1]) - : time.length === 3 ? Date.UTC(time[0], time[1], time[2]) - : time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3]) - : time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4]) - : time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5]) - : time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6]) - : null; - - if (!d) throw 'wrong number of arguments'; - - return new Date(d); -} - -export function isTimeSame(a: Date, b: Date): boolean { - return a.getTime() === b.getTime(); -} - -export function isTimeBefore(a: Date, b: Date): boolean { - return (a.getTime() - b.getTime()) < 0; -} - -export function isTimeAfter(a: Date, b: Date): boolean { - return (a.getTime() - b.getTime()) > 0; -} - -export function addTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date { - return new Date(x.getTime() + (value * dateTimeIntervals[span])); -} - -export function subtractTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date { - return new Date(x.getTime() - (value * dateTimeIntervals[span])); -} diff --git a/packages/client/src/scripts/timezones.ts b/packages/client/src/scripts/timezones.ts deleted file mode 100644 index 8ce07323f6..0000000000 --- a/packages/client/src/scripts/timezones.ts +++ /dev/null @@ -1,49 +0,0 @@ -export const timezones = [{ - name: 'UTC', - abbrev: 'UTC', - offset: 0, -}, { - name: 'Europe/Berlin', - abbrev: 'CET', - offset: 60, -}, { - name: 'Asia/Tokyo', - abbrev: 'JST', - offset: 540, -}, { - name: 'Asia/Seoul', - abbrev: 'KST', - offset: 540, -}, { - name: 'Asia/Shanghai', - abbrev: 'CST', - offset: 480, -}, { - name: 'Australia/Sydney', - abbrev: 'AEST', - offset: 600, -}, { - name: 'Australia/Darwin', - abbrev: 'ACST', - offset: 570, -}, { - name: 'Australia/Perth', - abbrev: 'AWST', - offset: 480, -}, { - name: 'America/New_York', - abbrev: 'EST', - offset: -300, -}, { - name: 'America/Mexico_City', - abbrev: 'CST', - offset: -360, -}, { - name: 'America/Phoenix', - abbrev: 'MST', - offset: -420, -}, { - name: 'America/Los_Angeles', - abbrev: 'PST', - offset: -480, -}]; diff --git a/packages/client/src/scripts/touch.ts b/packages/client/src/scripts/touch.ts deleted file mode 100644 index 5251bc2e27..0000000000 --- a/packages/client/src/scripts/touch.ts +++ /dev/null @@ -1,23 +0,0 @@ -const isTouchSupported = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0; - -export let isTouchUsing = false; - -export let isScreenTouching = false; - -if (isTouchSupported) { - window.addEventListener('touchstart', () => { - // maxTouchPointsなどでの判定だけだと、「タッチ機能付きディスプレイを使っているがマウスでしか操作しない」場合にも - // タッチで使っていると判定されてしまうため、実際に一度でもタッチされたらtrueにする - isTouchUsing = true; - - isScreenTouching = true; - }, { passive: true }); - - window.addEventListener('touchend', () => { - // 子要素のtouchstartイベントでstopPropagation()が呼ばれると親要素に伝搬されずタッチされたと判定されないため、 - // touchendイベントでもtouchstartイベントと同様にtrueにする - isTouchUsing = true; - - isScreenTouching = false; - }, { passive: true }); -} diff --git a/packages/client/src/scripts/unison-reload.ts b/packages/client/src/scripts/unison-reload.ts deleted file mode 100644 index 59af584c1b..0000000000 --- a/packages/client/src/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(); - } -} diff --git a/packages/client/src/scripts/upload.ts b/packages/client/src/scripts/upload.ts deleted file mode 100644 index 9a39652ef5..0000000000 --- a/packages/client/src/scripts/upload.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { reactive, ref } from 'vue'; -import * as Misskey from 'misskey-js'; -import { readAndCompressImage } from 'browser-image-resizer'; -import { getCompressionConfig } from './upload/compress-config'; -import { defaultStore } from '@/store'; -import { apiUrl } from '@/config'; -import { $i } from '@/account'; -import { alert } from '@/os'; -import { i18n } from '@/i18n'; - -type Uploading = { - id: string; - name: string; - progressMax: number | undefined; - progressValue: number | undefined; - img: string; -}; -export const uploads = ref<Uploading[]>([]); - -const mimeTypeMap = { - 'image/webp': 'webp', - 'image/jpeg': 'jpg', - 'image/png': 'png', -} as const; - -export function uploadFile( - file: File, - folder?: any, - name?: string, - keepOriginal: boolean = defaultStore.state.keepOriginalUploading, -): Promise<Misskey.entities.DriveFile> { - if ($i == null) throw new Error('Not logged in'); - - if (folder && typeof folder === 'object') folder = folder.id; - - return new Promise((resolve, reject) => { - const id = Math.random().toString(); - - const reader = new FileReader(); - reader.onload = async (): Promise<void> => { - const ctx = reactive<Uploading>({ - id: id, - name: name ?? file.name ?? 'untitled', - progressMax: undefined, - progressValue: undefined, - img: window.URL.createObjectURL(file), - }); - - uploads.value.push(ctx); - - const config = !keepOriginal ? await getCompressionConfig(file) : undefined; - let resizedImage: Blob | undefined; - if (config) { - try { - const resized = await readAndCompressImage(file, config); - if (resized.size < file.size || file.type === 'image/webp') { - // The compression may not always reduce the file size - // (and WebP is not browser safe yet) - resizedImage = resized; - } - if (_DEV_) { - const saved = ((1 - resized.size / file.size) * 100).toFixed(2); - console.log(`Image compression: before ${file.size} bytes, after ${resized.size} bytes, saved ${saved}%`); - } - - ctx.name = file.type !== config.mimeType ? `${ctx.name}.${mimeTypeMap[config.mimeType]}` : ctx.name; - } catch (err) { - console.error('Failed to resize image', err); - } - } - - const formData = new FormData(); - formData.append('i', $i.token); - formData.append('force', 'true'); - formData.append('file', resizedImage ?? file); - formData.append('name', ctx.name); - if (folder) formData.append('folderId', folder); - - const xhr = new XMLHttpRequest(); - xhr.open('POST', apiUrl + '/drive/files/create', true); - xhr.onload = ((ev: ProgressEvent<XMLHttpRequest>) => { - if (xhr.status !== 200 || ev.target == null || ev.target.response == null) { - // TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい - uploads.value = uploads.value.filter(x => x.id !== id); - - if (ev.target?.response) { - const res = JSON.parse(ev.target.response); - if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') { - alert({ - type: 'error', - title: i18n.ts.failedToUpload, - text: i18n.ts.cannotUploadBecauseInappropriate, - }); - } else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') { - alert({ - type: 'error', - title: i18n.ts.failedToUpload, - text: i18n.ts.cannotUploadBecauseNoFreeSpace, - }); - } else { - alert({ - type: 'error', - title: i18n.ts.failedToUpload, - text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`, - }); - } - } else { - alert({ - type: 'error', - title: 'Failed to upload', - text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`, - }); - } - - reject(); - return; - } - - const driveFile = JSON.parse(ev.target.response); - - resolve(driveFile); - - uploads.value = uploads.value.filter(x => x.id !== id); - }) as (ev: ProgressEvent<EventTarget>) => any; - - xhr.upload.onprogress = ev => { - if (ev.lengthComputable) { - ctx.progressMax = ev.total; - ctx.progressValue = ev.loaded; - } - }; - - xhr.send(formData); - }; - reader.readAsArrayBuffer(file); - }); -} diff --git a/packages/client/src/scripts/upload/compress-config.ts b/packages/client/src/scripts/upload/compress-config.ts deleted file mode 100644 index 793c78ad20..0000000000 --- a/packages/client/src/scripts/upload/compress-config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import isAnimated from 'is-file-animated'; -import type { BrowserImageResizerConfig } from 'browser-image-resizer'; - -const compressTypeMap = { - 'image/jpeg': { quality: 0.85, mimeType: 'image/jpeg' }, - 'image/png': { quality: 1, mimeType: 'image/png' }, - 'image/webp': { quality: 0.85, mimeType: 'image/jpeg' }, - 'image/svg+xml': { quality: 1, mimeType: 'image/png' }, -} as const; - -export async function getCompressionConfig(file: File): Promise<BrowserImageResizerConfig | undefined> { - const imgConfig = compressTypeMap[file.type]; - if (!imgConfig || await isAnimated(file)) { - return; - } - - return { - maxWidth: 2048, - maxHeight: 2048, - debug: true, - ...imgConfig, - }; -} diff --git a/packages/client/src/scripts/url.ts b/packages/client/src/scripts/url.ts deleted file mode 100644 index 86735de9f0..0000000000 --- a/packages/client/src/scripts/url.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function query(obj: Record<string, any>): string { - const params = Object.entries(obj) - .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) - .reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>); - - return Object.entries(params) - .map((p) => `${p[0]}=${encodeURIComponent(p[1])}`) - .join('&'); -} - -export function appendQuery(url: string, query: string): string { - return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`; -} diff --git a/packages/client/src/scripts/use-chart-tooltip.ts b/packages/client/src/scripts/use-chart-tooltip.ts deleted file mode 100644 index 881e5e9ad5..0000000000 --- a/packages/client/src/scripts/use-chart-tooltip.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { onUnmounted, ref } from 'vue'; -import * as os from '@/os'; -import MkChartTooltip from '@/components/MkChartTooltip.vue'; - -export function useChartTooltip(opts: { position: 'top' | 'middle' } = { position: 'top' }) { - const tooltipShowing = ref(false); - const tooltipX = ref(0); - const tooltipY = ref(0); - const tooltipTitle = ref(null); - const tooltipSeries = ref(null); - let disposeTooltipComponent; - - os.popup(MkChartTooltip, { - showing: tooltipShowing, - x: tooltipX, - y: tooltipY, - title: tooltipTitle, - series: tooltipSeries, - }, {}).then(({ dispose }) => { - disposeTooltipComponent = dispose; - }); - - onUnmounted(() => { - if (disposeTooltipComponent) disposeTooltipComponent(); - }); - - function handler(context) { - if (context.tooltip.opacity === 0) { - tooltipShowing.value = false; - return; - } - - tooltipTitle.value = context.tooltip.title[0]; - tooltipSeries.value = context.tooltip.body.map((b, i) => ({ - backgroundColor: context.tooltip.labelColors[i].backgroundColor, - borderColor: context.tooltip.labelColors[i].borderColor, - text: b.lines[0], - })); - - const rect = context.chart.canvas.getBoundingClientRect(); - - tooltipShowing.value = true; - tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX; - if (opts.position === 'top') { - tooltipY.value = rect.top + window.pageYOffset; - } else if (opts.position === 'middle') { - tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY; - } - } - - return { - handler, - }; -} diff --git a/packages/client/src/scripts/use-interval.ts b/packages/client/src/scripts/use-interval.ts deleted file mode 100644 index 201ba417ef..0000000000 --- a/packages/client/src/scripts/use-interval.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { onMounted, onUnmounted } from 'vue'; - -export function useInterval(fn: () => void, interval: number, options: { - immediate: boolean; - afterMounted: boolean; -}): void { - if (Number.isNaN(interval)) return; - - let intervalId: number | null = null; - - if (options.afterMounted) { - onMounted(() => { - if (options.immediate) fn(); - intervalId = window.setInterval(fn, interval); - }); - } else { - if (options.immediate) fn(); - intervalId = window.setInterval(fn, interval); - } - - onUnmounted(() => { - if (intervalId) window.clearInterval(intervalId); - }); -} diff --git a/packages/client/src/scripts/use-leave-guard.ts b/packages/client/src/scripts/use-leave-guard.ts deleted file mode 100644 index a93b84d1fe..0000000000 --- a/packages/client/src/scripts/use-leave-guard.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { inject, onUnmounted, Ref } from 'vue'; -import { i18n } from '@/i18n'; -import * as os from '@/os'; - -export function useLeaveGuard(enabled: Ref<boolean>) { - /* TODO - const setLeaveGuard = inject('setLeaveGuard'); - - if (setLeaveGuard) { - setLeaveGuard(async () => { - if (!enabled.value) return false; - - const { canceled } = await os.confirm({ - type: 'warning', - text: i18n.ts.leaveConfirm, - }); - - return canceled; - }); - } else { - onBeforeRouteLeave(async (to, from) => { - if (!enabled.value) return true; - - const { canceled } = await os.confirm({ - type: 'warning', - text: i18n.ts.leaveConfirm, - }); - - return !canceled; - }); - } - */ - - /* - function onBeforeLeave(ev: BeforeUnloadEvent) { - if (enabled.value) { - ev.preventDefault(); - ev.returnValue = ''; - } - } - - window.addEventListener('beforeunload', onBeforeLeave); - onUnmounted(() => { - window.removeEventListener('beforeunload', onBeforeLeave); - }); - */ -} diff --git a/packages/client/src/scripts/use-note-capture.ts b/packages/client/src/scripts/use-note-capture.ts deleted file mode 100644 index e6bdb345c4..0000000000 --- a/packages/client/src/scripts/use-note-capture.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { onUnmounted, Ref } from 'vue'; -import * as misskey from 'misskey-js'; -import { stream } from '@/stream'; -import { $i } from '@/account'; - -export function useNoteCapture(props: { - rootEl: Ref<HTMLElement>; - note: Ref<misskey.entities.Note>; - isDeletedRef: Ref<boolean>; -}) { - const note = props.note; - const connection = $i ? stream : null; - - function onStreamNoteUpdated(noteData): void { - const { type, id, body } = noteData; - - if (id !== note.value.id) return; - - switch (type) { - case 'reacted': { - const reaction = body.reaction; - - if (body.emoji) { - const emojis = note.value.emojis || []; - if (!emojis.includes(body.emoji)) { - note.value.emojis = [...emojis, body.emoji]; - } - } - - // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる - const currentCount = (note.value.reactions || {})[reaction] || 0; - - note.value.reactions[reaction] = currentCount + 1; - - if ($i && (body.userId === $i.id)) { - note.value.myReaction = reaction; - } - break; - } - - case 'unreacted': { - const reaction = body.reaction; - - // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる - const currentCount = (note.value.reactions || {})[reaction] || 0; - - note.value.reactions[reaction] = Math.max(0, currentCount - 1); - - if ($i && (body.userId === $i.id)) { - note.value.myReaction = null; - } - break; - } - - case 'pollVoted': { - const choice = body.choice; - - const choices = [...note.value.poll.choices]; - choices[choice] = { - ...choices[choice], - votes: choices[choice].votes + 1, - ...($i && (body.userId === $i.id) ? { - isVoted: true, - } : {}), - }; - - note.value.poll.choices = choices; - break; - } - - case 'deleted': { - props.isDeletedRef.value = true; - break; - } - } - } - - function capture(withHandler = false): void { - if (connection) { - // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する - connection.send(document.body.contains(props.rootEl.value) ? 'sr' : 's', { id: note.value.id }); - if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated); - } - } - - function decapture(withHandler = false): void { - if (connection) { - connection.send('un', { - id: note.value.id, - }); - if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated); - } - } - - function onStreamConnected() { - capture(false); - } - - capture(true); - if (connection) { - connection.on('_connected_', onStreamConnected); - } - - onUnmounted(() => { - decapture(true); - if (connection) { - connection.off('_connected_', onStreamConnected); - } - }); -} diff --git a/packages/client/src/scripts/use-tooltip.ts b/packages/client/src/scripts/use-tooltip.ts deleted file mode 100644 index 1f6e0fb6ce..0000000000 --- a/packages/client/src/scripts/use-tooltip.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Ref, ref, watch, onUnmounted } from 'vue'; - -export function useTooltip( - elRef: Ref<HTMLElement | { $el: HTMLElement } | null | undefined>, - onShow: (showing: Ref<boolean>) => void, - delay = 300, -): void { - let isHovering = false; - - // iOS(Androidも?)では、要素をタップした直後に(おせっかいで)mouseoverイベントを発火させたりするため、それを無視するためのフラグ - // 無視しないと、画面に触れてないのにツールチップが出たりし、ユーザビリティが損なわれる - // TODO: 一度でもタップすると二度とマウスでツールチップ出せなくなるのをどうにかする 定期的にfalseに戻すとか...? - let shouldIgnoreMouseover = false; - - let timeoutId: number; - - let changeShowingState: (() => void) | null; - - const open = () => { - close(); - if (!isHovering) return; - if (elRef.value == null) return; - const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el; - if (!document.body.contains(el)) return; // openしようとしたときに既に元要素がDOMから消えている場合があるため - - const showing = ref(true); - onShow(showing); - changeShowingState = () => { - showing.value = false; - }; - }; - - const close = () => { - if (changeShowingState != null) { - changeShowingState(); - changeShowingState = null; - } - }; - - const onMouseover = () => { - if (isHovering) return; - if (shouldIgnoreMouseover) return; - isHovering = true; - timeoutId = window.setTimeout(open, delay); - }; - - const onMouseleave = () => { - if (!isHovering) return; - isHovering = false; - window.clearTimeout(timeoutId); - close(); - }; - - const onTouchstart = () => { - shouldIgnoreMouseover = true; - if (isHovering) return; - isHovering = true; - timeoutId = window.setTimeout(open, delay); - }; - - const onTouchend = () => { - if (!isHovering) return; - isHovering = false; - window.clearTimeout(timeoutId); - close(); - }; - - const stop = watch(elRef, () => { - if (elRef.value) { - stop(); - const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el; - el.addEventListener('mouseover', onMouseover, { passive: true }); - el.addEventListener('mouseleave', onMouseleave, { passive: true }); - el.addEventListener('touchstart', onTouchstart, { passive: true }); - el.addEventListener('touchend', onTouchend, { passive: true }); - el.addEventListener('click', close, { passive: true }); - } - }, { - immediate: true, - flush: 'post', - }); - - onUnmounted(() => { - close(); - }); -} |