ac-favorite-manager

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

目前為 2019-07-22 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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                                <button id=\"fav-manager-set-export-button\" type=\"button\" class=\"btn btn-default\">エクスポート</button>\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\">\r\n                        <table class=\"table\">\r\n                            <tbody id=\"fav-manager-users-table\">\r\n                            </tbody>\r\n                        </table>\r\n                    </div>\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                            <button id=\"fav-manager-export-all\" type=\"button\" class=\"btn btn-default col-sm-12\">全てエクスポート</button>\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/globalFavSets.js


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

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




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

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

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


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

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

// CONCATENATED MODULE: ./src/files.js
/**
 * ファイルに保存する
 * @param {string} content
 * @param {string} name
 */
function saveFile(content, name) {
    var bom = new Uint8Array([0xef, 0xbb, 0xbf]);
    var blob = new Blob([bom, content], { type: "text/plain" });

    var a = document.createElement("a");
    a.download = name;
    a.target = "_blank";
    a.href = window.URL.createObjectURL(blob);

    // for Firefox
    if (window.URL && window.URL.createObjectURL) {
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
    }
    // for Chrome
    else if (window.webkitURL && window.webkitURL.createObject) {
        a.href = window.webkitURL.createObjectURL(blob);
        a.click();
    }
}

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

// CONCATENATED MODULE: ./src/getTimeStamp.js
/* harmony default export */ var getTimeStamp = (function(){
    const now = new Date();
    return `${now.getFullYear()}${now.getMonth()}${now.getDate()}_${now.getHours()}${now.getMinutes()}${now.getSeconds()}`;
});
// CONCATENATED MODULE: ./src/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 userNameInputSelector = external_jQuery_("#fav-manager-add-user-input", 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 src_globalFavSets.isActive){
        setSelectSelector.append(`<option value="${E(key)}">${E(key)}${src_globalFavSets.isActive[key] ? "" : "(無効)"}</option>`);
    }
    setSelectedSet(selected);
}

function updateTable() {
    usersTableSelector.empty();
    appendRow();
    src_globalFavSets.sets[getSelectedSet()].forEach((user) => {
        if (usersTableSelector[0].lastElementChild.children.length === 3)
            appendRow();
        external_jQuery_(usersTableSelector[0].lastElementChild).append(
            `<td class="col-sm-4"><span>${E(user)}</span><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 updateView() {
    console.log(src_globalFavSets);
    const selectedSet = getSelectedSet();
    toggleSetActivenessButtonSelector.text(src_globalFavSets.isActive[selectedSet] ? "無効にする" : "有効にする");
    setDeleteButtonSelector.text(favSets.isSpecialSet(selectedSet) ? "クリア" : "セット削除");
    toggleSetActivenessButtonSelector.prop("disabled", selectedSet === "blacklist");
    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);

    external_jQuery_("#fav-manager-export-all", modalNode).click(() => {
        saveFile(favSets.stringify(src_globalFavSets), `all-favsets-${getTimeStamp()}.json`);
    });
    external_jQuery_("#fav-manager-set-export-button", modalNode).click(() => {
        const key = getSelectedSet();
        let exportSets = new favSets();
        exportSets.sets[key] = src_globalFavSets.sets[key];
        exportSets.isActive[key] = true;
        saveFile(favSets.stringify(exportSets, false), `favset-${key}-${getTimeStamp()}.json`);
    });
    setSelectSelector.change(updateView);
    setDeleteButtonSelector.click(() => {
        const key = getSelectedSet();
        if (favSets.isSpecialSet(key)) {
            src_globalFavSets.sets[key] = new Set();
        }
        else{
            delete src_globalFavSets.sets[key];
            delete src_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");
        src_globalFavSets.sets[key].delete(userName);
        storeFavs();
        updateView();
    });
    external_jQuery_("#fav-manager-add-set-button", modalNode).click(() => {
        const newSetName = setNameInputSelector.val();
        if (newSetName) {
            src_globalFavSets.createNewSet(newSetName);
            updateSelector();
            setSelectedSet(newSetName);
        }
        setNameInputSelector.val("");
        storeFavs();
        updateView();
    });
    external_jQuery_("#fav-manager-add-user-button", modalNode).click(() => {
        src_globalFavSets.sets[getSelectedSet()].add(userNameInputSelector.val());
        userNameInputSelector.val("");
        storeFavs();
        updateView();
    });
    toggleSetActivenessButtonSelector.click(() => {
        const selectedSet = getSelectedSet();
        src_globalFavSets.setActive(selectedSet, !src_globalFavSets.isActive[selectedSet]);
        storeFavs();
        updateView();
    });
    selectImportFileButtonSelector.change((event) => {
        let files = event.target.files;
        let reader = new FileReader();
        reader.onload = (readerEvent) => {
            try{
                let parsedSets = favSets.parse(readerEvent.target.result);
                src_globalFavSets.mergeWith(parsedSets);
                storeFavs();
                updateView();
            }
            catch{
                console.log("failed to load");
            }
        };
        for (let i = 0; i < files.length; i++){
            reader.readAsText(files[i]);
        }
    });
    external_jQuery_("#fav-manager-dropdown-button", dropdownNode).click(updateView);
    modalNode.ready(updateView);
});

// CONCATENATED MODULE: ./src/main.js
// ==UserScript==
// @name        ac-favorite-manager
// @namespace   https://atcoder.jp/
// @version     1.0.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"){
    injectFavHandler();
    generateElement();
}
else{
    let elem = $("<div style=\"text-align: center;\"><a style=\"color: gray;\" href=\"#\" onclick=\"return false;\">お気に入り用のファイルをダウンロード</a></div>");
    $("table").before(elem);
    $("a", elem).click(() => {
        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);
            });
            saveFile(favSets.stringify(exportSets, false), `circles-${circleName}-${getTimeStamp()}.json`);
        });
    });
}

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