mirror of
https://github.com/ZiuChen/ClipboardManager.git
synced 2025-06-08 14:24:03 +08:00
feat: 添加多选功能 支持合并文本/图片/文件
This commit is contained in:
parent
9b172f684c
commit
cfe567b9fb
@ -15,7 +15,8 @@ const dbName = '_utools_clipboard_manager_storage'
|
|||||||
|
|
||||||
const isMacOs = utools.isMacOs()
|
const isMacOs = utools.isMacOs()
|
||||||
const isWindows = utools.isWindows()
|
const isWindows = utools.isWindows()
|
||||||
const DBPath = `${isMacOs ? userDataPath : homePath}${isWindows ? '\\' : '/'}${dbName}`
|
const sep = isWindows ? '\\' : '/'
|
||||||
|
const DBPath = `${isMacOs ? userDataPath : homePath}${sep}${dbName}`
|
||||||
|
|
||||||
let globalImageOversize = false
|
let globalImageOversize = false
|
||||||
|
|
||||||
@ -45,7 +46,7 @@ class DB {
|
|||||||
)
|
)
|
||||||
this.updateDataBaseLocal()
|
this.updateDataBaseLocal()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
utools.showNotification('读取剪切板出错' + err)
|
utools.showNotification('读取剪切板出错: ' + err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@ -66,7 +67,7 @@ class DB {
|
|||||||
// 更新文件数据
|
// 更新文件数据
|
||||||
fs.writeFileSync(this.path, JSON.stringify(dataBase || this.dataBase), (err) => {
|
fs.writeFileSync(this.path, JSON.stringify(dataBase || this.dataBase), (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
utools.showNotification('写入剪切板出错' + err)
|
utools.showNotification('写入剪切板出错: ' + err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -186,6 +187,30 @@ const paste = () => {
|
|||||||
else utools.simulateKeyboardTap('v', 'ctrl')
|
else utools.simulateKeyboardTap('v', 'ctrl')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createFile = (item) => {
|
||||||
|
const tempPath = utools.getPath('temp')
|
||||||
|
const folderPath = tempPath + sep + 'utools-clipboard-manager'
|
||||||
|
if (!fs.existsSync(folderPath)) {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(folderPath)
|
||||||
|
} catch (err) {
|
||||||
|
utools.showNotification('创建临时文件夹出错: ' + err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { type } = item
|
||||||
|
if (type === 'image') {
|
||||||
|
const base64Data = item.data.replace(/^data:image\/\w+;base64,/, '') // remove the prefix
|
||||||
|
const buffer = Buffer.from(base64Data, 'base64') // to Buffer
|
||||||
|
const filePath = folderPath + sep + new Date().valueOf() + '.png'
|
||||||
|
fs.writeFileSync(filePath, buffer)
|
||||||
|
return filePath
|
||||||
|
} else if (type === 'text') {
|
||||||
|
const filePath = folderPath + sep + new Date().valueOf() + '.txt'
|
||||||
|
fs.writeFileSync(filePath, item.data)
|
||||||
|
return filePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const db = new DB(DBPath)
|
const db = new DB(DBPath)
|
||||||
db.init()
|
db.init()
|
||||||
|
|
||||||
@ -231,6 +256,7 @@ window.db = db
|
|||||||
window.copy = copy
|
window.copy = copy
|
||||||
window.paste = paste
|
window.paste = paste
|
||||||
window.remove = remove
|
window.remove = remove
|
||||||
|
window.createFile = createFile
|
||||||
window.openFile = utools.shellOpenPath
|
window.openFile = utools.shellOpenPath
|
||||||
window.openFileFolder = utools.shellShowItemInFolder
|
window.openFileFolder = utools.shellShowItemInFolder
|
||||||
window.getIcon = utools.getFileIcon
|
window.getIcon = utools.getFileIcon
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
@click.left="handleItemClick($event, item)"
|
@click.left="handleItemClick($event, item)"
|
||||||
@click.right="handleItemClick($event, item)"
|
@click.right="handleItemClick($event, item)"
|
||||||
@mouseover="handleMouseOver(index)"
|
@mouseover="handleMouseOver(index)"
|
||||||
:class="{ active: index === activeIndex }"
|
:class="{ active: index === activeIndex, select: selectItemList.indexOf(item) !== -1 }"
|
||||||
>
|
>
|
||||||
<div class="clip-info">
|
<div class="clip-info">
|
||||||
<div class="clip-time">
|
<div class="clip-time">
|
||||||
@ -29,7 +29,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="clip-operate" v-show="activeIndex === index">
|
<div class="clip-operate" v-show="activeIndex === index && !isMultiple">
|
||||||
<template v-for="{ id, title, icon } of operation">
|
<template v-for="{ id, title, icon } of operation">
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
@ -47,7 +47,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="clip-count" v-show="activeIndex !== index">
|
<div class="clip-count" v-show="isMultiple || activeIndex !== index">
|
||||||
{{ index + 1 }}
|
{{ index + 1 }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -66,9 +66,13 @@ const props = defineProps({
|
|||||||
fullData: {
|
fullData: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
isMultiple: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['onDataChange', 'onDataRemove'])
|
const emit = defineEmits(['onDataChange', 'onDataRemove', 'onSelectItemAdd'])
|
||||||
const isOverSizedContent = (item) => {
|
const isOverSizedContent = (item) => {
|
||||||
const { type, data } = item
|
const { type, data } = item
|
||||||
if (type === 'text') {
|
if (type === 'text') {
|
||||||
@ -77,18 +81,42 @@ const isOverSizedContent = (item) => {
|
|||||||
return JSON.parse(item.data).length >= 6
|
return JSON.parse(item.data).length >= 6
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const selectItemList = ref([])
|
||||||
|
const emptySelectItemList = () => (selectItemList.value = [])
|
||||||
|
defineExpose({
|
||||||
|
selectItemList, // 暴露给 Main/Switch中的操作按钮以执行复制
|
||||||
|
emptySelectItemList
|
||||||
|
})
|
||||||
|
watch(
|
||||||
|
() => props.isMultiple,
|
||||||
|
(val) => {
|
||||||
|
if (!val) {
|
||||||
|
emptySelectItemList() // 退出多选状态 清空列表
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
const handleItemClick = (ev, item) => {
|
const handleItemClick = (ev, item) => {
|
||||||
const { button } = ev
|
if (props.isMultiple === true) {
|
||||||
if (button === 0) {
|
const index = selectItemList.value.indexOf(item)
|
||||||
// 左键 复制后粘贴
|
console.log(index)
|
||||||
window.copy(item)
|
if (index !== -1) {
|
||||||
window.paste()
|
selectItemList.value.splice(index, 1) // 已经存在 点击移除
|
||||||
} else if (button === 2) {
|
} else {
|
||||||
// 右键 仅复制
|
selectItemList.value.push(item) // 添加到已选列表中
|
||||||
window.copy(item)
|
}
|
||||||
|
emit('onSelectItemAdd')
|
||||||
|
} else {
|
||||||
|
const { button } = ev
|
||||||
|
if (button === 0) {
|
||||||
|
// 左键 复制后粘贴
|
||||||
|
window.copy(item)
|
||||||
|
window.paste()
|
||||||
|
} else if (button === 2) {
|
||||||
|
// 右键 仅复制
|
||||||
|
window.copy(item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const handleContentExpand = (item) => emit('onDataChange', item)
|
|
||||||
const activeIndex = ref(0)
|
const activeIndex = ref(0)
|
||||||
const handleMouseOver = (index) => (activeIndex.value = index)
|
const handleMouseOver = (index) => (activeIndex.value = index)
|
||||||
const operation = [
|
const operation = [
|
||||||
@ -127,10 +155,7 @@ const handleOperateClick = ({ id, item }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 父组件中改变了引用类型的地址 故要用 getter返回
|
// 父组件中改变了引用类型的地址 故要用 getter返回
|
||||||
watch(
|
watch(props.showList, () => (activeIndex.value = 0))
|
||||||
() => props.showList,
|
|
||||||
() => (activeIndex.value = 0)
|
|
||||||
)
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 监听键盘事件
|
// 监听键盘事件
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
|
@ -16,6 +16,9 @@
|
|||||||
background-color: @text-bg-color-lighter;
|
background-color: @text-bg-color-lighter;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
}
|
}
|
||||||
|
&.select {
|
||||||
|
background-color: @text-bg-color-lighter;
|
||||||
|
}
|
||||||
.clip-info {
|
.clip-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -34,10 +34,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.clip-switch-btn {
|
.clip-switch-btn-list {
|
||||||
width: 25px;
|
margin-right: 5px;
|
||||||
height: 25px;
|
.clip-switch-btn {
|
||||||
padding: 10px;
|
width: 25px;
|
||||||
cursor: pointer;
|
height: 25px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 5px 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: @text-color;
|
||||||
|
background-color: @nav-bg-color;
|
||||||
|
&:hover {
|
||||||
|
background-color: @nav-hover-bg-color;
|
||||||
|
transition: all 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
&.clip-select-count {
|
||||||
|
background-color: @primary-color;
|
||||||
|
color: @bg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,26 @@
|
|||||||
></ClipFullData>
|
></ClipFullData>
|
||||||
<ClipSwitch ref="ClipSwitchRef" @onNavClick="handleNavClick">
|
<ClipSwitch ref="ClipSwitchRef" @onNavClick="handleNavClick">
|
||||||
<template #SidePanel>
|
<template #SidePanel>
|
||||||
<div v-show="!isSearchPanelExpand">
|
<div class="clip-switch-btn-list" v-show="!isSearchPanelExpand">
|
||||||
<span class="clip-switch-btn"> 👆 </span>
|
<span class="clip-switch-btn clip-select-count" v-show="isMultiple">
|
||||||
<span class="clip-switch-btn clip-search-btn" @click="handleSearchBtnClick"> 🔍 </span>
|
{{ selectCount }}
|
||||||
|
</span>
|
||||||
|
<span class="clip-switch-btn" v-show="isMultiple" @click="handleMultiCopyBtnClick(false)"
|
||||||
|
>📄 复制</span
|
||||||
|
>
|
||||||
|
<span class="clip-switch-btn" v-show="isMultiple" @click="handleMultiCopyBtnClick(true)"
|
||||||
|
>📑 粘贴</span
|
||||||
|
>
|
||||||
|
<span class="clip-switch-btn" @click="handleMultiBtnClick">{{
|
||||||
|
isMultiple ? '❌ 退出多选' : '👆'
|
||||||
|
}}</span>
|
||||||
|
<span
|
||||||
|
class="clip-switch-btn clip-search-btn"
|
||||||
|
v-show="!isMultiple"
|
||||||
|
@click="handleSearchBtnClick"
|
||||||
|
>
|
||||||
|
🔍
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ClipSearch
|
<ClipSearch
|
||||||
v-show="isSearchPanelExpand"
|
v-show="isSearchPanelExpand"
|
||||||
@ -23,8 +40,11 @@
|
|||||||
<div class="clip-break"></div>
|
<div class="clip-break"></div>
|
||||||
<div class="clip-empty-status" v-if="showList.length === 0">📪 无记录</div>
|
<div class="clip-empty-status" v-if="showList.length === 0">📪 无记录</div>
|
||||||
<ClipItemList
|
<ClipItemList
|
||||||
|
ref="ClipItemListRef"
|
||||||
:showList="showList"
|
:showList="showList"
|
||||||
:fullData="fullData"
|
:fullData="fullData"
|
||||||
|
:isMultiple="isMultiple"
|
||||||
|
@onSelectItemAdd="handleSelectItemAdd"
|
||||||
@onDataChange="toggleFullData"
|
@onDataChange="toggleFullData"
|
||||||
@onDataRemove="handleDataRemove"
|
@onDataRemove="handleDataRemove"
|
||||||
>
|
>
|
||||||
@ -42,13 +62,66 @@ import ClipFloatBtn from '../cpns/ClipFloatBtn.vue'
|
|||||||
|
|
||||||
const isMultiple = ref(false)
|
const isMultiple = ref(false)
|
||||||
|
|
||||||
|
const handleMultiBtnClick = () => {
|
||||||
|
isMultiple.value = !isMultiple.value
|
||||||
|
}
|
||||||
|
|
||||||
const isSearchPanelExpand = ref(false)
|
const isSearchPanelExpand = ref(false)
|
||||||
|
|
||||||
const handleSearchBtnClick = () => {
|
const handleSearchBtnClick = () => {
|
||||||
|
// 展开搜索框
|
||||||
isSearchPanelExpand.value = true
|
isSearchPanelExpand.value = true
|
||||||
nextTick(() => window.focus())
|
nextTick(() => window.focus())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ClipItemListRef = ref(null)
|
||||||
|
const selectCount = ref(0)
|
||||||
|
|
||||||
|
const handleSelectItemAdd = () => {
|
||||||
|
// 每次添加选择的 item都将 count更新
|
||||||
|
selectCount.value = ClipItemListRef.value.selectItemList.length
|
||||||
|
}
|
||||||
|
const handleMultiCopyBtnClick = (isPaste) => {
|
||||||
|
const itemList = ClipItemListRef.value.selectItemList
|
||||||
|
// 如果包含了图片/文件 则转为文件合并 否则仅合并文本
|
||||||
|
const isMergeFile =
|
||||||
|
itemList.filter((item) => item.type === 'image' || item.type === 'file').length !== 0
|
||||||
|
if (isMergeFile) {
|
||||||
|
const filePathArray = []
|
||||||
|
itemList.map((item) => {
|
||||||
|
const { type } = item
|
||||||
|
if (type === 'text') {
|
||||||
|
const textFile = window.createFile(item)
|
||||||
|
filePathArray.push({
|
||||||
|
path: textFile
|
||||||
|
})
|
||||||
|
} else if (type === 'image') {
|
||||||
|
const imageFile = window.createFile(item)
|
||||||
|
filePathArray.push({
|
||||||
|
path: imageFile
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// file
|
||||||
|
const files = JSON.parse(item.data)
|
||||||
|
filePathArray.push(...files)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.copy({
|
||||||
|
type: 'file',
|
||||||
|
data: JSON.stringify(filePathArray)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const result = itemList.map((item) => item.data).join('\n')
|
||||||
|
window.copy({
|
||||||
|
type: 'text',
|
||||||
|
data: result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isPaste && window.paste()
|
||||||
|
ClipItemListRef.value.emptySelectItemList()
|
||||||
|
isMultiple.value = false
|
||||||
|
}
|
||||||
|
|
||||||
const GAP = 15 // 懒加载 每次添加的条数
|
const GAP = 15 // 懒加载 每次添加的条数
|
||||||
const offset = ref(0) // 懒加载 偏移量
|
const offset = ref(0) // 懒加载 偏移量
|
||||||
const filterText = ref('') // 搜索框绑定值
|
const filterText = ref('') // 搜索框绑定值
|
||||||
|
Loading…
x
Reference in New Issue
Block a user