mirror of
https://github.com/simon987/sist2.git
synced 2025-12-11 14:38:54 +00:00
2.0 (#46)
* extract scan code to libscan, (wip) * submodules * replace curl with mongoose (wip) * replace onion with mongoose (wip) * replace onion with mongoose (wip) * It compiles! (I think) * Update readme * Entirely remove libonion (WIP) * unscramble submodules * recover screenshot * Update mappings * Bug fixes * update * media meta fix * memory fixes * More bug fixes... * Bug fix w/ libmagic & vfile * libmagic fix (again) * Better lightbox, better video handler, random reloads fix * Use svg for info icon * re-enable http auth * mobi support #41, fix logs * Update README & cleanup
This commit is contained in:
694
src/static/js/search.js
Normal file
694
src/static/js/search.js
Normal file
@@ -0,0 +1,694 @@
|
||||
const SIZE = 40;
|
||||
let mimeMap = [];
|
||||
let tagMap = [];
|
||||
let mimeTree;
|
||||
let tagTree;
|
||||
|
||||
let searchBar = document.getElementById("searchBar");
|
||||
let pathBar = document.getElementById("pathBar");
|
||||
let lastDoc = null;
|
||||
let reachedEnd = false;
|
||||
let docCount = 0;
|
||||
let coolingDown = false;
|
||||
let searchBusy = true;
|
||||
let selectedIndices = [];
|
||||
let indexMap = {};
|
||||
|
||||
let size_min = 0;
|
||||
let size_max = 10000000000000;
|
||||
|
||||
let date_min = null;
|
||||
let date_max = null;
|
||||
|
||||
const CONF = new Settings();
|
||||
|
||||
const _defaults = {
|
||||
display: "grid",
|
||||
fuzzy: true,
|
||||
highlight: true,
|
||||
sort: "score"
|
||||
};
|
||||
|
||||
SORT_MODES = {
|
||||
score: {
|
||||
text: "Relevance",
|
||||
mode: [
|
||||
{_score: {order: "desc"}},
|
||||
{_tie: {order: "asc"}}
|
||||
],
|
||||
key: hit => hit["_score"]
|
||||
},
|
||||
date_asc: {
|
||||
text: "Date (Ascending)", mode: [
|
||||
{mtime: {order: "asc"}},
|
||||
{_tie: {order: "asc"}}
|
||||
],
|
||||
key: hit => hit["_source"]["mtime"]
|
||||
},
|
||||
date_desc: {
|
||||
text: "Date (Descending)", mode: [
|
||||
{mtime: {order: "desc"}},
|
||||
{_tie: {order: "asc"}}
|
||||
],
|
||||
key: hit => hit["_source"]["mtime"]
|
||||
},
|
||||
size_asc: {
|
||||
text: "Size (Ascending)", mode: [
|
||||
{size: {order: "asc"}},
|
||||
{_tie: {order: "asc"}}
|
||||
],
|
||||
key: hit => hit["_source"]["size"]
|
||||
},
|
||||
size_desc: {
|
||||
text: "Size (Descending)", mode: [
|
||||
{size: {order: "desc"}},
|
||||
{_tie: {order: "asc"}}
|
||||
],
|
||||
key: hit => hit["_source"]["size"]
|
||||
},
|
||||
}
|
||||
|
||||
function Settings() {
|
||||
this.options = {};
|
||||
|
||||
this._onUpdate = function () {
|
||||
$("#fuzzyToggle").prop("checked", this.options.fuzzy);
|
||||
}
|
||||
|
||||
this.load = function () {
|
||||
const raw = window.localStorage.getItem("options");
|
||||
if (raw === null) {
|
||||
this.options = _defaults;
|
||||
} else {
|
||||
this.options = JSON.parse(raw);
|
||||
}
|
||||
|
||||
this._onUpdate();
|
||||
}
|
||||
|
||||
this.save = function () {
|
||||
window.localStorage.setItem("options", JSON.stringify(this.options));
|
||||
this._onUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showEsError() {
|
||||
$.toast({
|
||||
heading: "Elasticsearch connection error",
|
||||
text: "sist2 web module encountered an error while connecting " +
|
||||
"to Elasticsearch. See server logs for more information.",
|
||||
stack: false,
|
||||
bgColor: "#a94442",
|
||||
textColor: "#f2dede",
|
||||
position: 'bottom-right',
|
||||
hideAfter: false
|
||||
});
|
||||
}
|
||||
|
||||
jQuery["jsonPost"] = function (url, data) {
|
||||
return jQuery.ajax({
|
||||
url: url,
|
||||
type: "post",
|
||||
data: JSON.stringify(data),
|
||||
contentType: "application/json"
|
||||
}).fail(err => {
|
||||
showEsError();
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
window.onload = () => {
|
||||
$("#theme").on("click", () => {
|
||||
if (!document.cookie.includes("sist")) {
|
||||
document.cookie = "sist=dark";
|
||||
} else {
|
||||
document.cookie = "sist=; Max-Age=-99999999;";
|
||||
}
|
||||
window.location.reload();
|
||||
})
|
||||
CONF.load();
|
||||
};
|
||||
|
||||
function toggleFuzzy() {
|
||||
searchDebounced();
|
||||
}
|
||||
|
||||
$.jsonPost("i").then(resp => {
|
||||
|
||||
const urlIndices = (new URLSearchParams(location.search)).get("i");
|
||||
resp["indices"].forEach(idx => {
|
||||
indexMap[idx.id] = idx.name;
|
||||
const opt = $("<option>")
|
||||
.attr("value", idx.id)
|
||||
.append(idx.name);
|
||||
|
||||
if (urlIndices) {
|
||||
if (urlIndices.split(",").indexOf(idx.name) !== -1) {
|
||||
opt.attr("selected", true);
|
||||
selectedIndices.push(idx.id);
|
||||
}
|
||||
} else if (!idx.name.includes("(nsfw)")) {
|
||||
opt.attr("selected", true);
|
||||
selectedIndices.push(idx.id);
|
||||
}
|
||||
$("#indices").append(opt);
|
||||
});
|
||||
|
||||
createPathTree("#pathTree");
|
||||
});
|
||||
|
||||
function getDocumentInfo(id) {
|
||||
return $.getJSON("d/" + id).fail(e => {
|
||||
console.log(e);
|
||||
showEsError();
|
||||
})
|
||||
}
|
||||
|
||||
function handleTreeClick(tree) {
|
||||
return (node, e) => {
|
||||
if (e !== "checked") {
|
||||
return
|
||||
}
|
||||
|
||||
if (node.id === "any") {
|
||||
if (!node.itree.state.checked) {
|
||||
tree.deselect();
|
||||
}
|
||||
} else {
|
||||
tree.node("any").deselect();
|
||||
}
|
||||
|
||||
searchDebounced();
|
||||
}
|
||||
}
|
||||
|
||||
$.jsonPost("es", {
|
||||
aggs: {
|
||||
mimeTypes: {
|
||||
terms: {
|
||||
field: "mime",
|
||||
size: 10000
|
||||
}
|
||||
}
|
||||
},
|
||||
size: 0,
|
||||
}).then(resp => {
|
||||
resp["aggregations"]["mimeTypes"]["buckets"].sort((a, b) => a.key > b.key).forEach(bucket => {
|
||||
let tmp = bucket["key"].split("/");
|
||||
let category = tmp[0];
|
||||
let mime = tmp[1];
|
||||
|
||||
let category_exists = false;
|
||||
|
||||
let child = {
|
||||
"id": bucket["key"],
|
||||
"text": `${mime} (${bucket["doc_count"]})`
|
||||
};
|
||||
|
||||
mimeMap.forEach(node => {
|
||||
if (node.text === category) {
|
||||
node.children.push(child);
|
||||
category_exists = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!category_exists) {
|
||||
mimeMap.push({"text": category, children: [child]});
|
||||
}
|
||||
});
|
||||
mimeMap.push({"text": "All", "id": "any"});
|
||||
|
||||
mimeTree = new InspireTree({
|
||||
selection: {
|
||||
mode: 'checkbox'
|
||||
},
|
||||
data: mimeMap
|
||||
});
|
||||
new InspireTreeDOM(mimeTree, {
|
||||
target: '#mimeTree'
|
||||
});
|
||||
mimeTree.on("node.state.changed", handleTreeClick(mimeTree));
|
||||
mimeTree.deselect();
|
||||
mimeTree.node("any").select();
|
||||
});
|
||||
|
||||
// Tags tree
|
||||
$.jsonPost("es", {
|
||||
aggs: {
|
||||
tags: {
|
||||
terms: {
|
||||
field: "tag",
|
||||
size: 10000
|
||||
}
|
||||
}
|
||||
},
|
||||
size: 0,
|
||||
}).then(resp => {
|
||||
resp["aggregations"]["tags"]["buckets"]
|
||||
.sort((a, b) => a["key"].localeCompare(b["key"]))
|
||||
.forEach(bucket => {
|
||||
addTag(tagMap, bucket["key"], bucket["key"], bucket["doc_count"])
|
||||
});
|
||||
|
||||
tagMap.push({"text": "All", "id": "any"});
|
||||
tagTree = new InspireTree({
|
||||
selection: {
|
||||
mode: 'checkbox'
|
||||
},
|
||||
data: tagMap
|
||||
});
|
||||
new InspireTreeDOM(tagTree, {
|
||||
target: '#tagTree'
|
||||
});
|
||||
tagTree.on("node.state.changed", handleTreeClick(tagTree));
|
||||
tagTree.node("any").select();
|
||||
searchBusy = false;
|
||||
});
|
||||
|
||||
function addTag(map, tag, id, count) {
|
||||
let tags = tag.split("#")[0].split(".");
|
||||
|
||||
let child = {
|
||||
id: id,
|
||||
text: tags.length !== 1 ? tags[0] : `${tags[0]} (${count})`,
|
||||
children: []
|
||||
};
|
||||
|
||||
let found = false;
|
||||
map.forEach(node => {
|
||||
if (node.text === child.text) {
|
||||
found = true;
|
||||
if (tags.length !== 1) {
|
||||
addTag(node.children, tags.slice(1).join("."), id, count);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!found) {
|
||||
if (tags.length !== 1) {
|
||||
addTag(child.children, tags.slice(1).join("."), id, count);
|
||||
map.push(child);
|
||||
} else {
|
||||
map.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function insertHits(resultContainer, hits) {
|
||||
for (let i = 0; i < hits.length; i++) {
|
||||
|
||||
if (CONF.options.display === "grid") {
|
||||
resultContainer._brick.append(createDocCard(hits[i]));
|
||||
} else {
|
||||
resultContainer.appendChild(createDocLine(hits[i]));
|
||||
}
|
||||
docCount++;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", function () {
|
||||
if (!searchBusy) {
|
||||
let threshold = 400;
|
||||
|
||||
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - threshold) {
|
||||
if (!reachedEnd) {
|
||||
coolingDown = true;
|
||||
search(lastDoc);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function getSelectedNodes(tree) {
|
||||
let selectedNodes = [];
|
||||
|
||||
let selected = tree.selected();
|
||||
|
||||
for (let i = 0; i < selected.length; i++) {
|
||||
|
||||
if (selected[i].id === "any") {
|
||||
return ["any"]
|
||||
}
|
||||
|
||||
//Only get children
|
||||
if (selected[i].text.indexOf("(") !== -1) {
|
||||
selectedNodes.push(selected[i].id);
|
||||
}
|
||||
}
|
||||
|
||||
return selectedNodes
|
||||
}
|
||||
|
||||
function search(after = null) {
|
||||
lastDoc = null;
|
||||
|
||||
if (searchBusy) {
|
||||
return;
|
||||
}
|
||||
searchBusy = true;
|
||||
|
||||
let searchResults = document.getElementById("searchResults");
|
||||
//Clear old search results
|
||||
let preload;
|
||||
if (!after) {
|
||||
while (searchResults.firstChild) {
|
||||
searchResults.removeChild(searchResults.firstChild);
|
||||
}
|
||||
preload = makePreloader();
|
||||
searchResults.appendChild(preload);
|
||||
}
|
||||
|
||||
let query = searchBar.value;
|
||||
let empty = query === "";
|
||||
let condition = empty ? "should" : "must";
|
||||
let filters = [
|
||||
{range: {size: {gte: size_min, lte: size_max}}},
|
||||
{terms: {index: selectedIndices}}
|
||||
];
|
||||
let fields = [
|
||||
"name^8",
|
||||
"content^3",
|
||||
"album^8", "artist^8", "title^8", "genre^2", "album_artist^8",
|
||||
"font_name^6"
|
||||
];
|
||||
|
||||
if ($("#fuzzyToggle").prop("checked")) {
|
||||
fields.push("content.nGram");
|
||||
fields.push("name.nGram^3");
|
||||
}
|
||||
|
||||
let path = pathBar.value.replace(/\/$/, "").toLowerCase(); //remove trailing slashes
|
||||
if (path !== "") {
|
||||
filters.push([{term: {path: path}}])
|
||||
}
|
||||
let mimeTypes = getSelectedNodes(mimeTree);
|
||||
if (!mimeTypes.includes("any")) {
|
||||
filters.push([{terms: {"mime": mimeTypes}}]);
|
||||
}
|
||||
|
||||
let tags = getSelectedNodes(tagTree);
|
||||
if (!tags.includes("any")) {
|
||||
filters.push([{terms: {"tag": tags}}]);
|
||||
}
|
||||
|
||||
if (date_min && date_max) {
|
||||
filters.push([{range: {mtime: {gte: date_min, lte: date_max}}}])
|
||||
} else if (date_min) {
|
||||
filters.push([{range: {mtime: {gte: date_min}}}])
|
||||
} else if (date_max) {
|
||||
filters.push([{range: {mtime: {lte: date_max}}}])
|
||||
}
|
||||
|
||||
let q = {
|
||||
"_source": {
|
||||
excludes: ["content", "_tie"]
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
[condition]: {
|
||||
simple_query_string: {
|
||||
query: query,
|
||||
fields: fields,
|
||||
default_operator: "and"
|
||||
}
|
||||
},
|
||||
filter: filters
|
||||
}
|
||||
},
|
||||
"sort": SORT_MODES[CONF.options.sort].mode,
|
||||
aggs:
|
||||
{
|
||||
total_size: {"sum": {"field": "size"}},
|
||||
total_count: {"value_count": {"field": "size"}}
|
||||
},
|
||||
size: SIZE,
|
||||
};
|
||||
|
||||
if (after) {
|
||||
q.search_after = [SORT_MODES[CONF.options.sort].key(after), after["_id"]];
|
||||
}
|
||||
|
||||
if (CONF.options.highlight) {
|
||||
q.highlight = {
|
||||
pre_tags: ["<mark>"],
|
||||
post_tags: ["</mark>"],
|
||||
fields: {
|
||||
content: {},
|
||||
// "content.nGram": {},
|
||||
name: {},
|
||||
"name.nGram": {},
|
||||
font_name: {},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$.jsonPost("es", q).then(searchResult => {
|
||||
let hits = searchResult["hits"]["hits"];
|
||||
if (hits) {
|
||||
lastDoc = hits[hits.length - 1];
|
||||
}
|
||||
|
||||
if (!after) {
|
||||
preload.remove();
|
||||
searchResults.appendChild(makeStatsCard(searchResult));
|
||||
} else {
|
||||
let pageIndicator = makePageIndicator(searchResult);
|
||||
searchResults.appendChild(pageIndicator);
|
||||
}
|
||||
|
||||
//Setup page
|
||||
let resultContainer = makeResultContainer();
|
||||
searchResults.appendChild(resultContainer);
|
||||
|
||||
if (CONF.options.display === "grid") {
|
||||
resultContainer._brick = new Bricklayer(resultContainer);
|
||||
}
|
||||
|
||||
if (!after) {
|
||||
docCount = 0;
|
||||
}
|
||||
reachedEnd = hits.length !== SIZE;
|
||||
insertHits(resultContainer, hits);
|
||||
searchBusy = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
let searchDebounced = _.debounce(function () {
|
||||
coolingDown = false;
|
||||
search()
|
||||
}, 500);
|
||||
|
||||
searchBar.addEventListener("keyup", searchDebounced);
|
||||
pathBar.addEventListener("keyup", searchDebounced);
|
||||
|
||||
//Size slider
|
||||
$("#sizeSlider").ionRangeSlider({
|
||||
type: "double",
|
||||
grid: false,
|
||||
force_edges: true,
|
||||
min: 0,
|
||||
max: 3684.03149864,
|
||||
from: 0,
|
||||
to: 3684.03149864,
|
||||
min_interval: 5,
|
||||
drag_interval: true,
|
||||
prettify: function (num) {
|
||||
|
||||
if (num === 0) {
|
||||
return "0 B"
|
||||
} else if (num >= 3684) {
|
||||
return humanFileSize(num * num * num) + "+";
|
||||
}
|
||||
|
||||
return humanFileSize(num * num * num)
|
||||
},
|
||||
onChange: function (e) {
|
||||
size_min = (e.from * e.from * e.from);
|
||||
size_max = (e.to * e.to * e.to);
|
||||
|
||||
if (e.to >= 3684) {
|
||||
size_max = 10000000000000;
|
||||
}
|
||||
|
||||
searchDebounced();
|
||||
}
|
||||
});
|
||||
|
||||
//Date slider
|
||||
$.jsonPost("es", {
|
||||
aggs: {
|
||||
date_min: {min: {field: "mtime"}},
|
||||
date_max: {max: {field: "mtime"}},
|
||||
},
|
||||
size: 0
|
||||
}).then(resp => {
|
||||
$("#dateSlider").ionRangeSlider({
|
||||
type: "double",
|
||||
grid: false,
|
||||
force_edges: true,
|
||||
min: resp["aggregations"]["date_min"]["value"],
|
||||
max: resp["aggregations"]["date_max"]["value"],
|
||||
from: resp["aggregations"]["date_min"]["value"],
|
||||
to: (Date.now() / 1000),
|
||||
min_interval: 3600 * 24 * 7,
|
||||
step: 3600 * 24,
|
||||
drag_interval: true,
|
||||
prettify: function (num) {
|
||||
let date = (new Date(num * 1000));
|
||||
return date.getUTCFullYear() + "-" + ("0" + (date.getUTCMonth() + 1)).slice(-2) + "-" + ("0" + date.getUTCDate()).slice(-2)
|
||||
},
|
||||
onFinish: function (e) {
|
||||
date_min = e.from === e.min ? null : e.from;
|
||||
date_max = e.to === e.max ? null : e.to;
|
||||
searchDebounced();
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
function updateIndices() {
|
||||
let selected = $('#indices').find('option:selected');
|
||||
selectedIndices = [];
|
||||
$(selected).each(function () {
|
||||
selectedIndices.push($(this).val());
|
||||
});
|
||||
|
||||
searchDebounced();
|
||||
}
|
||||
|
||||
document.getElementById("indices").addEventListener("change", updateIndices);
|
||||
updateIndices();
|
||||
|
||||
window.onkeyup = function (e) {
|
||||
if (e.key === "/" || e.key === "Escape") {
|
||||
const bar = document.getElementById("searchBar");
|
||||
bar.scrollIntoView();
|
||||
bar.focus();
|
||||
}
|
||||
};
|
||||
|
||||
function getNextDepth(node) {
|
||||
let q = {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{term: {index: node.index}},
|
||||
{range: {_depth: {gte: node.depth + 1, lte: node.depth + 3}}},
|
||||
]
|
||||
}
|
||||
},
|
||||
aggs: {
|
||||
paths: {
|
||||
terms: {
|
||||
field: "path",
|
||||
size: 10000
|
||||
}
|
||||
}
|
||||
},
|
||||
size: 0
|
||||
}
|
||||
|
||||
if (node.depth > 0) {
|
||||
q.query.bool.must = {
|
||||
prefix: {
|
||||
path: node.id,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return $.jsonPost("es", q).then(resp => {
|
||||
const buckets = resp["aggregations"]["paths"]["buckets"];
|
||||
if (!buckets) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const paths = [];
|
||||
|
||||
return buckets
|
||||
.filter(bucket => bucket.key.length > node.id.length || node.id.startsWith("/"))
|
||||
.sort((a, b) => a.key > b.key)
|
||||
.map(bucket => {
|
||||
|
||||
if (paths.some(n => bucket.key.startsWith(n))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = node.id.startsWith("/") ? bucket.key : bucket.key.slice(node.id.length + 1);
|
||||
|
||||
paths.push(bucket.key);
|
||||
|
||||
return {
|
||||
id: bucket.key,
|
||||
text: `${name}/ (${bucket.doc_count})`,
|
||||
depth: node.depth + 1,
|
||||
index: node.index,
|
||||
children: true,
|
||||
}
|
||||
}).filter(x => x !== null)
|
||||
})
|
||||
}
|
||||
|
||||
function handlePathTreeClick(tree) {
|
||||
return (event, node, handler) => {
|
||||
|
||||
if (node.depth !== 0) {
|
||||
$("#pathBar").val(node.id);
|
||||
$("#pathTreeModal").modal("hide");
|
||||
searchDebounced();
|
||||
}
|
||||
|
||||
handler();
|
||||
}
|
||||
}
|
||||
|
||||
function createPathTree(target) {
|
||||
let pathTree = new InspireTree({
|
||||
data: function (node, resolve, reject) {
|
||||
return getNextDepth(node);
|
||||
}
|
||||
});
|
||||
|
||||
selectedIndices.forEach(index => {
|
||||
pathTree.addNode({
|
||||
id: "/" + index,
|
||||
text: `/[${indexMap[index]}]`,
|
||||
index: index,
|
||||
depth: 0,
|
||||
children: true
|
||||
})
|
||||
})
|
||||
|
||||
new InspireTreeDOM(pathTree, {
|
||||
target: target
|
||||
});
|
||||
|
||||
pathTree.on("node.click", handlePathTreeClick(pathTree));
|
||||
}
|
||||
|
||||
function updateSettings() {
|
||||
CONF.options.display = $("#settingDisplay").val();
|
||||
CONF.options.fuzzy = $("#settingFuzzy").prop("checked");
|
||||
CONF.options.highlight = $("#settingHighlight").prop("checked");
|
||||
CONF.save();
|
||||
|
||||
searchDebounced();
|
||||
|
||||
$.toast({
|
||||
heading: "Settings updated",
|
||||
text: "Settings saved to browser storage",
|
||||
stack: 3,
|
||||
bgColor: "#00a4bc",
|
||||
textColor: "#fff",
|
||||
position: 'bottom-right',
|
||||
hideAfter: 3000,
|
||||
loaderBg: "#08c7e8",
|
||||
});
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
CONF.load();
|
||||
|
||||
$("#settingDisplay").val(CONF.options.display);
|
||||
$("#settingFuzzy").prop("checked", CONF.options.fuzzy);
|
||||
$("#settingHighlight").prop("checked", CONF.options.highlight);
|
||||
}
|
||||
Reference in New Issue
Block a user