ac-favorite-manager

AtCoderのお気に入りの管理を行います。

当前为 2019-07-28 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// define getter function for harmony exports
/******/ 	__webpack_require__.d = function(exports, name, getter) {
/******/ 		if(!__webpack_require__.o(exports, name)) {
/******/ 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ 		}
/******/ 	};
/******/
/******/ 	// define __esModule on exports
/******/ 	__webpack_require__.r = function(exports) {
/******/ 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 		}
/******/ 		Object.defineProperty(exports, '__esModule', { value: true });
/******/ 	};
/******/
/******/ 	// create a fake namespace object
/******/ 	// mode & 1: value is a module id, require it
/******/ 	// mode & 2: merge all properties of value into the ns
/******/ 	// mode & 4: return value when already ns object
/******/ 	// mode & 8|1: behave like require
/******/ 	__webpack_require__.t = function(value, mode) {
/******/ 		if(mode & 1) value = __webpack_require__(value);
/******/ 		if(mode & 8) return value;
/******/ 		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ 		var ns = Object.create(null);
/******/ 		__webpack_require__.r(ns);
/******/ 		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ 		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ 		return ns;
/******/ 	};
/******/
/******/ 	// getDefaultExport function for compatibility with non-harmony modules
/******/ 	__webpack_require__.n = function(module) {
/******/ 		var getter = module && module.__esModule ?
/******/ 			function getDefault() { return module['default']; } :
/******/ 			function getModuleExports() { return module; };
/******/ 		__webpack_require__.d(getter, 'a', getter);
/******/ 		return getter;
/******/ 	};
/******/
/******/ 	// Object.prototype.hasOwnProperty.call
/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";
/******/
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = 3);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {

module.exports = jQuery;

/***/ }),
/* 1 */
/***/ (function(module, exports) {

module.exports = "<li><a id=\"fav-manager-dropdown-button\" data-toggle=\"modal\" data-target=\"#modal-fav-manager\" style=\"cursor : pointer;\"><span class=\"glyphicon glyphicon-star\" aria-hidden=\"true\"></span> お気に入り管理</a></li>";

/***/ }),
/* 2 */
/***/ (function(module, exports) {

module.exports = "<div id=\"modal-fav-manager\" class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\r\n\t<div class=\"modal-dialog\" role=\"document\">\r\n\t\t<div class=\"modal-content\">\r\n\t\t\t<div class=\"modal-header\">\r\n\t\t\t\t<button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-label=\"Close\"><span aria-hidden=\"true\">×</span></button>\r\n\t\t\t\t<h4 class=\"modal-title\">お気に入り管理</h4>\r\n\t\t\t</div>\r\n\t\t\t<div class=\"modal-body\">\r\n                <div class=\"container-fluid\">\r\n                    <div class=\"row\">\r\n                        <div class=\"form-inline\">\r\n                            <div class=\"col-sm-12 form-group\">\r\n                                <label for=\"fav-manager-set-select\">セット: </label>\r\n                                <select id=\"fav-manager-set-select\" class=\"form-control\" data-placeholder=\"default\" data-allow-clear=\"false\" style=\"width: 30%;\">\r\n                                    <option value=\"default\">default</option>\r\n                                    <option value=\"blacklist\">blacklist</option>\r\n                                </select>\r\n                                <button id=\"fav-manager-toggle-set-activeness-button\" type=\"button\" class=\"btn btn-default\"></button>\r\n                                <a id=\"fav-manager-set-export-button\" type=\"button\" class=\"btn btn-default\" target=\"_blank\">エクスポート</a>\r\n                                <button id=\"fav-manager-set-delete-button\" type=\"button\" class=\"btn btn-danger pull-right\">削除</button>\r\n                            </div>\r\n                        </div>\r\n                    </div>\r\n                    <div class=\"row\" style=\"max-height: 500px; overflow-y: auto; overflow-x: hidden\">\r\n                        <table class=\"table\">\r\n                            <tbody id=\"fav-manager-users-table\">\r\n                            </tbody>\r\n                        </table>\r\n                    </div>\r\n                    <p></p>\r\n                    <div class=\"row\">\r\n                        <div class=\"col-sm-6\">\r\n                            <div class=\"input-group\">\r\n                                <span class=\"input-group-addon\">セット名</span>\r\n                                <input id=\"fav-manager-add-set-input\" type=\"text\" class=\"form-control\" placeholder=\"サークル\">\r\n                                <span class=\"input-group-btn\">\r\n                                    <button id=\"fav-manager-add-set-button\" type=\"button\" class=\"btn btn-default\">作成</button>\r\n                                </span>\r\n                            </div>\r\n                        </div>\r\n                        <div class=\"col-sm-6\">\r\n                            <div class=\"input-group\">\r\n                                <span class=\"input-group-addon\">ユーザ名</span>\r\n                                <input id=\"fav-manager-add-user-input\" type=\"text\" class=\"form-control\" placeholder=\"tourist\">\r\n                                <span class=\"input-group-btn\">\r\n                                    <button id=\"fav-manager-add-user-button\" type=\"button\" class=\"btn btn-default\">追加</button>\r\n                                </span>\r\n                            </div>\r\n                        </div>\r\n                    </div>\r\n                    <p></p>\r\n                    <div class=\"row\">\r\n                        <label class=\"col-sm-6\">\r\n                            <span class=\"btn btn-primary col-sm-12\">ファイルからインポート<input id=\"fav-manager-select-import-file-button\" type=\"file\" style=\"display:none\" accept=\".json\" multiple=\"\"></span>\r\n                        </label>\r\n                        <label class=\"col-sm-6\">\r\n                            <a id=\"fav-manager-export-all-button\" type=\"button\" target=\"_blank\" class=\"btn btn-default col-sm-12\">全てエクスポート</a>\r\n                        </label>\r\n                    </div>\r\n                </div>\r\n\t\t\t</div>\r\n\t\t\t<div class=\"modal-footer\">\r\n\t\t\t\t<button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">閉じる</button>\r\n\t\t\t</div>\r\n\t\t</div>\r\n\t</div>\r\n</div>";

/***/ }),
/* 3 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);

// CONCATENATED MODULE: ./src/favs.js

class favSets{
    constructor(){
        this.sets = {};
        this.isActive = {};
    }

    initialize(){
        this.sets = {default:new Set(),blacklist:new Set()};
        this.isActive = {default:true,blacklist:true};
    }

    createNewSet(key){
        if (typeof(key) !== "string") throw new Error(`set名 ${JSON.stringify(key)} は文字列型ではありません`);
        if (this.sets[key]) throw new Error(`set名 ${key} は既に存在しています`);
        this.sets[key] = new Set();
        this.isActive[key] = true;
    }

    setActive(key, activeness){
        if (typeof(key) !== "string") throw new Error(`set名 ${JSON.stringify(key)} は文字列型ではありません`);
        if (key === "blacklist") throw Error(`set ${key} の有効値は変更できません`);
        if (!this.isActive.hasOwnProperty(key)) throw Error(`set名 ${key} は存在していません`);
        this.isActive[key] = !!(activeness);
    }

    mergeWith(newSets){
        for (const key in newSets.sets){
            if (this.sets.hasOwnProperty(key)) {
                newSets.sets[key].forEach((user) => {
                    this.sets[key].add(user);
                });
                this.isActive[key] |= !!newSets.isActive[key];
            }
            else{
                this.sets[key] = newSets.sets[key];
                this.isActive[key] = !!newSets.isActive[key];
            }
        }
    }

    //defaultは常に有効、blacklistは常に無効
    get favSet() {
        let a = [];
        for (const key in this.isActive){
            if (!this.isActive[key]) continue;
            a.push(...this.sets[key]);
        }
        let set = new Set(a);
        this.sets["blacklist"].forEach((user) => {
            set.delete(user);
        });
        return set;
    }

    /**
     * favSetsをJSON化する
     * @param {favSets} favSets
     * @param {boolean} containActivenessData
     * @return {string}
     */
    static stringify(favSets, containActivenessData = true){
        let res = [];
        for (const key in favSets.isActive){
            let data = {name: key, users: [...favSets.sets[key]]};
            if (containActivenessData) data.isActive = favSets.isActive[key];
            res.push(data);
        }
        return JSON.stringify(res);
    }

    /**
     * JSONからfavSetsを復元する
     * @param {string} json
     * @return {favSets}
     */
    static parse(json){
        let sets = new favSets();
        JSON.parse(json).forEach((elem) => {
            if (!elem.hasOwnProperty("name")) throw new Error(`key "name" がオブジェクト ${JSON.stringify(elem)} に存在しません`);
            if (!elem.hasOwnProperty("users")) throw new Error(`key "users" がオブジェクト ${JSON.stringify(elem)} に存在しません`);
            if (typeof(elem.name) !== "string") throw new Error(`key "name" の値 (${JSON.stringify(elem.name)}) は文字列型でありません`);
            if (!Array.isArray(elem.users)) throw new Error(`key "users" の値 (${JSON.stringify(elem.users)}) は配列ではありません`);

            sets.sets[elem.name] = arrayToSet(elem.users);
            sets.isActive[elem.name] = elem.hasOwnProperty("isActive") ? !!(elem.isActive) : true;
        });
        return sets;
    }

    static isSpecialSet(key){
        return key === "default" || key === "blacklist";
    }
}

// CONCATENATED MODULE: ./src/atcoder/globalFavSets.js


let globalFavSets = new favSets();
globalFavSets.initialize();

/* harmony default export */ var atcoder_globalFavSets = (globalFavSets);
// CONCATENATED MODULE: ./src/atcoder/injectFavHandler.js




/* harmony default export */ var injectFavHandler = (function () {
    storeFavs = () => {
        setLS('favmanager-favSets', favSets.stringify(atcoder_globalFavSets));
        setLS('fav', setToArray(favSet = atcoder_globalFavSets.favSet));
    };

    reloadFavs = () => {
        atcoder_globalFavSets.initialize();
        atcoder_globalFavSets.mergeWith(favSets.parse(getLS('favmanager-favSets') || "[]"));
        favSet = atcoder_globalFavSets.favSet;
    };

    toggleFav = (val) => {
        reloadFavs();
        let res;
        if (favSet.has(val)) {
            atcoder_globalFavSets.sets.default.delete(val);
            atcoder_globalFavSets.sets.blacklist.add(val);
            res = false;
        } else {
            atcoder_globalFavSets.sets.default.add(val);
            atcoder_globalFavSets.sets.blacklist.delete(val);
            res = true;
        }
        favSet = atcoder_globalFavSets.favSet;
        storeFavs();
        return res; // has val now
    };


    //migration
    if (!getLS('favmanager-favSets')) {
        getLS('fav').forEach((user) => {
            atcoder_globalFavSets.sets.default.add(user);
        });
        storeFavs();
    }
    else{
        reloadFavs();
    }
});
// EXTERNAL MODULE: ./src/atcoder/html/dropdownElement.html
var dropdownElement = __webpack_require__(1);
var dropdownElement_default = /*#__PURE__*/__webpack_require__.n(dropdownElement);

// EXTERNAL MODULE: ./src/atcoder/html/modal.html
var modal = __webpack_require__(2);
var modal_default = /*#__PURE__*/__webpack_require__.n(modal);

// CONCATENATED MODULE: ./src/files.js
/**
 * ファイルに保存するためのURLを取得
 * @param {string} content
 */
function getObjectURL(content) {
    const bom = new Uint8Array([0xef, 0xbb, 0xbf]);
    const blob = new Blob([bom, content], { type: "text/plain" });
    return window.URL.createObjectURL(blob);
}

// EXTERNAL MODULE: external "jQuery"
var external_jQuery_ = __webpack_require__(0);

// CONCATENATED MODULE: ./src/getTimeStamp.js
function padNumber(n) {
    const padZero = "00"+n;
    return padZero.substring(padZero.length - 2);
}

/* harmony default export */ var getTimeStamp = (function(){
    const now = new Date();
    return `${now.getFullYear()}${padNumber(now.getMonth())}${padNumber(now.getDate())}-${padNumber(now.getHours())}${padNumber(now.getMinutes())}${padNumber(now.getSeconds())}`;
});
// CONCATENATED MODULE: ./src/atcoder/generateElement.js








const modalNode = external_jQuery_(modal_default.a);
const dropdownNode = external_jQuery_(dropdownElement_default.a);

const setSelectSelector = external_jQuery_("#fav-manager-set-select", modalNode);
const usersTableSelector = external_jQuery_("#fav-manager-users-table", modalNode);
const setNameInputSelector = external_jQuery_("#fav-manager-add-set-input", modalNode);
const addSetButtonSelector = external_jQuery_("#fav-manager-add-set-button", modalNode);
const userNameInputSelector = external_jQuery_("#fav-manager-add-user-input", modalNode);
const addUserButtonSelector = external_jQuery_("#fav-manager-add-user-button", modalNode)
const setDeleteButtonSelector = external_jQuery_("#fav-manager-set-delete-button", modalNode);
const selectImportFileButtonSelector = external_jQuery_("#fav-manager-select-import-file-button", modalNode);
const toggleSetActivenessButtonSelector = external_jQuery_("#fav-manager-toggle-set-activeness-button", modalNode);

function getSelectedSet() {
    return setSelectSelector.val();
}

function setSelectedSet(value) {
    setSelectSelector.val(value);
}

function updateSelector() {
    const selected = getSelectedSet();
    setSelectSelector.empty();
    for (const key in atcoder_globalFavSets.isActive){
        setSelectSelector.append(`<option value="${E(key)}">${E(key)}${atcoder_globalFavSets.isActive[key] ? "" : "(無効)"}</option>`);
    }
    setSelectedSet(selected);
}

function updateTable() {
    usersTableSelector.empty();
    appendRow();
    atcoder_globalFavSets.sets[getSelectedSet()].forEach((user) => {
        if (usersTableSelector[0].lastElementChild.children.length === 3)
            appendRow();
        external_jQuery_(usersTableSelector[0].lastElementChild).append(
            `<td class="col-sm-4"><a href="/users/${encodeURIComponent(user)}">${E(user)}</a><a class="fav-manager-user-delete-button pull-right" name="${E(user)}" style="cursor : pointer; user-select: none;">×</a></td>`
        );
    });
    while (usersTableSelector[0].lastElementChild.children.length < 3){
        external_jQuery_(usersTableSelector[0].lastElementChild).append('<td class="col-sm-4"></td>');
    }
    function appendRow() {
        usersTableSelector.append("<tr></tr>");
    }
}

function updateDownloadLink() {
    const setExportElem = external_jQuery_("#fav-manager-set-export-button", modalNode)[0];
    const key = getSelectedSet();
    let exportSets = new favSets();
    exportSets.sets[key] = atcoder_globalFavSets.sets[key];
    exportSets.isActive[key] = true;
    setExportElem.download = `favset-${key}-${getTimeStamp()}.json`;
    setExportElem.href = getObjectURL(favSets.stringify(exportSets, false));

    const allExportElem = external_jQuery_("#fav-manager-export-all-button", modalNode)[0];
    allExportElem.download = `all-favsets-${getTimeStamp()}.json`;
    allExportElem.href = getObjectURL(favSets.stringify(atcoder_globalFavSets));
}

function updateView() {
    const selectedSet = getSelectedSet();
    toggleSetActivenessButtonSelector.text(atcoder_globalFavSets.isActive[selectedSet] ? "無効にする" : "有効にする");
    setDeleteButtonSelector.text(favSets.isSpecialSet(selectedSet) ? "クリア" : "セット削除");
    toggleSetActivenessButtonSelector.prop("disabled", selectedSet === "blacklist");
    updateDownloadLink();
    updateSelector();
    updateTable();
}

window.addEventListener("storage", event => {
    if (event.key !== 'favmanager-favSets') return;
    reloadFavs();
    updateView();
});

/* harmony default export */ var generateElement = (function(){
    external_jQuery_("body").prepend(modalNode);
    external_jQuery_(".navbar-right .dropdown-menu .divider:nth-last-child(2)").before(dropdownNode);
    setSelectSelector.change(updateView);
    setDeleteButtonSelector.click(() => {
        const key = getSelectedSet();
        if (favSets.isSpecialSet(key)) {
            atcoder_globalFavSets.sets[key] = new Set();
        }
        else{
            delete atcoder_globalFavSets.sets[key];
            delete atcoder_globalFavSets.isActive[key];
            updateSelector();
            setSelectedSet("default");
        }
        storeFavs();
        updateView();
    });
    usersTableSelector.on("click", ".fav-manager-user-delete-button", (event) => {
        const key = getSelectedSet();
        const userName = event.target.getAttribute("name");
        atcoder_globalFavSets.sets[key].delete(userName);
        storeFavs();
        updateView();
    });
    setNameInputSelector.keypress(e => {
         if (e.which === 13) addSetButtonSelector.click();
    });
    addSetButtonSelector.click(() => {
        const newSetName = setNameInputSelector.val().trim();
        if (newSetName) {
            atcoder_globalFavSets.createNewSet(newSetName);
            updateSelector();
            setSelectedSet(newSetName);
        }
        setNameInputSelector.val("");
        storeFavs();
        updateView();
    });
    userNameInputSelector.keypress(e => {
        if (e.which === 13) addUserButtonSelector.click();
    });
    addUserButtonSelector.click(() => {
        atcoder_globalFavSets.sets[getSelectedSet()].add(userNameInputSelector.val().trim());
        userNameInputSelector.val("");
        storeFavs();
        updateView();
    });
    toggleSetActivenessButtonSelector.click(() => {
        const selectedSet = getSelectedSet();
        atcoder_globalFavSets.setActive(selectedSet, !atcoder_globalFavSets.isActive[selectedSet]);
        storeFavs();
        updateView();
    });

    let reader = new FileReader();
    reader.onload = (readerEvent) => {
        try{
            let parsedSets = favSets.parse(readerEvent.target.result);
            atcoder_globalFavSets.mergeWith(parsedSets);
            updateSelector();
            setSelectedSet(Object.keys(parsedSets.sets)[0]);
            storeFavs();
            updateView();
        }
        catch (e){
            alert("failed to load");
        }
    };
    selectImportFileButtonSelector.change((event) => {
        let files = event.target.files;
        for (let i = 0; i < files.length; i++){
            reader.readAsText(files[i]);
        }
        selectImportFileButtonSelector.val("");
    });
    external_jQuery_("#fav-manager-dropdown-button", dropdownNode).click(updateView);
    modalNode.ready(updateView);
});

// CONCATENATED MODULE: ./src/atcoder/main.js



/* harmony default export */ var main = (function(){
    injectFavHandler();
    generateElement();
});

// CONCATENATED MODULE: ./src/circles/main.js





/* harmony default export */ var circles_main = (function () {
    const circleName = location.pathname.split('/')[2];
    $.get(`/circles/${circleName}/api`).then(members => {
        let exportSets = new favSets();
        exportSets.sets[circleName] = new Set();
        exportSets.isActive[circleName] = true;
        members.forEach((member) => {
            exportSets.sets[circleName].add(member);
        });
        let elem = $(`<div style="text-align: center;">
    <a style="color: gray;" download="${`circles-${circleName}-${getTimeStamp()}.json`}" href="${getObjectURL(favSets.stringify(exportSets, false))}" target="_blank">
        お気に入り登録用ファイルをダウンロード
    </a>
</div>`);
        $("table").before(elem);
    });
});
// CONCATENATED MODULE: ./src/main.js
// ==UserScript==
// @name        ac-favorite-manager
// @namespace   https://atcoder.jp/
// @version     1.1.0
// @description AtCoderのお気に入りの管理を行います。
// @author      keymoon
// @license     MIT
// @match       https://atcoder.jp/*
// @exclude     https://atcoder.jp/*/json
// @match       http://atcoder-circles.com/circles/*
// @exclude     http://atcoder-circles.com/circles/
// ==/UserScript==




if (location.hostname === "atcoder.jp"){
    main();
}
else if (location.hostname === "atcoder-circles.com"){
    circles_main();
}

/***/ })
/******/ ]);