From 4a5d30e0bf20628b46534dda5210cb746a603be1 Mon Sep 17 00:00:00 2001 From: discomrade <83621080+discomrade@users.noreply.github.com> Date: Fri, 23 Feb 2024 03:50:57 +0000 Subject: [PATCH] [filter] add filter extension This extension is derived from Danbooru's blacklist code --- ext/filter/info.php | 17 +++ ext/filter/main.php | 49 ++++++++ ext/filter/script.js | 263 +++++++++++++++++++++++++++++++++++++++++++ ext/filter/style.css | 4 + ext/filter/theme.php | 27 +++++ 5 files changed, 360 insertions(+) create mode 100644 ext/filter/info.php create mode 100644 ext/filter/main.php create mode 100644 ext/filter/script.js create mode 100644 ext/filter/style.css create mode 100644 ext/filter/theme.php diff --git a/ext/filter/info.php b/ext/filter/info.php new file mode 100644 index 000000000..16a2574ab --- /dev/null +++ b/ext/filter/info.php @@ -0,0 +1,17 @@ + "", "Discomrade" => ""]; + public string $license = "WTFPL"; + public string $description = "Allow users to filter out tags."; + public ?string $documentation = "Admins can set default filters and users can override them in user settings. This is derived from Danbooru's blacklist code, it works in the user's browser with JavaScript, and will hide posts until the filter runs."; +} diff --git a/ext/filter/main.php b/ext/filter/main.php new file mode 100644 index 000000000..1735b13c5 --- /dev/null +++ b/ext/filter/main.php @@ -0,0 +1,49 @@ +set_default_string("filter_tags", "spoilers\nguro\nscat\nfurry -rating:s\n"); + } + + public function onPageRequest(PageRequestEvent $event): void + { + global $page; + $this->theme->addFilterBox(); + $page->add_html_header(""); + } + + public function onSetupBuilding(SetupBuildingEvent $event): void + { + $sb = $event->panel->create_new_block("Filters"); + $sb->add_longtext_option("filter_tags", 'Default filtered tags'); + $sb->add_label("This controls the tags which are hidden by default. This feature currently requires JavaScript. Separate filters by line, or by commas. You can enter multiple tags per filter, as well as negative tags."); + } + + public function onInitUserConfig(InitUserConfigEvent $event): void + { + global $config; + $event->user_config->set_default_string("filter_tags", $config->get_string("filter_tags")); + } + + public function onUserOptionsBuilding(UserOptionsBuildingEvent $event): void + { + global $user; + + $sb = $event->panel->create_new_block("Filters"); + $sb->add_longtext_option("filter_tags", 'Default filtered tags'); + $sb->add_label("This controls the tags which are hidden by default. This feature currently requires JavaScript. Separate filters by line, or by commas. You can enter multiple tags per filter, as well as negative tags."); + } +} diff --git a/ext/filter/script.js b/ext/filter/script.js new file mode 100644 index 000000000..05aa2de15 --- /dev/null +++ b/ext/filter/script.js @@ -0,0 +1,263 @@ +// This code is a modified, jquery-less adaptation of danbooru/app/javascript/src/javascripts/blacklists.js, with snippets from utility.js +// The following copyright notice is a legal requirement of Danbooru's BSD 2-Clause license + +// Copyright (c) 2013~2021, Danbooru Project +// All rights reserved. + +let Utility = {}; + +Utility.is_subset = function(array, subarray) { + var all = true; + + $.each(subarray, function(i, val) { + if ($.inArray(val, array) === -1) { + all = false; + } + }); + + return all; +} + +Utility.intersect = function(a, b) { + a = a.slice(0).sort(); + b = b.slice(0).sort(); + var result = []; + while (a.length > 0 && b.length > 0) { + if (a[0] < b[0]) { + a.shift(); + } else if (a[0] > b[0]) { + b.shift(); + } else { + result.push(a.shift()); + b.shift(); + } + } + return result; +} + +let Filter = {}; + +Filter.entries = []; + +Filter.parse_entry = function(string) { + var entry = { + "tags": string, + "require": [], + "exclude": [], + "optional": [], + "disabled": false, + "hits": 0, + "min_score": null + }; + + let tags = string.split(/ +/); + tags.forEach(function(tag) { + if (tag.charAt(0) === '-') { + entry.exclude.push(tag.slice(1)); + } else if (tag.charAt(0) === '~') { + entry.optional.push(tag.slice(1)); + } else if (tag.match(/^score:<.+/)) { + var score = tag.match(/^score:<(.+)/)[1]; + entry.min_score = parseInt(score); + } else { + entry.require.push(tag); + } + }); + return entry; +} + +Filter.parse_entries = function() { + var entries = document.getElementById("filter-tags").getAttribute("tags").replace(/(rating:\w)\w+/ig, "$1").toLowerCase().split(/[,\n]/); + entries = entries.filter(e => e.trim() !== ""); + + entries.forEach(function(tags) { + var entry = Filter.parse_entry(tags); + Filter.entries.push(entry); + }); +} + +Filter.toggle_entry = function(e) { + var link = e.target; + var tags = link.innerText; + var match = Filter.entries.find(function(entry, i) { + return entry.tags === tags; + }); + if (match) { + match.disabled = !match.disabled; + if (match.disabled) { + link.classList.add("filter-inactive"); + } else { + link.classList.remove("filter-inactive"); + } + } + Filter.apply(); + e.preventDefault(); +} + +Filter.update_sidebar = function() { + Filter.entries.forEach(function(entry) { + if (entry.hits.length === 0) { + return; + } + + var item = document.createElement("li"); + var link = document.createElement("a"); + var count = document.createElement("span"); + + link.innerText = entry.tags; + link.classList.add("filter"); + link.setAttribute("href", `/posts?tags=${encodeURIComponent(entry.tags)}`); + link.setAttribute("title", entry.tags); + link.addEventListener("click", Filter.toggle_entry); + let unique_hits = new Set(entry.hits).size; + count.innerText = unique_hits; + count.classList.add("tag_count"); + item.append(link); + item.append(" "); + item.append(count); + + document.getElementById("filter-list").append(item); + }); + + document.getElementById("Filtersleft").style.display = ""; +} + +Filter.disable_all = function() { + Filter.entries.forEach(function(entry) { + entry.disabled = true; + }); + // There is no need to process the filter when disabling + Array.from(Filter.posts()).forEach(function(post) { + post.classList.remove("filtered-active"); + }); + document.getElementById("disable-all-filters").style.display = "none"; + document.getElementById("re-enable-all-filters").style.display = ""; + Array.from(document.getElementsByClassName("filter")).forEach(function(post) { + post.classList.add("filter-inactive"); + }); +} + +Filter.enable_all = function() { + Filter.entries.forEach(function(entry) { + entry.disabled = false; + }); + Filter.apply(); + document.getElementById("disable-all-filters").style.display = ""; + document.getElementById("re-enable-all-filters").style.display = "none"; + Array.from(document.getElementsByClassName("filter")).forEach(function(post) { + post.classList.remove("filter-inactive"); + }); +} + +Filter.initialize_disable_all_filters = function() { + if (shm_cookie_get("ui-disable-filters") === "1") { + Filter.disable_all(); + } else { + // The filter has already been processed by this point + document.getElementById("disable-all-filters").style.display = ""; + } + + document.getElementById("disable-all-filters").addEventListener("click", function(e) { + shm_cookie_set("ui-disable-filters", "1"); + Filter.disable_all(); + e.preventDefault(); + }); + + document.getElementById("re-enable-all-filters").addEventListener("click", function(e) { + shm_cookie_set("ui-disable-filters", "0"); + Filter.enable_all(); + e.preventDefault(); + }); +} + + +Filter.apply = function() { + Filter.entries.forEach(function(entry) { + entry.hits = []; + }); + + var count = 0 + Array.from(Filter.posts()).forEach(function(post) { + count += Filter.apply_post(post); + }); + + return count; +} + +Filter.apply_post = function(post) { + var post_count = 0; + Filter.entries.forEach(function(entry) { + if (Filter.post_match(post, entry)) { + let post_id = post.getAttribute("data-post-id"); + entry.hits.push(post_id); + post_count += 1; + } + }); + if (post_count > 0) { + Filter.post_hide(post); + } else { + Filter.post_unhide(post); + } + return post_count; +}; + +Filter.posts = function() { + return document.getElementsByClassName("thumb"); +} + +Filter.post_match = function(post, entry) { + if (entry.disabled) { + return false; + } + + var score = parseInt(post.getAttribute("data-score")); + var score_test = entry.min_score === null || score < entry.min_score; + + var tags = post.getAttribute("data-tags").split(/ +/); +// tags.push(...(post.getAttribute("data-pools")).split(/ +/)); + tags.push("rating:" + post.getAttribute("data-rating")); +// tags.push("uploaderid:" + post.getAttribute("data-uploader-id")); +// post.getAttribute("flags").split(/ +/).forEach(function(v) { +// tags.push("status:" + v); +// }); + + return (Utility.is_subset(tags, entry.require) && score_test) + && (!entry.optional.length || Utility.intersect(tags, entry.optional).length) + && !Utility.intersect(tags, entry.exclude).length; +} + +Filter.post_hide = function(post) { + post.classList.add("filtered"); + post.classList.add("filtered-active"); + + var video = post.querySelector("video"); + if (video) { + video.pause(); + video.currentTime = 0; + } +} + +Filter.post_unhide = function(post) { + post.classList.add("filtered") + post.classList.remove("filtered-active"); + + var video = post.querySelector("video"); + if (video) { + video.play(); + } +} + +Filter.initialize_all = function() { + Filter.parse_entries(); + + if (Filter.apply() > 0) { + Filter.update_sidebar(); + Filter.initialize_disable_all_filters(); + } else { + document.getElementById("Filtersleft").style.display = "none"; + } +} + +if (document.getElementById("Filtersleft")) { + Filter.initialize_all(); +} diff --git a/ext/filter/style.css b/ext/filter/style.css new file mode 100644 index 000000000..ead4ca372 --- /dev/null +++ b/ext/filter/style.css @@ -0,0 +1,4 @@ +#filter-list{padding:revert; text-align:left;} +.thumb.filtered{display:unset} +.thumb.filtered.filtered-active{display:none;} +.filter-inactive,.filter-inactive:hover{text-decoration: line-through;} \ No newline at end of file diff --git a/ext/filter/theme.php b/ext/filter/theme.php new file mode 100644 index 000000000..53206d14c --- /dev/null +++ b/ext/filter/theme.php @@ -0,0 +1,27 @@ +can(Permissions::CHANGE_USER_SETTING)) { + $tags = $user_config->get_string("filter_tags"); + } else { + $tags = $config->get_string("filter_tags"); + } + $html = " +