Greasy Fork

Greasy Fork is available in English.

🔐 密码填充

为Via设计的第三方密码自动保存/填充工具

当前为 2024-10-06 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         🔐 密码填充
// @namespace    https://ez118.github.io/
// @version      0.2.2
// @description  为Via设计的第三方密码自动保存/填充工具
// @author       ZZY_WISU
// @match        *://*/*
// @license      GPLv3
// @icon         data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgLTk2MCA5NjAgOTYwIj4KICA8cGF0aCBmaWxsPSIjODg4IiBkPSJNMTYwLTQ0MHEtNTAgMC04NS0zNXQtMzUtODVxMC01MCAzNS04NXQ4NS0zNXE1MCAwIDg1IDM1dDM1IDg1cTAgNTAtMzUgODV0LTg1IDM1Wk04MC0yMDB2LTgwaDgwMHY4MEg4MFptNDAwLTI0MHEtNTAgMC04NS0zNXQtMzUtODVxMC01MCAzNS04NXQ4NS0zNXE1MCAwIDg1IDM1dDM1IDg1cTAgNTAtMzUgODV0LTg1IDM1Wm0zMjAgMHEtNTAgMC04NS0zNXQtMzUtODVxMC01MCAzNS04NXQ4NS0zNXE1MCAwIDg1IDM1dDM1IDg1cTAgNTAtMzUgODV0LTg1IDM1WiI+PC9wYXRoPgo8L3N2Zz4=
// @run-at       document-end
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @require      https://unpkg.com/[email protected]/dist/jquery.min.js
// ==/UserScript==


/* =====[ 变量存储 ]===== */

const ICONS = {
    'del': '<svg viewBox="0 0 24 24" width="20px" height="20px"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path></svg>'
};

var savedAccount = [];

/* ====================== */
function Toast(text) {
    try{
        if (typeof(window.via) == "object") window.via.toast(text);
        else if (typeof(window.mbrowser) == "object") window.mbrowser.showToast(text);
        else alert(text);
    }catch{
        alert(text);
    }
}

function hash(str) {
    let hash = 5381;
    for (let i = 0; i < str.length; i++) {
        hash = (hash * 33) ^ str.charCodeAt(i);
    }
    return hash >>> 0;
}

function getHost() {
    return window.location.host;
}

function isLoginPage() {
    let forms = document.getElementsByTagName("form");
    let isLogin = false;
    let formPosition = {x: 0, y: 0};
    let formobj = null;

    Array.prototype.forEach.call(forms, (form) => {
        let hasTextInput = false;
        let hasPasswordInput = false;

        // 获取所有 input 元素
        let inputs = form.getElementsByTagName("input");

        // 检查每个 input 的类型
        Array.prototype.forEach.call(inputs, (input) => {
            if (input.type === "text" || input.type === "email") {
                hasTextInput = true;
            } else if (input.type === "password") {
                hasPasswordInput = true;
            }
        });

        // 如果同时存在 text 和 password 类型的输入框,认为是登录页面
        if (hasTextInput && hasPasswordInput) {
            isLogin = true;

            let rectData = form.getClientRects()[0];
            formPosition.x = rectData.left + rectData.width / 2 - 90;
            formPosition.y = rectData.top + rectData.height - 15;

            formobj = form;
        }
    });

    return { isLogin, x: formPosition.x, y: formPosition.y, obj: formobj };
}

function getFormData(ele){
    let inputs = ele.getElementsByTagName("input");
    let usr = null;
    let psw = null;

    // 检查每个 input 的类型
    Array.prototype.forEach.call(inputs, (input) => {
        if ((input.type === "text" || input.type === "email") && !usr) {
            usr = input;
        } else if (input.type === "password" && !psw) {
            psw = input;
        }
    });

    return {password: psw.value, username: usr.value, psw: psw, usr: usr};
}


function findByKeyValue(array, key, value) {
    /* 在JSON中,以键值匹配项 */
    return array.findIndex(item => item[key] === value);
}

function showPswMgr() {
    if($("#userscript-pswmgrDlg").length > 0) { return; }

    let newAccountList = savedAccount;
    let origAccountList = savedAccount.slice();

    var $optDlg = $('<div>', {
        class: 'userscript-pswmgrDlg',
        id: 'userscript-pswmgrDlg',
        style: 'display:none;'
    }).appendTo('body');

    $optDlg.hide();
    $optDlg.fadeIn(100);

    var listHtml = '';
    $.each(newAccountList, (index, item) => {
        listHtml += `
            <div class="list-item" acid="${item.id}">
                <p class="item-title">${item.username} (${item.host})</p>
                <p class="item-delbtn" acid="${item.id}" title="移除">${ICONS.del}</p>
            </div>`;
    });

    $optDlg.html(`
        <div style="height:fit-content; max-height:calc(80vh - 60px); overflow-x:hidden; overflow-y:auto;">
            <h3>管理</h3>
            <div style="height:fit-content; margin:5px;">
                <p class="subtitle">已保存的账户:</p>
                ` + listHtml + `
            </div>
        </div>
        <div align="right">
            <input type="button" value="取消" class="ctrlbtn" id="userscript-cancelBtn">
            <input type="button" value="保存" class="ctrlbtn" id="userscript-saveBtn">
        </div>
    `);

    $(document).on('click', '.list-item>.item-delbtn', function(e) {
        // 删除账号
        let acid = $(e.target).parent().attr("acid");
        const index = findByKeyValue(newAccountList, 'id', acid);
        if (index !== -1) {
            newAccountList.splice(index, 1);
            $(`.list-item[acid="${acid}"]`).remove();
        }
    });

    $(document).on('click', '#userscript-cancelBtn', function(e) {
        // 对话框 取消
        newAccountList = origAccountList;

        $(document).off('click', '#userscript-saveBtn')
        $(document).off('click', '.list-item>.item-delbtn');

        let $optDlg = $("#userscript-pswmgrDlg");
        $optDlg.fadeOut(100);
        setTimeout(() => {
            $optDlg.remove();

            $(document).off('click', '#userscript-cancelBtn');

            location.reload();
        }, 110);
    });

    $(document).on('click', '#userscript-saveBtn', function(e) {
        // 对话框保存
        GM_setValue('savedAccount', newAccountList);
        //alert("【已保存】请刷新页面以应用更改");
        Toast("已保存,刷新页面以应用更改");

        $(document).off('click', '#userscript-cancelBtn');
        $(document).off('click', '.list-item>.item-delbtn');

        let $optDlg = $("#userscript-pswmgrDlg");
        $optDlg.fadeOut(100);
        setTimeout(() => {
            $optDlg.remove();

            $(document).off('click', '#userscript-saveBtn');
        }, 110);
    });
}


function initEle(form, cx, cy) {
    // 创建搜索栏元素并添加到页面
    var $quickFill = $('<div>', {
        class: 'userscript-quickFill',
        id: 'userscript-quickFill'
    }).appendTo('body');

    let html = '';
    const host = getHost();
    $.each(savedAccount, (index, item) => {
        if(item.host == host) {
            html += `<div class="item" acid="${item.id}">${item.username}</div>`;
        }
    })

    // 设定快速填充栏HTML内容
    $quickFill.append(`
        <font color="#333333" size="small">&nbsp;保存的密码:</font>
        ${html}
        <div class="hideBtn">[隐藏]</div>
    `);

    // 设置快速填充栏 位置
    $("#userscript-quickFill")
        .css("left", cx + "px")
        .css("top", cy + "px");

    // 选择保存过的第一个账号,自动填充到网页
    const formdata = getFormData(form);
    let dataindex = findByKeyValue(savedAccount, 'host', host);
    if (dataindex !== -1){
        formdata.psw.value = savedAccount[dataindex].password;
        formdata.usr.value = savedAccount[dataindex].username;
    }

    // 添加点击事件监听器
    $(document).on('click', '#userscript-quickFill>.item', function(e) {
        const acid = $(e.target).attr("acid");
        const formdata = getFormData(form);
        let dataindex = findByKeyValue(savedAccount, 'id', acid);
        formdata.psw.value = savedAccount[dataindex].password;
        formdata.usr.value = savedAccount[dataindex].username;
    });

    $(document).on('click', '#userscript-quickFill>.hideBtn', function(e) {
        $quickFill.hide();
    });
}

function init() {
    let judgeRes = isLoginPage();

    if(judgeRes.isLogin){
        /* 存储初始化 */
        console.log("【提示】检测到登录页面");
        initEle(judgeRes.obj, judgeRes.x, judgeRes.y);

        $(document).on('submit', judgeRes.obj, () => {
            // 获取表单输入内容
            const formdata = getFormData(judgeRes.obj);
            const newdata = {
                "id": hash(getHost() + formdata.username + formdata.password).toString(),
                "host": getHost(),
                "username": formdata.username,
                "password": formdata.password
            };

            // 检查是否数据重复
            const oldidx = findByKeyValue(savedAccount, "host", newdata.host);
            if(oldidx !== -1 && savedAccount[oldidx] && savedAccount[oldidx].id == newdata.id) {
                return;
            }

            // 如果不是重复账号,则询问是否保存
            let res = window.confirm("【询问】是否保存账号?");
            if(res) {
                // 保存账户
                savedAccount.push(newdata);
                GM_setValue('savedAccount', savedAccount);

                Toast("账号已保存!");
            }
        });
    }
}


/* =====[ 菜单注册 ]===== */
var menu_mgr = GM_registerMenuCommand('⚙️ 管理密码', function () { showPswMgr(); }, 'o');


(function () {
    'use strict';

    if(GM_getValue('savedAccount') == null || GM_getValue('savedAccount') == "" || GM_getValue('savedAccount') == undefined){ GM_setValue('savedAccount', savedAccount); }
    else { savedAccount = GM_getValue('savedAccount'); }

    var websiteThemeColor = "#FFFFFFEE";
    var websiteFontColor = "#000";

    GM_addStyle(`
        body{ -webkit-appearance:none!important; }

        .userscript-quickFill{ user-select:none; background-color:` + websiteThemeColor + `; color:` + websiteFontColor + `; border:1px solid #99999999; padding:2px; font-size:12px; line-height:20px; width:180px; height:fit-content; position:absolute; display:flex; flex-direction:column; overflow:hidden auto; box-sizing:border-box; z-index:100000; font-family:"Hiragino Sans GB","Microsoft YaHei","WenQuanYi Micro Hei",sans-serif; border-radius:5px; box-shadow:0px 0px 5px #666; }
        .userscript-quickFill>.item{ margin:1px 0px; border-radius:5px; padding:5px 9px; width:100%; flex-basis:fit-content; flex-shrink:0; cursor:pointer; background-color:transparent; box-sizing:border-box }
        .userscript-quickFill>.item:hover{ background-color:rgba(128, 128, 128, 0.2); }
        .userscript-quickFill>.hideBtn{ margin:1px 0px; padding:5px 9px; width:100%; flex-basis:fit-content; flex-shrink:0; color:` + websiteFontColor + `; opacity:0.6; font-size:12px; font-weight:bold; box-sizing:border-box; cursor:pointer; }

        .userscript-pswmgrDlg{ user-select:none; background-color:` + websiteThemeColor + `; color:` + websiteFontColor + `; border:1px solid #99999999; position:fixed; top:50%; height:fit-content; left:50%; transform:translateX(-50%) translateY(-50%); width:92vw; max-width:300px; max-height:92vh; padding:15px; border-radius:15px; box-sizing:initial; z-index:100000; box-shadow:0 1px 10px #00000088; font-family:"Hiragino Sans GB","Microsoft YaHei","WenQuanYi Micro Hei",sans-serif; }
        .userscript-pswmgrDlg .ctrlbtn{ border:none; background-color:transparent; padding:8px; margin:0; color:#6d7fb4; cursor:pointer; overflow:hidden; }
        .userscript-pswmgrDlg h3{ margin:5px; margin-bottom:15px; font-size:24px; }
        .userscript-pswmgrDlg .subtitle{ margin:5px 1px; font-size:16px; font-weight:400; }

        .userscript-pswmgrDlg .list-item{ width:calc(100% - 10px); padding:10px 5px; margin:0; display:flex; flex-direction:row; vertical-align:middle; box-sizing:initial; }
        .userscript-pswmgrDlg .list-item:hover{ background-color:#55555555; }
        .userscript-pswmgrDlg .list-item>p{ padding:0; margin:0; font-size:16px; }
        .userscript-pswmgrDlg .list-item>.item-title{ flex-grow:1; margin-left:5px; }

        .userscript-pswmgrDlg .list-item>.item-delbtn{ cursor:pointer; width:25px; }
        .userscript-pswmgrDlg .list-item>.item-delbtn svg{ fill:` + websiteFontColor + `; height:100%; min-height:16px; }
    `);

    init();

    setTimeout(() => {
        if($(".userscript-quickFill").length == 0){
            init();
        }
    }, 1000)

})();