// ==UserScript==
// @name Instagram Source Opener
// @version 1.1.6
// @description Open the original source of an IG post, story or profile picture. No jQuery
// @author jomifepe
// @icon https://www.instagram.com/favicon.ico
// @include https://www.instagram.com/*
// @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==
(function() {
"use strict"
const LOGGING_ENABLED = false
/* NOTE: this script relies a lot on class names, I'll keep an eye on changes */
/* Instagram classes and selectors */
const IG_C_STORY_CONTAINER = "yS4wN",
IG_C_STORY_MEDIA_CONTAINER = "qbCDp",
IG_C_POST_IMG = "FFVAD",
IG_C_POST_VIDEO = "tWeCl",
IG_C_SINGLE_POST_CONTAINER = "JyscU",
IG_S_MULTI_POST_LIST_ITEMS = ".vi798 .Ckrof",
IG_C_MULTI_POST_PREV_ARROW_BTN = "POSa_",
IG_C_MULTI_POST_NEXT_ARROW_BTN = "_6CZji",
IG_C_POST_CONTAINER = "_8Rm4L",
IG_S_POST_BUTTONS = ".eo2As > section",
IG_C_PROFILE_PIC_CONTAINER = "RR-M-",
IG_C_PRIVATE_PROFILE_PIC_CONTAINER = "M-jxE",
IG_C_PRIVATE_PIC_IMG_CONTAINER = "_2dbep",
IG_C_PRIVATE_PROFILE_PIC_IMG_CONTAINER = "IalUJ",
IG_C_PROFILE_CONTAINER = "v9tJq",
IG_C_PROFILE_USERNAME_TITLE = "fKFbl",
IG_C_POST_BLOCKER = "_9AhH0",
IG_C_TOP_PAGE_BAR = "Hz2lF",
IG_C_POST_TIME = "_1o9PC"
/* Custom classes and selectors */
const C_BTN_STORY = "iso-story-btn",
C_BTN_STORY_CONTAINER = "iso-story-container",
C_POST_WITH_BUTTON = "iso-post",
C_BTN_POST_OUTER_SPAN = "iso-post-container",
C_BTN_POST = "iso-post-btn",
C_BTN_POST_INNER_SPAN = "iso-post-span",
C_BTN_PROFILE_PIC_CONTAINER = "iso-profile-pic-container",
C_BTN_PROFILE_PIC = "iso-profile-picture-btn",
C_BTN_PROFILE_PIC_SPAN = "iso-profile-picture-span",
C_BTN_SETTINGS = "iso-settings-btn",
C_SETTINGS_MENU = "iso-settings-menu",
C_SETTINGS_MENU_TITLE_CONTAINER = "iso-settings-menu-title-container",
C_LINK_SETTINGS_INFO = "iso-settings-info-link",
C_BTN_SETTINGS_CLOSE = "iso-settings-menu-close-btn",
C_SETTINGS_MENU_OPTIONS_CONTAINER = "iso-settings-menu-container",
C_SETTINGS_MENU_OPTION = "iso-settings-menu-option"
const S_IG_POST_CONTAINER_WITHOUT_BUTTON = `.${IG_C_POST_CONTAINER}:not(.${C_POST_WITH_BUTTON})`
/* Storage keys */
const POST_STORY_KB_STORAGE_KEY = "iso_post_story_kb",
PROFILE_PICTURE_KB_STORAGE_KEY = "iso_profile_picture_kb"
/* Default letters for key bindings */
const DEFAULT_POST_STORY_KB = "O",
DEFAULT_PROFILE_PICTURE_KB = "P"
const HOMEPAGE_URL = "https://gf.qytechs.cn/en/scripts/372366-instagram-source-opener"
/* Global scope variables */
let isStoryKeyBindingSetup,
isSinglePostKeyBindingSetup,
isProfileKeyBindingSetup,
openPostStoryKeyBinding = DEFAULT_POST_STORY_KB,
openProfilePictureKeyBinding = DEFAULT_PROFILE_PICTURE_KB
/* Arrive.js library (https://github.com/uzairfarooq/arrive) */
let 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);
/* BEGIN SCRIPT ------------------------------ */
registerMenuCommands()
/* injects the needed CSS into DOM */
injectStyles()
/* triggered whenever a new instagram post is loaded on the feed */
document.arrive(S_IG_POST_CONTAINER_WITHOUT_BUTTON, node => generatePostButton(node))
/* triggered whenever a single post is opened (on a profile) */
document.arrive(`.${IG_C_SINGLE_POST_CONTAINER}`, node => {
generatePostButton(node)
setupSinglePostEventListeners()
})
/* triggered whenever a story is opened */
document.arrive(`.${IG_C_STORY_CONTAINER}`, node => {
generateStoryButton(node)
setupStoryEventListeners()
})
/* triggered whenever a profile page is loaded */
document.arrive(`.${IG_C_PROFILE_CONTAINER}`, node => {
generateProfilePictureButton(node)
setupProfileEventListeners()
})
document.arrive(`.${IG_C_TOP_PAGE_BAR}`, node => {
createSettingsPageMenu()
})
/* triggered whenever a single post is closed (on a profile) */
document.leave(`.${IG_C_SINGLE_POST_CONTAINER}`, node => removeSinglePostEventListeners())
/* triggered whenever a story is closed */
document.leave(`.${IG_C_STORY_CONTAINER}`, node => removeStoryEventListeners())
/* triggered whenever a profile page is left */
document.leave(`.${IG_C_PROFILE_CONTAINER}`, node => removeProfileEventListeners())
/**
* Window load callback
* Checks if there are relevant nodes already loaded in DOM and performs the corresponding actions
*/
window.onload = (e => {
if (/* is on post feed */ window.location.pathname === '/') {
let postArticles = document.querySelectorAll(S_IG_POST_CONTAINER_WITHOUT_BUTTON)
postArticles.forEach(node => generatePostButton(node))
} else if (isOnSinglePostPage()) {
let node = document.querySelector(`.${IG_C_SINGLE_POST_CONTAINER}`)
if (node != null) {
generatePostButton(node)
}
setupSinglePostEventListeners()
} else if (isOnStoryPage()) {
let node = document.querySelector(`.${IG_C_STORY_CONTAINER}`)
if (node == null) {
generateStoryButton(node)
}
setupStoryEventListeners()
} else if (isOnProfilePage()) {
let node = document.querySelector(`.${IG_C_PROFILE_CONTAINER}`)
if (node != null) {
generateProfilePictureButton(node)
}
setupProfileEventListeners()
}
createSettingsPageMenu()
})
/**
* 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() {
try {
GM_registerMenuCommand("Change post/story key binding", handlePostStoryKBMenuCommand, null)
GM_registerMenuCommand("Change profile picture key binding", handleProfilePictureKBMenuCommand, null)
logMessage("Registered menu commands using GM_registerMenuCommand")
} catch (error) {
logError("Failed to register menu commands using GM_registerMenuCommand")
try {
GM.registerMenuCommand("Change post/story key binding", handlePostStoryKBMenuCommand, null)
GM.registerMenuCommand("Change profile picture key binding", handleProfilePictureKBMenuCommand, null)
logMessage("Registered menu commands using GM.registerMenuCommand")
} catch (error) {
logError("Failed to register menu commands using GM.registerMenuCommand\nUse the on-page settings menu")
}
}
}
/**
* Handles the click action on the option to change the single post and story opening key binding, on the settings menu
*/
function handlePostStoryKBMenuCommand() {
handleKBMenuCommand(POST_STORY_KB_STORAGE_KEY, DEFAULT_POST_STORY_KB, "single post and story")
.then(newKeyBinding => {
openPostStoryKeyBinding = newKeyBinding
if (isOnSinglePostPage()) {
removeSinglePostEventListeners()
setupSinglePostEventListeners()
} else if (isOnStoryPage()) {
removeStoryEventListeners()
setupSinglePostEventListeners()
}
})
}
/**
* Handles the click action on the option to change the profile picture opening key binding, on the settings menu
*/
function handleProfilePictureKBMenuCommand() {
handleKBMenuCommand(PROFILE_PICTURE_KB_STORAGE_KEY, DEFAULT_PROFILE_PICTURE_KB, "profile picture")
.then(newKeyBinding => {
openProfilePictureKeyBinding = newKeyBinding
removeProfileEventListeners()
setupProfileEventListeners()
})
}
/**
* 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_BTN_SETTINGS}`)) {
/* Creating the settings button */
let button = document.createElement("button")
button.classList.add(C_BTN_SETTINGS)
button.setAttribute("type", "button")
button.setAttribute("title", "Open ISO settings")
button.addEventListener("click", () => setPageSettingsMenuVisibility(true))
document.querySelector(`.${IG_C_TOP_PAGE_BAR}`).appendChild(button)
logMessage('Created script settings button')
}
if (!document.querySelector(`.${C_SETTINGS_MENU}`)) {
/* Creating the settings menu */
let menu = document.createElement("div")
let menuTitleContainer = document.createElement("div")
let infoLink = document.createElement("a")
let menuCloseButton = document.createElement("button")
menu.classList.add(C_SETTINGS_MENU)
menuTitleContainer.classList.add(C_SETTINGS_MENU_TITLE_CONTAINER)
infoLink.classList.add(C_LINK_SETTINGS_INFO)
menuCloseButton.classList.add(C_BTN_SETTINGS_CLOSE)
menuTitleContainer.innerHTML += "ISO Settings"
infoLink.innerHTML = "(?)"
menuCloseButton.innerHTML += "x"
infoLink.setAttribute("href", HOMEPAGE_URL)
infoLink.setAttribute("target", "_blank")
infoLink.setAttribute("title", "What is this?")
menuCloseButton.setAttribute("type", "button")
menuCloseButton.setAttribute("title", "Close ISO settings")
menuCloseButton.addEventListener("click", () => setPageSettingsMenuVisibility(false))
menuTitleContainer.appendChild(infoLink)
menuTitleContainer.appendChild(menuCloseButton)
let menuOptionsContainer = document.createElement("div")
let optionPostStoryKB = document.createElement("button")
let optionProfilePictureKB = document.createElement("button")
menuOptionsContainer.classList.add(C_SETTINGS_MENU_OPTIONS_CONTAINER)
optionPostStoryKB.classList.add(C_SETTINGS_MENU_OPTION)
optionProfilePictureKB.classList.add(C_SETTINGS_MENU_OPTION)
optionPostStoryKB.innerHTML += "Change post/story key"
optionProfilePictureKB.innerHTML += "Change profile picture key"
optionPostStoryKB.addEventListener("click", handlePostStoryKBMenuCommand)
optionProfilePictureKB.addEventListener("click", handleProfilePictureKBMenuCommand)
menuOptionsContainer.appendChild(optionPostStoryKB)
menuOptionsContainer.appendChild(optionProfilePictureKB)
menu.appendChild(menuTitleContainer)
menu.appendChild(menuOptionsContainer)
document.body.appendChild(menu)
logMessage('Created script settings menu')
}
}
/**
* Handles clicks outside the settings menu when it's open
* @param {Object} e
*/
function handlePageSettingsFocusLeave(e) {
if (e.target.classList[0].startsWith("iso-settings")) 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_MENU}`).style.display = 'block'
/* adding a listener to the whole page to close de menu on "focus leave" */
document.addEventListener("mouseup", handlePageSettingsFocusLeave)
} else {
document.querySelector(`.${C_SETTINGS_MENU}`).style.display = 'none'
}
}
/**
* 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 on promise resolve
* @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, when resolved contains the new key binding and when rejected contains
* an exception error object or nothing when the prompt was canceled or the input was left empty
*/
function handleKBMenuCommand(keyBindingStorageKey, defaultKeyBinding, keyBindingName) {
return new Promise(async (resolve, reject) => {
let currentKey = null
try {
currentKey = await GM_getValue(keyBindingStorageKey, defaultKeyBinding)
} catch (error) {
logError(`Failed to get current "${keyBindingName} key binding using GM_getValue`)
}
if (await currentKey == null) {
try {
currentKey = await GM.getValue(keyBindingStorageKey, defaultKeyBinding)
} catch (error) {
logError(`Failed to get current "${keyBindingName} key binding using GM.getValue`)
}
}
if (await currentKey == null) {
currentKey = defaultKeyBinding
logMessage(`Falling back to default key binding: Alt + ${defaultKeyBinding}`)
}
let newKeyBinding = prompt(`Enter a new letter for the key binding used to open a ${keyBindingName}\n` +
`This letter then can be combined with the Alt key to perform said action\n\n` +
`Current key binding: Alt + ${(await currentKey).toUpperCase()}`)
if (newKeyBinding != null) {
if (!isKeyBindingValid(newKeyBinding)) {
showAndLogError(`Couldn't save new key binding to open ${keyBindingName}, invalid option.`)
reject()
return
}
let successMessage = `Saved new key binding to open ${keyBindingName}: Alt + ${newKeyBinding.toUpperCase()}`
try {
await GM_setValue(keyBindingStorageKey, newKeyBinding)
showMessage(successMessage)
resolve(newKeyBinding)
} catch (gmu_error) {
logError(`Failed to save new key binding to open ${keyBindingName} using GM_setValue`)
try {
await GM.setValue(keyBindingStorageKey, newKeyBinding)
showMessage(successMessage)
resolve(newKeyBinding)
} catch (gmd_error) {
showAndLogError(`Failed to save new key binding to open ${keyBindingName} using GM.setValue`)
reject(gmd_error)
}
}
}
})
}
/**
* 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 (elementExistsInNode(`.${C_BTN_STORY_CONTAINER}`, node)) return
try {
let buttonStoryContainer = document.createElement("span")
let 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 (exception) {
logError("Failed to generate story button", exception)
}
}
/**
* 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 (elementExistsInNode(`.${C_BTN_POST_OUTER_SPAN}`, node)) return
try {
/* removes the div that's blocking the img element on a post */
let blocker = node.querySelector(`.${IG_C_POST_BLOCKER}`)
if (blocker) blocker.parentNode.removeChild(blocker)
let buttonsContainer = node.querySelector(IG_S_POST_BUTTONS)
let newElementOuterSpan = document.createElement("span")
let newElementButton = document.createElement("button")
let 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)
let timeElement = node.querySelector(`.${IG_C_POST_TIME}`)
if (timeElement) {
let fullDateStr = timeElement.getAttribute("datetime")
if (fullDateStr) timeElement.innerHTML += ` (${fullDateStr})`
}
} catch (exception) {
logError("Failed to generate post button", exception)
}
}
/**
* 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 (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}`)
}
let newElementOuterSpan = document.createElement("span")
let newElementButton = document.createElement("button")
let 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 (error) {
logError(error)
}
}
/**
* 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 {
let container = (node || document).querySelector(`.${IG_C_STORY_MEDIA_CONTAINER}`)
let video = container.querySelector("video")
let image = container.querySelector("img")
if (video) {
let videoElement = video.querySelector("source")
let videoSource = videoElement ? videoElement.getAttribute("src") : null
if (!videoSource) {
throw "Video source isn't available"
}
window.open(videoSource, "_blank")
} else if (image) {
let imageSource = image.getAttribute("src")
if (!imageSource) {
throw "Image source isn't available"
}
window.open(imageSource, "_blank")
} else {
throw "Story media isn't available"
}
} catch (exception) {
showAndLogError("Failed to open story source", exception)
}
}
/**
* 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
*/
function openPostSourceFromSrcAttribute(node = document.querySelector(`.${IG_C_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 {
let sourceListItems = node.querySelectorAll(IG_S_MULTI_POST_LIST_ITEMS)
if (/* is single post */ sourceListItems.length == 0) {
openPostMediaSource(node)
return
}
if (/* is on the first or last item */ sourceListItems.length == 2) {
if (/* next arrow exist */ node.querySelector(`.${IG_C_MULTI_POST_NEXT_ARROW_BTN}`)) {
openPostMediaSource(sourceListItems[0]) /* opens last item */
} else if (/* previous arrow exists */ node.querySelector(`.${IG_C_MULTI_POST_PREV_ARROW_BTN}`)) {
openPostMediaSource(sourceListItems[1]) /* opens last item */
} else /* something is not right */ {
showAndLogError("Failed to open post source", exception)
}
} else if (/* is on any other item */ sourceListItems.length == 3) {
openPostMediaSource(sourceListItems[1])
} else /* something is not right */ {
showAndLogError("Failed to open post source", exception)
}
} catch (exception) {
showAndLogError("Failed to open post source", exception)
}
}
/**
* 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
*/
function openPostMediaSource(node) {
let image = node.querySelector(`.${IG_C_POST_IMG}`)
let video = node.querySelector(`.${IG_C_POST_VIDEO}`)
if (!image && !video) {
throw "Failed to open source, no media found"
}
window.open((video || image).getAttribute("src"), "_blank")
}
/**
* 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() {
let pageUsername = document.querySelector(`.${IG_C_PROFILE_USERNAME_TITLE}`).innerText
var pictureUrl = null
document.body.style.cursor = "wait"
logMessage("Trying to get user's profile picture from 3rd party websites", true)
/* trying to get the picture from instadp.org */
try {
logMessage("Trying to get user's profile picture from instadp.org")
pictureUrl = await getProfilePictureFromInstadpDotOrg(pageUsername)
if (!(await pictureUrl)) logError("No profile picture url found on instadp.org response")
} catch (error) {
logError("Couldn't get picture from instadp.org", error)
}
if (!pictureUrl) {
/* trying to get the picture from instadp.com */
try {
logMessage("Trying to get user's profile picture from instadp.com")
pictureUrl = await getProfilePictureFromInstadpDotCom(pageUsername)
if (!(await pictureUrl)) logError("No profile picture url found on instadp.com response")
} catch (error) {
logError("Couldn't get picture from instadp.com", error)
}
}
if (!pictureUrl) {
/* trying to get the picture from izuum.com */
try {
logMessage("Trying to get user's profile picture from izuum.com")
pictureUrl = await getProfilePictureFromIzuum(pageUsername)
if (!(await pictureUrl)) logError("No profile picture url found on izuum.com response")
} catch (error) {
logError("Couldn't get picture from izuum.com", error)
}
}
if (!pictureUrl) {
/* trying to get the picture from existing data on the user's profile */
try {
logMessage("Trying to get user's profile picture from existing user data", true)
pictureUrl = await getPictureFromExistingData(pageUsername, window._sharedData.entry_data.ProfilePage)
if (!(await pictureUrl)) logError("No profile picture url found on any existing data")
} catch (error) {
logError("Couldn't get picture from existing data on user's profile (3 methods failed)", error)
}
}
document.body.style.cursor = "default"
if (pictureUrl) {
logMessage("Profile picture found, opening in a new tab")
window.open(pictureUrl, "_blank")
} else {
showMessage("Couldn't get user's profile picture")
}
}
/**
* Adds event listener(s) to the current document meant to handle key presses on a single post page
*/
async function setupSinglePostEventListeners() {
if (!isSinglePostKeyBindingSetup) {
loadPostStoryKeyBindings()
.then(() => {
document.addEventListener('keydown', handleSinglePostKeyPress)
isSinglePostKeyBindingSetup = true
logMessage("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() {
if (!isStoryKeyBindingSetup) {
loadPostStoryKeyBindings()
.then(() => {
document.addEventListener('keydown', handleStoryKeyPress)
isStoryKeyBindingSetup = true
logMessage("Defined story opening event listener")
})
}
}
/**
* Adds event listener(s) to the current document meant to handle key presses on a profile page
*/
function setupProfileEventListeners() {
if (!isProfileKeyBindingSetup) {
loadProfilePictureKeyBindings()
.then(() => {
document.addEventListener('keydown', handleProfileKeyPress)
isProfileKeyBindingSetup = true
logMessage("Defined profile picture opening event listener")
})
}
}
/**
* Removes the previously added event listener(s) meant to handle key presses on a single post page
*/
function removeSinglePostEventListeners() {
if (isSinglePostKeyBindingSetup) {
document.removeEventListener('keydown', handleSinglePostKeyPress)
isSinglePostKeyBindingSetup = false
logMessage("Removed single post opening event listener")
}
}
/**
* Removes the previously added event listener(s) meant to handle key presses on a story page
*/
function removeStoryEventListeners() {
if (isStoryKeyBindingSetup) {
document.removeEventListener('keydown', handleStoryKeyPress)
isStoryKeyBindingSetup = false
logMessage("Removed story opening event listener")
}
}
/**
* Removes the previously added event listener(s) meant to handle key presses on a profile page
*/
function removeProfileEventListeners() {
if (isProfileKeyBindingSetup) {
document.removeEventListener('keydown', handleProfileKeyPress)
isProfileKeyBindingSetup = false
logMessage("Removed profile picture opening event listener")
}
}
/**
* Handles key up events on a story page
* @param {Object} e Event object
*/
function handleStoryKeyPress(e) {
handleKeyPress(e, openPostStoryKeyBinding, () => isOnStoryPage(),
"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, () => isOnSinglePostPage(),
"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, () => !isOnStoryPage() && !isOnSinglePostPage(),
"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) {
console.log(event.altKey, event.code.toLowerCase() === `key${keyBinding.toLowerCase()}`, checkConditionsAreMet(), keyPressAction)
if (event.altKey && event.code.toLowerCase() === `key${keyBinding.toLowerCase()}` && checkConditionsAreMet()) {
logMessage(logMessageString)
keyPressAction()
}
}
/**
* 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, always resolved after loading key binding
*/
function loadPostStoryKeyBindings() {
return new Promise(async resolve => {
try {
let kbName = "single post and story"
openPostStoryKeyBinding = await loadKeyBindingFromStorage(
POST_STORY_KB_STORAGE_KEY, DEFAULT_POST_STORY_KB, kbName)
} catch (error) {
logError(`Failed to load "${kbName}" key binding, considering default (Alt + ${DEFAULT_POST_STORY_KB})`, error)
} finally {
resolve()
}
})
}
/**
* 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, always resolved after loading key binding
*/
function loadProfilePictureKeyBindings() {
return new Promise(async resolve => {
try {
let kbName = "profile picture"
openProfilePictureKeyBinding = await loadKeyBindingFromStorage(
PROFILE_PICTURE_KB_STORAGE_KEY, DEFAULT_PROFILE_PICTURE_KB, kbName)
} catch (error) {
logError(`Failed to load "${kbName}" key binding, considering default (Alt + ${DEFAULT_PROFILE_PICTURE_KB})`, error)
} finally {
resolve()
}
})
}
/**
* 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, when resolved contains the loaded key and when rejected contains an exception error object
*/
function loadKeyBindingFromStorage(storageKey, defaultKeyBinding, keyBindingName) {
return new Promise(async resolve => {
let kb = null
try {
kb = await GM_getValue(storageKey, defaultKeyBinding)
} catch (error) {
logError(`Failed to load key binding from storage using GM_getValue`)
}
if (await kb == null) {
try {
kb = await GM.getValue(storageKey, defaultKeyBinding)
} catch (error) {
logError(`Failed to load key binding from storage using GM.getValue`)
}
}
if (await kb == null) {
kb = defaultKeyBinding
logMessage(`Falling back to default key binding: Alt + ${defaultKeyBinding}`)
}
try {
if (isKeyBindingValid(await kb)) {
let newKey = kb.toUpperCase()
logMessage(`Discovered ${keyBindingName} key binding: Alt + ${newKey}`)
resolve(newKey)
} else {
logError(`Couldn't load "${keyBindingName}" key binding, "${kb}" key is invalid, considering default (Alt + ${defaultKeyBinding})`)
resolve(defaultKeyBinding)
}
} catch (error) {
if (kb != defaultKeyBinding) {
logError(`Failed to load "${keyBindingName}" key binding, falling back to default: Alt + ${defaultKeyBinding}`, error)
}
reject(error)
}
})
}
/**
* Tries to get the user's profile picture URL from sharedData and graphql API
* Picture URLs from shared data are usually low-res versions
* @param {string} username
* @param {Object} existingData
* @returns {Promise} Promise object, when resolved contains the picture URL
*/
async function getPictureFromExistingData(username, existingData) {
return new Promise(async (resolve, reject) => {
var pictureUrl = null
/* trying to get the from current page's sharedData variable */
try {
logMessage("Trying to get user's profile picture from current page's sharedData")
let userSharedData = existingData[0].graphql.user
/* if sharedData is correct */
if (userSharedData.username === username) {
pictureUrl = userSharedData.profile_pic_url_hd
if (!pictureUrl) logError("No profile picture url found on current page's sharedData")
} else {
logError("Current sharedData is incorrect, discarding url")
}
} catch (error) {
logError("Couldn't get url from current page's sharedData", error)
}
if (!pictureUrl) {
/* trying to get the picture from user data graphql API (?__a=1) */
try {
logMessage("Trying to get user's profile picture from user data graphql API (?__a=1)")
pictureUrl = await getProfilePictureFromUpdatedSharedData()
if (!(await pictureUrl)) logError("No profile picture url found on user data graphql API (?__a=1)")
} catch (error) {
logError("Couldn't get picture from user data graphql API (?__a=1)", error)
}
}
if (!pictureUrl) {
/* last resort: trying to get the picture from an updated HTML profile page */
try {
logMessage("Trying to get user's profile picture from updated HTML profile page")
pictureUrl = await getProfilePictureFromUpdatedHTMLPage()
if (!(await pictureUrl)) logError("No profile picture url found on updated HTML page")
} catch (error) {
logError("Couldn't get picture from updated HTML page", error)
}
}
pictureUrl ? resolve(pictureUrl) : reject()
})
}
/**
* Parses a whole HTML page in order to get the user's profile picture URL
* @returns {Promise} Promise object, when resolved returns the user's profile picture URL
* and when rejected contains an exception error object
*/
function getProfilePictureFromUpdatedHTMLPage() {
getProfilePictureFromUp
return new Promise((resolve, reject) => {
httpGETRequest(window.location, false)
.then(response => {
let parser = new DOMParser()
let doc = parser.parseFromString(response, "text/html")
let 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
resolve(userInfo)
break
}
}
})
.catch(error => reject(error))
})
}
/**
* Requests user information from the Instagram API
* @deprecated This endpoint was almost completely shut down by instagram and only works with a different user agent and provides low-res pictures
* @param {number} userId
* @returns {Promise} Promise object, when resolved contains an object with the user's information
* and when rejected contains an exception error object
*/
function getUserFromUserInfoAPI(userId) {
return new Promise((resolve, reject) => {
httpGETRequest(`https://i.instagram.com/api/v1/users/${userId}/info/`)
.then(response => resolve(response.user))
.catch(error => reject(error))
})
}
/**
* Requests the user profile page data from graphql in order to get its profile picture URL
* @returns {Promise} Promise object, when resolved contains the user's profile picture URL
* and when rejected contains an exception error object
*/
function getProfilePictureFromUpdatedSharedData() {
return new Promise((resolve, reject) => {
httpGETRequest(`${window.location}?__a=1`)
.then(response => resolve(response.graphql.user.profile_pic_url_hd))
.catch(error => reject(error))
})
}
/**
* 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, when resolved contains the user's profile picture URL
* and when rejected contains an exception error object or a null if no URL was found
*/
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
*/
return new Promise((resolve, reject) => {
httpGETRequest(`https://www.instadp.com/fullsize/${username}`, false)
.then(response => {
let urls = extractUrlsFromString(response)
let instagramUrls = urls.filter(u =>
u.includes("cdninstagram") && !u.includes("s150x150"))
if (instagramUrls.length > 0) {
resolve(instagramUrls[instagramUrls.length - 1])
} else {
reject()
}
})
.catch(error => reject(error))
})
}
/**
* 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, when resolved contains the user's profile picture URL and
* when rejected contains an exception error object or a null if no URL was found
*/
function getProfilePictureFromInstadpDotOrg(username) {
return new Promise((resolve, reject) => {
let headers = {"Content-Type": "application/x-www-form-urlencoded"}
let data = `username=${username}`
httpPOSTRequest('https://instadp.org/', headers, data, false)
.then(response => {
let urls = extractUrlsFromString(response)
let instagramUrls = urls.filter(u => u.includes("cdninstagram"))
if (instagramUrls.length > 0) {
resolve(instagramUrls[0])
} else {
reject()
}
})
.catch(error => reject(error))
})
}
/**
* 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, when resolved contains the user's profile picture URL
* and when rejected contains an exception error object or a null if no URL was found
*/
function getProfilePictureFromIzuum(username) {
return new Promise((resolve, reject) => {
let headers = {"Content-Type": "application/x-www-form-urlencoded"}
let data = `submit=${username}`
httpPOSTRequest('http://izuum.com/index.php', headers, data, false)
.then(response => {
let urls = extractUrlsFromString(response)
let instagramUrls = urls.filter(u => u.includes("cdninstagram"))
let cleanUrls = instagramUrls.map(u => u.replace(/amp;/g, ''))
if (cleanUrls.length > 0) {
resolve(cleanUrls[0])
} else {
reject()
}
})
.catch(error => reject(error))
})
}
/**
* 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
*/
function httpGETRequest(url, parseToJSON = true) {
return new Promise((resolve, reject) => {
let options = {
method: "GET",
url: url,
timeout: 10000,
onload: res => {
if (res.status === 200) {
let response = res.responseText
if (parseToJSON) {
response = JSON.parse(res.responseText)
}
resolve(response)
} else {
reject(`Status Code ${res.status} ${res.statusText.length > 0 ? `, ${res.statusText}` : ''}`)
}
},
onerror: error => reject(error),
ontimeout: () => reject("Request Timeout"),
onabort: () => reject("Aborted")
}
try {
GM_xmlhttpRequest(options)
} catch (error) {
logError("Failed to perform GET request using GM_xmlhttpRequest")
try {
GM.xmlHttpRequest(options)
} catch (error) {
logError("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
*/
function httpPOSTRequest(url, headers = null, data = null, parseToJSON = true) {
return new Promise((resolve, reject) => {
let options = {
method: "POST",
url: url,
...(headers && {headers: headers}),
...(data && {data: data}),
timeout: 10000,
onload: res => {
if (res.status === 200) {
let response = res.responseText
if (parseToJSON) {
response = JSON.parse(res.responseText)
}
resolve(response)
} else {
reject(`Status Code ${res.status} ${res.statusText.length > 0 ?
', ' + res.statusText : ''}`)
}
},
onerror: error => reject(error),
ontimeout: () => reject("Request Timeout"),
onabort: () => reject("Aborted")
}
try {
GM_xmlhttpRequest(options)
} catch (error) {
logError("Failed to perform POST request using GM_xmlhttpRequest")
try {
GM.xmlHttpRequest(options)
} catch (error) {
logError("Failed to perform POST request using GM.xmlHttpRequest")
}
}
})
}
/**
* Checks if the current location corresponds to a story page
* @returns {boolean} true is it is a story page, false otherwise
*/
function isOnStoryPage() {
return window.location.pathname.startsWith("/stories/")
}
/**
* Checks if the current location corresponds to a single post page
* @returns {boolean} true is it is a story page, false otherwise
*/
function isOnSinglePostPage() {
return window.location.pathname.startsWith("/p/")
}
/**
* Checks if the current location corresponds to a profile page
* @returns {boolean} true is it is a story page, false otherwise
*/
function isOnProfilePage() {
return window.location.pathname.length > 1 &&
document.querySelector(`.${IG_C_PROFILE_CONTAINER}`)
}
/**
* 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)
}
/**
* Extracts every substring found between quotes and double quotes in a given string
* @param {string} string String to match
*/
function extractStringsBetweenQuotes(string) {
return string.match(/(?=["'])(?:"[^"\\]*(?:\\[\s\S][^"\\]*)*"|'[^'\\]*(?:\\[\s\S][^'\\]*)*')/gi)
}
/**
* 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.querySelector(selector) != null)
}
/**
* Appends the necessary style elements to DOM
*/
function injectStyles() {
let b64StoryBtnIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAJcEhZcwAADdcAAA3XAUIom3gAAAAHdElNRQfiAxwDOBTFNFQBAAABKklEQVQ4y6WTvUoDQRSFvwkbxCqijY2okEIEixQpBMHKykIFC8H/yiew8Rl8i4ClCoJYWGkhaGXjrkmTbsUiTQoVf45Ndp1lZ8eIp7vnnnPvnBkG/gjjb2uAOcoW0fplnvaVRccAaIFZhnPqW3OkMa4Zz84o60RunAFoQm2bDDhgmSsOHad7BjBtrXFjb3jUi0Y8KUYV2hvQly77kH/qKTFIF33Id5MsHoMl30njdwoNlnw75SqaLDC45EnLYbDkW/lZOYMl3wRQTTW/4bQn3+jVoUK/YUqxPrSe1pGin26QD2wizVM15+7LDlykadIseswSbwzhgUpUeLWJO0nTHsOSpIa1XSsc06VR8PnqrGKom3t7xp66KkasxUw+AA0y4/iiADEP5p3/4BuEXi9gkPrfQgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOC0wMy0yOFQwMzo1NjoyMCswMjowMO7sj9MAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTgtMDMtMjhUMDM6NTY6MjArMDI6MDCfsTdvAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAABJRU5ErkJggg=="
let b64SettingsBtnIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAHdElNRQfkAwICCBKUTG5BAAAAvUlEQVQoz4XRP04CQRTH8Q+LjayaCNmGVhOCN9AbEBNqSjkA99rQcARiwBAJBY2b0HABS6LRCgoHl10S+Dbz3pvfzPvHGapHkbaK71OCEea5G4Wzqff/wxo9zcNXiUwK6rbukMok++sbC29i8OTXBS6NLd1Czat310Hc9xGs2NTMVWSormMTwi2rYH3piqWRIrngj0o5RealmKJYZNWPR4pFHrZ5b6tBuc18UM8+g18a1J6ByenVJR7ObfeIHQnsKMIAvGM4AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIwLTAzLTAyVDAyOjA4OjE4KzAwOjAwpKR+fgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMC0wMy0wMlQwMjowODoxOCswMDowMNX5xsIAAAAZdEVYdFNvZnR3YXJlAHd3dy5pbmtzY2FwZS5vcmeb7jwaAAAAAElFTkSuQmCC"
let styles = `.${C_BTN_POST_OUTER_SPAN}{margin-left:10px;margin-right:-10px;}
.${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;min-height:40px;min-width:40px;padding:0;}
.${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_POST_INNER_SPAN},.${C_BTN_PROFILE_PIC_SPAN}{display:block;background-repeat:no-repeat;background-position:100%-26px;height:24px;width:24px;background-image:url(/static/bundles/base/sprite_glyphs.png/4b550af4600d.png);cursor:pointer;}
.${C_BTN_PROFILE_PIC_SPAN}{margin: auto;}
.${C_BTN_STORY}{border:none;position:fixed;top:0;right:0;margin:20px;cursor:pointer;width:24px;height:24px;background-color:transparent;background-image:url(${b64StoryBtnIcon})}
.${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_BTN_SETTINGS}{width:16px;height:16px;cursor:pointer;top:16px;border:none;right:16px;position:fixed;background-color:transparent;background-image:url(${b64SettingsBtnIcon})}
.${C_SETTINGS_MENU}{background-color:#fff;right:0;top:0;position:fixed;z-index:5;border-radius:4px;padding:8px;display:none;-webkit-box-shadow:-1px 2px 14px 3px rgba(0,0,0,.5);-moz-box-shadow:-1px 2px 14px 3px rgba(0,0,0,.5);box-shadow:-1px 2px 14px 3px rgba(0,0,0,.5)}
.${C_SETTINGS_MENU_TITLE_CONTAINER}{display:inline-block;font-weight:700;width:100%}
.${C_BTN_SETTINGS_CLOSE}{width:16px;height:16px;background-color:#8b0000;border-radius:4px;font-weight:700;padding:0;margin-left:8px;line-height:0;color:#fff;border:1px solid #8b0000;padding-bottom:2px;padding-right:1px;cursor:pointer;float:right}
.${C_LINK_SETTINGS_INFO}{margin-left:8px;color:#4287f5!important;text-decoration:underline!important}
.${C_SETTINGS_MENU_OPTIONS_CONTAINER}{padding-top:8px;width:100%}
.${C_SETTINGS_MENU_OPTION}{display:block;color:#00f;cursor:pointer;border:none;background:0 0;text-align:left;padding-left:0;padding-right:0}
.${C_SETTINGS_MENU_OPTION}:hover{text-decoration:underline}`
let element = document.createElement('style');
element.type = 'text/css';
element.innerHTML = styles;
document.head.appendChild(element);
}
/**
* Shows an alert with an error message and logs an exception to the console
* @param {string} error
* @param {(Object|string)} exception
*/
function showAndLogError(error, exception) {
showMessage(error)
logError(error, exception)
}
/**
* Shows an alert with a message
* @param {string} message
*/
function showMessage(message) {
alert(`Instagram Source Opener:\n${message}`)
}
/**
* Prints a message to the console, either as info or warning
* @param {string} message
* @param {boolean} warning
*/
function logMessage(message, warning = false) {
if (LOGGING_ENABLED && message) {
if (warning) {
console.warn(`[ISO] ${message}`)
} else {
console.info(`[ISO] ${message}`)
}
}
}
/**
* Logs an error string and exception to the console
* @param {string} error
* @param {(Object|string)} exception
*/
function logError(error, exception = null) {
if (LOGGING_ENABLED && error) {
if (exception) {
console.error(`[ISO] ${error}`, exception)
} else {
console.error(`[ISO] ${error}`)
}
}
}
})()