Instagram - Add notes to the user

Add notes (aliases/tags) for users to help identify and search, and support WebDAV sync

// ==UserScript==
// @name                Instagram - Add notes to the user
// @name:zh-CN          Instagram - 为用户添加备注(别名/标签)
// @name:zh-TW          Instagram - 為使用者新增備註(別名/標籤)
// @namespace           https://greasyfork.org/zh-CN/users/193133-pana
// @homepage            https://greasyfork.org/zh-CN/users/193133-pana
// @icon                data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0cHgiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiBhcmlhLWxhYmVsbGVkYnk9Im5ld0ljb25UaXRsZSIgc3Ryb2tlPSJyZ2JhKDI5LDE2MSwyNDIsMS4wMCkiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgZmlsbD0ibm9uZSIgY29sb3I9InJnYmEoMjksMTYxLDI0MiwxLjAwKSI+IDx0aXRsZSBpZD0ibmV3SWNvblRpdGxlIj5OZXc8L3RpdGxlPiA8cGF0aCBkPSJNMTkgMTRWMjJIMi45OTk5N1Y0SDEzIi8+IDxwYXRoIGQ9Ik0xNy40NjA4IDQuMDM5MjFDMTguMjQxOCAzLjI1ODE3IDE5LjUwODIgMy4yNTgxNiAyMC4yODkyIDQuMDM5MjFMMjAuOTYwOCA0LjcxMDc5QzIxLjc0MTggNS40OTE4NCAyMS43NDE4IDYuNzU4MTcgMjAuOTYwOCA3LjUzOTIxTDExLjU4NTggMTYuOTE0MkMxMS4yMTA3IDE3LjI4OTMgMTAuNzAyIDE3LjUgMTAuMTcxNiAxNy41TDcuNSAxNy41TDcuNSAxNC44Mjg0QzcuNSAxNC4yOTggNy43MTA3MSAxMy43ODkzIDguMDg1NzkgMTMuNDE0MkwxNy40NjA4IDQuMDM5MjFaIi8+IDxwYXRoIGQ9Ik0xNi4yNSA1LjI1TDE5Ljc1IDguNzUiLz4gPC9zdmc+
// @version             6.1.8
// @description         Add notes (aliases/tags) for users to help identify and search, and support WebDAV sync
// @description:zh-CN   为用户添加备注(别名/标签)功能,以帮助识别和搜索,并支持 WebDAV 同步功能
// @description:zh-TW   為使用者新增備註(別名/標籤)功能,以幫助識別和搜尋,並支援 WebDAV 同步功能
// @license             GNU General Public License v3.0 or later
// @compatible          chrome
// @compatible          firefox
// @author              pana
// @match               *://*.instagram.com/*
// @require             https://gcore.jsdelivr.net/gh/LightAPIs/greasy-fork-library@da0437e6a856e05158df61225f2b9ea9943ad9ef/Note_Obj.js
// @connect             *
// @noframes
// @grant               GM_info
// @grant               GM_getValue
// @grant               GM_setValue
// @grant               GM_deleteValue
// @grant               GM_listValues
// @grant               GM_openInTab
// @grant               GM_addStyle
// @grant               GM_registerMenuCommand
// @grant               GM_unregisterMenuCommand
// @grant               GM_addValueChangeListener
// @grant               GM_removeValueChangeListener
// ==/UserScript==

(function () {
  'use strict';
  const UPDATED = '2023-04-21';
  const INS_ICON = {
    NOTE_BLACK: 'url(data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0cHgiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiBhcmlhLWxhYmVsbGVkYnk9Im5ld0ljb25UaXRsZSIgc3Ryb2tlPSJyZ2IoMzgsIDM4LCAzOCkiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgZmlsbD0ibm9uZSIgY29sb3I9InJnYigzOCwgMzgsIDM4KSI+IDx0aXRsZSBpZD0ibmV3SWNvblRpdGxlIj5OZXc8L3RpdGxlPiA8cGF0aCBkPSJNMTkgMTRWMjJIMi45OTk5N1Y0SDEzIi8+IDxwYXRoIGQ9Ik0xNy40NjA4IDQuMDM5MjFDMTguMjQxOCAzLjI1ODE3IDE5LjUwODIgMy4yNTgxNiAyMC4yODkyIDQuMDM5MjFMMjAuOTYwOCA0LjcxMDc5QzIxLjc0MTggNS40OTE4NCAyMS43NDE4IDYuNzU4MTcgMjAuOTYwOCA3LjUzOTIxTDExLjU4NTggMTYuOTE0MkMxMS4yMTA3IDE3LjI4OTMgMTAuNzAyIDE3LjUgMTAuMTcxNiAxNy41TDcuNSAxNy41TDcuNSAxNC44Mjg0QzcuNSAxNC4yOTggNy43MTA3MSAxMy43ODkzIDguMDg1NzkgMTMuNDE0MkwxNy40NjA4IDQuMDM5MjFaIi8+IDxwYXRoIGQ9Ik0xNi4yNSA1LjI1TDE5Ljc1IDguNzUiLz4gPC9zdmc+)'
  };
  const selector = {
    homepage: {
      article: '[role="main"] article',
      id: '._aaqt a',
      icon: 'span._aamz',
      commentId: '._ab8x .xt0psk2 a.notranslate',
      commentAt: '._ab8x ._aacl a.notranslate'
    },
    homepageStories: {
      id: '._aad6',
      idShell: 'li [role="menuitem"]'
    },
    homepageRecommend: {
      id: '._aak3 ._ab8x .xt0psk2 a'
    },
    userPage: {
      frame: '._aa_y',
      id: 'h2',
      bar: '.x8j4wrb',
      box: 'ul',
      common: 'span._aaai',
      suggest: '._acj1 a.notranslate',
      infoAt: '.notranslate',
      userName: '._aa_c > span'
    },
    watchList: {
      initialItem: '[role="dialog"] [aria-labelledby]',
      laterItem: '._aaei',
      id: '._ab8w a.notranslate'
    },
    stories: {
      id: 'a.notranslate',
      idShell: '._ac0o',
      cardId: '._afgd ._aacw'
    },
    dialog: {
      frame: '[role="dialog"] article',
      commentId: '._a9zc ._ab8w a, ._aacx._aacu a',
      commentAt: '._a9zs .notranslate'
    },
    request: {
      follow: '._aajc ._ab8x .xt0psk2 a'
    },
    suggest: {
      user: '._aa0- ._ab8x .xt0psk2 a'
    }
  };
  const nameSet = {
    backgroundBox: 'note-obj-ins-background-box',
    userpageTag: 'note-obj-ins-userpage-tag',
    fontBold: 'note-obj-ins-font-bold',
    addBtn: 'note-obj-ins-add-btn',
    homepageBtn: 'note-obj-ins-homepage-btn',
    userpageBtn: 'note-obj-ins-userpage-btn'
  };
  const INS_STYLE = `
    .${nameSet.backgroundBox} {
      display: inline-block;
      align-items: center;
      white-space: nowrap;
      border-radius: 50px;
      padding: 0px 10px;
      background-color: #336699;
      color: #fff;
    }
    .${nameSet.addBtn} {
      background-image: ${INS_ICON.NOTE_BLACK};
      background-size: 24px;
      background-repeat: no-repeat;
      background-position: center;
      margin-left: 5px;
      cursor: pointer;
      width: 24px;
      height: 24px;
    }
    .${nameSet.homepageBtn} {
      margin: 6px !important;
    }
    .${nameSet.homepageBtn}:hover {
      opacity: 0.5;
    }
    .${nameSet.userpageBtn} {
      margin-top: 2px;
    }
    .${nameSet.userpageTag} {
      display: block;
      font-size: 20px;
      margin-bottom: 20px;
      white-space: nowrap;
    }
    .${nameSet.fontBold} {
      font-weight: bold;
    }
    .note-obj-settings-frame-card label {
      color: #2f2f2f;
    }
    .note-obj-interface-dark .note-obj-settings-frame-card label {
      color: #fff;
    }
    .note-obj-group-frame-tbody input,
    .note-obj-webdav-frame-form-item input {
      color: #000;
    }`;
  const noteObj = new Note_Obj({
    id: 'myInstagramNote',
    script: {
      author: {
        name: 'pana',
        homepage: 'https://greasyfork.org/zh-CN/users/193133-pana'
      },
      url: 'https://greasyfork.org/scripts/387871',
      updated: UPDATED
    },
    style: INS_STYLE,
    primaryColor: '#336699',
    settings: {
      replaceHomepageID: {
        type: 'checkbox',
        lang: {
          en: 'Allow replacing user IDs on the home page',
          zhHans: '允许替换首页上的用户 ID',
          zhHant: '允許替換首頁上的使用者 ID'
        },
        default: true,
        event: instagramHomepageEvent
      }
    },
    changeEvent: instagramChangeEvent
  });
  function homepageNote(ele, changeId) {
    const user = noteObj.fn.queryAnchor(ele, selector.homepage.id);
    if (user) {
      const replaceHomepageID = noteObj.getOtherConfig().replaceHomepageID === true;
      const eleId = noteObj.fn.getIdFromUrl(user.href);
      if (!changeId || changeId === eleId) {
        noteObj.handler(eleId, user, undefined, {
          add: replaceHomepageID ? undefined : 'sapn',
          className: replaceHomepageID ? undefined : [nameSet.backgroundBox]
        });
      }
    }
  }
  function homepageCommentNote(ele, changeId) {
    for (const comment of noteObj.fn.queryAllAnchor(ele, selector.homepage.commentId, 'info')) {
      const commentId = noteObj.fn.getIdFromUrl(comment.href);
      if (!changeId || changeId === commentId) {
        noteObj.handler(commentId, comment);
      }
    }
  }
  function homepageCommentAtNote(ele, changeId) {
    const commentAtId = noteObj.fn.getIdFromUrl(ele.href);
    if (!changeId || changeId === commentAtId) {
      noteObj.handler(commentAtId, ele, undefined, {
        prefix: '@',
        title: true
      });
    }
  }
  function dialogCommentNote(ele, chagneId) {
    const picCommentId = noteObj.fn.getIdFromUrl(ele.href);
    if (!chagneId || chagneId === picCommentId) {
      noteObj.handler(picCommentId, ele);
    }
  }
  function dialogCommentAtNote(ele, changeId) {
    if (!ele.classList.contains(selector.homepage.commentId.replace(/^\.|\s+.*$/g, ''))) {
      const picCommentAtId = noteObj.fn.getIdFromUrl(ele.href);
      if (!changeId || changeId === picCommentAtId) {
        noteObj.handler(picCommentAtId, ele, undefined, {
          prefix: '@',
          title: true
        });
      }
    }
  }
  function homepageStoriesNote(ele, changeId) {
    const homepageStoriesId = noteObj.fn.getText(ele, selector.homepageStories.id);
    if (!changeId || changeId === homepageStoriesId) {
      ele.title = noteObj.getUserTag(homepageStoriesId);
    }
  }
  function anchorElementNote(ele, changeId) {
    const itemId = noteObj.fn.getIdFromUrl(ele.href);
    if (!changeId || changeId === itemId) {
      noteObj.handler(itemId, ele);
    }
  }
  function userPageNote(ele, changeId) {
    const userPageId = noteObj.fn.getText(ele, selector.userPage.id);
    const userPageBox = noteObj.fn.query(ele, selector.userPage.box);
    if (userPageId && userPageBox) {
      if (changeId) {
        if (changeId === userPageId) {
          noteObj.handler(userPageId, ele, undefined, {
            add: 'div',
            after: userPageBox,
            maskSecondaryColor: true,
            offsetWidth: -20,
            className: [nameSet.userpageTag, nameSet.fontBold]
          });
        }
      } else {
        const userNameText = noteObj.fn.getText(ele, selector.userPage.userName, 'warn');
        noteObj.handler(userPageId, ele, undefined, {
          add: 'div',
          after: userPageBox,
          maskSecondaryColor: true,
          offsetHeight: -20,
          className: [nameSet.userpageTag, nameSet.fontBold]
        }, userNameText);
      }
    }
  }
  function userPageCommonNote(ele, changeId) {
    for (const commonUser of noteObj.fn.queryAll(ele, selector.userPage.common, 'info')) {
      const commonUserId = commonUser.textContent?.trim();
      if (commonUserId) {
        if (!changeId || changeId === commonUserId) noteObj.handler(commonUserId, commonUser, undefined, {
          title: true,
          notModify: true
        });
      }
    }
  }
  function userPageInfoAtNote(ele, changeId) {
    for (const infoAtUser of noteObj.fn.queryAllAnchor(ele, selector.userPage.infoAt, 'info')) {
      const infoAtUserId = noteObj.fn.getIdFromUrl(infoAtUser.href);
      if (!changeId || changeId === infoAtUserId) {
        noteObj.handler(infoAtUserId, infoAtUser, undefined, {
          prefix: '@',
          title: true
        });
      }
    }
  }
  function storiesNote(ele, changeId) {
    itemNote(ele, selector.stories.id, changeId);
    noteObj.fn.queryAll(selector.stories.cardId).forEach(item => {
      const itemId = item.textContent?.trim() || '';
      if (!changeId || changeId === itemId) {
        noteObj.handler(itemId, item, undefined, {
          notModify: true,
          title: true
        });
      }
    });
  }
  function watchListItemNote(ele, changeId) {
    itemNote(ele, selector.watchList.id, changeId);
  }
  function itemNote(ele, idSelector, changeId) {
    const item = noteObj.fn.queryAnchor(ele, idSelector);
    if (item) {
      const itemId = noteObj.fn.getIdFromUrl(item.href);
      if (!changeId || changeId === itemId) {
        noteObj.handler(itemId, item);
      }
    }
  }
  function instagramChangeEvent(changeId) {
    for (const article of noteObj.fn.queryAll(selector.homepage.article, 'none')) {
      homepageNote(article, changeId);
      homepageCommentNote(article, changeId);
      for (const commentAt of noteObj.fn.queryAllAnchor(selector.homepage.commentAt, 'none')) {
        homepageCommentAtNote(commentAt, changeId);
      }
      for (const picCommentUser of noteObj.fn.queryAllAnchor(selector.dialog.commentId, 'none')) {
        dialogCommentNote(picCommentUser, changeId);
      }
      for (const picCommentAt of noteObj.fn.queryAllAnchor(selector.dialog.commentAt, 'none')) {
        dialogCommentAtNote(picCommentAt, changeId);
      }
    }
    for (const homepageStories of noteObj.fn.queryAll(selector.homepageStories.idShell, 'none')) {
      homepageStoriesNote(homepageStories, changeId);
    }
    for (const homepageRecommend of noteObj.fn.queryAllAnchor(selector.homepageRecommend.id, 'none')) {
      anchorElementNote(homepageRecommend, changeId);
    }
    for (const userPage of noteObj.fn.queryAll(selector.userPage.frame, 'none')) {
      userPageNote(userPage, changeId);
      userPageCommonNote(userPage, changeId);
      userPageInfoAtNote(userPage, changeId);
    }
    for (const storiesShell of noteObj.fn.queryAll(selector.stories.idShell, 'none')) {
      storiesNote(storiesShell, changeId);
    }
    for (const initial of noteObj.fn.queryAll(selector.watchList.initialItem, 'none')) {
      watchListItemNote(initial, changeId);
    }
    for (const later of noteObj.fn.queryAll(selector.watchList.laterItem, 'none')) {
      watchListItemNote(later, changeId);
    }
    for (const dialog of noteObj.fn.queryAll(selector.dialog.frame, 'none')) {
      homepageNote(dialog, changeId);
      homepageCommentNote(dialog, changeId);
      for (const commentUser of noteObj.fn.queryAllAnchor(selector.dialog.commentId, 'none')) {
        dialogCommentNote(commentUser, changeId);
      }
      for (const commentAt of noteObj.fn.queryAllAnchor(selector.dialog.commentAt, 'none')) {
        dialogCommentAtNote(commentAt, changeId);
      }
    }
    for (const follow of noteObj.fn.queryAllAnchor(selector.request.follow, 'none')) {
      anchorElementNote(follow, changeId);
    }
    for (const suggestUser of noteObj.fn.queryAllAnchor(selector.suggest.user, 'none')) {
      anchorElementNote(suggestUser, changeId);
    }
    for (const suggest of noteObj.fn.queryAllAnchor(selector.userPage.suggest, 'none')) {
      anchorElementNote(suggest, changeId);
    }
  }
  function instagramHomepageEvent(newValue, oldValue) {
    for (const article of noteObj.fn.queryAll(selector.homepage.article)) {
      const articleUser = noteObj.fn.queryAnchor(article, selector.homepage.id);
      if (articleUser) {
        const articleUserId = noteObj.fn.getIdFromUrl(articleUser.href);
        noteObj.handler(articleUserId, articleUser, undefined, {
          add: oldValue ? undefined : 'span',
          className: oldValue ? undefined : [nameSet.backgroundBox],
          title: oldValue,
          restore: true
        });
        noteObj.handler(articleUserId, articleUser, undefined, {
          add: newValue ? undefined : 'span',
          className: newValue ? undefined : [nameSet.backgroundBox],
          title: newValue
        });
      }
    }
  }
  function initInstagram() {
    const arriveOption = {
      fireOnAttributesModification: true,
      existing: true
    };
    noteObj.arrive(document.body, selector.homepage.article, arriveOption, article => {
      const homepageIcon = noteObj.fn.query(article, selector.homepage.icon);
      const articleUserId = noteObj.fn.getUrlId(article, selector.homepage.id);
      if (homepageIcon && articleUserId) {
        homepageIcon.insertAdjacentElement('beforebegin', noteObj.createNoteBtn(articleUserId, undefined, [nameSet.addBtn, nameSet.homepageBtn], 'span'));
      }
      homepageNote(article);
      homepageCommentNote(article);
      noteObj.arrive(article, selector.homepage.commentAt, arriveOption, commentAt => {
        homepageCommentAtNote(commentAt);
      });
      noteObj.arrive(article, selector.dialog.commentId, arriveOption, picCommentUser => {
        dialogCommentNote(picCommentUser);
      });
      noteObj.arrive(article, selector.dialog.commentAt, arriveOption, picCommentAt => {
        dialogCommentAtNote(picCommentAt);
      });
    });
    noteObj.arrive(document.body, selector.homepageStories.idShell, arriveOption, homepageStories => {
      homepageStoriesNote(homepageStories);
    });
    noteObj.arrive(document.body, selector.homepageRecommend.id, arriveOption, homepageRecommend => {
      anchorElementNote(homepageRecommend);
    });
    noteObj.arrive(document.body, selector.userPage.frame, arriveOption, userPage => {
      const userPageBar = noteObj.fn.query(userPage, selector.userPage.bar);
      const userPageId = noteObj.fn.getText(userPage, selector.userPage.id);
      if (userPageBar && userPageId) {
        const userNameText = noteObj.fn.getText(userPage, selector.userPage.userName, 'info');
        userPageBar.after(noteObj.createNoteBtn(userPageId, userNameText, [nameSet.addBtn, nameSet.userpageBtn]));
      }
      userPageNote(userPage);
      userPageCommonNote(userPage);
      userPageInfoAtNote(userPage);
    });
    noteObj.arrive(document.body, selector.stories.idShell, arriveOption, storiesShell => {
      storiesNote(storiesShell);
      const stories = noteObj.fn.queryAnchor(storiesShell, selector.stories.id);
      if (stories) {
        const userIdChange = new MutationObserver(() => {
          const newUserId = noteObj.fn.getIdFromUrl(stories.href);
          if (noteObj.judgeUsers(newUserId)) {
            noteObj.handler(newUserId, stories);
          } else {
            noteObj.handler(newUserId, stories, undefined, {
              restore: true
            });
          }
        });
        userIdChange.observe(stories, {
          attributeFilter: ['href']
        });
      }
    });
    noteObj.arrive(document.body, selector.watchList.initialItem, arriveOption, initial => {
      watchListItemNote(initial);
    });
    noteObj.arrive(document.body, selector.watchList.laterItem, arriveOption, later => {
      watchListItemNote(later);
    });
    noteObj.arrive(document.body, selector.dialog.frame, arriveOption, dialog => {
      const homepageIcon = noteObj.fn.query(dialog, selector.homepage.icon);
      const dialogUserId = noteObj.fn.getUrlId(dialog, selector.homepage.id);
      if (homepageIcon && dialogUserId) {
        homepageIcon.insertAdjacentElement('beforebegin', noteObj.createNoteBtn(dialogUserId, undefined, [nameSet.addBtn, nameSet.homepageBtn], 'span'));
      }
      homepageNote(dialog);
      homepageCommentNote(dialog);
      noteObj.arrive(dialog, selector.dialog.commentId, arriveOption, commentUser => {
        dialogCommentNote(commentUser);
      });
      noteObj.arrive(dialog, selector.dialog.commentAt, arriveOption, commentAt => {
        dialogCommentAtNote(commentAt);
      });
    });
    noteObj.arrive(document.body, selector.request.follow, arriveOption, follow => {
      anchorElementNote(follow);
    });
    noteObj.arrive(document.body, selector.suggest.user, arriveOption, suggestUser => {
      anchorElementNote(suggestUser);
    });
    noteObj.arrive(document.body, selector.userPage.suggest, arriveOption, suggest => {
      anchorElementNote(suggest);
    });
  }
  initInstagram();
})();