??給女票寫了一款剪貼簿App??

路從今夜白丶發表於2020-02-14

Git地址github.com/sunxiuguo/V…

??給女票寫了一款剪貼簿App??

背景

女票:有的時候複製貼上過的內容還想再看一下,然而又忘了原來的內容是在哪了,找起來還挺麻煩的

我:看爸爸給你寫個app,允你免費試用!

女票:??給你臉了?

??給女票寫了一款剪貼簿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
        });
    }
}
複製程式碼
  • 影像這裡就比較坑了

    老哥們如果有更好的方法歡迎提出,我學習一波。因為我是第一次寫,賊菜,實在沒想到其他的方法...

  1. 從剪貼簿讀取到的是NativeImage物件
  2. 本來想轉換為base64儲存,嘗試過後放棄了,因為儲存的內容太大了,會非常卡。
  3. 最終實現是將讀到的影像儲存為本地臨時檔案,以{md5}.jpeg命名
  4. 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);
複製程式碼

渲染列表項

我們的列表項中需要包含

  1. 主體內容
  2. 剪貼內容的時間
  3. 複製按鈕,以更方便地複製列表項內容
  4. 對於比較長的內容,需要支援點選彈窗顯示全部內容
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最後的下場吧!
好吧,就是很雞肋,我和女票只是使用了幾天,後來發現使用場景還是不多,就棄置了。

說到底我只是想多折騰折騰罷了。

相關文章