I wrote a clipboard app for my girlfriend

Time:2021-1-17

Git address:https://github.com/sunxiuguo/VisualClipboard

I wrote a clipboard app for my girlfriend

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?

I wrote a clipboard app for my girlfriend

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

  1. The native image object is read from the clipboard
  2. 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.
  3. The final implementation is to store the read image as a local temporary file named {MD5}. JPEG
  4. 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

  1. Main content
  2. Time of clip content
  3. Copy button to make it easier to copy list item content
  4. 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.