420 lines
15 KiB
TypeScript
420 lines
15 KiB
TypeScript
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, openAddNewTagModal, openRenameFileModal } from '@/components/functionalCallableComp'
|
||
import { batchDownload, events, imgTransferBus, stackCache, tagStore, useEventListen, useHookShareState, global } from '.'
|
||
import { closeImageFullscreenPreview, openImageFullscreenPreview } from '@/util/imagePreviewOperation'
|
||
import { openTiktokViewWithFiles } from '@/util/tiktokHelper'
|
||
|
||
|
||
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 === 'add-custom-tag') {
|
||
openAddNewTagModal()
|
||
} 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 'tiktokView': {
|
||
// 打开 TikTok 式观看,从当前文件开始
|
||
openTiktokViewWithFiles(sortedFiles.value, idx)
|
||
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 onContextMenuClick({ key: `toggle-tag-${tag.id}` } as MenuInfo, file, idx)
|
||
}
|
||
if (action.startsWith('copy_to_')) {
|
||
const path = action.split('copy_to_')[1]
|
||
return onContextMenuClick({ key: `copy-to-${path}` } as MenuInfo, file, idx)
|
||
}
|
||
if (action.startsWith('move_to_')) {
|
||
const path = action.split('move_to_')[1]
|
||
return onContextMenuClick({ key: `move-to-${path}` } 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
|
||
}
|
||
} |