Instagram - Add notes to the user

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

  1. // ==UserScript==
  2. // @name Instagram - Add notes to the user
  3. // @name:zh-CN Instagram - 为用户添加备注(别名/标签)
  4. // @name:zh-TW Instagram - 為使用者新增備註(別名/標籤)
  5. // @namespace https://greasyfork.org/zh-CN/users/193133-pana
  6. // @homepage https://greasyfork.org/zh-CN/users/193133-pana
  7. // @icon data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0cHgiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiBhcmlhLWxhYmVsbGVkYnk9Im5ld0ljb25UaXRsZSIgc3Ryb2tlPSJyZ2JhKDI5LDE2MSwyNDIsMS4wMCkiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgZmlsbD0ibm9uZSIgY29sb3I9InJnYmEoMjksMTYxLDI0MiwxLjAwKSI+IDx0aXRsZSBpZD0ibmV3SWNvblRpdGxlIj5OZXc8L3RpdGxlPiA8cGF0aCBkPSJNMTkgMTRWMjJIMi45OTk5N1Y0SDEzIi8+IDxwYXRoIGQ9Ik0xNy40NjA4IDQuMDM5MjFDMTguMjQxOCAzLjI1ODE3IDE5LjUwODIgMy4yNTgxNiAyMC4yODkyIDQuMDM5MjFMMjAuOTYwOCA0LjcxMDc5QzIxLjc0MTggNS40OTE4NCAyMS43NDE4IDYuNzU4MTcgMjAuOTYwOCA3LjUzOTIxTDExLjU4NTggMTYuOTE0MkMxMS4yMTA3IDE3LjI4OTMgMTAuNzAyIDE3LjUgMTAuMTcxNiAxNy41TDcuNSAxNy41TDcuNSAxNC44Mjg0QzcuNSAxNC4yOTggNy43MTA3MSAxMy43ODkzIDguMDg1NzkgMTMuNDE0MkwxNy40NjA4IDQuMDM5MjFaIi8+IDxwYXRoIGQ9Ik0xNi4yNSA1LjI1TDE5Ljc1IDguNzUiLz4gPC9zdmc+
  8. // @version 6.1.8
  9. // @description Add notes (aliases/tags) for users to help identify and search, and support WebDAV sync
  10. // @description:zh-CN 为用户添加备注(别名/标签)功能,以帮助识别和搜索,并支持 WebDAV 同步功能
  11. // @description:zh-TW 為使用者新增備註(別名/標籤)功能,以幫助識別和搜尋,並支援 WebDAV 同步功能
  12. // @license GNU General Public License v3.0 or later
  13. // @compatible chrome
  14. // @compatible firefox
  15. // @author pana
  16. // @match *://*.instagram.com/*
  17. // @require https://gcore.jsdelivr.net/gh/LightAPIs/greasy-fork-library@da0437e6a856e05158df61225f2b9ea9943ad9ef/Note_Obj.js
  18. // @connect *
  19. // @noframes
  20. // @grant GM_info
  21. // @grant GM_getValue
  22. // @grant GM_setValue
  23. // @grant GM_deleteValue
  24. // @grant GM_listValues
  25. // @grant GM_openInTab
  26. // @grant GM_addStyle
  27. // @grant GM_registerMenuCommand
  28. // @grant GM_unregisterMenuCommand
  29. // @grant GM_addValueChangeListener
  30. // @grant GM_removeValueChangeListener
  31. // ==/UserScript==
  32.  
  33. (function () {
  34. 'use strict';
  35. const UPDATED = '2023-04-21';
  36. const INS_ICON = {
  37. NOTE_BLACK: 'url(data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjI0cHgiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDI0IDI0IiBhcmlhLWxhYmVsbGVkYnk9Im5ld0ljb25UaXRsZSIgc3Ryb2tlPSJyZ2IoMzgsIDM4LCAzOCkiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InNxdWFyZSIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgZmlsbD0ibm9uZSIgY29sb3I9InJnYigzOCwgMzgsIDM4KSI+IDx0aXRsZSBpZD0ibmV3SWNvblRpdGxlIj5OZXc8L3RpdGxlPiA8cGF0aCBkPSJNMTkgMTRWMjJIMi45OTk5N1Y0SDEzIi8+IDxwYXRoIGQ9Ik0xNy40NjA4IDQuMDM5MjFDMTguMjQxOCAzLjI1ODE3IDE5LjUwODIgMy4yNTgxNiAyMC4yODkyIDQuMDM5MjFMMjAuOTYwOCA0LjcxMDc5QzIxLjc0MTggNS40OTE4NCAyMS43NDE4IDYuNzU4MTcgMjAuOTYwOCA3LjUzOTIxTDExLjU4NTggMTYuOTE0MkMxMS4yMTA3IDE3LjI4OTMgMTAuNzAyIDE3LjUgMTAuMTcxNiAxNy41TDcuNSAxNy41TDcuNSAxNC44Mjg0QzcuNSAxNC4yOTggNy43MTA3MSAxMy43ODkzIDguMDg1NzkgMTMuNDE0MkwxNy40NjA4IDQuMDM5MjFaIi8+IDxwYXRoIGQ9Ik0xNi4yNSA1LjI1TDE5Ljc1IDguNzUiLz4gPC9zdmc+)'
  38. };
  39. const selector = {
  40. homepage: {
  41. article: '[role="main"] article',
  42. id: '._aaqt a',
  43. icon: 'span._aamz',
  44. commentId: '._ab8x .xt0psk2 a.notranslate',
  45. commentAt: '._ab8x ._aacl a.notranslate'
  46. },
  47. homepageStories: {
  48. id: '._aad6',
  49. idShell: 'li [role="menuitem"]'
  50. },
  51. homepageRecommend: {
  52. id: '._aak3 ._ab8x .xt0psk2 a'
  53. },
  54. userPage: {
  55. frame: '._aa_y',
  56. id: 'h2',
  57. bar: '.x8j4wrb',
  58. box: 'ul',
  59. common: 'span._aaai',
  60. suggest: '._acj1 a.notranslate',
  61. infoAt: '.notranslate',
  62. userName: '._aa_c > span'
  63. },
  64. watchList: {
  65. initialItem: '[role="dialog"] [aria-labelledby]',
  66. laterItem: '._aaei',
  67. id: '._ab8w a.notranslate'
  68. },
  69. stories: {
  70. id: 'a.notranslate',
  71. idShell: '._ac0o',
  72. cardId: '._afgd ._aacw'
  73. },
  74. dialog: {
  75. frame: '[role="dialog"] article',
  76. commentId: '._a9zc ._ab8w a, ._aacx._aacu a',
  77. commentAt: '._a9zs .notranslate'
  78. },
  79. request: {
  80. follow: '._aajc ._ab8x .xt0psk2 a'
  81. },
  82. suggest: {
  83. user: '._aa0- ._ab8x .xt0psk2 a'
  84. }
  85. };
  86. const nameSet = {
  87. backgroundBox: 'note-obj-ins-background-box',
  88. userpageTag: 'note-obj-ins-userpage-tag',
  89. fontBold: 'note-obj-ins-font-bold',
  90. addBtn: 'note-obj-ins-add-btn',
  91. homepageBtn: 'note-obj-ins-homepage-btn',
  92. userpageBtn: 'note-obj-ins-userpage-btn'
  93. };
  94. const INS_STYLE = `
  95. .${nameSet.backgroundBox} {
  96. display: inline-block;
  97. align-items: center;
  98. white-space: nowrap;
  99. border-radius: 50px;
  100. padding: 0px 10px;
  101. background-color: #336699;
  102. color: #fff;
  103. }
  104. .${nameSet.addBtn} {
  105. background-image: ${INS_ICON.NOTE_BLACK};
  106. background-size: 24px;
  107. background-repeat: no-repeat;
  108. background-position: center;
  109. margin-left: 5px;
  110. cursor: pointer;
  111. width: 24px;
  112. height: 24px;
  113. }
  114. .${nameSet.homepageBtn} {
  115. margin: 6px !important;
  116. }
  117. .${nameSet.homepageBtn}:hover {
  118. opacity: 0.5;
  119. }
  120. .${nameSet.userpageBtn} {
  121. margin-top: 2px;
  122. }
  123. .${nameSet.userpageTag} {
  124. display: block;
  125. font-size: 20px;
  126. margin-bottom: 20px;
  127. white-space: nowrap;
  128. }
  129. .${nameSet.fontBold} {
  130. font-weight: bold;
  131. }
  132. .note-obj-settings-frame-card label {
  133. color: #2f2f2f;
  134. }
  135. .note-obj-interface-dark .note-obj-settings-frame-card label {
  136. color: #fff;
  137. }
  138. .note-obj-group-frame-tbody input,
  139. .note-obj-webdav-frame-form-item input {
  140. color: #000;
  141. }`;
  142. const noteObj = new Note_Obj({
  143. id: 'myInstagramNote',
  144. script: {
  145. author: {
  146. name: 'pana',
  147. homepage: 'https://greasyfork.org/zh-CN/users/193133-pana'
  148. },
  149. url: 'https://greasyfork.org/scripts/387871',
  150. updated: UPDATED
  151. },
  152. style: INS_STYLE,
  153. primaryColor: '#336699',
  154. settings: {
  155. replaceHomepageID: {
  156. type: 'checkbox',
  157. lang: {
  158. en: 'Allow replacing user IDs on the home page',
  159. zhHans: '允许替换首页上的用户 ID',
  160. zhHant: '允許替換首頁上的使用者 ID'
  161. },
  162. default: true,
  163. event: instagramHomepageEvent
  164. }
  165. },
  166. changeEvent: instagramChangeEvent
  167. });
  168. function homepageNote(ele, changeId) {
  169. const user = noteObj.fn.queryAnchor(ele, selector.homepage.id);
  170. if (user) {
  171. const replaceHomepageID = noteObj.getOtherConfig().replaceHomepageID === true;
  172. const eleId = noteObj.fn.getIdFromUrl(user.href);
  173. if (!changeId || changeId === eleId) {
  174. noteObj.handler(eleId, user, undefined, {
  175. add: replaceHomepageID ? undefined : 'sapn',
  176. className: replaceHomepageID ? undefined : [nameSet.backgroundBox]
  177. });
  178. }
  179. }
  180. }
  181. function homepageCommentNote(ele, changeId) {
  182. for (const comment of noteObj.fn.queryAllAnchor(ele, selector.homepage.commentId, 'info')) {
  183. const commentId = noteObj.fn.getIdFromUrl(comment.href);
  184. if (!changeId || changeId === commentId) {
  185. noteObj.handler(commentId, comment);
  186. }
  187. }
  188. }
  189. function homepageCommentAtNote(ele, changeId) {
  190. const commentAtId = noteObj.fn.getIdFromUrl(ele.href);
  191. if (!changeId || changeId === commentAtId) {
  192. noteObj.handler(commentAtId, ele, undefined, {
  193. prefix: '@',
  194. title: true
  195. });
  196. }
  197. }
  198. function dialogCommentNote(ele, chagneId) {
  199. const picCommentId = noteObj.fn.getIdFromUrl(ele.href);
  200. if (!chagneId || chagneId === picCommentId) {
  201. noteObj.handler(picCommentId, ele);
  202. }
  203. }
  204. function dialogCommentAtNote(ele, changeId) {
  205. if (!ele.classList.contains(selector.homepage.commentId.replace(/^\.|\s+.*$/g, ''))) {
  206. const picCommentAtId = noteObj.fn.getIdFromUrl(ele.href);
  207. if (!changeId || changeId === picCommentAtId) {
  208. noteObj.handler(picCommentAtId, ele, undefined, {
  209. prefix: '@',
  210. title: true
  211. });
  212. }
  213. }
  214. }
  215. function homepageStoriesNote(ele, changeId) {
  216. const homepageStoriesId = noteObj.fn.getText(ele, selector.homepageStories.id);
  217. if (!changeId || changeId === homepageStoriesId) {
  218. ele.title = noteObj.getUserTag(homepageStoriesId);
  219. }
  220. }
  221. function anchorElementNote(ele, changeId) {
  222. const itemId = noteObj.fn.getIdFromUrl(ele.href);
  223. if (!changeId || changeId === itemId) {
  224. noteObj.handler(itemId, ele);
  225. }
  226. }
  227. function userPageNote(ele, changeId) {
  228. const userPageId = noteObj.fn.getText(ele, selector.userPage.id);
  229. const userPageBox = noteObj.fn.query(ele, selector.userPage.box);
  230. if (userPageId && userPageBox) {
  231. if (changeId) {
  232. if (changeId === userPageId) {
  233. noteObj.handler(userPageId, ele, undefined, {
  234. add: 'div',
  235. after: userPageBox,
  236. maskSecondaryColor: true,
  237. offsetWidth: -20,
  238. className: [nameSet.userpageTag, nameSet.fontBold]
  239. });
  240. }
  241. } else {
  242. const userNameText = noteObj.fn.getText(ele, selector.userPage.userName, 'warn');
  243. noteObj.handler(userPageId, ele, undefined, {
  244. add: 'div',
  245. after: userPageBox,
  246. maskSecondaryColor: true,
  247. offsetHeight: -20,
  248. className: [nameSet.userpageTag, nameSet.fontBold]
  249. }, userNameText);
  250. }
  251. }
  252. }
  253. function userPageCommonNote(ele, changeId) {
  254. for (const commonUser of noteObj.fn.queryAll(ele, selector.userPage.common, 'info')) {
  255. const commonUserId = commonUser.textContent?.trim();
  256. if (commonUserId) {
  257. if (!changeId || changeId === commonUserId) noteObj.handler(commonUserId, commonUser, undefined, {
  258. title: true,
  259. notModify: true
  260. });
  261. }
  262. }
  263. }
  264. function userPageInfoAtNote(ele, changeId) {
  265. for (const infoAtUser of noteObj.fn.queryAllAnchor(ele, selector.userPage.infoAt, 'info')) {
  266. const infoAtUserId = noteObj.fn.getIdFromUrl(infoAtUser.href);
  267. if (!changeId || changeId === infoAtUserId) {
  268. noteObj.handler(infoAtUserId, infoAtUser, undefined, {
  269. prefix: '@',
  270. title: true
  271. });
  272. }
  273. }
  274. }
  275. function storiesNote(ele, changeId) {
  276. itemNote(ele, selector.stories.id, changeId);
  277. noteObj.fn.queryAll(selector.stories.cardId).forEach(item => {
  278. const itemId = item.textContent?.trim() || '';
  279. if (!changeId || changeId === itemId) {
  280. noteObj.handler(itemId, item, undefined, {
  281. notModify: true,
  282. title: true
  283. });
  284. }
  285. });
  286. }
  287. function watchListItemNote(ele, changeId) {
  288. itemNote(ele, selector.watchList.id, changeId);
  289. }
  290. function itemNote(ele, idSelector, changeId) {
  291. const item = noteObj.fn.queryAnchor(ele, idSelector);
  292. if (item) {
  293. const itemId = noteObj.fn.getIdFromUrl(item.href);
  294. if (!changeId || changeId === itemId) {
  295. noteObj.handler(itemId, item);
  296. }
  297. }
  298. }
  299. function instagramChangeEvent(changeId) {
  300. for (const article of noteObj.fn.queryAll(selector.homepage.article, 'none')) {
  301. homepageNote(article, changeId);
  302. homepageCommentNote(article, changeId);
  303. for (const commentAt of noteObj.fn.queryAllAnchor(selector.homepage.commentAt, 'none')) {
  304. homepageCommentAtNote(commentAt, changeId);
  305. }
  306. for (const picCommentUser of noteObj.fn.queryAllAnchor(selector.dialog.commentId, 'none')) {
  307. dialogCommentNote(picCommentUser, changeId);
  308. }
  309. for (const picCommentAt of noteObj.fn.queryAllAnchor(selector.dialog.commentAt, 'none')) {
  310. dialogCommentAtNote(picCommentAt, changeId);
  311. }
  312. }
  313. for (const homepageStories of noteObj.fn.queryAll(selector.homepageStories.idShell, 'none')) {
  314. homepageStoriesNote(homepageStories, changeId);
  315. }
  316. for (const homepageRecommend of noteObj.fn.queryAllAnchor(selector.homepageRecommend.id, 'none')) {
  317. anchorElementNote(homepageRecommend, changeId);
  318. }
  319. for (const userPage of noteObj.fn.queryAll(selector.userPage.frame, 'none')) {
  320. userPageNote(userPage, changeId);
  321. userPageCommonNote(userPage, changeId);
  322. userPageInfoAtNote(userPage, changeId);
  323. }
  324. for (const storiesShell of noteObj.fn.queryAll(selector.stories.idShell, 'none')) {
  325. storiesNote(storiesShell, changeId);
  326. }
  327. for (const initial of noteObj.fn.queryAll(selector.watchList.initialItem, 'none')) {
  328. watchListItemNote(initial, changeId);
  329. }
  330. for (const later of noteObj.fn.queryAll(selector.watchList.laterItem, 'none')) {
  331. watchListItemNote(later, changeId);
  332. }
  333. for (const dialog of noteObj.fn.queryAll(selector.dialog.frame, 'none')) {
  334. homepageNote(dialog, changeId);
  335. homepageCommentNote(dialog, changeId);
  336. for (const commentUser of noteObj.fn.queryAllAnchor(selector.dialog.commentId, 'none')) {
  337. dialogCommentNote(commentUser, changeId);
  338. }
  339. for (const commentAt of noteObj.fn.queryAllAnchor(selector.dialog.commentAt, 'none')) {
  340. dialogCommentAtNote(commentAt, changeId);
  341. }
  342. }
  343. for (const follow of noteObj.fn.queryAllAnchor(selector.request.follow, 'none')) {
  344. anchorElementNote(follow, changeId);
  345. }
  346. for (const suggestUser of noteObj.fn.queryAllAnchor(selector.suggest.user, 'none')) {
  347. anchorElementNote(suggestUser, changeId);
  348. }
  349. for (const suggest of noteObj.fn.queryAllAnchor(selector.userPage.suggest, 'none')) {
  350. anchorElementNote(suggest, changeId);
  351. }
  352. }
  353. function instagramHomepageEvent(newValue, oldValue) {
  354. for (const article of noteObj.fn.queryAll(selector.homepage.article)) {
  355. const articleUser = noteObj.fn.queryAnchor(article, selector.homepage.id);
  356. if (articleUser) {
  357. const articleUserId = noteObj.fn.getIdFromUrl(articleUser.href);
  358. noteObj.handler(articleUserId, articleUser, undefined, {
  359. add: oldValue ? undefined : 'span',
  360. className: oldValue ? undefined : [nameSet.backgroundBox],
  361. title: oldValue,
  362. restore: true
  363. });
  364. noteObj.handler(articleUserId, articleUser, undefined, {
  365. add: newValue ? undefined : 'span',
  366. className: newValue ? undefined : [nameSet.backgroundBox],
  367. title: newValue
  368. });
  369. }
  370. }
  371. }
  372. function initInstagram() {
  373. const arriveOption = {
  374. fireOnAttributesModification: true,
  375. existing: true
  376. };
  377. noteObj.arrive(document.body, selector.homepage.article, arriveOption, article => {
  378. const homepageIcon = noteObj.fn.query(article, selector.homepage.icon);
  379. const articleUserId = noteObj.fn.getUrlId(article, selector.homepage.id);
  380. if (homepageIcon && articleUserId) {
  381. homepageIcon.insertAdjacentElement('beforebegin', noteObj.createNoteBtn(articleUserId, undefined, [nameSet.addBtn, nameSet.homepageBtn], 'span'));
  382. }
  383. homepageNote(article);
  384. homepageCommentNote(article);
  385. noteObj.arrive(article, selector.homepage.commentAt, arriveOption, commentAt => {
  386. homepageCommentAtNote(commentAt);
  387. });
  388. noteObj.arrive(article, selector.dialog.commentId, arriveOption, picCommentUser => {
  389. dialogCommentNote(picCommentUser);
  390. });
  391. noteObj.arrive(article, selector.dialog.commentAt, arriveOption, picCommentAt => {
  392. dialogCommentAtNote(picCommentAt);
  393. });
  394. });
  395. noteObj.arrive(document.body, selector.homepageStories.idShell, arriveOption, homepageStories => {
  396. homepageStoriesNote(homepageStories);
  397. });
  398. noteObj.arrive(document.body, selector.homepageRecommend.id, arriveOption, homepageRecommend => {
  399. anchorElementNote(homepageRecommend);
  400. });
  401. noteObj.arrive(document.body, selector.userPage.frame, arriveOption, userPage => {
  402. const userPageBar = noteObj.fn.query(userPage, selector.userPage.bar);
  403. const userPageId = noteObj.fn.getText(userPage, selector.userPage.id);
  404. if (userPageBar && userPageId) {
  405. const userNameText = noteObj.fn.getText(userPage, selector.userPage.userName, 'info');
  406. userPageBar.after(noteObj.createNoteBtn(userPageId, userNameText, [nameSet.addBtn, nameSet.userpageBtn]));
  407. }
  408. userPageNote(userPage);
  409. userPageCommonNote(userPage);
  410. userPageInfoAtNote(userPage);
  411. });
  412. noteObj.arrive(document.body, selector.stories.idShell, arriveOption, storiesShell => {
  413. storiesNote(storiesShell);
  414. const stories = noteObj.fn.queryAnchor(storiesShell, selector.stories.id);
  415. if (stories) {
  416. const userIdChange = new MutationObserver(() => {
  417. const newUserId = noteObj.fn.getIdFromUrl(stories.href);
  418. if (noteObj.judgeUsers(newUserId)) {
  419. noteObj.handler(newUserId, stories);
  420. } else {
  421. noteObj.handler(newUserId, stories, undefined, {
  422. restore: true
  423. });
  424. }
  425. });
  426. userIdChange.observe(stories, {
  427. attributeFilter: ['href']
  428. });
  429. }
  430. });
  431. noteObj.arrive(document.body, selector.watchList.initialItem, arriveOption, initial => {
  432. watchListItemNote(initial);
  433. });
  434. noteObj.arrive(document.body, selector.watchList.laterItem, arriveOption, later => {
  435. watchListItemNote(later);
  436. });
  437. noteObj.arrive(document.body, selector.dialog.frame, arriveOption, dialog => {
  438. const homepageIcon = noteObj.fn.query(dialog, selector.homepage.icon);
  439. const dialogUserId = noteObj.fn.getUrlId(dialog, selector.homepage.id);
  440. if (homepageIcon && dialogUserId) {
  441. homepageIcon.insertAdjacentElement('beforebegin', noteObj.createNoteBtn(dialogUserId, undefined, [nameSet.addBtn, nameSet.homepageBtn], 'span'));
  442. }
  443. homepageNote(dialog);
  444. homepageCommentNote(dialog);
  445. noteObj.arrive(dialog, selector.dialog.commentId, arriveOption, commentUser => {
  446. dialogCommentNote(commentUser);
  447. });
  448. noteObj.arrive(dialog, selector.dialog.commentAt, arriveOption, commentAt => {
  449. dialogCommentAtNote(commentAt);
  450. });
  451. });
  452. noteObj.arrive(document.body, selector.request.follow, arriveOption, follow => {
  453. anchorElementNote(follow);
  454. });
  455. noteObj.arrive(document.body, selector.suggest.user, arriveOption, suggestUser => {
  456. anchorElementNote(suggestUser);
  457. });
  458. noteObj.arrive(document.body, selector.userPage.suggest, arriveOption, suggest => {
  459. anchorElementNote(suggest);
  460. });
  461. }
  462. initInstagram();
  463. })();