diff options
Diffstat (limited to 'packages/client/src')
| -rw-r--r-- | packages/client/src/components/autocomplete.vue | 22 | ||||
| -rw-r--r-- | packages/client/src/components/drive.vue | 10 | ||||
| -rw-r--r-- | packages/client/src/components/post-form.vue | 3 | ||||
| -rw-r--r-- | packages/client/src/os.ts | 75 | ||||
| -rw-r--r-- | packages/client/src/pages/messaging/messaging-room.form.vue | 3 | ||||
| -rw-r--r-- | packages/client/src/pages/settings/index.vue | 6 | ||||
| -rw-r--r-- | packages/client/src/pages/settings/preferences-registry.vue | 380 | ||||
| -rw-r--r-- | packages/client/src/scripts/select-file.ts | 3 | ||||
| -rw-r--r-- | packages/client/src/scripts/upload.ts | 114 | ||||
| -rw-r--r-- | packages/client/src/store.ts | 10 | ||||
| -rw-r--r-- | packages/client/src/ui/_common_/common.vue | 3 | ||||
| -rw-r--r-- | packages/client/src/ui/_common_/upload.vue | 2 |
12 files changed, 539 insertions, 92 deletions
diff --git a/packages/client/src/components/autocomplete.vue b/packages/client/src/components/autocomplete.vue index adeac4e050..1e4a4506f7 100644 --- a/packages/client/src/components/autocomplete.vue +++ b/packages/client/src/components/autocomplete.vue @@ -131,8 +131,8 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: 'done', v: { type: string; value: any }): void; - (e: 'closed'): void; + (event: 'done', value: { type: string; value: any }): void; + (event: 'closed'): void; }>(); const suggests = ref<Element>(); @@ -152,7 +152,7 @@ function complete(type: string, value: any) { emit('closed'); if (type === 'emoji') { let recents = defaultStore.state.recentlyUsedEmojis; - recents = recents.filter((e: any) => e !== value); + recents = recents.filter((emoji: any) => emoji !== value); recents.unshift(value); defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32)); } @@ -232,7 +232,7 @@ function exec() { } else if (props.type === 'emoji') { if (!props.q || props.q === '') { // 最近使った絵文字をサジェスト - emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji === emoji)).filter(x => x) as EmojiDef[]; + emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[]; return; } @@ -269,17 +269,17 @@ function exec() { } } -function onMousedown(e: Event) { - if (!contains(rootEl.value, e.target) && (rootEl.value !== e.target)) props.close(); +function onMousedown(event: Event) { + if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close(); } -function onKeydown(e: KeyboardEvent) { +function onKeydown(event: KeyboardEvent) { const cancel = () => { - e.preventDefault(); - e.stopPropagation(); + event.preventDefault(); + event.stopPropagation(); }; - switch (e.key) { + switch (event.key) { case 'Enter': if (select.value !== -1) { cancel(); @@ -310,7 +310,7 @@ function onKeydown(e: KeyboardEvent) { break; default: - e.stopPropagation(); + event.stopPropagation(); props.textarea.focus(); } } diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue index e044c67523..2ec885b00c 100644 --- a/packages/client/src/components/drive.vue +++ b/packages/client/src/components/drive.vue @@ -97,6 +97,7 @@ import * as os from '@/os'; import { stream } from '@/stream'; import { defaultStore } from '@/store'; import { i18n } from '@/i18n'; +import { uploadFile, uploads } from '@/scripts/upload'; const props = withDefaults(defineProps<{ initialFolder?: Misskey.entities.DriveFolder; @@ -127,8 +128,9 @@ const moreFolders = ref(false); const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]); const selectedFiles = ref<Misskey.entities.DriveFile[]>([]); const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]); -const uploadings = os.uploads; +const uploadings = uploads; const connection = stream.useChannel('drive'); +const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい // ドロップされようとしているか const draghover = ref(false); @@ -355,7 +357,7 @@ function onChangeFileInput() { } function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) { - os.upload(file, (folderToUpload && typeof folderToUpload == 'object') ? folderToUpload.id : null).then(res => { + uploadFile(file, (folderToUpload && typeof folderToUpload == 'object') ? folderToUpload.id : null, undefined, keepOriginal.value).then(res => { addFile(res, true); }); } @@ -562,6 +564,10 @@ function fetchMoreFiles() { function getMenu() { return [{ + type: 'switch', + text: i18n.ts.keepOriginalUploading, + ref: keepOriginal, + }, null, { text: i18n.ts.addFile, type: 'label' }, { diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue index 656689ddcb..241c726c11 100644 --- a/packages/client/src/components/post-form.vue +++ b/packages/client/src/components/post-form.vue @@ -87,6 +87,7 @@ import MkInfo from '@/components/ui/info.vue'; import { i18n } from '@/i18n'; import { instance } from '@/instance'; import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account'; +import { uploadFile } from '@/scripts/upload'; const modal = inject('modal'); @@ -372,7 +373,7 @@ function updateFileName(file, name) { } function upload(file: File, name?: string) { - os.upload(file, defaultStore.state.uploadFolder, name).then(res => { + uploadFile(file, defaultStore.state.uploadFolder, name).then(res => { files.push(res); }); } diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index 43c110555f..b8a3f94cc8 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -1,6 +1,6 @@ // TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する -import { Component, defineAsyncComponent, markRaw, reactive, Ref, ref } from 'vue'; +import { Component, markRaw, Ref, ref } from 'vue'; import { EventEmitter } from 'eventemitter3'; import insertTextAtCursor from 'insert-text-at-cursor'; import * as Misskey from 'misskey-js'; @@ -10,7 +10,6 @@ import MkWaitingDialog from '@/components/waiting-dialog.vue'; import { MenuItem } from '@/types/menu'; import { resolve } from '@/router'; import { $i } from '@/account'; -import { defaultStore } from '@/store'; export const pendingApiRequestsCount = ref(0); @@ -537,78 +536,6 @@ export function post(props: Record<string, any> = {}) { export const deckGlobalEvents = new EventEmitter(); -export const uploads = ref<{ - id: string; - name: string; - progressMax: number | undefined; - progressValue: number | undefined; - img: string; -}[]>([]); - -export function upload(file: File, folder?: any, name?: string, keepOriginal: boolean = defaultStore.state.keepOriginalUploading): Promise<Misskey.entities.DriveFile> { - if (folder && typeof folder === 'object') folder = folder.id; - - return new Promise((resolve, reject) => { - const id = Math.random().toString(); - - const reader = new FileReader(); - reader.onload = (e) => { - const ctx = reactive({ - id: id, - name: name || file.name || 'untitled', - progressMax: undefined, - progressValue: undefined, - img: window.URL.createObjectURL(file) - }); - - uploads.value.push(ctx); - - console.log(keepOriginal); - - const data = new FormData(); - data.append('i', $i.token); - data.append('force', 'true'); - data.append('file', file); - - if (folder) data.append('folderId', folder); - if (name) data.append('name', name); - - const xhr = new XMLHttpRequest(); - xhr.open('POST', apiUrl + '/drive/files/create', true); - xhr.onload = (ev) => { - if (xhr.status !== 200 || ev.target == null || ev.target.response == null) { - // TODO: 消すのではなくて再送できるようにしたい - uploads.value = uploads.value.filter(x => x.id != id); - - alert({ - type: 'error', - text: 'upload failed' - }); - - reject(); - return; - } - - const driveFile = JSON.parse(ev.target.response); - - resolve(driveFile); - - uploads.value = uploads.value.filter(x => x.id != id); - }; - - xhr.upload.onprogress = e => { - if (e.lengthComputable) { - ctx.progressMax = e.total; - ctx.progressValue = e.loaded; - } - }; - - xhr.send(data); - }; - reader.readAsArrayBuffer(file); - }); -} - /* export function checkExistence(fileData: ArrayBuffer): Promise<any> { return new Promise((resolve, reject) => { diff --git a/packages/client/src/pages/messaging/messaging-room.form.vue b/packages/client/src/pages/messaging/messaging-room.form.vue index 3863c8f82b..35cb75743f 100644 --- a/packages/client/src/pages/messaging/messaging-room.form.vue +++ b/packages/client/src/pages/messaging/messaging-room.form.vue @@ -31,6 +31,7 @@ import * as os from '@/os'; import { stream } from '@/stream'; import { Autocomplete } from '@/scripts/autocomplete'; import { throttle } from 'throttle-debounce'; +import { uploadFile } from '@/scripts/upload'; export default defineComponent({ props: { @@ -164,7 +165,7 @@ export default defineComponent({ }, upload(file: File, name?: string) { - os.upload(file, this.$store.state.uploadFolder, name).then(res => { + uploadFile(file, this.$store.state.uploadFolder, name).then(res => { this.file = res; }); }, diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue index e6670ea930..3106a2e5c6 100644 --- a/packages/client/src/pages/settings/index.vue +++ b/packages/client/src/pages/settings/index.vue @@ -132,6 +132,11 @@ const menuDef = computed(() => [{ text: i18n.ts.plugins, to: '/settings/plugin', active: props.initialPage === 'plugin', + }, { + icon: 'fas fa-floppy-disk', + text: i18n.ts.preferencesRegistryShort, + to: '/settings/preferences-registry', + active: props.initialPage === 'preferences-registry', }], }, { title: i18n.ts.otherSettings, @@ -225,6 +230,7 @@ const component = computed(() => { case 'deck': return defineAsyncComponent(() => import('./deck.vue')); case 'plugin': return defineAsyncComponent(() => import('./plugin.vue')); case 'plugin/install': return defineAsyncComponent(() => import('./plugin.install.vue')); + case 'preferences-registry': return defineAsyncComponent(() => import('./preferences-registry.vue')); case 'import-export': return defineAsyncComponent(() => import('./import-export.vue')); case 'account-info': return defineAsyncComponent(() => import('./account-info.vue')); case 'delete-account': return defineAsyncComponent(() => import('./delete-account.vue')); diff --git a/packages/client/src/pages/settings/preferences-registry.vue b/packages/client/src/pages/settings/preferences-registry.vue new file mode 100644 index 0000000000..c01e4ee83d --- /dev/null +++ b/packages/client/src/pages/settings/preferences-registry.vue @@ -0,0 +1,380 @@ +<template> +<div class="_formRoot"> + <div :class="$style.buttons"> + <MkButton inline class="primary" @click="saveNew">{{ ts._preferencesRegistry.saveNew }}</MkButton> + <MkButton inline class="" @click="loadFile">{{ ts._preferencesRegistry.loadFile }}</MkButton> + </div> + + <FormSection> + <template #label>{{ ts._preferencesRegistry.list }}</template> + <div + v-if="registries && Object.keys(registries).length > 0" + v-for="(registry, id) in registries" + :key="id" + class="_formBlock _panel" + :class="$style.registry" + @click="$event => menu($event, id)" + @contextmenu.prevent.stop="$event => menu($event, id)" + > + <div :class="$style.registryName">{{ registry.name }}</div> + <div :class="$style.registryTime">{{ t('_preferencesRegistry.createdAt', { date: (new Date(registry.createdAt)).toLocaleDateString(), time: (new Date(registry.createdAt)).toLocaleTimeString() }) }}</div> + <div :class="$style.registryTime" v-if="registry.updatedAt">{{ t('_preferencesRegistry.updatedAt', { date: (new Date(registry.updatedAt)).toLocaleDateString(), time: (new Date(registry.updatedAt)).toLocaleTimeString() }) }}</div> + </div> + <div v-else-if="registries"> + <MkInfo>{{ ts._preferencesRegistry.noRegistries }}</MkInfo> + </div> + <MkLoading v-else /> + </FormSection> +</div> +</template> + +<script lang="ts" setup> +import { onMounted, onUnmounted, useCssModule } from 'vue'; +import FormSection from '@/components/form/section.vue'; +import MkButton from '@/components/ui/button.vue'; +import MkInfo from '@/components/ui/info.vue'; +import * as os from '@/os'; +import { v4 as uuid } from 'uuid'; +import { ColdDeviceStorage, defaultStore } from '@/store'; +import * as symbols from '@/symbols'; +import { unisonReload } from '@/scripts/unison-reload'; +import { stream } from '@/stream'; +import { $i } from '@/account'; +import { i18n } from '@/i18n'; +import { version } from '@/config'; +const { t, ts } = i18n; + +useCssModule(); + +const scope = ['clientPreferencesProfiles']; + +const connection = $i && stream.useChannel('main'); + +const registryProps = ['name', 'createdAt', 'updatedAt', 'version', 'defaultStore', 'coldDeviceStorage', 'fontSize', 'useSystemFont', 'wallpaper']; +type Registry = { + name: string; + createdAt: string; + updatedAt: string | null; + version: string; + defaultStore: Partial<typeof defaultStore.state>; + coldDeviceStorage: Partial<typeof ColdDeviceStorage.default>; + fontSize: string | null; + useSystemFont: 't' | null; + wallpaper: string | null; +}; + +type Registries = { + [key: string]: Registry; +}; + +let registries = $ref<Registries | null>(null); + +os.api('i/registry/get-all', { scope }) + .then(res => { + registries = res || {}; + }); + +function getDefaultStoreValues() { + return (Object.keys(defaultStore.state) as (keyof typeof defaultStore.state)[]).reduce((acc, key) => { + if (defaultStore.def[key].where !== 'account') acc[key] = defaultStore.state[key]; + return acc; + }, {} as any); +} + +function isObject(value: any) { + return value && typeof value === 'object' && !Array.isArray(value); +} + +function validate(registry: any): void { + if (!registries) return; + + // Check if unnecessary properties exist + if (Object.keys(registry).some(key => !registryProps.includes(key))) throw Error('Unnecessary properties exist'); + + if (!registry.name) throw Error('Name is falsy'); + if (!registry.version) throw Error('Version is falsy'); + + // Check if createdAt and updatedAt is Date + // https://zenn.dev/lollipop_onl/articles/eoz-judge-js-invalid-date + if (!registry.createdAt || Number.isNaN(new Date(registry.createdAt).getTime())) throw Error('createdAt is falsy or not Date'); + if (registry.updatedAt) { + if (Number.isNaN(new Date(registry.updatedAt).getTime())) { + throw Error('updatedAt is not Date'); + } + } else if (registry.updatedAt !== null) { + throw Error('updatedAt is not null'); + } + + if (!registry.defaultStore || !isObject(registry.defaultStore)) throw Error('defaultStore is falsy or not an object'); + if (!registry.coldDeviceStorage || !isObject(registry.coldDeviceStorage)) throw Error('coldDeviceStorage is falsy or not an object'); +} + +async function saveNew() { + if (!registries) return; + + const { canceled, result: name } = await os.inputText({ + title: ts._preferencesRegistry.inputName, + text: ts._preferencesRegistry.saveNewDescription, + }); + + if (canceled) return; + if (Object.entries(registries).some(e => e[1].name === name)) { + return os.alert({ + title: ts._preferencesRegistry.cannotSave, + text: t('_preferencesRegistry.nameAlreadyExists', { name }), + }); + } + + const id = uuid(); + const registry: Registry = { + name, + createdAt: (new Date()).toISOString(), + updatedAt: null, + version, + defaultStore: getDefaultStoreValues(), + coldDeviceStorage: ColdDeviceStorage.getAll(), + fontSize: localStorage.getItem('fontSize'), + useSystemFont: localStorage.getItem('useSystemFont') as 't' | null, + wallpaper: localStorage.getItem('wallpaper'), + }; + await os.api('i/registry/set', { scope, key: id, value: registry }); +} + +function loadFile() { + const input = document.createElement('input'); + input.type = 'file'; + input.multiple = false; + input.onchange = async () => { + if (!registries) return; + if (!input.files || input.files.length === 0) return; + + const file = input.files[0]; + + if (file.type !== 'application/json') { + return os.alert({ + type: 'error', + title: ts._preferencesRegistry.cannotLoad, + text: ts._preferencesRegistry.invalidFile, + }); + } + + let registry: Registry; + try { + registry = JSON.parse(await file.text()) as unknown as Registry; + validate(registry); + } catch (e) { + return os.alert({ + type: 'error', + title: ts._preferencesRegistry.cannotLoad, + text: e?.message, + }); + } + + const id = uuid(); + await os.api('i/registry/set', { scope, key: id, value: registry }); + + // 一応廃棄 + (window as any).__misskey_input_ref__ = null; + }; + + // https://qiita.com/fukasawah/items/b9dc732d95d99551013d + // iOS Safari で正常に動かす為のおまじない + (window as any).__misskey_input_ref__ = input; + + input.click(); +} + +async function applyRegistry(id: string) { + if (!registries) return; + + const registry = registries[id]; + + const { canceled: cancel1 } = await os.confirm({ + type: 'warning', + title: ts._preferencesRegistry.apply, + text: t('_preferencesRegistry.applyConfirm', { name: registry.name }), + }); + if (cancel1) return; + + // defaultStore + for (const [key, value] of Object.entries(registry.defaultStore)) { + if (key in defaultStore.def && defaultStore.def[key].where !== 'account') { + defaultStore.set(key as keyof Registry['defaultStore'], value); + } + } + + // coldDeviceStorage + for (const [key, value] of Object.entries(registry.coldDeviceStorage)) { + ColdDeviceStorage.set(key as keyof Registry['coldDeviceStorage'], value); + } + + // fontSize + if (registry.fontSize) { + localStorage.setItem('fontSize', registry.fontSize); + } else { + localStorage.removeItem('fontSize'); + } + + // useSystemFont + if (registry.useSystemFont) { + localStorage.setItem('useSystemFont', registry.useSystemFont); + } else { + localStorage.removeItem('useSystemFont'); + } + + // wallpaper + if (registry.wallpaper != null) { + localStorage.setItem('wallpaper', registry.wallpaper); + } else { + localStorage.removeItem('wallpaper'); + } + + const { canceled: cancel2 } = await os.confirm({ + type: 'info', + text: ts.reloadToApplySetting, + }); + if (cancel2) return; + + unisonReload(); +} + +async function deleteRegistry(id: string) { + if (!registries) return; + + const { canceled } = await os.confirm({ + type: 'info', + title: ts._preferencesRegistry.delete, + text: t('_preferencesRegistry.deleteConfirm', { name: registries[id].name }), + }); + if (canceled) return; + + await os.api('i/registry/remove', { scope, key: id }); + delete registries[id]; +} + +async function save(id: string) { + if (!registries) return; + + const { name, createdAt } = registries[id]; + + const { canceled } = await os.confirm({ + type: 'info', + title: ts._preferencesRegistry.save, + text: t('_preferencesRegistry.saveConfirm', { name }), + }); + if (canceled) return; + + const registry: Registry = { + name, + createdAt, + updatedAt: (new Date()).toISOString(), + version, + defaultStore: getDefaultStoreValues(), + coldDeviceStorage: ColdDeviceStorage.getAll(), + fontSize: localStorage.getItem('fontSize'), + useSystemFont: localStorage.getItem('useSystemFont') as 't' | null, + wallpaper: localStorage.getItem('wallpaper'), + }; + await os.api('i/registry/set', { scope, key: id, value: registry }); +} + +async function rename(id: string) { + if (!registries) return; + + const { canceled: cancel1, result: name } = await os.inputText({ + title: ts._preferencesRegistry.inputName, + }); + if (cancel1 || registries[id].name === name) return; + + if (Object.entries(registries).some(e => e[1].name === name)) { + return os.alert({ + title: ts._preferencesRegistry.cannotSave, + text: t('_preferencesRegistry.nameAlreadyExists', { name }), + }); + } + + const registry = Object.assign({}, { ...registries[id] }); + + const { canceled: cancel2 } = await os.confirm({ + type: 'info', + title: ts._preferencesRegistry.rename, + text: t('_preferencesRegistry.renameConfirm', { old: registry.name, new: name }), + }); + if (cancel2) return; + + registry.name = name; + await os.api('i/registry/set', { scope, key: id, value: registry }); +} + +function menu(ev: MouseEvent, registryId: string) { + if (!registries) return; + + return os.popupMenu([{ + text: ts._preferencesRegistry.apply, + icon: 'fas fa-circle-down', + action: () => applyRegistry(registryId), + }, { + type: 'a', + text: ts._preferencesRegistry.download, + icon: 'fas fa-download', + href: URL.createObjectURL(new Blob([JSON.stringify(registries[registryId], null, 2)], { type: 'application/json' })), + download: `${registries[registryId].name}.json`, + }, { + text: ts._preferencesRegistry.rename, + icon: 'fas fa-i-cursor', + action: () => rename(registryId), + }, { + text: ts._preferencesRegistry.save, + icon: 'fas fa-floppy-disk', + action: () => save(registryId), + }, { + text: ts._preferencesRegistry.delete, + icon: 'fas fa-trash-can', + action: () => deleteRegistry(registryId), + }], ev.currentTarget ?? ev.target) +} + +onMounted(() => { + // streamingのuser storage updateイベントを監視して更新 + connection?.on('registryUpdated', ({ scope: recievedScope, key, value }) => { + if (!recievedScope || recievedScope.length !== scope.length || recievedScope[0] !== scope[0]) return; + if (!registries) return; + + registries[key] = value; + }); +}); + +onUnmounted(() => { + connection?.off('registryUpdated'); +}); + +defineExpose({ + [symbols.PAGE_INFO]: { + title: ts.preferencesRegistry, + icon: 'fas fa-floppy-disk', + bg: 'var(--bg)', + } +}) +</script> + +<style lang="scss" module> +.buttons { + display: flex; + gap: var(--margin); + flex-wrap: wrap; +} + +.registry { + padding: 20px; + cursor: pointer; + + &Name { + font-weight: 700; + } + + &Time { + font-size: .85em; + opacity: .7; + } +} +</style> diff --git a/packages/client/src/scripts/select-file.ts b/packages/client/src/scripts/select-file.ts index 23df4edf54..49a46f0bb2 100644 --- a/packages/client/src/scripts/select-file.ts +++ b/packages/client/src/scripts/select-file.ts @@ -4,6 +4,7 @@ import { stream } from '@/stream'; import { i18n } from '@/i18n'; import { defaultStore } from '@/store'; import { DriveFile } from 'misskey-js/built/entities'; +import { uploadFile } from '@/scripts/upload'; function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> { return new Promise((res, rej) => { @@ -14,7 +15,7 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv input.type = 'file'; input.multiple = multiple; input.onchange = () => { - const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder, undefined, keepOriginal.value)); + 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]); diff --git a/packages/client/src/scripts/upload.ts b/packages/client/src/scripts/upload.ts new file mode 100644 index 0000000000..7e4f793b44 --- /dev/null +++ b/packages/client/src/scripts/upload.ts @@ -0,0 +1,114 @@ +import { reactive, ref } from 'vue'; +import { defaultStore } from '@/store'; +import { apiUrl } from '@/config'; +import * as Misskey from 'misskey-js'; +import { $i } from '@/account'; +import { readAndCompressImage } from 'browser-image-resizer'; +import { alert } from '@/os'; + +type Uploading = { + id: string; + name: string; + progressMax: number | undefined; + progressValue: number | undefined; + img: string; +}; +export const uploads = ref<Uploading[]>([]); + +const compressTypeMap = { + 'image/jpeg': { quality: 0.85, mimeType: 'image/jpeg' }, + 'image/webp': { quality: 0.85, mimeType: 'image/jpeg' }, + 'image/svg+xml': { quality: 1, mimeType: 'image/png' }, +} as const; + +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 (folder && typeof folder == 'object') folder = folder.id; + + return new Promise((resolve, reject) => { + const id = Math.random().toString(); + + const reader = new FileReader(); + reader.onload = async (e) => { + const ctx = reactive<Uploading>({ + id: id, + name: name || file.name || 'untitled', + progressMax: undefined, + progressValue: undefined, + img: window.URL.createObjectURL(file) + }); + + uploads.value.push(ctx); + + let resizedImage: any; + if (!keepOriginal && file.type in compressTypeMap) { + const imgConfig = compressTypeMap[file.type]; + + const config = { + maxWidth: 2048, + maxHeight: 2048, + debug: true, + ...imgConfig, + }; + + try { + resizedImage = await readAndCompressImage(file, config); + ctx.name = file.type !== imgConfig.mimeType ? `${ctx.name}.${mimeTypeMap[compressTypeMap[file.type].mimeType]}` : ctx.name; + } catch (e) { + console.error('Failed to resize image', e); + } + } + + const data = new FormData(); + data.append('i', $i.token); + data.append('force', 'true'); + data.append('file', resizedImage || file); + data.append('name', ctx.name); + if (folder) data.append('folderId', folder); + + const xhr = new XMLHttpRequest(); + xhr.open('POST', apiUrl + '/drive/files/create', true); + xhr.onload = (ev) => { + if (xhr.status !== 200 || ev.target == null || ev.target.response == null) { + // TODO: 消すのではなくて再送できるようにしたい + uploads.value = uploads.value.filter(x => x.id != id); + + 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); + }; + + xhr.upload.onprogress = e => { + if (e.lengthComputable) { + ctx.progressMax = e.total; + ctx.progressValue = e.loaded; + } + }; + + xhr.send(data); + }; + reader.readAsArrayBuffer(file); + }); +} diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index b9800ec607..296eaa2068 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -286,6 +286,16 @@ export class ColdDeviceStorage { } } + public static getAll(): Partial<typeof this.default> { + return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce((acc, key) => { + const value = localStorage.getItem(PREFIX + key); + if (value != null) { + acc[key] = JSON.parse(value); + } + return acc; + }, {} as any); + } + public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void { localStorage.setItem(PREFIX + key, JSON.stringify(value)); diff --git a/packages/client/src/ui/_common_/common.vue b/packages/client/src/ui/_common_/common.vue index 05688d7c53..50d95539d1 100644 --- a/packages/client/src/ui/_common_/common.vue +++ b/packages/client/src/ui/_common_/common.vue @@ -17,7 +17,8 @@ <script lang="ts"> import { defineAsyncComponent, defineComponent } from 'vue'; -import { popup, popups, uploads, pendingApiRequestsCount } from '@/os'; +import { popup, popups, pendingApiRequestsCount } from '@/os'; +import { uploads } from '@/scripts/upload'; import * as sound from '@/scripts/sound'; import { $i } from '@/account'; import { stream } from '@/stream'; diff --git a/packages/client/src/ui/_common_/upload.vue b/packages/client/src/ui/_common_/upload.vue index ab7678a505..f3703d0e8f 100644 --- a/packages/client/src/ui/_common_/upload.vue +++ b/packages/client/src/ui/_common_/upload.vue @@ -20,8 +20,8 @@ <script lang="ts" setup> import { } from 'vue'; import * as os from '@/os'; +import { uploads } from '@/scripts/upload'; -const uploads = os.uploads; const zIndex = os.claimZIndex('high'); </script> |