// ==UserScript==
// @name Lemmy post utilities - Filter posts by title
// @namespace Violentmonkey Scripts
// @description Filters posts on any lemmy instance by text in the title. It can also auto-open image posts, unblur thumbnails, and other things.
// @match https://*lemmy*.*/*
// @include https://lemy.nl/*
// @include https://burggit.moe/*
// @include https://lemmit.online/*
// @include https://yiffit.net/*
// @include https://reddthat.com/*
// @include https://sh.itjust.works/*
// @exclude https://lemmyverse.net/*
// @exclude https://lemmy-status.org/*
// @exclude https://search-lemmy.com/*
// @exclude https://join-lemmy.org/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_setClipboard
// @version 2.2.10
// @icon data:image/png;base64,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
// @author Xynoth
// @license GPT-3
// @date 14/7/2023, 20:15:03
// ==/UserScript==
//----------------------------------------
// CONSTANTS
//----------------------------------------
// Key constants
const BLOCKED_TAGS_KEY = "blockedTags";
const BLOCKED_CASE_SENSITIVE_TAGS_KEY = "blockedCaseSensitiveTags";
const AUTO_OPEN_MEDIA_POSTS_KEY = "autoOpenMediaPosts";
const UNBLUR_THUMBNAILS_KEY = "unblurThumbs";
const SHOW_FILTERED_STUBS_KEY = "showFilteredStubs";
const COMMAS_AS_SEPARATORS_KEY = "useCommasAsSeparators";
const CASE_SENSITIVE_KEY = "caseSensitiveTag";
const WIDGET_HEIGHT_KEY = "widgetHeight";
const FIX_BROKEN_VIDEO_PREVIEWS_KEY = "fixBrokenVideoPreviews";
const MARK_POSTS_AS_NSFW_KEY = "markNewPostsAsNSFW";
// Constant selectors
const postsContainerClass = "post-listings";
const profileContainerClass = "person-details";
const searchContainerClass = "search"
const postContainer = "post-listing";
const loadingSpinnerSelector = ".icon.spin"; // This is the "creators" select in the search page
const postCommunityContainer = ".community-link";
const postCommunityNameContainer = `${postCommunityContainer} > span`;
const postSrcLink = `div:nth-of-type(2) p a`;
const fixedPreviewContainerClass = "fixed-preview";
const fixedPreviewVideoClass = "fixed-preview-video";
const postAsNSFW = "#post-nsfw[type='checkbox']";
const createPostContainerId = "createPostForm";
const editPostClass = "post-form";
// GUI main elements
const settingsWidgetId = "settings-widget";
const blockedTagsDialogId = "tags-dialog";
const dialogBgId = "dialog-bg";
const settingsWidgetContainerId = "widget-container";
const blockTagListId = "blocked-insensitive-tag-list";
const csBlockTagListId = "blocked-sensitive-tags-list";
const openedPostChecker = "already-opened";
const filteredPostChecker = "filter-checked";
const processedPageChecker = "processed-page";
// CSS Color constants
const caseInsensitiveTagColor = "#dd2222";
const caseSensitiveTagColor = "#2052b3";
const primaryBtnColor = "#0052cc";
const primaryBtnHoverColor = "#0066ff";
const primaryBtnActiveColor = "#0047b3";
const widgetBgColor = "#1a1a1b";
const errorToastColor = "#f94b4b";
const errorToastBgColor = "#2f0808";
// Other constants
const logoImg = GM_info.script.icon;
const namedRegex = "regex\\((.*?)\\):";
// HTML content constants
const bottomWidgetContent = `
<div id="${settingsWidgetContainerId}">
<h1><span id="widget-logo"></span>Lemmy post utilities</h1>
<div class="form-entry">
<label>Blocked tags: </label>
<div class="btn-container">
<button id="block-tag-btn" type="button">🖊</button>
</div>
</div>
<div class="form-entry">
<label>
Show stubs on filter:
</label>
<div class="btn-container">
<span class="switch" id="show-stub-swt">
<input type="checkbox">
<span class="slider round"></span>
</span>
</div>
</div>
<div class="form-entry">
<label>
Fix broken video previews:
</label>
<div class="btn-container">
<span class="switch" id="fix-video-previews-swt">
<input type="checkbox">
<span class="slider round"></span>
</span>
</div>
</div>
<div class="form-entry">
<label>
Auto open media previews:
</label>
<div class="btn-container">
<span class="switch" id="auto-open-swt">
<input type="checkbox">
<span class="slider round"></span>
</span>
</div>
</div>
<div class="form-entry">
<label>
Unblur NSFW thumbnails:
</label>
<div class="btn-container">
<span class="switch" id="unblur-swt">
<input type="checkbox">
<span class="slider round"></span>
</span>
</div>
</div>
<div class="form-entry">
<label>
Mark new posts as NSFW:
</label>
<div class="btn-container">
<span class="switch" id="default-nsfw-posts-swt">
<input type="checkbox">
<span class="slider round"></span>
</span>
</div>
</div>
</div>
`;
const tagsDialogContent = `
<div id="blocked-tags-dialog-container">
<div id ="blocked-tags-dialog-head">
<h1>Blocked tags editor</h1>
<button type="button" class="close-dialog-btn">⨯</button>
</div>
<div>
<p>Any tag added here will hide any post that contains the word in its title.</p>
<p>You can also use some advanced filtering options like the following:</p>
<ul>
<li>Use <a href="https://regex101.com/">regex</a> starting a tag with <code>regex:</code></li>
<li>Create a named regex starting a tag with <code>regex(your-regex-name):</code></li>
<li>Filter by linked source instead of title starting a tag with <code>source:</code></li>
<li>Combine <code>source:</code> with any variant of <code>regex:</code></li>
<li>You can click any tag to copy it's raw value.</li>
</ul>
<p>These are the blocked tags you have for this instance:</p>
<div id="blocked-tags-field-container">
<div id="blocked-tags-field">
<p id="empty-blocked-tags">You haven't blocked anything yet.</p>
<ul id="${blockTagListId}" class="blocked-tags-list" hidden>
</ul>
<hr id="blocked-tags-separator" hidden>
<ul id="${csBlockTagListId}" class="blocked-tags-list" hidden>
</ul>
</div>
<div id="blocked-tags-field-legend">
<span id="tag-case-insensitive-legend" class="tag-legend">
<span class="tag-color-legend"></span>
<small class="tag-label-legend">Case insensitive tag</small>
</span>
<span id="tag-case-sensitive-legend" class="tag-legend">
<span class="tag-color-legend"></span>
<small class="tag-label-legend">Case sensitive tag</small>
</span>
</div>
</div>
<p id="clipboard-notice" hidden>Tag value copied to the clipboard!</p>
<div class="switch-container">
<label>
Use commas as tag separators:
</label>
<div class="btn-container">
<span class="switch" id="use-commas-swt">
<input type="checkbox">
<span class="slider round"></span>
</span>
</div>
</div>
<div class="switch-container">
<label>
Add tags as case sensitive:
</label>
<div class="btn-container">
<span class="switch" id="case-sensitive-swt">
<input type="checkbox">
<span class="slider round"></span>
</span>
</div>
</div>
<div id="tag-input-container">
<input type="text" id="tag-input" placeholder="Add your tags">
<button type="button" id="tag-save-btn">Save</button>
</div>
<label id="blocked-tags-input-error" hidden>Tag must be longer than 1 non-whitespace character!</label>
</div>
</div>
`;
const tagContent = `
<span title="TOOLTIP-CONTENT">
<label>TAG-NAME</label>
<button type="button">⨯</button>
</span>
`;
const filteredPostStubContent = `
<div class="hidden-post-stub-meta-container">
<p>This post was hidden because it contained the tag 'TAG'.</p>
<span class="hidden-post-stub-btn-container">
<button class="show-hidden-post-title-btn" type="button">Show title</button>
<button class="show-hidden-post-btn" type="button">Show post</button>
</span>
</div>
<span class="stub-hidden-post-title" hidden>
<br>
<p>Post title was '<b>POST-TITLE</b>' from <a class="stub-hidden-post-community-link">POST-COMMUNITY</a> community.</p>
</span>
`;
const videoSourceContent = `
<source src="VIDEO-SOURCE" type="video/VIDEO-TYPE">
`;
// CSS to add to style elements
const initialCSS = getInitialCSS();
const guiCSS = () => `
/* Fix for stubs leaving empty space at the bottom of the page for some reason */
html {
overflow-y: auto;
}
body {
overflow-y: clip;
scrollbar-width: none;
}
/* The settings widget */
#${settingsWidgetId} {
display: flex;
float: right;
bottom: 0;
right: 0.8rem;
max-width: 15rem;
position: fixed;
transform: translateY(${getData(WIDGET_HEIGHT_KEY) ?? storeData(WIDGET_HEIGHT_KEY, "21rem")});
transition: 200ms ease-in-out transform;
}
#${settingsWidgetId}:hover {
transform: translateY(1px);
}
/* The container for the widget */
#widget-container {
flex-direction: column;
border: 1px solid #333;
font-size: 0.9rem;
background-color: ${widgetBgColor};
width: 100%;
padding: 1rem;
border-radius: .5rem .5rem 0 0;
margin-top: 50px; /* This is the space that will allow showing the popup when hovering over it */
}
#widget-container:hover {
margin-top: 0;
}
/* Widget title */
#widget-container > h1 {
font-size: 1rem;
font-weight: bold;
height: 1rem;
}
#widget-logo {
display: inline-block;
background-image: url('${logoImg}');
height: 1rem;
width: 1rem;
background-size: contain;
background-repeat: no-repeat;
margin-right: 0.5rem;
}
/* Widget block button */
#block-tag-btn {
appearance: none;
color: #ddd;
background: rgba(255,255,255,0.1);
border: none;
border-radius: 2rem;
padding: .3rem .5rem;
}
/* The layout for each label + form control */
.form-entry {
display: inline-flex;
}
.form-entry:not(:first-child) {
margin-top: 0.8rem;
}
.form-entry > label {
display: flex;
align-items: center;
justify-content: left;
width: 10rem;
}
.btn-container {
display: grid;
place-items: center;
}
/* The dialog to filter tags */
#${blockedTagsDialogId} {
appearance: none;
border: none;
background-color: ${widgetBgColor};
padding: 1rem;
border-radius: 0.5rem;
box-shadow: 0 0 20px 5px rgba(255,255,255,0.2);
position: fixed;
top: 50%;
bottom: 50%;
z-index: 1001;
max-width: 36.5rem;
}
#dialog-bg {
display: none;
background-color: rgba(0,0,0,.5); /* For browsers that don't support backdrop-filter */
position: fixed;
height: 100vh;
width: 100vw;
backdrop-filter: blur(2px);
top: 0;
left: 0;
z-index: 1000;
}
#blocked-tags-separator {
width: 100%;
margin-left: auto;
margin-right: auto;
border-top: 2px solid #666;
margin-top: .5rem;
margin-bottom: .5rem;
}
#blocked-tags-dialog-head {
display: flex;
}
#blocked-tags-dialog-head > h1 {
font-size: 1.5rem;
margin-bottom: 1rem;
color: #fff;
}
#tag-input-container {
margin-top: 1rem;
margin-bottom: 1rem;
display: flex;
}
#blocked-tags-input-error {
color: ${errorToastColor};
font-size: 0.8rem;
background-color: ${errorToastBgColor};
padding: .4rem;
border-radius: .5rem;
font-weight: bold;
}
#blocked-tags-field-container {
margin-bottom: 1rem;
}
#blocked-tags-field {
display: grid;
padding: 1rem;
max-height: 20rem;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
place-items: center;
border: 1px solid #555;
border-radius: 0.3rem;
}
#empty-blocked-tags {
color: #666;
margin: 0;
padding: 1rem;
}
#clipboard-notice {
color: #2f2;
font-size: 0.8rem;
width: 100%;
text-align: center;
}
#tag-input {
border: none;
flex-grow: 100;
background-color: #333;
border-radius: .5rem;
padding: .5rem 1rem;
width: 80%;
margin-right: 1rem;
color: #ddd;
}
#tag-save-btn {
appearance: none;
border: none;
background-color: ${primaryBtnColor};
color: #ddd;
padding: .5rem 1rem;
border-radius: 0.3rem;
}
#tag-save-btn:hover {
background-color: ${primaryBtnHoverColor};
color: #fff;
}
#tag-save-btn:active {
background-color: ${primaryBtnActiveColor};
color: #fff;
}
#tag-case-insensitive-legend > span {
background-color: ${caseInsensitiveTagColor};
}
#tag-case-sensitive-legend > span {
background-color: ${caseSensitiveTagColor};
}
#blocked-tags-field-legend {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
}
.close-dialog-btn {
appearance: none;
color: #fff;
font-weight: bold;
background-color: transparent;
border: none;
padding: 0.2rem 0.6rem;
position: absolute;
right: 0;
top: 0;
transition: 200ms all;
}
.close-dialog-btn:hover {
border-radius: 6rem;
background-color: rgba(255,255,255,0.2);
}
.switch-container {
width: 100%;
display: flex;
}
.switch-container > label {
width: 16rem;
margin-right: 1rem;
}
.blocked-tags-list {
display: flex;
flex-wrap: wrap;
max-width: 35.5rem;
margin-bottom: 0;
padding: 0;
}
.blocked-tags-list > li {
display: inline-flex;
list-style-type: none;
}
.tag-legend {
margin-right: 1rem;
}
.tag-color-legend {
display: inline-block;
width: 7px;
height: 7px;
}
.tag-label-legend {
font-size: 0.6rem;
}
.blocked-tag {
border-radius: 0.4rem;
font-size: 0.8rem;
color: #fff;
margin: .2rem;
cursor: grab;
}
.blocked-tag[data-dragged-item] {
cursor: grabbing;
opacity: 0.7;
}
.blocked-tag label {
padding-left: .5rem;
white-space: pre-wrap;
cursor: grab;
}
.blocked-tag button {
appearance: none;
color: #fff;
background: transparent;
border: none;
border-radius: .4rem;
}
.blocked-tag button:hover {
background-color: rgba(255,255,255,0.2);
}
.case-insensitive-tag {
background-color: ${caseInsensitiveTagColor};
}
.case-sensitive-tag {
background-color: ${caseSensitiveTagColor};
}
/* The filtered post stubs */
.filtered-post-stub p {
margin-bottom: 0;
}
.hidden-post-stub-meta-container {
display: flex;
}
.hidden-post-stub-btn-container {
margin-left: auto;
}
.hidden-post-stub-btn-container > button {
appearance: none;
background-color: transparent;
border: none;
cursor: pointer;
color: #3498db;
margin-right: 1rem;
}
.hide-post-btn {
appearance: none;
color: #f22;
font-weight: bold;
font-size: 2rem;
line-height: 1rem;
width: 1.8rem;
background-color: transparent;
border: none;
padding: 0.2rem 0.6rem;
height: 0;
float: right;
transition: 200ms all;
}
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 30px;
height: 16px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #777;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 12px;
width: 12px;
left: 2px;
bottom: 2px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(12px);
-ms-transform: translateX(12px);
transform: translateX(12px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
`;
const unblurCSS = `
/* Unblurs thumbnails */
.img-blur {
filter: none !important;
}
`
//----------------------------------------
// GUI AND INITIAL SETUP
//----------------------------------------
// Load initial settings and store defaults if they didn't exist
console.info(`Loading data for domain '${document.location.host}'.`);
const blockedTitleTags = getData(BLOCKED_TAGS_KEY) ?? storeData(BLOCKED_TAGS_KEY, []);
const csBlockedTitleTags = getData(BLOCKED_CASE_SENSITIVE_TAGS_KEY) ?? storeData(BLOCKED_CASE_SENSITIVE_TAGS_KEY, []);
const expandMediaPosts = getData(AUTO_OPEN_MEDIA_POSTS_KEY) ?? storeData(AUTO_OPEN_MEDIA_POSTS_KEY, false);
const unblurThumbnails = getData(UNBLUR_THUMBNAILS_KEY) ?? storeData(UNBLUR_THUMBNAILS_KEY, false);
const useCommasAsSeparators = getData(COMMAS_AS_SEPARATORS_KEY) ?? storeData(COMMAS_AS_SEPARATORS_KEY, true);
const showStubsForFilteredPosts = getData(SHOW_FILTERED_STUBS_KEY) ?? storeData(SHOW_FILTERED_STUBS_KEY, true);
const fixPostVideoPreviews = getData(FIX_BROKEN_VIDEO_PREVIEWS_KEY) ?? storeData(FIX_BROKEN_VIDEO_PREVIEWS_KEY, true);
const caseSensitiveTags = getData(CASE_SENSITIVE_KEY) ?? storeData(CASE_SENSITIVE_KEY, false);
const markNewPostsAsNSFW = getData(MARK_POSTS_AS_NSFW_KEY) ?? storeData(MARK_POSTS_AS_NSFW_KEY, false);
if (blockedTitleTags.length > 0 || csBlockedTitleTags.length > 0)
console.info("Waiting for page to fully load to block tags: ", blockedTitleTags, csBlockedTitleTags);
// Add the GUI to control the settings to the document
const settingWidget = addElement(document.body, "DIV", settingsWidgetId, bottomWidgetContent);
const tagsDialog = addElement(document.body, "DIALOG", blockedTagsDialogId, tagsDialogContent);
const dialogBg = addElement(document.body, "DIV", dialogBgId);
// Add initial CSS changes
updateCSS();
// Load tags into the dialog window
updateVisibleTags();
// Reflect boolean values of settings on GUI
document.querySelector("#show-stub-swt > input").checked = showStubsForFilteredPosts;
document.querySelector("#auto-open-swt > input").checked = expandMediaPosts;
document.querySelector("#unblur-swt > input").checked = unblurThumbnails;
document.querySelector("#use-commas-swt > input").checked = useCommasAsSeparators;
document.querySelector("#case-sensitive-swt > input").checked = caseSensitiveTags;
document.querySelector("#fix-video-previews-swt > input").checked = fixPostVideoPreviews;
document.querySelector("#default-nsfw-posts-swt > input").checked = markNewPostsAsNSFW;
// --------- Add event listeners ---------
// Blocked list dialog button
document.getElementById("block-tag-btn").onclick = () => {
openDialog(tagsDialog);
}
// Close dialog button
tagsDialog.getElementsByClassName("close-dialog-btn")[0].onclick = () => {
closeDialog(tagsDialog);
}
// Close dialog on clicking outside the dialog
dialogBg.onclick = (event) => {
event.stopPropagation();
closeDialog(tagsDialog);
}
// Show stubs on filtering posts
document.getElementById("show-stub-swt").onclick = (event) => {
const showStubs = storeData(SHOW_FILTERED_STUBS_KEY, toggleCheckbox(event));
const postsContainer = getPostContainer();
if (postsContainer)
postsContainer.getElementsByClassName(postContainer).forEach(post => {
let siblingNode = post.nextElementSibling;
if (showStubs && post.hasAttribute("hidden")) {
// Show sibling separator as well
if (siblingNode && siblingNode.tagName == "HR")
siblingNode.removeAttribute("hidden");
// Show post with the stub
post.removeAttribute("hidden");
} else if (post.getElementsByClassName("filtered-post-stub")[0]) {
// Remove separator if it exists
if (siblingNode && siblingNode.tagName == "HR")
siblingNode.setAttribute("hidden", true);
// Hide post completelly
post.setAttribute("hidden", true);
}
})
}
// Fix some broken video previews
document.getElementById("fix-video-previews-swt").onclick = (event) => {
const fixVideoPreviews = storeData(FIX_BROKEN_VIDEO_PREVIEWS_KEY, toggleCheckbox(event));
const fixedPreviewContainers = document.getElementsByClassName(fixedPreviewContainerClass);
const fixedPreviewVideos = document.getElementsByClassName(fixedPreviewVideoClass);
const postsContainer = getPostContainer();
const wasProcessed = postsContainer.id === processedPageChecker;
if (!postsContainer)
return;
if (!wasProcessed)
postsContainer.setAttribute("id", processedPageChecker);
if (fixVideoPreviews) {
if (fixedPreviewContainers.length > 0 || fixedPreviewVideos.length > 0) {
for(let i = 0; i < fixedPreviewContainers.length; i++) {
fixedPreviewContainers[i].querySelector("picture").setAttribute("hidden", true);
fixedPreviewVideos[i].removeAttribute("hidden");
}
} else {
fixBrokenVideoPreviews(postsContainer);
}
} else if (fixedPreviewContainers.length > 0 || fixedPreviewVideos.length > 0) {
for(let i = 0; i < fixedPreviewContainers.length; i++) {
fixedPreviewContainers[i].querySelector("picture").removeAttribute("hidden");
fixedPreviewVideos[i].setAttribute("hidden", true);
}
}
}
// Auto-open tags on page reload
document.getElementById("auto-open-swt").onclick = (event) => {
const openMediaPosts = storeData(AUTO_OPEN_MEDIA_POSTS_KEY, toggleCheckbox(event));
const postsContainer = getPostContainer();
const wasProcessed = postsContainer.id === processedPageChecker;
if (!postsContainer)
return;
if (!wasProcessed)
postsContainer.setAttribute("id", processedPageChecker);
// Open posts if they weren't already
if (openMediaPosts) {
openPosts(postsContainer);
}
}
// Unblur setting
document.getElementById("unblur-swt").onclick = (event) => {
storeData(UNBLUR_THUMBNAILS_KEY, toggleCheckbox(event));
// Update CSS of site after having changed the setting
updateCSS();
}
// Mark new posts as NSFW by default
document.getElementById("default-nsfw-posts-swt").onclick = (event) => {
const markAsNSFW = storeData(MARK_POSTS_AS_NSFW_KEY, toggleCheckbox(event));
const NSFWCheckbox = document.querySelector(postAsNSFW);
const createPostForm = document.getElementById(createPostContainerId);
if (NSFWCheckbox && !NSFWCheckbox.checked && markAsNSFW && createPostForm)
NSFWCheckbox.click();
}
// Auto-open tags on page reload
document.getElementById("use-commas-swt").onclick = (event) => {
storeData(COMMAS_AS_SEPARATORS_KEY, toggleCheckbox(event));
}
// Auto-open tags on page reload
document.getElementById("case-sensitive-swt").onclick = (event) => {
storeData(CASE_SENSITIVE_KEY, toggleCheckbox(event));
}
// Accept pressing enter while in some input to send the data
document.getElementById("tag-input").onkeydown = (event) => {
if(event.keyCode === 13){
document.getElementById("tag-save-btn").click();
}
}
// Event for tag save on button click or enter on input of tag blocking
document.getElementById("tag-save-btn").onclick = () => {
const tagInput = document.getElementById("tag-input");
const errorEl = document.getElementById("blocked-tags-input-error");
const clipboardNoticeEl = document.getElementById("clipboard-notice");
const tagsToSubmit = document.getElementById("tag-input").value;
clipboardNoticeEl.setAttribute("hidden", true);
if (tagsToSubmit.trim().length > 1) {
const isCaseSensitive = getData(CASE_SENSITIVE_KEY);
const splitOnCommas = getData(COMMAS_AS_SEPARATORS_KEY);
let tagsAsArray = getTagsAsArray(tagsToSubmit, splitOnCommas);
let tagsKey;
if (isCaseSensitive)
tagsKey = BLOCKED_CASE_SENSITIVE_TAGS_KEY;
else
tagsKey = BLOCKED_TAGS_KEY;
const oldTags = getData(tagsKey);
tagsAsArray = tagsAsArray.filter(tag => !oldTags.includes(tag));
for(let i = 0; i < tagsAsArray.length; i++) {
const keywordStart = /^(regex:|source:(regex(\((.*?)\):|:))*|regex\((.*?)\):)/ig;
console.log(tagsAsArray[i].replace(keywordStart, ""));
if (tagsAsArray[i].replace(keywordStart, "").trim().length === 0) {
errorEl.removeAttribute("hidden");
return;
}
}
// Hide the error message if it was visible
if (errorEl.hasAttribute("hidden"))
errorEl.setAttribute("hidden", true);
if (tagsAsArray.length > 0) {
// Get the old array of tags and concatenate the new one
const allTags = oldTags.concat(tagsAsArray);
// Store the new array of tags and update the tags to show
storeData(tagsKey, allTags);
addTagsToDialog(tagsAsArray, tagsKey);
// Hide empty tags element if it was visible and show the tags
const blockedTagsListContainer = document.getElementById(blockTagListId);
const csBlockedTagsListContainer = document.getElementById(csBlockTagListId);
// Update tag section visibility if required
checkForEmptyTags(getData(BLOCKED_TAGS_KEY), getData(BLOCKED_CASE_SENSITIVE_TAGS_KEY));
}
// Clear input
tagInput.value = "";
} else {
errorEl.removeAttribute("hidden");
}
}
//----------------------------------------
// MAIN METHODS
//----------------------------------------
// Wait for page to fully load to start doing things
window.onload = () => {
const baseContainer = document.getElementById("app");
let NSFWCheckbox = document.querySelector(postAsNSFW);
let createPostForm = document.getElementById(createPostContainerId);
let searchContainer = document.getElementsByClassName(searchContainerClass)[0];
let postsContainer = getPostContainer();
if (NSFWCheckbox && !NSFWCheckbox.checked && markNewPostsAsNSFW && createPostForm)
NSFWCheckbox.click();
// Make sure that the widget height is correct
updateWidgetHeightCSS();
// If there is a posts container in the page
if (postsContainer) {
// Perform first filter
if (blockedTitleTags.length > 0 || csBlockedTitleTags.length > 0) {
console.info("Page loaded, filtering tags...");
filterPosts(postsContainer);
}
// Open remaining posts if enabled
if (expandMediaPosts)
openPosts(postsContainer);
// Fix video previews if there was any post
if (fixPostVideoPreviews)
fixBrokenVideoPreviews(postsContainer);
document.getElementsByClassName(postsContainer.className)[0].setAttribute("id", processedPageChecker);
}
// Observe the changes of the page to know when to rethrow the filter method when the user changes the page
const observer = new MutationObserver((e) => {
if (document.getElementById(processedPageChecker))
return;
const blockedTags = getData(BLOCKED_TAGS_KEY);
const csBlockedTags = getData(BLOCKED_CASE_SENSITIVE_TAGS_KEY);
const autoOpenMedia = getData(AUTO_OPEN_MEDIA_POSTS_KEY);
const fixVideoPreviews = getData(FIX_BROKEN_VIDEO_PREVIEWS_KEY);
const markAsNSFW = getData(MARK_POSTS_AS_NSFW_KEY);
searchContainer = document.getElementsByClassName(searchContainerClass)[0];
NSFWCheckbox = document.querySelector(postAsNSFW);
createPostForm = document.getElementById(createPostContainerId);
postsContainer = getPostContainer();
// If on the creation post page, mark as NSFW the post
if (NSFWCheckbox && !NSFWCheckbox.checked && markAsNSFW && createPostForm)
NSFWCheckbox.click();
for (let i = 0; i < e.length; i++) {
if (e[i].target.getElementsByClassName(editPostClass)[0])
return;
let postEdit = e[i].target.getElementsByClassName("form-control")[0];
let filteredPostStub = e[i].target.getElementsByClassName("filtered-post-stub")[0];
let filteredPostBtn = e[i].target.getElementsByClassName("hide-post-btn")[0];
// Prevent filtering to retrigger if editting a post
if (postEdit && searchContainer)
return;
// If a post was already filtered don't add stubs again
if (filteredPostBtn || filteredPostStub)
continue;
// Perform actions if on one of the pages
if (postsContainer) {
if (blockedTags.length > 0 || csBlockedTags.length > 0) {
console.info("Page reloaded, filtering new posts...");
filterPosts(postsContainer);
}
if (autoOpenMedia)
openPosts(postsContainer);
if (fixVideoPreviews)
fixBrokenVideoPreviews(postsContainer);
// Mark page as already processed
document.getElementsByClassName(postsContainer.className)[0].setAttribute("id", processedPageChecker);
break;
}
}
});
observer.observe(baseContainer, {subtree: true, childList: true});
}
// Gets initial CSS content
function getInitialCSS() {
let style = document.head.getElementsByTagName("style")[0];
if (!style) {
style = document.createElement('style');
document.head.appendChild(style);
return "";
}
return style.innerHTML;
}
// Gets the CSS from the script to be in effect
function getEffectiveCSS() {
let fullCSS = guiCSS();
// Check each setting that changes CSS apart from the GUI
if (getData(UNBLUR_THUMBNAILS_KEY))
fullCSS += unblurCSS;
return fullCSS;
}
// Splits tags on commas if necessary
function getTagsAsArray(tagsString, splitOnCommas=true) {
if (splitOnCommas) {
return tagsString.split(",");
}
return [tagsString];
}
// Gets the current page posts container
function getPostContainer() {
return document.getElementsByClassName(postsContainerClass)[0] ||
document.getElementsByClassName(profileContainerClass)[0] ||
document.getElementsByClassName(searchContainerClass)[0] ||
document.querySelector(".post");
}
// Adds an element to the document
function addElement(parentEl, elementTag, elementId, html="", idAsClass=false) {
let p = parentEl;
let newElement = document.createElement(elementTag);
if (idAsClass)
newElement.className = elementId;
else
newElement.setAttribute('id', elementId);
newElement.innerHTML = html;
p.appendChild(newElement);
return newElement;
}
// Adds the new tags to the dialog
function addTagsToDialog(tagArray, tagsType) {
let tagsContainerSelector = csBlockTagListId;
if (tagsType === BLOCKED_TAGS_KEY)
tagsContainerSelector = blockTagListId;
const blockedTagsListEl = document.getElementById(tagsContainerSelector);
tagArray.forEach(tag => {
addTagElement(tag, tagsType, blockedTagsListEl);
});
}
// Adds a tag element to the dialog
function addTagElement(tag, tagType, tagListElement) {
// Get the specific css class for tag type
let tagSpecificClass = "case-sensitive-tag";
if (tagType === BLOCKED_TAGS_KEY)
tagSpecificClass = "case-insensitive-tag";
// Add the element to the dialog and bind the button event
const newTag = addElement(tagListElement,
"LI",
`blocked-tag ${tagSpecificClass}`,
tagContent
.replace("TAG-NAME", parseTagName(tag))
.replace("TAG-TYPE", tagType)
.replace("TOOLTIP-CONTENT", `'${tag}' (${tagSpecificClass})`),
true);
newTag.getElementsByTagName("button")[0].onclick = () => {
onRemoveBlockedTag(tagType, tag, newTag);
}
newTag.getElementsByTagName("label")[0].onclick = () => {
GM_setClipboard(tag);
document.getElementById("clipboard-notice").removeAttribute("hidden");
}
// Mark element as draggable
newTag.setAttribute("draggable", true);
// Handle drag & drop events
newTag.ondragstart = (e) => {
newTag.setAttribute("data-dragged-item", true);
}
newTag.ondragover = (e) => {
e.preventDefault();
}
newTag.ondragend = () => {
newTag.removeAttribute("data-dragged-item");
}
newTag.ondrop = (e) => {
const target = e.target;
const targetTag = target.nodeName === "LABEL" || target.nodeName == "BUTTON" ?
target.parentNode.parentNode : target.parentNode;
const draggedItem = document.querySelector('li[data-dragged-item]');
const nextTagName = targetTag.getElementsByTagName("label")[0].innerHTML;
const movedTagName = draggedItem.getElementsByTagName("label")[0].innerHTML;
let movedTagParentId = targetTag.parentNode.id;
let draggedTagType = BLOCKED_TAGS_KEY;
if (draggedItem.className.includes("case-sensitive-tag"))
draggedTagType = BLOCKED_CASE_SENSITIVE_TAGS_KEY;
// Only accept dropping it in the same area
if (draggedItem.parentNode.id === movedTagParentId) {
// Update order of tags
let tags = getData(draggedTagType);
// We need to change a bit the logic depending on the position of the element
if (tags.indexOf(nextTagName) > tags.indexOf(movedTagName)) {
tags.splice(tags.indexOf(nextTagName) + 1, 0, null);
targetTag.parentNode.insertBefore(draggedItem, targetTag.nextSibling);
} else {
tags.splice(tags.indexOf(nextTagName), 0, null);
targetTag.parentNode.insertBefore(draggedItem, targetTag);
}
tags.splice(tags.indexOf(movedTagName), 1);
tags[tags.indexOf(null)] = movedTagName;
storeData(draggedTagType, tags);
}
newTag.removeAttribute('data-dragged-item');
}
}
// Updates the CSS of the site
function updateCSS() {
let style = document.head.getElementsByTagName("style")[0];
if (!style) {
style = document.createElement('style');
style.innerHTML = css;
document.head.appendChild(style);
return;
}
// Update the CSS with the initial one + the settings CSS
style.innerHTML = initialCSS + getEffectiveCSS();
}
// Update the visible part of the settings widget
function updateWidgetHeightCSS() {
const widget = document.getElementById(settingsWidgetContainerId);
const widgetHeight = widget.getBoundingClientRect().height;
const visibleTopHeight = 10;
if (widgetHeight > 0 && widgetHeight - visibleTopHeight > 0) {
const oldHeight = getData(WIDGET_HEIGHT_KEY);
let updatedHeight = (widgetHeight - visibleTopHeight) + "px";
if (oldHeight != updatedHeight) {
storeData(WIDGET_HEIGHT_KEY, (widgetHeight - visibleTopHeight) + "px");
updateCSS();
}
}
}
// Updates the visible tags in the blocked tags dialog
function updateVisibleTags() {
const csBlockedTags = getData(BLOCKED_CASE_SENSITIVE_TAGS_KEY);
const blockedTags = getData(BLOCKED_TAGS_KEY);
const blockedTagsListEl = document.getElementById(blockTagListId);
const csBlockedTagsListEl = document.getElementById(csBlockTagListId);
// Reset list
blockedTagsListEl.innerHTML = "";
csBlockedTagsListEl.innerHTML = "";
blockedTags.forEach(tag => {
addTagElement(tag, BLOCKED_TAGS_KEY, blockedTagsListEl);
});
csBlockedTags.forEach(tag => {
addTagElement(tag, BLOCKED_CASE_SENSITIVE_TAGS_KEY, csBlockedTagsListEl);
});
checkForEmptyTags(blockedTags, csBlockedTags);
}
// Parses the tag name if it's a named regex
function parseTagName(tag) {
if ((tag.toLowerCase().startsWith("regex(") || tag.toLowerCase().startsWith("source:regex(")) && new RegExp(namedRegex, "i").test(tag)) {
let regexName = getRegexNameFromTag(tag);
if (regexName != null) {
return tag.replace(new RegExp(namedRegex + ".*", "i"), `regex: ${regexName}`);
}
// If the named regex wasn't correctly formatted we just return the regular tag however it was typed
return tag;
}
return tag;
}
// Gets the regex name from a named regex tab
function getRegexNameFromTag(tag) {
let regexMatch = new RegExp(namedRegex).exec(tag);
if (regexMatch != null) {
return regexMatch[1];
}
return null;
}
// Toggles checkboxes on sliders inside the onclick event
function toggleCheckbox(event) {
let checkbox = event.target.parentNode.getElementsByTagName("input")[0];
checkbox.checked = !checkbox.checked;
return checkbox.checked;
}
// Opens a dialog box
function openDialog(dialogSelector) {
dialogSelector.show();
dialogBg.style.display = "block";
}
// Closes the dialog box
function closeDialog(dialogSelector) {
dialogSelector.close();
dialogBg.style.display = "none";
document.getElementById("blocked-tags-input-error").setAttribute("hidden", true);
document.getElementById("clipboard-notice").setAttribute("hidden", true);
}
// Adds the notice for no blocked tag if necessary
function checkForEmptyTags(blockedTags, csBlockedTags) {
const emptyTagsEl = document.getElementById("empty-blocked-tags");
const blockedTagsListEl = document.getElementById(blockTagListId);
const csBlockedTagsListEl = document.getElementById(csBlockTagListId);
const tagsSeparatorEl = document.getElementById("blocked-tags-separator");
const hasAnyBlockedTag = blockedTags.length > 0;
const hasAnyCsBlockedTag = csBlockedTags.length > 0;
const hasAnyTag = hasAnyBlockedTag || hasAnyCsBlockedTag;
const isBlockedTagsListHidden = blockedTagsListEl.hasAttribute("hidden");
const isCsBlockedTagsListHidden = csBlockedTagsListEl.hasAttribute("hidden");
// Check if any has tags
if (hasAnyTag) {
emptyTagsEl.setAttribute("hidden", true);
if (hasAnyBlockedTag)
blockedTagsListEl.removeAttribute("hidden");
else
blockedTagsListEl.setAttribute("hidden", true);
if (hasAnyCsBlockedTag)
csBlockedTagsListEl.removeAttribute("hidden");
else
csBlockedTagsListEl.setAttribute("hidden", true);
// Either both have tags or at least one has tags
if (hasAnyBlockedTag && hasAnyCsBlockedTag)
tagsSeparatorEl.removeAttribute("hidden");
else
tagsSeparatorEl.setAttribute("hidden", true);
// If it has no tags
} else if (!hasAnyTag) {
emptyTagsEl.removeAttribute("hidden");
blockedTagsListEl.setAttribute("hidden", true);
tagsSeparatorEl.setAttribute("hidden", true);
csBlockedTagsListEl.setAttribute("hidden", true);
}
}
// Checks if the page is loading
function checkIfPageIsLoading(postsContainer, posts) {
const searchContainer = document.getElementsByClassName(searchContainerClass)[0];
const emptyResultsContainer = document.querySelector(loadingSpinnerSelector);
return posts.length === 0 && postsContainer === searchContainer && emptyResultsContainer;
}
// Removes a tag from the view and the storeData
function onRemoveBlockedTag(tagType, tagToRemove, tagElement) {
// Remove the actual tag element
tagElement.parentNode.removeChild(tagElement);
// Update the storage
let allTags = getData(tagType);
let tagIndex = allTags.indexOf(tagToRemove);
if (tagIndex != -1)
allTags.splice(tagIndex, 1);
storeData(tagType, allTags);
// Toggle visibility of the elements where the tag was removed if they are empty
checkForEmptyTags(getData(BLOCKED_TAGS_KEY), getData(BLOCKED_CASE_SENSITIVE_TAGS_KEY));
}
// Filters posts by words from the title as specified by the user
function filterPosts(postsContainer) {
let posts = postsContainer.getElementsByClassName(postContainer);
if (checkIfPageIsLoading(postsContainer, posts)) {
let intervalCount = 0;
// Check for posts in 1 second intervals
const checkInterval = setInterval(() => {
posts = document.getElementsByClassName(postContainer);
// Finish interval checking the search finished or 30 seconds passed
if (posts.length > 0 || intervalCount > 30 || !document.querySelector(loadingSpinnerSelector)) {
clearInterval(checkInterval);
filterPostsInPage(posts);
}
intervalCount++;
}, 1000);
return;
}
filterPostsInPage(posts);
}
// Filters the posts in the current page
function filterPostsInPage(postsList) {
const showStubs = getData(SHOW_FILTERED_STUBS_KEY);
const blockedTags = getData(BLOCKED_TAGS_KEY);
const csBlockedTags = getData(BLOCKED_CASE_SENSITIVE_TAGS_KEY);
const blockedTagsByType = {
byTitle: [],
bySource: []
}
const csBlockedTagsByType = {
byTitle: [],
bySource: []
}
// Get type of filter for each tag first to avoid unnecesary iterations
blockedTags.forEach(tag => {
if (tag.startsWith("source:"))
blockedTagsByType.bySource.push(tag);
else
blockedTagsByType.byTitle.push(tag);
})
csBlockedTags.forEach(tag => {
if (tag.startsWith("source:"))
csBlockedTagsByType.bySource.push(tag);
else
csBlockedTagsByType.byTitle.push(tag);
})
// Filter every post
Array.from(postsList).forEach(post => {
if (post.getElementsByClassName(editPostClass)[0])
return;
post.setAttribute(filteredPostChecker, true);
const communityEl = post.querySelector(postCommunityNameContainer);
const communityLinkEl = post.querySelector(postCommunityContainer);
const sourceEl = post.querySelector(postSrcLink)
let titleEl = post.querySelector(".post-title h1 span");
let source = null;
let community;
let communityLink;
let title;
// Make sure we get a valid title selector
if (!titleEl)
titleEl = post.querySelector(".post-title h1 a");
title = titleEl.innerHTML;
// Make sure community info is in post, otherwise get from page assuming the user
// is in the community view instead, where the community isn't in the posts but the page
if (!communityEl) {
community = document.querySelector(postCommunityNameContainer).innerHTML;
communityLink = document.querySelector(postCommunityContainer).href;
} else {
community = communityEl.innerHTML;
communityLink = communityLinkEl.href;
}
if (sourceEl)
source = sourceEl.href;
const postInfo = {
post: post,
title: title,
communityName: community,
communityLink: communityLink,
source: source
}
// Filter posts by title first
const wasFiltered = filterPost(blockedTagsByType.byTitle, csBlockedTagsByType.byTitle, showStubs, postInfo);
// Filter posts by source after if it wasn't already filtered by title
if (!wasFiltered && source)
filterPost(blockedTagsByType.bySource, csBlockedTagsByType.bySource, showStubs, postInfo, true);
})
}
function filterPost(blockedTags, csBlockedTags, showStubs, postInfo, filterBySource=false) {
// Filter posts using case insensitive tags
if (blockedTags.length > 0) {
for(let i = 0; i < blockedTags.length; i++) {
let tag = blockedTags[i];
if (filterBySource)
tag = tag.substring(7);
const regex = new RegExp(escapeRegex(tag), "i");
if (regex.test(filterBySource ? postInfo.source : postInfo.title)) {
removePost(postInfo, blockedTags[i], showStubs);
return true;
}
}
}
// Filter posts using case sensitive tags
if (csBlockedTags.length > 0) {
for(let i = 0; i < csBlockedTags.length; i++) {
const tag = csBlockedTags[i];
if (filterBySource)
tag = tag.substring(7);
const regex = new RegExp(escapeRegex(tag));
if (regex.test(filterBySource ? postInfo.source : postInfo.title)) {
removePost(postInfo, csBlockedTags[i], showStubs);
return true;
}
}
}
}
// Escapes the special characters entered by a user
function escapeRegex(regex) {
// Escape \'s in regex with double \\ if it's a regex to be used in "new RegExp()" method
if (regex.toLowerCase().startsWith("regex:"))
return regex.substring(6).replace("\\", "\\");
// If it's a named regex
else if (regex.toLowerCase().startsWith("regex(") && new RegExp(namedRegex, "i").test(regex)) {
const regStart = `regex(${getRegexNameFromTag(regex)}):`;
return regex.substring(regStart.length).replace("\\", "\\");
}
return regex.replace(/([()[{*+.$^\\|?])/g, '\\$1');
}
// Filters out a post
function removePost(postInfo, tag, showStubs) {
const post = postInfo.post;
// Hide the content of the children and add a stub by default
post.style.display = "none";
addFilterStubToPost(postInfo, tag)
// If the setting to show stubs is disabled we hide the post completelly
if (!showStubs) {
// Removes the posts from the body completelly and logs it to the console
console.info(`Removing post with title ${postInfo.title} from community ${postInfo.communityName} because the post contained the tag ${parseTagName(tag)}.`);
post.setAttribute("hidden", true);
let siblingNode = post.nextElementSibling;
// Remove separator if it exists
if (siblingNode && siblingNode.tagName == "HR")
siblingNode.setAttribute("hidden", true);
}
}
// Adds a filter stub after filtering a post
function addFilterStubToPost(postInfo, tag) {
let stub = addElement(postInfo.post, "DIV", "filtered-post-stub",
filteredPostStubContent.replace("TAG", parseTagName(tag))
.replace("POST-TITLE", postInfo.title)
.replace("POST-COMMUNITY", postInfo.communityName), true);
stub.getElementsByClassName("stub-hidden-post-community-link")[0].href = postInfo.communityLink;
// Add actions for the buttons
stub.getElementsByClassName("show-hidden-post-title-btn")[0].onclick = () => {
stub.getElementsByClassName("stub-hidden-post-title")[0].removeAttribute("hidden");
stub.getElementsByClassName("show-hidden-post-title-btn")[0].setAttribute("hidden", true);
}
// Add theming and add it before the filtered post
stub.classList.add("post-listing");
postInfo.post.parentNode.insertBefore(stub, postInfo.post);
stub.getElementsByClassName("show-hidden-post-btn")[0].onclick = () => {
stub.setAttribute("hidden", true);
postInfo.post.style= "";
// Make sure the title button element is hidden
stub.getElementsByClassName("stub-hidden-post-title")[0].setAttribute("hidden", true);
stub.getElementsByClassName("show-hidden-post-title-btn")[0].removeAttribute("hidden");
// Prepend the post re-remover if it didn't exist yet
let postRemoveEl = postInfo.post.getElementsByClassName("hide-post-btn")[0];
if (!postInfo.post.getElementsByClassName("hide-post-btn")[0]) {
postRemoveEl = document.createElement("BUTTON");
postRemoveEl.className = "hide-post-btn";
postRemoveEl.innerHTML = "🗶";
postRemoveEl.setAttribute("type", "button");
postInfo.post.prepend(postRemoveEl);
postRemoveEl.onclick = () => {
stub.removeAttribute("hidden");
postRemoveEl.setAttribute("hidden", true);
postInfo.post.style.display = "none";
}
} else
postRemoveEl.removeAttribute("hidden");
for (let i = 0; i < postInfo.post.children.length; i++) {
postInfo.post.children[i].style = "";
}
}
}
// Opens media posts so that the image or video is shown for all posts in the current page of the timeline
function openPosts(postsContainer) {
let posts = postsContainer.getElementsByClassName(postContainer);
if (checkIfPageIsLoading(postsContainer, posts)) {
let intervalCount = 0;
// Check for posts in 1 second intervals
const checkInterval = setInterval(() => {
posts = document.getElementsByClassName(postContainer);
// Finish interval checking the search finished or 30 seconds passed
if (posts.length > 0 || intervalCount > 30 || !document.querySelector(loadingSpinnerSelector)) {
clearInterval(checkInterval);
clickPostsThumbnail(posts);
}
intervalCount++;
}, 1000);
return;
}
clickPostsThumbnail(posts)
}
// Clicks the thumbnail of the post to open it
function clickPostsThumbnail(posts) {
Array.from(posts).forEach(post => {
const postHasMedia = post.querySelector("picture, video");
let clickableThumbnail = post.querySelector("button.thumbnail");
if (postHasMedia) {
if (!clickableThumbnail)
clickableThumbnail = post.querySelector("a.text-body[href$='.mp4'] .thumbnail, a.text-body[href$='.webm'] .thumbnail, a.text-body[href^='https://www.redgifs.com/watch'] .thumbnail")
if (clickableThumbnail && !post.getElementsByClassName("filtered-post-stub")[0]){
let intervalCount = 0;
const checkInterval = setInterval(() => {
const postChildren = post.children;
clickableThumbnail.click();
// Check that the post opened within 5 tries
if (postChildren.length > 2 || intervalCount > 5)
clearInterval(checkInterval);
intervalCount++;
}, 1000);
}
post.setAttribute(openedPostChecker, true);
}
})
}
// Fixes imgur previews showing as jpg instead of the actual video
function fixBrokenVideoPreviews(postsContainer) {
let posts = postsContainer.getElementsByClassName(postContainer);
if (checkIfPageIsLoading(postsContainer, posts)) {
let intervalCount = 0;
// Check for posts in 1 second intervals
const checkInterval = setInterval(() => {
posts = document.getElementsByClassName(postContainer);
// Finish interval checking the search finished or 30 seconds passed
if (posts.length > 0 || intervalCount > 30 || !document.querySelector(loadingSpinnerSelector)) {
clearInterval(checkInterval);
checkVideoPreviews(postsContainer);
}
intervalCount++;
}, 1000);
return;
}
checkVideoPreviews(postsContainer);
}
// Updates the video previews when needed so that they work again
function checkVideoPreviews(postsContainer) {
setTimeout(() => {
const posts = postsContainer.getElementsByClassName(postContainer);
updateVideoPreviews(posts);
}, 500);
}
// Updates the video previews
function updateVideoPreviews(posts) {
Array.from(posts).forEach(post => {
const postSourceLinkEl = post.querySelector(postSrcLink);
const imageLinkContainer = post.querySelectorAll("div:nth-of-type(3) > a:not(.btn)");
if (postSourceLinkEl && imageLinkContainer.length > 0) {
const postSourceLink = postSourceLinkEl.href;
let newSrc = postSourceLink;
if (postSourceLink.includes("i.imgur.com") && postSourceLink.endsWith(".gifv"))
newSrc = postSourceLink.replace(".gifv", ".mp4");
else if (!postSourceLink.includes("i.imgur.com") && postSourceLink.includes("imgur.com"))
newSrc = postSourceLink.replace("imgur.com", "i.imgur.com") + ".mp4";
else if (imageLinkContainer.length >= 1 && (postSourceLink != imageLinkContainer[0].href) && imageLinkContainer[0].href.endsWith(".webm") || imageLinkContainer[0].href.endsWith(".mp4")) {
newSrc = imageLinkContainer[0].href;
}
// Only apply changes if the src was suposed to be different
if (newSrc != postSourceLink) {
imageLinkContainer.forEach(imageContainer => {
let pictureContainer = imageContainer.querySelector("picture");
if (pictureContainer) {
pictureContainer.setAttribute("hidden", true);
let videoElement = addElement(post.querySelector("div:nth-child(3).my-2"), "VIDEO",
`embed-responsive-item ${fixedPreviewVideoClass} col-12`,
videoSourceContent.replace("VIDEO-SOURCE", newSrc)
.replace("VIDEO-TYPE", newSrc.split(".").at(-1)),
true)
videoElement.setAttribute("controls", "");
videoElement.setAttribute("loop", "");
videoElement.parentNode.classList.add("embed-responsive", fixedPreviewContainerClass);
}
});
}
}
});
}
//----------------------------------------
// STORAGE METHODS
//----------------------------------------
// Composes the key with the current instance name to store data per-instance
function composeInstanceKey(key) {
return document.location.host + "->" + key;
}
// Saves data to the storage of the userscript
function storeData(key, value) {
const instanceKey = composeInstanceKey(key);
let treatedValue = value;
if (typeof value === "array" || typeof value === "object")
treatedValue = JSON.stringify(value);
GM_setValue(instanceKey, treatedValue);
return treatedValue;
}
// Gets data from the userscript storage
function getData(key) {
const instanceKey = composeInstanceKey(key);
let value = GM_getValue(instanceKey);
if (value === null || value === undefined)
return null;
if (typeof value === "string") {
let isValueArray = value.startsWith("[") && value.endsWith("]");
let isValueObject = value.startsWith("{") && value.endsWith("}");
if (isValueArray || isValueObject)
return JSON.parse(value);
}
return value;
}