Instagram - 為使用者新增備註(別名/標籤)

為使用者新增備註(別名/標籤)功能,以幫助識別和搜尋,並支援 WebDAV 同步功能

目前為 2023-04-12 提交的版本,檢視 最新版本

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

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

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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.7
// @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@2c441882e206c11de36f3042d5ad7eaa987efe46/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-12';
  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.docQueryAll(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.docQueryAll(selector.homepage.article, 'none')) {
      homepageNote(article, changeId);
      homepageCommentNote(article, changeId);
      for (const commentAt of noteObj.fn.docQueryAllAnchor(selector.homepage.commentAt, 'none')) {
        homepageCommentAtNote(commentAt, changeId);
      }
      for (const picCommentUser of noteObj.fn.docQueryAllAnchor(selector.dialog.commentId, 'none')) {
        dialogCommentNote(picCommentUser, changeId);
      }
      for (const picCommentAt of noteObj.fn.docQueryAllAnchor(selector.dialog.commentAt, 'none')) {
        dialogCommentAtNote(picCommentAt, changeId);
      }
    }
    for (const homepageStories of noteObj.fn.docQueryAll(selector.homepageStories.idShell, 'none')) {
      homepageStoriesNote(homepageStories, changeId);
    }
    for (const homepageRecommend of noteObj.fn.docQueryAllAnchor(selector.homepageRecommend.id, 'none')) {
      anchorElementNote(homepageRecommend, changeId);
    }
    for (const userPage of noteObj.fn.docQueryAll(selector.userPage.frame, 'none')) {
      userPageNote(userPage, changeId);
      userPageCommonNote(userPage, changeId);
      userPageInfoAtNote(userPage, changeId);
    }
    for (const storiesShell of noteObj.fn.docQueryAll(selector.stories.idShell, 'none')) {
      storiesNote(storiesShell, changeId);
    }
    for (const initial of noteObj.fn.docQueryAll(selector.watchList.initialItem, 'none')) {
      watchListItemNote(initial, changeId);
    }
    for (const later of noteObj.fn.docQueryAll(selector.watchList.laterItem, 'none')) {
      watchListItemNote(later, changeId);
    }
    for (const dialog of noteObj.fn.docQueryAll(selector.dialog.frame, 'none')) {
      homepageNote(dialog, changeId);
      homepageCommentNote(dialog, changeId);
      for (const commentUser of noteObj.fn.docQueryAllAnchor(selector.dialog.commentId, 'none')) {
        dialogCommentNote(commentUser, changeId);
      }
      for (const commentAt of noteObj.fn.docQueryAllAnchor(selector.dialog.commentAt, 'none')) {
        dialogCommentAtNote(commentAt, changeId);
      }
    }
    for (const follow of noteObj.fn.docQueryAllAnchor(selector.request.follow, 'none')) {
      anchorElementNote(follow, changeId);
    }
    for (const suggestUser of noteObj.fn.docQueryAllAnchor(selector.suggest.user, 'none')) {
      anchorElementNote(suggestUser, changeId);
    }
    for (const suggest of noteObj.fn.docQueryAllAnchor(selector.userPage.suggest, 'none')) {
      anchorElementNote(suggest, changeId);
    }
  }
  function instagramHomepageEvent(newValue, oldValue) {
    for (const article of noteObj.fn.docQueryAll(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();
})();