Git address:https://github.com/sunxiuguo/VisualClipboard
background
Female ticket: sometimes I want to look at the copied and pasted content again, but I forget where the original content is. It’s very troublesome to find it
Me: look, Dad wrote you an app, allowing you to try it for free!
Female ticket:?? Give you face?
Do it
Keke, I started to write code instead of being beaten by women
Although I have never written about electron, I remember that this product supports the clipboard API, so roll up your sleeves and start working. It’s time to practice!
First of all, make clear our goal:
- Real time acquisition of system clipboard contents (including but not limited to text and image)
- Store acquired information
- Displays a list of stored information
- Can quickly view a record and copy it again
- Support keyword search
Monitor system clipboard
The temporary implementation of monitoring system clipboard is to read the current contents of the clipboard at a fixed time. The fixed time task uses node schedule, which can easily set the frequency.
//Here is to take the contents of the clipboard every second and store them
startWatching = () => {
if (!this.watcherId) {
this.watcherId = schedule.scheduleJob('* * * * * *', () => {
Clipboard.writeImage();
Clipboard.writeHtml();
});
}
return clipboard;
};
storage
At present, it is only a local application, and it has not done multi terminal synchronization, so it directly uses indexdb for storage.
In the above codeClipboard.writeImage()
as well asClipboard.writeHtml()
That is to write to indexdb.
- The storage of text is very simple, read directly and write directly
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
});
}
}
-
The image here is more pit
If you have a better way, you are welcome to put forward. I will learn a lot. Because it's my first time to write, thief dish, I really didn't think of other ways
- The native image object is read from the clipboard
- Originally, I wanted to convert to Base64 storage, but I gave up after trying, because the content of storage is too large, and it will be very difficult to store.
- The final implementation is to store the read image as a local temporary file named {MD5}. JPEG
- The MD5 value is directly stored in the indexdb and can be accessed directly with 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);
}
});
}
}
}
-
Delete expired temporary image files
Because the image file is temporarily stored in the hard disk, in order to prevent too many junk files, we add the function of overdue cleaning.
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);
}
});
}
});
}
Display list
The above has completed the timing of writing to DB, and the next thing we need to do is to display the contents stored in dB in real time.
1. Define a userinterval to prepare for a scheduled refresh
/**
* 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();
}
//Pause interval when delay = = null
if (delay !== null) {
const timer = setInterval(tick, delay);
return () => clearInterval(timer);
}
}, [delay]);
}
2. Use userinterval to display the list
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);
Render list items
Our list items need to contain
- Main content
- Time of clip content
- Copy button to make it easier to copy list item content
- For relatively long content, you need to support clicking the pop-up window to display all content
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)}
>
copy
</Button>
</CardActions>
</Card>
);
};
The contents read from the clipboard need to be displayed in the original format
just rightclipboard.readHTML([type])
You can read the HTML content directly, so we just need to display the HTML content correctly.
<div
dangerouslySetInnerHTML={{ __html: item.html }}
style={{
height: 300,
maxHeight: 300,
width: '100%',
overflow: 'scroll',
marginBottom: 10
}}
/>
The list is too long. You need to add a button to go back to the top
<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);
}
};
Using react window to optimize long list
There are too many elements in the list, and it will get stuck when browsing for a long time. Use react window to optimize the list display, and only display a fixed number of elements in the visual area.
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>
);
Write at the end
Although this device can barely be used in the end, there are still many shortcomings, especially in the image processing area. Later, I want to use canvas to compress and store images.
And in the end, I didn’t use the knowledge of electron very much. After all, I used electron react boilerplate directly, and the rest was piling up code.
Let’s talk about the final end of visual clipboard!
Well, it’s just chicken ribs. We only used the ticket for a few days, but later we found that there were not many scenes, so we abandoned it.
At the end of the day, I just want to do more.