wip
This commit is contained in:
parent
bfb5f070fd
commit
d73904ff1f
|
@ -111,7 +111,6 @@ import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { uploadFile, uploa______ds } from '@/utility/upload.js';
|
|
||||||
import { claimAchievement } from '@/utility/achievements.js';
|
import { claimAchievement } from '@/utility/achievements.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
import { chooseFileFromPc } from '@/utility/select-file.js';
|
import { chooseFileFromPc } from '@/utility/select-file.js';
|
||||||
|
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="$style.body">
|
<div :class="$style.body">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.footer">
|
<div v-if="$slots.footer" :class="$style.footer">
|
||||||
<slot name="footer"></slot>
|
<slot name="footer"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -125,15 +125,14 @@ defineExpose({
|
||||||
.body {
|
.body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background: var(--MI_THEME-panel);
|
background: var(--MI_THEME-bg);
|
||||||
container-type: size;
|
container-type: size;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
display: flex;
|
padding: 8px 16px;
|
||||||
flex-shrink: 0;
|
overflow: auto;
|
||||||
background: var(--MI_THEME-windowHeader);
|
background: var(--MI_THEME-bg);
|
||||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
border-top: 1px solid var(--MI_THEME-divider);
|
||||||
backdrop-filter: var(--MI-blur, blur(15px));
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -127,7 +127,6 @@ import { i18n } from '@/i18n.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
import { ensureSignin, notesCount, incNotesCount } from '@/i.js';
|
import { ensureSignin, notesCount, incNotesCount } from '@/i.js';
|
||||||
import { getAccounts, openAccountMenu as openAccountMenu_ } from '@/accounts.js';
|
import { getAccounts, openAccountMenu as openAccountMenu_ } from '@/accounts.js';
|
||||||
import { uploadFile } from '@/utility/upload.js';
|
|
||||||
import { deepClone } from '@/utility/clone.js';
|
import { deepClone } from '@/utility/clone.js';
|
||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
|
|
|
@ -8,34 +8,44 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
ref="dialog"
|
ref="dialog"
|
||||||
:width="800"
|
:width="800"
|
||||||
:height="500"
|
:height="500"
|
||||||
@click="cancel()"
|
|
||||||
@close="cancel()"
|
@close="cancel()"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
{{ i18n.tsx.uploadNFiles({ n: files.length }) }}
|
<i class="ti ti-upload"></i> {{ i18n.tsx.uploadNFiles({ n: files.length }) }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div>
|
<div :class="$style.root" class="_gaps_s">
|
||||||
<div :class="$style.items">
|
<MkSwitch v-model="compress">
|
||||||
<div v-for="ctx in items" :key="ctx.id" :class="$style.item">
|
<template #label>{{ i18n.ts.compress }}</template>
|
||||||
<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ ctx.thumbnail })` }"></div>
|
</MkSwitch>
|
||||||
<div class="top">
|
<div :class="$style.items" class="_gaps_s">
|
||||||
<p class="name"><MkLoading :em="true"/>{{ ctx.name }}</p>
|
<div v-for="ctx in items" :key="ctx.id" v-panel :class="$style.item" :style="{ '--p': ctx.progressValue !== null ? `${ctx.progressValue / ctx.progressMax * 100}%` : '0%' }">
|
||||||
<p class="status">
|
<div :class="$style.itemInner">
|
||||||
<span v-if="ctx.progressValue === null" class="initing">{{ i18n.ts.waiting }}<MkEllipsis/></span>
|
<div>
|
||||||
<span v-if="ctx.progressValue !== null" class="kb">{{ String(Math.floor(ctx.progressValue / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i> / {{ String(Math.floor(ctx.progressMax / 1024)).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,') }}<i>KB</i></span>
|
<MkButton :iconOnly="true" rounded><i class="ti ti-dots"></i></MkButton>
|
||||||
<span v-if="ctx.progressValue !== null" class="percentage">{{ Math.floor((ctx.progressValue / ctx.progressMax) * 100) }}</span>
|
</div>
|
||||||
</p>
|
<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ ctx.thumbnail })` }"></div>
|
||||||
|
<div :class="$style.itemBody">
|
||||||
|
<div>{{ ctx.name }}</div>
|
||||||
|
<div style="opacity: 0.7; margin-top: 4px; font-size: 90%;">
|
||||||
|
<span>{{ bytes(ctx.file.size) }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<MkLoading v-if="ctx.uploading" :em="true"/>
|
||||||
|
<MkSystemIcon v-else-if="ctx.uploaded" type="success" style="width: 40px;"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<progress :value="ctx.progressValue || 0" :max="ctx.progressMax || 0" :class="{ initing: ctx.progressValue === null, waiting: ctx.progressValue !== null && ctx.progressValue === ctx.progressMax }"></progress>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div>
|
<div class="_buttonsCenter">
|
||||||
<MkButton primary rounded @click="upload()">{{ i18n.ts.upload }}</MkButton>
|
<MkButton primary rounded @click="upload()"><i class="ti ti-upload"></i> {{ i18n.ts.upload }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</MkModalWindow>
|
</MkModalWindow>
|
||||||
|
@ -46,13 +56,18 @@ import { markRaw, onMounted, ref, useTemplateRef } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
|
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
|
||||||
|
import { apiUrl } from '@@/js/config.js';
|
||||||
import { getCompressionConfig } from '@/utility/upload/compress-config.js';
|
import { getCompressionConfig } from '@/utility/upload/compress-config.js';
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
import { $i } from '@/i.js';
|
import { ensureSignin } from '@/i.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import bytes from '@/filters/bytes.js';
|
||||||
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
|
|
||||||
|
const $i = ensureSignin();
|
||||||
|
|
||||||
const mimeTypeMap = {
|
const mimeTypeMap = {
|
||||||
'image/webp': 'webp',
|
'image/webp': 'webp',
|
||||||
|
@ -78,12 +93,15 @@ const items = ref([] as {
|
||||||
progressMax: number | null;
|
progressMax: number | null;
|
||||||
progressValue: number | null;
|
progressValue: number | null;
|
||||||
thumbnail: string;
|
thumbnail: string;
|
||||||
|
uploading: boolean;
|
||||||
uploaded: Misskey.entities.DriveFile | null;
|
uploaded: Misskey.entities.DriveFile | null;
|
||||||
file: File;
|
file: File;
|
||||||
}[]);
|
}[]);
|
||||||
|
|
||||||
const dialog = useTemplateRef('dialog');
|
const dialog = useTemplateRef('dialog');
|
||||||
|
|
||||||
|
const compress = ref(true);
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
// TODO: アップロードを中止しますか?
|
// TODO: アップロードを中止しますか?
|
||||||
dialog.value?.close();
|
dialog.value?.close();
|
||||||
|
@ -91,52 +109,51 @@ function cancel() {
|
||||||
|
|
||||||
function upload() {
|
function upload() {
|
||||||
for (const item of items.value) {
|
for (const item of items.value) {
|
||||||
if ((file.size > instance.maxFileSize) || (file.size > ($i.policies.maxFileSizeMb * 1024 * 1024))) {
|
if ((item.file.size > instance.maxFileSize) || (item.file.size > ($i.policies.maxFileSizeMb * 1024 * 1024))) {
|
||||||
alert({
|
alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: i18n.ts.failedToUpload,
|
title: i18n.ts.failedToUpload,
|
||||||
text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
|
text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
|
||||||
});
|
});
|
||||||
return Promise.reject();
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = async (): Promise<void> => {
|
reader.onload = async (): Promise<void> => {
|
||||||
const config = !keepOriginal ? await getCompressionConfig(file) : undefined;
|
const config = compress.value ? await getCompressionConfig(item.file) : undefined;
|
||||||
let resizedImage: Blob | undefined;
|
let resizedImage: Blob | undefined;
|
||||||
if (config) {
|
if (config) {
|
||||||
try {
|
try {
|
||||||
const resized = await readAndCompressImage(file, config);
|
const resized = await readAndCompressImage(item.file, config);
|
||||||
if (resized.size < file.size || file.type === 'image/webp') {
|
if (resized.size < item.file.size || item.file.type === 'image/webp') {
|
||||||
// The compression may not always reduce the file size
|
// The compression may not always reduce the file size
|
||||||
// (and WebP is not browser safe yet)
|
// (and WebP is not browser safe yet)
|
||||||
resizedImage = resized;
|
resizedImage = resized;
|
||||||
}
|
}
|
||||||
if (_DEV_) {
|
if (_DEV_) {
|
||||||
const saved = ((1 - resized.size / file.size) * 100).toFixed(2);
|
const saved = ((1 - resized.size / item.file.size) * 100).toFixed(2);
|
||||||
console.log(`Image compression: before ${file.size} bytes, after ${resized.size} bytes, saved ${saved}%`);
|
console.log(`Image compression: before ${item.file.size} bytes, after ${resized.size} bytes, saved ${saved}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.name = file.type !== config.mimeType ? `${ctx.name}.${mimeTypeMap[config.mimeType]}` : ctx.name;
|
item.name = item.file.type !== config.mimeType ? `${item.name}.${mimeTypeMap[config.mimeType]}` : item.name;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to resize image', err);
|
console.error('Failed to resize image', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('i', $i!.token);
|
formData.append('i', $i.token);
|
||||||
formData.append('force', 'true');
|
formData.append('force', 'true');
|
||||||
formData.append('file', resizedImage ?? file);
|
formData.append('file', resizedImage ?? item.file);
|
||||||
formData.append('name', ctx.name);
|
formData.append('name', item.name);
|
||||||
if (_folder) formData.append('folderId', _folder);
|
if (props.folderId) formData.append('folderId', props.folderId);
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open('POST', apiUrl + '/drive/files/create', true);
|
xhr.open('POST', apiUrl + '/drive/files/create', true);
|
||||||
xhr.onload = ((ev: ProgressEvent<XMLHttpRequest>) => {
|
xhr.onload = ((ev: ProgressEvent<XMLHttpRequest>) => {
|
||||||
if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
|
item.uploading = false;
|
||||||
// TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい
|
|
||||||
uploa______ds.value = uploa______ds.value.filter(x => x.id !== id);
|
|
||||||
|
|
||||||
|
if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
|
||||||
if (xhr.status === 413) {
|
if (xhr.status === 413) {
|
||||||
alert({
|
alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
@ -177,22 +194,19 @@ function upload() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const driveFile = JSON.parse(ev.target.response);
|
const driveFile = JSON.parse(ev.target.response);
|
||||||
|
item.uploaded = driveFile;
|
||||||
resolve(driveFile);
|
|
||||||
|
|
||||||
uploa______ds.value = uploa______ds.value.filter(x => x.id !== id);
|
|
||||||
}) as (ev: ProgressEvent<EventTarget>) => any;
|
}) as (ev: ProgressEvent<EventTarget>) => any;
|
||||||
|
|
||||||
xhr.upload.onprogress = ev => {
|
xhr.upload.onprogress = ev => {
|
||||||
if (ev.lengthComputable) {
|
if (ev.lengthComputable) {
|
||||||
ctx.progressMax = ev.total;
|
item.progressMax = ev.total;
|
||||||
ctx.progressValue = ev.loaded;
|
item.progressValue = ev.loaded;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
};
|
};
|
||||||
reader.readAsArrayBuffer(file);
|
reader.readAsArrayBuffer(item.file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,23 +229,50 @@ onMounted(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.mk-uploader > ol > li > progress {
|
.root {
|
||||||
display: block;
|
padding: 12px;
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
grid-column: 2/3;
|
|
||||||
grid-row: 2/3;
|
|
||||||
z-index: 2;
|
|
||||||
width: 100%;
|
|
||||||
height: 8px;
|
|
||||||
}
|
}
|
||||||
.mk-uploader > ol > li > progress::-webkit-progress-value {
|
|
||||||
background: var(--MI_THEME-accent);
|
.items {
|
||||||
}
|
}
|
||||||
.mk-uploader > ol > li > progress::-webkit-progress-bar {
|
|
||||||
//background: var(--MI_THEME-accentAlpha01);
|
.item {
|
||||||
background: transparent;
|
position: relative;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: clip;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: var(--p);
|
||||||
|
height: 100%;
|
||||||
|
background: color(from var(--MI_THEME-accent) srgb r g b / 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemInner {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 8px 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemThumbnail {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemBody {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -95,7 +95,6 @@ import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { validators } from '@/components/grid/cell-validators.js';
|
import { validators } from '@/components/grid/cell-validators.js';
|
||||||
import { chooseFileFromDrive, chooseFileFromPc } from '@/utility/select-file.js';
|
import { chooseFileFromDrive, chooseFileFromPc } from '@/utility/select-file.js';
|
||||||
import { uploadFile } from '@/utility/upload.js';
|
|
||||||
import { extractDroppedItems, flattenDroppedFiles } from '@/utility/file-drop.js';
|
import { extractDroppedItems, flattenDroppedFiles } from '@/utility/file-drop.js';
|
||||||
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
|
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
|
||||||
import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
|
import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
|
||||||
|
|
|
@ -41,7 +41,6 @@ import { formatTimeString } from '@/utility/format-time-string.js';
|
||||||
import { selectFile } from '@/utility/select-file.js';
|
import { selectFile } from '@/utility/select-file.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { uploadFile } from '@/utility/upload.js';
|
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
|
|
|
@ -9,7 +9,6 @@ import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { useStream } from '@/stream.js';
|
import { useStream } from '@/stream.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { uploadFile } from '@/utility/upload.js';
|
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
|
|
||||||
export function chooseFileFromPc(
|
export function chooseFileFromPc(
|
||||||
|
@ -31,7 +30,7 @@ export function chooseFileFromPc(
|
||||||
|
|
||||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUploadDialog.vue')), {
|
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUploadDialog.vue')), {
|
||||||
files: markRaw(Array.from(input.files)),
|
files: markRaw(Array.from(input.files)),
|
||||||
uploadFolder,
|
folderId: uploadFolder,
|
||||||
}, {
|
}, {
|
||||||
done: driveFiles => {
|
done: driveFiles => {
|
||||||
res(driveFiles);
|
res(driveFiles);
|
||||||
|
|
Loading…
Reference in New Issue