🔥🔥给女票写了一款剪贴板App🔥🔥

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
        });
    }
}
  • 图像这里就比较坑了

    老哥们如果有更好的方法欢迎提出,我学习一波。因为我是第一次写,贼菜,实在没想到其他的方法...

  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="" -="" 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 10="" dangerouslySetInnerHTML="{{" __html:="" item.html="" }}="" style="{{" height:="" 300,="" maxHeight:="" width:="" '100%',="" overflow:="" 'scroll',="" marginBottom:=""/>

列表太长,还得加一个回到顶部的按钮

<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最后的下场吧!
好吧,就是很鸡肋,我和女票只是使用了几天,后来发现使用场景还是不多,就弃置了。

说到底我只是想多折腾折腾罢了。

原文链接:juejin.im

上一篇:从Next.js到服务端渲染的学习
下一篇:简要分析JavaScript基于原型模式的面向对象

相关推荐

  • 高德APP启动耗时剖析与优化实践(iOS篇)

    前言最近高德地图APP完成了一次启动优化专项,超预期将双端启动的耗时都降低了65%以上,iOS在iPhone7上速度达到了400毫秒以内。就像产品们用后说的,快到不习惯。

    5 个月前
  • 面试常谈之手写new、call、apply和bind

    new function myNew(){ //创建一个空对象 let obj = new Object(); //获取构造函数 let Constructor = [...

    1 年前
  • 面试官问:能否模拟实现JS的call和apply方法

    之前写过两篇《面试官问:能否模拟实现JS的new操作符》和《面试官问:能否模拟实现JS的bind方法》 其中模拟bind方法时是使用的call和apply修改this指向。

    2 年前
  • 面试官再问call、apply、bind。来来来、我给你手写一个!

    哈喽,大家好!我是前端Up主。一个有代码洁癖的前端攻城狮( 哈哈,生活很邋遢(* ̄︶ ̄) ) 相信不少小伙伴在面试中,都会被问到怎么改变this的指向呢,然后你说:call、apply、bind。

    21 天前
  • 面试官:你能手写实现call,apply,bind方法吗?

    [实践系列] 主要是让我们通过实践去加深对一些原理的理解。 实践系列-前端路由 实践系列-Babel原理 实践系列-Promises/A+规范 实践系列-浏览器缓存机制 有兴趣的同学可以关注 我的博客...

    2 年前
  • 随着nativescript Appium测试应用程序

    Zakaria Acharkiuser70192提出了一个问题:Testing NativeScript app with Appium,或许与您遇到的问题类似。 回答者nullpointer给出了该...

    2 年前
  • 问道Angular——APP_INITIALIZER

    概述   有时需要在加载应用之前运行代码,有时希望暂停应用初始化,直到完成某些限制之后再执行。APP_INITIALIZER令牌可以完成这项操作。 APP_INITIALIZER是一个函数,在应用改程...

    2 年前
  • 针对于iosAPP内嵌H5,-webit-overflow-scrolling:touch;产生空白情况

    问题描述:一个内嵌IOSAPP的H5页面,长页面,大概1.6个屏幕高度,由于有列表滑动起来很不流畅,所以用了-webit-overflow-scrolling:touch;这个只针对ios端的物理滚动...

    2 年前
  • 都2020年了,你应该知道如何手写Call、Apply、Bind了吧

    导读 作为面试中面试官最宠爱的一个问题,在这里进行一个详细的介绍,大家重点要放在理解,而不是背。 写的不好或不对的地方,请大家积极指出,好了,话不多说,我们“圆规正转” 先说一下三者的区别 共同...

    9 个月前
  • 通过this深入理解javascript函数的bind、call、apply

    我们都知道函数这个高级公民在js中的地位很高。用处很大,可是却很难搞!函数内部有一个很厉害的内部属性this关键字。而bind、call、apply是函数的3个非继承方法(但是在原型链中找到了这些方法...

    1 年前

官方社区

扫码加入 JavaScript 社区