Compare commits

..

14 Commits

Author SHA1 Message Date
Jetsparrow
0086202aa5
Update index.md for v0.0.4 2023-09-23 20:52:43 +03:00
jetsparrow
3c3a7c8180 Version 0.0.4
- added download button via ggntw.com
2023-09-23 20:48:04 +03:00
jetsparrow
036cf4df77 Add download button 2023-09-23 20:46:00 +03:00
Jetsparrow
a802f7952d
Update index.md 2022-02-12 01:31:08 +03:00
Jetsparrow
7239df4613
Update index.md 2022-02-12 01:29:01 +03:00
Jetsparrow
16c7158763
... 2022-02-12 01:06:34 +03:00
Jetsparrow
8b89c267e3
Fix video tag yet again 2022-02-12 01:03:27 +03:00
Jetsparrow
6b6564ba01
Fix video embed 2022-02-12 00:58:35 +03:00
c0f11aa940 v0.0.3
Proper ui controls
Doesn't mess with layout
Demo video
2022-02-12 00:54:42 +03:00
7b7f516b7a buttons corners fix, hover states 2022-02-11 23:52:23 +03:00
6a254ef49f images for filter toggle 2022-02-11 22:28:11 +03:00
9a0a848850 Image files for v0.0.3 2022-02-11 22:11:31 +03:00
4be078da65 Enable github pages 2022-02-11 22:07:25 +03:00
Jetsparrow
3625ea3bda v0.0.2-alpha
- Renamed (planning to do guides too)
- Added filter toggle to switch between filtering and displaying hidden content
- General tidying up
2022-02-11 22:07:24 +03:00
15 changed files with 325 additions and 168 deletions

View File

@ -1,14 +1,8 @@
# Steam Workshop Janitor # Steam Janitor
_A userscript that makes browsing through thousands of mods viable_ _A userscript that makes browsing mods viable_
## [Install 0.0.1](https://github.com/Jetsparrow/steam-workshop-janitor/raw/main/steam-workshop-janitor.user.js) [Demo, install links](https://jetsparrow.github.io/steam-janitor/)
**N.B.: this is a very crude, hacked together proof of concept. It doesn't even have a way to undo the filtering yet.** * Adds a "hide" button to all items on the workshop browse page. Hides item forever, until unhidden.
* Adds a "filter" checkbox to the top paging controls, allowing to switch between filtered and unfiltered view.
## Filter mods * Adds endless scrolling to workshop browse pages.
Adds a little "filter mod" X button to any workshop browse page. Use it, and you will no longer see the hundreds of miscellaneous translations to laguages you don't know, obsolete mods, etc.
## Endless scrolling
After the filter runs its course, you will be left with barely any mods. Wndless scrolling fixes that.

3
_config.yml Normal file
View File

@ -0,0 +1,3 @@
theme: jekyll-theme-midnight
title: Steam Janitor
description: A userscript that makes browsing mods viable

27
index.md Normal file
View File

@ -0,0 +1,27 @@
<img src="https://user-images.githubusercontent.com/37241560/153679204-15e543d6-f45d-401c-90da-591694240515.png">
# [Steam Janitor v0.0.4-alpha - install](https://github.com/Jetsparrow/steam-janitor/raw/main/steam-janitor.user.js)
_Requires a userscript extension for your browser, e.g. [Tampermonkey](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=en)_
**N.B.: this is an early alpha. Tested only in Tampermonkey on Google Chrome**
# How to use
Navigate to the browse tab of your game's workshop (`steamcommunity.com/workshop/browse/?appid=###`) and use the new buttons on the workshop items.
<video src="https://user-images.githubusercontent.com/37241560/153673942-b35b53e9-4c88-4695-8631-855243251740.mp4" autoplay muted loop style="max-width: 480px;" ></video>
# Features
## Filter unwanted items
A "hide" button is added to every item on a workshop browse page.
Clicking it will hide the item forever - until you turn off the filter and unhide it.
Translations to laguages you don't know, obsolete mods, gachimuchi memes, and mods that are just not quite your cup of tea - hide once and never see them again.
The "filter" checkbox at the top paging controls allows you to switch between the filtered and unfiltered view.
## Endless scrolling
After the filter runs its course, you will be left with barely any mods. Endless scrolling fixes that.
## Download button
Download the workshop item directly from the web as a .zip archive

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
res/filter_toggle_open.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
res/janitor_download.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
res/janitor_hide.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
res/janitor_hide_hover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
res/janitor_unhide.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Binary file not shown.

289
steam-janitor.user.js Normal file
View File

@ -0,0 +1,289 @@
// ==UserScript==
// @name Steam Janitor
// @namespace jetsparrow-steam-janitor
// @author Jetsparrow
// @description Hide unwanted user content in browse view, endless scrolling, downloads
// @match *://*.steamcommunity.com/workshop/browse/*
// @run-at document-end
// @version 0.0.4
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_download
// @connect ggntw.com
// @downloadURL https://jetsparrow.github.io/steam-janitor/steam-janitor.user.js
// ==/UserScript==
const addGlobalStyle = (doc, css) => {
let head = doc.getElementsByTagName('head')[0];
if (!head) return null;
let style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = css;
head.appendChild(style);
return style;
}
const htmlToElement = (doc, html) => {
var template = doc.createElement('template');
template.innerHTML = html.trim();
return template.content.firstChild;
}
const onVisible = (element, callback) => {
const observer = new IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
callback();
observer.unobserve(entry.target);
}
});
},
{ rootMargin: "0px 0px 200px 0px" }
);
observer.observe(element);
};
const selector = {
PAGING_INFO: ".workshopBrowsePagingWithBG",
ITEM_CONTAINER: ".workshopBrowseItems .workshopItemPreviewHolder",
ITEMS_CONTAINER: ".workshopBrowseItems",
ITEMS_HOVERS: ".workshopBrowseItems script",
NEXT_BUTTON: ".workshopBrowsePagingControls .pagebtn:last-child",
PAGINATOR: ".workshopBrowsePagingControls",
PAGE_INFO: ".workshopBrowsePagingInfo",
FOOTER: ".workshopBrowsePaging",
};
const elemId = {
scrollTarget: "footer",
filterToggleCheckbox: "janitorFilterToggleCheckbox",
filterToggleOn: "janitorFilterOnIcon",
filterToggleOff: "janitorFilterOffIcon",
};
const cssClass = {
unhidden: "janitorItem",
hiddenFiltered: "janitorItemHidden",
hiddenUnfiltered: "janitorItemHiddenUnfiltered",
hideButton: "janitorHideButton",
showButton: "janitorShowButton",
filterToggle: "janitorFilterToggle",
downloadButton: "janitorDownloadButton"
};
const resource = {
iconEyeOpen:"https://jetsparrow.github.io/steam-janitor/res/filter_toggle_open.png",
iconEyeClosed:"https://jetsparrow.github.io/steam-janitor/res/filter_toggle_closed.png",
btnHide:"https://jetsparrow.github.io/steam-janitor/res/janitor_hide.png",
btnHideHover:"https://jetsparrow.github.io/steam-janitor/res/janitor_hide_hover.png",
btnUnhide:"https://jetsparrow.github.io/steam-janitor/res/janitor_unhide.png",
btnUnhideHover:"https://jetsparrow.github.io/steam-janitor/res/janitor_unhide_hover.png",
btnDownload:"https://jetsparrow.github.io/steam-janitor/res/janitor_download.png",
btnDownloadHover:"https://jetsparrow.github.io/steam-janitor/res/janitor_download_hover.png",
};
const janitorCss = `
.${cssClass.filterToggle} * { vertical-align: middle; }
.${cssClass.hiddenFiltered} {display:none !important; }
.${cssClass.hiddenUnfiltered} img {opacity: 0.25;}
.${cssClass.hideButton} {width:25px; height:25px;}
.${cssClass.hiddenUnfiltered} .${cssClass.hideButton}:hover {background-image:url("${resource.btnUnhideHover}")}
.${cssClass.hiddenUnfiltered} .${cssClass.hideButton} {background-image:url("${resource.btnUnhide}")}
.${cssClass.unhidden} .${cssClass.hideButton}:hover {background-image:url("${resource.btnHideHover}")}
.${cssClass.unhidden} .${cssClass.hideButton} {background-image:url("${resource.btnHide}")}
.workshopItem .${cssClass.hideButton} { visibility: hidden; }
.workshopItem:hover .${cssClass.hideButton} { visibility: visible; position: absolute; top: 4px; right: 6px;}
.${cssClass.downloadButton} {width:25px; height:25px;}
.${cssClass.downloadButton}:hover {background-image:url("${resource.btnDownloadHover}")}
.${cssClass.downloadButton} {background-image:url("${resource.btnDownload}")}
.workshopItem .${cssClass.downloadButton} { visibility: hidden; }
.workshopItem:hover .${cssClass.downloadButton} { visibility: visible; position: absolute; top: 4px; right: 35px;}
`;
const setting = {
filterEnabled: "janitorFilterEnabled"
};
const defaultModData = () => {
let d = new Object();
d.hide = false;
return d;
}
const loadModData = (modId) => {
var j = GM_getValue("modid:" + modId, "");
return j == "" ? defaultModData() : JSON.parse(j);
}
const saveModData = (modId, data) => GM_setValue("modid:" + modId, JSON.stringify(data));
// Update classes on mod container when model changes
const updateHiddenClass = (doc, modId, filterOn) => {
var container = doc.getElementById(modId)?.parentElement?.parentElement;
if (!container) return;
const d = loadModData(modId);
container.classList.remove(cssClass.unhidden);
container.classList.remove(cssClass.hiddenFiltered);
container.classList.remove(cssClass.hiddenUnfiltered);
if (!d.hide) container.classList.add(cssClass.unhidden);
else if (filterOn) container.classList.add(cssClass.hiddenFiltered);
else container.classList.add(cssClass.hiddenUnfiltered);
}
// When pressing the hide/unhide button on mod container
const toggleHidden = (doc, modId) => {
var d = loadModData(modId);
d.hide = !d.hide;
saveModData(modId, d);
const filterOn = GM_getValue(setting.filterEnabled);
updateHiddenClass(doc, modId, filterOn);
}
// When pressing the download button on mod container
const downloadMod = (doc, modId) => {
const id = modId.replace("sharedfile_", "");
GM_xmlhttpRequest(
{
url:"https://api.ggntw.com/steam.request",
method: "POST",
data: JSON.stringify(
{
url:"https://steamcommunity.com/sharedfiles/filedetails/?id=" + id
}),
headers:
{
"Content-type": "application/json; charset=UTF-8"
},
onload: function(response)
{
if (response.status>299) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const res = JSON.parse(response.responseText);
console.log(res);
const name = res.url.substring(res.url.lastIndexOf('/') + 1);
GM_download(res.url, name);
}
});
}
// Add hide butons to mod container
const addJanitorButtons = (doc, container, id) => {
const hideButton = htmlToElement(doc, `<div class="${cssClass.hideButton}"> </>`);
hideButton.onclick = (e) => {
e.cancelBubble = true;
toggleHidden(document, id);
};
container.append(hideButton);
const downloadButton = htmlToElement(doc, `<div class="${cssClass.downloadButton}"> </>`);
downloadButton.onclick = (e) => {
e.cancelBubble = true;
downloadMod(document, id);
};
container.append(downloadButton);
}
// Update all loaded mod containers, update their visibility and add janitor buttons if necessary
const processContainers = (doc) => {
const filterOn = GM_getValue(setting.filterEnabled);
for (var el of doc.querySelectorAll(selector.ITEM_CONTAINER)){
const container = el.parentElement.parentElement;
const id = el.id;
updateHiddenClass(doc, id, filterOn);
if (container.janitorButtonsAdded) continue;
container.janitorButtonsAdded = true;
addJanitorButtons(doc, container, id);
}
}
const loadNextPage = (url) => {
fetch(url, { credentials: "same-origin" })
.then(response => response.text())
.then(html => {
const parser = new DOMParser();
const newDoc = parser.parseFromString(html, "text/html");
processContainers(newDoc);
const newMods = newDoc.querySelectorAll(selector.ITEM_CONTAINER);
const modContainer = document.querySelector(selector.ITEMS_CONTAINER);
for (const mod of newMods) {
const container = mod.parentElement.parentElement;
modContainer.appendChild(container);
}
const scripts = newDoc.querySelectorAll(selector.ITEMS_HOVERS);
for (const newScript of scripts){
const matches = newScript.innerHTML.match(/(sharedfile_\d+)/);
if (matches.length < 1) continue;
const data = loadModData(matches[0]);
if (data.hide) continue;
eval("try{ "+ newScript.innerHTML + " } catch {} ");
}
const nextUrl = newDoc.querySelector(selector.NEXT_BUTTON)?.getAttribute("href");
const footer = document.getElementById(elemId.scrollTarget);
if (nextUrl) onVisible(footer, loadNextPage.bind(null, nextUrl));
window.history.pushState("", "", url);
});
};
// When the global filter toggle is pressed
function toggleFilter(checkbox) {
GM_setValue(setting.filterEnabled, checkbox.checked);
const doc = checkbox.ownerDocument;
doc.getElementById(elemId.filterToggleOff).hidden = checkbox.checked;
doc.getElementById(elemId.filterToggleOn).hidden = !checkbox.checked;
processContainers(doc);
}
// main
(() => {
const load = () => {
addGlobalStyle(document, janitorCss);
document.querySelector(selector.PAGE_INFO)?.remove();
const filterToggleRoot = document.querySelector(selector.PAGING_INFO);
const toggle = htmlToElement(document, `
<div class="${cssClass.filterToggle}">
&nbsp;
<input type="checkbox" id="${elemId.filterToggleCheckbox}">
&nbsp;
<label for="${elemId.filterToggleCheckbox}">
<img src="${resource.iconEyeOpen}" id="${elemId.filterToggleOff}">
<img src="${resource.iconEyeClosed}" id="${elemId.filterToggleOn}">
</label>
&nbsp;
</div>
`);
const checkbox = toggle.children[0];
checkbox.checked = GM_getValue(setting.filterEnabled, true);
checkbox.onclick = () => toggleFilter(checkbox);
filterToggleRoot.prepend(toggle);
toggleFilter(checkbox);
processContainers(document);
const nextUrl = document.querySelector(selector.NEXT_BUTTON)?.getAttribute("href");
if (!nextUrl) {
console.error(`Could not find nextUrl through "${selector.NEXT_BUTTON}"`);
return;
}
loadNextPage(nextUrl);
document.querySelector(selector.PAGINATOR)?.remove();
};
load();
})();

View File

@ -1,156 +0,0 @@
// ==UserScript==
// @name Steam Workshop Janitor
// @description Hide unwanted mods in the browse view, endless scrolling
// @match *://*.steamcommunity.com/workshop/browse/*
// @run-at document-end
// @version 0.0.1
// @grant GM_setValue
// @grant GM_getValue
// @updateURL https://raw.githubusercontent.com/Jetsparrow/steam-workshop-janitor/main/steam-workshop-janitor.js
// @downloadURL https://raw.githubusercontent.com/Jetsparrow/steam-workshop-janitor/main/steam-workshop-janitor.js
// ==/UserScript==
const selectors = {
ITEM_CONTAINER: ".workshopBrowseItems .workshopItemPreviewHolder",
ITEMS_CONTAINER: ".workshopBrowseItems",
ITEMS_HOVERS: ".workshopBrowseItems script",
NEXT_BUTTON: ".workshopBrowsePagingControls .pagebtn:last-child",
PAGINATOR: ".workshopBrowsePagingControls"
};
function htmlToElement(doc, html) {
var template = doc.createElement('template');
html = html.trim(); // Never return a text node of whitespace as the result
template.innerHTML = html;
return template.content.firstChild;
}
const on_visible = (element, callback) => {
const observer = new IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
callback();
observer.unobserve(entry.target);
}
});
},
{ rootMargin: "0px 0px -200px 0px" }
);
observer.observe(element);
};
function defaultModData()
{
var res = new Object();
res.hide = false;
return res;
}
function loadModData(id){
var j = GM_getValue("modid:" + id, "");
return j == "" ? defaultModData() : JSON.parse(j);
}
function saveModData(id, data){
GM_setValue("modid:" + id, JSON.stringify(data));
}
function hideId(id){
var d = loadModData(id);
d.hide = true;
saveModData(id, d);
var elem = document.getElementById(id);
if (elem){
elem.parentElement.parentElement.remove();
}
}
function sweepHidden(doc){
var containers = doc.querySelectorAll(selectors.ITEM_CONTAINER);
for (var container of containers)
{
var id = container.id;
var data = loadModData(id);
if (data.hide){
container.parentElement.parentElement.remove();
}
}
}
function addButtonToElement(doc, elem){
var janitorButton = htmlToElement(doc, `<a>X</a>`)
let id = elem.id;
janitorButton.onclick = function(){hideId(id);};
elem.parentElement.parentElement.prepend(janitorButton);
}
function addButtons(doc){
var containers = doc.querySelectorAll(selectors.ITEM_CONTAINER);
for (var container of containers){
addButtonToElement(doc, container);
}
}
const load_next = (url) => {
fetch(url, { credentials: "same-origin" })
.then(response => response.text())
.then(html => {
const parser = new DOMParser();
const newDoc = parser.parseFromString(html, "text/html");
sweepHidden(newDoc);
addButtons(newDoc);
const newPaginator = newDoc.querySelector(selectors.PAGINATOR);
const oldPaginator = document.querySelector(selectors.PAGINATOR);
const pagParent = oldPaginator.parentElement;
oldPaginator.remove();
pagParent.append(newPaginator);
const newMods = newDoc.querySelectorAll(selectors.ITEM_CONTAINER);
const lastMod = newMods[newMods.length - 1];
const modContainer = document.querySelector(selectors.ITEMS_CONTAINER);
for (var mod of newMods) {
var container = mod.parentElement.parentElement;
modContainer.appendChild(container);
}
const scripts = newDoc.querySelectorAll(selectors.ITEMS_HOVERS);
for (var newScript of scripts){
var matches = newScript.innerHTML.match(/(sharedfile_\d+)/);
if (matches.length < 1) continue;
var data = loadModData(matches[0]);
if (data.hide) continue;
eval("try{ "+ newScript.innerHTML + " } catch {} ");
}
const nextButton = document.querySelector(selectors.NEXT_BUTTON);
if (lastMod && nextButton) {
const nextUrl = nextButton.getAttribute("href");
on_visible(lastMod, load_next.bind(null, nextUrl));
}
window.history.pushState("", "", url);
});
};
(() => {
const load = () => {
const next_button = document.querySelector(selectors.NEXT_BUTTON);
if (!next_button) {
console.error(`Could not find "${selectors.NEXT_BUTTON}"`);
return;
}
sweepHidden(document);
addButtons(document);
const nextUrl = next_button.getAttribute("href");
load_next(nextUrl);
};
load();
})();