Merge pull request #34 from ZiuChen/v1.2.2

V1.2.2
This commit is contained in:
ZiuChen 2022-09-07 10:03:06 +08:00 committed by GitHub
commit c37c42ff94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 372 additions and 139 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -1,5 +1,5 @@
{
"version": "1.1.6",
"version": "1.2.2",
"pluginName": "超级剪贴板",
"description": "强大的剪贴板管理工具",
"author": "ZiuChen",

View File

@ -7,6 +7,7 @@
const fs = require('fs')
const crypto = require('crypto')
const { clipboard } = require('electron')
const time = require('./time')
const homePath = utools.getPath('home')
const userDataPath = utools.getPath('userData')
@ -36,10 +37,12 @@ class DB {
// 读取磁盘记录到内存
const dataBase = JSON.parse(data)
this.dataBase = dataBase
// 将超过14天的数据删除
// 将超过14天的数据删除 排除掉收藏
const now = new Date().getTime()
const deleteTime = now - '\u0031\u0034' * '\u0032\u0034' * 60 * 60 * 1000 // unicode
this.dataBase.data = this.dataBase.data.filter((item) => item.updateTime > deleteTime)
this.dataBase.data = this.dataBase.data?.filter(
(item) => item.updateTime > deleteTime || item.collect
)
this.updateDataBaseLocal()
} catch (err) {
utools.showNotification('读取剪切板出错' + err)
@ -71,14 +74,16 @@ class DB {
addItem(cItem) {
this.dataBase.data.unshift(cItem)
this.updateDataBase()
// unicode
if (this.dataBase.data.length > '\u0035\u0030\u0030') {
// 达到条数限制
this.dataBase.data.pop()
// 仍然大于: 超出了不止一条
if (this.dataBase.data.length > '\u0035\u0030\u0030') {
this.dataBase.data = this.dataBase.data.splice(0, 499)
const exceedCount = this.dataBase.data.length - '\u0035\u0030\u0030'
if (exceedCount > 0) {
// 达到条数限制 在收藏条数限制内遍历非收藏历史并删除
// 所有被移除的 item都存入tempList
const tmpList = []
for (let i = 0; i < exceedCount; i++) {
const item = this.dataBase.data.pop()
tmpList.push(item)
}
tmpList.forEach((item) => !item.collect || this.dataBase.data.push(item)) // 收藏内容 重新入栈
}
this.updateDataBaseLocal()
}
@ -118,36 +123,36 @@ class DB {
}
}
const pbpaste = async () => {
return new Promise((res) => {
const pbpaste = () => {
// file
const files = utools.getCopyedFiles() // null | Array
if (files) {
res({
return {
type: 'file',
data: JSON.stringify(files)
})
}
}
// text
const text = clipboard.readText()
if (text.trim()) res({ type: 'text', data: text })
if (text.trim()) return { type: 'text', data: text }
// image
const image = clipboard.readImage() // 大图卡顿来源
const data = image.toDataURL()
globalImageOversize = data.length > 4e5
if (!image.isEmpty()) {
res({
return {
type: 'image',
data: data
})
}
})
}
}
const watchClipboard = async (db, fn) => {
let prev = db.dataBase.data[0] || {}
setInterval(() => {
pbpaste().then((item) => {
function loop() {
time.sleep(250).then(loop)
const item = pbpaste()
if (!item) return
item.id = crypto.createHash('md5').update(item.data).digest('hex')
if (item && prev.id != item.id) {
// 剪切板元素 与最近一次复制内容不同
@ -156,11 +161,11 @@ const watchClipboard = async (db, fn) => {
} else {
// 剪切板元素 与上次复制内容相同
}
})
}, 250)
}
loop()
}
const copy = (item) => {
const copy = (item, isHideMainWindow = true) => {
switch (item.type) {
case 'text':
utools.copyText(item.data)
@ -173,7 +178,7 @@ const copy = (item) => {
utools.copyFile(paths)
break
}
utools.hideMainWindow()
isHideMainWindow && utools.hideMainWindow()
}
const paste = () => {
@ -186,7 +191,13 @@ db.init()
const remove = (item) => db.removeItemViaId(item.id)
const focus = () => document.querySelector('.clip-search input')?.focus()
const select = () => document.querySelector('.clip-search input').select()
const focus = () => {
document.querySelector('.clip-search-input').style.display !== 'none'
? document.querySelector('.clip-search-input')?.focus()
: (document.querySelector('.clip-search-btn')?.click(),
document.querySelector('.clip-search-input')?.focus())
}
const toTop = () => (document.scrollingElement.scrollTop = 0)
const resetNav = () => document.querySelectorAll('.clip-switch-item')[0]?.click()
@ -208,8 +219,8 @@ utools.onPluginEnter(() => {
utools.copyText('ImageOverSized')
globalImageOversize = false
}
document.querySelector('.clip-search input').select() // 进入插件将搜索框内容全选
focus()
select() // 进入插件将搜索框内容全选
toTop()
resetNav()
})
@ -219,6 +230,7 @@ window.copy = copy
window.paste = paste
window.remove = remove
window.openFile = utools.shellOpenPath
window.openFileFolder = utools.shellShowItemInFolder
window.getIcon = utools.getFileIcon
window.focus = focus
window.toTop = toTop

68
public/time.js Normal file
View File

@ -0,0 +1,68 @@
// author: inu1255
const path = require('path')
function newPromise(fn) {
let a, b
var tmp = {
resolve(x) {
if (this.pending) {
a(x)
this.resolved = true
this.pending = false
}
},
reject(e) {
if (this.pending) {
b(e)
this.rejectd = true
this.pending = false
}
},
pending: true,
resolved: false,
rejected: false
}
/** @type {Promise<T>} */
var pms = new Promise(function (resolve, reject) {
a = resolve
b = reject
if (fn) fn(tmp.resolve, tmp.reject)
})
return Object.assign(pms, tmp)
}
let cbIdx = 1
const cbMap = new Map()
function getWorker() {
if (getWorker.worker) return getWorker.worker
const worker = new Worker(path.join(__dirname, 'time.worker.js'))
getWorker.worker = worker
worker.onmessage = (e) => {
if (e.data && cbMap.has(e.data.cb)) {
cbMap.get(e.data.cb).apply(null, e.data.args)
}
}
return worker
}
function call(method, args) {
const cb = cbIdx++
let pms = newPromise()
cbMap.set(cb, function (err, data) {
if (err) pms.reject(err)
else pms.resolve(data)
})
getWorker().postMessage({
method,
args,
cb
})
return pms
}
function sleep(ms) {
return call('sleep', [ms])
}
exports.sleep = sleep

21
public/time.worker.js Normal file
View File

@ -0,0 +1,21 @@
// author: inu1255
const apis = {
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
}
onmessage = (event) => {
const data = event.data
if (!data) return
const { cb, method, args } = data
if (!apis[method]) {
postMessage({ cb, err: 'no such method' })
return
}
apis[method].apply(null, args).then(
(res) => postMessage({ cb, data: res }),
(err) => postMessage({ cb, err })
)
}

18
src/cpns/ClipFloatBtn.vue Normal file
View File

@ -0,0 +1,18 @@
<template>
<div class="clip-float-btn">
<div @click="restoreDataBase">🧭</div>
</div>
</template>
<script setup>
// const emit = defineEmits(['onBtnClick'])
// const handleBtnClick = () => emit('onBtnClick')
const restoreDataBase = () => {
//
const flag = window.confirm('确定要清空剪贴板记录吗?')
if (flag) {
window.db.emptyDataBase()
updateShowList('all')
}
}
</script>

View File

@ -1,12 +1,23 @@
<template>
<div class="clip-full-data">
<Transition name="fade">
<div class="clip-full" v-show="isShow">
<div v-if="fullData.type === 'text'">
<div v-text="fullData.data"></div>
<div class="clip-full-wrapper" v-show="isShow">
<div class="clip-full-operate-list">
<template v-for="{ id, name } of btns">
<div
class="clip-full-operate-list-item"
v-if="id !== 'word-split' || (id === 'word-split' && fullData.type !== 'file')"
@click="handleBtnClick(id)"
>
{{ name }}
</div>
<div v-else>
<FileList :data="fullData.data"></FileList>
</template>
</div>
<template v-if="fullData.type === 'text'">
<div class="clip-full-content" v-text="fullData.data"></div>
</template>
<div v-else class="clip-full-content">
<FileList :data="JSON.parse(fullData.data)"></FileList>
</div>
</div>
</Transition>
@ -32,6 +43,28 @@ const props = defineProps({
const emit = defineEmits(['onOverlayClick'])
const onOverlayClick = () => emit('onOverlayClick')
const btns = [
{
id: 'copy-all',
name: '📄 复制全部'
},
{
id: 'word-split',
name: '🎁 智慧分词'
}
]
const handleBtnClick = (id) => {
switch (id) {
case 'copy-all':
window.copy(props.fullData)
emit('onOverlayClick') // 退
break
case 'word-split':
window.alert('增值服务 Comming Soon...')
break
}
}
onMounted(() => {
document.addEventListener('keydown', (e) => {
const { key } = e
@ -48,10 +81,11 @@ onMounted(() => {
@import '../style';
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.15s ease;
transition: all 0.15s ease;
}
.fade-enter-from,
.fade-leave-to {
width: 0px;
opacity: 0;
}
</style>

View File

@ -14,7 +14,7 @@
<template v-if="item.type === 'text'">
<span
class="clip-data-status"
v-if="item.data.split(`\n`).length - 1 > 8"
v-if="item.data.split(`\n`).length - 1 > 7"
@click.stop="handleDataClick(item)"
>
查看全部
@ -27,7 +27,7 @@
<template v-if="item.type === 'file'">
<span
class="clip-data-status"
v-if="JSON.parse(item.data).length >= 8"
v-if="JSON.parse(item.data).length >= 7"
@click.stop="handleDataClick(item)"
>
查看全部
@ -37,7 +37,7 @@
</div>
<div class="clip-data">
<template v-if="item.type === 'text'">
<div>{{ item.data.split(`\n`).slice(0, 8).join(`\n`).trim() }}</div>
<div>{{ item.data.split(`\n`).slice(0, 7).join(`\n`).trim() }}</div>
</template>
<template v-if="item.type === 'image'">
<img class="clip-data-image" :src="item.data" alt="Image" />
@ -48,14 +48,20 @@
</div>
</div>
<div class="clip-operate" v-show="activeIndex === index">
<template v-for="{ id, title } of operation">
<template v-for="{ id, title, icon } of operation">
<div
v-if="id !== 'collect' || (id === 'collect' && item.collect !== true)"
v-if="
(id !== 'collect' && id !== 'view' && id !== 'open-folder' && id !== 'un-collect') ||
(id === 'collect' && item.collect !== true) ||
(id === 'view' && item.type !== 'image') ||
(id === 'open-folder' && item.type === 'file') ||
(id === 'un-collect' && item.collect === true)
"
:class="id"
:title="title"
@click.stop="handleOperateClick({ id, item })"
>
{{ title.slice(0, 1) }}
{{ icon }}
</div>
</template>
</div>
@ -96,17 +102,32 @@ const handleDataClick = (item) => emit('onDataChange', item)
const activeIndex = ref(0)
const handleMouseOver = (index) => (activeIndex.value = index)
const operation = [
{ id: 'copy', title: '复制' },
{ id: 'collect', title: '收藏' },
{ id: 'remove', title: '删除' }
{ id: 'copy', title: '复制', icon: '📄' },
{ id: 'view', title: '查看全部', icon: '💬' },
{ id: 'open-folder', title: '打开文件夹', icon: '📁' },
{ id: 'collect', title: '收藏', icon: '⭐' },
{ id: 'un-collect', title: '取消收藏', icon: '📤' },
{ id: 'remove', title: '删除', icon: '❌' }
]
const handleOperateClick = ({ id, item }) => {
switch (id) {
case 'copy':
window.copy(item)
window.copy(item, false)
break
case 'view':
emit('onDataChange', item)
break
case 'open-folder':
const { data } = item
const fl = JSON.parse(data)
window.openFileFolder(fl[0].path) //
break
case 'collect':
item.collect = true // important
item.collect = true
window.db.updateDataBaseLocal(db)
break
case 'un-collect':
item.collect = undefined
window.db.updateDataBaseLocal(db)
break
case 'remove':

View File

@ -1,6 +1,12 @@
<template>
<div class="clip-search">
<span class="clip-search-btn" v-show="!filterText && !isFocus" @click="toggleFocusStatus(true)"
>🔍</span
>
<input
class="clip-search-input"
@focusout="toggleFocusStatus(false)"
v-show="filterText || isFocus"
v-model="filterText"
type="text"
:placeholder="itemCount ? `🔍 在${itemCount}条历史中检索...` : '🔍 检索剪贴板历史...'"
@ -10,7 +16,7 @@
</template>
<script setup>
import { ref, watch } from 'vue'
import { ref, watch, nextTick } from 'vue'
const props = defineProps({
modelValue: {
type: String,
@ -20,6 +26,11 @@ const props = defineProps({
type: Number
}
})
const isFocus = ref(true)
const toggleFocusStatus = (status) =>
status ? ((isFocus.value = status), nextTick(() => window.focus())) : (isFocus.value = status)
const filterText = ref('')
const emit = defineEmits(['update:modelValue'])
// filterText modelValue

View File

@ -0,0 +1,22 @@
.clip-float-btn {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
z-index: 99999;
bottom: 15px;
right: 15px;
height: 50px;
width: 50px;
cursor: pointer;
border-radius: 50%;
font-size: 20px;
background-color: @text-bg-color-lighter;
user-select: none;
&:hover {
font-size: 25px;
color: @bg-color;
background-color: @primary-color;
transition: all 0.15s;
}
}

View File

@ -1,15 +1,44 @@
.clip-full {
.clip-full-wrapper {
z-index: 9999999999;
position: fixed;
top: 0;
height: 100%;
height: -webkit-fill-available;
width: 70%;
color: @text-color;
background: @bg-color;
padding: 0px 20px 0px 20px;
margin: 0px 0px;
padding: 10px 20px;
overflow-y: scroll;
word-break: break-all;
white-space: pre-wrap;
.clip-full-operate-list {
display: flex;
align-items: center;
justify-content: flex-end;
color: @text-color;
background-color: @text-bg-color;
padding: 5px 10px;
margin-bottom: 5px;
border-radius: 5px;
&-item {
padding: 10px;
border-radius: 5px;
cursor: pointer;
user-select: none;
margin: 5px 5px;
background-color: @bg-color;
&:hover {
color: @bg-color;
background-color: @primary-color;
transition: all 0.15s;
}
}
}
.clip-full-content {
background-color: @text-bg-color;
padding: 20px;
border-radius: 5px;
}
&::-webkit-scrollbar {
width: 10px;
height: 10px;
@ -25,10 +54,10 @@
&::-webkit-scrollbar-thumb {
background: @text-color-lighter;
border-radius: 5px;
}
&::-webkit-scrollbar-thumb:hover {
&:hover {
background: @text-color;
}
}
}
.clip-overlay {
z-index: 999999999;

View File

@ -52,14 +52,15 @@
display: flex;
overflow: hidden;
word-break: break-all;
max-height: 150px;
max-height: 120px;
max-width: 500px;
padding: 5px;
white-space: pre-wrap;
flex-direction: column;
color: @text-color;
img.clip-data-image {
// 此 class用于区分 file的 image
max-height: 140px; // 比外框 max-height少一点 因为有 5px的边框
max-height: 100px; // 比外框 max-height少一点 因为有 5px的边框
max-width: 90%;
box-shadow: 0px 0px 3px @text-color;
}
@ -79,8 +80,8 @@
display: flex;
align-items: center;
justify-content: center;
min-width: 100px;
padding: 10px;
min-width: 150px;
padding: 0px 10px;
& * {
display: flex;
align-items: center;

View File

@ -1,6 +1,16 @@
.clip-search {
display: flex;
align-items: center;
justify-content: flex-end;
min-width: 300px;
input {
margin-right: 10px;
.clip-search-btn {
width: 25px;
height: 25px;
padding: 10px;
cursor: pointer;
}
.clip-search-input {
width: 90%;
/* normalize */
background: none;

View File

@ -7,7 +7,7 @@
&:hover {
text-decoration: underline;
&::after {
content: '📤';
content: '📝';
}
}
.clip-file-icon {

View File

@ -1,5 +1,6 @@
.import() {
/* 导入全部涉及到变量的样式文件 */
@import (multiple) './cpns/clip-float-btn.less';
@import (multiple) './cpns/clip-full-data.less';
@import (multiple) './cpns/clip-item-list.less';
@import (multiple) './cpns/clip-search.less';

View File

@ -1,6 +1,6 @@
<template>
<div class="main">
<div class="clip-restore" @click="restoreDataBase">🧺</div>
<ClipFloatBtn></ClipFloatBtn>
<ClipFullData
:isShow="fullDataShow"
:fullData="fullData"
@ -29,6 +29,7 @@ import ClipItemList from '../cpns/ClipItemList.vue'
import ClipFullData from '../cpns/ClipFullData.vue'
import ClipSearch from '../cpns/ClipSearch.vue'
import ClipSwitch from '../cpns/ClipSwitch.vue'
import ClipFloatBtn from '../cpns/ClipFloatBtn.vue'
const GAP = 15 //
const offset = ref(0) //
@ -43,12 +44,25 @@ const updateShowList = (type) => {
type === 'collect' ? item.collect === true : type === 'all' ? item : item.type === type
) // collect type
.filter((item) => (filterText.value ? item.type !== 'image' : item)) // DataURL
.filter(
(item) =>
filterText.value
? item.data.toLowerCase().indexOf(filterText.value.toLowerCase()) !== -1 //
: item //
)
.filter((item) => {
if (filterText.value.trim()) {
if (filterText.value.trim().indexOf(' ') !== -1) {
//
const hitArray = []
for (const f of filterText.value.trim().split(' ')) {
hitArray.push(item.data.toLowerCase().indexOf(f.toLowerCase()) !== -1)
}
// false
return hitArray.indexOf(false) === -1
} else {
//
return item.data.toLowerCase().indexOf(filterText.value.trim().toLowerCase()) !== -1
}
} else {
//
return true
}
})
.slice(0, GAP) //
window.toTop()
}
@ -58,18 +72,13 @@ const handleNavClick = (type) => {
offset.value = 0 //
}
const fullData = ref({ type: 'text', data: '' })
const fullData = ref({ type: 'text' })
const fullDataShow = ref(false)
const toggleFullData = (item) => {
// ()
const { type, data } = item
// type: 'text' | 'file'
if (type === 'text') {
fullData.value.type = 'text'
fullData.value.data = data
} else if (type === 'file') {
fullData.value.type = 'file'
fullData.value.data = JSON.parse(data)
const { type } = item
if (type === 'text' || type === 'file') {
fullData.value = item
}
fullDataShow.value = !fullDataShow.value
}
@ -82,15 +91,6 @@ const handleDataRemove = () => {
updateShowList(ClipSwitchRef.value.activeTab)
}
const restoreDataBase = () => {
//
const flag = window.confirm('确定要清空剪贴板记录吗?')
if (flag) {
window.db.emptyDataBase()
updateShowList('all')
}
}
onMounted(() => {
// Ref
const activeTab = computed(() => ClipSwitchRef.value.activeTab)
@ -143,6 +143,8 @@ onMounted(() => {
const isSearch =
(ctrlKey && (key === 'F' || key === 'f')) || (ctrlKey && (key === 'L' || key === 'l'))
const isExit = key === 'Escape'
const isArrow = key === 'ArrowDown' || key === 'ArrowUp'
const isEnter = key === 'Enter'
if (isTab) {
const tabTypes = tabs.map((item) => item.type)
const index = tabTypes.indexOf(activeTab.value)
@ -156,9 +158,8 @@ onMounted(() => {
filterText.value = ''
e.stopPropagation()
}
} else if (ctrlKey || metaKey) {
// Ctrl
// utools
} else if (ctrlKey || metaKey || isArrow || isEnter) {
// Ctrl (utools)
} else {
window.focus() //
}
@ -168,25 +169,6 @@ onMounted(() => {
<style lang="less" scoped>
@import '../style';
.clip-restore {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
bottom: 10px;
right: 10px;
height: 50px;
width: 50px;
cursor: pointer;
border-radius: 50%;
font-size: 20px;
background-color: rgb(232, 232, 232);
user-select: none;
&:hover {
// background-color: @primary-color;
transition: all 0.15s;
}
}
.clip-break {
height: 60px;
}

View File

@ -5,22 +5,25 @@ const path = require('path')
module.exports = {
publicPath: './',
productionSourceMap: false,
chainWebpack: config => {
config.optimization
.minimizer('uglify-plugin')
.use(UglifyJsPlugin, [{
chainWebpack: (config) => {
config.optimization.minimizer('uglify-plugin').use(UglifyJsPlugin, [
{
uglifyOptions: {
drop_console: false,
drop_debugger: false,
pure_funcs: ['console.log']
}
}])
config.plugin('copy-plugin')
.use(CopyPlugin, [{
patterns: [{
}
])
config.plugin('copy-plugin').use(CopyPlugin, [
{
patterns: [
{
from: path.join(__dirname, 'README.md'),
to: path.join(__dirname, 'dist', 'README.md'),
}],
}])
},
to: path.join(__dirname, 'dist', 'README.md')
}
]
}
])
}
}