sd-webui-infinite-image-bro.../vue/src/page/fileTransfer/hooks/useFileItemActions.tsx

402 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 {
delay,
useWatchDocument} from 'vue3-ts-util'
import {
createReactiveQueue,
isImageFile,
copy2clipboardI18n} from '@/util'
import { type FileNodeInfo, deleteFiles, moveFiles, copyFiles } 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 '.'
import { closeImageFullscreenPreview, openImageFullscreenPreview } from '@/util/imagePreviewOperation'
export function useFileItemActions (
{ openNext }: { openNext: (file: FileNodeInfo) => Promise<void> }
) {
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
} else if (key.startsWith('copy-to-')){
const targetPath = key.split('copy-to-')[1]
const selectedFiles = getSelectedImg()
const paths = selectedFiles.map((v) => v.fullpath)
await copyFiles(paths, targetPath, true)
events.emit('addFiles', { files: selectedFiles, loc: targetPath })
message.success(t('copySuccess'))
return
} else if (key.startsWith('move-to-')){
const targetPath = key.split('move-to-')[1]
const selectedFiles = getSelectedImg()
const paths = selectedFiles.map((v) => v.fullpath)
await moveFiles(paths, targetPath, true)
events.emit('removeFiles', { paths, loc: currLocation.value })
events.emit('addFiles', { files: selectedFiles, loc: targetPath })
message.success(t('moveSuccess'))
return
}
switch (e.key) {
case 'previewInNewWindow':
return window.open(url)
case 'copyFilePath':
return copy2clipboardI18n(file.fullpath)
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 'openFileLocationInNewTab':
case 'openInNewTab': {
const tab = global.tabList[props.value.tabIdx]
const pane: FileTransferTabPane = {
type: 'local',
key: uniqueId(),
path: e.key === 'openInNewTab' ? file.fullpath : Path.getParentDirectory(file.fullpath),
name: t('local'),
mode: 'scanned-fixed'
}
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'))
if (previewing.value) {
const isFullscreenFirst = toRawFileUrl(file) === global.fullscreenPreviewInitialUrl
const isEnd = previewIdx.value === sortedFiles.value.length - 1
if (isFullscreenFirst || isEnd) {
closeImageFullscreenPreview()
await delay(100)
if (isFullscreenFirst && sortedFiles.value.length > 1) {
const nextIdx = previewIdx.value
delay(0).then(() => openImageFullscreenPreview(nextIdx, stackViewEl.value!))
}
}
}
events.emit('removeFiles', { paths: paths, loc: currLocation.value })
}
if (selectedFiles.length === 1 && global.ignoredConfirmActions.deleteOneOnly) {
return removeFile()
}
await new Promise<void>((resolve) => {
Modal.confirm({
title: t('confirmDelete'),
maskClosable: true,
width: '60vw',
content:() =>
<div>
<ol style={{ maxHeight: '50vh', overflow: 'auto' }}>
{selectedFiles.map((v) => <li>{v.fullpath.split(/[/\\]/).pop()}</li>)}
</ol>
<MultiSelectTips />
<Checkbox v-model:checked={global.ignoredConfirmActions.deleteOneOnly}>{t('deleteOneOnlySkipConfirm')} ({t('resetOnGlobalSettingsPage')})</Checkbox>
</div>,
async onOk () {
await removeFile()
resolve()
}
})
})
break
}
}
return {}
}
const { isOutside } = useMouseInElement(stackViewEl)
useWatchDocument('keydown', (e) => {
const keysStr = getShortcutStrFromEvent(e)
if (previewing.value) {
if (keysStr === 'Esc') {
closeImageFullscreenPreview()
}
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': {
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
}
}