This commit is contained in:
syuilo 2025-05-13 15:29:13 +09:00
parent 566f729300
commit 37f846af8a
10 changed files with 86 additions and 71 deletions

View File

@ -33,27 +33,23 @@ import { onMounted, useTemplateRef, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import Cropper from 'cropperjs'; import Cropper from 'cropperjs';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { apiUrl } from '@@/js/config.js';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { $i } from '@/i.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { getProxiedImageUrl } from '@/utility/media-proxy.js';
import { prefer } from '@/preferences.js';
const emit = defineEmits<{
(ev: 'ok', cropped: Misskey.entities.DriveFile): void;
(ev: 'cancel'): void;
(ev: 'closed'): void;
}>();
const props = defineProps<{ const props = defineProps<{
file: Misskey.entities.DriveFile; imageFile: File;
aspectRatio: number; aspectRatio: number;
uploadFolder?: string | null; uploadFolder?: string | null;
}>(); }>();
const imgUrl = getProxiedImageUrl(props.file.url, undefined, true); const emit = defineEmits<{
(ev: 'ok', cropped: File | Blob): void;
(ev: 'cancel'): void;
(ev: 'closed'): void;
}>();
const imgUrl = URL.createObjectURL(props.imageFile);
const dialogEl = useTemplateRef('dialogEl'); const dialogEl = useTemplateRef('dialogEl');
const imgEl = useTemplateRef('imgEl'); const imgEl = useTemplateRef('imgEl');
let cropper: Cropper | null = null; let cropper: Cropper | null = null;
@ -71,26 +67,7 @@ const ok = async () => {
const croppedCanvas = await croppedSection?.$toCanvas({ width: widthToRender }); const croppedCanvas = await croppedSection?.$toCanvas({ width: widthToRender });
croppedCanvas?.toBlob(blob => { croppedCanvas?.toBlob(blob => {
if (!blob) return; if (!blob) return;
const formData = new FormData(); res(blob);
formData.append('file', blob);
formData.append('name', `cropped_${props.file.name}`);
formData.append('isSensitive', props.file.isSensitive ? 'true' : 'false');
if (props.file.comment) { formData.append('comment', props.file.comment);}
formData.append('i', $i!.token);
if (props.uploadFolder) {
formData.append('folderId', props.uploadFolder);
} else if (props.uploadFolder !== null && prefer.s.uploadFolder) {
formData.append('folderId', prefer.s.uploadFolder);
}
window.fetch(apiUrl + '/drive/files/create', {
method: 'POST',
body: formData,
})
.then(response => response.json())
.then(f => {
res(f);
});
}); });
}); });

View File

@ -148,7 +148,7 @@ import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.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 { chooseFileFromPcAndUpload } from '@/utility/select-file.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
import { isSeparatorNeeded, getSeparatorInfo, makeDateGroupedTimelineComputedRef } from '@/utility/timeline-date-separate.js'; import { isSeparatorNeeded, getSeparatorInfo, makeDateGroupedTimelineComputedRef } from '@/utility/timeline-date-separate.js';
import { usePagination } from '@/composables/use-pagination.js'; import { usePagination } from '@/composables/use-pagination.js';
@ -555,7 +555,7 @@ function getMenu() {
text: i18n.ts.upload, text: i18n.ts.upload,
icon: 'ti ti-upload', icon: 'ti ti-upload',
action: () => { action: () => {
chooseFileFromPc(true, { uploadFolder: folder.value?.id, keepOriginal: true }); chooseFileFromPcAndUpload(true, { uploadFolder: folder.value?.id, keepOriginal: true });
}, },
}, { }, {
text: i18n.ts.fromUrl, text: i18n.ts.fromUrl,

View File

@ -145,7 +145,7 @@ async function describe(file: Misskey.entities.DriveFile) {
async function crop(file: Misskey.entities.DriveFile): Promise<void> { async function crop(file: Misskey.entities.DriveFile): Promise<void> {
if (mock) return; if (mock) return;
const newFile = await os.cropImage(file, { aspectRatio: NaN }); const newFile = await os.createCroppedImageDriveFileFromImageDriveFile(file, { aspectRatio: NaN });
emit('replaceFile', file, newFile); emit('replaceFile', file, newFile);
} }

View File

@ -37,7 +37,7 @@ import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
import FormSlot from '@/components/form/slot.vue'; import FormSlot from '@/components/form/slot.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import { chooseFileFromPc } from '@/utility/select-file.js'; import { chooseFileFromPcAndUpload } from '@/utility/select-file.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { ensureSignin } from '@/i.js'; import { ensureSignin } from '@/i.js';
@ -49,7 +49,7 @@ const description = ref($i.description ?? '');
watch(name, () => { watch(name, () => {
os.apiWithDialog('i/update', { os.apiWithDialog('i/update', {
// null??使 // null??使
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
name: name.value || null, name: name.value || null,
}, undefined, { }, undefined, {
'0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': { '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': {
@ -62,13 +62,13 @@ watch(name, () => {
watch(description, () => { watch(description, () => {
os.apiWithDialog('i/update', { os.apiWithDialog('i/update', {
// null??使 // null??使
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
description: description.value || null, description: description.value || null,
}); });
}); });
function setAvatar(ev) { function setAvatar(ev) {
chooseFileFromPc(false).then(async (files) => { os.chooseFileFromPc({ multiple: false }).then(async (files) => {
const file = files[0]; const file = files[0];
let originalOrCropped = file; let originalOrCropped = file;
@ -81,13 +81,15 @@ function setAvatar(ev) {
}); });
if (!canceled) { if (!canceled) {
originalOrCropped = await os.cropImage(file, { originalOrCropped = await os.cropImageFile(file, {
aspectRatio: 1, aspectRatio: 1,
}); });
} }
const driveFile = (await os.launchUploader([originalOrCropped], {}))[0];
const i = await os.apiWithDialog('i/update', { const i = await os.apiWithDialog('i/update', {
avatarId: originalOrCropped.id, avatarId: driveFile.id,
}); });
$i.avatarId = i.avatarId; $i.avatarId = i.avatarId;
$i.avatarUrl = i.avatarUrl; $i.avatarUrl = i.avatarUrl;

View File

@ -8,6 +8,7 @@
import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue'; import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue';
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { getProxiedImageUrl } from './utility/media-proxy.js';
import type { Component, Ref } from 'vue'; import type { Component, Ref } from 'vue';
import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/utility/form.js'; import type { Form, GetFormResultType } from '@/utility/form.js';
@ -653,15 +654,13 @@ export async function pickEmoji(src: HTMLElement, opts: ComponentProps<typeof Mk
}); });
} }
export async function cropImage(image: Misskey.entities.DriveFile, options: { export async function cropImageFile(imageFile: File, options: {
aspectRatio: number; aspectRatio: number;
uploadFolder?: string | null; }): Promise<File> {
}): Promise<Misskey.entities.DriveFile> {
return new Promise(resolve => { return new Promise(resolve => {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), {
file: image, imageFile: imageFile,
aspectRatio: options.aspectRatio, aspectRatio: options.aspectRatio,
uploadFolder: options.uploadFolder,
}, { }, {
ok: x => { ok: x => {
resolve(x); resolve(x);
@ -671,6 +670,29 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: {
}); });
} }
export async function createCroppedImageDriveFileFromImageDriveFile(imageDriveFile: Misskey.entities.DriveFile, options: {
aspectRatio: number;
uploadFolder?: string | null;
}): Promise<Misskey.entities.DriveFile> {
return new Promise(resolve => {
const imgUrl = getProxiedImageUrl(imageDriveFile.url, undefined, true);
const image = new Image();
image.src = imgUrl;
image.onload = () => {
const canvas = window.document.createElement('canvas');
const ctx = canvas.getContext('2d')!;
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
const imageFile = new File([canvas.toBlob()], imageDriveFile.name, { type: imageDriveFile.type });
cropImageFile(imageFile).then(croppedImageFile => {
});
};
});
}
export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | null, options?: { export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | null, options?: {
align?: string; align?: string;
width?: number; width?: number;
@ -774,6 +796,32 @@ export function checkExistence(fileData: ArrayBuffer): Promise<any> {
}); });
}*/ }*/
export function chooseFileFromPc(
options: {
multiple?: boolean;
} = {},
): Promise<File[]> {
return new Promise((res, rej) => {
const input = window.document.createElement('input');
input.type = 'file';
input.multiple = options.multiple ?? false;
input.onchange = () => {
if (!input.files) return res([]);
res(Array.from(input.files));
// 一応廃棄
(window as any).__misskey_input_ref__ = null;
};
// https://qiita.com/fukasawah/items/b9dc732d95d99551013d
// iOS Safari で正常に動かす為のおまじない
(window as any).__misskey_input_ref__ = input;
input.click();
});
}
export function launchUploader( export function launchUploader(
files: File[], files: File[],
options?: { options?: {
@ -781,6 +829,7 @@ export function launchUploader(
}, },
): Promise<Misskey.entities.DriveFile[]> { ): Promise<Misskey.entities.DriveFile[]> {
return new Promise((res, rej) => { return new Promise((res, rej) => {
if (files.length === 0) return;
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUploaderDialog.vue')), { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUploaderDialog.vue')), {
files: markRaw(files), files: markRaw(files),
folderId: options?.folderId, folderId: options?.folderId,

View File

@ -94,7 +94,7 @@ import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue'; 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, chooseFileFromPcAndUpload } from '@/utility/select-file.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';
@ -364,7 +364,7 @@ async function onDrop(ev: DragEvent) {
} }
async function onFileSelectClicked() { async function onFileSelectClicked() {
const driveFiles = await chooseFileFromPc( const driveFiles = await chooseFileFromPcAndUpload(
true, true,
{ {
uploadFolder: selectedFolderId.value, uploadFolder: selectedFolderId.value,

View File

@ -130,7 +130,7 @@ function postThis() {
function crop() { function crop() {
if (!file.value) return; if (!file.value) return;
os.cropImage(file.value, { os.createCroppedImageDriveFileFromImageDriveFile(file.value, {
aspectRatio: NaN, aspectRatio: NaN,
uploadFolder: file.value.folderId ?? null, uploadFolder: file.value.folderId ?? null,
}); });

View File

@ -268,7 +268,7 @@ function changeAvatar(ev) {
}); });
if (!canceled) { if (!canceled) {
originalOrCropped = await os.cropImage(file, { originalOrCropped = await os.createCroppedImageDriveFileFromImageDriveFile(file, {
aspectRatio: 1, aspectRatio: 1,
}); });
} }
@ -294,7 +294,7 @@ function changeBanner(ev) {
}); });
if (!canceled) { if (!canceled) {
originalOrCropped = await os.cropImage(file, { originalOrCropped = await os.createCroppedImageDriveFileFromImageDriveFile(file, {
aspectRatio: 2, aspectRatio: 2,
}); });
} }

View File

@ -116,7 +116,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
menuItems.push({ menuItems.push({
text: i18n.ts.cropImage, text: i18n.ts.cropImage,
icon: 'ti ti-crop', icon: 'ti ti-crop',
action: () => os.cropImage(file, { action: () => os.createCroppedImageDriveFileFromImageDriveFile(file, {
aspectRatio: NaN, aspectRatio: NaN,
uploadFolder: folder ? folder.id : folder, uploadFolder: folder ? folder.id : folder,
}), }),

View File

@ -11,34 +11,21 @@ import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
export function chooseFileFromPc( export function chooseFileFromPcAndUpload(
options: { options: {
multiple?: boolean; multiple?: boolean;
folderId?: string | null; folderId?: string | null;
} = {}, } = {},
): Promise<Misskey.entities.DriveFile[]> { ): Promise<Misskey.entities.DriveFile[]> {
return new Promise((res, rej) => { return new Promise((res, rej) => {
const input = window.document.createElement('input'); os.chooseFileFromPc({ multiple: options.multiple }).then(files => {
input.type = 'file'; if (files.length === 0) return;
input.multiple = options.multiple ?? false; os.launchUploader(files, {
input.onchange = () => {
if (!input.files) return res([]);
os.launchUploader(Array.from(input.files), {
folderId: options.folderId, folderId: options.folderId,
}).then(driveFiles => { }).then(driveFiles => {
res(driveFiles); res(driveFiles);
}); });
});
// 一応廃棄
(window as any).__misskey_input_ref__ = null;
};
// https://qiita.com/fukasawah/items/b9dc732d95d99551013d
// iOS Safari で正常に動かす為のおまじない
(window as any).__misskey_input_ref__ = input;
input.click();
}); });
} }
@ -91,7 +78,7 @@ function select(src: HTMLElement | EventTarget | null, label: string | null, mul
} : undefined, { } : undefined, {
text: i18n.ts.upload, text: i18n.ts.upload,
icon: 'ti ti-upload', icon: 'ti ti-upload',
action: () => chooseFileFromPc(multiple, { keepOriginal: true }).then(files => res(files)), action: () => chooseFileFromPcAndUpload({ multiple }).then(files => res(files)),
}, { }, {
text: i18n.ts.fromDrive, text: i18n.ts.fromDrive,
icon: 'ti ti-cloud', icon: 'ti ti-cloud',