Git地址:github.com/sunxiuguo/V…
背景
女票:有的時候複製貼上過的內容還想再看一下,然而又忘了原來的內容是在哪了,找起來還挺麻煩的
我:看爸爸給你寫個app,允你免費試用!
女票:??給你臉了?
動手
咳咳 是動手開始寫程式碼, 不是被女票動手打
雖然從來沒寫過electron,但是記得這貨是支援 剪貼簿API 的,那就擼袖子開始幹,就當練練手了!
首先明確我們的目標:
- 實時獲取系統剪貼簿的內容(包括但不限於文字、影像)
- 儲存獲取到的資訊
- 展示儲存的資訊列表
- 能夠快速檢視某一項紀錄並再次複製
- 支援關鍵字搜尋
監聽系統剪貼簿
監聽系統剪貼簿,暫時的實現是定時去讀剪貼簿當前的內容,定時任務使用的是node-schedule,可以很方便地設定頻率。
// 這裡是每秒都去拿一次剪貼簿的內容,然後進行儲存
startWatching = () => {
if (!this.watcherId) {
this.watcherId = schedule.scheduleJob('* * * * * *', () => {
Clipboard.writeImage();
Clipboard.writeHtml();
});
}
return clipboard;
};
複製程式碼
儲存
目前只是本地應用,還沒有做多端的同步,所以直接用了indexDB來做儲存。
上面程式碼中的Clipboard.writeImage()
以及Clipboard.writeHtml()
就是向indexDB中寫入。
- 文字的儲存很簡單,直接讀取,寫入即可
static writeHtml() {
if (Clipboard.isDiffText(this.previousText, clipboard.readText())) {
this.previousText = clipboard.readText();
Db.add('html', {
createTime: Date.now(),
html: clipboard.readHTML(),
content: this.previousText
});
}
}
複製程式碼
-
影像這裡就比較坑了
老哥們如果有更好的方法歡迎提出,我學習一波。因為我是第一次寫,賊菜,實在沒想到其他的方法...
- 從剪貼簿讀取到的是NativeImage物件
- 本來想轉換為base64儲存,嘗試過後放棄了,因為儲存的內容太大了,會非常卡。
- 最終實現是將讀到的影像儲存為本地臨時檔案,以{md5}.jpeg命名
- indexDB中直接儲存md5值,使用的時候直接用md5.jpeg訪問即可
static writeImage() {
const nativeImage = clipboard.readImage();
const jpegBufferLow = nativeImage.toJPEG(jpegQualityLow);
const md5StringLow = md5(jpegBufferLow);
if (Clipboard.isDiffText(this.previousImageMd5, md5StringLow)) {
this.previousImageMd5 = md5StringLow;
if (!nativeImage.isEmpty()) {
const jpegBuffer = nativeImage.toJPEG(jpegQualityHigh);
const md5String = md5(jpegBuffer);
const now = Date.now();
const pathByDate = `${hostPath}/${DateFormat.format(
now,
'YYYYMMDD'
)}`;
xMkdirSync(pathByDate);
const path = `${pathByDate}/${md5String}.jpeg`;
const pathLow = `${pathByDate}/${md5StringLow}.jpeg`;
fs.writeFileSync(pathLow, jpegBufferLow);
Db.add('image', {
createTime: now,
content: path,
contentLow: pathLow
});
fs.writeFile(path, jpegBuffer, err => {
if (err) {
console.error(err);
}
});
}
}
}
複製程式碼
- 刪除過期的臨時影像檔案
由於影像檔案我們是臨時儲存在硬碟裡的,為了防止存有太多垃圾檔案,新增了過期清理的功能。
startWatching = () => {
if (!this.deleteSchedule) {
this.deleteSchedule = schedule.scheduleJob('* * 1 * * *', () => {
Clipboard.deleteExpiredRecords();
});
}
return clipboard;
};
static deleteExpiredRecords() {
const now = Date.now();
const expiredTimeStamp = now - 1000 * 60 * 60 * 24 * 7;
// delete record in indexDB
Db.deleteByTimestamp('html', expiredTimeStamp);
Db.deleteByTimestamp('image', expiredTimeStamp);
// remove jpg with fs
const dateDirs = fs.readdirSync(hostPath);
dateDirs.forEach(dirName => {
if (
Number(dirName) <=
Number(DateFormat.format(expiredTimeStamp, 'YYYYMMDD'))
) {
rimraf(`${hostPath}/${dirName}`, error => {
if (error) {
console.error(error);
}
});
}
});
}
複製程式碼
展示列表
上面已經完成了定時的寫入db,接下來我們要做的是實時展示db中儲存的內容。
1. 定義userInterval來準備定時重新整理
/**
* react hooks - useInterval
* https://overreacted.io/zh-hans/making-setinterval-declarative-with-react-hooks/
*/
import { useEffect, useRef } from 'react';
export default function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
// 當delay === null時, 暫停interval
if (delay !== null) {
const timer = setInterval(tick, delay);
return () => clearInterval(timer);
}
}, [delay]);
}
複製程式碼
2. 使用userInterval展示列表
const [textList, setTextList] = React.useState([]);
useInterval(() => {
const getTextList = async () => {
let textArray = await Db.get(TYPE_MAP.HTML);
if (searchWords) {
textArray = textArray.filter(
item => item.content.indexOf(searchWords) > -1
);
}
if (JSON.stringify(textArray) !== JSON.stringify(textList)) {
setTextList(textArray);
}
};
if (type === TYPE_MAP.HTML) {
getTextList();
}
}, 500);
複製程式碼
渲染列表項
我們的列表項中需要包含
- 主體內容
- 剪貼內容的時間
- 複製按鈕,以更方便地複製列表項內容
- 對於比較長的內容,需要支援點選彈窗顯示全部內容
const renderTextItem = props => {
const { columnIndex, rowIndex, data, style } = props;
const index = 2 * rowIndex + columnIndex;
const item = data[index];
if (!item) {
return null;
}
if (rowIndex > 3) {
setScrollTopBtn(true);
} else {
setScrollTopBtn(false);
}
return (
<Card
className={classes.textCard}
key={index}
style={{
...style,
left: style.left,
top: style.top + recordItemGutter,
height: style.height - recordItemGutter,
width: style.width - recordItemGutter
}}
>
<CardActionArea>
<CardMedia
component="img"
className={classes.textMedia}
image={bannerImage}
/>
<CardContent className={classes.textItemContentContainer}>
...
</CardContent>
</CardActionArea>
<CardActions
style={{ display: 'flex', justifyContent: 'space-between' }}
>
<Chip
variant="outlined"
icon={<AlarmIcon />}
label={DateFormat.format(item.createTime)}
/>
<Button
size="small"
color="primary"
variant="contained"
onClick={() => handleClickText(item.content)}
>
複製
</Button>
</CardActions>
</Card>
);
};
複製程式碼
從剪貼簿中讀到的內容,需要按照原有格式展示
恰好clipboard.readHTML([type])
可以直接讀到html內容,那麼我們只需要正確展示html內容即可。
<div
dangerouslySetInnerHTML={{ __html: item.html }}
style={{
height: 300,
maxHeight: 300,
width: '100%',
overflow: 'scroll',
marginBottom: 10
}}
/>
複製程式碼
列表太長,還得加一個回到頂部的按鈕
<Zoom in={showScrollTopBtn}>
<div
onClick={handleClickScrollTop}
role="presentation"
className={classes.scrollTopBtn}
>
<Fab
color="secondary"
size="small"
aria-label="scroll back to top"
>
<KeyboardArrowUpIcon />
</Fab>
</div>
</Zoom>
const handleClickScrollTop = () => {
const options = {
top: 0,
left: 0,
behavior: 'smooth'
};
if (textListRef.current) {
textListRef.current.scroll(options);
} else if (imageListRef.current) {
imageListRef.current.scroll(options);
}
};
複製程式碼
使用react-window優化長列表
列表元素太多,瀏覽時間長了會卡頓,使用react-window來優化列表展示,可視區域內只展示固定元素數量。
import { FixedSizeList, FixedSizeGrid } from 'react-window';
const renderDateImageList = () => (
<AutoSizer>
{({ height, width }) => (
<FixedSizeList
height={height}
width={width}
itemSize={400}
itemCount={imageList.length}
itemData={imageList}
innerElementType={listInnerElementType}
outerRef={imageListRef}
>
{renderDateImageItem}
</FixedSizeList>
)}
</AutoSizer>
);
複製程式碼
寫在最後
雖然這玩意最後勉強能用,但是還有很多的不足,尤其影像處理的那塊,後期想改成用canvas壓縮儲存圖片。
而且說到底也沒怎麼用到electron的知識,畢竟直接使用了electron-react-boilerplate,剩下的就是在堆砌程式碼。
說說這個visualClipboard最後的下場吧!
好吧,就是很雞肋,我和女票只是使用了幾天,後來發現使用場景還是不多,就棄置了。
說到底我只是想多折騰折騰罷了。