// ==UserScript==
// @name extendgram
// @description Extends functionality and user experience on instagram
// @version 1.0.0
// @author ryano
// @source https://github.com/ryanocf/extendgram
// @updateURL https://raw.githubusercontent.com/ryanocf/extendgram/main/src/dist/js/extendgram.user.js
// @downloadURL https://raw.githubusercontent.com/ryanocf/extendgram/main/src/dist/js/extendgram.user.js
// @supportURL https://github.com/ryanocf/extendgram
// @include http://*.instagram.com/*
// @include https://*.instagram.com/*
// @run-at document-end
// @grant GM_addStyle
// @grant GM_addValueChangeListener
// @grant GM_deleteValue
// @grant GM_download
// @grant GM_getValue
// @grant GM_info
// @grant GM_listValues
// @grant GM_notification
// @grant GM_openInTab
// @grant GM_removeValueChangeListener
// @grant GM_setClipboard
// @grant GM_setValue
// ==/UserScript==
const _DEBUG = false;
const _PATTERN = {
dm: /^\/direct\/t\/.+$/i,
feed: /^\/$/i,
inbox: /^\/direct\/[inbox\/]+$$/i,
profile: /^\/p\/.+$/i,
story: /^\/stories\/.+$/i,
};
const _POST = {
attribute: `data-${GM_info.script.name}`,
carousel: 'Tgarh',
class: 'M9sTE',
icons: ['ltpMr', 'wmtNn'],
image: 'FFVAD',
position: ['rQDP3', 1, 'XCodT', '_9nCnY', 'Ckrof'],
tag: 'article',
video: 'tWeCl',
};
/*
* *** STORY ***
* video(qbCDp)
* yes -> source
* no -> check img
* yes -> img
* no -> error
*/
const _SCRIPT = GM_info.script;
const _STORY = {
offsets: ['video', 'qbCDp', 'source', 'img'],
};
(function() {
'use strict';
GM_addStyle(`
/* No CSS *//*# sourceMappingURL=extendgram.min.css.map */
`);
class Logger {
constructor() {
this.codes = {
debug: {
prefix: '🐛',
style:
'font-weight: bold; color: rgba(255, 102, 0, 1.0); background-color: rgba(0, 0, 0, 0.7);',
},
error: {
prefix: '❌',
style:
'font-weight: bold; color: rgba(255, 0, 0, 1.0); background-color: rgba(0, 0, 0, 0.7);',
},
info: {
prefix: '📌',
style:
'font-weight: bold; color: rgba(0, 123, 255, 1.0); background-color: rgba(0, 0, 0, 0.7);',
},
warning: {
prefix: '⚠️',
style:
'font-weight: bold; color: rgba(255, 255, 0, 1.0); background-color: rgba(0, 0, 0, 0.7);',
},
success: {
prefix: '✔️',
style:
'font-weight: bold; color: rgba(0, 190, 0, 1.0); background-color: rgba(0, 0, 0, 0.7);',
},
};
this.prefix = '[extendgram]';
if (_DEBUG) this.debug('constructor of class Logger called');
}
debug(msg) {
return console.log(
`${this.prefix} ${this.codes.debug.prefix} - %c${msg}!`,
this.codes.debug.style
);
}
error(msg) {
return console.log(
`${this.prefix} ${this.codes.error.prefix} - %c${msg}!`,
this.codes.error.style
);
}
info(msg) {
return console.log(
`${this.prefix} ${this.codes.info.prefix} - %c${msg}!`,
this.codes.info.style
);
}
warning(msg) {
return console.log(
`${this.prefix} ${this.codes.warning.prefix} - %c${msg}!`,
this.codes.warning.style
);
}
success(msg) {
return console.log(
`${this.prefix} ${this.codes.success.prefix} - %c${msg}!`,
this.codes.success.style
);
}
}
class Helper {
constructor() {
if (_DEBUG) L.debug('constructor of class Helper called');
}
download = async function (src) {
await GM_download(src, 'download');
};
/* https://plainjs.com/javascript/manipulation/insert-an-element-after-or-before-another-32/ */
insert_after = function (element, reference) {
reference.parentNode.insertBefore(element, reference.nextSibling);
};
insert_before = function (element, reference) {
reference.parentNode.insertBefore(element, reference);
};
in_new = async function (data) {
await GM_openInTab(data, { active: true, insert: true });
};
notification = async function (text) {
/* image = 256x256 */
await GM_notification({
text: text,
title: `${_SCRIPT.name} v${_SCRIPT.version}`,
silent: true,
timeout: 3000,
});
};
to_clipboard = async function (data, mode) {
switch (mode) {
case 'url':
await GM_setClipboard(data, {
type: 'text',
mimetype: 'text/plain',
});
break;
default:
break;
}
this.notification('Copied to clipboard!');
};
}
class Settings {
defaults = {
main_color: 'rgba(102, 0, 255, 1)',
volume: 0.3,
};
constructor() {
this.obj = this.get();
console.log(this.obj);
if (_DEBUG) L.debug('constructor of class Settings called');
}
delete(key) {
GM_deleteValue(key);
delete this.obj[key];
if (_DEBUG) L.debug(`${key} deleted`);
return key;
}
get() {
let obj = {};
for (let key in this.defaults) {
let value = GM_getValue(key);
if (typeof value === 'undefined') {
obj[key] = this.defaults[key];
L.warning(
`${key} was not found so it got replaced with the default value`
);
this.set(key, this.defaults[key]);
} else {
obj[key] = value;
}
}
return obj;
}
set(key, value) {
GM_setValue(key, value);
this.obj[key] = value;
if (_DEBUG) L.debug(`${key} set to ${value}`);
return { key: value };
}
}
class Gather {
constructor() {
this.interval = setInterval(() => {
this.main();
}, 1000);
this.url = window.location.pathname;
this.observer_opt = {
attributes: true,
};
this.observer = new MutationObserver((list, observer) => {
let p = list[0].target;
if (p.getElementsByClassName(_POST.video).length >= 1) {
let e = p;
for (let i = 0; i < 4; i++) {
e = e.parentElement;
}
this.handle_carousel(e);
}
});
if (_DEBUG) L.debug('constructor of class Gather called');
}
main = function () {
this.url = window.location.pathname;
switch (true) {
case _PATTERN.dm.test(this.url):
break;
case _PATTERN.feed.test(this.url):
this.handle_posts();
break;
case _PATTERN.inbox.test(this.url):
break;
case _PATTERN.profile.test(this.url):
this.handle_posts();
break;
case _PATTERN.story.test(this.url):
this.handle_stories();
break;
default:
break;
}
};
handle_carousel = function (post) {
let index = 0;
let p = post.parentElement.parentElement.parentElement;
const positions = p.getElementsByClassName(_POST.position[0])[0]
.children[_POST.position[1]].children;
for (let i = 0; i < positions.length; i++) {
if (positions[i].classList.contains(_POST.position[2])) {
index = i;
break;
}
}
if (index > 1) index = 1;
let _p = p.getElementsByClassName(_POST.position[4])[index];
let src =
_p.getElementsByClassName(_POST.image).length == 1
? _p.getElementsByClassName(_POST.image)[0]
: _p.getElementsByClassName(_POST.video)[0];
if (src.classList.contains(_POST.video)) {
src.volume = settings.obj.volume;
this.handle_videos(_p);
}
return src.src;
};
handle_posts = function () {
const posts = document.getElementsByClassName(_POST.class);
for (let i = 0; i < posts.length; i++) {
if (posts[i].getElementsByClassName('_8jZFn'))
this.handle_videos(posts[i]); // image always appears again if scrolling down in feed
if (posts[i].getAttribute(_POST.attribute)) continue;
let src;
if (posts[i].classList.contains(_POST.carousel)) {
const p = posts[i].getElementsByClassName(_POST.position[4]);
const c = posts[i].getElementsByClassName(_POST.position[3])[0];
let links = [];
for (let j = 0; j < p.length; j++) {
try {
links.push(
p[j].getElementsByClassName(_POST.image).length == 1
? p[j].getElementsByClassName(_POST.image)[0]
.src
: p[j].getElementsByClassName(_POST.video)[0]
.src
);
this.observer.observe(c, this.observer_opt);
} catch (err) {
L.error(err);
}
}
src = links;
} else {
try {
if (
posts[i].getElementsByClassName(_POST.image).length == 1
) {
src = posts[i].getElementsByClassName(_POST.image)[0]
.src;
} else {
let p = posts[i].getElementsByClassName(_POST.video)[0];
src = p.src;
p.volume = settings.obj.volume;
this.handle_videos(posts[i]);
}
} catch (err) {
L.error(err);
}
}
if (typeof src === 'undefined') continue;
this.append_icons(posts[i], src, 'copy');
this.append_icons(posts[i], src, 'download');
this.append_icons(posts[i], src, 'in_new');
posts[i].setAttribute(_POST.attribute, 'true');
}
};
handle_videos = function (post) {
try {
post.getElementsByClassName('fXIG0')[0].style.display = 'none';
post.getElementsByClassName('PyenC')[0].style.display = 'none';
post.getElementsByClassName(_POST.video)[0].controls = true;
post.getElementsByClassName('_8jZFn')[0].style.display = 'none';
} catch (e) {}
};
handle_stories = function () {};
append_icons = function (post, src, icon) {
let range = document.createRange();
let element = post.getElementsByClassName(_POST.icons[0])[0];
range.selectNode(element);
let append;
let p;
switch (icon) {
case 'copy':
append = range.createContextualFragment(`
`);
helper.insert_before(
append,
post.getElementsByClassName(_POST.icons[1])[0]
);
p = post.getElementsByClassName(`${_SCRIPT.name}-${icon}`)[0];
if (typeof src === 'object')
p.addEventListener('click', () => {
helper.to_clipboard(this.handle_carousel(p), 'url');
});
else
p.addEventListener('click', () => {
helper.to_clipboard(src, 'url');
});
break;
case 'download':
append = range.createContextualFragment(`
`);
helper.insert_before(
append,
post.getElementsByClassName(_POST.icons[1])[0]
);
p = post.getElementsByClassName(`${_SCRIPT.name}-${icon}`)[0];
if (typeof src === 'object')
p.addEventListener('click', () => {
helper.download(this.handle_carousel(p));
});
else
p.addEventListener('click', () => {
helper.download(src);
});
break;
case 'in_new':
append = range.createContextualFragment(`
`);
helper.insert_before(
append,
post.getElementsByClassName(_POST.icons[1])[0]
);
p = post.getElementsByClassName(`${_SCRIPT.name}-${icon}`)[0];
if (typeof src === 'object')
p.addEventListener('click', () => {
helper.in_new(this.handle_carousel(p));
});
else
p.addEventListener('click', () => {
helper.in_new(src);
});
break;
default:
break;
}
};
}
/* Always call Logger first or everything will go downhill :( */
var L = new Logger();
var helper = new Helper();
var settings = new Settings();
var gather = new Gather();
})();