(已废弃)B站观看内容统计-我的时间不见了

方便查过去浏览记录

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         (已废弃)B站观看内容统计-我的时间不见了
// @version      0.1.2
// @description  方便查过去浏览记录
// @author       strangeZombies
// @namespace    https://www.github.com/strangeZombies
// @match        *://*.bilibili.com/*
// @require      https://static.hdslb.com/js/jquery.min.js
// @icon         https://static.hdslb.com/images/favicon.ico
// @run-at       document-end

// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_listValues
// @grant        GM_addStyle
// @grant        GM_addElement

// ==/UserScript==
/* globals $ */
/* jshint esversion: 11 */

// (废弃中) 不要使用该脚本在积累了大量数据后会导致浏览器卡死

// 最早基于 判官喵的B站观看内容统计-我的时间都去哪了
// 最新基于 人工智能生成的代码
// 未完成的 搜索功能模块
// 建议加装 ClearURLs 扩展插件
// 即使在编写本脚本的过程中大量使用智能,仍不能保证脚本的出厂质量
// 由于会累计大量数据,已知存在性能问题,可能致使页面卡顿
(function () {
    'use strict';
    // 引入 GM_addElement 和 GM_addEvent 函数
    function GM_addElement(tag, options) {
        var node = document.createElement(tag);
        if (options && typeof options === "object") {
            for (var prop in options) {
                var value = options[prop];
                if (prop === "style" && typeof value === "object") {
                    for (var styleProp in value) {
                        node.style[styleProp] = value[styleProp];
                    }
                } else if (prop === "text") {
                    node.textContent = value;
                } else if (prop === "className") {
                    node.className = value;
                } else {
                    node[prop] = value;
                }
            }
        }
        return node;
    }
    function GM_addEvent(element, event, handler) {
        if (element.addEventListener) {
            element.addEventListener(event, handler, false);
        } else if (element.attachEvent) {
            element.attachEvent("on" + event, handler);
        }
    }
    //下载 ajax get请求携带cookie Json
    async function getOneAjax(addr) {
        try {
            let res = await $.ajax({
                type: "get",
                url: addr,
                dataType: "json",
                async: true,
                // 允许请求携带cookie
                xhrFields: {
                    withCredentials: true
                }
            }).then(json => { return json; })
            return res;
        } catch (err) {
            return err;
        }
    }
    //读取缓存模块 返回string->json格式化缓存内容
    // "lishijilulist"
    function GMgetStrToJson(cacheName) {
        // 初始字符
        let cacheJson, cacheStr;
        // 获取缓存
        cacheStr = GM_getValue(cacheName);
        // 如果存在缓存则格式化为Json并返回
        if (cacheStr == undefined) {
            cacheJson = cacheStr; // 如果不存在则为 undifined
        } else {
            cacheJson = JSON.parse(cacheStr);
        }
        return cacheJson;
    }
    //存储缓存列表模块 Json
    function GMsetJson(cacheName, cacheValue) {
        let cacheValueTemp = JSON.stringify(cacheValue);
        GM_setValue(cacheName, cacheValueTemp);
    }

    // 计数统计
    const samer = {
        same: 0,
        page: 0,
        inSame() {
            this.same++;
        },
        getSame() {
            return this.same;
        },
        reSame() {
            this.same = 0;
        },
        inPage() {
            this.page++;
        },
        getPage() {
            return this.page;
        }
    };
    //获取进度 (未完成)
    const preEC = {
        // 进度条
        preE: 0,
        setPreE(preE) {
            this.preE = preE;
            return this.preE;
        },
        getPreE() {
            return this.preE;
        }
    }

    //设置标头存储模块
    function headerCache(firstCache, preJson, cacheBucket, nextCache) {
        let getId, getViewAt, PerJsonBucketLen;
        //如果没登录就跳出循环
        if (preJson.code === -101) {
            preEC.setPreE(-101);
            nextCache = -101;
        } else {
            PerJsonBucketLen = preJson.data.list.length;
            if (PerJsonBucketLen != 0) {
                //提取最后观看数据项oid和观看时间
                getId = preJson.data.list[0].history.oid;
                getViewAt = preJson.data.list[0].view_at;
            }
            //判断是否为首次缓存
            if (firstCache == 1) {
                if (cacheBucket.length == 0) {
                    console.log("首次获取数据时间较长请耐心等待!")
                    //是首次缓存则 添加标头项首次缓存标记,最后观看oid和时间数据,返回存储列表
                    cacheBucket[0] = ({ first_cache: firstCache, last_oid: getId, last_view_at: getViewAt, thisCache_len: 0 })
                }
                return cacheBucket;
            } else {
                //非首次缓存存储最后观看数据项
                //是则 把此页最后观看数据添加到标头项备用栏,返回存储列表
                cacheBucket[0].beiyong_last_oid = getId;
                cacheBucket[0].beiyong_last_view_at = getViewAt;
                cacheBucket[0].bencicunchu_len = 0;
                return cacheBucket;
            }
        }
    }
    //prejson数据添加到存储列表模块
    function preJsonToCacheBucket(preJson, i, cacheBucket) {
        //每个视频bvid 每个视频时长 每个视频观看时长 每个视频观看时间
        let every_oid, //视频是av号,专栏是cv号,直播是直播间号
            every_author_mid, //up主uid
            every_author_name, //up主名字
            every_badge, //此条记录的类型
            every_title, //标题
            every_duration, //视频时长
            every_progress, //观看时长
            every_view_at; //观看时间
        //可能增加的数据
        //every_cover,
        //every_author_face;

        //开始赋值
        every_oid = preJson.data.list[i].history.oid;
        every_author_mid = preJson.data.list[i].author_mid;
        every_author_name = preJson.data.list[i].author_name;
        every_badge = preJson.data.list[i].badge;
        every_title = preJson.data.list[i].title;
        every_duration = preJson.data.list[i].duration;
        every_progress = preJson.data.list[i].progress;
        every_view_at = preJson.data.list[i].view_at;
        //可能增加的数据
        //every_cover = preJson.data.list[i].cover;
        //every_author_face = preJson.data.list[i].author_face;

        //判断是否有重复内容
        //寻找相同观看时间内容
        let sameViewAt = cacheBucket.find(item => item.view_at === every_view_at);

        if (sameViewAt == undefined) {
            // 视频观看时间不存在于缓存中,检查是否已经存在相同的 oid。
            let sameOid = cacheBucket.find(item => item.oid === every_oid && item.view_at !== every_view_at);
            if (sameOid == undefined) {
                // 视频 bvid 也不存在于缓存中,将数据添加到缓存列表中
                cacheBucket.push({
                    oid: every_oid, author_mid: every_author_mid,
                    author_name: every_author_name, badge: every_badge, title: every_title,
                    duration: every_duration, progress: every_progress, view_at: every_view_at
                });
                cacheBucket[0].bencicunchu_len += 1;
            } else {
                samer.inSame();
                //console.log('有找到',samer.getSame());
                // 视频 bvid 已经存在于缓存中,但是观看时间不同,忽略该条记录
            }
        } else {
            samer.inSame();
            //console.log('有找到',samer.getSame());
            // 视频观看时间已经存在于缓存中,忽略该条记录
        }

        return cacheBucket;
    }

    //处理json数据加入存储列表模块  调用preJsonToCacheBucket
    function jsonToCacheBucket(preJson, cacheBucket) {
        //提取首次存储标记
        let firstCache = cacheBucket[0].first_cache;
        //提取json数据列表长度
        let preJsonListLen = preJson.data.list.length;
        //提取记录的最后一个观看时间
        let lastViewAt = cacheBucket[0].last_view_at;
        let lastOid = cacheBucket[0].last_oid;
        //提取记录的备用最后一个观看时间
        let backupLastViewAt = cacheBucket[0].beiyong_last_view_at
        let backupLastOid = cacheBucket[0].beiyong_last_oid;
        //列表执行计数
        let i;
        //获取时间戳
        let tistime = Date.now();
        //判断数据长度是否为0
        if (preJsonListLen == 0) {
            //如果没有数据则是提取到最后一页,将首次存储状态改为0无效,并存储 返回下页状态为0
            cacheBucket[0].first_cache = 0;
            cacheBucket[0].last_jiancha_time = tistime;
            GMsetJson('lishijilulist', cacheBucket);
            //console.log("最早一条");
            return 0;
            //返回状态0不再进行下个页面获取
        } else {
            //有数据则判断是否为首次缓存
            if (firstCache == 1) {
                //首次缓存直接循环执行 api页面json数据添加到存储列表模块
                for (i = 0; i < preJsonListLen; i++) {
                    cacheBucket = preJsonToCacheBucket(preJson, i, cacheBucket);
                }
                GMsetJson('lishijilulist', cacheBucket);
                //返回状态1继续进行下个页面获取
                return 1;
            } else {
                //非首次缓存则,判断缓存最后一个观看时间与记录列表时间(包括 oid 和 view_at)大小
                for (i = 0; i < preJsonListLen; i++) {
                    let currentViewAt = preJson.data.list[i].view_at;
                    let currentOid = preJson.data.list[i].history.oid;
                    // 如果当前记录的时间和 oid 都等于缓存最后一条记录,则停止获取
                    if (lastViewAt === currentViewAt && lastOid === currentOid) {
                        //将备用最后时间添加到存储列表的最后时间,直接存储已有列表
                        cacheBucket[0].last_view_at = backupLastViewAt;
                        cacheBucket[0].last_oid = backupLastOid;
                        cacheBucket[0].last_jiancha_time = tistime;
                        GMsetJson('lishijilulist', cacheBucket);
                        return 0;
                        //返回状态0不再进行下个页面获取
                    } else {
                        //否则,将记录添加到缓存列表中
                        cacheBucket = preJsonToCacheBucket(preJson, i, cacheBucket);
                        lastViewAt = cacheBucket[0].last_view_at;
                        lastOid = cacheBucket[0].last_oid;
                    }
                }
                //整页获取完后存储,返回状态1继续进行下个页面获取
                GMsetJson('lishijilulist', cacheBucket);
                return 1;
            }
        }
    }

    //获取一组数据
    async function ajaxOneHistory(maxId, viewAt, businessId) {
        let url = `https://api.bilibili.com/x/web-interface/history/cursor?max=${maxId}&view_at=${viewAt}&business=${businessId}`;
        //console.log('正在获取', url);
        await new Promise(resolve => setTimeout(resolve, 100));
        let data = await getOneAjax(url);
        return data;
    }
    // MAIN FETCH
    async function ajaxHistory() {
        //首次缓存标记,1有效,0无效 | 读取列表  | 存入列表 | 获取列表
        let originCacheBucket, cacheBucket = [], preJson, firstCache;
        //读取缓存
        originCacheBucket = GMgetStrToJson('lishijilulist');
        cacheBucket = originCacheBucket; // 如果没有则此处为undifined;

        //判断缓存是否存在
        if (originCacheBucket == undefined) {
            //不存在则执行以下
            //是首次缓存
            firstCache = 1;
            cacheBucket = [];
        } else {
            firstCache = 0; //007 006
        }
        preEC.setPreE(0);
        let nextCache = 1;
        let maxId = 0, viewAtId = 0, businessId = '';

        while (nextCache === 1) {
            preJson = await ajaxOneHistory(maxId, viewAtId, businessId).then(preJson => { return preJson });
            preEC.setPreE(10);
            cacheBucket = headerCache(firstCache, preJson, cacheBucket);
            if (preEC.getPreE() === -101) {
                echo.log(echo.asWarning("我的时间不见了"), echo.asAlert("请先登录!"));
                break;
            }
            nextCache = jsonToCacheBucket(preJson, cacheBucket);
            maxId = preJson.data.cursor.max;
            viewAtId = preJson.data.cursor.view_at;
            businessId = preJson.data.cursor.business;

            //
            if (samer.getSame() === 20) {
                samer.inPage();
            }
            samer.reSame();
            if (samer.getPage() === 2) {
                //  console.log('Enough');
                nextCache = 0;
            }
        }
        if (preEC.getPreE() != -101) { preEC.setPreE(20); }
    }

    //数量时长计算模块
    function pharseResult(cacheBucket) {
        //获取时间戳
        const nowtime = new Date(),
            //获取年份
            thisyear = nowtime.getFullYear(),
            //获取当月的月份
            thismonth = nowtime.getMonth() + 1,
            lastmonth = nowtime.getMonth(),
            //获取当前日期
            today = nowtime.getDate(),
            //当天0点时间戳
            todaytime = (new Date(thisyear + "-" + thismonth + "-" + today) / 1000),
            //前一天0点时间戳
            yesterdaytime = (new Date(thisyear + "-" + thismonth + "-" + today) / 1000) - 24 * 60 * 60,
            //6天前的时间戳
            weektime = (new Date(thisyear + "-" + thismonth + "-" + today) / 1000) - 24 * 60 * 60 * 6,
            //本月1号的时间戳
            thismonthtime = (new Date(thisyear + "-" + thismonth + "-" + 1) / 1000),
            //上月1号的时间戳
            lastmonthtime = (new Date(thisyear + "-" + lastmonth + "-" + 1) / 1000);
        //计算结果
        let pharseResult = [],
            judgeTime, //判断时间状态,5统计所有,4统计上月,3统计本月,2统计7天,1统计昨天,0统计今天
            every_badge, //此条记录的类型
            every_duration, //视频时长
            every_progress, //观看时长
            every_view_at, //观看时间
            //获取数据长度
            cacheBucketLen = cacheBucket.length,
            //计数用
            ntjs;
        //逐个统计计算
        for (ntjs = 1; ntjs < cacheBucketLen; ntjs++) {
            //逐个获取数据
            every_badge = cacheBucket[ntjs].badge;
            every_duration = cacheBucket[ntjs].duration;
            every_progress = cacheBucket[ntjs].progress;
            every_view_at = cacheBucket[ntjs].view_at;
            //计入统计所有
            judgeTime = 5;
            //执行集中统计模块 返回统计结果
            pharseResult = pharseAll(every_badge, every_duration, every_progress, judgeTime, pharseResult);
            if (lastmonthtime < every_view_at && every_view_at < thismonthtime) {
                //判断时间是否在上月范围内
                judgeTime = 4;
                //是则执行集中统计模块 返回统计结果
                pharseResult = pharseAll(every_badge, every_duration, every_progress, judgeTime, pharseResult);
            }
            if (thismonthtime < every_view_at) {
                //判断时间是否在本月范围内
                judgeTime = 3;
                //是则执行集中统计模块 返回统计结果
                pharseResult = pharseAll(every_badge, every_duration, every_progress, judgeTime, pharseResult);
            }
            if (weektime < every_view_at) {
                //判断时间是否在7天范围内
                judgeTime = 2;
                //是则执行集中统计模块 返回统计结果
                pharseResult = pharseAll(every_badge, every_duration, every_progress, judgeTime, pharseResult);
            }
            if (yesterdaytime < every_view_at && every_view_at < todaytime) {
                //判断时间是否在昨天范围内
                judgeTime = 1;
                //是则执行集中统计模块 返回统计结果
                pharseResult = pharseAll(every_badge, every_duration, every_progress, judgeTime, pharseResult);
            }
            if (todaytime < every_view_at) {
                //判断时间是否在今天范围内
                judgeTime = 0;
                //是则执行集中统计模块 返回统计结果
                pharseResult = pharseAll(every_badge, every_duration, every_progress, judgeTime, pharseResult);
            }
        }
        return pharseResult;
    }
    //集中统计模块
    function pharseAll(every_badge, every_duration, every_progress, judgeTime, pharseResult) {
        //判断是否属于视频
        if (every_badge == "" || every_badge == "综艺" || every_badge == "电影" || every_badge == "番剧" || every_badge == "纪录片" || every_badge == "电视剧" || every_badge == "国创") {
            if (pharseResult.length == 0) {
                //赋值初始内容
                for (let j = 0; j < 6; j++) {
                    pharseResult[j] = ({ av: 0, cv: 0, live: 0, avtime: 0, gktime: 0 });
                }
            }
            //属于视频则添加计数,并加总视频时长和观看时长
            pharseResult[judgeTime].av = pharseResult[judgeTime].av + 1;
            pharseResult[judgeTime].avtime = pharseResult[judgeTime].avtime + every_duration;
            //如果every_duration是-1值,则说明看完了此视频
            if (every_progress == -1) {
                every_progress = every_duration;
            }
            pharseResult[judgeTime].gktime = pharseResult[judgeTime].gktime + every_progress;
        } else if (every_badge == "专栏" || every_badge == "笔记") {
            //判断是否属于专栏
            pharseResult[judgeTime].cv = pharseResult[judgeTime].cv + 1;
        } else if (every_badge == "未开播" || every_badge == "直播中") {
            //判断是否属于直播
            pharseResult[judgeTime].live = pharseResult[judgeTime].live + 1;
        }
        //返回统计结果
        return pharseResult;
    }
    //根据数据特性判断是否要补全视频描述信息
    function videoTag(cacheBucket) {
        let cacheLen = cacheBucket.length;
        let disc = cacheBucket[cacheLen - 1].hasOwnProperty('tag_name');
        //从最早的数据开始处理
        //最早数据有视频描述
        if (cacheBucket[1].hasOwnProperty('tag_name')) {
            //最新数据有 最早也有
            if (disc) {
                cacheBucket[0].repair = cacheBucket[cacheLen - 1].view_at;
                cacheBucket[0].first_cache = 8 //完全不需要处理
                preEC.setPreE(40);
            } else { //最新没有 最早有
                cacheBucket[0].first_cache = 7; //完全处理过但需更新
                //console.log("需更新");
                if (cacheBucket[0].hasOwnProperty('repair')) {
                    // 遍历数组,找到第一个属性prop等于value的元素
                    const foundElement = cacheBucket.find(e => e.view_at === cacheBucket[0].repair);
                    // 如果找到了元素,返回它在数组中的下标;否则返回-1
                    const index = foundElement ? cacheBucket.indexOf(foundElement) : -1;
                    videoDisc(index, cacheBucket);
                }
            }
        } else {
            //第一次直接完全处理
            //最早没有 最新也没有
            if (!disc) {
                //由于是第一次则直接获取全部数组
                cacheBucket[0].first_cache = 7;
                videoDisc(1, cacheBucket);
            } else { //最早没有 最新有
                //不会出现的情况
            }
        }
    }

    //给数据添加视频描述
    async function videoDisc(iii, cacheBucket) {
        const cacheLen = cacheBucket.length;
        //从什么位置开始
        let tag, repair;
        for (let i = iii; i < cacheLen; i++) {
            tag = await getOneAjax(`https://api.bilibili.com/x/web-interface/view/detail/tag?aid=${cacheBucket[i].oid}`);
            if (tag.code === 0) {
                let tag_data = tag.data;
                //视频标签循环获取及拼接
                //设置无描述计数
                let tag_name = '';
                for (let k = 0; k < tag_data.length; k++) {
                    //设置 tag_name
                    if (tag_name == '') {
                        tag_name = '' + tag_data[k].tag_name;
                    } else {
                        tag_name = tag_name + ';' + tag_data[k].tag_name;
                    }
                    cacheBucket[i].tag_name = tag_name;
                }
                repair = cacheBucket[i].view_at;
            }
            //是否到最后的位置
            if (cacheBucket[cacheLen - 1].view_at === repair) {
                cacheBucket[0].first_cache = 8;
            }
            cacheBucket[0].repair = repair;
            GMsetJson('lishijilulist', cacheBucket);
        }
        GMsetJson('lishijilulist', cacheBucket);
    }

    
    //备份数据
    function BackupData(opts) {
        let oldCacheBucket, backupCacheBucket;
        if (opts == 1) {
            //备份旧数据 1
            oldCacheBucket = GMgetStrToJson("lishijilulist");
            backupCacheBucket = oldCacheBucket;
            GMsetJson('backupCacheBucket', oldCacheBucket);
            backupCacheBucket = GMsetJson("backupCacheBucket", backupCacheBucket)
            console.log(GMgetStrToJson('backupCacheBucket'), GM_listValues());
        } else if (opts == 2) {
            // 用备份还原数据 2
            backupCacheBucket = GMgetStrToJson('backupCacheBucket');
            if (backupCacheBucket == undefined | backupCacheBucket == '') {
                console.log('没有备份数据');
            } else {
                GMsetJson('lishijilulist', backupCacheBucket);
            }
        } else if (opts == 3) {
            // 删除旧有数据仅保留备份 3
            GM_deleteValue("lishijilulist");
        }
    }

    
    checkPageUrl();

    // function searchArrayObject(arr, propName, searchStr) {
    //     // 如果搜索字符串包含空格,则将其拆分为多个关键词
    //     const keywords = searchStr.split(' ');

    //     let matchingIndexes = [];

    //     for (let i = 0; i < arr.length; i++) {
    //         let isMatched = true;
    //         const val = arr[i][propName];
    //         if (val) {
    //             // 对于每个关键词,判断当前元素中的指定属性是否包含该关键词
    //             for (let j = 0; j < keywords.length; j++) {
    //                 if (!val.includes(keywords[j])) {
    //                     isMatched = false;
    //                     break;
    //                 }
    //             }
    //             if (isMatched) {
    //                 matchingIndexes.push(i);
    //             }
    //         }
    //     }

    //     console.log("Matching indexes:", matchingIndexes);
    //     return matchingIndexes;
    // }

    // 点击tipsBox下载按钮时执行的函数
    
    function checkPreE() {
        const updateTipsBox = (text) => {
            const tipsBox = document.getElementById("tipsBox");
            if (tipsBox) {
                tipsBox.innerText = text;
                tipsBox.onclick = () => {
                    const preECData = GMgetStrToJson('lishijilulist');
                    if (preECData) {
                        preECData.shift();
                        const csvContent = preECData.map(row => Object.values(row).join(",")).join("\n");
                        const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
                        const link = document.createElement('a');
                        link.download = 'PreEC Data.csv';
                        link.href = window.URL.createObjectURL(blob);
                        link.click();
                    }
                };
                tipsBox.title = text; // 添加 title 属性
            }
        };

        let currentStep = 0;

        const timer = setInterval(() => {
            const result = preEC.getPreE();
            switch (result) {
                case 0:
                    updateTipsBox('界面未启用');
                    break;
                case 10:
                    updateTipsBox('过往追溯中');
                    break;
                case 20:
                    updateTipsBox('过往已溯源');
                    preEC.setPreE(30);
                    currentStep = 30;
                    break;
                case -101:
                    updateTipsBox('请登录帐号');
                    clearInterval(timer);
                    break;
                case 30:
                    updateTipsBox('分析过往中');
                    currentStep = 30;
                    videoTag(GMgetStrToJson('lishijilulist'));
                    break;
                case 40:
                    updateTipsBox('过往已在线');
                    preEC.setPreE(50);
                    currentStep = 50;
                    break;
                case 50:
                    updateTipsBox('点击读入过往');
                    console.log('观看数据', GMgetStrToJson('lishijilulist'));
                    clearInterval(timer);
                    break;
            }
        }, 1000);
    }

    // 页面地址变化时执行的逻辑
    function checkPageUrl() {
        if (window.location.href.includes("bilibili.com")) {
            if (window.location.pathname === "/index.html" || window.location.pathname === "/" || window.location.pathname === "" || window.location.pathname === "index" || window.location.pathname.startsWith("?")) {
                ajaxHistory();
                checkPreE();
                // 调用函数并将返回值赋值给变量
                const maskBox = createMask();
                const popupBox = createPopupBox(maskBox);
                popup(maskBox, popupBox);
            }
            else if (window.location.href.includes("www.bilibili.com/correspond/") || window.location.href.includes("message.bilibili.com/pages/")) {
                // do nothing
            }
        }
    }

    function createMask() {
        // 创建遮罩层元素
        var mask = GM_addElement("div", {
            style: {
                position: "fixed",
                top: "0",
                left: "0",
                width: "100%",
                height: "100%",
                backgroundColor: "rgba(0, 0, 0, 0.5)",
                zIndex: "99",
                display: "none"
            },
        });
        // 将遮罩层添加到 body 中
        document.body.appendChild(mask);
        return mask;
    }

    function createPopupBox(mask) {
        // 创建弹窗元素
        // 创建弹窗元素
        var popup = GM_addElement("div", {
            id: "popupDiv",
            className: "popup",
        });

        // 创建提示区元素
        var tipsBox = GM_addElement("span", {
            id: "tipsBox",
            textContent: "这里是提示区",
        });

        // 创建标题元素
        var title = GM_addElement("h4", {
            textContent: "🐻你生命在哪里展开,哪里就是历史的界面🐻",
        });

        // 创建关闭按钮元素
        var closeBtn = GM_addElement("span", {
            id: "closeBtn",
            textContent: "❌",
        });

        // 将创建的子元素添加到弹窗元素中
        popup.appendChild(tipsBox);
        popup.appendChild(title);
        popup.appendChild(closeBtn);

        // 将弹窗元素添加到 body 中
        document.body.appendChild(popup);

        // 给关闭按钮添加 mouseover 和 mouseout 事件
        GM_addEvent(
            closeBtn,
            "mouseover",
            function () {
                this.style.color = "black";
            }
        );
        GM_addEvent(
            closeBtn,
            "mouseout",
            function () {
                this.style.color = "red";
            }
        );

        // 防止多次点击弹窗按钮
        var clicked = false;

        // 给关闭按钮添加 click 事件
        GM_addEvent(closeBtn, "click", function () {
            mask.style.display = 'none';
            popup.style.display = 'none';
            clicked = false;
            document.querySelector("#popupBtn").classList.remove("disabled");
        });

        // 防止点击弹窗时关闭弹窗
        GM_addEvent(popup, "click", function (event) {
            event.stopPropagation();
        });

        // 点击遮罩层关闭弹窗
        GM_addEvent(mask, "click", function () {
            if (!clicked) {
                mask.style.display = 'none';
                popup.style.display = 'none';
                clicked = false;
                document.querySelector("#popupBtn").classList.remove("disabled");
            }
        });

        document.addEventListener('keydown', function (event) {
            if (event.key === 'Escape') {
                if (!clicked) {
                    mask.style.display = 'none';
                    popup.style.display = 'none';
                    clicked = false;
                    document.querySelector("#popupBtn").classList.remove("disabled");
                }
            }
        });
        return popup;
    }

    function popup(mask, popup) {
        // 创建按钮元素并添加点击事件
        var isFirstClick = true; // 设置为第一次点击按钮
        var btn = GM_addElement("div", {
            id: "popupBtn",
            text: "🐻历史",
            onclick: function () {
                if (!this.classList.contains("disabled")) {
                    this.classList.add("disabled");
                    // 如果是第一次点击,继续加载内容
                    if (isFirstClick) {
                        const lishi = GMgetStrToJson('lishijilulist'); 
                        delete lishi[0];
                        addMessInfo(popup, lishi);
                        //addSearchBox(popup, lishi);
                        addTagBox(popup, lishi);
                        addCardBox(popup, lishi);
                        
                        isFirstClick = false; // 标记为不是第一次点击
                    }
                    mask.style.display = 'block';
                    popup.style.display = 'block';
                }
            },
            style: {
                transition: "transform 0.2s, width 0.2s",
                transformOrigin: "center center",
                cursor: "pointer",
                display: "inline-block" // 添加 display 样式
            }
        });

        // 添加按钮缩放效果
        GM_addEvent(btn, "mouseover", function () {
            this.style.transform = "scale(1.01)"; 
            this.style.width = "10rem";
            this.textContent = "🐻我的时间不见了";
        });
        GM_addEvent(btn, "mouseout", function () {
            this.style.transform = "scale(1)"; 
            this.style.width = "auto";
            this.textContent = "🐻历史";
        });

        document.body.appendChild(btn);
    }

    function addMessInfo(container, lishi) {
        let his = pharseResult(lishi);
        var messInfoBox = GM_addElement("div", {
            style:
                "display: flex; margin: 0 auto; justify-content: center; padding: .2rem; font-size: 1.1rem;",
        });

        var loadingDiv = GM_addElement("div", {
            style: "text-align:center; font-weight:bold;",
            text: "正在获取中...",
        });

        container.appendChild(messInfoBox);
        messInfoBox.appendChild(loadingDiv);

        const info = ["◎    今日", "◎   昨日", "◎   本周", "◎   本月", "◎   上月", "◎   以往"];

        setTimeout(() => {
            loadingDiv.style.display = "none";

            for (let i = 0; i < info.length; i++) {
                var infoDiv = GM_addElement("div", {
                    style: "border:1px solid #000;margin: 0 .5rem;padding:.1rem .2rem;",
                });
                messInfoBox.appendChild(infoDiv);
                infoDiv.append(GM_addElement("div", { text: info[i] }));
                infoDiv.append(GM_addElement("div", { text: his[i].av }));
                infoDiv.append(GM_addElement("div", { text: "00:00:00" }));
            }
        }, 2000);
    }


    function addSearchBox(container,lishi) {
        // 添加搜索框和按钮元素的容器
        var searchDiv = GM_addElement("div", {
            id: "searchDiv"
        });
        // 添加搜索框和按钮元素
        var searchInputKeyword = GM_addElement("input", {
            type: "text",
            className: "searchInput",
            required: true,
            placeholder: "你的记忆中的关键词"
        });
        var searchInputTag = GM_addElement("input", {
            type: "text",
            className: "searchInput",
            placeholder: "你觉得视频应属的标签"
        });
        // 获取当前日期
        const today = new Date().toISOString().split('T')[0];
        var searchInputDateF = GM_addElement("input", {
            type: "date",
            className: "searchDate",
            max: today,
            value: today,
            required: true
        });
        var searchInputDateE = GM_addElement("input", {
            type: "date",
            className: "searchDate",
            max: today,
            value: today,
            required: true
        });
        var searchButton = GM_addElement("span", {
            className: "searchButton",
            textContent: "►"
        });

     
        // 添加搜索框和按钮元素
        var searchTextA = GM_addElement("span", {
            text: "从",
            className: "searchText"
        });
        var searchTextB = GM_addElement("span", {
            text: "到",
            className: "searchText"
        });

        // 将搜索框添加到指定的容器元素中
        container.appendChild(searchDiv);
        searchDiv.appendChild(searchInputKeyword);
        searchDiv.appendChild(searchInputTag);
        searchDiv.appendChild(searchTextA);
        searchDiv.appendChild(searchInputDateF);
        searchDiv.appendChild(searchTextB);
        searchDiv.appendChild(searchInputDateE);
        searchDiv.appendChild(searchButton);
        // 添加点击事件处理程序
        searchButton.addEventListener("click", function (event) {
            event.preventDefault();
            searchButton.classList.add('clicked'); // 添加点击效果的 CSS 类名

            var results;
            //results="功能完成中";
            if (searchInputKeyword.value === ""){
                alert("请输入内容")
            }else if(searchInputDateE>searchInputDateF){
                alert("开始日期不应小于结束日期")
            }else{
                if (searchInputTag === "") {
                    results = searchPosts(lishi, { title: searchInputKeyword.value, startDate: searchInputDateF.value, endDate: searchInputDateE.value})
                }else{
                    results = searchPosts(lishi, { title: searchInputKeyword.value, startDate: searchInputDateF.value, endDate: searchInputDateE.value, tags: searchInputTag.value })
                }
            }
            console.log(results);

            setTimeout(() => {
                searchButton.classList.remove('clicked');
            }, 200); // 移除点击效果的 CSS 类名
        });
        // searchButton.addEventListener('click', function (event) {
        //     event.preventDefault();
        //     searchButton.classList.add('clicked'); // 添加点击效果的 CSS 类名
        //     setTimeout(() => {
        //         searchButton.classList.remove('clicked');
        //     }, 200); // 移除点击效果的 CSS 类名
        // });
    }

    function searchPosts(lishi,query) {
        const { author, title, startDate, endDate, tags } = query;
        var data = lishi;
        return data.filter(post => {
            // 检查作者名称匹配
            if (author && post.author_name.toLowerCase().indexOf(author.toLowerCase()) === -1) {
                return false;
            }

            // 检查标题匹配
            if (title && post.title.toLowerCase().indexOf(title.toLowerCase()) === -1) {
                return false;
            }

            // 检查日期范围匹配
            if (startDate && endDate && (post.date < startDate || post.date > endDate)) {
                return false;
            }

            if (tags && Array.isArray(tags) && tags.length > 0) {
                const postTags = post.tag_name.split(';');
                // 检查是否有不存在的标签
                if (tags.some(tag => !postTags.includes(tag))) {
                    return false;
                }
                const matches = tags.filter(tag => postTags.includes(tag));
                if (matches.length !== tags.length) {
                    return false;
                }
            }


            return true;
        });
    }


    function addCardBox(container, lishio) {
        const cardDiv = document.createElement("div");
        cardDiv.style.display = "flex";
        cardDiv.style.padding = ".2rem";
        cardDiv.style.fontSize = "1.1rem";
        cardDiv.style.overflowX = "auto";
        cardDiv.style.maxWidth = "1000px";

        container.appendChild(cardDiv);

        let lishi = lishio.sort((a, b) => b.view_at - a.view_at); // Sort in descending order

        const batchSize = 10; // Generate cards in batches of 10

        function generateCards(startIndex) {
            for (let i = startIndex; i < startIndex + batchSize; i++) {
                if (i >= lishi.length) return; // Stop generating cards after the last card
                oneCardInfo(cardDiv, lishi, i);
            }
        }

        // Show "Loading..." message while generating the first batch of cards
        cardDiv.innerHTML = "<p>Loading...</p>";

        setTimeout(function () {
            generateCards(0);
            cardDiv.removeChild(cardDiv.firstChild); // Remove the "Loading..." message
        }, 500); // Wait for half a second before generating cards

        // Use requestAnimationFrame for smooth scrolling
        let requestId;
        let delta = 0;

        function scrollDiv() {
            if (!requestId) requestId = window.requestAnimationFrame(updateScrollPosition);
        }

        function updateScrollPosition() {
            cardDiv.scrollLeft += Math.max(-1, Math.min(1, delta)) * 70; // Use the delta value from the wheel event
            delta = 0; // Reset the delta value
            requestId = null;
        }

        // Add wheel event listener to the div
        cardDiv.addEventListener("wheel", function (event) {
            event.preventDefault();
            delta += event.deltaY || -event.detail;
            scrollDiv();
        });

        // Show more cards when scrolling to the end of the container
        cardDiv.addEventListener("scroll", function () {
            if (cardDiv.scrollLeft + cardDiv.offsetWidth >= cardDiv.scrollWidth) {
                const startIndex = cardDiv.children.length;
                generateCards(startIndex);
            }
        });
    }

    async function addTagBox(popup, lishi) {
        const sortedTags = await tagStatsSorted(lishi);
        const tagContainer = document.createDocumentFragment();
        const loadingElement = document.createElement("div");

        // Show the loading spinner before the tags are populated
        loadingElement.innerText = "Loading...";
        loadingElement.setAttribute("style", `
    display: block;
    margin-bottom: 0.5rem;
    text-align: center;
  `);
        tagContainer.appendChild(loadingElement);

        for (let i = 0; i < Math.min(sortedTags.length, sortedTags.length - 1); i++) {
            const tag = sortedTags[i];
            const tagElement = document.createElement("div");
            tagElement.innerText = `${tag.name}: ${tag.count}`;
            tagElement.style.backgroundColor = `hsl(${i * 36}, 75%, 87.5%)`;
            tagElement.classList.add("tags");
            tagContainer.appendChild(tagElement);
        }

        const tagBox = document.createElement("div");
        tagBox.appendChild(tagContainer);
        tagBox.setAttribute("style", `
    background-color: #f5f5f5;
    border-radius: 5px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
    padding: 12px;
    font-size: 14px;
    overflow-y: scroll;
    max-height: 10rem;
    display: flex;
    flex-wrap: wrap;
    ${popup.getAttribute("style")}
  `);

        // Hide the loading spinner once the tags are loaded
        loadingElement.style.display = "none";
        popup.appendChild(tagBox);
    }

    //根据下标生成信息卡片
    async function oneCardInfo(cardDiv, lishi, i) {
        const singleData = lishi[i];
        const cardElement = document.createElement("div");
        cardElement.classList.add("card");

        const authorElementDiv = document.createElement("div");
        const authorElement = document.createElement("a");
        authorElement.href = `https://space.bilibili.com/${singleData.author_mid}`;
        authorElement.target = "_blank";
        authorElement.title = singleData.author_name;
        authorElement.textContent = singleData.author_name;

        const titleElement = document.createElement("a");
        if (singleData.badge === "专栏") { 
            //alert("请复制标题后自行搜索");
            //titleElement.href = `https://www.bilibili.com/read/cv${singleData.oid}`;
        }else if(singleData.badge === ""){
            titleElement.href = `https://www.bilibili.com/av${singleData.oid}`;
        }else{

        }
        titleElement.target = "_blank";
        titleElement.title = singleData.title;
        titleElement.textContent = singleData.title;

        cardDiv.appendChild(cardElement);
        cardElement.appendChild(titleElement);
        cardElement.appendChild(authorElementDiv);
        authorElementDiv.appendChild(authorElement);

        let html = "";
        const tags = await tagStats([singleData]);
        for (let i = 0; i < Math.min(tags.names.length, tags.names.length - 1); i++) {
            const tag = tags.names[i];
            const hue = Math.floor(Math.random() * 360);
            const saturation = Math.floor(Math.random() * 25) + 75;
            const lightness = Math.floor(Math.random() * 15) + 80;
            const bgColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
            html += `<li class='tag' style="background-color: ${bgColor};">${tag}</li>`;
        }

        const tagsElement = document.createElement("ul");
        tagsElement.innerHTML = html;
        tagsElement.title = tags.names;
        tagsElement.classList.add("tags");

        cardElement.appendChild(tagsElement);
    }

    async function tagStatsSorted(lishi, start = 0, end = lishi.length - 1) {
        // 使用 tagStats 函数来获取标签计数数据
        const tagData = await tagStats(lishi, start, end);
        // 使用 sortTagsByFrequency 函数来对标签进行排序,并且返回名称和计数的标签数据对象数组
        const sortedTags = sortTagsByFrequency(tagData);
        // 返回排序后的标签数据对象数组
        return sortedTags;
    }

    function sortTagsByFrequency(tagData) {
        const sortedNames = tagData.names.sort((a, b) => {
            const countDiff = tagData.counts[b] - tagData.counts[a];
            if (countDiff !== 0) {
                return countDiff; // 相同频率的标签按照出现次数排序
            } else {
                return a.localeCompare(b); // 字母顺序排列
            }
        });

        return sortedNames.map(name => ({ name, count: tagData.counts[name] }));
    }

    async function tagStats(lishi, start = 0, end = lishi.length - 1) {
        var tagCounts = {};
        var tagNames = {};

        for (let i = start; i <= end; i++) {
            var tagName = lishi[i]?.tag_name?.trim() || '无数据';

            if (tagName === '无数据') {
                continue;
            }

            var singleTagNames = tagName.split(';');

            singleTagNames.forEach((singleTagName) => {
                singleTagName = singleTagName.trim();
                if (singleTagName === '') return;
                var tagCount = tagCounts[singleTagName] || 0;
                tagCounts[singleTagName] = tagCount + 1;
                tagNames[singleTagName] = true;
            });
        }

        return {
            counts: tagCounts,
            names: Object.keys(tagNames),
        };
    }
  
    //     // 绑定按钮点击事件
    //     searchButton.addEventListener("click", function () {
    //         var query = searchInput.value;
    //         var result = findIndex(query);

    //         // 处理搜索结果
    //         if (result) {
    //             alert(result);
    //         } else {
    //             alert("没有匹配项");
    //         }
    //     });


    // 添加样式
    GM_addStyle(`

.tags {
    display: inline-block;
}
            
.clicked {
  background-color: #e6e6e6;
  box-shadow: inset 0 2px 3px rgba(0,0,0,.2);
}

/* 卡片展示视频信息 */
.card {
  width: 300px;
  border: 1px solid #ccc;
  border-radius: 5px;
  box-shadow: 0 0 10px rgba(0,0,0,0.2);
  margin: .5rem;
  padding: .5rem;
}

.card a {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 19rem;
    display:inline-block;
}

.card .tags {
    list-style: none;
    text-align: left;
    white-space: nowrap; /* 文本不换行 */
    overflow: hidden; /* 隐藏多余文本 */
    text-overflow: ellipsis; /* 使用省略号表示被截断的文本 */
    max-width: 20rem;
}
.card .tags .tag {
    display: inline-block;
    margin-left: .3rem;
    border: 1px solid #000;
}



/* 搜索框 */
#searchDiv {
    text-align:left;
}
#searchDiv .searchInput {
  padding: 8px;
  border: none;
  border-radius: 5px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  font-size: 16px;
  width: 270px;
}
#searchDiv .searchText {
  padding: 8px;
  border: none;
  border-radius: 5px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  font-size: 16px;
}
#searchDiv .searchDate  {
  padding: 8px;
  border: none;
  border-radius: 5px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  font-size: 16px;
  width: 130px;
}

/* 搜索按钮 */
#searchDiv  .searchButton{
  background-color:  #FFFFFF;
  color:  #666666;
  border: none;
  border-radius: 5px;
  padding: 8px;
  margin-left: 5px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  cursor: pointer;
  user-select: none;
  font-size: 1.2rem;
}

#searchDiv  .searchButton:hover {
    background-color: #F5F5F5;
    color: #333333;
    font-size: 1.3rem;
}

#searchDiv  .searchButton:focus {
    outline: none;
}

.popup {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: .2rem;
  background-color: #fff;
  border-radius: 5px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
  z-index: 100;
  font-family: Arial, sans-serif;
  font-size: 16px;
  color: #333;
  text-align: center;
  aspect-ratio: 16 / 9;
  display: none;
  max-width: 1000px;
  margin-top:4rem;
}

.popup h4 {
  font-size: 1.2rem;
  padding: .3rem;
  margin-right: 1.1rem;
  user-select: none;
  display:inline-block;
  border: 1px solid #000;
}

#tipsBox {
  float: left;
  border: 1px solid #000;
  top: .3rem;
  right: .3rem;
  cursor: pointer;
  font-size: 1.2rem;
  font-family: Arial, sans-serif;
  user-select: none;
}

#closeBtn{
  position: absolute;
  top: .3rem;
  right: .3rem;
  cursor: pointer;
  font-size: 1.3rem;
  width: 1.2rem;
  height: 1.2rem;
  font-family: Arial, sans-serif;
  user-select: none;
}

#popupBtn{
  position: fixed;
  top: 50%;
  left: 0;
  background-color: #428bca;
  border: none;
  color: #fff;
  padding: 8px 0;
  font-size: 18px;
  cursor: pointer;
  z-index: 101;
  transform: translateY(-50%);
  width: 4rem;
  text-align: center;
  user-select: none;
  border-radius: .5rem;
}

#popupBtn:hover {
  background-color: #3071a9;
}

#popupBtn.disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

@media only screen and (max-width: 480px) {
  #popupBtn{
    width: auto;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}
`);

})();