ExHentai Viewer

manage your favorite tags, enhance searching, improve comic page

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name              ExHentai Viewer
// @namespace         Violentmonkey Scripts
// @description       manage your favorite tags, enhance searching, improve comic page
// @description:zh-CN 管理标签,增强搜索,改进漫画页面
// @match             *://exhentai.org/*
// @match             *://e-hentai.org/*
// @grant             GM_setValue
// @grant             GM_getValue
// @version           1.2.0
// ==/UserScript==
/*
 * To-do:
 *   1. preload images?
 */

const currentVersion = '1.2.0';
const sortFilters = false;
const $ = selector => document.querySelector(selector);
const $$ = selector => document.querySelectorAll(selector);
const create = tag => document.createElement(tag);
let data = {
    custom_filter: GM_getValue('custom_filter', {}),
    script_config: GM_getValue('script_config', {}),
    tag_pref: GM_getValue('tag_pref', { liked_tags: [], disliked_tags: [] })
};

init();

function init() {
    // upgrade and load script config
    loadScriptData();
    // router
    const url = window.location.href;
    if (url.includes('/s/')) {
        // comic page
        EHViewer('s');
    } else if (url.includes('/mpv')) {
        // multi page mode
        EHViewer('mpv');
    } else if (url.includes('/g/')) {
        // gallery page
        handleGallery();
    } else if ($('#searchbox')) {
        // add panel below the searchbox
        handleSearchBox();
    } else if (url.includes('/gallerytorrents.php')) {
        // show magnet link
        showMagnetLink();
    }

    // add EHV setting button
    let ehvSettingBtn = create('div');
    ehvSettingBtn.innerHTML = '<a href="#">EHV Settings</a>';
    ehvSettingBtn.onclick = createSettingPanel;
    $('#nb').append(ehvSettingBtn);
    $('#nb').style.maxWidth = 'max-content';

    // highlight tags
    highlightTags();
}

function loadScriptData() {
    switch (data.script_config.current_version) {
        // don't break, just go through the flow to upgrade script data by version
        case undefined:
        // just install
        default:
            data.script_config.current_version = currentVersion;
            GM_setValue('custom_filter', data.custom_filter);
            GM_setValue('script_config', data.script_config);
    }
}

function changeStyle(css, mode, id = 'ehv-style') {
    let cssEl = $('#' + id);
    if (!cssEl) {
        cssEl = create('style');
        cssEl.type = 'text/css';
        cssEl.id = id;
        document.head.append(cssEl);
    }
    switch (mode) {
        case 'add':
            cssEl.innerHTML += css;
            break;
        case 'replace':
            cssEl.innerHTML = css;
            break;
    }
}

function addBtnContainer() {
    let btnStyle = `
          #ehv-btn-c{
              text-align: center;
              list-style: none;
              position: fixed;
              bottom: 30px;
              right: 30px;
              z-index: 999;
          }
          .ehv-btn {
              line-height: 32px;
              font-size: 16px;
              padding: 2px;
              margin: 5px;
              color: #233;
              position: relative;
              width: 32px;
              height: 32px;
              border: none;
              border-radius: 50%;
              box-shadow: 0 0 3px 0 #0006;
              cursor: pointer;
              user-select: none;
              outline: none;
              background-color: #fff;
              background-repeat: no-repeat;
              background-position: center;
          }
          .ehv-btn:hover{
              box-shadow: 0 0 3px 1px #0005!important;
          }
          .ehv-btn:active {
              top: 1px;
              box-shadow: 0 0 1px 1px #0004!important;
          }`;
    let btnContainer = create('ul');
    btnContainer.id = 'ehv-btn-c';
    document.body.append(btnContainer);
    changeStyle(btnStyle, 'add', 'ehv-btn-c-style');
    return btnContainer;
}

function addBtn(btn, container) {
    let btnEl = create('li');
    if (btn.icon.startsWith('data:image')) {
        btnEl.style.backgroundImage = "url('" + btn.icon + "')";
    } else {
        btnEl.innerHTML = btn.icon;
    }
    btnEl.classList.add('ehv-btn');
    btnEl.addEventListener(btn.event, btn.cb);
    container.append(btnEl);
    return btnEl;
}

function EHViewer(mode) {
    const host = window.location.host;
    const svgIcons = {
        autofit:
            'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="M8 3V5H4V9H2V3H8ZM2 21V15H4V19H8V21H2ZM22 21H16V19H20V15H22V21ZM22 9H20V5H16V3H22V9Z" fill="rgba(120,120,120,1)"></path></svg>',
        zoomIn: 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748ZM10 10V7H12V10H15V12H12V15H10V12H7V10H10Z" fill="rgba(120,120,120,1)"></path></svg>',
        zoomOut:
            'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748ZM7 10H15V12H7V10Z" fill="rgba(120,120,120,1)"></path></svg>',
        prevPage:
            'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="M10.8284 12.0007L15.7782 16.9504L14.364 18.3646L8 12.0007L14.364 5.63672L15.7782 7.05093L10.8284 12.0007Z" fill="rgba(120,120,120,1)"></path></svg>',
        nextPage:
            'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="M13.1714 12.0007L8.22168 7.05093L9.63589 5.63672L15.9999 12.0007L9.63589 18.3646L8.22168 16.9504L13.1714 12.0007Z" fill="rgba(120,120,120,1)"></path></svg>',
        reload: 'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z" fill="rgba(120,120,120,1)"></path></svg>',
        gallery:
            'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="M5.82843 6.99955L8.36396 9.53509L6.94975 10.9493L2 5.99955L6.94975 1.0498L8.36396 2.46402L5.82843 4.99955H13C17.4183 4.99955 21 8.58127 21 12.9996C21 17.4178 17.4183 20.9996 13 20.9996H4V18.9996H13C16.3137 18.9996 19 16.3133 19 12.9996C19 9.68584 16.3137 6.99955 13 6.99955H5.82843Z" fill="rgba(120,120,120,1)"></path></svg>'
    };
    let css = '';
    let btnContainer = addBtnContainer();
    let currentScale = 1;
    let autofitEnable = true;

    switch (mode) {
        case 's':
            css += `
                  #i1{
                      width:98%!important;
                      max-width:98%!important;
                      min-width:800px;
                      background-color: inherit!important;
                      border: none!important
                  }
                  #img{
                      max-width:none!important;
                      max-height:none!important;
                      background-color: #4f535b!important;
                      padding: 8px;
                      border: 1px solid #000;
                      border-radius: 2px;
                  }
                  h1, #i2, #i5, #i6, #i7, .ip, .sn{
                      display:none!important;
                  }
                  ::-webkit-scrollbar{
                      display:none;
                  }
                  html{
                      scrollbar-width: none;
                  }`;
            // add btns
            [
                { icon: svgIcons.autofit, event: 'mousedown', cb: autofit },
                { icon: svgIcons.zoomIn, event: 'mousedown', cb: e => zoomer(zoomInS) },
                { icon: svgIcons.zoomOut, event: 'mousedown', cb: e => zoomer(zoomOutS) },
                { icon: svgIcons.prevPage, event: 'click', cb: prevPage },
                { icon: svgIcons.nextPage, event: 'click', cb: nextPage },
                { icon: svgIcons.reload, event: 'click', cb: e => $('#loadfail').click() },
                { icon: svgIcons.gallery, event: 'click', cb: e => $('div.sb>a').click() }
            ].forEach(v => addBtn(v, btnContainer));
            // handle key events
            document.body.addEventListener('keydown', e => {
                switch (e.key) {
                    case '=':
                        zoomInS(0.1);
                        break;
                    case '-':
                        zoomOutS(0.1);
                        break;
                    case ',':
                        window.scroll(0, 0);
                        break;
                    case '.':
                        window.scroll(0, window.innerHeight);
                        break;
                    case '[':
                        window.scrollBy(0, -window.innerHeight * 0.3);
                        break;
                    case ']':
                        window.scrollBy(0, window.innerHeight * 0.3);
                        break;
                }
            });
            // set scale on page load
            setNewPage();
            // set scale on img load
            let observer = new MutationObserver(setNewPage);
            observer.observe($('#i3'), { attributes: false, childList: true, subtree: false });
            break;
        case 'mpv':
            css += '#pane_images_inner>div{margin:auto;}';
            // add btns
            [
                { icon: svgIcons.zoomIn, event: 'mousedown', cb: e => zoomer(zoomInMpv) },
                { icon: svgIcons.zoomOut, event: 'mousedown', cb: e => zoomer(zoomOutMpv) }
            ].forEach(v => addBtn(v, btnContainer));
            document.body.addEventListener('keydown', e => {
                switch (e.key) {
                    case '=':
                        zoomInMpv(0.1);
                        break;
                    case '-':
                        zoomOutMpv(0.1);
                        break;
                }
            });
            break;
    }

    // add style
    if (data.script_config.hide_button) {
        css += `
              #ehv-btn-c{
                  padding: 80px 30px 30px 80px;
                  bottom: 0!important;
                  right: -80px!important;
                  transition-duration: 300ms;
              }
              #ehv-btn-c:hover{
                  padding: 80px 30px 30px 20px!important;
                  right: 0!important;
              }`;
    }
    css += '.ehv-btn{background-color: #44454B!important;}';
    if (host === 'e-hentai.org') {
        // btn color
        css = css.replaceAll('#44454B', '#D3D0D1');
        // page bg & border (for mode 's')
        css = css.replace('#4f535b', '#EDEBDF').replace('1px solid #000', '1px solid #5C0D12');
    }
    changeStyle(css, 'replace', 'ehv-style');

    // === functions ===
    function autofit() {
        autofitEnable = true;
        const img = $('#img');
        const imgRatio = img.height / img.width;
        const windowRatio = window.innerHeight / window.innerWidth;
        if (imgRatio > windowRatio) {
            // img thinner than window
            currentScale = (window.innerHeight - 25) / img.height;
        } else {
            //img wider than window
            currentScale = (window.innerWidth - 25) / img.width;
        }
        window.scrollTo(0, 0);
        zoom4S();
    }
    function zoomer(cb) {
        // long press to trigger continuous scaling(zoomTimeout > zoomInterval)
        let zoomInterval = -1;
        let zoomTimeout = setTimeout(function () {
            zoomInterval = setInterval(cb, 100);
        }, 800);
        document.addEventListener(
            'mouseup',
            e => {
                if (zoomInterval === -1) {
                    cb(0.1);
                    clearTimeout(zoomTimeout);
                } else {
                    clearInterval(zoomInterval);
                }
            },
            { once: true }
        );
    }
    function prevPage() {
        const currentPage = $('.sn>span:first-child').innerText;
        currentPage !== '1' ? $('#prev').click() : alert('This is first page (⊙_⊙)');
    }
    function nextPage() {
        const currentPage = $('.sn span:first-child').innerText;
        const lastPage = $('.sn span:last-child').innerText;
        currentPage !== lastPage ? $('#next').click() : alert('This is last page (⊙ω⊙)');
    }
    function setNewPage() {
        // add page indicator
        let footer = $('#i4 > div:first-child');
        const currentPage = $('.sn span:first-child').innerText;
        const totalPage = $('.sn span:last-child').innerText;
        footer.innerHTML = currentPage + 'P / ' + totalPage + 'P :: ' + footer.innerText + ' :: ';
        // add origin source
        let imgLink = $('#i7>a');
        imgLink ? footer.append(imgLink) : (footer.innerHTML += 'No download');
        if (autofitEnable) {
            autofit();
        } else {
            // inherit the zoom scale of previous page
            zoom4S();
        }
    }
    function zoomInS(pace = 0.02) {
        autofitEnable = false;
        currentScale = 1 + pace;
        zoom4S();
    }
    function zoomOutS(pace = 0.02) {
        autofitEnable = false;
        const img = $('#img');
        currentScale = 1 - pace;
        zoom4S();
    }
    function zoom4S() {
        const img = $('#img');
        // img.style.width = currentScale * 100 + '%';
        img.style.width = img.width * currentScale + 'px';
        img.style.height = 'auto';
    }
    function zoomInMpv(pace = 0.02) {
        const maxWidth = Number($('#pane_images').style.width.replace('px', '')) - 20;
        const originalWidth = Number($('#image_1').style.maxWidth.replace('px', ''));
        currentScale = originalWidth * (1 + pace) < maxWidth ? currentScale + pace : currentScale;
        zoom4Mpv(originalWidth * currentScale);
    }
    function zoomOutMpv(pace = 0.02) {
        const minWidth = 200;
        const originalWidth = Number($('#image_1').style.maxWidth.replace('px', ''));
        currentScale = originalWidth * (currentScale - pace) > minWidth ? currentScale - pace : currentScale;
        zoom4Mpv(originalWidth * currentScale);
    }
    function zoom4Mpv(width) {
        let mpvStyle =
            'img[id^="imgsrc"], div[id^="image"]{width:mpvWidth!important;height:auto!important; max-width:100%!important;min-width:200px!important}"';
        mpvStyle = mpvStyle.replace('mpvWidth', width + 'px');
        changeStyle(mpvStyle, 'replace', 'custom-width');
    }
}

function handleGallery() {
    // add searchbox
    let searchBox = create('form');
    searchBox.innerHTML = `
          <p class="nopm">
              <input type="text" id="f_search" name="f_search" placeholder="Search Keywords" value="" size="60">
              <input type="submit" name="f_apply" value="Search">
          </p>`;
    searchBox.setAttribute(
        'style',
        'display: none; width: 720px; margin: 10px auto; border: 2px ridge black; padding: 10px;'
    );
    searchBox.setAttribute('action', 'https://' + window.location.host + '/');
    searchBox.setAttribute('method', 'get');
    $('.gm').before(searchBox);

    // add btn to show/hide searchbox
    let tbody = $('#taglist > table > tbody');
    tbody.innerHTML += `
          <tr>
              <td class="tc">EHV:</td>
              <td>
                  <div id="ehv-panel-btn" class="gt" style="cursor:pointer">show panel</div>
              </td>
          </tr>`;
    $('#ehv-panel-btn').addEventListener('click', e => {
        const t = e.target;
        if (t.innerText == 'show panel') {
            searchBox.style.display = 'block';
            t.innerText = 'hide panel';
        } else {
            searchBox.style.display = 'none';
            t.innerText = 'show panel';
        }
    });

    // add panel
    setPanel();

    // add gallery tag to searchbox by right-click
    tbody.addEventListener('contextmenu', applyGalleryTag);

    function applyGalleryTag(e) {
        const t = e.target;
        if (t.tagName === 'A') {
            e.preventDefault();
            const searchInput = $('#f_search');
            let tag = t.href.split('/').pop().replaceAll('+', ' ');
            filter = `"${tag}" `;
            // add tailing space
            if (searchInput.value.length > 0 && searchInput.value.endsWith(' ') === false) searchInput.value += ' ';
            searchInput.value.includes(filter)
                ? (searchInput.value = searchInput.value.replace(filter, ''))
                : (searchInput.value += filter);
        }
    }
}

function handleSearchBox() {
    setPanel();
    const ehvPanel = $('#ehv-panel');
    if (data.script_config.hide_panel) {
        ehvPanel.style.display = 'none';
        const a = create('a');
        a.id = 'ehv-panel-btn';
        a.innerText = '[Show EHV Panel]';
        a.setAttribute('href', '#');
        a.setAttribute('style', 'margin-left: 1em;');
        a.addEventListener('click', function () {
            if (ehvPanel.style.display == 'none') {
                ehvPanel.style.display = 'block';
                a.innerText = '[Hide EHV Panel]';
            } else {
                ehvPanel.style.display = 'none';
                a.innerText = '[Show EHV Panel]';
            }
        });
        $$('#searchbox>form>div')[1].append(a);
    } else {
        $('#ehv-panel-btn') && $('#ehv-panel-btn').remove();
    }
}

function setPanel() {
    const container = $('#f_search').parentNode;
    const searchInput = $('#f_search');

    // set style
    const panelCss = `#ehv-panel > input[type="button"]{ margin: 2px; }`;
    let panelStyle = $('#panel-style');
    if (!panelStyle) {
        panelStyle = create('style');
        panelStyle.id = 'panel-style';
        panelStyle.textContent = panelCss;
        document.head.append(panelStyle);
    }

    let ehvPanel = $('#ehv-panel');

    if (ehvPanel) ehvPanel.remove();

    ehvPanel = document.createElement('div');
    ehvPanel.setAttribute('class', 'nopm');
    ehvPanel.setAttribute('id', 'ehv-panel');
    ehvPanel.setAttribute('style', 'padding-top:8px;');
    container.append(ehvPanel);
    // filter buttons
    for (let key in data.custom_filter) {
        let filterBtn = create('input');
        filterBtn.setAttribute('type', 'button');
        filterBtn.setAttribute('value', key);
        filterBtn.setAttribute('title', data.custom_filter[key].toString());
        filterBtn.addEventListener('click', applyFilter);
        filterBtn.addEventListener('contextmenu', removeFilter);
        if (isExist(data.custom_filter[key])) {
            filterBtn.setAttribute('style', 'filter: invert(20%);');
        } else {
            filterBtn.removeAttribute('style');
        }
        ehvPanel.append(filterBtn);
    }
    //new filter button
    let addFilterBtn = create('input');
    addFilterBtn.setAttribute('type', 'button');
    addFilterBtn.setAttribute('value', '+');
    addFilterBtn.addEventListener('click', addFilter);
    addFilterBtn.addEventListener('contextmenu', ehvSetting);
    ehvPanel.append(addFilterBtn);

    // enhace apply filter button
    const searchBtn = $('#f_search+input');
    searchBtn.addEventListener('contextmenu', newtabSearch);
    searchBtn.title = 'right click to search in new page';

    // === function ===
    function isExist(values) {
        const inputValue = searchInput.value;
        return values.every(v => inputValue.includes(v));
    }
    function applyFilter(e) {
        let t = e.target;
        let key = t.value;
        let values = data.custom_filter[key];

        // add tailing space
        if (searchInput.value.length > 0 && searchInput.value.endsWith(' ') === false) searchInput.value += ' ';

        if (isExist(values)) {
            values.forEach(v => (searchInput.value = searchInput.value.replaceAll(`"${v}" `, '')));
            t.removeAttribute('style');
        } else {
            values.forEach(v => (searchInput.value += `"${v}" `));
            t.setAttribute('style', 'filter: invert(20%);');
        }
    }
    function addFilter() {
        data.custom_filter = GM_getValue('custom_filter', {}); // get latest filter data
        const input = prompt('Add filter like format below', '[tag] or [name@tag] or [name@tag+tag+tag+tag]');
        const filterStr = input ? input.split('@') : '';
        let key, value;
        if (filterStr.length === 2) {
            key = filterStr[0];
            value = filterStr[1].split('+');
            data.custom_filter[key] = value;
        } else if (filterStr.length === 1 && filterStr[0] !== '') {
            key = value = filterStr[0];
            data.custom_filter[key] = [value];
        } else {
            alert('Invalid input...:(');
            return;
        }
        // sort filters by char codepoint
        if (sortFilters === true) {
            const newFilters = {};
            const sortedKeys = Object.keys(data.custom_filter).sort();
            for (let key of sortedKeys) {
                newFilters[key] = data.custom_filter[key];
            }
            data.custom_filter = newFilters;
        }
        GM_setValue('custom_filter', data.custom_filter);
        setPanel();
    }
    function ehvSetting(e) {
        e.preventDefault();
        data.script_config = GM_getValue('script_config', {}); // sync latest config data
        data.script_config.hide_panel = confirm('hide EHV panel by default?');
        data.script_config.hide_button = confirm(
            'hide comic page buttons by default? (hover lower right corner to show)'
        );
        GM_setValue('script_config', data.script_config);
        handleSearchBox();
    }
    function removeFilter(e) {
        e.preventDefault();
        data.custom_filter = GM_getValue('custom_filter', {}); // get latest filter data
        if (confirm('Delete this tag?')) {
            let key = e.target.value;
            delete data.custom_filter[key];
            GM_setValue('custom_filter', data.custom_filter);
            setPanel();
        }
    }
    function newtabSearch(e) {
        e.preventDefault();
        const keyword = e.target.previousElementSibling.value;
        const url = `https://${location.host}/?f_search=${keyword}`;
        open(url);
    }
}

function showMagnetLink() {
    const links = $$('a');
    for (let link of links) {
        if (link.href.endsWith('.torrent')) {
            let magnetLink = create('a');
            let torrentHash = link.href.match(/[\w\d]{40}/)[0];
            magnetLink.href = `magnet:?xt=urn:btih:${torrentHash}&dn=${link.innerText}`;
            magnetLink.innerText = '[MAGNET]';
            link.before(magnetLink);
            let span = create('span');
            span.innerText = ' ';
            link.before(span);
        }
    }
}

function createSettingPanel() {
    // todo:
    // custom filter editor
    // hide ehv search panel by default
    // hide ehv sidebar buttons by default
    console.log('creating ehv setting panel');
    let container = create('div');
    container.innerHTML = `
        <div id="ehv-setting-panel" style="display: flex; flex-direction: column; gap:.5rem; background-color: #CCCE; border-radius: 3px; box-shadow: 0 0 3px 0 #0008; margin: 5rem auto; padding: 1rem; max-width: 50rem;">
            <span style="color:#233; text-align:left; font-size:12px;">liked tags:</span>
            <textarea id="ehv-liked-tags" style="padding: .5em;" placeholder="enter tags you like here, separated by comma"></textarea>
            <span style="color:#233; text-align:left; font-size:12px;">disliked tags:</span>
            <textarea id="ehv-disliked-tags" style="padding: .5em;" placeholder="enter tags you don't like here, separated by comma"></textarea>
            <div style="text-align: right;">
                <button id="ehv-save-btn">save</button>
                <button id="ehv-cancel-btn">cancel</button>
            <div>
        </div>`;
    container.setAttribute(
        'style',
        'background-color: transparent; width: 100vw; height: 100vh; position: fixed; top: 0; left: 0; z-index: 9999'
    );

    const likedTagsInput = container.querySelector('#ehv-liked-tags');
    const dislikedTagsInput = container.querySelector('#ehv-disliked-tags');
    const saveBtn = container.querySelector('#ehv-save-btn');
    const cancelBtn = container.querySelector('#ehv-cancel-btn');

    // get latest tag_pref value
    data.tag_pref = GM_getValue('tag_pref', { liked_tags: [], disliked_tags: [] });

    likedTagsInput.value = data.tag_pref.liked_tags.join(',');
    dislikedTagsInput.value = data.tag_pref.disliked_tags.join(',');

    saveBtn.onclick = () => {
        data.tag_pref.liked_tags = likedTagsInput.value.split(',');
        data.tag_pref.disliked_tags = dislikedTagsInput.value.split(',');
        highlightTags(data);
        GM_setValue('tag_pref', data.tag_pref);
        container.remove();
    };
    cancelBtn.onclick = () => container.remove();

    document.body.append(container);
}

function highlightTags() {
    // highlight tags
    let tagEls = $$('.gt, .gtl');
    const likedTags = data.tag_pref.liked_tags;
    const dislikedTags = data.tag_pref.disliked_tags;

    for (let tagEl of tagEls) {
        let tagStr = tagEl.firstChild.textContent;
        if (likedTags.includes(tagStr)) {
            tagEl.style.backgroundColor = '#CCE8';
            tagEl.class;
        } else if (dislikedTags.includes(tagStr)) {
            tagEl.style.backgroundColor = '#2338';
        } else {
            tagEl.style.backgroundColor = '';
        }
    }
}