import { type FileTransferTabPane, type Shortcut } from '@/store/useGlobalStore' import { useMouseInElement } from '@vueuse/core' import { ref } from 'vue' import { genInfoCompleted, getImageGenerationInfo, openFolder, openWithDefaultApp, setImgPath } from '@/api' import { useWatchDocument} from 'vue3-ts-util' import { createReactiveQueue, isImageFile, copy2clipboardI18n} from '@/util' import { type FileNodeInfo, deleteFiles, moveFiles } from '@/api/files' import { last, range, uniqueId } from 'lodash-es' import * as Path from '@/util/path' import { Checkbox, Modal, message } from 'ant-design-vue' import type { MenuInfo } from 'ant-design-vue/lib/menu/src/interface' import { t } from '@/i18n' import { batchUpdateImageTag, toggleCustomTagToImg } from '@/api/db' import { downloadFileInfoJSON, downloadFiles, toRawFileUrl } from '@/util/file' import { getShortcutStrFromEvent } from '@/util/shortcut' import { MultiSelectTips, openRenameFileModal } from '@/components/functionalCallableComp' import { batchDownload, events, imgTransferBus, stackCache, tagStore, useEventListen, useHookShareState, global } from '.' export function useFileItemActions ( { openNext }: { openNext: (file: FileNodeInfo) => Promise } ) { const showGenInfo = ref(false) const imageGenInfo = ref('') const { sortedFiles, previewIdx, multiSelectedIdxs, stack, currLocation, spinning, previewing, stackViewEl, eventEmitter, props, deletedFiles } = useHookShareState().toRefs() const nor = Path.normalize useEventListen('removeFiles', ({ paths, loc }) => { if (nor(loc) !== nor(currLocation.value)) { return } const top = last(stack.value) if (!top) { return } paths.forEach(path => deletedFiles.value.add(path)) paths.filter(isImageFile).forEach(path => deletedFiles.value.add(path.replace(/\.\w+$/, '.txt'))) }) useEventListen('addFiles', ({ files, loc }) => { if (nor(loc) !== nor(currLocation.value)) { return } const top = last(stack.value) if (!top) { return } top.files.unshift(...files) }) const q = createReactiveQueue() const onFileItemClick = async (e: MouseEvent, file: FileNodeInfo, idx: number) => { previewIdx.value = idx global.fullscreenPreviewInitialUrl = toRawFileUrl(file) const idxInSelected = multiSelectedIdxs.value.indexOf(idx) if (e.shiftKey) { if (idxInSelected !== -1) { multiSelectedIdxs.value.splice(idxInSelected, 1) } else { multiSelectedIdxs.value.push(idx) multiSelectedIdxs.value.sort((a, b) => a - b) const first = multiSelectedIdxs.value[0] const last = multiSelectedIdxs.value[multiSelectedIdxs.value.length - 1] multiSelectedIdxs.value = range(first, last + 1) } e.stopPropagation() } else if (e.ctrlKey || e.metaKey) { if (idxInSelected !== -1) { multiSelectedIdxs.value.splice(idxInSelected, 1) } else { multiSelectedIdxs.value.push(idx) } e.stopPropagation() } else { await openNext(file) } } const onContextMenuClick = async (e: MenuInfo, file: FileNodeInfo, idx: number) => { const url = toRawFileUrl(file) const path = currLocation.value const preset = { IIB_container_id: parent.IIB_container_id } /** * 获取选中的图片信息 * 选中的图片信息数组 */ const getSelectedImg = () => { let selectedFiles: FileNodeInfo[] = [] if (multiSelectedIdxs.value.includes(idx)) { // 如果索引已被选中,则获取所有已选中的图片信息 selectedFiles = multiSelectedIdxs.value.map((idx) => sortedFiles.value[idx]) } else { // 否则,只获取当前图片信息 selectedFiles.push(file) } return selectedFiles } const copyImgTo = async (tab: ['txt2img', 'img2img', 'inpaint', 'extras'][number]) => { if (spinning.value) { return } try { spinning.value = true await setImgPath(file.fullpath) // 设置图像路径 imgTransferBus.postMessage({ ...preset, event: 'click_hidden_button', btnEleId: 'iib_hidden_img_update_trigger' }) // 触发图像组件更新 // ok(await genInfoCompleted(), 'genInfoCompleted timeout') // 等待消息生成完成 await genInfoCompleted() // 等待消息生成完成 imgTransferBus.postMessage({ ...preset, event: 'click_hidden_button', btnEleId: `iib_hidden_tab_${tab}` }) // 触发粘贴 } catch (error) { console.error(error) message.error('发送图像失败,请携带console的错误消息找开发者') } finally { spinning.value = false } } const key = `${e.key}` if (key.startsWith('toggle-tag-')) { const tagId = +key.split('toggle-tag-')[1] const { is_remove } = await toggleCustomTagToImg({ tag_id: tagId, img_path: file.fullpath }) const tag = global.conf?.all_custom_tags.find((v) => v.id === tagId)?.name! await tagStore.refreshTags([file.fullpath]) message.success(t(is_remove ? 'removedTagFromImage' : 'addedTagToImage', { tag })) return } else if (key.startsWith('batch-add-tag-') || key.startsWith('batch-remove-tag-')) { const tagId = +key.split('-tag-')[1] const action = key.includes('add') ? 'add' : 'remove' const paths = getSelectedImg().map(v => v.fullpath) await batchUpdateImageTag({ tag_id: tagId, img_paths: paths, action }) await tagStore.refreshTags(paths) message.success(t(action === 'add' ? 'addCompleted' : 'removeCompleted')) return } switch (e.key) { case 'previewInNewWindow': return window.open(url) case 'saveSelectedAsJson': return downloadFileInfoJSON(getSelectedImg()) case 'openWithDefaultApp': return openWithDefaultApp(file.fullpath) case 'download':{ const selectedFiles = getSelectedImg() downloadFiles(selectedFiles.map(file => toRawFileUrl(file, true))) break } case 'copyPreviewUrl': { return copy2clipboardI18n(parent.document.location.origin + url) } case 'rename': { let newPath = await openRenameFileModal(file.fullpath) newPath = Path.normalize(newPath) const map = tagStore.tagMap map.set(newPath, map.get(file.fullpath) ?? []) map.delete(file.fullpath) file.fullpath = newPath file.name = newPath.split(/[\\/]/).pop() ?? '' return } case 'send2txt2img': return copyImgTo('txt2img') case 'send2img2img': return copyImgTo('img2img') case 'send2inpaint': return copyImgTo('inpaint') case 'send2extras': return copyImgTo('extras') case 'send2savedDir': { const dir = global.quickMovePaths.find((v) => v.key === 'outdir_save') if (!dir) { return message.error(t('unknownSavedDir')) } const absolutePath = Path.normalizeRelativePathToAbsolute(dir.dir, global.conf?.sd_cwd!) const selectedImg = getSelectedImg() await moveFiles( selectedImg.map((v) => v.fullpath), absolutePath, true ) events.emit('removeFiles', { paths: selectedImg.map((v) => v.fullpath), loc: currLocation.value }) events.emit('addFiles', { files: selectedImg, loc: absolutePath }) break } case 'send2controlnet-img2img': case 'send2controlnet-txt2img': { const type = e.key.split('-')[1] as 'img2img' | 'txt2img' imgTransferBus.postMessage({ ...preset, event: 'send_to_control_net', type, url: toRawFileUrl(file) }) break } case 'send2outpaint': { imageGenInfo.value = await q.pushAction(() => getImageGenerationInfo(file.fullpath)).res const [prompt, negPrompt] = (imageGenInfo.value || '').split('\n') imgTransferBus.postMessage({ ...preset, event: 'send_to_outpaint', url: toRawFileUrl(file), prompt, negPrompt: negPrompt.slice('Negative prompt: '.length) }) break } case 'openWithWalkMode': { stackCache.set(path, stack.value) const tab = global.tabList[props.value.tabIdx] const pane: FileTransferTabPane = { type: 'local', key: uniqueId(), path: file.fullpath, name: t('local'), stackKey: path, mode: 'walk' } tab.panes.push(pane) tab.key = pane.key break } case 'openInNewTab': { stackCache.set(path, stack.value) const tab = global.tabList[props.value.tabIdx] const pane: FileTransferTabPane = { type: 'local', key: uniqueId(), path: file.fullpath, name: t('local'), stackKey: path } tab.panes.push(pane) tab.key = pane.key break } case 'openOnTheRight': { stackCache.set(path, stack.value) let tab = global.tabList[props.value.tabIdx + 1] if (!tab) { tab = { panes: [], key: '', id: uniqueId() } global.tabList[props.value.tabIdx + 1] = tab } const pane: FileTransferTabPane = { type: 'local', key: uniqueId(), path: file.fullpath, name: t('local'), stackKey: path } tab.panes.push(pane) tab.key = pane.key break } case 'send2BatchDownload': { batchDownload.addFiles(getSelectedImg()) break } case 'viewGenInfo': { showGenInfo.value = true imageGenInfo.value = await q.pushAction(() => getImageGenerationInfo(file.fullpath)).res break } case 'openWithLocalFileBrowser': { await openFolder(file.fullpath) break } case 'deleteFiles': { const selectedFiles = getSelectedImg() const removeFile = async () => { const paths = selectedFiles.map((v) => v.fullpath) await deleteFiles(paths) message.success(t('deleteSuccess')) events.emit('removeFiles', { paths: paths, loc: currLocation.value }) } if (selectedFiles.length === 1 && global.ignoredConfirmActions.deleteOneOnly) { return removeFile() } await new Promise((resolve) => { Modal.confirm({ title: t('confirmDelete'), maskClosable: true, width: '60vw', content:() =>
    {selectedFiles.map((v) =>
  1. {v.fullpath.split(/[/\\]/).pop()}
  2. )}
{t('deleteOneOnlySkipConfirm')} ({t('resetOnGlobalSettingsPage')})
, async onOk () { await removeFile() resolve() } }) }) break } } return {} } const { isOutside } = useMouseInElement(stackViewEl) useWatchDocument('keydown', (e) => { const keysStr = getShortcutStrFromEvent(e) if (previewing.value) { const action = Object.entries(global.shortcut).find( (v) => v[1] === keysStr && v[1] )?.[0] as keyof Shortcut if (action) { e.stopPropagation() e.preventDefault() const idx = previewIdx.value const file = sortedFiles.value[idx] switch (action) { case 'delete': { if (toRawFileUrl(file) === global.fullscreenPreviewInitialUrl) { return message.warn(t('fullscreenRestriction')) } return onContextMenuClick({ key: 'deleteFiles' } as MenuInfo, file, idx) } case 'download': { return onContextMenuClick({ key: 'download' } as MenuInfo, file, idx) } default: { const name = /^toggle_tag_(.*)$/.exec(action)?.[1] const tag = global.conf?.all_custom_tags.find((v) => v.name === name) if (!tag) return return onContextMenuClick({ key: `toggle-tag-${tag.id}` } as MenuInfo, file, idx) } } } } else if (!isOutside.value && ['Ctrl + KeyA', 'Cmd + KeyA'].includes(keysStr)) { e.preventDefault() e.stopPropagation() eventEmitter.value.emit('selectAll') } }) return { onFileItemClick, onContextMenuClick, showGenInfo, imageGenInfo, q } }