// ==UserScript==
// @name Instagram Source Opener
// @version 0.7.2
// @description Open the original source of an IG post, story or profile picture. No jQuery
// @author jomifepe
// @icon https://www.instagram.com/favicon.ico
// @require https://cdnjs.cloudflare.com/ajax/libs/arrive/2.4.1/arrive.min.js
// @include https://www.instagram.com/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @namespace https://gf.qytechs.cn/users/192987
// ==/UserScript==
(function() {
"use strict"
const LOGGING_ENABLED = false
/* this script relies a lot on class names, I'll keep an eye on changes */
const IG_C_STORY_CONTAINER = "yS4wN"
const IG_C_STORY_MEDIA_CONTAINER = "qbCDp"
const IG_C_POST_IMG = "FFVAD"
const IG_C_POST_VIDEO = "tWeCl"
const IG_C_SINGLE_POST_CONTAINER = "JyscU"
const IG_C_MULTI_POST_SCROLLER = "MreMs"
const IG_C_MULTI_POST_LIST_ITEM = "_-1_m6"
const IG_C_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_C_PROFILE_CONTAINER = "v9tJq"
const IG_C_PROFILE_USERNAME_TITLE = "fKFbl"
const IG_C_INSTAGRAM_POST_BOCKER = "_9AhH0"
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 S_IG_POST_CONTAINER_WITHOUT_BUTTON = `.${IG_C_POST_CONTAINER}:not(.${C_POST_WITH_BUTTON})`
/* 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) => {
generatePostSourceOpeners(node)
})
/* triggered whenever a single post is opened (on a profile) */
document.arrive(`.${IG_C_SINGLE_POST_CONTAINER}`, (node) => {
generatePostSourceOpeners(node)
})
/* triggered whenever a story is opened */
document.arrive(`.${IG_C_STORY_CONTAINER}`, (node) => {
generateStoryButton(node)
})
/* triggered whenever a profile page is loaded */
document.arrive(`.${IG_C_PROFILE_CONTAINER}`, (node) => {
generateProfilePictureButton(node)
})
/**
* 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 => generatePostSourceOpeners(node))
} else if (/* is on single post page */ window.location.pathname.startsWith("/p/")) {
let node = document.querySelector(`.${IG_C_SINGLE_POST_CONTAINER}`)
if (node != null) {
generatePostSourceOpeners(node)
}
} else if (/* is on story page */ window.location.pathname.startsWith("/stories/")) {
let node = document.querySelector(`.${IG_C_STORY_CONTAINER}`)
if (node == null) {
generateStoryButton(node)
}
}
})
/**
* 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 generatePostSourceOpeners(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_INSTAGRAM_POST_BOCKER}`)
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)
} 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) {
try {
let container = node.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) {
let nodeListItems = node.querySelectorAll(`.${IG_C_MULTI_POST_LIST_ITEM}`)
try {
if (/* is multi post */ nodeListItems.length != 0) {
let scroller = node.querySelector(`.${IG_C_MULTI_POST_SCROLLER}`)
let scrollerOffset = Math.abs((() => {
let scrollerStyles = window.getComputedStyle(scroller)
return parseInt(scrollerStyles.getPropertyValue("transform").split(",")[4])
})())
let mediaIndex = 0
if (scrollerOffset != 0) {
let totalWidth = 0
nodeListItems.forEach(item => {
let itemStyles = window.getComputedStyle(item)
totalWidth += parseInt(itemStyles.getPropertyValue("width"))
})
mediaIndex = ((scrollerOffset * nodeListItems.length) / totalWidth)
}
openPostMediaSource(nodeListItems[mediaIndex])
} else /* is single post */ {
openPostMediaSource(node)
}
} catch (exception) {
showAndLogError("Failed o 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 user's profile picture using multiple methods, including 3rd party websites
* Shows an alert if it doesn't find any URL
*/
async function openProfilePicture() {
let pageUsername = document.querySelector(`.${IG_C_PROFILE_USERNAME_TITLE}`).innerText
let profilePageData = _sharedData.entry_data.ProfilePage
var pictureUrl = null
document.body.style.cursor = "wait"
logMessage("Trying to get user's 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, profilePageData)
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")
}
}
/**
* 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 as a last attempt to get the user id
* - This function is only used when:
* - The profile page is private
* - The user isn't logged in
* - The sharedData variable, which holds the profile user's id, isn't correct
* @returns {Promise} Promise with the ProfilePage user data object or an error
*/
function getProfilePictureFromUpdatedHTMLPage() {
return new Promise((resolve, reject) => {
httpGETRequest(window.location, false)
.then(response => {
try {
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)
}
})
.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 with the user info object or an error
*/
function getProfilePictureFromUserInfoAPI(userId) {
return new Promise((resolve, reject) => {
httpGETRequest(`https://i.instagram.com/api/v1/users/${userId}/info/`)
.then(response => {
let userInfo = response.user
resolve(userInfo)
})
.catch(error => reject(error))
})
}
/**
* Requests the user profile page data from graphql
* @returns {Promise} Promise with the graphql user info object or an error
*/
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
*/
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
* @returns {Promise} Promise object, when resolved contains the picture URL
*/
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
* @returns {Promise} Promise object, when resolved contains the picture URL
*/
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 function
* @param {string} url
* @param {boolean} [parseToJSON = true] default true
* @returns {Promise} Promise object with the response text or an error
*/
function httpGETRequest(url, parseToJSON = true) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: url,
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")
})
})
}
/**
* Performs an HTTP POST request using the 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 with the response text or an error
*/
function httpPOSTRequest(url, headers = null, data = null, parseToJSON = true) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "POST",
url: url,
...(headers && {headers: headers}),
...(data && {data: data}),
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")
})
})
}
/**
* 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
*/
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
*/
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 using the GM_addStyle function
*/
function injectStyles() {
let b64icon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAQAAABKfvVzAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAJcEhZcwAADdcAAA3XAUIom3gAAAAHdElNRQfiAxwDOBTFNFQBAAABKklEQVQ4y6WTvUoDQRSFvwkbxCqijY2okEIEixQpBMHKykIFC8H/yiew8Rl8i4ClCoJYWGkhaGXjrkmTbsUiTQoVf45Ndp1lZ8eIp7vnnnPvnBkG/gjjb2uAOcoW0fplnvaVRccAaIFZhnPqW3OkMa4Zz84o60RunAFoQm2bDDhgmSsOHad7BjBtrXFjb3jUi0Y8KUYV2hvQly77kH/qKTFIF33Id5MsHoMl30njdwoNlnw75SqaLDC45EnLYbDkW/lZOYMl3wRQTTW/4bQn3+jVoUK/YUqxPrSe1pGin26QD2wizVM15+7LDlykadIseswSbwzhgUpUeLWJO0nTHsOSpIa1XSsc06VR8PnqrGKom3t7xp66KkasxUw+AA0y4/iiADEP5p3/4BuEXi9gkPrfQgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxOC0wMy0yOFQwMzo1NjoyMCswMjowMO7sj9MAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTgtMDMtMjhUMDM6NTY6MjArMDI6MDCfsTdvAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAABJRU5ErkJggg=="
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;display:flex;-webkit-box-flex:0;flex-grow:0;-webkit-box-pack:center;justify-content:center;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_STORY}{border:none;position:fixed;top:0;right:0;margin:20px;cursor:pointer;width:24px;height:24px;background-color:transparent;background-image:url(${b64icon})}`,
`.${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}`
]
styles.forEach((style) => GM_addStyle(style))
}
/**
* 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}`)
}
}
}
})()