summaryrefslogtreecommitdiff
path: root/packages/client/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/client/src')
-rw-r--r--packages/client/src/components/autocomplete.vue22
-rw-r--r--packages/client/src/components/drive.vue10
-rw-r--r--packages/client/src/components/post-form.vue3
-rw-r--r--packages/client/src/os.ts75
-rw-r--r--packages/client/src/pages/messaging/messaging-room.form.vue3
-rw-r--r--packages/client/src/pages/settings/index.vue6
-rw-r--r--packages/client/src/pages/settings/preferences-registry.vue380
-rw-r--r--packages/client/src/scripts/select-file.ts3
-rw-r--r--packages/client/src/scripts/upload.ts114
-rw-r--r--packages/client/src/store.ts10
-rw-r--r--packages/client/src/ui/_common_/common.vue3
-rw-r--r--packages/client/src/ui/_common_/upload.vue2
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>