This commit is contained in:
syuilo 2025-05-12 18:30:20 +09:00
parent bfb5f070fd
commit d73904ff1f
7 changed files with 103 additions and 68 deletions

View File

@ -111,7 +111,6 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
import { uploadFile, uploa______ds } from '@/utility/upload.js';
import { claimAchievement } from '@/utility/achievements.js';
import { prefer } from '@/preferences.js';
import { chooseFileFromPc } from '@/utility/select-file.js';

View File

@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.body">
<slot></slot>
</div>
<div :class="$style.footer">
<div v-if="$slots.footer" :class="$style.footer">
<slot name="footer"></slot>
</div>
</div>
@ -125,15 +125,14 @@ defineExpose({
.body {
flex: 1;
overflow: auto;
background: var(--MI_THEME-panel);
background: var(--MI_THEME-bg);
container-type: size;
}
.footer {
display: flex;
flex-shrink: 0;
background: var(--MI_THEME-windowHeader);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
padding: 8px 16px;
overflow: auto;
background: var(--MI_THEME-bg);
border-top: 1px solid var(--MI_THEME-divider);
}
</style>

View File

@ -127,7 +127,6 @@ import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { ensureSignin, notesCount, incNotesCount } from '@/i.js';
import { getAccounts, openAccountMenu as openAccountMenu_ } from '@/accounts.js';
import { uploadFile } from '@/utility/upload.js';
import { deepClone } from '@/utility/clone.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { miLocalStorage } from '@/local-storage.js';

View File

@ -8,34 +8,44 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="dialog"
:width="800"
:height="500"
@click="cancel()"
@close="cancel()"
@closed="emit('closed')"
>
<template #header>
{{ i18n.tsx.uploadNFiles({ n: files.length }) }}
<i class="ti ti-upload"></i> {{ i18n.tsx.uploadNFiles({ n: files.length }) }}
</template>
<div>
<div :class="$style.items">
<div v-for="ctx in items" :key="ctx.id" :class="$style.item">
<div :class="$style.itemThumbnail" :style="{ backgroundImage: `url(${ ctx.thumbnail })` }"></div>
<div class="top">
<p class="name"><MkLoading :em="true"/>{{ ctx.name }}</p>
<p class="status">
<span v-if="ctx.progressValue === null" class="initing">{{ i18n.ts.waiting }}<MkEllipsis/></span>
<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>
<span v-if="ctx.progressValue !== null" class="percentage">{{ Math.floor((ctx.progressValue / ctx.progressMax) * 100) }}</span>
</p>
<div :class="$style.root" class="_gaps_s">
<MkSwitch v-model="compress">
<template #label>{{ i18n.ts.compress }}</template>
</MkSwitch>
<div :class="$style.items" class="_gaps_s">
<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%' }">
<div :class="$style.itemInner">
<div>
<MkButton :iconOnly="true" rounded><i class="ti ti-dots"></i></MkButton>
</div>
<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>
<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>
<template #footer>
<div>
<MkButton primary rounded @click="upload()">{{ i18n.ts.upload }}</MkButton>
<div class="_buttonsCenter">
<MkButton primary rounded @click="upload()"><i class="ti ti-upload"></i> {{ i18n.ts.upload }}</MkButton>
</div>
</template>
</MkModalWindow>
@ -46,13 +56,18 @@ import { markRaw, onMounted, ref, useTemplateRef } from 'vue';
import * as Misskey from 'misskey-js';
import { v4 as uuid } from 'uuid';
import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
import { apiUrl } from '@@/js/config.js';
import { getCompressionConfig } from '@/utility/upload/compress-config.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import { $i } from '@/i.js';
import { ensureSignin } from '@/i.js';
import { instance } from '@/instance.js';
import MkButton from '@/components/MkButton.vue';
import bytes from '@/filters/bytes.js';
import MkSwitch from '@/components/MkSwitch.vue';
const $i = ensureSignin();
const mimeTypeMap = {
'image/webp': 'webp',
@ -78,12 +93,15 @@ const items = ref([] as {
progressMax: number | null;
progressValue: number | null;
thumbnail: string;
uploading: boolean;
uploaded: Misskey.entities.DriveFile | null;
file: File;
}[]);
const dialog = useTemplateRef('dialog');
const compress = ref(true);
function cancel() {
// TODO:
dialog.value?.close();
@ -91,52 +109,51 @@ function cancel() {
function upload() {
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({
type: 'error',
title: i18n.ts.failedToUpload,
text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit,
});
return Promise.reject();
continue;
}
const reader = new FileReader();
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;
if (config) {
try {
const resized = await readAndCompressImage(file, config);
if (resized.size < file.size || file.type === 'image/webp') {
const resized = await readAndCompressImage(item.file, config);
if (resized.size < item.file.size || item.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}%`);
const saved = ((1 - resized.size / item.file.size) * 100).toFixed(2);
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) {
console.error('Failed to resize image', err);
}
}
const formData = new FormData();
formData.append('i', $i!.token);
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);
formData.append('file', resizedImage ?? item.file);
formData.append('name', item.name);
if (props.folderId) formData.append('folderId', props.folderId);
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: ()
uploa______ds.value = uploa______ds.value.filter(x => x.id !== id);
item.uploading = false;
if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
if (xhr.status === 413) {
alert({
type: 'error',
@ -177,22 +194,19 @@ function upload() {
}
const driveFile = JSON.parse(ev.target.response);
resolve(driveFile);
uploa______ds.value = uploa______ds.value.filter(x => x.id !== id);
item.uploaded = driveFile;
}) as (ev: ProgressEvent<EventTarget>) => any;
xhr.upload.onprogress = ev => {
if (ev.lengthComputable) {
ctx.progressMax = ev.total;
ctx.progressValue = ev.loaded;
item.progressMax = ev.total;
item.progressValue = ev.loaded;
}
};
xhr.send(formData);
};
reader.readAsArrayBuffer(file);
reader.readAsArrayBuffer(item.file);
}
}
@ -215,23 +229,50 @@ onMounted(() => {
</script>
<style lang="scss" module>
.mk-uploader > ol > li > progress {
display: block;
background: transparent;
border: none;
border-radius: 4px;
overflow: hidden;
grid-column: 2/3;
grid-row: 2/3;
z-index: 2;
width: 100%;
height: 8px;
.root {
padding: 12px;
}
.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);
background: transparent;
.item {
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>

View File

@ -95,7 +95,6 @@ import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { validators } from '@/components/grid/cell-validators.js';
import { chooseFileFromDrive, chooseFileFromPc } from '@/utility/select-file.js';
import { uploadFile } from '@/utility/upload.js';
import { extractDroppedItems, flattenDroppedFiles } from '@/utility/file-drop.js';
import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';

View File

@ -41,7 +41,6 @@ import { formatTimeString } from '@/utility/format-time-string.js';
import { selectFile } from '@/utility/select-file.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { uploadFile } from '@/utility/upload.js';
import { miLocalStorage } from '@/local-storage.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { prefer } from '@/preferences.js';

View File

@ -9,7 +9,6 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
import { uploadFile } from '@/utility/upload.js';
import { prefer } from '@/preferences.js';
export function chooseFileFromPc(
@ -31,7 +30,7 @@ export function chooseFileFromPc(
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUploadDialog.vue')), {
files: markRaw(Array.from(input.files)),
uploadFolder,
folderId: uploadFolder,
}, {
done: driveFiles => {
res(driveFiles);