// ==UserScript==
// @name         B站一键备注
// @namespace    https://bbs.tampermonkey.net.cn/
// @homepage     https://scriptcat.org/zh-CN/script-show-page/3059
// @version      1.0.3
// @license      AGPL-3.0-or-later
// @description  Bilibili一键备注 | B站备注功能
// @author       pxoxq
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_deleteValue
// @match        https://*.bilibili.com/**
// @match        https://www.bilibili.com/video/**
// @match        https://message.bilibili.com/**
// @run-at       document-idle
// ==/UserScript==

const BPINK = '#FB7299'
const HL_COLOR = '#FB7299'
const HL_CLS = 'pxoxq-memo-hl'
const GRN = '#42BB85'
const LGRN = '#12ef03'
const BBLUE = '#00AEEC'
const ORANGE = '#F85F59'
const EDT_BTN_CLS = 'pxoxq-memo-edit-btn'
const EDT_IPT_CLS = 'pxoxq-memo-edit-input'
const SPACE_UPNAME_CLS = 'pxoxq-space-upname'
const MAX_TIMEOUT = 5 
const HELP_LINK = 'https://scriptcat.org/zh-CN/script-show-page/3059'
const CONF_KEY = {
  importM: 'importM',
  memoM: 'memoM', 
}
const IMPORT_MODES = {
  IGNOER: 0,
  OVERWRITE: 1,
  MERGE: 2,
}
const CONF_INIT = {
  importM: 0,
  memoM: 1,
}
const INLINE_HL = {
  color: 'transparent',
  background: 'linear-gradient(64deg, #1493ff, #ff00af,rgb(207, 117, 0),rgb(0, 209, 35),rgb(157, 11, 255), blue,rgb(253, 106, 22), purple)',
  backgroundClip: 'text',
}
const CM_STYLE = `
.${HL_CLS} {
color: transparent !important;
background: linear-gradient(64deg, #1493ff, #ff00af,rgb(207, 117, 0),rgb(0, 209, 35),rgb(157, 11, 255), blue,rgb(253, 106, 22), purple) !important;
background-clip: text !important;
}
.pxoxq-memo-edit-btn{
background: transparent;
border: none;
outline: none;
color:${BPINK};
margin-left: 3px;
}
.pxoxq-memo-edit-btn:hover{
color:#9200fd;
}
.pxoxq-memo-edit-input{
  border: none;
  outline: none;
  padding:unset;
  color: ${BPINK};
  border-bottom: 1px solid ${BPINK};
}
.pxoxq-space-upname::after{
content: '';
position: absolute;
inset: 0;
z-index: -1;
border-radius: 5px 5px 0 0;
background:rgba(255, 255, 255, 0.75);}
`
async function asyncGetNodeOnce(selector, root, callback){
  root = root || document
  return new Promise((resolve, reject) => {
    elmGetter.each(selector, root, node => {
      if(callback) callback(node)
      resolve(node)
      return false
    })
  })

}

function pxolog(msg, title='pxoxq'){
  if(typeof(msg) == 'string' || typeof(msg) == 'number'){
    console.log(`%c${title}: %c${msg}`, 'background:#000;color:white;', `color:${BPINK};`)
  }else{
    console.log(`%c${title}: %c`, 'background:#000;color:white;', `color:${BPINK};`, msg)
  }
}
function isSameNode(a, b, excludeAtts=[]){
  const aAtts = Array.from(a.attributes).filter(aa=>!excludeAtts.includes(aa.name))
  const bAtts = Array.from(b.attributes).filter(bb=>!excludeAtts.includes(bb.name))
  if(!excludeAtts.includes('innerText') && a.innerText !== b.innerText){
    return false
  }
  if(!excludeAtts.includes('innerHTML') && a.innerHTML !== b.innerHTML){
    return false
  }
  if(aAtts.length !== bAtts.length){
    return false 
  }
  for(let i=0; i<aAtts.length; i++){
    const attName = aAtts[i].name
    if(a.attributes[attName].value !== b.attributes[attName].value){
      return false
    }
  }
  return true
}

class NodeSet{
  constructor({arr = [], excludeAtts = []}){
    this._set = Array.from(arr)
    this.mp = new WeakMap()
    this._removeSame()
    this.excludeAtts = excludeAtts
  }
  has(node){
    if(this._set.includes(node)){
      const _node = this.mp.get(node)
      return isSameNode(node, _node, this.excludeAtts)
    }else{
      return false
    }
  }

  add(node){
    if(!this.has(node)){
      !this._set.includes(node) && this._set.push(node)
      this.mp.set(node, node.cloneNode(true))
    }
  }

  remove(node){
    if(this.has(node)){
      this._set = this._set.filter(item=>!isSameNode(node, item, this.excludeAtts))
      this.mp.delete(node)
    }
  }
  _removeSame(){
    this._set = this._set.filter((item, idx)=> {
      const i = this._set.indexOf(item)
      return i === idx
    })
    this._set.forEach(item => {
      this.mp.set(item, item.cloneNode(true))
    })
  }
}

var elmGetter = function() {
  const win = window.unsafeWindow || document.defaultView || window;
  const doc = win.document;
  const listeners = new WeakMap();
  let mode = 'css';
  let $;
  const elProto = win.Element.prototype;
  const matches = elProto.matches ||
      elProto.matchesSelector ||
      elProto.webkitMatchesSelector || 
      elProto.mozMatchesSelector ||
      elProto.oMatchesSelector;
  const MutationObs = win.MutationObserver ||
      win.WebkitMutationObserver ||
      win.MozMutationObserver;
  function addObserver(target, callback) {
      const observer = new MutationObs(mutations => {
          for (const mutation of mutations) {
              if (mutation.type === 'attributes') {
                  callback(mutation.target);
                  if (observer.canceled) return;
              }
              for (const node of mutation.addedNodes) {
                  if (node instanceof Element) callback(node);
                  if (observer.canceled) return;
              }
          }
      });
      observer.canceled = false;
      observer.observe(target, {childList: true, subtree: true, attributes: true});
      return () => {
          observer.canceled = true;
          observer.disconnect();
      };
  }
  function addFilter(target, filter) {
      let listener = listeners.get(target);
      if (!listener) {
          listener = {
              filters: new Set(),
              remove: addObserver(target, el => listener.filters.forEach(f => f(el)))
          };
          listeners.set(target, listener);
      }
      listener.filters.add(filter);
  }
  function removeFilter(target, filter) {
      const listener = listeners.get(target);
      if (!listener) return;
      listener.filters.delete(filter);
      if (!listener.filters.size) {
          listener.remove();
          listeners.delete(target);
      }
  }
  function query(all, selector, parent, includeParent, curMode) {
      switch (curMode) {
          case 'css':
              const checkParent = includeParent && matches.call(parent, selector);
              if (all) {
                  const queryAll = parent.querySelectorAll(selector);
                  return checkParent ? [parent, ...queryAll] : [...queryAll];
              }
              return checkParent ? parent : parent.querySelector(selector);
          case 'jquery':
              let jNodes = $(includeParent ? parent : []);
              jNodes = jNodes.add([...parent.querySelectorAll('*')]).filter(selector);
              if (all) return $.map(jNodes, el => $(el));
              return jNodes.length ? $(jNodes.get(0)) : null;
          case 'xpath':
              const ownerDoc = parent.ownerDocument || parent;
              selector += '/self::*';
              if (all) {
                  const xPathResult = ownerDoc.evaluate(selector, parent, null, 7, null);
                  const result = [];
                  for (let i = 0; i < xPathResult.snapshotLength; i++) {
                      result.push(xPathResult.snapshotItem(i));
                  }
                  return result;
              }
              return ownerDoc.evaluate(selector, parent, null, 9, null).singleNodeValue;
      }
  }
  function isJquery(jq) {
      return jq && jq.fn && typeof jq.fn.jquery === 'string';
  }
  function getOne(selector, parent, timeout) {
      const curMode = mode;
      return new Promise(resolve => {
          const node = query(false, selector, parent, false, curMode);
          if (node) return resolve(node);
          let timer;
          const filter = el => {
              const node = query(false, selector, el, true, curMode);
              if (node) {
                  removeFilter(parent, filter);
                  timer && clearTimeout(timer);
                  resolve(node);
              }
          };
          addFilter(parent, filter);
          if (timeout > 0) {
              timer = setTimeout(() => {
                  removeFilter(parent, filter);
                  resolve(null);
              }, timeout);
          }
      });
  }
  return {
      get currentSelector() {
          return mode;
      },
      get(selector, ...args) {
          let parent = typeof args[0] !== 'number' && args.shift() || doc;
          if (mode === 'jquery' && parent instanceof $) parent = parent.get(0);
          const timeout = args[0] || 0;
          if (Array.isArray(selector)) {
              return Promise.all(selector.map(s => getOne(s, parent, timeout)));
          }
          return getOne(selector, parent, timeout);
      },
      each(selector, ...args) {
        let parent = typeof args[0] !== 'function' && args.shift() || doc;
        if (mode === 'jquery' && parent instanceof $) parent = parent.get(0);
        const callback = args[0];
        const curMode = mode;
        const refs = new WeakSet();
        for (const node of query(true, selector, parent, false, curMode)) {
            refs.add(curMode === 'jquery' ? node.get(0) : node);
            if (callback(node, false) === false) return;
        }
        const filter = el => {
            for (const node of query(true, selector, el, true, curMode)) {
                const _el = curMode === 'jquery' ? node.get(0) : node;
                if (refs.has(_el)) break;
                refs.add(_el);
                if (callback(node, true) === false) {
                    return removeFilter(parent, filter);
                }
            }
        };
        addFilter(parent, filter);
    },
      feach(selector, opt, ...args) {
        const {excludeAtts = []} = opt
        let parent = typeof args[0] !== 'function' && args.shift() || doc;
        if (mode === 'jquery' && parent instanceof $) parent = parent.get(0);
        const callback = args[0];
        const curMode = mode;
        const refs = new NodeSet({excludeAtts});
        for (const node of query(true, selector, parent, false, curMode)) {
            refs.add(curMode === 'jquery' ? node.get(0) : node);
            if (callback(node, false) === false) return;
        }
        const filter = el => {
            for (const node of query(true, selector, el, true, curMode)) {
                const _el = curMode === 'jquery' ? node.get(0) : node;
                if (refs.has(_el)) break;
                refs.add(_el);
                if (callback(node, true) === false) {
                    return removeFilter(parent, filter);
                }
            }
        };
        addFilter(parent, filter);
      },
      create(domString, ...args) {
          const returnList = typeof args[0] === 'boolean' && args.shift();
          const parent = args[0];
          const template = doc.createElement('template');
          template.innerHTML = domString;
          const node = template.content.firstElementChild;
          if (!node) return null;
          parent ? parent.appendChild(node) : node.remove();
          if (returnList) {
              const list = {};
              node.querySelectorAll('[id]').forEach(el => list[el.id] = el);
              list[0] = node;
              return list;
          }
          return node;
      },
      selector(desc) {
          switch (true) {
              case isJquery(desc):
                  $ = desc;
                  return mode = 'jquery';
              case !desc || typeof desc.toLowerCase !== 'function':
                  return mode = 'css';
              case desc.toLowerCase() === 'jquery':
                  for (const jq of [window.jQuery, window.$, win.jQuery, win.$]) {
                      if (isJquery(jq)) {
                          $ = jq;
                          break;
                      };
                  }
                  return mode = $ ? 'jquery' : 'css';
              case desc.toLowerCase() === 'xpath':
                  return mode = 'xpath';
              default:
                  return mode = 'css';
          }
      }
  };
}();

function fixUrl(url){
    const currUrl = new URL(window.location.href) ;
    if(url.startsWith('//')){
      return `${currUrl.protocol}${url}`;
    }else if(url.startsWith('/')){
      return `${currUrl.origin}${url}`;
    }else{
      return url;
    }
}
function dateTimeNow(){
  const now = new Date()
  const year = now.getFullYear()
  const month = (now.getMonth() + 1).toString().padStart(2, '0')
  const day = now.getDate().toString().padStart(2, '0')
  const hour = now.getHours().toString().padStart(2, '0')
  const minute = now.getMinutes().toString().padStart(2, '0')
  return `${year}-${month}-${day}_${hour}:${minute}`
}

class BMemo {
  constructor(bid, nickname, memo, avatar, info='') {
    this.bid = bid
    this.nickname = nickname
    this.memo = memo
    this.avatar = avatar
    this.info = info
  }
}

class MemoService{

  static getCllKey(bid){
    return `bmemo${String(bid)[0]}`
  }

  static getMemo({bid='', nickname=''}){
    if(bid){
      const memo = this.getMemoByid(bid)
      if(nickname && memo && memo.nickname !== nickname){
        memo.nickname = nickname
        this.setMemo(memo)
      }
      return memo
    }else if(nickname){
      return this.getMemoByNickname(nickname)
    }else{
      return ''
    }
  }

  static getMemoByid(bid){
    const memos = GM_getValue(this.getCllKey(bid), {})
    return memos[bid] || ''
  }

  static getMemoByNickname(nickname){
    for(let i=1;i<10;i++){
      const memos = GM_getValue(`bmemo${i}`, {})
      for(const bid in memos){
        if(memos[bid].nickname === nickname){
          return memos[bid]
        }
      }
    }
    return ''
  }

  static setMemo(memo){
    const ckey = this.getCllKey(memo.bid)
    const memos = GM_getValue(ckey, {})
    memos[memo.bid] = {...memos[memo.bid], ...memo}
    GM_setValue(ckey, memos)
  }

  static deleteMemo(bid){
    const ckey = this.getCllKey(bid)
    const memos = GM_getValue(ckey, {})
    delete memos[bid]
    GM_setValue(ckey, memos)
  }

  static getAllMemos(){
    let memos = {}
    for(let i = 1; i < 10; i++){
      const memo = GM_getValue(`bmemo${i}`, {})
      Object.assign(memos, memo)
    }
    return memos
  }

}

class ConfService{
  static getConf(key){
    const conf = GM_getValue('bmemoConf', {})
    return conf[key] || null
  }

  static setConf(key, value){
    const conf = GM_getValue('bmemoConf', {})
    conf[key] = value
    GM_setValue('bmemoConf', conf)
  }

  static getAllConf(){
    return GM_getValue('bmemoConf', null)
  }
}

class BMemoUtils{

  static getUserShow({nickname, memo}){
    if(!memo){
      return nickname
    }
    return this.userNameShow({nickname, memo}) || nickname
  }

  static userNameShow({nickname, memo}){
    const mMode = ConfService.getConf(CONF_KEY.memoM) ?? CONF_INIT.memoM
    if(mMode == 0){
      return ''
    }else if(mMode == 1){
      return `${memo}(${nickname})`
    }else if(mMode == 2){
      return `${nickname}(${memo})`
    }else if(mMode == 3){
      return memo
    }
  }

  static saveMemo({bid, nickname, memo, avatar='', info = ''}){
    if(bid && nickname){
      if(memo){
        MemoService.setMemo({bid, nickname, memo, avatar, info})
        return 1
      }else{
        MemoService.deleteMemo(bid)
        return -1
      }
    }
    return 0
  }

  static importMemos(memos, mode){
    const dataStatus = testData(memos)
    if(!dataStatus){
      return null
    }else{
      let failCnt = 0
      let successCnt = 0

      if(dataStatus == 1){
        memos.forEach(memo => {
          const bid = memo.bid || ''
          const nickname = memo.nick_name || ''
          const mm = memo.bname || ''
          if(bid && mm){
            saveOneM({bid, nickname, memo: mm})
            successCnt++
          }else{
            failCnt++
          }
        })
      }else if(dataStatus == 2){
        for(const k in memos){
          const memo = memos[k]
          const bid = memo.bid || ''
          const nickname = memo.nickname || ''
          const mm = memo.memo || ''
          if(bid && mm){
            saveOneM({bid, nickname, memo: mm})
            successCnt++
          }else{
            failCnt++
          }
        }
      }
      return {successCnt, failCnt}
    } 

    function saveOneM(memo){
      if(mode == 1){
        BMemoUtils.saveMemo(memo)
      }else{
        const _memo = MemoService.getMemoByid(memo.bid)
        if(_memo){
          if(mode == 2){
            BMemoUtils.saveMemo({..._memo, ...memo})
          }
        }else{
          BMemoUtils.saveMemo(memo)
        }
      }
    }

    function testData(data){
      if(Array.isArray(data)){
        for(let d of data){
          if(typeof d != 'object'){
            alert("数据列表中数据项格式错误")
            return 0
          }
        }
        return 1
      }else if(typeof data == 'object'){
        for(const k in data){
          if(typeof data[k] != 'object'){
            alert("数据列表中数据项格式错误")
            return 0
          }
        }
        return 2
      }else{
        alert("数据格式错误")
        return 0
      }
    }
  }
}

class BSpaceUI{

  static init(){
    this.relationsHandler()
    this.nicknameHandler()
    this.favideosHandler()
  }
  static relationsHandler(){
    elmGetter.each('.relation-content .items .item .relation-card-info > a', async (user)=>{
      const newWrap = document.createElement('div')
      for(let i=0;i<user.attributes.length;i++){
        const attr = user.attributes[i]
        if(attr.name === 'href' || attr.name === 'target') continue
        newWrap.setAttribute(attr.name, attr.value)
      }
      newWrap.style.display = `flex`
      newWrap.innerHTML = user.innerHTML
      const nameDiv = newWrap.childNodes[0]
      const bid = new URL(user.href)?.pathname?.split('/')?.pop()
      const nickname = nameDiv.innerText
      let avatar = ''
      const aImg = await asyncGetNodeOnce('.relation-card-avatar picture img', user.closest('.relation-card'))
      if(aImg){
        avatar = aImg.src
      }

      const mInput = document.createElement('input')
      nameDiv.style.cssText = `transition: all 0.2s linear;`
      mInput.style.cssText = `transition: all 0.2s linear;`
      mInput.classList.add(EDT_IPT_CLS)
      let memo = MemoService.getMemoByid(bid)
      const nameShow = BMemoUtils.getUserShow({nickname, memo: memo.memo})
      nameDiv.innerText = nameShow
      if(memo){
        nameDiv.classList.add(HL_CLS)
        if(memo.nickname != nickname || memo.avatar != avatar){
          memo = {...memo, nickname, avatar}
          BMemoUtils.saveMemo(memo)
        }
      }
      mInput.type = 'text'
      mInput.maxLength = 20
      mInput.style.cssText = `width: 0;`

      nameDiv.addEventListener('click', ()=>{
        memo = MemoService.getMemoByid(bid)
        mInput.value = memo?.memo? memo.memo: nickname
        mInput.style.width = '130px'
        mInput.style.maxWidth = '160px'
        mInput.focus()
        nameDiv.style.width = '0'
        nameDiv.style.height = '0'
      })

      mInput.addEventListener('blur', ()=>{
        if(!mInput.readOnly){
          handleSave()
        }
      })
      mInput.addEventListener('keydown', (e)=>{
        if(e.code === 'Enter'){
          handleSave()
        }
      })

      function handleSave(){
        mInput.style.width = '0'
        nameDiv.style.width = 'auto'
        nameDiv.style.height = 'auto'
        let m = mInput.value?.trim()
        m = m == nickname ? '' : m
        const _memo = new BMemo(bid, nickname, m, avatar)
        memo = _memo
        const res = BMemoUtils.saveMemo(_memo)
        if(res == 1){
          nameDiv.classList.add(HL_CLS)
        }else{
          nameDiv.classList.remove(HL_CLS)
        }
        nameDiv.innerText = BMemoUtils.getUserShow({nickname, memo: memo.memo})
      }
      
      newWrap.appendChild(mInput)
      
      user.insertAdjacentElement('beforebegin', newWrap)
      user.remove()
    })
  }
  static nicknameHandler() {
    elmGetter.each('div.upinfo__main > div.upinfo-detail > div.upinfo-detail__top > div.nickname', async user => {
      const nickname = user.innerText
      const bid = new URL(location.href)?.pathname?.split('/')?.[1]
      let avatar = ''
      const aImg = await asyncGetNodeOnce(`div.upinfo__main > div.upinfo-avatar .b-avatar picture > img`)
      if(aImg){
        avatar = aImg.src
      }
      const mInput = document.createElement('div')
      mInput.classList.add(SPACE_UPNAME_CLS)
      mInput.style.cssText = `padding: 6px 24px;position:relative;border:none;outline:none;border-bottom: 2px solid ${BPINK};font-size: 23px;text-align: center;border-radius: 8px 8px 0 0;font-weight: 600;`
      mInput.contentEditable = false 
      let memo = MemoService.getMemo({bid, nickname})
      let nameShow = BMemoUtils.getUserShow({memo: memo?.memo, nickname})
      mInput.innerText = nameShow
      mInput.title = nameShow
      let isSaving = false
      if(memo){
        mInput.classList.add(HL_CLS)
        if(memo.nickname != nickname || memo.avatar != avatar){
          memo = {...memo, nickname, avatar}
          BMemoUtils.saveMemo(memo)
        }
      }else{
        mInput.classList.remove(HL_CLS)
      }
      mInput.addEventListener('click', ()=>{
        if(!mInput.contentEditable || mInput.contentEditable == 'false'){
          memo = MemoService.getMemo({bid, nickname})
          mInput.contentEditable = true
          mInput.focus()
          mInput.classList.remove(HL_CLS)
          mInput.innerText = memo?.memo ? memo.memo: nickname
          mInput.style.color = BPINK
        }
      })
      mInput.addEventListener('blur', ()=>{
        if(!isSaving){
          handleSave()
        }
      })
      mInput.addEventListener('keydown', (e)=>{
        if(e.code === 'Enter'){
          isSaving = true
          handleSave()
        }
      })
      user.insertAdjacentElement('beforebegin', mInput)
      user.remove()
      function handleSave(){
        mInput.contentEditable = false 
        let m = mInput?.innerText?.trim()
        m = m == nickname ? '' : m
        let _memo = new BMemo(bid, nickname, m, avatar)
        memo = _memo
        const res = BMemoUtils.saveMemo(_memo)
        if(res == 1){
          mInput.classList.add(HL_CLS)
          nameShow = BMemoUtils.userNameShow({nickname, memo: _memo.memo}) 
          mInput.innerText = nameShow 
          mInput.title = nameShow
        }else{
          mInput.classList.remove(HL_CLS)
          mInput.innerText = nickname
          mInput.title = nickname
        }
        isSaving = false
      }
    })
  }
  static favideosHandler() {
    elmGetter.each('.fav-list-main .items div.bili-video-card__subtitle > a', async user => {
      const bid = new URL(user.href)?.pathname?.split('/')?.pop()
      const contentSpan = user.querySelector('div:nth-child(2) > span')
      let contentL = contentSpan.innerText.split(' · ')
      const nickname = contentL[0]
      const memo = MemoService.getMemo({bid, nickname})
      if(memo){
        const nameShow = BMemoUtils.getUserShow({nickname, memo: memo?.memo})
        contentSpan.innerHTML = `${nameShow} · ${contentL[1]}`
        contentSpan.classList.add(HL_CLS)
      }else{
        contentSpan.classList.remove(HL_CLS)
      }
    })
  }
}

class IndexUI{

  static init(){
    this.vCardHandler()
    this.favideosHandler()
    this.dynamicHandler()
    this.historyHandler()
  }
  static vCardHandler() {
    elmGetter.each('#i_cecream div.container.is-version8 div.bili-video-card__info div.bili-video-card__info--bottom > a', async user => {
      const unameSpan = user.querySelector('span.bili-video-card__info--author')
      const bid = new URL(user.href)?.pathname?.split('/')?.[1]
      const nickname = unameSpan.innerText
      const memo = MemoService.getMemo({bid, nickname})
      if(memo){
        const nameShow = BMemoUtils.getUserShow({nickname, memo: memo?.memo})
        unameSpan.innerHTML = nameShow
        unameSpan.title = nameShow
        unameSpan.classList.add(HL_CLS)
      }else{
        unameSpan.classList.remove(HL_CLS)
      }
    })
  }
  static dynamicHandler() {
    elmGetter.each('#biliHeaderDynScrollCon div.header-content-panel  div.header-dynamic__box--center div.dynamic-name-line div > a', async user => {
      const bid = new URL(user.href)?.pathname?.split('/')?.[1]
      const nickname = user.innerText
      let avatar = ''
      const wrap = user.closest('.header-dynamic-container')
      const aImg = await asyncGetNodeOnce('.header-dynamic-avatar .bili-avatar img', wrap)
      if(aImg){
        avatar = fixUrl(aImg.dataset.src)
      }
      let memo = MemoService.getMemo({bid, nickname})
      if(memo){
        const nameShow = BMemoUtils.getUserShow({nickname, memo: memo?.memo})
        user.innerHTML = nameShow
        user.title = nameShow
        user.classList.add(HL_CLS)

        if(memo.nickname != nickname || avatar != memo.avatar){
          memo = {...memo, nickname, avatar}
          BMemoUtils.saveMemo(memo)
        }
      }else{
        user.classList.remove(HL_CLS)
      }
    })
  }
  static favideosHandler() {
    elmGetter.feach('#favorite-content-scroll > a > div.header-fav-card__info > span', {excludeAtts: ['title', 'innerText', 'innerHTML']}, async user => {
      const bid = new URL(fixUrl(user.getAttribute('href')))?.pathname?.split('/')?.[1]
      const nameSpan = user.querySelector('span')
      const nickname = nameSpan.innerText
      const memo = MemoService.getMemo({bid, nickname})
      if(memo){
        const nameShow = BMemoUtils.getUserShow({nickname, memo: memo?.memo})
        nameSpan.innerText = nameShow
        nameSpan.title = nameShow
        nameSpan.classList.add(HL_CLS)
      }else{
        nameSpan.classList.remove(HL_CLS)
      }
    })
  }
  static historyHandler() {
      elmGetter.each('ul.right-entry .header-tabs-panel__content a .header-history-card__info .header-history-card__info--name', async user => {
        const nameSpan = user.querySelector('span')
        let bid = user.href ? new URL(user.href)?.pathname?.split('/')?.[1] : ''
        const nickname = nameSpan.innerText
        const memo = MemoService.getMemo({bid, nickname})
        if(memo){
          const nameShow = BMemoUtils.getUserShow({nickname, memo: memo?.memo})
          nameSpan.innerText = nameShow
          nameSpan.title = nameShow
          nameSpan.classList.add(HL_CLS)
        }else{
          nameSpan.classList.remove(HL_CLS)
        }
      })
  }

}

class VideoPlayUI{
  static init(){
    setTimeout(()=>{
      this.upHandler()
    }, 1800)
    this.rcmdHandler()
    this.commentUserHander()
  }
  static upHandler(){
    elmGetter.each('#mirror-vdcon .right-container .up-panel-container .up-info-container .up-info--right .up-info__detail .up-detail-top > a:nth-child(1)', async user => {
      const bid = new URL(user.href)?.pathname?.split('/')?.[1]
      const nickname = user.innerText
      let avatar = ''
      const aImg = await asyncGetNodeOnce('.up-info--left img', user.closest('.up-info-container'))
      if(aImg){
        avatar = aImg.src
      }

      const mInput = document.createElement('div')
      const mBtn = document.createElement('button')
      user.style.transition = 'all 0.3s linear'
      let memo = MemoService.getMemo({bid, nickname})
      let nameShow
      if(memo){
        nameShow = BMemoUtils.getUserShow({nickname, memo: memo?.memo})
        user.innerText = nameShow
        user.classList.add(HL_CLS)

        if(nickname != memo.nickname || avatar != memo.avatar){
          memo = {...memo, nickname, avatar}
          BMemoUtils.saveMemo(memo)
        }
      }else{
        user.classList.remove(HL_CLS)
      }
      mInput.style.cssText = `width: 0px;transition: width 0.3s linear;border:none;border-bottom: 1px solid #FB7299;overflow: hidden;font-size:15px;`
      mInput.contentEditable = true
      mBtn.innerText = '备注'
      mBtn.style.marginRight = '4px'
      mBtn.style.fontSize = '15px'
      mBtn.classList.add(EDT_BTN_CLS)
      mBtn.addEventListener('click', () => {
        if(mBtn.innerText === '备注'){
          memo = MemoService.getMemo({bid, nickname})
          mInput.readOnly = false
          mInput.focus()
          mInput.innerText = memo?.memo ? memo.memo : nickname
          mInput.style.width = 'auto'
          mInput.style.height = 'auto'
          mInput.style.padding = '2px 5px'
          user.style.width = '0'
          mBtn.innerText = '保存'
        }else{
          handleSave()
        }
      })
      mInput.addEventListener('keydown', (e) => {
        if(e.code == 'Enter'){
          handleSave()
        }
      })
      mInput.addEventListener('blur', ()=> {
        if(!mInput.readOnly){
          handleSave()
        }
      })
      
      function handleSave(){
        mInput.readOnly = true
        mBtn.innerText = '备注'
        user.style.width = 'auto'
        mInput.style.width = '0px'
        mInput.style.height = '0'
        mInput.style.padding = '0'
        let m = mInput?.innerText?.trim()
        m = m == nickname ? '' : m
        let _memo = new BMemo(bid, nickname, m, avatar)
        memo = _memo
        const res = BMemoUtils.saveMemo(_memo)
        if(res == 1){
          user.classList.add(HL_CLS)
          nameShow = BMemoUtils.getUserShow({nickname, memo: _memo.memo}) 
          user.innerText = nameShow 
        }else{
          user.classList.remove(HL_CLS)
          user.innerText = nickname
        }
      }

      user.insertAdjacentElement('afterend', mBtn)
      user.insertAdjacentElement('afterend', mInput)
    })
  }
  static rcmdHandler() {
    elmGetter.each('#mirror-vdcon > div.right-container div.rcmd-tab div.recommend-list-v1 div.rec-list div.pic-box  div.pic div  picture  img', 
      async cover => {
      const card = cover.closest('div.card-box')
      const user = card.querySelector('.info .upname > a')
      const nameSpan = user.querySelector('.name')
      const nickname = nameSpan?.innerText
      const bid = new URL(user.href)?.pathname?.split('/')[1]
      const memo = MemoService.getMemo({bid, nickname})
      if(memo){
        nameSpan.innerText = BMemoUtils.userNameShow({nickname, memo: memo.memo})
        nameSpan.classList.add(HL_CLS)
      }else{
        nameSpan.classList.remove(HL_CLS)
      }
    }
    )
  }
  static commentUserHander(){
    function handleUser(user, {avatar=''}){
      const bid = new URL(user.href)?.pathname?.split('/')[1]
      const nickname = user.innerText 
      user.style.transition = 'all .3s'
      const mInput = document.createElement('input')
      const mBtn = document.createElement('button')
      let memo = MemoService.getMemo({bid, nickname})
      if(memo){
        mInput.value = memo.memo ?? nickname
        user.innerText = BMemoUtils.getUserShow({nickname, memo: memo.memo})
        for(let s in INLINE_HL){
          user.style[s] = INLINE_HL[s]
        }
        if(nickname != memo.nickname || avatar != memo.avatar){
          memo = {...memo, avatar, nickname}
          BMemoUtils.saveMemo(memo)
        }
      }else{
        mInput.value = nickname
        for(let s in INLINE_HL){
          user.style[s] = 'unset'
        }
      }
      mInput.style.cssText = `width:0;border:none;outline:none;border-bottom:1px solid #FB7299;transition:all .3s;overflow:hidden;padding:0;`
      mBtn.innerText = '备注'
      mBtn.style.cssText = `border:none;outline:none;color:${BPINK};background:none;`
      mBtn.addEventListener('click',()=>{
        if(mBtn.innerText === '备注'){
          memo = MemoService.getMemo({bid, nickname})
          mInput.style.width = '100px'
          mInput.style.padding = '2px 4px'
          mInput.readOnly = false
          mInput.focus()
          mInput.value = memo?.memo ?? nickname
          user.style.display = 'none'
          mBtn.innerText = '保存'
        }else{
          handleSave()
        }
      })
      mInput.addEventListener('keydown',(e)=>{
        if(e.code == 'Enter'){
          handleSave()
        }
      })
      mInput.addEventListener('blur',()=>{
        if(!mInput.readOnly){
          handleSave()
        }
      })
      function handleSave(){
        mInput.style.width = '0'
        mInput.style.padding = '0'
        mInput.readOnly = true
        user.style.display = 'unset'
        mBtn.innerText = '备注'
        let m = mInput?.value?.trim()
        m = m == nickname ? '' : m
        let _memo = new BMemo(bid, nickname, m, avatar)
        memo = _memo
        const res = BMemoUtils.saveMemo(_memo)
        if(res == 1){
          for(let s in INLINE_HL){
            user.style[s] = INLINE_HL[s]
          }
          user.innerText = BMemoUtils.userNameShow({nickname, memo: _memo.memo}) 
        }else{
          for(let s in INLINE_HL){
            user.style[s] = 'unset'
          }
          user.innerText = nickname
        }
      }
      user.parentNode?.insertAdjacentElement('afterend', mBtn)
      user.parentNode?.insertAdjacentElement('afterend', mInput)
    }
    elmGetter.each('bili-comments', async cmts => {
      const cRoot = cmts.shadowRoot
      elmGetter.each('bili-comment-thread-renderer', cRoot, async cmt => {
        const cmtRoot = cmt.shadowRoot
        elmGetter.each('bili-comment-renderer', cmtRoot, async topCmt => {
          const topCmtRoot = topCmt.shadowRoot
          let avatar = ''
          const aBox = await asyncGetNodeOnce('bili-avatar', topCmtRoot)
          const aImg = await asyncGetNodeOnce('#canvas > .layers:nth-child(2) > div.layer.center picture  img', aBox.shadowRoot)
          if(aImg){
            avatar = aImg.src
          }
          elmGetter.each('bili-comment-user-info', topCmtRoot, async uinfo => {
            const userInfoRoot = uinfo.shadowRoot
            elmGetter.each('#user-name a', userInfoRoot, async user => {
              handleUser(user, {avatar})
            })
          })
        })
        elmGetter.each('#replies bili-comment-replies-renderer', cmtRoot, async replies => {
          const repliesRoot = replies.shadowRoot
          elmGetter.each('bili-comment-reply-renderer', repliesRoot, async rpl => {
            const rplRoot = rpl.shadowRoot
            elmGetter.each('bili-comment-user-info', rplRoot, async uinfo => {
              const userInfoRoot = uinfo.shadowRoot
              let avatar = ''
              const aImg = uinfo.querySelector('#user-avatar img')
              if(aImg){
                avatar = aImg.src
              }
              elmGetter.each('#user-name a', userInfoRoot, async user => {
                handleUser(user, {avatar})
              })
            })
          })
        })
      })
    })
  }
}

class MsgUI{
  static init() {
    this.whisperHandler()
    this.replyHandler()
  }

  static whisperHandler(){
    elmGetter.each('#link-message-container .space-right .bili-im .left .list-container .list .name-box .name-value',
      async user => {
        let nickname = user.innerText 
        const startT = new Date().getTime()
        if(nickname){
          handleUser()
        }else{
          const timer = setInterval(() => {
            if(nickname || new Date().getTime() - startT > MAX_TIMEOUT * 1e3){
              handleUser()
              clearInterval(timer)
            }else{
              nickname = user.innerText
            }
          }, 300)

        }

        function handleUser(){
          const memo = MemoService.getMemo({nickname})
          if(memo){
            user.innerText = BMemoUtils.getUserShow(memo)
            user.classList.add(HL_CLS)
          }else{
            user.classList.remove(HL_CLS)
          }
        }
      }
    )
  }

  static replyHandler(){
    elmGetter.each('#link-message-container .container .space-right .router-view .basic-list-item .center-box .line-1 span.name-field > a',
      async user => {
        const bid = new URL(user.href)?.pathname.split('/')[1]
        const nickname = user.innerText 
        let memo = MemoService.getMemo({bid, nickname})
        if(memo){
          user.innerText = BMemoUtils.getUserShow(memo)
          user.classList.add(HL_CLS)
        }else{
          user.classList.remove(HL_CLS)
        }
        const mInput = document.createElement('div')
        const mBtn  = document.createElement('button')
        let isSaving = false
        mInput.style.cssText = `display:inline-block;outline:none;border-bottom: 1px solid ${BPINK};transition: all 0.3s ease;width: 0;padding: 0;height: 0;overflow: hidden;`
        user.style.transition = 'all 0.3s ease'
        const line1 = user.closest('.line-1')
        if(line1){
          line1.style.display = 'flex'
          line1.style.gap = '4px'
          line1.style.alignItems = 'center'
        }
        mBtn.classList.add(EDT_BTN_CLS)
        mBtn.innerText = '备注'
        mBtn.addEventListener('click', (e) => {
          e.stopPropagation()
          e.preventDefault()
          memo = MemoService.getMemo({bid, nickname})
          mInput.innerText = memo?.memo? memo.memo : nickname
          mInput.contentEditable = true 
          mInput.style.width = 'auto' 
          mInput.style.height = 'auto' 
          mInput.style.padding = '2px 8px'
          mInput.focus()
          user.style.display = 'none'
          mBtn.style.display = 'none'
        })
        mInput.addEventListener('blur', () => {
          if(!isSaving){
            isSaving = true
            handleSave()
          }
        })
        mInput.addEventListener('click', (e) => {
          e.stopPropagation()
          e.preventDefault()
        })
        mInput.addEventListener('keydown', (e) =>{
          if(e.code == 'Enter'){
            isSaving = true
            handleSave()
          }
        })

        user.insertAdjacentElement('afterend', mInput)
        user.insertAdjacentElement('afterend', mBtn)
        function handleSave(){
          mInput.contentEditable = false
          isSaving = false
          mInput.style.width = '0'
          mInput.style.height = '0'
          mInput.style.padding = '0'
          user.style.display = 'inline'
          mBtn.style.display = 'inline'
          let m = mInput.innerText?.trim()
          m = m === nickname ? '' : m
          const _memo = new BMemo(bid, nickname, m) 
          memo = {..._memo}
          const res = BMemoUtils.saveMemo(memo)
          if(res == 1){
            user.innerText = BMemoUtils.getUserShow(memo)
            user.classList.add(HL_CLS)
          }else{
            user.innerText = nickname 
            user.classList.remove(HL_CLS)
          }
        }
      }
    )
  }
}
class ManagerMenu{

  static flushMemoTab

  static init(){

  }

  static renderMenuAll(){
    const menuH = '46vh'
    const menu = document.createElement('div')
    document.body.appendChild(menu)

    const menuBox = document.createElement('div')
    const toggleBtn = document.createElement('div')
    const helpInfo = document.createElement('div')
    menu.appendChild(menuBox)
    menu.appendChild(helpInfo)
    menuBox.appendChild(toggleBtn)
    menuBox.appendChild(this.renderOptsUI())
    menuBox.appendChild(this.renderMemosUI())

    const helpA = document.createElement('a')
    helpInfo.appendChild(helpA)
    helpA.href = HELP_LINK
    helpA.target = '_blank'
    helpA.innerText = '帮助'
    helpInfo.style.cssText = `position: absolute; right: 8px;top: 6px;`
    helpA.style.cssText = `color: ${BPINK};border: 2px solid ${BPINK};padding: 4px 6px;display: inline-block; border-radius: 3px;`

    menu.style.cssText = `position: fixed;height: ${menuH};left:0;right:0;bottom: -${menuH};z-index:9999;
    background: white;
    border-top:2px solid ${BPINK};transition: all .4s ease;`
    menuBox.style.cssText = `padding: 8px;position:relative;height: 100%;`
    toggleBtn.style.cssText = `position: absolute;top:-30px;left: 20px;width: 50px;
    opacity: .45;
    border-radius: 6px 6px 0 0;user-select: none;cursor: pointer;font-size: 20px;
    font-weight: 600;color:white;
    height: 30px;background: ${BPINK};text-align: center;line-height: 28px;`
    toggleBtn.innerText = 'o_o'
    toggleBtn.addEventListener('click',()=>{
      if(toggleBtn.innerText === 'o_o'){
        toggleBtn.innerText = 'O^O'
        menu.style.bottom = '0'
        toggleBtn.style.opacity = '1'
        this.flushMemoTab()

      }else{
        toggleBtn.innerText = 'o_o'
        menu.style.bottom = `-${menuH}`
        toggleBtn.style.opacity = '.45'
      }
    }) 
  }

  static renderOptsUI(){

    const optsUI = document.createElement('div')
    optsUI.style.cssText = `border-bottom: 1px solid #ccc;`

    optsUI.appendChild(renderMemoModeOpt())

    function renderMemoModeOpt(){
      const memoModes = [
        {label: '昵称', value: 0},
        {label: '备注(昵称)', value: 1},
        {label: '昵称(备注)', value: 2},
        {label: '备注', value: 3},
      ]
      const memoModeBox = document.createElement('div')
      const mmLabel = document.createElement('div')
      memoModeBox.appendChild(mmLabel)
      mmLabel.innerText = '昵称显示模式:'
      mmLabel.style.cssText = 'font-size: 17px; font-weight: 600;'
      memoModeBox.style.cssText = 'display: flex;align-items: center;gap: 18px;padding: 8px;font-size: 16px'
      const memoM = ConfService.getConf(CONF_KEY.memoM)
      memoModes.forEach(m => {
        const wrap = document.createElement('div')
        const label = document.createElement('label')
        const input = document.createElement('input')
        input.type = 'radio'
        input.name = 'memoMode'
        input.value = m.value
        input.id = 'pxo-memomode-' + m.value
        label.innerText = m.label
        label.style.userSelect = 'none'
        label.setAttribute('for', input.id)
        input.addEventListener('change', (e) => {
          ConfService.setConf(CONF_KEY.memoM, e.target.value)
        })
        if(memoM == m.value){
          input.checked = true
        }
        wrap.appendChild(input)
        wrap.appendChild(label)
        memoModeBox.appendChild(wrap)
      })
      return memoModeBox
    }

    return optsUI
  }

  static renderMemosUI(){
    const mWrap = document.createElement('div')
    const header = document.createElement('div')
    const memoTab = document.createElement('div')
    mWrap.appendChild(header)
    mWrap.appendChild(memoTab)
    const importBtn = document.createElement('button')
    const exportBtn = document.createElement('button')
    const memoTitle = document.createElement('div')
    const searchBox = document.createElement('div')
    const searchInput = document.createElement('input')
    const clearBtn = document.createElement('div')
    searchBox.appendChild(searchInput)
    searchBox.appendChild(clearBtn)
    header.appendChild(importBtn)
    header.appendChild(memoTitle)
    header.appendChild(searchBox)
    header.appendChild(exportBtn)
    searchBox.style.cssText = `border: 1px solid #ccc;padding: 6px 4px;border-radius: 4px;width: 200px;flex-shrink: 0;display: flex;align-items: center;gap: 4px;`
    searchInput.style.cssText = `border: none;outline: none;width: 100%;font-size: 16px;color: #333;padding: 0 6px`
    searchInput.placeholder = '搜索....'
    clearBtn.innerHTML = '&times;'
    clearBtn.style.cssText = `color: #ccc;font-size: 16px;cursor: pointer;text-align: center;border-radius: 50%;
    line-height: 20px;
    border: 1px solid #ccc;width:20px;height:20px;flex-shrink: 0;`
    header.style.cssText = 'display: flex;align-items: center;justify-content: space-between;gap: 8px;padding: 8px;border-bottom: 1px solid #ccc'
    memoTitle.innerText = '备注列表'
    memoTitle.style.cssText = 'font-size: 18px;font-weight: 600;flex-grow: 2;text-align: center;'
    importBtn.innerText = '导入'
    importBtn.style.cssText = `padding: 8px 18px;border:none;outline:none;background:${BPINK};border-radius: 4px;color: white;`
    exportBtn.innerText = '导出'
    exportBtn.style.cssText = importBtn.style.cssText
    exportBtn.addEventListener('click', () => {
      const memos = MemoService.getAllMemos()
      const mlist = JSON.stringify(memos, null, 2)
      const blob = new Blob([mlist], {type: 'application/json'})
      const url = URL.createObjectURL(blob)
      const a = document.createElement('a')
      a.href = url
      a.download = `B站备注备份_${dateTimeNow()}.json`
      a.click()
      URL.revokeObjectURL(url)
    })
    {
      const importModes = [
        {name: '跳过', value: 0},
        {name: '覆盖', value: 1},
        {name: '合并', value: 2},
      ]
      const imptDog = document.createElement('dialog')
      document.body.appendChild(imptDog)
      importBtn.addEventListener('click', () => {
        imptDog.showModal()
        imptDog.style.display = 'block'
      })

      imptDog.style.cssText = `display:none;border: 1px solid #ccc;border-radius: 5px;width: 500px;box-shadow: 2px 2px 4px 3px #00000027;`

      const dTitle = document.createElement('div')
      dTitle.style.cssText = 'font-size: 17px;font-weight: bold;margin-bottom: 8px;text-align: center;'
      dTitle.innerText = '批量导入备注'
      imptDog.appendChild(dTitle)
      const imptModeWrap = document.createElement('div')
      imptDog.appendChild(imptModeWrap)
      const iptTitle = document.createElement('div')
      imptModeWrap.appendChild(iptTitle)
      iptTitle.innerText = '导入时遇到重复的:'
      iptTitle.style.cssText = `font-size: 16px; font-weight: 600;`
      imptModeWrap.style.cssText = 'display: flex;margin-bottom: 8px;font-size: 16px;gap:18px;align-items:center;'
      let currMode = 1
      importModes.forEach(mode => {
        const wrap = document.createElement('div')
        const label = document.createElement('label')
        const input = document.createElement('input')
        input.type = 'radio'
        input.name = 'imptMode'
        input.value = mode.value
        input.id = 'pxo-imptmode-' + mode.value
        input.checked = currMode == mode.value
        label.innerText = mode.name
        label.style.userSelect = 'none'
        label.setAttribute('for', input.id)
        input.addEventListener('change', (e) => {
          currMode = e.target.value
        })
        wrap.appendChild(input)
        wrap.appendChild(label)
        imptModeWrap.appendChild(wrap)
      })

      const imtIpt = document.createElement('textarea')
      imptDog.appendChild(imtIpt)
      imtIpt.style.cssText = `box-sizing: border-box;border: 1px solid #ccc;border-radius: 4px;padding:10px;`
      imtIpt.style.width = '100%'
      imtIpt.rows = 14

      const optBtnWrap = document.createElement('div')
      optBtnWrap.style.cssText = `display:flex;align-items:center;justify-content:flex-end;gap:18px;`
      imptDog.appendChild(optBtnWrap)
      const cancelBtn = document.createElement('button')
      optBtnWrap.appendChild(cancelBtn)
      cancelBtn.innerText = '取消'
      cancelBtn.style.cssText = `border:1px solid ${BPINK};padding:3px 8px;background:${BPINK};color:white;font-size:16px;border-radius:4px;`
      cancelBtn.addEventListener('click', () => {
        imptDog.close()
        imptDog.style.display = 'none'
      })
      const cfmBtn = document.createElement('button')
      optBtnWrap.appendChild(cfmBtn)
      cfmBtn.innerText = '导入'
      cfmBtn.style.cssText = cancelBtn.style.cssText
      cfmBtn.addEventListener('click', () => {
        const imptData = imtIpt.value
        let data
        try{
          data = JSON.parse(imptData)
        }catch(e){
          alert('输入内容格式错误')
          return false 
        }
        if(data){
          const res = BMemoUtils.importMemos(data, currMode)
          if(res){
            imptDog.close()
            imptDog.style.display = 'none'
            flushMemoTab()
            alert(`${res.successCnt || 0} 条数据导入成功;${res.failCnt || 0} 条数据导入失败`)
          }
        }
      })
    }

    let mFilter = null
    let filterTimer = null

    function searchHandler(e){
      if(filterTimer) {clearTimeout(filterTimer)}
      filterTimer = setTimeout(() => {
        const searhKey = searchInput.value.trim() 
        if(mFilter) mFilter(searhKey)
      }, 800)
    }

    clearBtn.addEventListener('click', () => {
      searchInput.value = ''
      searchInput.focus()
      searchHandler()
    })

    {
      memoTab.style.cssText = `height:calc(45vh - 110px);overflow-y:scroll;`
      const {memoList, memoFilter} = this.renderMemoTab()
      mFilter = memoFilter
      searchInput.addEventListener('input', searchHandler)
      if(memoList){
        memoTab.appendChild(memoList)
      }
    }

    this.flushMemoTab = () => flushMemoTab()
    function flushMemoTab(){
      const  {memoList, memoFilter} = ManagerMenu.renderMemoTab()
      mFilter = memoFilter
      memoTab.innerHTML = ''
      memoTab.appendChild(memoList)
    }
    return mWrap
  }

  static renderMemoTab() {
    const memos = MemoService.getAllMemos()
    const mp = new Map()
    const memoKeys = ['avatar', 'bid', 'nickname', 'memo']
    const mLabels = {
      'avatar': '头像',
      'bid': 'BilibiliID',
      'nickname': '昵称',
      'memo': '备注'
    }
    const mwrap = document.createElement('div')
    mwrap.style.cssText = `display:flex;flex-wrap:wrap;width:100%;align-items:center;justify-content:space-between;`
    for(const bid in memos){
      const memo = memos[bid]
      const row = document.createElement('div')
      mwrap.appendChild(row)
      mp.set(bid, row)
      row.style.cssText = `display: flex;align-items: center;gap: 8px;
      box-shadow: 1px 1px 2px 1px #00000017;gap: 40px;
      width: 45%;min-width: 410px;flex-wrap: wrap;
      padding: 8px 18px;border:1px solid #e9e9e9;border-radius: 5px;margin: 5px;`
      memoKeys.forEach(k => {
        const item = document.createElement('div')
        row.appendChild(item)
        if(k != 'avatar'){
          const label = document.createElement('div')
          item.appendChild(label)
          label.innerText = mLabels[k]
          label.style.cssText = `font-size: 12px;color: #a1a0a0;margin-bottom: 9px;`
        }else{
          const {avatar} = memo
          item.style.borderRadius = '50%'
          item.style.overflow = 'hidden'
          item.style.boxShadow = '1px 1px 3px 2px #00000021'
          item.style.flexShrink = '0'
          item.style.width = '50px'
          item.style.height = '50px'
          if(avatar){
            const img = document.createElement('img')
            item.appendChild(img)
            img.src = memo[k]
            img.style.cssText = `width: 50px;height:50px`
          }else{
            const fakeA = document.createElement('div')
            item.appendChild(fakeA)
            fakeA.innerText = memo.memo[0] || 'B'
            fakeA.style.cssText = `width: 50px;height: 50px;text-align: center;line-height: 48px;
            user-select: none;
            background: linear-gradient(45deg, ${BPINK}, blue);color: white;font-size: 24px;font-weight: 600;`
          }
        }

        if(k == 'nickname'){
          item.style.cssText = 'min-width: 60px;max-width:110px;overflow: hidden;flex-shrink:0;'
          item.title = memo[k]
          const a = document.createElement('a')
          a.title = memo[k]
          a.innerText = memo[k]
          a.style.cssText = `font-size: 14px;color:${BBLUE};display:block;font-size: 15px;`
          a.href = 'https://space.bilibili.com/' + bid
          a.target= '_blank'
          item.appendChild(a)
        }else if(k == 'memo'){
          const memoInput = document.createElement('input')
          memoInput.value = memo[k]
          memoInput.style.cssText = `border:none;outline:none;font-size: 15px;width:120px;color: ${BPINK};background:transparent;`
          memoInput.readOnly = true 
          memoInput.title = '单击修改备注'
          memoInput.addEventListener('click', () => {
            memoInput.readOnly = false     
            memoInput.style.borderBottom = `1px solid ${BPINK}`
            memoInput.focus()     
          })
          memoInput.addEventListener('blur', () => {
            memoInput.readOnly = true
            memoInput.style.borderBottom = 'none'
            let m = memoInput.value.trim()
            if(!m && !confirm(`确定删除该备注吗?【${memo.nickname} | ${memo.memo}】`)){
              memoInput.value = memo.memo
              return false
            }
            const _memo = {...memo, memo: m}
            const res = BMemoUtils.saveMemo(_memo)
            if(res == -1){
              row.remove()
            }else if(res == 0){
              memoInput.value = memo.memo
            }else{
              memo.memo = m
            }
            GM_setValue(bid, memo)
          })
          item.appendChild(memoInput)
        }else if(k == 'bid'){
          const val = document.createElement('div')
          val.innerText = memo[k]
          item.appendChild(val)
        }
      })
    }

    function memoFilter(keyword = ''){
      let kwd = keyword.trim()
      if(!kwd){
        for(const k of mp.keys()){
          mp.get(k).style.display = 'flex'
        }
      }else{
        const keys = kwd.split(/\s+/)
        for(const k of mp.keys()){
          const m = memos[k]
          const show = keys.some(k => (m.memo.includes(k) || m.nickname.includes(k) || m.bid.includes(k)))
          mp.get(k).style.display = show ? 'flex' : 'none'
        }
      }
    }
    return {memoList: mwrap, memoFilter}
  }
}
function init(){
  GM_addStyle(CM_STYLE)
  const confs = ConfService.getAllConf()
  if(confs == null || (confs !== null && Object.keys(confs).length == 0)){
    for(let k in CONF_INIT){
      ConfService.setConf(k, CONF_INIT[k])
    }
  }
  BSpaceUI.init()
  IndexUI.init()
  VideoPlayUI.init()
  MsgUI.init()
  ManagerMenu.renderMenuAll()
}

(function() {
    'use strict';
  init()
})();