您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Open the original source of an IG post, story or profile picture. No jQuery
当前为
// ==UserScript== // @name Instagram Source Opener // @version 1.1.10 // @description Open the original source of an IG post, story or profile picture. No jQuery // @author jomifepe // @icon https://www.instagram.com/favicon.ico // @match https://www.instagram.com/* // @grant GM_xmlhttpRequest // @grant GM.xmlHttpRequest // @grant GM_registerMenuCommand // @grant GM.registerMenuCommand // @grant GM_getValue // @grant GM.getValue // @grant GM_setValue // @grant GM.setValue // @namespace https://gf.qytechs.cn/users/192987 // ==/UserScript== /* jshint esversion: 8 */ (function () { 'use strict'; const LOGGING_ENABLED = false; const SCRIPT_NAME = 'Instagram Source Opener'; const SCRIPT_NAME_SHORT = 'ISO'; const LOGGING_TAG = `[${SCRIPT_NAME_SHORT}]`; const HOMEPAGE_URL = 'https://gf.qytechs.cn/en/scripts/372366-instagram-source-opener'; /* If you encounter a bug, please report at https://gf.qytechs.cn/en/scripts/372366-instagram-source-opener/feedback */ /* Instagram classes and selectors */ const IG_S_STORY_CONTAINER = '.yS4wN'; const IG_S_SINGLE_POST_CONTAINER = '.JyscU'; const IG_S_PROFILE_CONTAINER = '.v9tJq'; const IG_S_STORY_MEDIA_CONTAINER = '.qbCDp'; const IG_S_POST_IMG = '.FFVAD'; const IG_S_POST_VIDEO = '.tWeCl'; const IG_S_MULTI_POST_LIST_ITEMS = '.vi798 .Ckrof'; const IG_S_MULTI_POST_PREV_ARROW_BTN = '.POSa_'; const IG_S_MULTI_POST_NEXT_ARROW_BTN = '._6CZji'; const IG_S_POST_CONTAINER = '._8Rm4L'; const IG_S_POST_BUTTONS = '.eo2As > section'; const IG_C_PROFILE_PIC_CONTAINER = 'RR-M-'; const IG_C_PRIVATE_PROFILE_PIC_CONTAINER = 'M-jxE'; const IG_C_PRIVATE_PIC_IMG_CONTAINER = '_2dbep'; const IG_C_PRIVATE_PROFILE_PIC_IMG_CONTAINER = 'IalUJ'; const IG_S_PROFILE_USERNAME_TITLE = '.fKFbl'; const IG_S_POST_BLOCKER = '._9AhH0'; const IG_S_TOP_BAR = '.Hz2lF'; const IG_S_POST_TIME_ANCHOR = '.c-Yi7'; /* Custom classes and selectors */ const C_BTN_STORY = 'iso-story-btn'; const C_BTN_STORY_CONTAINER = 'iso-story-container'; const C_POST_WITH_BUTTON = 'iso-post'; const C_BTN_POST_OUTER_SPAN = 'iso-post-container'; const C_BTN_POST = 'iso-post-btn'; const C_BTN_POST_INNER_SPAN = 'iso-post-span'; const C_BTN_PROFILE_PIC_CONTAINER = 'iso-profile-pic-container'; const C_BTN_PROFILE_PIC = 'iso-profile-picture-btn'; const C_BTN_PROFILE_PIC_SPAN = 'iso-profile-picture-span'; const C_SETTINGS_PREFFIX = 'iso-settings'; const C_SETTINGS_CONTAINER = `${C_SETTINGS_PREFFIX}-container`; const C_SETTINGS_BTN = `${C_SETTINGS_PREFFIX}-btn`; const C_SETTINGS_MENU = `${C_SETTINGS_PREFFIX}-menu`; const C_SETTINGS_MENU_TITLE_CONTAINER = `${C_SETTINGS_PREFFIX}-menu-title-container`; const C_SETTINGS_MENU_TITLE = `${C_SETTINGS_PREFFIX}-menu-title`; const C_SETTINGS_MENU_TITLE_LINK = `${C_SETTINGS_PREFFIX}-menu-title-link`; const C_SETTINGS_MENU_TITLE_CLOSE_BTN = `${C_SETTINGS_PREFFIX}-menu-close-btn`; const C_SETTINGS_MENU_OPTIONS = `${C_SETTINGS_PREFFIX}-menu-options`; const C_SETTINGS_MENU_OPTION = `${C_SETTINGS_PREFFIX}-menu-option`; const ID_SETTINGS_POST_STORY_KB_BTN = `${C_SETTINGS_PREFFIX}-post-story-kb-btn`; const ID_SETTINGS_PROFILE_PICTURE_KB_BTN = `${C_SETTINGS_PREFFIX}-profile-picture-kb-btn`; const S_IG_POST_CONTAINER_WITHOUT_BUTTON = `${IG_S_POST_CONTAINER}:not(.${C_POST_WITH_BUTTON})`; /* Storage keys */ const POST_STORY_KB_STORAGE_KEY = 'iso_post_story_kb'; const PROFILE_PICTURE_KB_STORAGE_KEY = 'iso_profile_picture_kb'; /* Default letters for key bindings */ const DEFAULT_POST_STORY_KB = 'O'; const DEFAULT_PROFILE_PICTURE_KB = 'P'; const IG_POST_INFO_API_URL = (postRelUrl) => `https://www.instagram.com${postRelUrl}?__a=1`; const INSTA_DP_COM_URL = (username) => `https://www.instadp.com/fullsize/${username}`; const INSTA_DP_ORG_URL = 'https://instadp.org/'; const IZUUM_URL = 'https://izuum.com/index.php'; const trigger = { ARRIVE: 'arrive', LEAVE: 'leave' }; let isStoryKeyBindingSetup, isSinglePostKeyBindingSetup, isProfileKeyBindingSetup; let openPostStoryKeyBinding = DEFAULT_POST_STORY_KB; let openProfilePictureKeyBinding = DEFAULT_PROFILE_PICTURE_KB; //#region Logging utilities const log = (...args) => LOGGING_ENABLED && console.log(...[LOGGING_TAG, ...args]); const error = (...args) => LOGGING_ENABLED && console.error(...[LOGGING_TAG, ...args]); const warn = (...args) => LOGGING_ENABLED && console.warn(...[LOGGING_TAG, ...args]); const message = (...args) => alert(`${SCRIPT_NAME}:\n\n${args.join(' ')}`); const errorMessage = (msg, ...errorArgs) => { if (LOGGING_ENABLED) error(msg, ...errorArgs); message(msg); }; //#endregion /* Arrive.js library - https://github.com/uzairfarooq/arrive */ // eslint-disable-next-line const Arrive=function(e,t,n){'use strict';function r(e,t,n){l.addMethod(t,n,e.unbindEvent),l.addMethod(t,n,e.unbindEventWithSelectorOrCallback),l.addMethod(t,n,e.unbindEventWithSelectorAndCallback);}function i(e){e.arrive=f.bindEvent,r(f,e,'unbindArrive'),e.leave=d.bindEvent,r(d,e,'unbindLeave');}if(e.MutationObserver&&'undefined'!=typeof HTMLElement){var o=0,l=function(){var t=HTMLElement.prototype.matches||HTMLElement.prototype.webkitMatchesSelector||HTMLElement.prototype.mozMatchesSelector||HTMLElement.prototype.msMatchesSelector;return{matchesSelector:function(e,n){return e instanceof HTMLElement&&t.call(e,n);},addMethod:function(e,t,r){var i=e[t];e[t]=function(){return r.length==arguments.length?r.apply(this,arguments):'function'==typeof i?i.apply(this,arguments):n;};},callCallbacks:function(e,t){t&&t.options.onceOnly&&1==t.firedElems.length&&(e=[e[0]]);for(var n,r=0;n=e[r];r++)n&&n.callback&&n.callback.call(n.elem,n.elem);t&&t.options.onceOnly&&1==t.firedElems.length&&t.me.unbindEventWithSelectorAndCallback.call(t.target,t.selector,t.callback);},checkChildNodesRecursively:function(e,t,n,r){for(var i,o=0;i=e[o];o++)n(i,t,r)&&r.push({callback:t.callback,elem:i}),i.childNodes.length>0&&l.checkChildNodesRecursively(i.childNodes,t,n,r);},mergeArrays:function(e,t){var n,r={};for(n in e)e.hasOwnProperty(n)&&(r[n]=e[n]);for(n in t)t.hasOwnProperty(n)&&(r[n]=t[n]);return r;},toElementsArray:function(t){return n===t||'number'==typeof t.length&&t!==e||(t=[t]),t;}};}(),c=function(){var e=function(){this._eventsBucket=[],this._beforeAdding=null,this._beforeRemoving=null;};return e.prototype.addEvent=function(e,t,n,r){var i={target:e,selector:t,options:n,callback:r,firedElems:[]};return this._beforeAdding&&this._beforeAdding(i),this._eventsBucket.push(i),i;},e.prototype.removeEvent=function(e){for(var t,n=this._eventsBucket.length-1;t=this._eventsBucket[n];n--)if(e(t)){this._beforeRemoving&&this._beforeRemoving(t);var r=this._eventsBucket.splice(n,1);r&&r.length&&(r[0].callback=null);}},e.prototype.beforeAdding=function(e){this._beforeAdding=e;},e.prototype.beforeRemoving=function(e){this._beforeRemoving=e;},e;}(),a=function(t,r){var i=new c,o=this,a={fireOnAttributesModification:!1};return i.beforeAdding(function(n){var i,l=n.target;(l===e.document||l===e)&&(l=document.getElementsByTagName('html')[0]),i=new MutationObserver(function(e){r.call(this,e,n);});var c=t(n.options);i.observe(l,c),n.observer=i,n.me=o;}),i.beforeRemoving(function(e){e.observer.disconnect();}),this.bindEvent=function(e,t,n){t=l.mergeArrays(a,t);for(var r=l.toElementsArray(this),o=0;o<r.length;o++)i.addEvent(r[o],e,t,n);},this.unbindEvent=function(){var e=l.toElementsArray(this);i.removeEvent(function(t){for(var r=0;r<e.length;r++)if(this===n||t.target===e[r])return!0;return!1;});},this.unbindEventWithSelectorOrCallback=function(e){var t,r=l.toElementsArray(this),o=e;t='function'==typeof e?function(e){for(var t=0;t<r.length;t++)if((this===n||e.target===r[t])&&e.callback===o)return!0;return!1;}:function(t){for(var i=0;i<r.length;i++)if((this===n||t.target===r[i])&&t.selector===e)return!0;return!1;},i.removeEvent(t);},this.unbindEventWithSelectorAndCallback=function(e,t){var r=l.toElementsArray(this);i.removeEvent(function(i){for(var o=0;o<r.length;o++)if((this===n||i.target===r[o])&&i.selector===e&&i.callback===t)return!0;return!1;});},this;},s=function(){function e(e){var t={attributes:!1,childList:!0,subtree:!0};return e.fireOnAttributesModification&&(t.attributes=!0),t;}function t(e,t){e.forEach(function(e){var n=e.addedNodes,i=e.target,o=[];null!==n&&n.length>0?l.checkChildNodesRecursively(n,t,r,o):'attributes'===e.type&&r(i,t,o)&&o.push({callback:t.callback,elem:i}),l.callCallbacks(o,t);});}function r(e,t){return l.matchesSelector(e,t.selector)&&(e._id===n&&(e._id=o++),-1==t.firedElems.indexOf(e._id))?(t.firedElems.push(e._id),!0):!1;}var i={fireOnAttributesModification:!1,onceOnly:!1,existing:!1};f=new a(e,t);var c=f.bindEvent;return f.bindEvent=function(e,t,r){n===r?(r=t,t=i):t=l.mergeArrays(i,t);var o=l.toElementsArray(this);if(t.existing){for(var a=[],s=0;s<o.length;s++)for(var u=o[s].querySelectorAll(e),f=0;f<u.length;f++)a.push({callback:r,elem:u[f]});if(t.onceOnly&&a.length)return r.call(a[0].elem,a[0].elem);setTimeout(l.callCallbacks,1,a);}c.call(this,e,t,r);},f;},u=function(){function e(){var e={childList:!0,subtree:!0};return e;}function t(e,t){e.forEach(function(e){var n=e.removedNodes,i=[];null!==n&&n.length>0&&l.checkChildNodesRecursively(n,t,r,i),l.callCallbacks(i,t);});}function r(e,t){return l.matchesSelector(e,t.selector);}var i={};d=new a(e,t);var o=d.bindEvent;return d.bindEvent=function(e,t,r){n===r?(r=t,t=i):t=l.mergeArrays(i,t),o.call(this,e,t,r);},d;},f=new s,d=new u;t&&i(t.fn),i(HTMLElement.prototype),i(NodeList.prototype),i(HTMLCollection.prototype),i(HTMLDocument.prototype),i(Window.prototype);var h={};return r(f,h,'unbindAllArrive'),r(d,h,'unbindAllLeave'),h;}}(window,'undefined'==typeof jQuery?null:jQuery,void 0); const pages = { feed: { isVisible: () => window.location.pathname === '/', onLoadActions: () => { document.querySelectorAll(S_IG_POST_CONTAINER_WITHOUT_BUTTON) .forEach(node => generatePostButton(node)); } }, story: { isVisible: () => window.location.pathname.startsWith('/stories/'), onLoadActions: () => { const node = document.querySelector(IG_S_STORY_CONTAINER); if (!node) return; generateStoryButton(node); setupStoryEventListeners(); } }, profile: { isVisible: () => window.location.pathname.length > 1 && document.querySelector(IG_S_PROFILE_CONTAINER), onLoadActions: () => { const node = document.querySelector(IG_S_PROFILE_CONTAINER); if (!node) return; generateProfilePictureButton(node); setupProfileEventListeners(); } }, post: { isVisible: () => window.location.pathname.startsWith('/p/'), onLoadActions: () => { const node = document.querySelector(IG_S_SINGLE_POST_CONTAINER); if (!node) return; generatePostButton(node); setupSinglePostEventListeners(); } } }; const actionTriggers = { [trigger.ARRIVE]: { /* triggered whenever a new instagram post is loaded on the feed */ [S_IG_POST_CONTAINER_WITHOUT_BUTTON]: (node) => generatePostButton(node), /* triggered whenever a single post is opened (on a profile) */ [IG_S_SINGLE_POST_CONTAINER]: (node) => { generatePostButton(node); setupSinglePostEventListeners(); }, /* triggered whenever a story is opened */ [IG_S_STORY_CONTAINER]: (node) => { generateStoryButton(node); setupStoryEventListeners(); }, /* triggered whenever a profile page is loaded */ [IG_S_PROFILE_CONTAINER]: (node) => { generateProfilePictureButton(node); setupProfileEventListeners(); }, /* triggered whener the top bar is created */ [IG_S_TOP_BAR]: createSettingsPageMenu }, [trigger.LEAVE]: { /* triggered whenever a single post is closed (on a profile) */ [IG_S_SINGLE_POST_CONTAINER]: removeSinglePostEventListeners, /* triggered whenever a story is closed */ [IG_S_STORY_CONTAINER]: removeStoryEventListeners, /* triggered whenever a profile page is left */ [IG_S_PROFILE_CONTAINER]: removeProfileEventListeners } }; const profilePictureSources = { 'instadp.org': (username) => getProfilePictureFromInstadpDotOrg(username), 'instadp.com': (username) => getProfilePictureFromInstadpDotCom(username), 'izuum.com': (username) => getProfilePictureFromIzuum(username), 'current page sharedData': (username) => getProfilePictureFromSharedData(username), 'user data graphql API (?__a=1)': () => getProfilePictureFromUpdatedSharedData(), 'updated HTML profile page': () => getProfilePictureFromUpdatedHTMLPage() }; //#region Script setup and on load actions registerMenuCommands(); /* register GM menu commands */ injectStyles(); /* injects the needed CSS into DOM */ setupTriggers(); /* setup arrive and leave triggers for elements */ performOnLoadActions(); /* first load actions */ window.onload = performOnLoadActions; /* first load actions (backup) */ //#endregion /** * Setup the arrive and leave triggers for relevant elements */ function setupTriggers() { let count = 0; for (const [event, triggers] of Object.entries(actionTriggers)) { for (const [actuator, fireTrigger] of Object.entries(triggers)) { document[event](actuator, node => { if (!node) return; fireTrigger(node); log(`Triggered ${event} for selector ${actuator}`); }); count++; } } log(`SETUP: Created ${count} element triggers`); } /** * Actions that are performed on page load (window.onload) * Roughly the same as the ones performed by the triggers */ function performOnLoadActions() { for (const [name, page] of Object.entries(pages)) { if (page.isVisible()) { page.onLoadActions(); log(`Performed onload actions for ${name} page`); } } createSettingsPageMenu(); } //#endregion //#region Settings menu commands /** * Creates the commands to appear on the menu created by the <Any>monkey extension that's being used * For example, on Tampermonkey, this menu is accessible by clicking on the extension icon */ function registerMenuCommands() { /* eslint-disable no-undef */ try { GM_registerMenuCommand('Change post/story shortcut', handlePostStoryKBMenuCommand, null); GM_registerMenuCommand('Change profile picture shortcut', handleProfilePictureKBMenuCommand, null); log('SETUP: Registered menu commands using GM_registerMenuCommand'); } catch (err) { error('Failed to register menu commands using GM_registerMenuCommand'); try { GM.registerMenuCommand('Change post/story shortcut', handlePostStoryKBMenuCommand, null); GM.registerMenuCommand('Change profile picture shortcut', handleProfilePictureKBMenuCommand, null); log('Registered menu commands using GM.registerMenuCommand'); } catch (err) { error('Failed to register menu commands using GM.registerMenuCommand\nUse the on-page settings menu'); } } /* eslint-enable no-undef */ } /** * Handle the click on the settings menu option to change the single post and story opening key binding */ async function handlePostStoryKBMenuCommand() { const kb = await handleKBMenuCommand(POST_STORY_KB_STORAGE_KEY, DEFAULT_POST_STORY_KB, 'single post and story'); if (!kb) return; openPostStoryKeyBinding = kb; if (pages.post.isVisible()) { removeSinglePostEventListeners(); setupSinglePostEventListeners(); return; } if (pages.story.isVisible()) { removeStoryEventListeners(); setupStoryEventListeners(); return; } } /** * Handle the click on the settings menu option to change the profile picture opening key binding */ async function handleProfilePictureKBMenuCommand() { const kb = await handleKBMenuCommand(PROFILE_PICTURE_KB_STORAGE_KEY, DEFAULT_PROFILE_PICTURE_KB, 'profile picture'); if (!kb) return; openProfilePictureKeyBinding = kb; removeProfileEventListeners(); setupProfileEventListeners(); } /** * Generic handler for the click action on the key binding changing options of the settings menu. * Launches a prompt that asks the user for a new key binding for a specific action, saves it locally and returns it * @param {string} keyBindingStorageKey Unique name used to store the key binding * @param {string} defaultKeyBinding Default key binding, used on the prompt message * @param {string} keyBindingName Key binding name to show on log messages, just for context * @returns {Promise} Promise object, returns either the new key binding or a null if it failed */ async function handleKBMenuCommand(keyBindingStorageKey, defaultKeyBinding, keyBindingName) { let currentKey = null; try { // eslint-disable-next-line no-undef currentKey = await GM_getValue(keyBindingStorageKey, defaultKeyBinding); } catch (err) { error(`Failed to get current "${keyBindingName} key binding using GM_getValue`); } if (currentKey == null) { try { // eslint-disable-next-line no-undef currentKey = await GM.getValue(keyBindingStorageKey, defaultKeyBinding); } catch (err) { error(`Failed to get current "${keyBindingName} key binding using GM.getValue`); } } if (currentKey == null) { currentKey = defaultKeyBinding; log(`Falling back to default key binding: Alt + ${defaultKeyBinding}`); } const newKeyBinding = prompt(`${SCRIPT_NAME}:\n\nKey binding to open a ${keyBindingName}:\n` + 'Choose a letter to be combined with the Alt/⌥ key\n\n' + `Current key binding: Alt/⌥ + ${currentKey.toUpperCase()}`); if (newKeyBinding == null) return null; if (!isKeyBindingValid(newKeyBinding)) { errorMessage(`Couldn't save new key binding to open ${keyBindingName}, invalid option`); return null; } const successMessage = `Saved new shortcut to open ${keyBindingName}:\nAlt + ${newKeyBinding.toUpperCase()}`; try { // eslint-disable-next-line no-undef await GM_setValue(keyBindingStorageKey, newKeyBinding); message(successMessage); return newKeyBinding; } catch (gmuError) { error(`Failed to save new key binding to open ${keyBindingName} using GM_setValue`); try { // eslint-disable-next-line no-undef await GM.setValue(keyBindingStorageKey, newKeyBinding); message(successMessage); return newKeyBinding; } catch (gmdError) { errorMessage(`Failed to save new key binding to open ${keyBindingName} using GM.setValue`); return null; } } } //#endregion //#region Element creation /** * Creates a visual settings menu on the page, as an alternative to the commands menu method, * since it isn't supported by all extensions */ function createSettingsPageMenu() { if (!document.querySelector(`.${C_SETTINGS_BTN}`)) { /* Create the settings button */ const button = document.createElement('button'); button.classList.add(C_SETTINGS_BTN); button.setAttribute('type', 'button'); button.setAttribute('title', `Open ${SCRIPT_NAME_SHORT} settings`); button.addEventListener('click', () => setPageSettingsMenuVisibility(true)); document.querySelector(IG_S_TOP_BAR)?.appendChild(button); log('Created script settings button'); } if (!document.querySelector(`.${C_SETTINGS_CONTAINER}`)) { /* Create the settings menu */ const menu = document.createElement('div'); menu.innerHTML = `<div class="${C_SETTINGS_CONTAINER}"><div class="${C_SETTINGS_MENU}"><div class="${C_SETTINGS_MENU_TITLE_CONTAINER}"><div class="${C_SETTINGS_MENU_TITLE}">${SCRIPT_NAME_SHORT} Settings<a class="${C_SETTINGS_MENU_TITLE_LINK}" href="${HOMEPAGE_URL}" target="_blank" title="What's this?">(?)</a></div><button class="${C_SETTINGS_MENU_TITLE_CLOSE_BTN}" title="Close"><div class="coreSpriteClose"></div></button></div><div class="${C_SETTINGS_MENU_OPTIONS}"><button id="${ID_SETTINGS_POST_STORY_KB_BTN}" class="${C_SETTINGS_MENU_OPTION}">Change post/story shortcut</button><button id="${ID_SETTINGS_PROFILE_PICTURE_KB_BTN}" class="${C_SETTINGS_MENU_OPTION}">Change profile picture shortcut</button></div></div></div>`; menu.querySelector(`.${C_SETTINGS_MENU}`) ?.addEventListener('click', (event) => event.stopPropagation()); menu.querySelector(`.${C_SETTINGS_CONTAINER}`) ?.addEventListener('click', () => setPageSettingsMenuVisibility(false)); menu.querySelector(`.${C_SETTINGS_MENU_TITLE_CLOSE_BTN}`) ?.addEventListener('click', () => setPageSettingsMenuVisibility(false)); menu.querySelector(`#${ID_SETTINGS_POST_STORY_KB_BTN}`) ?.addEventListener('click', handlePostStoryKBMenuCommand); menu.querySelector(`#${ID_SETTINGS_PROFILE_PICTURE_KB_BTN}`) ?.addEventListener('click', handleProfilePictureKBMenuCommand); document.body.appendChild(menu); log('SETUP: Created script settings menu'); } } /** * Appends new elements to DOM containing the story source opening button * @param {Object} node DOM element node */ function generateStoryButton(node) { /* exits if the story button already exists */ if (!node || elementExistsInNode(`.${C_BTN_STORY_CONTAINER}`, node)) return; try { const buttonStoryContainer = document.createElement('div'); const buttonStory = document.createElement('button'); buttonStoryContainer.classList.add(C_BTN_STORY_CONTAINER); buttonStory.classList.add(C_BTN_STORY); buttonStoryContainer.setAttribute('title', 'Open source'); buttonStory.addEventListener('click', () => openStoryContent(node)); buttonStoryContainer.appendChild(buttonStory); node.appendChild(buttonStoryContainer); } catch (err) { error('Failed to generate story button', err); } } /** * Appends new elements to DOM containing the post source opening button * @param {Object} node DOM element node */ function generatePostButton(node) { /* exits if the post button already exists */ if (!node || elementExistsInNode(`.${C_BTN_POST_OUTER_SPAN}`, node)) return; try { /* removes the div that's blocking the img element on a post */ const blocker = node.querySelector(IG_S_POST_BLOCKER); if (blocker) blocker.parentNode.removeChild(blocker); const buttonsContainer = node.querySelector(IG_S_POST_BUTTONS); if (!buttonsContainer) { error(`Failed to generate post button, couldn't find by selector ${IG_S_POST_BUTTONS}`); return; } const newElementOuterSpan = document.createElement('span'); const newElementButton = document.createElement('button'); const newElementInnerSpan = document.createElement('span'); newElementOuterSpan.classList.add(C_BTN_POST_OUTER_SPAN); newElementButton.classList.add(C_BTN_POST); newElementInnerSpan.classList.add(C_BTN_POST_INNER_SPAN); newElementOuterSpan.setAttribute('title', 'Open source'); newElementButton.addEventListener('click', () => openPostSourceFromSrcAttribute(node)); newElementButton.appendChild(newElementInnerSpan); newElementOuterSpan.appendChild(newElementButton); buttonsContainer.appendChild(newElementOuterSpan); node.classList.add(C_POST_WITH_BUTTON); const timeElement = node.querySelector(`${IG_S_POST_TIME_ANCHOR} time`); if (timeElement) { const fullDateStr = timeElement.getAttribute('datetime'); if (fullDateStr) timeElement.innerHTML += ` (${fullDateStr})`; } } catch (err) { error('Failed to generate post button', err); } } /** * Appends new elements to DOM containing the profile picture source opening button * @param {Object} node DOM element node */ function generateProfilePictureButton(node) { /* exits if the profile picture button already exists */ if (!node || elementExistsInNode(`.${C_BTN_PROFILE_PIC_CONTAINER}`, node)) return; try { let profilePictureContainer = node.querySelector(`.${IG_C_PROFILE_PIC_CONTAINER}`); /* if the profile is private and the user isn't following or isn't logged in */ if (!profilePictureContainer) { profilePictureContainer = node.querySelector(`.${IG_C_PRIVATE_PROFILE_PIC_CONTAINER}`); } if (!profilePictureContainer) { error(`Failed to generate profile picture button, couldn't find by selector ${IG_C_PROFILE_PIC_CONTAINER}`); return; } const newElementOuterSpan = document.createElement('span'); const newElementButton = document.createElement('button'); const newElementInnerSpan = document.createElement('span'); newElementOuterSpan.setAttribute('title', 'Open full size picture'); newElementButton.addEventListener('click', e => { e.stopPropagation(); openProfilePicture(); }); newElementOuterSpan.classList.add(C_BTN_PROFILE_PIC_CONTAINER); newElementButton.classList.add(C_BTN_PROFILE_PIC); newElementInnerSpan.classList.add(C_BTN_PROFILE_PIC_SPAN); newElementButton.appendChild(newElementInnerSpan); newElementOuterSpan.appendChild(newElementButton); profilePictureContainer.appendChild(newElementOuterSpan); } catch (err) { error(err); } } /** * Handles clicks outside the settings menu when it's open * @param {Object} e */ function handlePageSettingsFocusLeave(e) { if (e.target.classList[0].startsWith(C_SETTINGS_PREFFIX)) return; setPageSettingsMenuVisibility(false); document.removeEventListener('mouseup', handlePageSettingsFocusLeave); } /** * Changes the visibility of the page settings menu * @param {boolean} visible */ function setPageSettingsMenuVisibility(visible) { if (visible) { document.querySelector(`.${C_SETTINGS_CONTAINER}`).style.display = 'flex'; /* add a listener to the whole page to close de menu on "focus leave" */ document.addEventListener('mouseup', handlePageSettingsFocusLeave); } else { document.querySelector(`.${C_SETTINGS_CONTAINER}`).style.display = 'none'; } } //#endregion //#region Content parsing logic and opening /** * Gets the story source url from the src attribute on the node and opens it in a new tab * @param {Object} node DOM element node */ function openStoryContent(node = null) { try { const container = (node || document).querySelector(IG_S_STORY_MEDIA_CONTAINER); const video = container.querySelector('video'); const image = container.querySelector('img'); if (video) { const source = getStoryVideoSrc(video); if (!source) throw 'Video source not available'; window.open(source, '_blank'); return; } if (image) { const source = getStoryImageSrc(image); if (!source) throw 'Video source not available'; window.open(source, '_blank'); return; } throw 'Story media source not available'; } catch (exception) { errorMessage('Failed to open story source', exception); } } /** * Get the source url of a story video * @param {Object} video DOM element node */ function getStoryVideoSrc(video) { try { const videoElement = video.querySelector('source'); return videoElement ? videoElement.getAttribute('src') : null; } catch (err) { error('Failed to get story video source', err); return null; } } /** * Get the source url of a story image * @param {Object} image DOM element node */ function getStoryImageSrc(image) { const defaultSrc = image.getAttribute('src'); try { const srcs = image.getAttribute('srcset').split(','); const images = srcs.map(src => { const [url, size] = src.split(' '); return { url, size: parseInt(size.replace(/[^0-9.,]/g, '')) }; }); return images ? images.reduce((pi, ci) => pi.size > ci.size ? pi : ci).url : defaultSrc; } catch (err) { error('Failed to get story image source', err); return defaultSrc || null; } } /** * Gets the source url of a post from the src attribute on the node and opens it in a new tab * @param {Object} node DOM element node */ async function openPostSourceFromSrcAttribute(node = document.querySelector(IG_S_SINGLE_POST_CONTAINER)) { /* if is on single post page and the node is null, the picture container can be found, since there's only one */ if (node == null) return; try { const postRelativeUrl = node.querySelector(IG_S_POST_TIME_ANCHOR)?.getAttribute('href'); const sourceListItems = node.querySelectorAll(IG_S_MULTI_POST_LIST_ITEMS); if (sourceListItems.length == 0 /* is single post */) { await openPostMediaSource(node, postRelativeUrl); return; } if (sourceListItems.length == 2 /* is on the first or last item */ ) { if (node.querySelector(IG_S_MULTI_POST_NEXT_ARROW_BTN) /* next arrow exist */ ) { await openPostMediaSource(sourceListItems[0], postRelativeUrl); /* opens last item */ } else if (node.querySelector(IG_S_MULTI_POST_PREV_ARROW_BTN) /* previous arrow exists */) { await openPostMediaSource(sourceListItems[1], postRelativeUrl); /* opens last item */ } else /* something is not right */ { errorMessage('Failed to open post source', 'Failed to open first or last post carousel item'); } } else if (sourceListItems.length == 3 /* is on any other item */) { await openPostMediaSource(sourceListItems[1], postRelativeUrl); } else /* something is not right */ { errorMessage('Failed to open post source', 'Failed to open post carousel item other than first and last'); } } catch (exception) { errorMessage('Failed to open post source', exception); } finally { document.body.style.cursor = 'default'; } } /** * Gets the source url of a post from the src attribute on the node and opens it in a new tab * @param {Object} node DOM element node */ async function openPostMediaSource(node, postRelativeUrl) { let image = node.querySelector(IG_S_POST_IMG); let video = node.querySelector(IG_S_POST_VIDEO); if (image) { window.open(image.getAttribute('src'), '_blank'); return; } if (video) { /* video url is available on the element */ const videoSrc = video.getAttribute('src'); if (!videoSrc?.startsWith('blob')) { window.open(videoSrc, '_blank'); return; } if (!postRelativeUrl) throw 'No post relative url found'; /* try to get the video url using the ig api */ document.body.style.cursor = 'wait'; const response = await httpGETRequest(IG_POST_INFO_API_URL(postRelativeUrl), true); const postData = response?.graphql?.shortcode_media; if (!postData) throw 'No post data found'; if (!postData.is_video) throw 'Post is not a video'; if (!postData.video_url) throw 'No video url found'; window.open(postData.video_url, '_blank'); return; } throw 'Failed to open source, no media found'; } /** * Tries to get the source URL of the user's profile picture using multiple methods, including 3rd party websites * Opens the image in a new tab or shows an alert if it doesn't find any URL */ async function openProfilePicture() { try { const pageUsername = document.querySelector(IG_S_PROFILE_USERNAME_TITLE)?.innerText; if (!pageUsername) throw 'Couldn\'t find username'; document.body.style.cursor = 'wait'; let pictureUrl = null; for (const [sourceName, getProfilePicture] of Object.entries(profilePictureSources)) { log(`Trying to get user's profile picture from ${sourceName}`); const url = await getProfilePicture(pageUsername); if (!url) { error(`Couldn't get profile picture url from ${sourceName}`); continue; } pictureUrl = url; break; } if (!pictureUrl) throw 'Nothing returned from 3rd party sources'; log('Profile picture found, opening in a new tab...'); window.open(pictureUrl, '_blank'); } catch (err) { errorMessage('Couldn\'t get user\'s profile picture', err); } finally { document.body.style.cursor = 'default'; } } //#endregion //#region Content sources /** * Parses a whole HTML page in order to get the user's profile picture URL * @returns {Promise} Promise object, returns the user's profile picture URL or a null if it fails */ async function getProfilePictureFromUpdatedHTMLPage() { const response = await httpGETRequest(window.location, false); if (response) { const parser = new DOMParser(); const doc = parser.parseFromString(response, 'text/html'); const allScripts = doc.querySelectorAll('script'); for (let i = 0; i < allScripts.length; i++) { if (/window._sharedData/.test(allScripts[i].innerText)) { let extractedJSON = /window._sharedData = (.+)/.exec(allScripts[i].innerText)[1]; extractedJSON = extractedJSON.slice(0, -1); let sharedData = JSON.parse(extractedJSON); let userInfo = sharedData.entry_data.ProfilePage[0].graphql.user.profile_pic_url_hd; return userInfo; } } } return null; } function getProfilePictureFromSharedData(username) { const userSharedData = window._sharedData.entry_data.ProfilePage[0].graphql.user; /* return picture url if sharedData is correct */ return userSharedData.username === username ? userSharedData.profile_pic_url_hd : null; } /** * Requests the user profile page data from graphql in order to get its profile picture URL * @returns {Promise} Promise object, returns the user's profile picture URL or a null if it fails */ async function getProfilePictureFromUpdatedSharedData() { const response = await httpGETRequest(`${window.location}?__a=1`); return response ? response.graphql.user.profile_pic_url_hd : null; } /** * Performs a request to instadp.com and extracts the profile picture URL from the HTML response * @todo Adapt to be able to get pictures from a user that was never searched on their website * @param {string} username The user's Instagram username * @returns {Promise} Promise object, returns the user's profile picture URL or a null if it fails */ async function getProfilePictureFromInstadpDotCom(username) { /* Instadp.com has a different process and requires a POST, probably to populate their database If you ever searched for a user on their website, this request succeeds, otherwise it fails */ const response = await httpGETRequest(INSTA_DP_COM_URL(username), false); if (!response) return null; const urls = extractUrlsFromString(response); const instagramUrls = urls.filter(u => u.includes('cdninstagram') && !u.includes('s150x150')); return instagramUrls.length > 0 ? instagramUrls[instagramUrls.length - 1] : null; } /** * Performs a request to instadp.org and extracts the profile picture URL from the HTML response * @param {string} username The user's Instagram username * @returns {Promise} Promise object, returns the user's profile picture URL or a null if it fails */ async function getProfilePictureFromInstadpDotOrg(username) { const response = await httpPOSTRequest(INSTA_DP_ORG_URL, {'Content-Type': 'application/x-www-form-urlencoded'}, `username=${username}`, false); if (!response) return null; const instagramUrls = extractUrlsFromString(response).filter(u => u.includes('cdninstagram')); return instagramUrls.length > 0 ? instagramUrls[0] : null; } /** * Performs a request to izuum.com and extracts the profile picture URL from the HTML response * @param {string} username The user's Instagram username * @returns {Promise} Promise object, returns the user's profile picture URL or a null if it fails */ async function getProfilePictureFromIzuum(username) { const response = await httpPOSTRequest(IZUUM_URL, {'Content-Type': 'application/x-www-form-urlencoded'}, `submit=${username}`, false); if (!response) return null; const instagramUrls = extractUrlsFromString(response).filter(u => u.includes('cdninstagram')); const cleanUrls = instagramUrls.map(u => u.replace(/amp;/g, '')); return cleanUrls.length > 0 ? cleanUrls[0] : null; } //#endregion //#region Key bindings and other event listeners /** * Loads the key bind to open a single post or a story from storage into a global scope variable, in order * to be used on the key binding handler method * @returns {Promise} Promise object, resolved after loading the key bindings */ async function loadPostStoryKeyBindings() { const kbName = 'single post and story'; try { const kb = await loadKeyBindingFromStorage(POST_STORY_KB_STORAGE_KEY, DEFAULT_POST_STORY_KB, kbName); if (kb) openPostStoryKeyBinding = kb; } catch (err) { error(`Failed to load "${kbName}" key binding, considering default (Alt + ${DEFAULT_POST_STORY_KB})`, err); } } /** * Loads the key bind to open a profile picture from storage into a global scope variable in order * to be used on the key binding handler method * @returns {Promise} Promise object, resolved after loading key bindings */ async function loadProfilePictureKeyBindings() { const kbName = 'profile picture'; try { const kb = await loadKeyBindingFromStorage(PROFILE_PICTURE_KB_STORAGE_KEY, DEFAULT_PROFILE_PICTURE_KB, kbName); if (kb) openProfilePictureKeyBinding = kb; } catch (err) { error(`Failed to load "${kbName}" key binding, considering default (Alt + ${DEFAULT_PROFILE_PICTURE_KB})`, err); } } /** * Loads a key binding from storage, if it fails or doesn't have anything stores, returns the fallback key binding * @param {string} storageKey Unique name used to store the key binding * @param {string} defaultKeyBinding Fallback key binding * @param {string} keyBindingName Key binding name to show on log messages, just for context * @returns {Promise} Promise object, returns the loaded key and when or a null if it fails */ async function loadKeyBindingFromStorage(storageKey, defaultKeyBinding, keyBindingName) { let kb = null; try { // eslint-disable-next-line no-undef kb = await GM_getValue(storageKey, defaultKeyBinding); } catch (err) { error('Failed to load key binding from storage using GM_getValue'); } if (kb == null) { try { // eslint-disable-next-line no-undef kb = await GM.getValue(storageKey, defaultKeyBinding); } catch (err) { error('Failed to load key binding from storage using GM.getValue'); } } if (kb == null) { kb = defaultKeyBinding; log(`Falling back to default key binding: Alt + ${defaultKeyBinding}`); } try { if (isKeyBindingValid(kb)) { const newKey = kb.toUpperCase(); log(`Discovered ${keyBindingName} key binding: Alt + ${newKey}`); return newKey; } else { error(`Couldn't load "${keyBindingName}" key binding, "${kb}" key is invalid, using default (Alt + ${defaultKeyBinding})`); return defaultKeyBinding; } } catch (err) { if (kb != defaultKeyBinding) { error(`Failed to load "${keyBindingName}" key binding, falling back to default: Alt + ${defaultKeyBinding}`, err); } return null; } } /** * Adds event listener(s) to the current document meant to handle key presses on a single post page */ function setupSinglePostEventListeners() { setupKBEventListener(isSinglePostKeyBindingSetup, loadPostStoryKeyBindings, handleSinglePostKeyPress, () => { isSinglePostKeyBindingSetup = true; }, 'Defined single post opening event listener'); } /** * Adds event listener(s) to the current document meant to handle key presses on a story page */ function setupStoryEventListeners() { setupKBEventListener(isStoryKeyBindingSetup, loadPostStoryKeyBindings, handleStoryKeyPress, () => { isStoryKeyBindingSetup = true; }, 'Defined story opening event listener'); } /** * Adds event listener(s) to the current document meant to handle key presses on a profile page */ function setupProfileEventListeners() { setupKBEventListener(isProfileKeyBindingSetup, loadProfilePictureKeyBindings, handleProfileKeyPress, () => { isProfileKeyBindingSetup = true; }, 'Defined profile picture opening event listener'); } /** * Generic method to add an event listener for a key binding * @param {boolean} condition condition that determines if the event should be added * @param {*} keyPressHandler handler function for the event (key binding press) * @param {*} callback function to call after adding the event listener * @param {*} logMessage string logged after adding the event listener */ function setupKBEventListener(condition, loadingFn, keyPressHandler, callback, logMessage) { if (condition) return; loadingFn().then(() => { document.addEventListener('keydown', keyPressHandler); callback(); log(logMessage); }); } /** * Removes the previously added event listener(s) meant to handle key presses on a single post page */ function removeSinglePostEventListeners() { removeKBEventListeners(isSinglePostKeyBindingSetup, handleSinglePostKeyPress, () => { isSinglePostKeyBindingSetup = false; }, 'Removed single post opening event listener'); } /** * Removes the previously added event listener(s) meant to handle key presses on a story page */ function removeStoryEventListeners() { removeKBEventListeners(isStoryKeyBindingSetup, handleStoryKeyPress, () => { isStoryKeyBindingSetup = false; }, 'Removed story opening event listener'); } /** * Removes the previously added event listener(s) meant to handle key presses on a profile page */ function removeProfileEventListeners() { removeKBEventListeners(isProfileKeyBindingSetup, handleProfileKeyPress, () => { isProfileKeyBindingSetup = false; }, 'Removed profile picture opening event listener'); } /** * Generic method to remove an event listener for a key binding * @param {boolean} condition condition that determines if the event should be removed * @param {*} keyPressHandler handler function previously assigned to the event * @param {*} callback function to call after removing the event listener * @param {*} logMessage string logged after removing the event listener */ function removeKBEventListeners(condition, keyPressHandler, callback, logMessage) { if (!condition) return; document.removeEventListener('keydown', keyPressHandler); callback(); log(logMessage); } /** * Handles key up events on a story page * @param {Object} e Event object */ function handleStoryKeyPress(e) { handleKeyPress(e, openPostStoryKeyBinding, () => pages.story.isVisible(), 'Detected source opening shortcut on a story page', openStoryContent); } /** * Handles key up events on a single post page * @param {Object} e Event object */ function handleSinglePostKeyPress(e) { handleKeyPress(e, openPostStoryKeyBinding, () => pages.post.isVisible(), 'Detected source opening shortcut on a single post page', openPostSourceFromSrcAttribute); } /** * Handles key up events on a profile page * @param {Object} e Event object */ function handleProfileKeyPress(e) { handleKeyPress(e, openProfilePictureKeyBinding, () => !(pages.story.isVisible() || pages.post.isVisible()), 'Detected profile picture opening shortcut on a profile page', openProfilePicture); } /** * Handles key up with the alt key events on certain conditions and performs an action * @param {Object} event Event object * @param {string} keyBinding Target key binding (letter) * @param {*} checkConditionsAreMet Function that determines if the conditions are met * @param {string} logMessageString Message logged when the keybinding is used and the conditions are met * @param {*} keyPressAction Function executed when the keybinding is used and the conditions are met */ function handleKeyPress(event, keyBinding, checkConditionsAreMet, logMessageString, keyPressAction) { if (event.altKey && event.code.toLowerCase() === `key${keyBinding.toLowerCase()}` && checkConditionsAreMet()) { log(logMessageString); keyPressAction(); } } //#endregion //#region Networking /** * Performs an HTTP GET request using the GM_xmlhttpRequest or GM.xmlHttpRequest function * @param {string} url * @param {boolean} [parseToJSON = true] default true * @returns {Promise} Promise object, when resolved contains the response text and * when rejected contains a message or an exception error object */ async function httpGETRequest(url, parseToJSON = true) { return new Promise((resolve, reject) => { const options = { method: 'GET', url: url, timeout: 10000, onload: res => { if (res.status !== 200) { reject('Status Code', res?.status, res?.statusText || ''); return; } let response = res.responseText; if (parseToJSON) { response = JSON.parse(res.responseText); } resolve(response); }, onerror: error => reject(error), ontimeout: () => reject('Request Timeout'), onabort: () => reject('Request Aborted') }; try { // eslint-disable-next-line no-undef GM_xmlhttpRequest(options); } catch (gmuError) { error('Failed to perform GET request using GM_xmlhttpRequest'); try { // eslint-disable-next-line no-undef GM.xmlHttpRequest(options); } catch (gmdError) { error('Failed to perform GET request using GM.xmlHttpRequest'); } } }); } /** * Performs an HTTP POST request using the GM_xmlhttpRequest or GM.xmlHttpRequest function * @param {string} url * @param {Object} [headers = null] default null * @param {string} [data = null] default null * @param {boolean} [parseToJSON = true] default true * @returns {Promise} Promise object, when resolved contains the response text and * when rejected contains a message or an exception error object */ async function httpPOSTRequest(url, headers = null, data = null, parseToJSON = true) { return new Promise((resolve, reject) => { const options = { method: 'POST', url: url, ...(headers && {headers: headers}), ...(data && {data: data}), timeout: 10000, onload: res => { if (res.status !== 200) { reject('Status Code', res?.status, res?.statusText || ''); return; } let response = res.responseText; if (parseToJSON) { response = JSON.parse(res.responseText); } resolve(response); }, onerror: error => reject(error), ontimeout: () => reject('Request Timeout'), onabort: () => reject('Request Aborted') }; try { // eslint-disable-next-line no-undef GM_xmlhttpRequest(options); } catch (gmuError) { error('Failed to perform POST request using GM_xmlhttpRequest'); try { // eslint-disable-next-line no-undef GM.xmlHttpRequest(options); } catch (gmdError) { error('Failed to perform POST request using GM.xmlHttpRequest'); } } }); } //#endregion //#region Utils & others /** * Check if the key is valid to used as a key binding * @param {string} key Key binding key * @returns {boolean} true is the key is valid, false otherwise */ function isKeyBindingValid(key) { return /[a-zA-Z]/gm.test(key); } /** * Extracts every URL found between quotes and double quotes in a given string * Note: This regex is not bullet proof and has unnecessary rules, but it's works fine * @param {string} string String to match */ function extractUrlsFromString(string) { return string.match(/(?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-A-Z0-9+&@#\/%=~_|$?!:;,.]*\)|[-A-Z0-9+&@#\/%=~_|$?!:;,.])*(?:\([-A-Z0-9+&@#\/%=~_|$?!:;,.]*\)|[A-Z0-9+&@#\/%=~_|$])/igm); } /** * Matches a CSS selector against a DOM element object to check if the element exist in the node * @param {string} selector * @param {Object} node DOM element node * @returns {boolean} true if the element exists in the node, otherwise false */ function elementExistsInNode(selector, node) { return (node && node.querySelector(selector) != null); } /** * Appends the necessary style elements to DOM */ function injectStyles() { const b64PostBtnIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAB9wAAAfcBHrop/AAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAViSURBVHic7ZtfaB1FFMZ/s7ShDUn6T0mTYlGslZaioEYrpIixVQs1FfWlRVCfpOJLVfAPQl8UtYIgttIXFcE+iFZqqVbpQ60oNaYq+FJQtEFJ1bRJq4nGNE3Gh9kLcw+7m9nduXOT1A+GsJM7Z77z7cyZ2bOzSmuNTyilWoA7gTVAe1yWxX+bgd+BfuBUXPqBz4Be7ZuMC18ffSql2oFuYDPQBTQUMHMKOADsB45orc+XJuYCrXXhAnQAh4FJQHssfwLPAwvK8HPyoaDjK4H3cjg0CZwGTgL/5Gh3BtgONEwLAYAFwB5gPIP0WeAdYCuwFlguHQAWAquA24AngWNTjKKTwH11FQBYAZxIITgM7AbWA3MLjqo24GHgaIYQLwNRcAEwgW0ogdA48DrQ6pUU3A58lyLCxz5jgwuZR1KG/D5gZS2GZdyviqdRX0LfJ4Crai5A7HzSXd9WK8cTOCzCrDSSxymgvWYCxMNe3vlBoCuU8xaXOcCuBBG+BuZ5FyAOeHLO/wCsCO284LUtYbXY61UAzFIno/1gvZ23+D2bMBKe8inAnoQ5H3zYTyHCu4LjBLCmtACYHZ6c98ECXg4BGoFvBc+DPgSQ29t99XY2Q4QrgTHB95bCAmAebGxj56fLvM8Q4RXBuaeMAHKtfbXeDjoIsBjz7GHzvje3AJhkhb28nAMuqbeDjiI8IQT4ME/7CINuzNazgje01meYGdgN/G1db1BKNbo2rgiwWdQfKMsqFLTWo5jpW8F84A7X9lGcw+uy6oaAL/zQCwZ5w+52bRhhEph2Du8jrfWED1YBcRATwyrYpJSK0n5sI8Jkb20c8sUqFLTWp4HjVtViYKlL2wizAtj4yROv0JC8l7k0ShKg3wud8JC8pV+JiKhWagLz4mImorAA9g//mIEBsILCAjRb18Pe6ISH5N6c+CuBiOoh3+aNTnhI7gMujSKqh06LUqrJG6WwkFHfKZZFmOxqlqGZgv8FENfOAsjo2eGFTkAopRRwvVU1iXmfOCUizOEEG91+aAXFDVQve8e11mddGkZAL9XTYK1S6lKP5EJA3rRPXRtG2qRV7MfJCNjkg1VASAE+cW1YeWTcL+q3lKITEEqp1cA1VtU5oMe1fUWAI8BfVv0GpdSt5ekFwQvi+u1c23krufgc1cnFXuJDVNO1AOsE53/J+cbYNtaC2T7aBrfU28kpBPhK8N2V24Yw+Kgw2AcsqrejKc4/JLiOAZeVFWAu8KMwfBiYU2+HBc8OYFTw3FnIVoLxu4ThQkOrhs63YXavNr9vKHiULq0TGRCnxVtiYB5mibN5DVPivFBaRwqzN7A7mgSeqaPzSxOCngYeKGU3o8Mm4PuEDvdS8lxOAeevA35N4DIOdNZEgLjjK4BfEjruAZYHcn4r5t2f5GBPgcIiuBBoBb5M6HgUeAlYWCPHbwY+T3FanmIpLIIrmQbgzRQyg8DjwHxPjq8GPkjpawR4EOiMnS4tQl5y24ELGeTeB+4n5+YJuBbYQfrxWB3/72qrjRcRcn8woZRaBbxIduLkAuYE+M+YXMNv8d8RzJRqx6zn7ZhkxuUZtsYxhySf1lqPCS6dmHeZdiJ3BNiotXZ7w11iqK4jeVnyVUaB15hie0vJkeBjzt6D2TPk+RAiqwwAO8lxAr2MCKUFsEg0Yk6avIX5OsTV4Yl4JO0AbqTg9wBFRfDy0ZREfDihFbN7a7NKcyzOgFX6tNZDnvrNHRNqIkA9kVcEp2MkMwmxkxsxTlfQBByKxanCrBMA8okwKwUAdxFmrQDgJsKsC4JJyAiM6y8KASBVhKOzegrYSJkOSy6aEVCBUuomTB5jCfDYf7qkKwGO/Em3AAAAAElFTkSuQmCC'; const b64StoryBtnIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAeFBMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////GqOSsAAAAJ3RSTlMAAgMLDRQVFkVOUGFiaIKap6ipqquszM/Q0dbY5ufv8PP0+fr8/f7DeTSmAAAAAWJLR0QnLQ+oIwAAALpJREFUOMvNk90SgiAUBj8tzdK0JCv7tdJ4/zfsqIgI6Ex37o3M7MoZRgRmgp+9eEuZAKtrvxZkXPIAgm5dyoDeP7GG3RpwY1rc6kIGnJ+d4dCo0gJm+Oo+EZD/btl40HiMB60fD+r5ET1jfrEGnYcb+LZA7K/RB3bfB3J/YvNMjED12CvfQgSh6i3HXH5Ubwnc4zvEVABngelgyJ+BfmFoXq4EdOVypkG+kMGBW0ll4LHC1EXqzeW3/AFfeiRd23tgxAAAAABJRU5ErkJggg=='; const b64SettingsBtnIcon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAAA8klEQVRYhe2WMQrCMBSGP8ULCA7eofQe4qyT3sOtxs2zFZcuHkAP0UGhDiYYJG2T9rUO5odAeHm8/yN5CYGoeq2BO1B5jhxYSAKEmJtRSEKYor55hTREKMAcuOj5FViODYA0RBcAUYiuAGIQfQDg3YimMfMhAXyv62AAK+D2S4BedaYCBr0UASLAbASPSdPiz3cgAkgDKCCriZ9Di4U+xUrnP4DEiqc6VumcQQCM+RPYO9a3FoT3TvgC2Oa7hryNC0KiB0pr3nTn7bWyNstSyBEc8D+Co2fN1p/OtxSfJkyteELHJmz76bikgJMjnoWa/49eJ0SVuX3p8rIAAAAASUVORK5CYII='; const opacityTransition = 'transition:opacity .2s ease-in-out;-webkit-transition:opacity .2s ease-in-out;-moz-transition:opacity .2s ease-in-out;-ms-transition:opacity .2s ease-in-out;-o-transition:opacity .2s ease-in-out;'; const styles = ` .${C_BTN_POST_OUTER_SPAN}{margin-left:8px;} .${C_BTN_POST}{outline:none;-webkit-box-align:center;align-items:center;background:0;border:0;cursor:pointer;display:flex;-webkit-box-flex:0;flex-grow:0;-webkit-box-pack:center;justify-content:center;padding:8px 0 8px 8px;} .${C_BTN_POST_INNER_SPAN},.${C_BTN_PROFILE_PIC_SPAN}{display:block;background-repeat:no-repeat;height:24px;width:24px;background-image:url(${b64PostBtnIcon});background-size:24px 24px;cursor:pointer;opacity:1;${opacityTransition};} .${C_BTN_POST_INNER_SPAN}:hover{opacity:.6;} .${C_BTN_PROFILE_PIC}{outline:none;background-color:white;border:0;cursor:pointer;min-height:40px;min-width:40px;padding:0;border-radius:50%;transition:background-color .5s ease;-webkit-transition:background-color .5s ease;} .${C_BTN_PROFILE_PIC}:hover{background-color:#D0D0D0;transition:background-color .5s ease;-webkit-transition:background-color .5s ease;} .${C_BTN_PROFILE_PIC_SPAN}{margin: auto;} .${C_BTN_STORY_CONTAINER}{position:fixed;top:0;right:0;margin:16px;} .${C_BTN_STORY}{width:24px;height:24px;border:none;cursor:pointer;background-color:transparent;background-image:url(${b64StoryBtnIcon});background-size:24px 24px;opacity:.8;${opacityTransition}} .${C_BTN_STORY}:hover{opacity:1;} .${C_BTN_PROFILE_PIC_CONTAINER}{transition:.5s ease;opacity:0;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);text-align:center} .${IG_C_PRIVATE_PIC_IMG_CONTAINER}>img{transition:.5s ease;backface-visibility:hidden;} .${IG_C_PRIVATE_PROFILE_PIC_IMG_CONTAINER}>img{transition:.5s ease;backface-visibility:hidden;} .${IG_C_PROFILE_PIC_CONTAINER}:hover .${C_BTN_PROFILE_PIC_CONTAINER}{opacity:1} .${IG_C_PRIVATE_PROFILE_PIC_CONTAINER}:hover .${C_BTN_PROFILE_PIC_CONTAINER}{opacity:1} .${C_SETTINGS_BTN}{width:20px;height:20px;cursor:pointer;top:16px;border:none;right:16px;position:fixed;background-color:transparent;background-image:url(${b64SettingsBtnIcon});background-size:20px 20px;opacity:.8;${opacityTransition}} .${C_SETTINGS_BTN}:hover{opacity:1;} .${C_SETTINGS_CONTAINER}{position:fixed;display:flex;justify-content:center;align-items:center;width:100vw;height:100vh;top:0;left:0;background-color:rgba(0,0,0,.5);display:none} .${C_SETTINGS_MENU}{width:256px;display:flex;flex-direction:column;background-color:#fff;border-radius:4px;z-index:5;box-shadow:-1px 2px 14px 3px rgba(0,0,0,.5)} .${C_SETTINGS_MENU_TITLE_CONTAINER}{display:flex;flex-direction:row;justify-content:space-between;font-weight:700} .${C_SETTINGS_MENU_TITLE}{display:flex;justify-content:center;flex-direction:row;font-size:16px;padding:16px;text-align:left}.${C_SETTINGS_MENU_TITLE_CLOSE_BTN}{width:24px;height:24px;border:0;padding:0;background-color:transparent;margin-top:8px;margin-right:8px;cursor:pointer} .${C_SETTINGS_MENU_TITLE_LINK}{margin-left:6px;color:#4287f5!important} .${C_SETTINGS_MENU_OPTIONS}{display:flex;flex-direction:column;border-top:1px solid rgba(219,219,219,1)} .${C_SETTINGS_MENU_OPTION}{padding:16px;border:none;background-color:transparent;font-size:14px;cursor:pointer;transition:background-color 0.2s ease;padding-left:16px;text-align:left} .${C_SETTINGS_MENU_OPTION}:hover{background-color:rgba(214,214,214,.3)} .${C_SETTINGS_MENU_OPTION}:active{background-color:rgba(214,214,214,.4)} `; const element = document.createElement('style'); element.type = 'text/css'; element.innerHTML = styles; document.head.appendChild(element); log('Injected CSS into DOM'); } //#endregion }());
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址