diff options
author | Yorhel <git@yorhel.nl> | 2021-01-19 13:12:17 +0100 |
---|---|---|
committer | Yorhel <git@yorhel.nl> | 2021-01-20 09:06:12 +0100 |
commit | 9d1727c77d4b00d2a861c2c31f93d3aebff2f2d9 (patch) | |
tree | 61d77e79edb2590c06cca80c8d029d8630a5aabc | |
parent | b35af5ba52f3bceb603e8c7dda10d4abb84c6bd9 (diff) |
v2rw: Rewrite done, time to clean up old v2 code
Yay!
There are no more request handlers in the VNDB::* namespace and no more
Javascript in data/js/. This cleans up a lot of old legacy code that
wasn't fun to maintain.
38 files changed, 48 insertions, 4586 deletions
@@ -28,7 +28,6 @@ ALL_KEEP=\ www/robots.txt static/robots.txt ALL_CLEAN=\ - static/g/vndb.js \ static/g/plain.js \ static/g/elm.js \ data/icons/icons.css \ @@ -36,7 +35,6 @@ ALL_CLEAN=\ $(shell ls css/skins/*.sass | sed -e 's/css\/skins\/\(.\+\)\.sass/static\/g\/\1.css/g') PROD=\ - static/g/vndb.min.js static/g/vndb.min.js.gz \ static/g/plain.min.js static/g/plain.min.js.gz \ static/g/elm.min.js static/g/elm.min.js.gz \ static/g/icons.opt.png \ @@ -49,6 +47,7 @@ clean: rm -f ${ALL_CLEAN} ${PROD} rm -f static/g/icons.png rm -f static/f/{vndb,elm,plain}{,.min}.js{,.gz} static/f/icons{,.opt}.png static/s/*/style{,.min}.css{,.gz} static/s/*/boxbg.png + rm -f static/g/vndb{,.min}.js{,.gz} rm -rf elm/Gen/ rm -rf elm/elm-stuff/build-artifacts $(MAKE) -C sql/c clean @@ -81,8 +80,6 @@ chmod: all -# v2 & v2-rw - data/icons/icons.css: data/icons/*.png data/icons/*/*.png util/spritegen.pl | static/g util/spritegen.pl @@ -97,24 +94,12 @@ static/g/%.css: css/skins/%.sass css/v2.css data/icons/icons.css | static/g -# v2 - -static/g/vndb.js: data/js/*.js lib/VNDB/Types.pm util/jsgen.pl data/conf.pl | static/g - util/jsgen.pl - -static/g/vndb.min.js: static/g/vndb.js - uglifyjs $< --compress --mangle --comments '/(@license|@source|SPDX-)/' -o $@ - - - -# v2-rw - # Order of JS files matters, so we read an '//order:x' comment from the files and sort by that. # Files without that comment are assumed to have '//order:4'. # (This trick will not work if we ever add JS files generated by this Makefile) JS_FILES=$(shell find elm \! -path 'elm/elm-stuff/*' -name '*.js' -exec sh -c "echo \`grep -Eo '^// *order: *[0-9]+' \"{}\" || echo 4\` \"{}\"" \; | sed -E 's/\/\/ *order: *//' | sort | sed 's/..//') -static/g/plain.js: ${JS_FILES} | static/f +static/g/plain.js: ${JS_FILES} | static/g echo '// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0-only' > $@~ echo '// @source: https://code.blicky.net/yorhel/vndb/src/branch/master/elm' >>$@~ echo '// SPDX-License-Identifier: AGPL-3.0-only' >>$@~ @@ -157,11 +142,11 @@ endef elm/Gen/.generated: lib/VNWeb/*.pm lib/VNWeb/*/*.pm lib/VNDB/Types.pm lib/VNDB/ExtLinks.pm lib/VNDB/Config.pm data/conf.pl util/vndb.pl elmgen -static/g/elm.js: ${ELM_FILES} elm/Gen/.generated | static/f +static/g/elm.js: ${ELM_FILES} elm/Gen/.generated | static/g cd elm && ELM_HOME=elm-stuff elm make ${ELM_MODULES} --output ../$@ ${fix-elm} -static/g/elm.min.js: ${ELM_FILES} elm/Gen/.generated | static/f +static/g/elm.min.js: ${ELM_FILES} elm/Gen/.generated | static/g cd elm && ELM_HOME=elm-stuff elm make --optimize ${ELM_MODULES} --output ../$@ ${fix-elm} uglifyjs $@ --comments '/(@license|@source|SPDX-)/' --compress \ @@ -134,45 +134,6 @@ util/multi.pl (application server, optional): ``` -# Rewrites, rewrites, rewrites - -The VNDB website is currently (like every project beyond a certain age) in a -transitional state of rewrites. There are three "versions" and coding styles -across this repository: - -**Version 2** - -This is the code that powers the actual website. It lives in `lib/VNDB/` and -has `util/vndb.pl` as entry point. Front-end assets are in `data/js/`, -`data/style.css`, `data/icons/`, `static/f/` and `static/s/`. - -**Version 2-rw** - -This is a (recently started) backend rewrite of version 2. It lives in -`lib/VNWeb/` with Elm and Javascript code in `elm/`. Individual parts of the -website are gradually being moved into this new coding style and structure. -Version 2 and 2-rw run side-by-side in the same process and share a common -route table and database connection, so the entry point is still -`util/vndb.pl`. The primary goal of this rewrite is to make use of the clearer -version 3 structure and to slowly migrate the brittle frontend Javascript parts -to Elm and JSON APIs. - -**Version 3** - -There also used to be a "version 3" rewrite with a completely new user -interface. All of the improvements developed in version 3 are slowly being -backported and improved upon in version 2-rw and version 3 does not exist -anymore (though it can still be found in the version history). - -**Non-rewrites** - -Some parts of this repository are not affected by these rewrites. These include -the database structure, most of the scripts in `util/`, some common modules -spread across `lib/` and Multi, which resides in `lib/Multi/`. That's not to -say these are *final* or *stable*, but they're largely independent from the -website code. - - # License GNU AGPL, see COPYING file for details. diff --git a/data/js/dateselector.js b/data/js/dateselector.js deleted file mode 100644 index ed96ab8a..00000000 --- a/data/js/dateselector.js +++ /dev/null @@ -1,84 +0,0 @@ -/* Date selector widget for the 'release date'-style dates, with support for - * TBA and unknown month or day. Usage: - * - * <input type="hidden" class="dateinput" .. /> - * - * Will add a date selector to the HTML at that place, and automatically - * read/write the value of the hidden field. Alternative usage: - * - * var obj = dateLoad(ref, serfunc); - * - * If 'ref' is set, it will behave as above with 'ref' being the input object. - * Otherwise it will return the widget object. The setfunc, if set, will be - * called whenever the date widget is focussed or its value is changed. - * - * The object returned by dateLoad() can be used as follows: - * obj.date_val: Always contains the currently selected date. - * obj.dateSet(val): Change the selected date - */ -function load(obj, serfunc) { - var i; - var selops = {style: 'width: 70px', onfocus:serfunc, onchange: serialize, tabIndex: 10}; - - var year = tag('select', selops, - tag('option', {value:0}, '-year-'), - tag('option', {value:9999}, 'TBA') - ); - for(i=(new Date()).getFullYear()+5; i>=1980; i--) - year.appendChild(tag('option', {value: i}, i)); - - var month = tag('select', selops, - tag('option', {value:99}, '-month-') - ); - for(i=1; i<=12; i++) - month.appendChild(tag('option', {value: i}, i)); - - var day = tag('select', selops, - tag('option', {value:99}, '-day-') - ); - for(i=1; i<=31; i++) - day.appendChild(tag('option', {value: i}, i)); - - var div = tag('div', { - date_obj: obj, - date_serfunc: serfunc, - date_val: obj ? obj.value : 0 - }, year, month, day); - div.dateSet = function(v){ set(div, v) }; - - set(div, div.date_val); - return obj ? obj.parentNode.insertBefore(div, obj) : div; -} - -function set(div, val) { - val = +val || 0; - val = [ Math.floor(val/10000), Math.floor(val/100)%100, val%100 ]; - if(val[1] == 0) val[1] = 99; - if(val[2] == 0) val[2] = 99; - var l = byName(div, 'select'); - for(var i=0; i<l.length; i++) - for(var j=0; j<l[i].options.length; j++) - l[i].options[j].selected = l[i].options[j].value == val[i]; - serialize(div, true); -} - -function serialize(div, nonotify) { - div = div.dateSet ? div : this.parentNode; - var sel = byName(div, 'select'); - var val = [ - sel[0].options[sel[0].selectedIndex].value*1, - sel[1].options[sel[1].selectedIndex].value*1, - sel[2].options[sel[2].selectedIndex].value*1 - ]; - div.date_val = val[0] == 0 ? 0 : val[0] == 9999 ? 99999999 : val[0]*10000+val[1]*100+(val[1]==99?99:val[2]); - if(div.date_obj) - div.date_obj.value = div.date_val; - if(!nonotify && div.date_serfunc) - div.date_serfunc(div); -} - -var l = byClass('input', 'dateinput'); -for(var i=0; i<l.length; i++) - load(l[i]); - -window.dateLoad = load; diff --git a/data/js/dropdown.js b/data/js/dropdown.js deleted file mode 100644 index 76be3f35..00000000 --- a/data/js/dropdown.js +++ /dev/null @@ -1,91 +0,0 @@ -/* Dropdown widget, used as follows: - * - * ddInit(obj, align, func); - * - * Show a dropdown box on mouse-over on 'obj'. 'func' should generate and - * return the contents of the box as a DOM node, or null to not show a dropdown - * box at all. The 'align' argument indicates where the box should be shown, - * relative to the obj: - * - * left: To the left of obj - * bottom: To the bottom of obj - * tagmod: Special alignment for tagmod page - * - * Other functions: - * - * ddHide(); Hides the box - * ddRefresh(); Refreshes the box contents - */ -var box; - -function init(obj, align, contents) { - obj.dd_align = align; - obj.dd_contents = contents; - obj.onmouseover = show; -} - -function show() { - if(!box) { - box = tag('div', {id:'dd_box', 'class':'hidden'}); - addBody(box); - } - box.dd_lnk = this; - document.onmousemove = mouse; - document.onscroll = hide; - refresh(); -} - -function hide() { - if(box) { - setText(box, ''); - setClass(box, 'hidden', true); - box.dd_lnk = document.onmousemove = document.onscroll = null; - } -} - -function mouse(e) { - e = e || window.event; - // Don't hide if the cursor is on the link - for(var lnk = e.target || e.srcElement; lnk; lnk=lnk.parentNode) - if(lnk == box.dd_lnk) - return; - - // Hide if it's 10px outside of the box - var mouseX = e.pageX || (e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft); - var mouseY = e.pageY || (e.clientY + document.body.scrollTop + document.documentElement.scrollTop); - if(mouseX < box.dd_x-10 || mouseX > box.dd_x+box.offsetWidth+10 || mouseY < box.dd_y-10 || mouseY > box.dd_y+box.offsetHeight+10) - hide(); -} - -function refresh() { - if(!box || !box.dd_lnk) - return hide(); - var lnk = box.dd_lnk; - var content = lnk.dd_contents(lnk, box); - if(content == null) - return hide(); - setContent(box, content); - setClass(box, 'hidden', false); - - var o = lnk; - ddx = ddy = 0; - do { - ddx += o.offsetLeft; - ddy += o.offsetTop; - } while(o = o.offsetParent); - - if(lnk.dd_align == 'left') - ddx -= box.offsetWidth; - if(lnk.dd_align == 'tagmod') - ddx += lnk.offsetWidth-35; - if(lnk.dd_align == 'bottom') - ddy += lnk.offsetHeight; - box.dd_x = ddx; - box.dd_y = ddy; - box.style.left = ddx+'px'; - box.style.top = ddy+'px'; -} - -window.ddInit = init; -window.ddHide = hide; -window.ddRefresh = refresh; diff --git a/data/js/dropdownsearch.js b/data/js/dropdownsearch.js deleted file mode 100644 index 428858cd..00000000 --- a/data/js/dropdownsearch.js +++ /dev/null @@ -1,204 +0,0 @@ -/* Interactive drop-down search widget. Usage: - * - * dsInit(obj, url, trfunc, serfunc, retfunc); - * - * obj: An <input type="text"> object. - * - * url: The base URL of the XML API, e.g. "/xml/tags.xml?q=", the search query is appended to this URL. - * The resource at the URL should return an XML document with a - * <item id="something" ..>..</item> - * element for each result. - * - * trfunc(item, tr): Function that is given an <item> object given by the XML - * document and an empty <tr> object. The function should format the data of - * the item to be shown in the tr. - * - * serfunc(item, obj): Called whenever a user selects an item from the search - * results. Should return a string, which will be used as the new value of the - * input object. - * - * retfunc(obj): Called whenever the user selects an item from the search - * results (after setfunc()) or when enter is pressed (even if nothing is - * selected). - * - * setfunc and retfunc can be null. - * - * TODO: Some users of this widget consider serfunc() as their final "apply - * this selection" function, whereas others use retfunc() for this. Might be - * worth investigating whether the additional flexibility offered by - * retfunc() is actually necessary, and remove the callback if not. - */ -var boxobj; - -function box() { - if(!boxobj) { - boxobj = tag('div', {id: 'ds_box', 'class':'hidden'}, tag('b', 'Loading...')); - addBody(boxobj); - } - return boxobj; -} - -function init(obj, url, trfunc, serfunc, retfunc) { - obj.setAttribute('autocomplete', 'off'); - obj.onkeydown = keydown; - obj.onclick = obj.onchange = obj.oninput = function() { return textchanged(obj); }; - obj.onblur = blur; - obj.ds_returnFunc = retfunc; - obj.ds_trFunc = trfunc; - obj.ds_serFunc = serfunc; - obj.ds_searchURL = url; - obj.ds_selectedId = 0; - obj.ds_dosearch = null; - obj.ds_lastVal = obj.value; -} - -function blur() { - setTimeout(function () { - setClass(box(), 'hidden', true) - }, 500) -} - -function setselected(obj, id) { - obj.ds_selectedId = id; - var l = byName(box(), 'tr'); - for(var i=0; i<l.length; i++) - setClass(l[i], 'selected', id && l[i].id == 'ds_box_'+id); -} - -function setvalue(obj) { - if(obj.ds_selectedId != 0) - obj.value = obj.ds_lastVal = obj.ds_serFunc(byId('ds_box_'+obj.ds_selectedId).ds_itemData, obj); - if(obj.ds_returnFunc) - obj.ds_returnFunc(obj); - - setClass(box(), 'hidden', true); - setContent(box(), tag('b', 'Loading...')); - setselected(obj, 0); - if(obj.ds_dosearch) { - clearTimeout(obj.ds_dosearch); - obj.ds_dosearch = null; - } -} - -function enter(obj) { - // Make sure the form doesn't submit when enter is pressed. - // This solution is a hack, but it's simple and reliable. - var frm = obj; - while(frm && frm.nodeName.toLowerCase() != 'form') - frm = frm.parentNode; - if(frm) { - var oldsubmit = frm.onsubmit; - frm.onsubmit = function() { return false }; - setTimeout(function() { frm.onsubmit = oldsubmit }, 100); - } - - setvalue(obj); - return false; -} - -function updown(obj, up) { - var i, sel, l = byName(box(), 'tr'); - if(l.length < 1) - return true; - - if(obj.ds_selectedId == 0) - sel = up ? l.length-1 : 0; - else - for(i=0; i<l.length; i++) - if(l[i].id == 'ds_box_'+obj.ds_selectedId) - sel = up ? (i>0 ? i-1 : l.length-1) : (l[i+1] ? i+1 : 0); - - setselected(obj, l[sel].id.substr(7)); - return false; -} - -function textchanged(obj) { - // Ignore this event if the text hasn't actually changed. - if(obj.ds_lastVal == obj.value) - return true; - obj.ds_lastVal = obj.value; - - // perform search after a timeout - if(obj.ds_dosearch) - clearTimeout(obj.ds_dosearch); - obj.ds_dosearch = setTimeout(function() { - search(obj); - }, 500); - return true; -} - -function keydown(ev) { - var c = document.layers ? ev.which : document.all ? event.keyCode : ev.keyCode; - var obj = this; - - if(c == 9) // tab - return true; - - if(c == 13) // enter - return enter(obj); - - if(c == 38 || c == 40) // up / down - return updown(obj, c == 38); - - return textchanged(obj); -} - -function search(obj) { - var b = box(); - var val = obj.value; - - clearTimeout(obj.ds_dosearch); - obj.ds_dosearch = null; - - // hide the ds_box div if the search string is too short - if(val.length < 2) { - setClass(b, 'hidden', true); - setContent(b, tag('b', 'Loading...')); - setselected(obj, 0); - return; - } - - // position the div - var ddx=0; - var ddy=obj.offsetHeight; - var o = obj; - do { - ddx += o.offsetLeft; - ddy += o.offsetTop; - } while(o = o.offsetParent); - - b.style.position = 'absolute'; - b.style.left = ddx+'px'; - b.style.top = ddy+'px'; - b.style.width = obj.offsetWidth+'px'; - setClass(b, 'hidden', false); - - // perform search - ajax(obj.ds_searchURL + encodeURIComponent(val), function(hr) { results(hr, obj) }); -} - -function results(hr, obj) { - var lst = hr.responseXML.getElementsByTagName('item'); - var b = box(); - if(lst.length < 1) { - setContent(b, tag('b', 'No results...')); - setselected(obj, 0); - return; - } - - var tb = tag('tbody', null); - for(var i=0; i<lst.length; i++) { - var id = lst[i].getAttribute('id'); - var tr = tag('tr', {id: 'ds_box_'+id, ds_itemData: lst[i]} ); - - tr.onmouseover = function() { setselected(obj, this.id.substr(7)) }; - tr.onmousedown = function() { setselected(obj, this.id.substr(7)); setvalue(obj) }; - - obj.ds_trFunc(lst[i], tr); - tb.appendChild(tr); - } - setContent(b, tag('table', tb)); - setselected(obj, obj.ds_selectedId != 0 && !byId('ds_box_'+obj.ds_selectedId) ? 0 : obj.ds_selectedId); -} - -window.dsInit = init; diff --git a/data/js/filter.js b/data/js/filter.js deleted file mode 100644 index 8c2edb6d..00000000 --- a/data/js/filter.js +++ /dev/null @@ -1,727 +0,0 @@ -/* Filter box definition: - * [ <title>, - * [ <category_name>, - * [ <fieldcode>, <fieldname>, <fieldcontents>, <fieldreadfunc>, <fieldwritefunc>, <fieldshowfunc> ], .. - * ], .. - * ] - * Where: - * <title> human-readable title of the filter box - * <category_name> human-readable name of the category. ignored if there's only one category - * <fieldcode> code of this field, refers to the <field> in the filter format. Empty string for just a <tr> - * <fieldname> human-readanle name of the field. Empty to not display a label. Space for always-enabled items (without checkbox) - * <fieldcontents> tag() object, or an array of tag() objects - * <fieldreadfunc> function reference. argument: <fieldcontents>; must return data to be used in the filter format - * <fieldwritefunc> function reference, argument: <fieldcontents>, data from filter format; must update the contents with the passed data - * <fieldshowfunc> function reference, argument: <fieldcontents>, called when the field is displayed - * - * Filter string format: - * <field>-<value1>~<value2>.<field2>-<value>.<field3>-<value1>~<value2> - * Where: - * <field> = [a-z0-9]+ - * <value> = [a-zA-Z0-9_]+ and any UTF-8 characters not in the ASCII range - * Escaping of the <value>: - * "_<two-number-code>" - * Where <two-number-code> is the decimal index to the following array: - * _ <space> ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ ` { | } ~ - * For boolean fields, the <value> is either 0 or 1. - */ - -var fil_escape = "_ !\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~".split(''); -var fil_objs = []; - -function getObj(obj) { - while(!obj.fil_fields) - obj = obj.parentNode; - return obj; -} - - -function filLoad(lnk, serobj) { - var type = lnk.href.match(/#r$/) ? 'r' : lnk.href.match(/#c$/) ? 'c' : lnk.href.match(/#s$/) ? 's' : 'v'; - var l = {r: filReleases, c: filChars, s: filStaff, v: filVN}[type](); - - var fields = {}; - var cats = []; - var p = tag('p', {'class':'browseopts'}); - var c = tag('div', null); - var idx = 0; - for(var i=1; i<l.length; i++) { - if(!l[i]) - continue; - - // category link - var a = tag('a', { href: '#', onclick: selectCat, fil_onshow:[] }, l[i][0]); - cats.push(a); - p.appendChild(a); - p.appendChild(tag(' ')); - - // category contents - var t = tag('table', {'class':'formtable hidden', fil_a: a}, null); - a.fil_t = t; - for(var j=1; j<l[i].length; j++) { - var fd = l[i][j]; - var lab = typeof fd[1] == 'object' ? fd[1][0] : fd[1]; - var name = 'fil_check_'+type+'_'+fd[0]; - var f = tag('tr', {'class':'newfield', fil_code: fd[0], fil_readfunc: fd[3], fil_writefunc: fd[4]}, - // Checkbox - fd[0] ? tag('td', {'class':'check'}, - tag('input', {type:'checkbox', id:name, name:name, 'class': 'enabled_check'+(fd[1]==' '?' hidden':''), onclick: selectField })) - : tag('td', null), - // Label - fd[1] ? tag('td', {'class':'label'}, - tag('label', {'for':name}, lab), - typeof fd[1] == 'object' ? tag('b', fd[1][1]) : null - ) : null, - // Contents - tag('td', {'class':'cont' }, fd[2])); - if(fd[0]) - fields[fd[0]] = f; - if(fd[5]) - a.fil_onshow.push([ fd[5], fd[2] ]); - t.appendChild(f); - } - c.appendChild(t); - idx++; - } - - var savenote = tag('p', {'class':'hidden'}, '') - var obj = tag('div', { - 'class': 'fil_div hidden', - fil_fields: fields, - fil_cats: cats, - fil_savenote: savenote, - fil_serobj: serobj, - fil_lnk: lnk, - fil_type: type - }, - tag('a', {href:'#', onclick:show, 'class':'close'}, 'close'), - tag('h3', l[0]), - p, - tag('b', {'class':'ruler'}, null), - c, - tag('b', {'class':'ruler'}, null), - tag('input', {type:'button', 'class':'submit', value: 'Apply', onclick:function () { - var f = serobj; - while(f.nodeName.toLowerCase() != 'form') - f = f.parentNode; - f.submit(); - }}), - tag('input', {type:'button', 'class':'submit', value: 'Reset', onclick:function () { serobj.value = ''; deSerialize(obj) } }), - byId('pref_code') && lnk.id == 'filselect' ? tag('input', {type:'button', 'class':'submit', value: 'Save as default', onclick:saveDefault }) : null, - savenote - ); - lnk.fil_obj = obj; - lnk.onclick = show; - - addBody(obj); - fil_objs.push(obj); - deSerialize(obj); - selectCat(obj.fil_cats[0]); -} - - -function saveDefault() { - var but = this; - var obj = getObj(this); - var note = obj.fil_savenote; - setText(note, 'Loading...'); - but.enabled = false; - setClass(note, 'hidden', false); - var type = obj.fil_type == 'r' ? 'release' : 'vn'; - ajax('/xml/prefs.xml?formcode='+byId('pref_code').title+';key=filter_'+type+';value='+obj.fil_serobj.value, function (hr) { - setText(note, 'Your saved filters will be applied automatically to several other parts of the site as well, such as the homepage.'+ - ' To change these filters, come back to this page and use the "Save as default" button again.'+ - ' To remove your saved filters, hit "Reset" and then save.'); - but.enable = true; - }); -} - - -function selectCat(n) { - var lnk = n.fil_onshow ? n : this; - var obj = getObj(lnk); - setClass(obj.fil_savenote, 'hidden', true); - for(var i=0; i<obj.fil_cats.length; i++) { - var n = obj.fil_cats[i]; - setClass(n, 'optselected', n == lnk); - setClass(n.fil_t, 'hidden', n != lnk); - } - for(var i=0; i<lnk.fil_onshow.length; i++) - lnk.fil_onshow[i][0](lnk.fil_onshow[i][1]); - return false -} - - -function selectField(f) { - if(!f.parentNode) - f = this; - setClass(getObj(f).fil_savenote, 'hidden', true); - - // update checkbox and label - var o = f; - while(o.nodeName.toLowerCase() != 'tr') - o = o.parentNode; - var c = byClass(o, 'enabled_check')[0]; - if(c != f) - c.checked = true; - if(hasClass(c, 'hidden')) // When there's no label (e.g. tagspoil selector) - c.checked = true; - var l = byName(o, 'label')[0]; - if(l) - setClass(l, 'active', c.checked); - - // update category link - while(o.nodeName.toLowerCase() != 'table') - o = o.parentNode; - var l = byName(o, 'tr'); - var n=0; - for(var i=0; i<l.length; i++) { - var ch = byClass(l[i], 'enabled_check')[0]; - if(ch && !hasClass(ch, 'hidden') && ch.checked) - n++; - } - setClass(o.fil_a, 'active', n>0); - - // serialize - serialize(getObj(o)); - return true; -} - - -function escapeVal(val) { - var r = []; - for(var h=0; h<val.length; h++) { - var vs = (''+val[h]).split(''); - r[h] = ''; - - // this isn't a very fast escaping method, blame JavaScript for inflexible search/replace support - for(var i=0; i<vs.length; i++) { - for(var j=0; j<fil_escape.length; j++) - if(vs[i] == fil_escape[j]) - break; - r[h] += j == fil_escape.length ? vs[i] : '_'+(j<10?'0'+j:j); - } - } - - return r[0] == '' ? '' : r.join('~'); -} - - -function serialize(obj) { - if(!obj.fil_fields) - obj = getObj(this); - var num = 0; - var values = {}; - - for(var f in obj.fil_fields) { - var fo = obj.fil_fields[f]; - var ch = byClass(fo, 'enabled_check')[0]; - if(!ch || !ch.checked) - continue; - if(!hasClass(ch, 'hidden')) - num++; - - var v = escapeVal(fo.fil_readfunc(byClass(fo, 'cont')[0].childNodes[0])); - if(v != '') - values[fo.fil_code] = v; - } - - var l = []; - for(var f in values) - l.push(f+'-'+values[f]); - - obj.fil_serobj.value = l.join('.'); - setText(byName(obj.fil_lnk, 'i')[1], num > 0 ? ' ('+num+')' : ''); -} - - -function deSerialize(obj) { - var d = obj.fil_serobj.value; - var fs = d.split('.'); - - var f = {}; - for(var i=0; i<fs.length; i++) { - var v = fs[i].split('-'); - if(obj.fil_fields[v[0]]) - f[v[0]] = v[1]; - } - - for(var fn in obj.fil_fields) - if(!f[fn]) - f[fn] = ''; - for(var fn in f) { - var c = byClass(obj.fil_fields[fn], 'enabled_check')[0]; - if(!c) - continue; - c.checked = f[fn] != ''; - - var v = f[fn].split('~'); - for(var i=0; i<v.length; i++) - v[i] = v[i].replace(/_([0-9]{2})/g, function (a, e) { return fil_escape[Math.floor(e)] }); - - obj.fil_fields[fn].fil_writefunc(byClass(obj.fil_fields[fn], 'cont')[0].childNodes[0], v); - // not very efficient: selectField() does a lot of things that can be - // batched after all fields have been updated, and in some cases the - // writefunc() triggers the same selectField() as well - selectField(c); - } -} - - -function show() { - var obj = this.fil_obj || getObj(this); - - // Hide other filter objects - for(var i=0; i<fil_objs.length; i++) - if(fil_objs[i] != obj) { - setClass(fil_objs[i], 'hidden', true); - setText(byName(fil_objs[i].fil_lnk, 'i')[0], collapsed_icon); - } - - var hid = !hasClass(obj, 'hidden'); - setClass(obj, 'hidden', hid); - setText(byName(obj.fil_lnk, 'i')[0], hid ? collapsed_icon : expanded_icon); - setClass(obj.fil_savenote, 'hidden', true); - - var o = obj.fil_lnk; - ddx = ddy = 0; - do { - ddx += o.offsetLeft; - ddy += o.offsetTop; - } while(o = o.offsetParent); - ddy += obj.fil_lnk.offsetHeight+2; - ddx += (obj.fil_lnk.offsetWidth-obj.offsetWidth)/2; - obj.style.left = ddx+'px'; - obj.style.top = ddy+'px'; - - return false; -} - - -var curSlider = null; -function filFSlider(c, n, min, max, def, unit, ser, deser) { - // min/max/def/fil_val are in serialized form (i.e. a "value") - if(!ser) ser = function(v) { return v }; // integer -> value - if(!deser) deser = function(v) { return parseInt(v) }; // value -> integer - - min = deser(min); - max = deser(max); - var bw = 200; var pw = 1; // slidebar width and pointer width - var s = tag('p', {fil_val:def, 'class':'slider'}); - var b = tag('div', {style:'width:'+(bw-2)+'px;', s:s}); - var p = tag('div', {style:'width:'+pw+'px;', s:s}); - var v = tag('span', def+' '+unit); - s.appendChild(b); - b.appendChild(p); - s.appendChild(v); - - var set = function (e, v) { - var w = bw-pw-6; - var s,x; - - if(v) { - s = e; - x = deser(v[0] == '' ? def : v[0]); - x = (x-min)*w/(max-min); - } else { - s = curSlider; - if(!e) e = window.event; - x = (!e) ? (deser(def)-min)*w/(max-min) - : (e.pageX || e.clientX + document.body.scrollLeft - document.body.clientLeft)-5; - var o = s.childNodes[0]; - while(o.offsetParent) { - x -= o.offsetLeft; - o = o.offsetParent; - } - } - - if(x<0) x = 0; if(x>w) x = w; - s.fil_val = ser(min + Math.floor(x*(max-min)/w)); - s.childNodes[1].innerHTML = s.fil_val+' '+unit; - s.childNodes[0].childNodes[0].style.left = x+'px'; - return false; - } - - b.onmousedown = p.onmousedown = function (e) { - curSlider = this.s; - if(!curSlider.oldmousemove) curSlider.oldmousemove = document.onmousemove; - if(!curSlider.oldmouseup) curSlider.oldmouseup = document.onmouseup; - document.onmouseup = function () { - document.onmousemove = curSlider.oldmousemove; - curSlider.oldmousemove = null; - document.onmouseup = curSlider.oldmouseup; - curSlider.oldmouseup = null; - selectField(curSlider); - return false; - } - document.onmousemove = set; - return set(e); - } - - return [c, n, s, function (c) { return [ c.fil_val ]; }, set ]; -} - -function filFSelect(c, n, lines, opts) { - var s = tag('select', {onfocus: selectField, onchange: serialize, multiple: lines > 1, size: lines}); - for(var i=0; i<opts.length; i++) { - if(typeof opts[i][1] != 'object') - s.appendChild(tag('option', {name: opts[i][0]}, opts[i][1])); - else { - var g = tag('optgroup', {label: opts[i][0]}); - for(var j=1; j<opts[i].length; j++) - g.appendChild(tag('option', {name: opts[i][j][0]}, opts[i][j][1])); - s.appendChild(g); - } - } - return [ c, lines > 1 ? [ n, 'Boolean or, selecting more gives more results' ] : n, s, - function (c) { - var l = []; - for(var i=0; i<c.options.length; i++) - if(c.options[i].selected) - l.push(c.options[i].name); - return l; - }, - function (c, f) { - for(var i=0; i<c.options.length; i++) { - for(var j=0; j<f.length; j++) - if(c.options[i].name+'' == f[j]+'') // beware of JS logic: 0 == '', but '0' != '' - break; - c.options[i].selected = j != f.length; - } - } - ]; -} - -function filFInput(c, n) { - return [ c, n, - tag('input', {type: 'text', 'class': 'text', onfocus: selectField, onchange: serialize}), - function (c) { return [c.value] }, - function (c, f) { c.value = f } - ] -} - -function filFEngine(c, n) { - var input = tag('input', {type: 'text', 'class': 'text', onfocus: selectField, onchange: serialize}); - dsInit(input, '/xml/engines.xml?q=', - function(item, tr) { tr.appendChild(tag('td', shorten(item.firstChild.nodeValue, 40))); }, - function(item, obj) { return item.firstChild.nodeValue; }, - function(o) { selectField(o) } - ); - return [ c, n, input, - function (c) { return [c.value] }, - function (c, f) { c.value = f } - ] -} - - -function filFOptions(c, n, opts) { - var p = tag('p', {'class':'opts', fil_val:opts[0][0]}); - var sel = function (e) { - var o = typeof e == 'string' ? e : this.fil_n; - var l = byName(p, 'a'); - for(var i=0; i<l.length; i++) - setClass(l[i], 'tsel', l[i].fil_n+'' == o+''); - p.fil_val = o; - if(typeof e != 'string') - selectField(p); - return false - }; - for(var i=0; i<opts.length; i++) { - p.appendChild(tag('a', {href:'#', fil_n: opts[i][0], onclick:sel}, opts[i][1])); - if(i<opts.length-1) - p.appendChild(tag('b', '|')); - } - return [ c, n, p, - function (c) { return [ c.fil_val ] }, - function (c, v) { sel(v[0]) } - ]; -} - - -// fieldcode -> See <fieldcode> in filter definitions -// fieldname -> See <fieldname> in filter definitions -// src -> The API URL where to get items, must work with dropdownsearch.js and support appending ';q=', ';id=' and ';r=' to it. -// fmtlist -> Called with item id + XML data, should return an inline element to inject into the list view. -function filFDList(fieldcode, fieldname, src, fmtds, fmtlist) { - var visible = false; - var addel = function(ul, id, data) { - ul.appendChild( - tag('li', { fil_id: id }, - data ? fmtlist(id, data) : null, - ' (', tag('a', {href:'#', - onclick:function () { - // a -> li -> ul -> div - var ul = this.parentNode.parentNode; - ul.removeChild(this.parentNode); - selectField(ul.parentNode); - return false - } - }, 'remove'), ')' - )); - } - var fetch = function(c) { - var v = c.fil_val; - var ul = byName(c, 'ul')[0]; - var txt = byName(c, 'input')[0]; - if(v == null) - return; - if(!v[0]) { - setText(ul, ''); - txt.disabled = false; - txt.value = ''; - return; - } - if(!visible) - setText(ul, ''); - var q = []; - for(var i=0; i<v.length; i++) { - q.push('id='+v[i]); - if(!visible) - addel(ul, v[i]); - } - txt.value = 'Loading...'; - txt.disabled = true; - if(visible) - ajax(src+';r=50;'+q.join(';'), function (hr) { - var items = hr.responseXML.getElementsByTagName('item'); - setText(ul, ''); - for(var i=0; i<items.length; i++) - addel(ul, items[i].getAttribute('id'), items[i]); - txt.value = ''; - txt.disabled = false; - c.fil_val = null; - }, 1); - }; - var input = tag('input', {type:'text', 'class':'text', style:'width:300px', onfocus:selectField}); - var list = tag('ul', null); - dsInit(input, src+';q=', fmtds, - function(item, obj) { - if(byName(obj.parentNode, 'li').length >= 50) - alert('Too many items selected'); - else { - obj.parentNode.fil_val = null; - addel(byName(obj.parentNode, 'ul')[0], item.getAttribute('id'), item); - selectField(obj); - } - return ''; - }, - function(o) { selectField(o) } - ); - - return [ - fieldcode, fieldname, tag('div', list, input), - function(c) { - var v = []; var l = byName(c, 'li'); - for(var i=0; i<l.length; i++) - v.push(l[i].fil_id); - return v; - }, - function(c,v) { c.fil_val = v; fetch(c) }, - function(c) { visible = true; fetch(c); } - ]; -} - -function filFTagInput(code, name) { - return filFDList(code, name, '/xml/tags.xml?searchable=1', - function(item, tr) { - tr.appendChild(tag('td', shorten(item.firstChild.nodeValue, 40))); - }, - function(id, item) { - return tag('span', tag('a', {href:'/g'+id}, item.firstChild.nodeValue||'g'+id)) - } - ) -} - -function filFTraitInput(code, name) { - return filFDList(code, name, '/xml/traits.xml?searchable=1', - function(item, tr) { - var g = item.getAttribute('groupname'); - tr.appendChild(tag('td', - g ? tag('b', {'class':'grayedout'}, g+' / ') : null, - shorten(item.firstChild.nodeValue, 40) - )); - }, - function(id, item) { - var g = item.getAttribute('groupname'); - return tag('span', - g ? tag('b', {'class':'grayedout'}, g+' / ') : null, - tag('a', {href:'/i'+id}, item.firstChild.nodeValue||'i'+id) - ) - } - ) -} - -function filFProducerInput(code, name) { - return filFDList(code, name, '/xml/producers.xml?', - function(item, tr) { - tr.appendChild(tag('td', shorten(item.firstChild.nodeValue, 40))); - }, - function(id, item) { - return tag('span', tag('a', {href:'/p'+id}, item.firstChild.nodeValue||'p'+id)) - } - ) -} - -function filFStaffInput(code, name) { - return filFDList(code, name, '/xml/staff.xml?staffid=1', - function(item, tr) { - tr.appendChild(tag('td', shorten(item.firstChild.nodeValue, 40))); - }, - function(id, item) { - return tag('span', tag('a', {href:'/s'+id}, item.firstChild.nodeValue||'s'+id)) - } - ) -} - - -function filChars() { - var ontraitpage = location.pathname.indexOf('/i') == 0; - - var cup_ser = function(v) { return VARS.cup_size[parseInt(v)] }; - var cup_deser = function(v) { - for(var i=0; i<VARS.cup_size.length; i++) - if(VARS.cup_size[i] == v) - return i; - return 0; - }; - - return [ - 'Character filters', - [ 'General', - filFSelect('gender', 'Gender', 4, VARS.genders), - filFSelect('bloodt', 'Blood type', 5, VARS.blood_types), - '', - filFSlider('bust_min', 'Bust min', 20, 120, 40, 'cm'), - filFSlider('bust_max', 'Bust max', 20, 120, 100, 'cm'), - filFSlider('waist_min', 'Waist min', 20, 120, 40, 'cm'), - filFSlider('waist_max', 'Waist max', 20, 120, 100, 'cm'), - filFSlider('hip_min', 'Hips min', 20, 120, 40, 'cm'), - filFSlider('hip_max', 'Hips max', 20, 120, 100, 'cm'), - '', - filFSlider('height_min', 'Height min', 0, 300, 60, 'cm'), - filFSlider('height_max', 'Height max', 0, 300, 240, 'cm'), - filFSlider('weight_min', 'Weight min', 0, 400, 80, 'kg'), - filFSlider('weight_max', 'Weight max', 0, 400, 320, 'kg'), - filFSlider('cup_min', 'Cup size min', 'AAA', 'Z', 'AAA', '', cup_ser, cup_deser), - filFSlider('cup_max', 'Cup size max', 'AAA', 'Z', 'E', '', cup_ser, cup_deser) - ], - ontraitpage ? [ 'Traits', - [ '', ' ', tag('Additional trait filters are not available on this page. Use the character browser instead (available from the main menu -> characters).') ], - ] : [ 'Traits', - [ '', ' ', tag('Boolean and, selecting more gives less results') ], - filFTraitInput('trait_inc', 'Traits to include'), - filFTraitInput('trait_exc', 'Traits to exclude'), - filFOptions('tagspoil', ' ', [[0, 'Hide spoilers'],[1, 'Show minor spoilers'],[2, 'Spoil me!']]), - ], - [ 'Roles', filFSelect('role', 'Roles', 4, VARS.char_roles) ], - [ 'Seiyuu', - [ '', ' ', tag('Boolean or, selecting more gives more results') ], - filFStaffInput('va_inc', 'Seiyuu to include'), - filFStaffInput('va_exc', 'Seiyuu to exclude') - ], - ]; -} - -function filReleases() { - var plat = VARS.platforms; - plat.splice(0, 0, [ 'unk', 'Unknown' ]); - var med = VARS.media; - med.splice(0, 0, [ 'unk', 'Unknown' ]); - return [ - 'Release filters', - [ 'General', - filFOptions('type', 'Release type', VARS.release_types), - filFOptions('patch', 'Patch status', [ [1, 'Patch'], [0, 'Standalone'] ]), - filFOptions('freeware', 'Freeware', [ [1, 'Only freeware'], [0, 'Only non-free releases'] ]), - filFOptions('doujin', 'Doujin', [ [1, 'Only doujin releases'], [0, 'Only commercial releases'] ]), - filFOptions('uncensored','Censoring', [ [1, 'Only uncensored releases'], [0, 'Censored or non-erotic releases'] ]), - [ 'date_after', 'Released after', dateLoad(null, selectField), function (c) { return [c.date_val] }, function(o,v) { o.dateSet(v) } ], - [ 'date_before', 'Released before', dateLoad(null, selectField), function (c) { return [c.date_val] }, function(o,v) { o.dateSet(v) } ], - filFOptions('released', 'Release date', [ [1, 'Past (already released)'], [0, 'Future (to be released)'] ]) - ], - [ 'Age rating', filFSelect('minage', 'Age rating', 15, VARS.age_ratings) ], - [ 'Language', filFSelect('lang', 'Language', 20, VARS.languages) ], - byId('rfilselect') ? null : - [ 'Original language', filFSelect('olang', 'Original language', 20, VARS.languages) ], - [ 'Screen resolution', filFSelect('resolution', 'Screen resolution', 15, VARS.resolutions) ], - [ 'Platform', filFSelect('plat', 'Platform', 20, plat) ], - [ 'Producer', - [ '', ' ', tag('Boolean or, selecting more gives more results') ], - filFProducerInput('prod_inc', 'Producers to include'), - filFProducerInput('prod_exc', 'Producers to exclude') - ], - [ 'Misc', - filFSelect('med', 'Medium', 10, med), - filFSelect('voiced', 'Voiced', 5, VARS.voiced), - filFSelect('ani_story', 'Story animation', 5, VARS.animated), - filFSelect('ani_ero', 'Ero animation', 5, VARS.animated), - filFEngine('engine', 'Engine') - ] - ]; -} - -function filVN() { - var ontagpage = location.pathname.indexOf('/v/') < 0; - - return [ - 'Visual Novel Filters', - [ 'General', - filFSelect( 'length', 'Length', 6, VARS.vn_lengths), - filFOptions('hasani', 'Anime', [[1, 'Has anime'], [0, 'Does not have anime']]), - filFOptions('hasshot','Screenshots', [[1, 'Has screenshot'],[0, 'Does not have a screenshot']]), - [ 'date_after', 'Released after', dateLoad(null, selectField), function (c) { return [c.date_val] }, function(o,v) { o.dateSet(v) } ], - [ 'date_before', 'Released before', dateLoad(null, selectField), function (c) { return [c.date_val] }, function(o,v) { o.dateSet(v) } ], - filFOptions('released', 'Release date', [ [1, 'Past (already released)'], [0, 'Future (to be released)'] ]) - ], - ontagpage ? [ 'Tags', - [ '', ' ', tag('Additional tag filters are not available on this page. Use the visual novel browser instead (available from the main menu -> visual novels).') ], - ] : [ 'Tags', - [ '', ' ', tag('Boolean and, selecting more gives less results') ], - [ '', ' ', byId('pref_code') ? tag('These filters are ignored on tag pages (when set as default).') : null ], - filFTagInput('tag_inc', 'Tags to include'), - filFTagInput('tag_exc', 'Tags to exclude'), - filFOptions('tagspoil', ' ', [[0, 'Hide spoilers'],[1, 'Show minor spoilers'],[2, 'Spoil me!']]) - ], - [ 'Language', filFSelect('lang', 'Language', 20, VARS.languages) ], - [ 'Original language', filFSelect('olang','Original language', 20, VARS.languages) ], - [ 'Platform', filFSelect('plat', 'Platform', 20, VARS.platforms) ], - [ 'Staff', - [ '', ' ', tag('Boolean or, selecting more gives more results') ], - filFStaffInput('staff_inc', 'Staff to include'), - filFStaffInput('staff_exc', 'Staff to exclude') - ], - !byId('pref_code') ? null : [ - 'My lists', - filFOptions('ul_notblack', 'Blacklist', [[1, 'Exclude VNs on my blacklist']]), - filFOptions('ul_onwish', 'Wishlist', [[0, 'Not on my wishlist'],[1, 'On my wishlist']]), - filFOptions('ul_voted', 'Voted', [[0, 'Not voted on'], [1, 'Voted on' ]]), - filFOptions('ul_onlist', 'VN list', [[0, 'Not on my VN list'],[1, 'On my VN list']]) - ], - ]; -} - -function filStaff() { - var gend = VARS.genders.slice(0, 3); - - // Insert seiyuu into the list of roles, before the "staff" role. - var roles = VARS.credit_type; - roles.splice(-1, 0, ['seiyuu', 'Voice actor']); - - return [ - 'Staff filters', - [ 'General', - filFOptions('truename', 'Names', [[1, 'Primary names only'],[0, 'Include aliases']]), - filFSelect('role', 'Roles', roles.length, roles), - '', - filFSelect('gender', 'Gender', gend.length, gend), - ], - [ 'Language', filFSelect('lang', 'Language', 20, VARS.languages) ], - ]; -} - -if(byId('filselect')) - filLoad(byId('filselect'), byId('fil')); -if(byId('rfilselect')) - filLoad(byId('rfilselect'), byId('rfil')); -if(byId('cfilselect')) - filLoad(byId('cfilselect'), byId('cfil')); diff --git a/data/js/lib.js b/data/js/lib.js deleted file mode 100644 index a4921d3e..00000000 --- a/data/js/lib.js +++ /dev/null @@ -1,179 +0,0 @@ -window.expanded_icon = '▾', -window.collapsed_icon = '▸'; - - -var ajax_req; -window.ajax = function(url, func, async, body) { - if(!async && ajax_req) - ajax_req.abort(); - var req = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'); - if(!async) - ajax_req = req; - req.onreadystatechange = function() { - if(!req || req.readyState != 4 || !req.responseText) - return; - if(req.status != 200) - return alert('Whoops, error! :('); - func(req); - }; - if(!body) - url += (url.indexOf('?')>=0 ? ';' : '?')+(Math.floor(Math.random()*999)+1); - req.open(body ? 'POST' : 'GET', url, true); - req.send(body); - return req; -}; - - -window.setCookie = function(n,v) { - var date = new Date(); - date.setTime(date.getTime()+(365*24*60*60*1000)); - document.cookie = VARS.cookie_prefix+n+'='+v+'; expires='+date.toGMTString()+'; path=/'; -}; - -window.getCookie = function(n) { - var l = document.cookie.split(';'); - n = VARS.cookie_prefix+n; - for(var i=0; i<l.length; i++) { - var c = l[i]; - while(c.charAt(0) == ' ') - c = c.substring(1,c.length); - if(c.indexOf(n+'=') == 0) - return c.substring(n.length+1,c.length); - } - return null; -}; - - -window.byId = function(n) { - return document.getElementById(n) -}; - -window.byName = function(){ - var d = arguments.length > 1 ? arguments[0] : document; - var n = arguments.length > 1 ? arguments[1] : arguments[0]; - return d.getElementsByTagName(n); -}; - -window.byClass = function() { // [class], [parent, class], [tagname, class], [parent, tagname, class] - var par = typeof arguments[0] == 'object' ? arguments[0] : document; - var t = arguments.length == 2 && typeof arguments[0] == 'string' ? arguments[0] : arguments.length == 3 ? arguments[1] : '*'; - var c = arguments[arguments.length-1]; - var l = byName(par, t); - var ret = []; - for(var i=0; i<l.length; i++) - if(hasClass(l[i], c)) - ret[ret.length] = l[i]; - return ret; -}; - - -/* wrapper around DOM element creation - * tag('string') -> createTextNode - * tag('tagname', tag(), 'string', ..) -> createElement(), appendChild(), .. - * tag('tagname', { class: 'meh', title: 'Title' }) -> createElement(), setAttribute().. - * tag('tagname', { <attributes> }, <elements>) -> create, setattr, append */ -window.tag = function() { - if(arguments.length == 1) - return typeof arguments[0] != 'object' ? document.createTextNode(arguments[0]) : arguments[0]; - var el = typeof document.createElementNS != 'undefined' - ? document.createElementNS('http://www.w3.org/1999/xhtml', arguments[0]) - : document.createElement(arguments[0]); - for(var i=1; i<arguments.length; i++) { - if(arguments[i] == null) - continue; - if(typeof arguments[i] == 'object' && !arguments[i].appendChild) { - for(attr in arguments[i]) { - if(attr == 'style' || attr.match(/^data-/)) - el.setAttribute(attr, arguments[i][attr]); - else - el[ attr == 'class' ? 'className' : attr == 'for' ? 'htmlFor' : attr ] = arguments[i][attr]; - } - } else - el.appendChild(tag(arguments[i])); - } - return el; -}; - - -window.addBody = function(el) { - if(document.body.appendChild) - document.body.appendChild(el); - else if(document.documentElement.appendChild) - document.documentElement.appendChild(el); - else if(document.appendChild) - document.appendChild(el); -}; - -window.setContent = function() { - setText(arguments[0], ''); - for(var i=1; i<arguments.length; i++) - if(arguments[i] != null) - arguments[0].appendChild(tag(arguments[i])); -}; - -window.getText = function(obj) { - return obj.textContent || obj.innerText || ''; -}; - -window.setText = function(obj, txt) { - if(obj.textContent != null) - obj.textContent = txt; - else - obj.innerText = txt; -}; - - -window.listClass = function(obj) { - var n = obj.className; - if(!n) - return []; - return n.split(/ /); -}; - -window.hasClass = function(obj, c) { - var l = listClass(obj); - for(var i=0; i<l.length; i++) - if(l[i] == c) - return true; - return false; -}; - -window.setClass = function(obj, c, set) { - var l = listClass(obj); - var n = []; - if(set) { - n = l; - if(!hasClass(obj, c)) - n[n.length] = c; - } else { - for(var i=0; i<l.length; i++) - if(l[i] != c) - n[n.length] = l[i]; - } - obj.className = n.join(' '); -}; - -window.onSubmit = function(form, handler) { - var prev_handler = form.onsubmit; - form.onsubmit = function(e) { - if(prev_handler) - if(!prev_handler(e)) - return false; - return handler(e); - } -}; - - -window.shorten = function(v, l) { - return v.length > l ? v.substr(0, l-3)+'...' : v; -}; - - -window.fmtspoil = function(s) { - return ['neutral', 'no spoiler', 'minor spoiler', 'major spoiler'][s+1]; -} - - -window.jsonParse = function(s) { - return s ? JSON.parse(s) : ''; -}; diff --git a/data/js/main.js b/data/js/main.js deleted file mode 100644 index 8f3f6ca2..00000000 --- a/data/js/main.js +++ /dev/null @@ -1,39 +0,0 @@ -// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0-only -// @source: https://code.blicky.net/yorhel/vndb/src/branch/master/data/js -// SPDX-License-Identifier: AGPL-3.0-only - - -/* This is the main Javascript file. This file is processed by util/jsgen.pl to - * generate the final JS file(s) used by the site. */ - -// Variables from jsgen.pl -VARS = /*VARS*/; - -// Relic of the past -VARS.resolutions = [ - ["unknown","Unknown / console / handheld"], - ["nonstandard","Non-standard"], - ["4:3",["640x480","640x480"],["800x600","800x600"],["1024x768","1024x768"],["1280x960","1280x960"],["1600x1200","1600x1200"]], - ["widescreen",["640x400","640x400"],["960x600","960x600"],["960x640","960x640"],["1024x576","1024x576"],["1024x600","1024x600"],["1024x640","1024x640"],["1280x720","1280x720"],["1280x800","1280x800"],["1366x768","1366x768"],["1600x900","1600x900"],["1920x1080","1920x1080"]] -]; - -/* The include directives below automatically wrap the file contents inside an - * anonymous function, so each file has its own local namespace. Included files - * can't access variables or functions from other files, unless these variables - * are explicitely shared in DOM objects or (more commonly) the global 'window' - * object. - */ - -// Reusable library functions -//include lib.js - -// Reusable widgets -//include dropdown.js -//include dateselector.js -//include dropdownsearch.js - -// Page/functionality-specific widgets -//include filter.js -//include misc.js - -// @license-end diff --git a/data/js/misc.js b/data/js/misc.js deleted file mode 100644 index 9658bccf..00000000 --- a/data/js/misc.js +++ /dev/null @@ -1,25 +0,0 @@ -// search tabs -(function(){ - function click() { - var str = byId('q').value; - if(str.length > 1) { - this.href = this.href.split('?')[0]; - if(this.href.indexOf('/g') >= 0 || this.href.indexOf('/i') >= 0) - this.href += '/list'; - this.href += '?q=' + encodeURIComponent(str); - } - return true; - }; - if(byId('searchtabs')) { - var l = byName(byId('searchtabs'), 'a'); - for(var i=0; i<l.length; i++) - l[i].onclick = click; - } -})(); - - -// spam protection on all forms -setTimeout(function() { - for(var i=1; i<document.forms.length; i++) - document.forms[i].action = document.forms[i].action.replace(/\/nospam\?/,''); -}, 500); diff --git a/lib/VNDB/DB/Chars.pm b/lib/VNDB/DB/Chars.pm deleted file mode 100644 index 0b159452..00000000 --- a/lib/VNDB/DB/Chars.pm +++ /dev/null @@ -1,150 +0,0 @@ - -package VNDB::DB::Chars; - -use strict; -use warnings; -use Exporter 'import'; - -our @EXPORT = qw|dbCharFilters dbCharGet|; - - -# Character filters shared by dbCharGet and dbVNGet -sub dbCharFilters { - my($self, %o) = @_; - return ( - defined $o{gender} ? ( 'c.gender IN(!l)' => [ ref $o{gender} ? $o{gender} : [$o{gender}] ]) : (), - defined $o{bloodt} ? ( 'c.bloodt IN(!l)' => [ ref $o{bloodt} ? $o{bloodt} : [$o{bloodt}] ]) : (), - defined $o{bust_min} ? ( 'c.s_bust >= ?' => $o{bust_min} ) : (), - defined $o{bust_max} ? ( 'c.s_bust <= ? AND c.s_bust > 0' => $o{bust_max} ) : (), - defined $o{waist_min} ? ( 'c.s_waist >= ?' => $o{waist_min} ) : (), - defined $o{waist_max} ? ( 'c.s_waist <= ? AND c.s_waist > 0' => $o{waist_max} ) : (), - defined $o{hip_min} ? ( 'c.s_hip >= ?' => $o{hip_min} ) : (), - defined $o{hip_max} ? ( 'c.s_hip <= ? AND c.s_hip > 0' => $o{hip_max} ) : (), - defined $o{height_min} ? ( 'c.height >= ?' => $o{height_min} ) : (), - defined $o{height_max} ? ( 'c.height <= ? AND c.height > 0' => $o{height_max} ) : (), - defined $o{weight_min} ? ( 'c.weight >= ?' => $o{weight_min} ) : (), - defined $o{weight_max} ? ( 'c.weight <= ?' => $o{weight_max} ) : (), - defined $o{cup_min} ? ( 'c.cup_size >= ?' => $o{cup_min} ) : (), - defined $o{cup_max} ? ( 'c.cup_size <= ?' => $o{cup_max} ) : (), - $o{role} ? ( - 'EXISTS(SELECT 1 FROM chars_vns cvi WHERE cvi.id = c.id AND cvi.role IN(!l))', - [ ref $o{role} ? $o{role} : [$o{role}] ] ) : (), - $o{trait_inc} ? ( - 'c.id IN(SELECT cid FROM traits_chars WHERE tid IN(!l) AND spoil <= ? GROUP BY cid HAVING COUNT(tid) = ?)', - [ ref $o{trait_inc} ? $o{trait_inc} : [$o{trait_inc}], $o{tagspoil}, ref $o{trait_inc} ? $#{$o{trait_inc}}+1 : 1 ]) : (), - $o{trait_exc} ? ( - 'c.id NOT IN(SELECT cid FROM traits_chars WHERE tid IN(!l))' => [ ref $o{trait_exc} ? $o{trait_exc} : [$o{trait_exc}] ] ) : (), - $o{va_inc} ? ( 'c.id IN(SELECT ivs.cid FROM vn_seiyuu ivs JOIN staff_alias isa ON isa.aid = ivs.aid WHERE isa.id IN(!l))' => [ ref $o{va_inc} ? $o{va_inc} : [$o{va_inc}] ] ) : (), - $o{va_exc} ? ( 'c.id NOT IN(SELECT ivs.cid FROM vn_seiyuu ivs JOIN staff_alias isa ON isa.aid = ivs.aid WHERE isa.id IN(!l))' => [ ref $o{va_exc} ? $o{va_exc} : [$o{va_exc}] ] ) : (), - ) -} - - -# options: id instance tagspoil trait_inc trait_exc char what results page gender bloodt -# bust_min bust_max waist_min waist_max hip_min hip_max height_min height_max weight_min weight_max role -# what: extended traits vns changes -sub dbCharGet { - my $self = shift; - my %o = ( - page => 1, - results => 10, - what => '', - tagspoil => 0, - @_ - ); - - $o{search} =~ s/%//g if $o{search}; - - my %where = ( - !$o{id} ? ( 'c.hidden = FALSE' => 1 ) : (), - $o{id} ? ( 'c.id IN(!l)' => [ ref $o{id} ? $o{id} : [$o{id}] ] ) : (), - $o{notid} ? ( 'c.id <> ?' => $o{notid} ) : (), - $o{instance} ? ( 'c.main = ?' => $o{instance} ) : (), - $o{vid} ? ( 'c.id IN(SELECT id FROM chars_vns WHERE vid = ?)' => $o{vid} ) : (), - $o{search} ? ( - "(c.name ILIKE ? OR translate(c.original,' ','') ILIKE translate(?,' ','') OR c.alias ILIKE ?)", [ map '%'.$o{search}.'%', 1..3 ] ) : (), - $o{char} ? ( - 'LOWER(SUBSTR(c.name, 1, 1)) = ?' => $o{char} ) : (), - defined $o{char} && !$o{char} ? ( - '(ASCII(c.name) < 97 OR ASCII(c.name) > 122) AND (ASCII(c.name) < 65 OR ASCII(c.name) > 90)' => 1 ) : (), - $self->dbCharFilters(%o), - ); - - my @select = (qw|c.id c.name c.original c.gender|); - push @select, qw|c.hidden c.locked c.alias c.desc c.b_month c.b_day c.s_bust c.s_waist c.s_hip c.height c.weight c.bloodt c.cup_size c.age c.main c.main_spoil|, - 'coalesce(vndbid_num(c.image),0) AS image' if $o{what} =~ /extended/; - - my($r, $np) = $self->dbPage(\%o, q| - SELECT !s - FROM chars c - !W - ORDER BY c.name|, - join(', ', @select), \%where - ); - - return _enrich($self, $r, $np, 0, $o{what}, $o{vid}); -} - - -sub _enrich { - my($self, $r, $np, $rev, $what, $vid) = @_; - - if(@$r && $what =~ /vns|traits/) { - my($col, $hist, $colname) = $rev ? ('cid', '_hist', 'chid') : ('id', '', 'id'); - my %r = map { - $_->{traits} = []; - $_->{vns} = []; - ($_->{$col}, $_) - } @$r; - - if($what =~ /traits/) { - push @{$r{ delete $_->{xid} }{traits}}, $_ for (@{$self->dbAll(qq| - SELECT ct.$colname AS xid, ct.tid, ct.spoil, t.name, t.sexual, t."group", tg.name AS groupname - FROM chars_traits$hist ct - JOIN traits t ON t.id = ct.tid - JOIN traits tg ON tg.id = t."group" - WHERE ct.$colname IN(!l) - ORDER BY tg."order", t.name|, [ keys %r ] - )}); - } - - if($what =~ /vns(?:\((\d+)\))?/) { - push @{$r{ delete $_->{xid} }{vns}}, $_ for (@{$self->dbAll(" - SELECT cv.$colname AS xid, cv.vid, cv.rid, cv.spoil, cv.role, v.title AS vntitle, r.title AS rtitle - FROM chars_vns$hist cv - JOIN vn v ON cv.vid = v.id - LEFT JOIN releases r ON cv.rid = r.id - !W - ORDER BY v.c_released", - { "cv.$colname IN(!l)" => [[keys %r]], $1 ? ('cv.vid = ?', $1) : () } - )}); - } - } - - # Depends on the VN revision rather than char revision - if(@$r && $what =~ /seiyuu/) { - my %r = map { - $_->{seiyuu} = []; - ($_->{id}, $_) - } @$r; - - push @{$r{ delete $_->{cid} }{seiyuu}}, $_ for (@{$self->dbAll(q| - SELECT vs.cid, s.id AS sid, sa.name, sa.original, vs.note, v.id AS vid, v.title AS vntitle - FROM vn_seiyuu vs - JOIN staff_alias sa ON sa.aid = vs.aid - JOIN staff s ON s.id = sa.id - JOIN vn v ON v.id = vs.id - !W - ORDER BY v.c_released, sa.name|, { - 's.hidden = FALSE' => 1, - 'vs.cid IN(!l)' => [[ keys %r ]], - $vid ? ('v.id = ?' => $vid) : (), - } - )}); - } - - return wantarray ? ($r, $np) : $r; -} - - -1; diff --git a/lib/VNDB/DB/Producers.pm b/lib/VNDB/DB/Producers.pm deleted file mode 100644 index c9d4f95f..00000000 --- a/lib/VNDB/DB/Producers.pm +++ /dev/null @@ -1,108 +0,0 @@ - -package VNDB::DB::Producers; - -use strict; -use warnings; -use Exporter 'import'; - -our @EXPORT = qw|dbProducerGet dbProducerGetRev|; - - -# options: results, page, id, search, char, sort, inc_hidden -# what: extended relations -sub dbProducerGet { - my $self = shift; - my %o = ( - results => 10, - page => 1, - what => '', - @_ - ); - - $o{search} =~ s/%//g if $o{search}; - - my %where = ( - !$o{id} && !$o{inc_hidden} ? ( - 'p.hidden = FALSE' => 1 ) : (), - $o{id} ? ( - 'p.id IN(!l)' => [ ref $o{id} ? $o{id} : [$o{id}] ] ) : (), - $o{search} ? ( - '(p.name ILIKE ? OR p.original ILIKE ? OR p.alias ILIKE ?)', [ map '%'.$o{search}.'%', 1..3 ] ) : (), - $o{char} ? ( - 'LOWER(SUBSTR(p.name, 1, 1)) = ?' => $o{char} ) : (), - defined $o{char} && !$o{char} ? ( - '(ASCII(p.name) < 97 OR ASCII(p.name) > 122) AND (ASCII(p.name) < 65 OR ASCII(p.name) > 90)' => 1 ) : (), - ); - - my $select = 'p.id, p.type, p.name, p.original, p.lang'; - $select .= ', p.desc, p.alias, p.website, p.l_wp, p.l_wikidata, p.hidden, p.locked' if $o{what} =~ /extended/; - - my($order, @order) = ('p.name'); - if($o{sort} && $o{sort} eq 'search') { - $order = 'least(substr_score(p.name, ?), substr_score(p.original, ?)), p.name'; - @order = ($o{search}) x 2; - } - - my($r, $np) = $self->dbPage(\%o, qq| - SELECT !s - FROM producers p - !W - ORDER BY $order|, - $select, \%where, @order - ); - - return _enrich($self, $r, $np, 0, $o{what}); -} - - -# options: id, rev, what -# what: extended relations -sub dbProducerGetRev { - my $self = shift; - my %o = (what => '', @_); - - $o{rev} ||= $self->dbRow('SELECT MAX(rev) AS rev FROM changes WHERE type = \'p\' AND itemid = ?', $o{id})->{rev}; - - my $select = 'c.itemid AS id, p.type, p.name, p.original, p.lang'; - $select .= ', extract(\'epoch\' from c.added) as added, c.comments, c.rev, c.ihid, c.ilock, '.VNWeb::DB::sql_user(); - $select .= ', c.id AS cid, NOT EXISTS(SELECT 1 FROM changes c2 WHERE c2.type = c.type AND c2.itemid = c.itemid AND c2.rev = c.rev+1) AS lastrev'; - $select .= ', p.desc, p.alias, p.website, p.l_wp, p.l_wikidata, po.hidden, po.locked' if $o{what} =~ /extended/; - - my $r = $self->dbAll(q| - SELECT !s - FROM changes c - JOIN producers po ON po.id = c.itemid - JOIN producers_hist p ON p.chid = c.id - JOIN users u ON u.id = c.requester - WHERE c.type = 'p' AND c.itemid = ? AND c.rev = ?|, - $select, $o{id}, $o{rev} - ); - - return _enrich($self, $r, 0, 1, $o{what}); -} - - -sub _enrich { - my($self, $r, $np, $rev, $what) = @_; - - if(@$r && $what =~ /relations/) { - my($col, $hist, $colname) = $rev ? ('cid', '_hist', 'chid') : ('id', '', 'id'); - my %r = map { - $r->[$_]{relations} = []; - ($r->[$_]{$col}, $_) - } 0..$#$r; - - push @{$r->[$r{$_->{xid}}]{relations}}, $_ for(@{$self->dbAll(qq| - SELECT rel.$colname AS xid, rel.pid AS id, rel.relation, p.name, p.original - FROM producers_relations$hist rel - JOIN producers p ON rel.pid = p.id - WHERE rel.$colname IN(!l)|, - [ keys %r ] - )}); - } - - return wantarray ? ($r, $np) : $r; -} - -1; - diff --git a/lib/VNDB/DB/Releases.pm b/lib/VNDB/DB/Releases.pm deleted file mode 100644 index 2bfe8032..00000000 --- a/lib/VNDB/DB/Releases.pm +++ /dev/null @@ -1,224 +0,0 @@ - -package VNDB::DB::Releases; - -use strict; -use warnings; -use POSIX 'strftime'; -use Exporter 'import'; -use VNDB::Func 'gtintype'; - -our @EXPORT = qw|dbReleaseFilters dbReleaseGet dbReleaseGetRev dbReleaseEngines|; - - -# Release filters shared by dbReleaseGet and dbVNGet -sub dbReleaseFilters { - my($self, %o) = @_; - $o{plat} = [ $o{plat} ] if $o{plat} && !ref $o{plat}; - $o{med} = [ $o{med} ] if $o{med} && !ref $o{med}; - return ( - defined $o{patch} ? ( 'r.patch = ?' => $o{patch} == 1 ? 1 : 0) : (), - defined $o{freeware} ? ( 'r.freeware = ?' => $o{freeware} == 1 ? 1 : 0) : (), - defined $o{uncensored} ? ( 'r.uncensored = ?' => $o{uncensored} == 1 ? 1 : 0) : (), - defined $o{type} ? ( 'r.type = ?' => $o{type} ) : (), - defined $o{date_before} ? ( 'r.released <= ?' => $o{date_before} ) : (), - defined $o{date_after} ? ( 'r.released >= ?' => $o{date_after} ) : (), - defined $o{minage} ? ( 'r.minage IN(!l)' => [ ref $o{minage} ? $o{minage} : [$o{minage}] ] ) : (), - defined $o{doujin} ? ( 'NOT r.patch AND r.doujin = ?' => $o{doujin} == 1 ? 1 : 0) : (), - defined $o{resolution} ? ( 'NOT r.patch AND ARRAY[r.reso_x, r.reso_y] IN(!l)' => - [[ map $_ eq 'unknown' ? '{0,0}' : $_ eq 'nonstandard' ? '{0,1}' : '{'.(s/x/,/r).'}', - ref $o{resolution} ? $o{resolution}->@* : $o{resolution} ]] ) : (), - defined $o{voiced} ? ( 'NOT r.patch AND r.voiced IN(!l)' => [ ref $o{voiced} ? $o{voiced} : [$o{voiced}] ] ) : (), - defined $o{ani_story} ? ( 'NOT r.patch AND r.ani_story IN(!l)' => [ ref $o{ani_story} ? $o{ani_story} : [$o{ani_story}] ] ) : (), - defined $o{ani_ero} ? ( 'NOT r.patch AND r.ani_ero IN(!l)' => [ ref $o{ani_ero} ? $o{ani_ero} : [$o{ani_ero}] ] ) : (), - defined $o{engine} ? ( 'r.engine = ?' => $o{engine} ) : (), - defined $o{released} ? ( 'r.released !s ?' => [ $o{released} ? '<=' : '>', strftime('%Y%m%d', gmtime) ] ) : (), - $o{lang} ? ( - 'r.id IN(SELECT irl.id FROM releases_lang irl WHERE irl.lang IN(!l))' => [ ref $o{lang} ? $o{lang} : [ $o{lang} ] ] ) : (), - $o{olang} ? ( - 'r.id IN(SELECT irv.id FROM releases_vn irv JOIN vn v ON irv.vid = v.id WHERE v.c_olang && ARRAY[!l]::language[])' => [ ref $o{olang} ? $o{olang} : [ $o{olang} ] ] ) : (), - $o{plat} ? ('('.join(' OR ', - grep(/^unk$/, @{$o{plat}}) ? 'NOT EXISTS(SELECT 1 FROM releases_platforms irp WHERE irp.id = r.id)' : (), - grep(!/^unk$/, @{$o{plat}}) ? 'r.id IN(SELECT irp.id FROM releases_platforms irp WHERE irp.platform IN(!l))' : (), - ).')', [ [ grep !/^unk$/, @{$o{plat}} ] ]) : (), - $o{med} ? ('('.join(' OR ', - grep(/^unk$/, @{$o{med}}) ? 'NOT EXISTS(SELECT 1 FROM releases_media irm WHERE irm.id = r.id)' : (), - grep(!/^unk$/, @{$o{med}}) ? 'r.id IN(SELECT irm.id FROM releases_media irm WHERE irm.medium IN(!l))' : () - ).')', [ [ grep(!/^unk$/, @{$o{med}}) ] ]) : (), - $o{prod_inc} ? ('r.id IN(SELECT irp.id FROM releases_producers irp WHERE irp.pid IN(!l))' => [ ref $o{prod_inc} ? $o{prod_inc} : [$o{prod_inc}] ]) : (), - $o{prod_exc} ? ('r.id NOT IN(SELECT irp.id FROM releases_producers irp WHERE irp.pid IN(!l))' => [ ref $o{prod_exc} ? $o{prod_exc} : [$o{prod_exc}] ]) : (), - ); -} - - -# Options: id vid pid released page results what med sort reverse date_before date_after -# plat prod_inc prod_exc lang olang type minage search resolution freeware doujin voiced uncensored ani_story ani_ero hidden_only -# What: extended vn producers platforms media -# Sort: title released minage -sub dbReleaseGet { - my($self, %o) = @_; - $o{results} ||= 50; - $o{page} ||= 1; - $o{what} ||= ''; - - my @where = ( - !$o{id} && !$o{hidden_only} ? ( 'r.hidden = FALSE' => 0 ) : (), - $o{hidden_only} ? ('r.hidden = TRUE' => 1) : (), - $o{id} ? ( 'r.id = ?' => $o{id} ) : (), - $o{pid} ? ( 'rp.pid = ?' => $o{pid} ) : (), - $o{vid} ? ( 'r.id IN(SELECT id FROM releases_vn WHERE vid IN(!l))' => [ ref $o{vid} ? $o{vid} : [$o{vid}] ] ) : (), - $self->dbReleaseFilters(%o), - ); - - if($o{search}) { - for (split /[ -,._]/, $o{search}) { - s/%//g; - if(/^\d+$/ && gtintype($_)) { - push @where, 'r.gtin = ?', $_; - } elsif(length($_) > 0) { - $_ = "%$_%"; - push @where, '(r.title ILIKE ? OR r.original ILIKE ? OR r.catalog = ?)', - [ $_, $_, $_ ]; - } - } - } - - my @join = ( - $o{pid} ? 'JOIN releases_producers rp ON rp.id = r.id' : (), - ); - - my @select = ( - qw|r.id r.title r.original r.website r.released r.minage r.type r.patch|, - $o{what} =~ /extended/ ? qw| - r.notes r.catalog r.gtin r.reso_x r.reso_y r.voiced r.freeware r.doujin r.uncensored r.ani_story r.ani_ero r.engine r.hidden r.locked - | : (), - $o{pid} ? ('rp.developer', 'rp.publisher') : (), - $o{what} =~ /links/ ? qw| - r.gtin r.l_steam r.l_gog r.l_gyutto r.l_digiket r.l_melon r.l_getchu r.l_getchudl r.l_dmm r.l_itch r.l_jastusa r.l_egs r.l_erotrail r.l_mg r.l_denpa r.l_jlist r.l_dlsite r.l_dlsiteen r.l_melonjp r.l_toranoana r.l_gamejolt r.l_nutaku - | : () - ); - - my $order = sprintf { - title => 'r.title %s, r.released %1$s', - type => 'r.patch %s, r.type %1$s, r.released %1$s, r.title %1$s', - publication => 'r.doujin %s, r.freeware %1$s, r.patch %1$s, r.released %1$s, r.title %1$s', - resolution => 'r.reso_x %s, r.reso_y %1$s, r.patch %2$s, r.released %1$s, r.title %1$s', - voiced => 'r.voiced %s, r.patch %2$s, r.released %1$s, r.title %1$s', - ani_ero => 'r.ani_story %s, r.ani_ero %1$s, r.patch %2$s, r.released %1$s, r.title %1$s', - released => 'r.released %s, r.id %1$s', - minage => 'r.minage %s, r.released %1$s, r.title %1$s', - notes => 'r.notes %s, r.released %1$s, r.title %1$s', - }->{ $o{sort}||'released' }, $o{reverse} ? 'DESC' : 'ASC', !$o{reverse} ? 'DESC' : 'ASC'; - - my($r, $np) = $self->dbPage(\%o, q| - SELECT !s - FROM releases r - !s - !W - ORDER BY !s|, - join(', ', @select), join(' ', @join), \@where, $order - ); - - return _enrich($self, $r, $np, 0, $o{what}); -} - - -# options: id, rev, what -# what: extended vn producers platforms media -sub dbReleaseGetRev { - my $self = shift; - my %o = (what => '', @_); - - $o{rev} ||= $self->dbRow('SELECT MAX(rev) AS rev FROM changes WHERE type = \'r\' AND itemid = ?', $o{id})->{rev}; - - my $select = 'c.itemid AS id, r.title, r.original, r.website, r.released, r.minage, r.type, r.patch'; - $select .= ', r.notes, r.catalog, r.gtin, r.reso_x, r.reso_y, r.voiced, r.freeware, r.doujin, r.uncensored, r.ani_story, r.ani_ero, r.engine, ro.hidden, ro.locked' if $o{what} =~ /extended/; - $select .= ', extract(\'epoch\' from c.added) as added, c.comments, c.rev, c.ihid, c.ilock, '.VNWeb::DB::sql_user(); - $select .= ', c.id AS cid, NOT EXISTS(SELECT 1 FROM changes c2 WHERE c2.type = c.type AND c2.itemid = c.itemid AND c2.rev = c.rev+1) AS lastrev'; - $select .= ', r.gtin, r.l_steam, r.l_gog, r.l_gyutto, r.l_digiket, r.l_melon, r.l_getchu, r.l_getchudl, r.l_dmm, r.l_itch, r.l_jastusa, r.l_egs, r.l_erotrail, r.l_mg, r.l_denpa, r.l_jlist, r.l_dlsite, r.l_dlsiteen, r.l_melonjp, r.l_toranoana, r.l_gamejolt, r.l_nutaku' if $o{what} =~ /links/; - - my $r = $self->dbAll(q| - SELECT !s - FROM changes c - JOIN releases ro ON ro.id = c.itemid - JOIN releases_hist r ON r.chid = c.id - JOIN users u ON u.id = c.requester - WHERE c.type = 'r' AND c.itemid = ? AND c.rev = ?|, - $select, $o{id}, $o{rev} - ); - - return _enrich($self, $r, 0, 1, $o{what}); -} - - -sub _enrich { - my($self, $r, $np, $rev, $what) = @_; - - if(@$r) { - my($col, $hist, $colname) = $rev ? ('cid', '_hist', 'chid') : ('id', '', 'id'); - my %r = map { - $r->[$_]{producers} = []; - $r->[$_]{platforms} = []; - $r->[$_]{media} = []; - $r->[$_]{vn} = []; - $r->[$_]{languages} = []; - ($r->[$_]{$col}, $_) - } 0..$#$r; - - push(@{$r->[$r{$_->{xid}}]{languages}}, $_->{lang}) for (@{$self->dbAll(" - SELECT $colname AS xid, lang - FROM releases_lang$hist - WHERE $colname IN(!l)", - [ keys %r ] - )}); - - if($what =~ /vn/) { - push(@{$r->[$r{$_->{xid}}]{vn}}, $_) for (@{$self->dbAll(" - SELECT rv.$colname AS xid, v.id AS vid, v.title, v.original - FROM releases_vn$hist rv - JOIN vn v ON v.id = rv.vid - WHERE rv.$colname IN(!l) - ORDER BY v.title", - [ keys %r ] - )}); - } - - if($what =~ /producers/) { - push(@{$r->[$r{$_->{xid}}]{producers}}, $_) for (@{$self->dbAll(" - SELECT rp.$colname AS xid, rp.developer, rp.publisher, p.id, p.name, p.original, p.type - FROM releases_producers$hist rp - JOIN producers p ON rp.pid = p.id - WHERE rp.$colname IN(!l) - ORDER BY p.name", - [ keys %r ] - )}); - } - - if($what =~ /platforms/) { - push(@{$r->[$r{$_->{xid}}]{platforms}}, $_->{platform}) for (@{$self->dbAll(" - SELECT $colname AS xid, platform - FROM releases_platforms$hist - WHERE $colname IN(!l)", - [ keys %r ] - )}); - } - - if($what =~ /media/) { - push(@{$r->[$r{$_->{xid}}]{media}}, $_) for (@{$self->dbAll(" - SELECT $colname AS xid, medium, qty - FROM releases_media$hist - WHERE $colname IN(!l)", - [ keys %r ] - )}); - } - } - - return wantarray ? ($r, $np) : $r; -} - - -sub dbReleaseEngines { - shift->dbAll(q{SELECT engine, count(*) as cnt FROM releases WHERE engine <> '' GROUP BY engine ORDER BY COUNT(*) desc, engine}); -} - -1; - diff --git a/lib/VNDB/DB/Staff.pm b/lib/VNDB/DB/Staff.pm deleted file mode 100644 index 5a393dbb..00000000 --- a/lib/VNDB/DB/Staff.pm +++ /dev/null @@ -1,79 +0,0 @@ - -package VNDB::DB::Staff; - -use strict; -use warnings; -use Exporter 'import'; - -our @EXPORT = qw|dbStaffGet |; - -# options: results, page, id, aid, search, exact, truename, role, gender -sub dbStaffGet { - my $self = shift; - my %o = ( - results => 10, - page => 1, - what => '', - @_ - ); - my(@roles, $seiyuu); - if(defined $o{role}) { - if(ref $o{role}) { - $seiyuu = grep /^seiyuu$/, @{$o{role}}; - @roles = grep !/^seiyuu$/, @{$o{role}}; - } else { - $seiyuu = $o{role} eq 'seiyuu'; - @roles = $o{role} unless $seiyuu; - } - } - - $o{search} =~ s/%//g if $o{search}; - - my %where = ( - !$o{id} ? ( 's.hidden = FALSE' => 1 ) : (), - $o{id} ? ( ref $o{id} ? ('s.id IN(!l)' => [$o{id}]) : ('s.id = ?' => $o{id}) ) : (), - $o{aid} ? ( ref $o{aid} ? ('sa.aid IN(!l)' => [$o{aid}]) : ('sa.aid = ?' => $o{aid}) ) : (), - $o{id} || $o{truename} ? ( 's.aid = sa.aid' => 1 ) : (), - defined $o{gender} ? ( 's.gender IN(!l)' => [ ref $o{gender} ? $o{gender} : [$o{gender}] ]) : (), - defined $o{lang} ? ( 's.lang IN(!l)' => [ ref $o{lang} ? $o{lang} : [$o{lang}] ]) : (), - defined $o{role} ? ( - '('.join(' OR ', - @roles ? ( 'EXISTS(SELECT 1 FROM vn_staff vs JOIN vn v ON v.id = vs.id WHERE vs.aid = sa.aid AND vs.role IN(!l) AND NOT v.hidden)' ) : (), - $seiyuu ? ( 'EXISTS(SELECT 1 FROM vn_seiyuu vsy JOIN vn v ON v.id = vsy.id WHERE vsy.aid = sa.aid AND NOT v.hidden)' ) : () - ).')' => ( @roles ? [ \@roles ] : 1 ), - ) : (), - $o{exact} ? ( '(lower(sa.name) = lower(?) OR lower(sa.original) = lower(?))' => [ ($o{exact}) x 2 ] ) : (), - $o{search} ? - $o{search} =~ /[\x{3000}-\x{9fff}\x{ff00}-\x{ff9f}]/ ? - # match against 'original' column only if search string contains any - # japanese character. - # note: more precise regex would be /[\p{Hiragana}\p{Katakana}\p{Han}]/ - ( q|(sa.original LIKE ? OR translate(sa.original,' ','') LIKE ?)| => [ '%'.$o{search}.'%', ($o{search} =~ s/\s+//gr).'%' ] ) : - ( '(sa.name ILIKE ? OR sa.original ILIKE ?)' => [ map '%'.$o{search}.'%', 1..2 ] ) : (), - $o{char} ? ( 'LOWER(SUBSTR(sa.name, 1, 1)) = ?' => $o{char} ) : (), - defined $o{char} && !$o{char} ? - ( '(ASCII(sa.name) < 97 OR ASCII(sa.name) > 122) AND (ASCII(sa.name) < 65 OR ASCII(sa.name) > 90)' => 1 ) : (), - ); - - my $select = 's.id, sa.aid, sa.name, sa.original, s.gender, s.lang'; - - my($order, @order) = ('sa.name'); - if($o{sort} && $o{sort} eq 'search') { - $order = 'least(substr_score(sa.name, ?), substr_score(sa.original, ?)), sa.name'; - @order = ($o{search}) x 2; - } - - my($r, $np) = $self->dbPage(\%o, qq| - SELECT !s - FROM staff s - JOIN staff_alias sa ON sa.id = s.id - !W - ORDER BY $order|, - $select, \%where, @order - ); - - return wantarray ? ($r, $np) : $r; -} - - -1; diff --git a/lib/VNDB/DB/Tags.pm b/lib/VNDB/DB/Tags.pm deleted file mode 100644 index 1104bad8..00000000 --- a/lib/VNDB/DB/Tags.pm +++ /dev/null @@ -1,162 +0,0 @@ - -package VNDB::DB::Tags; - -use strict; -use warnings; -use Exporter 'import'; - -our @EXPORT = qw|dbTagGet dbTTTree dbTagStats|; - - -# %options->{ id noid name search state searchable applicable page results what sort reverse } -# what: parents childs(n) aliases addedby -# sort: id name added items search -sub dbTagGet { - my $self = shift; - my %o = ( - page => 1, - results => 10, - what => '', - @_ - ); - - $o{search} =~ s/%//g if $o{search}; - - my %where = ( - $o{id} ? ( - 't.id IN(!l)' => [ ref $o{id} ? $o{id} : [$o{id}] ] ) : (), - $o{noid} ? ( - 't.id <> ?' => $o{noid} ) : (), - $o{name} ? ( - 't.id = (SELECT id FROM tags LEFT JOIN tags_aliases ON id = tag WHERE lower(name) = ? OR lower(alias) = ? LIMIT 1)' => [ lc $o{name}, lc $o{name} ]) : (), - defined $o{state} && $o{state} != -1 ? ( - 't.state = ?' => $o{state} ) : (), - !defined $o{state} && !$o{id} && !$o{name} ? ( - 't.state <> 1' => 1 ) : (), - $o{search} ? ( - 't.id IN (SELECT id FROM tags LEFT JOIN tags_aliases ON id = tag WHERE name ILIKE ? OR alias ILIKE ?)' => [ "%$o{search}%", "%$o{search}%" ] ) : (), - defined $o{searchable} ? ('t.searchable = ?' => $o{searchable}?1:0 ) : (), - defined $o{applicable} ? ('t.applicable = ?' => $o{applicable}?1:0 ) : (), - ); - my @select = ( - qw|t.id t.searchable t.applicable t.name t.description t.state t.cat t.c_items t.defaultspoil|, - q|extract('epoch' from t.added) as added|, - $o{what} =~ /addedby/ ? (VNWeb::DB::sql_user()) : (), - ); - my @join = $o{what} =~ /addedby/ ? 'JOIN users u ON u.id = t.addedby' : (); - - my $order = sprintf { - id => 't.id %s', - name => 't.name %s', - added => 't.added %s', - items => 't.c_items %s', - search=> 'substr_score(t.name, ?) ASC, t.name %s', # Assigning a matching score for aliases is also possible, but more involved - }->{ $o{sort}||'id' }, $o{reverse} ? 'DESC' : 'ASC'; - my @order = $o{sort} && $o{sort} eq 'search' ? ($o{search}) : (); - - - my($r, $np) = $self->dbPage(\%o, qq| - SELECT !s - FROM tags t - !s - !W - ORDER BY $order|, - join(', ', @select), join(' ', @join), \%where, @order - ); - - if(@$r && $o{what} =~ /aliases/) { - my %r = map { - $_->{aliases} = []; - ($_->{id}, $_->{aliases}) - } @$r; - - push @{$r{$_->{tag}}}, $_->{alias} for (@{$self->dbAll(q| - SELECT tag, alias FROM tags_aliases WHERE tag IN(!l)|, [ keys %r ] - )}); - } - - if($o{what} =~ /parents\((\d+)\)/) { - $_->{parents} = $self->dbTTTree(tag => $_->{id}, $1, 1) for(@$r); - } - - if($o{what} =~ /childs\((\d+)\)/) { - $_->{childs} = $self->dbTTTree(tag => $_->{id}, $1) for(@$r); - } - - return wantarray ? ($r, $np) : $r; -} - - -# Walks the tag/trait tree -# type = tag | trait -# id = tag to start with, or 0 to start with top-level tags -# lvl = max. recursion level -# back = false for parent->child, true for child->parent -# Returns: [ { id, name, c_items, sub => [ { id, name, c_items, sub => [..] }, .. ] }, .. ] -sub dbTTTree { - my($self, $type, $id, $lvl, $back) = @_; - $lvl ||= 15; - my $xtra = $type eq 'trait' ? ', "order"' : ''; - my $xtra2 = $type eq 'trait' ? ', t."order"' : ''; - my $r = $self->dbAll(qq| - WITH RECURSIVE thetree(lvl, id, parent, name, c_items) AS ( - SELECT ?::integer, id, 0, name, c_items$xtra - FROM ${type}s - !W - UNION ALL - SELECT tt.lvl-1, t.id, tt.id, t.name, t.c_items$xtra2 - FROM thetree tt - JOIN ${type}s_parents tp ON !s - JOIN ${type}s t ON !s - WHERE tt.lvl > 0 - AND t.state = 2 - ) SELECT DISTINCT id, parent, name, c_items$xtra FROM thetree ORDER BY name|, $lvl, - $id ? {'id = ?' => $id} : {"NOT EXISTS(SELECT 1 FROM ${type}s_parents WHERE $type = id)" => 1, 'state = 2' => 1}, - !$back ? ('tp.parent = tt.id', "t.id = tp.$type") : ("tp.$type = tt.id", 't.id = tp.parent') - ); - - my %pars; # parent-id -> [ child-object, .. ] - push @{$pars{$_->{parent}}}, $_ for(@$r); - $_->{'sub'} = $pars{$_->{id}} || [] for(@$r); - my @r = grep !delete($_->{parent}), @$r; - return $id ? $r[0]{'sub'} : \@r; -} - - -# Fetch all tags related to a VN -# Argument: %options->{ vid minrating state results what page sort reverse } -# sort: name, rating -sub dbTagStats { - my($self, %o) = @_; - $o{results} ||= 10; - $o{page} ||= 1; - - my $rating = 'avg(CASE WHEN tv.ignore THEN NULL ELSE tv.vote END)'; - my $order = sprintf { - name => 't.name %s', - rating => "$rating %s", - }->{ $o{sort}||'name' }, $o{reverse} ? 'DESC' : 'ASC'; - - my %where = ( - 'tv.vid = ?' => $o{vid}, - defined $o{state} ? ('t.state = ?', $o{state}) : (), - ); - - my($r, $np) = $self->dbPage(\%o, qq| - SELECT t.id, t.name, t.cat, count(*) as cnt, $rating as rating, - COALESCE(avg(CASE WHEN tv.ignore THEN NULL ELSE tv.spoiler END), t.defaultspoil) as spoiler, - bool_or(tv.ignore) AS overruled - FROM tags t - JOIN tags_vn tv ON tv.tag = t.id - !W - GROUP BY t.id, t.name, t.cat - !s - ORDER BY !s|, - \%where, defined $o{minrating} ? "HAVING $rating > $o{minrating}" : '', $order - ); - - return wantarray ? ($r, $np) : $r; -} - -1; - diff --git a/lib/VNDB/DB/Traits.pm b/lib/VNDB/DB/Traits.pm deleted file mode 100644 index ac0e81b4..00000000 --- a/lib/VNDB/DB/Traits.pm +++ /dev/null @@ -1,86 +0,0 @@ - -package VNDB::DB::Traits; - -# This module is for a large part a copy of VNDB::DB::Tags. I could have chosen -# to modify that module to work for both traits and tags but that would have -# complicated the code, so I chose to maintain two versions with similar -# functionality instead. - -use strict; -use warnings; -use Exporter 'import'; - -our @EXPORT = qw|dbTraitGet|; - - -# Options: id noid search name state searchable applicable what results page sort reverse -# what: parents childs(n) addedby -# sort: id name name added items search -sub dbTraitGet { - my $self = shift; - my %o = ( - page => 1, - results => 10, - what => '', - @_, - ); - - $o{search} =~ s/%//g if $o{search}; - - my %where = ( - $o{id} ? ( 't.id IN(!l)' => [ ref($o{id}) ? $o{id} : [$o{id}] ]) : (), - $o{group} ? ( 't.group = ?' => $o{group} ) : (), - $o{noid} ? ( 't.id <> ?' => $o{noid} ) : (), - defined $o{state} && $o{state} != -1 ? ( - 't.state = ?' => $o{state} ) : (), - !defined $o{state} && !$o{id} && !$o{name} ? ( - 't.state = 2' => 1 ) : (), - $o{search} ? ( - '(t.name ILIKE ? OR t.alias ILIKE ?)' => [ "%$o{search}%", "%$o{search}%" ] ) : (), - $o{name} ? ( # TODO: This is terribly ugly, use an aliases table. - q{(LOWER(t.name) = LOWER(?) OR t.alias ~ ('(!sin)^'||?||'$'))} => [ $o{name}, '?', quotemeta $o{name} ] ) : (), - defined $o{applicable} ? ('t.applicable = ?' => $o{applicable}?1:0 ) : (), - defined $o{searchable} ? ('t.searchable = ?' => $o{searchable}?1:0 ) : (), - ); - - my @select = ( - qw|t.id t.searchable t.applicable t.name t.description t.state t.alias t."group" t."order" t.sexual t.c_items t.defaultspoil|, - 'tg.name AS groupname', 'tg."order" AS grouporder', q|extract('epoch' from t.added) as added|, - $o{what} =~ /addedby/ ? (VNWeb::DB::sql_user()) : (), - ); - my @join = $o{what} =~ /addedby/ ? 'JOIN users u ON u.id = t.addedby' : (); - push @join, 'LEFT JOIN traits tg ON tg.id = t."group"'; - - my $order = sprintf { - id => 't.id %s', - name => 't.name %s', - group => 'tg."order" %s, t.name %1$s', - added => 't.added %s', - items => 't.c_items %s', - search=> 'substr_score(t.name, ?) ASC, t.name %s', # Can't score aliases at the moment - }->{ $o{sort}||'id' }, $o{reverse} ? 'DESC' : 'ASC'; - my @order = $o{sort} && $o{sort} eq 'search' ? ($o{search}) : (); - - my($r, $np) = $self->dbPage(\%o, qq| - SELECT !s - FROM traits t - !s - !W - ORDER BY $order|, - join(', ', @select), join(' ', @join), \%where, @order, - ); - - if($o{what} =~ /parents\((\d+)\)/) { - $_->{parents} = $self->dbTTTree(trait => $_->{id}, $1, 1) for(@$r); - } - - if($o{what} =~ /childs\((\d+)\)/) { - $_->{childs} = $self->dbTTTree(trait => $_->{id}, $1) for(@$r); - } - - return wantarray ? ($r, $np) : $r; -} - - -1; - diff --git a/lib/VNDB/DB/Users.pm b/lib/VNDB/DB/Users.pm deleted file mode 100644 index 85654180..00000000 --- a/lib/VNDB/DB/Users.pm +++ /dev/null @@ -1,49 +0,0 @@ - -package VNDB::DB::Users; - -use strict; -use warnings; -use Exporter 'import'; - -our @EXPORT = qw| - dbUserGet -|; - - -# %options->{ uid results page what } -# sort: username registered votes changes tags -sub dbUserGet { - my $s = shift; - my %o = ( - page => 1, - results => 10, - what => '', - @_ - ); - - my %where = ( - $o{uid} && !ref($o{uid}) ? ( - 'id = ?' => $o{uid} ) : (), - $o{uid} && ref($o{uid}) ? ( - 'id IN(!l)' => [ $o{uid} ]) : (), - ); - - my @select = ( - qw|id username c_votes c_changes c_tags|, - VNWeb::DB::sql_user(), # XXX: This duplicates id and username, but updating all the code isn't going to be easy - q|extract('epoch' from registered) as registered|, - ); - - my($r, $np) = $s->dbPage(\%o, q| - SELECT !s - FROM users u - !W - ORDER BY id DESC|, - join(', ', @select), \%where - ); - - return wantarray ? ($r, $np) : $r; -} - -1; - diff --git a/lib/VNDB/DB/VN.pm b/lib/VNDB/DB/VN.pm deleted file mode 100644 index 668b7fec..00000000 --- a/lib/VNDB/DB/VN.pm +++ /dev/null @@ -1,257 +0,0 @@ - -package VNDB::DB::VN; - -use strict; -use warnings; -use v5.10; -use TUWF 'sqlprint'; -use POSIX 'strftime'; -use Exporter 'import'; -use VNDB::Func 'normalize_query', 'gtintype'; - -our @EXPORT = qw|dbVNGet dbVNGetRev|; - - -# Options: id, char, search, gtin, length, lang, olang, plat, tag_inc, tag_exc, tagspoil, -# hasani, hasshot, ul_notblack, ul_onwish, results, page, what, sort, -# reverse, inc_hidden, date_before, date_after, released, release, character -# What: extended anime staff seiyuu relations rating ranking vnlist -# Note: vnlist is ignored (no db search) unless a user is logged in -# Sort: id rel pop rating title tagscore rand -sub dbVNGet { - my($self, %o) = @_; - $o{results} ||= 10; - $o{page} ||= 1; - $o{what} ||= ''; - $o{sort} ||= 'title'; - $o{tagspoil} //= 2; - - # user input that is literally added to the query should be checked... - die "Invalid input for tagspoil or tag_inc at dbVNGet()\n" if - grep !defined($_) || $_!~/^\d+$/, $o{tagspoil}, - !$o{tag_inc} ? () : (ref($o{tag_inc}) ? @{$o{tag_inc}} : $o{tag_inc}); - - my $uid = $self->authInfo->{id}; - - $o{gtin} = delete $o{search} if $o{search} && $o{search} =~ /^\d+$/ && gtintype(local $_ = $o{search}); - - my @where = ( - $o{id} ? ( - 'v.id IN(!l)' => [ ref $o{id} ? $o{id} : [$o{id}] ] ) : (), - $o{char} ? ( - 'LOWER(SUBSTR(v.title, 1, 1)) = ?' => $o{char} ) : (), - defined $o{char} && !$o{char} ? ( - '(ASCII(v.title) < 97 OR ASCII(v.title) > 122) AND (ASCII(v.title) < 65 OR ASCII(v.title) > 90)' => 1 ) : (), - defined $o{length} ? ( - 'v.length IN(!l)' => [ ref $o{length} ? $o{length} : [$o{length}] ]) : (), - $o{lang} ? ( - 'v.c_languages && ARRAY[!l]::language[]' => [ ref $o{lang} ? $o{lang} : [$o{lang}] ]) : (), - $o{olang} ? ( - 'v.c_olang && ARRAY[!l]::language[]' => [ ref $o{olang} ? $o{olang} : [$o{olang}] ]) : (), - $o{plat} ? ( - 'v.c_platforms && ARRAY[!l]::platform[]' => [ ref $o{plat} ? $o{plat} : [$o{plat}] ]) : (), - defined $o{hasani} ? ( - '!sEXISTS(SELECT 1 FROM vn_anime va WHERE va.id = v.id)' => [ $o{hasani} ? '' : 'NOT ' ]) : (), - defined $o{hasshot} ? ( - '!sEXISTS(SELECT 1 FROM vn_screenshots vs WHERE vs.id = v.id)' => [ $o{hasshot} ? '' : 'NOT ' ]) : (), - $o{tag_inc} ? ( - 'v.id IN(SELECT vid FROM tags_vn_inherit WHERE tag IN(!l) AND spoiler <= ? GROUP BY vid HAVING COUNT(tag) = ?)', - [ ref $o{tag_inc} ? $o{tag_inc} : [$o{tag_inc}], $o{tagspoil}, ref $o{tag_inc} ? $#{$o{tag_inc}}+1 : 1 ]) : (), - $o{tag_exc} ? ( - 'v.id NOT IN(SELECT vid FROM tags_vn_inherit WHERE tag IN(!l))' => [ ref $o{tag_exc} ? $o{tag_exc} : [$o{tag_exc}] ] ) : (), - $o{search} ? ( - map +('v.c_search like ?', "%$_%"), normalize_query($o{search})) : (), - $o{gtin} ? ( - 'v.id IN(SELECT irv.vid FROM releases_vn irv JOIN releases ir ON ir.id = irv.id WHERE ir.gtin = ?)' => $o{gtin}) : (), - $o{staff_inc} ? ( 'v.id IN(SELECT ivs.id FROM vn_staff ivs JOIN staff_alias isa ON isa.aid = ivs.aid WHERE isa.id IN(!l))' => [ ref $o{staff_inc} ? $o{staff_inc} : [$o{staff_inc}] ] ) : (), - $o{staff_exc} ? ( 'v.id NOT IN(SELECT ivs.id FROM vn_staff ivs JOIN staff_alias isa ON isa.aid = ivs.aid WHERE isa.id IN(!l))' => [ ref $o{staff_exc} ? $o{staff_exc} : [$o{staff_exc}] ] ) : (), - $uid && $o{ul_notblack} ? ( - 'v.id NOT IN(SELECT vid FROM ulist_vns_labels WHERE uid = ? AND lbl = 6)' => $uid ) : (), - $uid && defined $o{ul_onwish} ? ( - 'v.id !s IN(SELECT vid FROM ulist_vns_labels WHERE uid = ? AND lbl = 5)' => [ $o{ul_onwish} ? '' : 'NOT', $uid ] ) : (), - $uid && defined $o{ul_voted} ? ( - 'v.id !s IN(SELECT vid FROM ulist_vns_labels WHERE uid = ? AND lbl = 7)' => [ $o{ul_voted} ? '' : 'NOT', $uid ] ) : (), - $uid && defined $o{ul_onlist} ? ( - 'v.id !s IN(SELECT vid FROM ulist_vns WHERE uid = ?)' => [ $o{ul_onlist} ? '' : 'NOT', $uid ] ) : (), - !$o{id} && !$o{inc_hidden} ? ( - 'v.hidden = FALSE' => 0 ) : (), - # optimize fetching random entries (only when there are no other filters present, otherwise this won't work well) - $o{sort} eq 'rand' && $o{results} <= 10 && !grep(!/^(?:results|page|what|sort|tagspoil)$/, keys %o) ? ( - 'v.id IN(SELECT floor(random() * last_value)::integer FROM generate_series(1,20), (SELECT MAX(id) AS last_value FROM vn) s1 LIMIT 20)' ) : (), - defined $o{date_before} ? ( 'v.c_released <= ?' => $o{date_before} ) : (), - defined $o{date_after} ? ( 'v.c_released >= ?' => $o{date_after} ) : (), - defined $o{released} ? ( 'v.c_released !s ?' => [ $o{released} ? '<=' : '>', strftime('%Y%m%d', gmtime) ] ) : (), - ); - - if($o{release}) { - my($q, @p) = sqlprint - 'v.id IN(SELECT rv.vid FROM releases r JOIN releases_vn rv ON rv.id = r.id !W)', - [ 'NOT r.hidden' => 1, $self->dbReleaseFilters(%{$o{release}}), ]; - push @where, $q, \@p; - } - if($o{character}) { - my($q, @p) = sqlprint - 'v.id IN(SELECT cv.vid FROM chars c JOIN chars_vns cv ON cv.id = c.id !W)', - [ 'NOT c.hidden' => 1, $self->dbCharFilters(%{$o{character}}) ]; - push @where, $q, \@p; - } - - my @join = ( - $uid && $o{what} =~ /vnlist/ ? ("LEFT JOIN ( - SELECT irv.vid, COUNT(*) AS userlist_all, - SUM(CASE WHEN irl.status = 2 THEN 1 ELSE 0 END) AS userlist_obtained - FROM rlists irl - JOIN releases_vn irv ON irv.id = irl.rid - WHERE irl.uid = $uid - GROUP BY irv.vid - ) AS vnlist ON vnlist.vid = v.id") : (), - ); - - my $tag_ids = $o{tag_inc} && join ',', ref $o{tag_inc} ? @{$o{tag_inc}} : $o{tag_inc}; - my @select = ( # see https://rt.cpan.org/Ticket/Display.html?id=54224 for the cast on c_languages and c_platforms - qw|v.id v.locked v.hidden v.c_released v.c_languages::text[] v.c_olang::text[] v.c_platforms::text[] v.title v.original|, - $o{what} =~ /extended/ ? ( - qw|v.alias v.length v.desc v.l_wp v.l_encubed v.l_renai v.l_wikidata|, 'coalesce(vndbid_num(v.image),0) as image' ) : (), - $o{what} =~ /rating/ ? (qw|v.c_popularity v.c_rating v.c_votecount|) : (), - $o{what} =~ /ranking/ ? ( - '(SELECT COUNT(*)+1 FROM vn iv WHERE iv.hidden = false AND iv.c_popularity > COALESCE(v.c_popularity, 0.0)) AS p_ranking', - '(SELECT COUNT(*)+1 FROM vn iv WHERE iv.hidden = false AND iv.c_rating > COALESCE(v.c_rating, 0.0)) AS r_ranking', - ) : (), - $uid && $o{what} =~ /vnlist/ ? (qw|vnlist.userlist_all vnlist.userlist_obtained|) : (), - # TODO: optimize this, as it will be very slow when the selected tags match a lot of VNs (>1000) - $tag_ids ? - qq|(SELECT AVG(tvh.rating) FROM tags_vn_inherit tvh WHERE tvh.tag IN($tag_ids) AND tvh.vid = v.id AND spoiler <= $o{tagspoil} GROUP BY tvh.vid) AS tagscore| : (), - ); - - no if $] >= 5.022, warnings => 'redundant'; - my $order = sprintf { - id => 'v.id %s', - rel => 'v.c_released %s, v.title ASC', - pop => 'v.c_popularity %s NULLS LAST', - rating => 'v.c_rating %s NULLS LAST', - title => 'v.title %s', - tagscore => 'tagscore %s, v.title ASC', - rand => 'RANDOM()', - }->{$o{sort}}, $o{reverse} ? 'DESC' : 'ASC'; - - my($r, $np) = $self->dbPage(\%o, q| - SELECT !s - FROM vn v - !s - !W - ORDER BY !s|, - join(', ', @select), join(' ', @join), \@where, $order, - ); - - return _enrich($self, $r, $np, 0, $o{what}); -} - - -sub dbVNGetRev { - my $self = shift; - my %o = (what => '', @_); - - $o{rev} ||= $self->dbRow('SELECT MAX(rev) AS rev FROM changes WHERE type = \'v\' AND itemid = ?', $o{id})->{rev}; - - # XXX: Too much duplication with code in dbVNGet() here. Can we combine some code here? - my $uid = $self->authInfo->{id}; - - my $select = 'c.itemid AS id, vo.c_released, vo.c_languages::text[], vo.c_olang::text[], vo.c_platforms::text[], v.title, v.original'; - $select .= ', extract(\'epoch\' from c.added) as added, c.comments, c.rev, c.ihid, c.ilock, '.VNWeb::DB::sql_user(); - $select .= ', c.id AS cid, NOT EXISTS(SELECT 1 FROM changes c2 WHERE c2.type = c.type AND c2.itemid = c.itemid AND c2.rev = c.rev+1) AS lastrev'; - $select .= ', v.alias, coalesce(vndbid_num(v.image), 0) as image, v.length, v.desc, v.l_wp, v.l_encubed, v.l_renai, v.l_wikidata, vo.hidden, vo.locked' if $o{what} =~ /extended/; - $select .= ', vo.c_popularity, vo.c_rating, vo.c_votecount' if $o{what} =~ /rating/; - $select .= ', (SELECT COUNT(*)+1 FROM vn iv WHERE iv.hidden = false AND iv.c_popularity > COALESCE(vo.c_popularity, 0.0)) AS p_ranking' - .', (SELECT COUNT(*)+1 FROM vn iv WHERE iv.hidden = false AND iv.c_rating > COALESCE(vo.c_rating, 0.0)) AS r_ranking' if $o{what} =~ /ranking/; - - my $r = $self->dbAll(q| - SELECT !s - FROM changes c - JOIN vn vo ON vo.id = c.itemid - JOIN vn_hist v ON v.chid = c.id - JOIN users u ON u.id = c.requester - WHERE c.type = 'v' AND c.itemid = ? AND c.rev = ?|, - $select, $o{id}, $o{rev} - ); - - return _enrich($self, $r, 0, 1, $o{what}); -} - - -sub _enrich { - my($self, $r, $np, $rev, $what) = @_; - - if(@$r && $what =~ /anime|relations|staff|seiyuu/) { - my($col, $hist, $colname) = $rev ? ('cid', '_hist', 'chid') : ('id', '', 'id'); - my %r = map { - $r->[$_]{anime} = []; - $r->[$_]{credits} = []; - $r->[$_]{seiyuu} = []; - $r->[$_]{relations} = []; - ($r->[$_]{$col}, $_) - } 0..$#$r; - - if($what =~ /staff/) { - push(@{$r->[$r{ delete $_->{xid} }]{credits}}, $_) for (@{$self->dbAll(" - SELECT vs.$colname AS xid, s.id, vs.aid, sa.name, sa.original, s.gender, s.lang, vs.role, vs.note - FROM vn_staff$hist vs - JOIN staff_alias sa ON vs.aid = sa.aid - JOIN staff s ON s.id = sa.id - WHERE vs.$colname IN(!l) - ORDER BY vs.role ASC, sa.name ASC", - [ keys %r ] - )}); - } - - if($what =~ /seiyuu/) { - # The seiyuu query needs the VN id to get the VN<->Char spoiler level. - # Obtaining this ID is different when using the hist table. - my($vid, $join) = $rev ? ('h.itemid', 'JOIN changes h ON h.id = vs.chid') : ('vs.id', ''); - push(@{$r->[$r{ delete $_->{xid} }]{seiyuu}}, $_) for (@{$self->dbAll(" - SELECT vs.$colname AS xid, s.id, vs.aid, sa.name, sa.original, s.gender, s.lang, c.id AS cid, c.name AS cname, vs.note, - (SELECT MAX(spoil) FROM chars_vns cv WHERE cv.vid = $vid AND cv.id = c.id) AS spoil - FROM vn_seiyuu$hist vs - JOIN staff_alias sa ON vs.aid = sa.aid - JOIN staff s ON s.id = sa.id - JOIN chars c ON c.id = vs.cid - $join - WHERE vs.$colname IN(!l) - ORDER BY c.name", - [ keys %r ] - )}); - } - - if($what =~ /anime/) { - push(@{$r->[$r{ delete $_->{xid} }]{anime}}, $_) for (@{$self->dbAll(" - SELECT va.$colname AS xid, a.id, a.year, a.ann_id, a.nfo_id, a.type, a.title_romaji, a.title_kanji, extract('epoch' from a.lastfetch) AS lastfetch - FROM vn_anime$hist va - JOIN anime a ON va.aid = a.id - WHERE va.$colname IN(!l)", - [ keys %r ] - )}); - } - - if($what =~ /relations/) { - push(@{$r->[$r{ delete $_->{xid} }]{relations}}, $_) for(@{$self->dbAll(" - SELECT rel.$colname AS xid, rel.vid AS id, rel.relation, rel.official, v.title, v.original - FROM vn_relations$hist rel - JOIN vn v ON rel.vid = v.id - WHERE rel.$colname IN(!l)", - [ keys %r ] - )}); - } - } - - VNWeb::DB::enrich_flatten(vnlist_labels => id => vid => sub { VNWeb::DB::sql(' - SELECT uvl.vid, ul.label - FROM ulist_vns_labels uvl - JOIN ulist_labels ul ON ul.uid = uvl.uid AND ul.id = uvl.lbl - WHERE uvl.uid =', \$self->authInfo->{id}, 'AND uvl.vid IN', $_[0], ' - ORDER BY CASE WHEN ul.id < 10 THEN ul.id ELSE 10 END, ul.label' - )}, $r) if $what =~ /vnlist/ && $self->authInfo->{id}; - - return wantarray ? ($r, $np) : $r; -} - - -1; diff --git a/lib/VNDB/Func.pm b/lib/VNDB/Func.pm index 1ce6b47b..a1ce84e5 100644 --- a/lib/VNDB/Func.pm +++ b/lib/VNDB/Func.pm @@ -3,7 +3,7 @@ package VNDB::Func; use strict; use warnings; -use TUWF ':html', 'uri_escape'; +use TUWF 'uri_escape'; use Exporter 'import'; use POSIX 'strftime'; use VNDBUtil; @@ -11,8 +11,8 @@ use VNDB::Config; use VNDB::Types; use VNDB::BBCode; our @EXPORT = (@VNDBUtil::EXPORT, 'bb_format', qw| - clearfloat cssicon minage fil_parse fil_serialize parenttags childtags - fmtvote fmtmedia fmtvnlen fmtage fmtdatestr fmtdate fmtrating fmtspoil + minage + fmtvote fmtmedia fmtage fmtdate fmtrating fmtspoil imgpath imgurl lang_attr query_encode @@ -20,26 +20,6 @@ our @EXPORT = (@VNDBUtil::EXPORT, 'bb_format', qw| |); -# three ways to represent the same information -our $fil_escape = '_ !"#$%&\'()*+,-./:;<=>?@[\]^`{}~'; -our @fil_escape = split //, $fil_escape; -our %fil_escape = map +($fil_escape[$_], sprintf '%02d', $_), 0..$#fil_escape; - - -# Clears a float, to make sure boxes always have the correct height -sub clearfloat { - div class => 'clearfloat', ''; -} - - -# Draws a CSS icon, arguments: class, title -sub cssicon { - abbr class => "icons $_[0]", title => $_[1]; - lit ' '; - end; -} - - sub minage { my($a, $ex) = @_; $a = $AGE_RATING{$a}; @@ -47,103 +27,6 @@ sub minage { } -# arguments: $filter_string, @allowed_keys -sub fil_parse { - my $str = shift; - my %keys = map +($_,1), @_; - my %r; - for (split /\./, $str) { - next if !/^([a-z0-9_]+)-([a-zA-Z0-9_~\x81-\x{ffffff}]+)$/ || !$keys{$1}; - my($f, $v) = ($1, $2); - my @v = split /~/, $v; - s/_([0-9]{2})/$1 > $#fil_escape ? '' : $fil_escape[$1]/eg for(@v); - $r{$f} = @v > 1 ? \@v : $v[0] - } - return \%r; -} - - -sub fil_serialize { - my $fil = shift; - my $e = qr/([\Q$fil_escape\E])/; - return join '.', map { - my @v = ref $fil->{$_} ? @{$fil->{$_}} : ($fil->{$_}); - s/$e/_$fil_escape{$1}/g for(@v); - $_.'-'.join '~', @v - } grep defined($fil->{$_}), sort keys %$fil; -} - - -# generates a parent tags/traits listing -sub parenttags { - my($t, $index, $type) = @_; - p; - my @p = _parenttags(@{$t->{parents}}); - for my $p (@p ? @p : []) { - a href => "/$type", $index; - for (reverse @$p) { - txt ' > '; - a href => "/$type$_->{id}", $_->{name}; - } - txt " > $t->{name}"; - br; - } - end 'p'; -} - -# arg: tag/trait hashref -# returns: [ [ tag1, tag2, tag3 ], [ tag1, tag2, tag5 ] ] -sub _parenttags { - my @r; - for my $t (@_) { - for (@{$t->{'sub'}}) { - push @r, [ $t, @$_ ] for _parenttags($_); - } - push @r, [$t] if !@{$t->{'sub'}}; - } - return @r; -} - - -# a child tags/traits box -sub childtags { - my($self, $title, $type, $t, $order) = @_; - - div class => 'mainbox'; - h1 $title; - ul class => 'tagtree'; - for my $p (sort { !$order ? @{$b->{'sub'}} <=> @{$a->{'sub'}} : $a->{$order} <=> $b->{$order} } @{$t->{childs}}) { - li; - a href => "/$type$p->{id}", $p->{name}; - b class => 'grayedout', " ($p->{c_items})" if $p->{c_items}; - end, next if !@{$p->{'sub'}}; - ul; - for (0..$#{$p->{'sub'}}) { - last if $_ >= 5 && @{$p->{'sub'}} > 6; - li; - txt '> '; - a href => "/$type$p->{sub}[$_]{id}", $p->{'sub'}[$_]{name}; - b class => 'grayedout', " ($p->{sub}[$_]{c_items})" if $p->{'sub'}[$_]{c_items}; - end; - } - if(@{$p->{'sub'}} > 6) { - my $c = @{$p->{'sub'}}-5; - li; - txt '> '; - a href => "/$type$p->{id}", style => 'font-style: italic', - sprintf '%d more %s%s', $c, $type eq 'g' ? 'tag' : 'trait', $c==1 ? '' : 's'; - end; - } - end; - end 'li'; - } - end 'ul'; - clearfloat; - br; - end 'div'; -} - - sub _path { my($t, $id) = $_[1] =~ /([a-z]+)([0-9]+)/; $t = 'st' if $t eq 'sf' && $_[2]; @@ -171,13 +54,6 @@ sub fmtmedia { $med->{ $med->{qty} && $qty > 1 ? 'plural' : 'txt' }; } -# Formats a VN length (xtra = time indication) -sub fmtvnlen { - my($len, $xtra) = @_; - $len = $VN_LENGTH{$len}; - $len->{txt}.($xtra && $len->{time} ? " ($len->{time})" : ''); -} - # Formats a UNIX timestamp as a '<number> <unit> ago' string sub fmtage { my $a = time-shift; @@ -193,26 +69,6 @@ sub fmtage { sprintf '%d %s ago', $t, $t == 1 ? $single : $plural; } -# argument: database release date format (yyyymmdd) -# y = 0000 -> unknown -# y = 9999 -> TBA -# m = 99 -> month+day unknown -# d = 99 -> day unknown -# return value: (unknown|TBA|yyyy|yyyy-mm|yyyy-mm-dd) -# if date > now: <b class="future">str</b> -sub fmtdatestr { - my $date = sprintf '%08d', shift||0; - my $future = $date > strftime '%Y%m%d', gmtime; - my($y, $m, $d) = ($1, $2, $3) if $date =~ /^([0-9]{4})([0-9]{2})([0-9]{2})$/; - - my $str = $y == 0 ? 'unknown' : $y == 9999 ? 'TBA' : - $m == 99 ? sprintf('%04d', $y) : - $d == 99 ? sprintf('%04d-%02d', $y, $m) : - sprintf('%04d-%02d-%02d', $y, $m, $d); - - return $str if !$future; - return qq|<b class="future">$str</b>|; -} # argument: unix timestamp and optional format (compact/full) sub fmtdate { @@ -299,4 +155,3 @@ sub md2html { } 1; - diff --git a/lib/VNDB/Handler/Chars.pm b/lib/VNDB/Handler/Chars.pm deleted file mode 100644 index f75e1b46..00000000 --- a/lib/VNDB/Handler/Chars.pm +++ /dev/null @@ -1,111 +0,0 @@ - -package VNDB::Handler::Chars; - -use strict; -use warnings; -use TUWF ':html', 'uri_escape'; -use Exporter 'import'; -use VNDB::Func; -use VNDB::Types; - -our @EXPORT = ('charBrowseTable'); - -TUWF::register( - qr{old/c/([a-z0]|all)} => \&list, -); - - -sub list { - my($self, $fch) = @_; - - my $f = $self->formValidate( - { get => 'p', required => 0, default => 1, template => 'page' }, - { get => 'q', required => 0, default => '' }, - { get => 'fil', required => 0, default => '' }, - ); - return $self->resNotFound if $f->{_err}; - - my($list, $np) = $self->filFetchDB(char => $f->{fil}, { - tagspoil => $self->authPref('spoilers')||0, - }, { - $fch ne 'all' ? ( char => $fch ) : (), - $f->{q} ? ( search => $f->{q} ) : (), - results => 50, - page => $f->{p}, - what => 'vns', - }); - - $self->htmlHeader(title => 'Browse characters'); - - my $quri = uri_escape($f->{q}); - form action => '/old/c/all', 'accept-charset' => 'UTF-8', method => 'get'; - div class => 'mainbox'; - h1 'Browse characters'; - $self->htmlSearchBox('c', $f->{q}); - p class => 'browseopts'; - for ('all', 'a'..'z', 0) { - a href => "/old/c/$_?q=$quri;fil=$f->{fil}", $_ eq $fch ? (class => 'optselected') : (), $_ eq 'all' ? 'ALL' : $_ ? uc $_ : '#'; - } - end; - - p class => 'filselect'; - a id => 'filselect', href => '#c'; - lit '<i>▸</i> Filters<i></i>'; - end; - end; - input type => 'hidden', class => 'hidden', name => 'fil', id => 'fil', value => $f->{fil}; - end; - end 'form'; - - if(!@$list) { - div class => 'mainbox'; - h1 'No results'; - p 'No characters found that matched your criteria.'; - end; - } - - @$list && $self->charBrowseTable($list, $np, $f, "/old/c/$fch?q=$quri;fil=$f->{fil}"); - - $self->htmlFooter; -} - - -# Also used on Handler::Traits -sub charBrowseTable { - my($self, $list, $np, $f, $uri) = @_; - - $self->htmlBrowse( - class => 'charb', - items => $list, - options => $f, - nextpage => $np, - pageurl => $uri, - sorturl => $uri, - header => [ [ '' ], [ '' ] ], - row => sub { - my($s, $n, $l) = @_; - Tr; - td class => 'tc1'; - cssicon "gen $l->{gender}", $GENDER{$l->{gender}} if $l->{gender} ne 'unknown'; - end; - td class => 'tc2'; - a href => "/c$l->{id}", title => $l->{original}||$l->{name}, shorten $l->{name}, 50; - b class => 'grayedout'; - my $i = 1; - my %vns; - for (@{$l->{vns}}) { - next if $_->{spoil} || $vns{$_->{vid}}++; - last if $i++ > 4; - txt ', ' if $i > 2; - a href => "/v$_->{vid}/chars", title => $_->{vntitle}, shorten $_->{vntitle}, 30; - } - end; - end; - end; - } - ) -} - - -1; - diff --git a/lib/VNDB/Handler/Misc.pm b/lib/VNDB/Handler/Misc.pm deleted file mode 100644 index cca10ed5..00000000 --- a/lib/VNDB/Handler/Misc.pm +++ /dev/null @@ -1,53 +0,0 @@ - -package VNDB::Handler::Misc; - - -use strict; -use warnings; -use TUWF ':html', ':xml', 'uri_escape'; -use VNDB::Func; -use VNDB::Types; - - -TUWF::register( - qr{nospam}, \&nospam, - qr{xml/prefs\.xml}, \&prefs, -); - - -sub nospam { - my $self = shift; - $self->htmlHeader(title => 'Could not send form', noindex => 1); - - div class => 'mainbox'; - h1 'Could not send form'; - div class => 'warning'; - h2 'Error'; - p 'The form could not be sent, please make sure you have Javascript enabled in your browser.'; - end; - end; - - $self->htmlFooter; -} - - -sub prefs { - my $self = shift; - return if !$self->authCheckCode; - return $self->resNotFound if !$self->authInfo->{id}; - my $f = $self->formValidate( - { get => 'key', enum => [qw|filter_vn filter_release|] }, - { get => 'value', required => 0, maxlength => 2000 }, - ); - return $self->resNotFound if $f->{_err}; - $self->authPref($f->{key}, $f->{value}); - - # doesn't really matter what we return, as long as it's XML - $self->resHeader('Content-type' => 'text/xml'); - xml; - tag 'done', ''; -} - - -1; - diff --git a/lib/VNDB/Handler/Producers.pm b/lib/VNDB/Handler/Producers.pm deleted file mode 100644 index 44201e79..00000000 --- a/lib/VNDB/Handler/Producers.pm +++ /dev/null @@ -1,45 +0,0 @@ - -package VNDB::Handler::Producers; - -use strict; -use warnings; -use TUWF ':html', ':xml'; -use VNDB::Func; -use VNDB::Types; - - -TUWF::register( - qr{xml/producers\.xml} => \&pxml, -); - - -# peforms a (simple) search and returns the results in XML format -sub pxml { - my $self = shift; - - my $f = $self->formValidate( - { get => 'q', required => 0, maxlength => 500 }, - { get => 'id', required => 0, multi => 1, template => 'id' }, - { get => 'r', required => 0, template => 'uint', min => 1, max => 50, default => 10 }, - ); - return $self->resNotFound if $f->{_err} || (!$f->{q} && !$f->{id} && !$f->{id}[0]); - - my($list, $np) = $self->dbProducerGet( - !$f->{q} ? () : $f->{q} =~ /^p([1-9]\d*)/ ? (id => $1) : (search => $f->{q}, sort => 'search'), - $f->{id} && $f->{id}[0] ? (id => $f->{id}) : (), - results => $f->{r}, - page => 1, - ); - - $self->resHeader('Content-type' => 'text/xml; charset=UTF-8'); - xml; - tag 'producers', more => $np ? 'yes' : 'no', query => $f->{q}||''; - for(@$list) { - tag 'item', id => $_->{id}, $_->{name}; - } - end; -} - - -1; - diff --git a/lib/VNDB/Handler/Releases.pm b/lib/VNDB/Handler/Releases.pm deleted file mode 100644 index 1fdbb0d6..00000000 --- a/lib/VNDB/Handler/Releases.pm +++ /dev/null @@ -1,160 +0,0 @@ - -package VNDB::Handler::Releases; - -use strict; -use warnings; -use TUWF ':html', ':xml', 'uri_escape'; -use VNDB::Func; -use VNDB::Types; - - -TUWF::register( - qr{old/r} => \&browse, - qr{xml/engines.xml} => \&enginexml, -); - - -sub browse { - my $self = shift; - - my $f = $self->formValidate( - { get => 'p', required => 0, default => 1, template => 'page' }, - { get => 'o', required => 0, default => 'a', enum => ['a', 'd'] }, - { get => 'q', required => 0, default => '', maxlength => 500 }, - { get => 's', required => 0, default => 'title', enum => [qw|released minage title|] }, - { get => 'fil',required => 0 }, - ); - return $self->resNotFound if $f->{_err}; - $f->{fil} //= $self->authPref('filter_release'); - - my %compat = _fil_compat($self); - my($list, $np) = !$f->{q} && !$f->{fil} && !keys %compat ? ([], 0) : $self->filFetchDB(release => $f->{fil}, \%compat, { - sort => $f->{s}, reverse => $f->{o} eq 'd', - page => $f->{p}, - results => 50, - what => 'platforms', - $f->{q} ? ( search => $f->{q} ) : (), - }); - - $self->htmlHeader(title => 'Browse releases'); - - form method => 'get', action => '/old/r', 'accept-charset' => 'UTF-8'; - div class => 'mainbox'; - h1 'Browse releases'; - $self->htmlSearchBox('r', $f->{q}); - p class => 'filselect'; - a id => 'filselect', href => '#r'; - lit '<i>▸</i> Filters<i></i>'; - end; - end; - input type => 'hidden', class => 'hidden', name => 'fil', id => 'fil', value => $f->{fil}; - end; - end 'form'; - - my $uri = sprintf '/old/r?q=%s;fil=%s', uri_escape($f->{q}), $f->{fil}; - $self->htmlBrowse( - class => 'relbrowse', - items => $list, - options => $f, - nextpage => $np, - pageurl => "$uri;s=$f->{s};o=$f->{o}", - sorturl => $uri, - header => [ - [ 'Released', 'released' ], - [ 'Rating', 'minage' ], - [ '', '' ], - [ 'Title', 'title' ], - ], - row => sub { - my($s, $n, $l) = @_; - Tr; - td class => 'tc1'; - lit fmtdatestr $l->{released}; - end; - td class => 'tc2', $l->{minage} < 0 ? '' : minage $l->{minage}; - td class => 'tc3'; - $_ ne 'oth' && cssicon $_, $PLATFORM{$_} for (@{$l->{platforms}}); - cssicon "lang $_", $LANGUAGE{$_} for (@{$l->{languages}}); - cssicon "rt$l->{type}", $l->{type}; - end; - td class => 'tc4'; - a href => "/r$l->{id}", title => $l->{original}||$l->{title}, shorten $l->{title}, 90; - b class => 'grayedout', ' (patch)' if $l->{patch}; - end; - end 'tr'; - }, - ) if @$list; - if(($f->{q} || $f->{fil}) && !@$list) { - div class => 'mainbox'; - h1 'No results found'; - div class => 'notice'; - p; - txt 'Sorry, couldn\'t find anything that comes through your filters. You might want to disable a few filters to get more results.'; - br; br; - txt 'Also, keep in mind that we don\'t have all information about all releases.' - .' So e.g. filtering on screen resolution will exclude all releases of which we don\'t know it\'s resolution,' - .' even though it might in fact be in the resolution you\'re looking for.'; - end - end; - end; - } - $self->htmlFooter(pref_code => 1); -} - - -# provide compatibility with old URLs -sub _fil_compat { - my $self = shift; - my %c; - my $f = $self->formValidate( - { get => 'ln', required => 0, multi => 1, default => '', enum => [ keys %LANGUAGE ] }, - { get => 'pl', required => 0, multi => 1, default => '', enum => [ keys %PLATFORM ] }, - { get => 'me', required => 0, multi => 1, default => '', enum => [ keys %MEDIUM ] }, - { get => 'tp', required => 0, default => '', enum => [ '', keys %RELEASE_TYPE ] }, - { get => 'pa', required => 0, default => 0, enum => [ 0..2 ] }, - { get => 'fw', required => 0, default => 0, enum => [ 0..2 ] }, - { get => 'do', required => 0, default => 0, enum => [ 0..2 ] }, - { get => 'ma_m', required => 0, default => 0, enum => [ 0, 1 ] }, - { get => 'ma_a', required => 0, default => 0, enum => [ keys %AGE_RATING ] }, - { get => 'mi', required => 0, default => 0, template => 'uint' }, - { get => 'ma', required => 0, default => 99999999, template => 'uint' }, - ); - return () if $f->{_err}; - $c{minage} = [ grep $_ >= 0 && ($f->{ma_m} ? $f->{ma_a} >= $_ : $f->{ma_a} <= $_), keys %AGE_RATING ] if $f->{ma_a} || $f->{ma_m}; - $c{date_after} = $f->{mi} if $f->{mi}; - $c{date_before} = $f->{ma} if $f->{ma} < 99990000; - $c{plat} = $f->{pl} if $f->{pl}[0]; - $c{lang} = $f->{ln} if $f->{ln}[0]; - $c{med} = $f->{me} if $f->{me}[0]; - $c{type} = $f->{tp} if $f->{tp}; - $c{patch} = $f->{pa} == 2 ? 0 : 1 if $f->{pa}; - $c{freeware} = $f->{fw} == 2 ? 0 : 1 if $f->{fw}; - $c{doujin} = $f->{do} == 2 ? 0 : 1 if $f->{do}; - return %c; -} - - -sub enginexml { - my $self = shift; - - # The list of engines happens to be small enough for this to make sense, and - # fetching all unique engines from the releases table also happens to be fast - # enough right now, but this may need a separate cache or index in the future. - my $lst = $self->dbReleaseEngines(); - - my $f = $self->formValidate( - { get => 'q', required => 1, maxlength => 500 }, - ); - return $self->resNotFound if $f->{_err}; - - $self->resHeader('Content-type' => 'text/xml; charset=UTF-8'); - xml; - tag 'engines'; - for(grep $lst->[$_]{engine} =~ /\Q$f->{q}\E/i, 0..$#$lst) { - tag 'item', count => $lst->[$_]{cnt}, id => $_+1, $lst->[$_]{engine}; - } - end; -} - -1; - diff --git a/lib/VNDB/Handler/Staff.pm b/lib/VNDB/Handler/Staff.pm deleted file mode 100644 index 6a291f09..00000000 --- a/lib/VNDB/Handler/Staff.pm +++ /dev/null @@ -1,116 +0,0 @@ - -package VNDB::Handler::Staff; - -use strict; -use warnings; -use TUWF qw(:html :xml uri_escape); -use VNDB::Func; -use VNDB::Types; -use List::Util qw(first); - -TUWF::register( - qr{old/s/([a-z0]|all)} => \&list, - qr{xml/staff\.xml} => \&staffxml, -); - - -sub list { - my ($self, $char) = @_; - - my $f = $self->formValidate( - { get => 'p', required => 0, default => 1, template => 'page' }, - { get => 'q', required => 0, default => '' }, - { get => 'fil', required => 0, default => '' }, - ); - return $self->resNotFound if $f->{_err}; - - my ($list, $np) = $self->filFetchDB(staff => $f->{fil}, {}, { - $char ne 'all' ? ( char => $char ) : (), - $f->{q} ? ($f->{q} =~ /^=(.+)$/ ? (exact => $1) : (search => $f->{q})) : (), - results => 150, - page => $f->{p} - }); - - return $self->resRedirect('/s'.$list->[0]{id}, 'temp') - if $f->{q} && @$list && (!first { $_->{id} != $list->[0]{id} } @$list) && $f->{p} == 1 && !$f->{fil}; - # redirect to the staff page if all results refer to the same entry - - my $quri = join(';', $f->{q} ? 'q='.uri_escape($f->{q}) : (), $f->{fil} ? "fil=$f->{fil}" : ()); - $quri = '?'.$quri if $quri; - my $pageurl = "/old/s/$char$quri"; - - $self->htmlHeader(title => 'Browse staff'); - - form action => '/old/s/all', 'accept-charset' => 'UTF-8', method => 'get'; - div class => 'mainbox'; - h1 'Browse staff'; - $self->htmlSearchBox('s', $f->{q}); - p class => 'browseopts'; - for ('all', 'a'..'z', 0) { - a href => "/old/s/$_$quri", $_ eq $char ? (class => 'optselected') : (), $_ eq 'all' ? 'ALL' : $_ ? uc $_ : '#'; - } - end; - - p class => 'filselect'; - a id => 'filselect', href => '#s'; - lit '<i>▸</i> Filters<i></i>'; - end; - end; - input type => 'hidden', class => 'hidden', name => 'fil', id => 'fil', value => $f->{fil}; - end; - end 'form'; - - $self->htmlBrowseNavigate($pageurl, $f->{p}, $np, 't'); - div class => 'mainbox staffbrowse'; - h1 $f->{q} ? 'Search results' : 'Staff list'; - if(!@$list) { - p 'No results found'; - } else { - # spread the results over 3 equivalent-sized lists - my $perlist = @$list/3 < 1 ? 1 : @$list/3; - for my $c (0..(@$list < 3 ? $#$list : 2)) { - ul; - for ($perlist*$c..($perlist*($c+1))-1) { - li; - cssicon 'lang '.$list->[$_]{lang}, $LANGUAGE{$list->[$_]{lang}}; - a href => "/s$list->[$_]{id}", - title => $list->[$_]{original}, $list->[$_]{name}; - end; - } - end; - } - } - clearfloat; - end 'div'; - $self->htmlBrowseNavigate($pageurl, $f->{p}, $np, 'b'); - $self->htmlFooter; -} - - -sub staffxml { - my $self = shift; - - my $f = $self->formValidate( - { get => 'q', required => 0, maxlength => 500 }, - { get => 'id', required => 0, multi => 1, template => 'id' }, - { get => 'staffid', required => 0, default => 0 }, # The returned id = staff id when set, otherwise it's the alias id - { get => 'r', required => 0, template => 'uint', min => 1, max => 50, default => 10 }, - ); - return $self->resNotFound if $f->{_err} || (!$f->{q} && !$f->{id} && !$f->{id}[0]); - - my($list, $np) = $self->dbStaffGet( - !$f->{q} ? () : $f->{q} =~ /^s([1-9]\d*)/ ? (id => $1) : $f->{q} =~ /^=(.+)/ ? (exact => $1) : (search => $f->{q}, sort => 'search'), - $f->{id} && $f->{id}[0] ? (id => $f->{id}) : (), - results => $f->{r}, page => 1, - ); - - $self->resHeader('Content-type' => 'text/xml; charset=UTF-8'); - xml; - tag 'staff', more => $np ? 'yes' : 'no'; - for(@$list) { - tag 'item', sid => $_->{id}, id => $f->{staffid} ? $_->{id} : $_->{aid}, orig => $_->{original}, $_->{name}; - } - end; -} - -1; diff --git a/lib/VNDB/Handler/Tags.pm b/lib/VNDB/Handler/Tags.pm deleted file mode 100644 index d4807055..00000000 --- a/lib/VNDB/Handler/Tags.pm +++ /dev/null @@ -1,202 +0,0 @@ - -package VNDB::Handler::Tags; - - -use strict; -use warnings; -use TUWF ':html', ':xml', 'xml_escape'; -use VNDB::Func; -use VNDB::Types; - - -TUWF::register( - qr{old/g([1-9]\d*)}, \&tagpage, - qr{g/debug}, \&fulltree, - qr{xml/tags\.xml}, \&tagxml, -); - - -sub tagpage { - my($self, $tag) = @_; - - my $t = $self->dbTagGet(id => $tag, what => 'parents(0) childs(2) aliases')->[0]; - return $self->resNotFound if !$t; - - my $f = $self->formValidate( - { get => 's', required => 0, default => 'tagscore', enum => [ qw|title rel pop tagscore rating| ] }, - { get => 'o', required => 0, default => 'd', enum => [ 'a','d' ] }, - { get => 'p', required => 0, default => 1, template => 'page' }, - { get => 'm', required => 0, default => $self->authPref('spoilers') || 0, enum => [qw|0 1 2|] }, - { get => 'fil', required => 0 }, - ); - return $self->resNotFound if $f->{_err}; - $f->{fil} //= $self->authPref('filter_vn'); - - my($list, $np) = !$t->{searchable} || $t->{state} != 2 ? ([],0) : $self->filFetchDB(vn => $f->{fil}, undef, { - what => 'rating', - results => 50, - page => $f->{p}, - sort => $f->{s}, reverse => $f->{o} eq 'd', - tagspoil => $f->{m}, - tag_inc => $tag, - tag_exc => undef, - }); - - my $title = "Tag: $t->{name}"; - $self->htmlHeader(title => $title, noindex => $t->{state} != 2); - $self->htmlMainTabs('g', $t); - - if($t->{state} != 2) { - div class => 'mainbox'; - h1 $title; - if($t->{state} == 1) { - div class => 'warning'; - h2 'Tag deleted'; - p; - txt 'This tag has been removed from the database, and cannot be used or re-added.'; - br; - txt 'File a request on the '; - a href => '/t/db', 'discussion board'; - txt ' if you disagree with this.'; - end; - end; - } else { - div class => 'notice'; - h2 'Waiting for approval'; - p 'This tag is waiting for a moderator to approve it. You can still use it to tag VNs as you would with a normal tag.'; - end; - } - end 'div'; - } - - div class => 'mainbox'; - a class => 'addnew', href => "/g$tag/add", 'Create child tag' if $self->authCan('tag') && $t->{state} != 1; - h1 $title; - - parenttags($t, 'Tags', 'g'); - - if($t->{description}) { - p class => 'description'; - lit bb_format $t->{description}; - end; - } - if(!$t->{applicable} || !$t->{searchable}) { - p class => 'center'; - b 'Properties'; - br; - txt 'Not searchable.' if !$t->{searchable}; - br; - txt 'Can not be directly applied to visual novels.' if !$t->{applicable}; - end; - } - p class => 'center'; - b 'Category'; - br; - txt $TAG_CATEGORY{$t->{cat}}; - end; - if(@{$t->{aliases}}) { - p class => 'center'; - b 'Aliases'; - br; - lit xml_escape($_).'<br />' for (@{$t->{aliases}}); - end; - } - end 'div'; - - childtags($self, 'Child tags', 'g', $t) if @{$t->{childs}}; - - if($t->{searchable} && $t->{state} == 2) { - form action => "/old/g$t->{id}", 'accept-charset' => 'UTF-8', method => 'get'; - div class => 'mainbox'; - a class => 'addnew', href => "/g/links?t=$tag", 'Recently tagged'; - h1 'Visual novels'; - - p class => 'browseopts'; - a href => "/old/g$t->{id}?fil=$f->{fil};s=$f->{s};o=$f->{o};m=0", $f->{m} == 0 ? (class => 'optselected') : (), 'Hide spoilers'; - a href => "/old/g$t->{id}?fil=$f->{fil};s=$f->{s};o=$f->{o};m=1", $f->{m} == 1 ? (class => 'optselected') : (), 'Show minor spoilers'; - a href => "/old/g$t->{id}?fil=$f->{fil};s=$f->{s};o=$f->{o};m=2", $f->{m} == 2 ? (class => 'optselected') : (), 'Spoil me!'; - end; - - p class => 'filselect'; - a id => 'filselect', href => '#v'; - lit '<i>▸</i> Filters<i></i>'; - end; - end; - input type => 'hidden', class => 'hidden', name => 'fil', id => 'fil', value => $f->{fil}; - input type => 'hidden', class => 'hidden', name => 'm', id => 'm', value => $f->{m}; - - if(!@$list) { - p; br; br; txt 'This tag has not been linked to any visual novels yet, or they were hidden because of your spoiler settings or default filters.'; end; - } - if(@{$t->{childs}}) { - p; br; txt 'The list below also includes all visual novels linked to child tags.'; end; - } - end 'div'; - end 'form'; - $self->htmlBrowseVN($list, $f, $np, "/old/g$t->{id}?fil=$f->{fil};m=$f->{m}", 1) if @$list; - } - - $self->htmlFooter(pref_code => 1); -} - - -# non-translatable debug page -sub fulltree { - my $self = shift; - return $self->htmlDenied if !$self->authCan('tagmod'); - - my $e; - $e = sub { - my $lst = shift; - ul style => 'list-style-type: none; margin-left: 15px'; - for (@$lst) { - li; - txt '> '; - a href => "/g$_->{id}", $_->{name}; - b class => 'grayedout', " ($_->{c_items})" if $_->{c_items}; - end; - $e->($_->{sub}) if $_->{sub}; - } - end; - }; - - my $tags = $self->dbTTTree(tag => 0, 25); - $self->htmlHeader(title => '[DEBUG] Tag tree', noindex => 1); - div class => 'mainbox'; - h1 '[DEBUG] Tag tree'; - $e->($tags); - end; - $self->htmlFooter; -} - - -sub tagxml { - my $self = shift; - - my $f = $self->formValidate( - { get => 'q', required => 0, maxlength => 500 }, - { get => 'id', required => 0, multi => 1, template => 'id' }, - { get => 'searchable', required => 0, default => 0 }, - { get => 'r', required => 0, template => 'uint', min => 1, max => 50, default => 15 }, - ); - return $self->resNotFound if $f->{_err} || (!$f->{q} && !$f->{id} && !$f->{id}[0]); - - my($list, $np) = $self->dbTagGet( - !$f->{q} ? () : $f->{q} =~ /^g([1-9]\d*)/ ? (id => $1) : $f->{q} =~ /^=(.+)$/ ? (name => $1) : (search => $f->{q}, sort => 'search'), - $f->{id} && $f->{id}[0] ? (id => $f->{id}) : (), - results => $f->{r}, - page => 1, - $f->{searchable} ? (state => 2, searchable => 1) : (), - ); - - $self->resHeader('Content-type' => 'text/xml; charset=UTF-8'); - xml; - tag 'tags', more => $np ? 'yes' : 'no', $f->{q} ? (query => $f->{q}) : (); - for(@$list) { - tag 'item', id => $_->{id}, searchable => $_->{searchable} ? 'yes' : 'no', applicable => $_->{applicable} ? 'yes' : 'no', state => $_->{state}, $_->{name}; - } - end; -} - - -1; diff --git a/lib/VNDB/Handler/Traits.pm b/lib/VNDB/Handler/Traits.pm deleted file mode 100644 index 9dc08b9f..00000000 --- a/lib/VNDB/Handler/Traits.pm +++ /dev/null @@ -1,165 +0,0 @@ - -package VNDB::Handler::Traits; - -use strict; -use warnings; -use TUWF ':html', ':xml', 'html_escape', 'xml_escape'; -use VNDB::Func; - - -TUWF::register( - qr{old/i([1-9]\d*)}, \&traitpage, - qr{xml/traits\.xml}, \&traitxml, -); - - -sub traitpage { - my($self, $trait) = @_; - - my $t = $self->dbTraitGet(id => $trait, what => 'parents(0) childs(2)')->[0]; - return $self->resNotFound if !$t; - - my $f = $self->formValidate( - { get => 'p', required => 0, default => 1, template => 'page' }, - { get => 'm', required => 0, default => $self->authPref('spoilers')||0, enum => [qw|0 1 2|] }, - { get => 'fil', required => 0, default => '' }, - ); - return $self->resNotFound if $f->{_err}; - - my $title = "Trait: $t->{name}"; - $self->htmlHeader(title => $title, noindex => $t->{state} != 2); - $self->htmlMainTabs('i', $t); - - if($t->{state} != 2) { - div class => 'mainbox'; - h1 $title; - if($t->{state} == 1) { - div class => 'warning'; - h2 'Trait deleted'; - p; - txt 'This trait has been removed from the database, and cannot be used or re-added. File a request on the '; - a href => '/t/db', 'discussion board'; - txt ' if you disagree with this.'; - end; - end; - } else { - div class => 'notice'; - h2 'Waiting for approval'; - p 'This trait is waiting for a moderator to approve it.'; - end; - } - end 'div'; - } - - div class => 'mainbox'; - a class => 'addnew', href => "/i$trait/add", 'Create child trait' if $self->authCan('edit') && $t->{state} != 1; - h1 $title; - - parenttags($t, 'Traits', 'i'); - - if($t->{description}) { - p class => 'description'; - lit bb_format $t->{description}; - end; - } - if(!$t->{applicable} || !$t->{searchable}) { - p class => 'center'; - b 'Properties'; - br; - txt 'Not searchable.' if !$t->{searchable}; - br; - txt 'Can not be directly applied to characters.' if !$t->{applicable}; - end; - } - if($t->{sexual}) { - p class => 'center'; - b 'Sexual content'; - end; - } - if($t->{alias}) { - p class => 'center'; - b 'Aliases'; - br; - lit html_escape($t->{alias}); - end; - } - end 'div'; - - childtags($self, 'Child traits', 'i', $t) if @{$t->{childs}}; - - if($t->{searchable} && $t->{state} == 2) { - my($chars, $np) = $self->filFetchDB(char => $f->{fil}, {}, { - trait_inc => $trait, - tagspoil => $f->{m}, - results => 50, - page => $f->{p}, - what => 'vns', - }); - - form action => "/i$t->{id}", 'accept-charset' => 'UTF-8', method => 'get'; - div class => 'mainbox'; - h1 'Characters'; - - p class => 'browseopts'; - a href => "/i$trait?fil=$f->{fil};m=0", $f->{m} == 0 ? (class => 'optselected') : (), 'Hide spoilers'; - a href => "/i$trait?fil=$f->{fil};m=1", $f->{m} == 1 ? (class => 'optselected') : (), 'Show minor spoilers'; - a href => "/i$trait?fil=$f->{fil};m=2", $f->{m} == 2 ? (class => 'optselected') : (), 'Spoil me!'; - end; - - p class => 'filselect'; - a id => 'filselect', href => '#c'; - lit '<i>▸</i> Filters<i></i>'; - end; - end; - input type => 'hidden', class => 'hidden', name => 'fil', id => 'fil', value => $f->{fil}; - input type => 'hidden', class => 'hidden', name => 'm', id => 'm', value => $f->{m}; - - if(!@$chars) { - p; br; br; txt 'This trait has not been linked to any characters yet, or they were hidden because of your spoiler settings.'; end; - } - if(@{$t->{childs}}) { - p; br; txt 'The list below also includes all characters linked to child traits.'; end; - } - end 'div'; - end 'form'; - @$chars && $self->charBrowseTable($chars, $np, $f, "/i$trait?m=$f->{m};fil=$f->{fil}"); - } - - $self->htmlFooter; -} - - -sub traitxml { - my $self = shift; - - my $f = $self->formValidate( - { get => 'q', required => 0, maxlength => 500 }, - { get => 'id', required => 0, multi => 1, template => 'id' }, - { get => 'r', required => 0, default => 15, template => 'uint', min => 1, max => 200 }, - { get => 'searchable', required => 0, default => 0 }, - ); - return $self->resNotFound if $f->{_err} || (!$f->{q} && !$f->{id} && !$f->{id}[0]); - - my($list, $np) = $self->dbTraitGet( - results => $f->{r}, - page => 1, - sort => 'group', - state => 2, - $f->{searchable} ? (searchable => 1) : (), - !$f->{q} ? () : $f->{q} =~ /^i([1-9]\d*)/ ? (id => $1) : (search => $f->{q}, sort => 'search'), - $f->{id} && $f->{id}[0] ? (id => $f->{id}) : (), - ); - - $self->resHeader('Content-type' => 'text/xml; charset=UTF-8'); - xml; - tag 'traits', more => $np ? 'yes' : 'no'; - for(@$list) { - tag 'item', id => $_->{id}, searchable => $_->{searchable} ? 'yes' : 'no', applicable => $_->{applicable} ? 'yes' : 'no', group => $_->{group}||'', - groupname => $_->{groupname}||'', state => $_->{state}, defaultspoil => $_->{defaultspoil}, $_->{name}; - } - end; -} - - -1; - diff --git a/lib/VNDB/Handler/VNBrowse.pm b/lib/VNDB/Handler/VNBrowse.pm deleted file mode 100644 index 090f58ad..00000000 --- a/lib/VNDB/Handler/VNBrowse.pm +++ /dev/null @@ -1,143 +0,0 @@ - -package VNDB::Handler::VNBrowse; - -use strict; -use warnings; -use TUWF ':html', 'uri_escape'; -use VNDB::Func; -use VNDB::Types; - - -TUWF::register( - qr{old/v/([a-z0]|all)} => \&list, -); - - -sub list { - my($self, $char) = @_; - - my $f = $self->formValidate( - { get => 's', required => 0, default => 'tagscore', enum => [ qw|title rel pop tagscore rating| ] }, - { get => 'o', required => 0, enum => [ 'a','d' ] }, - { get => 'p', required => 0, default => 1, template => 'page' }, - { get => 'q', required => 0, default => '' }, - { get => 'sq', required => 0, default => '' }, - { get => 'fil',required => 0 }, - { get => 'rfil', required => 0, default => '' }, - { get => 'cfil', required => 0, default => '' }, - { get => 'vnlist', required => 0, default => 2, enum => [ '0', '1' ] }, # 2: use pref - ); - return $self->resNotFound if $f->{_err}; - $f->{q} ||= $f->{sq}; - $f->{fil} //= $self->authPref('filter_vn'); - my %compat = _fil_compat($self); - my $uid = $self->authInfo->{id}; - - my $read_write_pref = sub { - my($type, $pref_name) = @_; - - return 0 if !$uid; # no data to display anyway - return $self->authPref($pref_name)?1:0 if $f->{$type} == 2; - - $self->authPref($pref_name => $f->{$type}?1:0) if ($self->authPref($pref_name)?1:0) != $f->{$type}; - return $f->{$type}; - }; - - $f->{vnlist} = $read_write_pref->('vnlist', 'vn_list_own'); - - return $self->resRedirect('/'.$1.$2.(!$3 ? '' : $1 eq 'd' ? '#'.$3 : '.'.$3), 'temp') - if $f->{q} && $f->{q} =~ /^([gvrptudcis])([0-9]+)(?:\.([0-9]+))?$/; - - $f->{s} = 'title' if $f->{fil} !~ /tag_inc-/ && $f->{s} eq 'tagscore'; - $f->{o} = $f->{s} eq 'tagscore' ? 'd' : 'a' if !$f->{o}; - - my $rfil = fil_parse $f->{rfil}, @{$VNDB::Util::Misc::filfields{release}}; - $self->filCompat(release => $rfil); - $f->{rfil} = fil_serialize $rfil, @{$VNDB::Util::Misc::filfields{release}}; - - my $cfil = fil_parse $f->{cfil}, @{$VNDB::Util::Misc::filfields{char}}; - $cfil->{tagspoil} //= $self->authPref('spoilers')||0 if keys %$cfil; - - my($list, $np) = $self->filFetchDB(vn => $f->{fil}, { - %compat, - tagspoil => $self->authPref('spoilers')||0, - }, { - what => ' rating'.($f->{vnlist} ? ' vnlist' : ''), - $char ne 'all' ? ( char => $char ) : (), - $f->{q} ? ( search => $f->{q} ) : (), - keys %$rfil ? ( release => $rfil ) : (), - keys %$cfil ? ( character => $cfil ) : (), - results => 50, - page => $f->{p}, - sort => $f->{s}, reverse => $f->{o} eq 'd', - }); - - $self->resRedirect('/v'.$list->[0]{id}, 'temp') - if $f->{q} && @$list == 1 && $f->{p} == 1; - - $self->htmlHeader(title => 'Browse visual novels', search => $f->{q}); - - my $quri = uri_escape($f->{q}); - form action => '/old/v/all', 'accept-charset' => 'UTF-8', method => 'get'; - - # url generator - my $url = sub { - my($char, $toggle) = @_; - - return "/old/v/$char?q=$quri;fil=$f->{fil};rfil=$f->{rfil};cfil=$f->{cfil};s=$f->{s};o=$f->{o}" . - ($toggle ? ";$toggle=".($f->{$toggle}?0:1) : ''); - }; - - div class => 'mainbox'; - h1 'Browse visual novels'; - $self->htmlSearchBox('v', $f->{q}); - p class => 'browseopts'; - for ('all', 'a'..'z', 0) { - a href => $url->($_), $_ eq $char ? (class => 'optselected') : (), $_ eq 'all' ? 'ALL' : $_ ? uc $_ : '#'; - } - end; - if($uid) { - p class => 'browseopts'; - a href => $url->($char, 'vnlist'), $f->{vnlist} ? (class => 'optselected') : (), 'User VN list'; - end 'p'; - } - - p class => 'filselect'; - a id => 'filselect', href => '#v'; - lit '<i>▸</i> Visual Novel Filters<i></i>'; - end; - a id => 'rfilselect', href => '#r'; - lit '<i>▸</i> Release filters<i></i>'; - end; - a id => 'cfilselect', href => '#c'; - lit '<i>▸</i> Character filters<i></i>'; - end; - end; - input type => 'hidden', class => 'hidden', name => $_, id => $_, value => $f->{$_} - for (qw{fil rfil cfil s o}); - end; - end 'form'; - - $self->htmlBrowseVN($list, $f, $np, "/old/v/$char?q=$quri;fil=$f->{fil};rfil=$f->{rfil};cfil=$f->{cfil}", $f->{fil} =~ /tag_inc-/); - $self->htmlFooter(pref_code => 1); -} - - -sub _fil_compat { - my $self = shift; - my %c; - my $f = $self->formValidate( - { get => 'ln', required => 0, multi => 1, enum => [ keys %LANGUAGE ], default => '' }, - { get => 'pl', required => 0, multi => 1, enum => [ keys %PLATFORM ], default => '' }, - { get => 'sp', required => 0, default => ($self->reqCookie('tagspoil')||'') =~ /^([0-2])$/ ? $1 : 0, enum => [0..2] }, - ); - return () if $f->{_err}; - $c{lang} //= $f->{ln} if $f->{ln}[0]; - $c{plat} //= $f->{pl} if $f->{pl}[0]; - $c{tagspoil} //= $f->{sp}; - return %c; -} - - -1; - diff --git a/lib/VNDB/Handler/VNPage.pm b/lib/VNDB/Handler/VNPage.pm deleted file mode 100644 index 555903db..00000000 --- a/lib/VNDB/Handler/VNPage.pm +++ /dev/null @@ -1,293 +0,0 @@ - -package VNDB::Handler::VNPage; - -use strict; -use warnings; -use TUWF ':html'; -use VNDB::Func; -use VNDB::Types; - - -TUWF::register( - qr{old/v([1-9]\d*)/releases} => \&releases, -); - - -# Description of each column, field: -# id: Identifier used in URLs -# sort_field: Name of the field when sorting -# what: Required dbReleaseGet 'what' flag -# column_string: String to use as column header -# column_width: Maximum width (in pixels) of the column in 'restricted width' mode -# button_string: String to use for the hide/unhide button -# na_for_patch: When the field is N/A for patch releases -# default: Set when it's visible by default -# has_data: Subroutine called with a release object, should return true if the release has data for the column -# draw: Subroutine called with a release object, should draw its column contents -my @rel_cols = ( - { # Title - id => 'tit', - sort_field => 'title', - column_string => 'Title', - draw => sub { a href => "/r$_[0]{id}", shorten $_[0]{title}, 60 }, - }, { # Type - id => 'typ', - sort_field => 'type', - button_string => 'Type', - default => 1, - draw => sub { cssicon "rt$_[0]{type}", $_[0]{type}; txt '(patch)' if $_[0]{patch} }, - }, { # Languages - id => 'lan', - button_string => 'Language', - default => 1, - has_data => sub { !!@{$_[0]{languages}} }, - draw => sub { - for(@{$_[0]{languages}}) { - cssicon "lang $_", $LANGUAGE{$_}; - br if $_ ne $_[0]{languages}[$#{$_[0]{languages}}]; - } - }, - }, { # Publication - id => 'pub', - sort_field => 'publication', - column_string => 'Publication', - column_width => 70, - button_string => 'Publication', - default => 1, - what => 'extended', - draw => sub { txt join ', ', $_[0]{freeware} ? 'Freeware' : 'Non-free', $_[0]{patch} ? () : ($_[0]{doujin} ? 'doujin' : 'commercial') }, - }, { # Platforms - id => 'pla', - button_string => 'Platforms', - default => 1, - what => 'platforms', - has_data => sub { !!@{$_[0]{platforms}} }, - draw => sub { - for(@{$_[0]{platforms}}) { - cssicon $_, $PLATFORM{$_}; - br if $_ ne $_[0]{platforms}[$#{$_[0]{platforms}}]; - } - txt 'Unknown' if !@{$_[0]{platforms}}; - }, - }, { # Media - id => 'med', - column_string => 'Media', - button_string => 'Media', - what => 'media', - has_data => sub { !!@{$_[0]{media}} }, - draw => sub { - for(@{$_[0]{media}}) { - txt fmtmedia($_->{medium}, $_->{qty}); - br if $_ ne $_[0]{media}[$#{$_[0]{media}}]; - } - txt 'Unknown' if !@{$_[0]{media}}; - }, - }, { # Resolution - id => 'res', - sort_field => 'resolution', - column_string => 'Resolution', - button_string => 'Resolution', - na_for_patch => 1, - default => 1, - what => 'extended', - has_data => sub { !!$_[0]{reso_y} }, - draw => sub { txt resolution($_[0]) || 'Unknown' }, - }, { # Voiced - id => 'voi', - sort_field => 'voiced', - column_string => 'Voiced', - column_width => 70, - button_string => 'Voiced', - na_for_patch => 1, - default => 1, - what => 'extended', - has_data => sub { !!$_[0]{voiced} }, - draw => sub { txt $VOICED{$_[0]{voiced}}{txt} }, - }, { # Animation - id => 'ani', - sort_field => 'ani_ero', - column_string => 'Animation', - column_width => 110, - button_string => 'Animation', - na_for_patch => '1', - what => 'extended', - has_data => sub { !!($_[0]{ani_story} || $_[0]{ani_ero}) }, - draw => sub { - txt join ', ', - $_[0]{ani_story} ? "Story: $ANIMATED{$_[0]{ani_story}}{txt}" :(), - $_[0]{ani_ero} ? "Ero scenes: $ANIMATED{$_[0]{ani_ero}}{txt}":(); - txt 'Unknown' if !$_[0]{ani_story} && !$_[0]{ani_ero}; - }, - }, { # Released - id => 'rel', - sort_field => 'released', - column_string => 'Released', - button_string => 'Released', - default => 1, - draw => sub { lit fmtdatestr $_[0]{released} }, - }, { # Age rating - id => 'min', - sort_field => 'minage', - button_string => 'Age rating', - default => 1, - has_data => sub { $_[0]{minage} != -1 }, - draw => sub { txt minage $_[0]{minage} }, - }, { # Notes - id => 'not', - sort_field => 'notes', - column_string => 'Notes', - column_width => 400, - button_string => 'Notes', - default => 1, - what => 'extended', - has_data => sub { !!$_[0]{notes} }, - draw => sub { lit bb_format $_[0]{notes} }, - } -); - - -sub releases { - my($self, $vid) = @_; - - my $v = $self->dbVNGet(id => $vid)->[0]; - return $self->resNotFound if !$v->{id}; - - my $title = "Releases for $v->{title}"; - $self->htmlHeader(title => $title); - $self->htmlMainTabs('v', $v, 'releases'); - - my $f = $self->formValidate( - map({ get => $_->{id}, required => 0, default => $_->{default}||0, enum => [0,1] }, grep $_->{button_string}, @rel_cols), - { get => 'cw', required => 0, default => 0, enum => [0,1] }, - { get => 'o', required => 0, default => 0, enum => [0,1] }, - { get => 's', required => 0, default => 'released', enum => [ map $_->{sort_field}, grep $_->{sort_field}, @rel_cols ]}, - { get => 'os', required => 0, default => 'all', enum => [ 'all', keys %PLATFORM ] }, - { get => 'lang', required => 0, default => 'all', enum => [ 'all', keys %LANGUAGE ] }, - ); - return $self->resNotFound if $f->{_err}; - - # Get the release info - my %what = map +($_->{what}, 1), grep $_->{what} && $f->{$_->{id}}, @rel_cols; - my $r = $self->dbReleaseGet(vid => $vid, what => join(' ', keys %what), sort => $f->{s}, reverse => $f->{o}, results => 200); - - # url generator - my $url = sub { - my %u = (%$f, @_); - return "/v$vid/releases?".join(';', map "$_=$u{$_}", sort keys %u); - }; - - div class => 'mainbox releases_compare'; - h1 $title; - - if(!@$r) { - td 'We don\'t have any information about releases of this visual novel yet...'; - } else { - _releases_buttons($self, $f, $url, $r); - } - end 'div'; - - _releases_table($self, $f, $url, $r) if @$r; - $self->htmlFooter; -} - - -sub _releases_buttons { - my($self, $f, $url, $r) = @_; - - # Column visibility - p class => 'browseopts'; - a href => $url->($_->{id}, $f->{$_->{id}} ? 0 : 1), $f->{$_->{id}} ? (class => 'optselected') : (), $_->{button_string} - for (grep $_->{button_string}, @rel_cols); - end; - - # Misc options - my $all_selected = !grep $_->{button_string} && !$f->{$_->{id}}, @rel_cols; - my $all_unselected = !grep $_->{button_string} && $f->{$_->{id}}, @rel_cols; - my $all_url = sub { $url->(map +($_->{id},$_[0]), grep $_->{button_string}, @rel_cols); }; - p class => 'browseopts'; - a href => $all_url->(1), $all_selected ? (class => 'optselected') : (), 'All on'; - a href => $all_url->(0), $all_unselected ? (class => 'optselected') : (), 'All off'; - a href => $url->('cw', $f->{cw} ? 0 : 1), $f->{cw} ? (class => 'optselected') : (), 'Restrict column width'; - end; - - # Platform/language filters - my $plat_lang_draw = sub { - my($row, $option, $txt, $csscat) = @_; - my %opts = map +($_,1), map @{$_->{$row}}, @$r; - return if !keys %opts; - p class => 'browseopts'; - for('all', sort keys %opts) { - a href => $url->($option, $_), $_ eq $f->{$option} ? (class => 'optselected') : (); - $_ eq 'all' ? txt 'All' : cssicon "$csscat $_", $txt->{$_}; - end 'a'; - } - end 'p'; - }; - $plat_lang_draw->('platforms', 'os', \%PLATFORM, '') if $f->{pla}; - $plat_lang_draw->('languages', 'lang',\%LANGUAGE, 'lang') if $f->{lan}; -} - - -sub _releases_table { - my($self, $f, $url, $r) = @_; - - # Apply language and platform filters - my @r = grep + - ($f->{os} eq 'all' || ($_->{platforms} && grep $_ eq $f->{os}, @{$_->{platforms}})) && - ($f->{lang} eq 'all' || ($_->{languages} && grep $_ eq $f->{lang}, @{$_->{languages}})), @$r; - - # Figure out which columns to display - my @col; - for my $c (@rel_cols) { - next if $c->{button_string} && !$f->{$c->{id}}; # Hidden by settings - push @col, $c if !@r || !$c->{has_data} || grep $c->{has_data}->($_), @r; # Must have relevant data - } - - div class => 'mainbox releases_compare'; - table; - - thead; - Tr; - for my $c (@col) { - td class => 'key'; - txt $c->{column_string} if $c->{column_string}; - for($c->{sort_field} ? (0,1) : ()) { - my $active = $f->{s} eq $c->{sort_field} && !$f->{o} == !$_; - a href => $url->(o => $_, s => $c->{sort_field}) if !$active; - lit $_ ? "\x{25BE}" : "\x{25B4}"; - end 'a' if !$active; - } - end 'td'; - } - end 'tr'; - end 'thead'; - - for my $r (@r) { - Tr; - # Combine "N/A for patches" columns - my $cspan = 1; - for my $c (0..$#col) { - if($r->{patch} && $col[$c]{na_for_patch} && $c < $#col && $col[$c+1]{na_for_patch}) { - $cspan++; - next; - } - td $cspan > 1 ? (colspan => $cspan) : (), - $col[$c]{column_width} && $f->{cw} ? (style => "max-width: $col[$c]{column_width}px") : (); - if($r->{patch} && $col[$c]{na_for_patch}) { - txt 'NA for patches'; - } else { - $col[$c]{draw}->($r); - } - end; - $cspan = 1; - } - end; - } - end 'table'; - end 'div'; -} - - - -1; - diff --git a/lib/VNDB/Util/Auth.pm b/lib/VNDB/Util/Auth.pm deleted file mode 100644 index f3094ff0..00000000 --- a/lib/VNDB/Util/Auth.pm +++ /dev/null @@ -1,81 +0,0 @@ -# Compatibility shim around VNWeb::Auth, new code should use that instead. -package VNDB::Util::Auth; - - -use strict; -use warnings; -use Exporter 'import'; -use TUWF ':html'; -use VNWeb::Auth; - - -our @EXPORT = qw| - authInfo authCan authGetCode authCheckCode authPref -|; - - -sub authInfo { - # Used to return a lot more, but only the id is still used now. - # (code using other fields has been migrated) - +{ id => auth->uid } -} - - -# returns whether the currently loggedin or anonymous user can perform -# a certain action. -sub authCan { - my(undef, $act) = @_; - auth && auth->{user}{"perm_$act"} -} - - -# Generate a code to be used later on to validate that the form was indeed -# submitted from our site and by the same user/visitor. Not limited to -# logged-in users. -# Arguments: -# form-id (ignored nowadyas) -# time (also ignored) -sub authGetCode { - auth->csrftoken; -} - - -# Validates the correctness of the returned code, creates an error page and -# returns false if it's invalid, returns true otherwise. Codes are valid for at -# least two and at most three hours. -# Arguments: -# [ form-id, [ code ] ] -# If the code is not given, uses the 'formcode' form parameter instead. If -# form-id is not given, the path of the current requests is used. -sub authCheckCode { - my $self = shift; - my $id = shift; - my $code = shift || $self->reqParam('formcode'); - return _incorrectcode($self) if !auth->csrfcheck($code); - 1; -} - - -sub _incorrectcode { - my $self = shift; - $self->resInit; - $self->htmlHeader(title => 'Validation code expired', noindex => 1); - - div class => 'mainbox'; - h1 'Validation code expired'; - div class => 'warning'; - p 'Please hit the back-button of your browser, refresh the page and try again.'; - end; - end; - - $self->htmlFooter; - return 0; -} - - -sub authPref { - my(undef, $key, $val) = @_; - @_ == 2 ? auth->pref($key)||'' : auth->prefSet($key, $val); -} - -1; diff --git a/lib/VNDB/Util/BrowseHTML.pm b/lib/VNDB/Util/BrowseHTML.pm deleted file mode 100644 index 3eb460a6..00000000 --- a/lib/VNDB/Util/BrowseHTML.pm +++ /dev/null @@ -1,190 +0,0 @@ - -package VNDB::Util::BrowseHTML; - -use strict; -use warnings; -use TUWF ':html', 'xml_escape'; -use Exporter 'import'; -use VNDB::Func; -use VNDB::Types; -use POSIX 'ceil'; - - -our @EXPORT = qw| htmlBrowse htmlBrowseNavigate htmlBrowseVN |; - - -# generates a browse box, arguments: -# items => arrayref with the list items -# options => hashref containing at least the keys s (sort key), o (order) and p (page) -# nextpage => whether there's a next page or not -# sorturl => base URL to append the sort options to (if there are any sortable columns) -# pageurl => base URL to append the page option to -# class => classname of the mainbox -# header => -# can be either an arrayref or subroutine reference, -# in the case of a subroutine, it will be called when the header should be written, -# in the case of an arrayref, the array should contain the header items. Each item -# can again be either an arrayref or subroutine ref. The arrayref would consist of -# two elements: the name of the header, and the name of the sorting column if it can -# be sorted -# row => subroutine ref, which is called for each item in $list, arguments will be -# $self, $item_number (starting from 0), $item_value -# footer => subroutine ref, called after all rows have been processed -sub htmlBrowse { - my($self, %opt) = @_; - - $opt{sorturl} .= $opt{sorturl} =~ /\?/ ? ';' : '?' if $opt{sorturl}; - - # top navigation - $self->htmlBrowseNavigate($opt{pageurl}, $opt{options}{p}, $opt{nextpage}, 't') if $opt{pageurl}; - - div class => 'mainbox browse'.($opt{class} ? ' '.$opt{class} : ''); - table class => 'stripe'; - - # header - thead; - Tr; - if(ref $opt{header} eq 'CODE') { - $opt{header}->($self); - } else { - for(0..$#{$opt{header}}) { - if(ref $opt{header}[$_] eq 'CODE') { - $opt{header}[$_]->($self, $_+1); - } else { - td class => $opt{header}[$_][3]||'tc'.($_+1), $opt{header}[$_][2] ? (colspan => $opt{header}[$_][2]) : (); - lit $opt{header}[$_][0]; - if($opt{header}[$_][1]) { - lit ' '; - $opt{options}{s} eq $opt{header}[$_][1] && $opt{options}{o} eq 'a' ? lit "\x{25B4}" : a href => "$opt{sorturl}o=a;s=$opt{header}[$_][1]", "\x{25B4}"; - $opt{options}{s} eq $opt{header}[$_][1] && $opt{options}{o} eq 'd' ? lit "\x{25BE}" : a href => "$opt{sorturl}o=d;s=$opt{header}[$_][1]", "\x{25BE}"; - } - end; - } - } - } - end; - end 'thead'; - - # footer - if($opt{footer}) { - tfoot; - $opt{footer}->($self); - end; - } - - # rows - $opt{row}->($self, $_+1, $opt{items}[$_]) - for 0..$#{$opt{items}}; - - end 'table'; - end 'div'; - - # bottom navigation - $self->htmlBrowseNavigate($opt{pageurl}, $opt{options}{p}, $opt{nextpage}, 'b') if $opt{pageurl}; -} - - -# creates next/previous buttons (tabs), if needed -# Arguments: page url, current page (1..n), nextpage (0/1 or [$total, $perpage]), alignment (t/b), noappend (0/1) -sub htmlBrowseNavigate { - my($self, $url, $p, $np, $al, $na) = @_; - my($cnt, $pp) = ref($np) ? @$np : ($p+$np, 1); - return if $p == 1 && $cnt <= $pp; - - $url .= $url =~ /\?/ ? ';p=' : '?p=' unless $na; - - my $tab = sub { - my($page, $label) = @_; - li; - a href => $url.$page; lit $label; end; - end; - }; - my $ell = sub { - use utf8; - li class => 'ellipsis'; - b '⋯'; - end; - }; - my $nc = 5; # max. number of buttons on each side - - div class => 'maintabs browsetabs '.($al eq 't' ? '' : 'bottom'); - ul; - $p > 2 and ref $np and $tab->(1, '« first'); - $p > $nc+1 and ref $np and $ell->(); - $p > $_ and ref $np and $tab->($p-$_, $p-$_) for (reverse 2..($nc>$p-2?$p-2:$nc-1)); - $p > 1 and $tab->($p-1, '‹ previous'); - end; - - ul; - my $l = ceil($cnt/$pp)-$p+1; - $l > 1 and $tab->($p+1, 'next ›'); - $l > $_ and $tab->($p+$_, $p+$_) for (2..($nc>$l-2?$l-2:$nc-1)); - $l > $nc+1 and $ell->(); - $l > 2 and $tab->($l+$p-1, 'last »'); - end; - end 'div'; -} - - -sub htmlBrowseVN { - my($self, $list, $f, $np, $url, $tagscore) = @_; - $self->htmlBrowse( - class => 'vnbrowse', - items => $list, - options => $f, - nextpage => $np, - pageurl => "$url;o=$f->{o};s=$f->{s}", - sorturl => $url, - header => [ - $tagscore ? [ 'Score', 'tagscore', undef, 'tc_s' ] : (), - [ 'Title', 'title', undef, $tagscore ? 'tc_t' : 'tc1' ], - $f->{vnlist} ? [ '', 0, undef, 'tc7' ] : (), - $f->{wish} ? [ '', 0, undef, 'tc8' ] : (), - [ '', 0, undef, 'tc2' ], - [ '', 0, undef, 'tc3' ], - [ 'Released', 'rel', undef, 'tc4' ], - [ 'Popularity', 'pop', undef, 'tc5' ], - [ 'Rating', 'rating', undef, 'tc6' ], - ], - row => sub { - my($s, $n, $l) = @_; - Tr; - if($tagscore) { - td class => 'tc_s'; - VNWeb::TT::Lib::tagscore_($l->{tagscore}); - end; - } - td class => $tagscore ? 'tc_t' : 'tc1'; - a href => '/v'.$l->{id}, title => $l->{original}||$l->{title}, shorten $l->{title}, 100; - end; - if($f->{vnlist}) { - td class => 'tc7'; - lit sprintf '<b class="%s">%d/%d</b>', $l->{userlist_obtained} == $l->{userlist_all} ? 'done' : 'todo', $l->{userlist_obtained}, $l->{userlist_all} if $l->{userlist_all}; - abbr title => join(', ', $l->{vnlist_labels}->@*), scalar $l->{vnlist_labels}->@* if $l->{vnlist_labels} && $l->{vnlist_labels}->@*; - abbr title => 'No labels', ' ' if $l->{vnlist_labels} && !$l->{vnlist_labels}->@*; - end 'td'; - } - td class => 'tc2'; - $_ ne 'oth' && cssicon $_, $PLATFORM{$_} - for (sort @{$l->{c_platforms}}); - end; - td class => 'tc3'; - cssicon "lang $_", $LANGUAGE{$_} - for (reverse sort @{$l->{c_languages}}); - end; - td class => 'tc4'; - lit fmtdatestr $l->{c_released}; - end; - td class => 'tc5', sprintf '%.2f', ($l->{c_popularity}||0)*100; - td class => 'tc6'; - txt sprintf '%.2f', ($l->{c_rating}||0)/10; - b class => 'grayedout', sprintf ' (%d)', $l->{c_votecount}; - end; - end 'tr'; - }, - ); -} - - -1; - diff --git a/lib/VNDB/Util/CommonHTML.pm b/lib/VNDB/Util/CommonHTML.pm deleted file mode 100644 index 85722f1b..00000000 --- a/lib/VNDB/Util/CommonHTML.pm +++ /dev/null @@ -1,34 +0,0 @@ - -package VNDB::Util::CommonHTML; - -use strict; -use warnings; -use Exporter 'import'; -use VNDB::Func; - -our @EXPORT = qw| - htmlMainTabs htmlDenied htmlSearchBox -|; - - -# generates the "main tabs". These are the commonly used tabs for -# 'objects', i.e. VN/producer/release entries and users -# Arguments: u/v/r/p/g/i/c/d, object, currently selected item (empty=main) -sub htmlMainTabs { - my($self, $type, $obj, $sel) = @_; - $obj->{entry_hidden} = $obj->{hidden}; - $obj->{entry_locked} = $obj->{locked}; - VNWeb::HTML::_maintabs_({ type => $type, dbobj => $obj, tab => $sel||''}); -} - - -# generates a full error page, including header and footer -sub htmlDenied { shift->resDenied } - - -sub htmlSearchBox { - shift; VNWeb::HTML::searchbox_(@_); -} - - -1; diff --git a/lib/VNDB/Util/LayoutHTML.pm b/lib/VNDB/Util/LayoutHTML.pm deleted file mode 100644 index a9e0c05f..00000000 --- a/lib/VNDB/Util/LayoutHTML.pm +++ /dev/null @@ -1,44 +0,0 @@ - -package VNDB::Util::LayoutHTML; - -use strict; -use warnings; -use TUWF ':html'; -use VNDB::Config; -use VNWeb::HTML; -use Exporter 'import'; - -our @EXPORT = qw|htmlHeader htmlFooter|; - -sub htmlHeader { # %options->{ title, noindex, search, feeds, metadata } - my($self, %o) = @_; - %VNWeb::HTML::pagevars = (); - - $o{og} = $o{metadata} ? +{ map +(s/og://r, $o{metadata}{$_}), keys $o{metadata}->%* } : undef; - $o{index} = !$o{noindex}; - - html lang => 'en'; - head sub { VNWeb::HTML::_head_(\%o) }; - body; - div id => 'bgright', ' '; - div id => 'header', sub { h1 sub { a href => '/', 'the visual novel database' } }; - div id => 'menulist', sub { VNWeb::HTML::_menu_(\%o) }; - div id => 'maincontent'; -} - - -sub htmlFooter { # %options => { pref_code => 1 } - my($self, %o) = @_; - div id => 'footer', sub { VNWeb::HTML::_footer_ }; - end 'div'; # maincontent - - # Abuse an empty noscript tag for the formcode to update a preference setting, if the page requires one. - noscript id => 'pref_code', title => $self->authGetCode('/xml/prefs.xml'), '' - if $o{pref_code} && $self->authInfo->{id}; - script type => 'text/javascript', src => config->{url_static}.'/g/vndb.js?'.config->{version}, ''; - VNWeb::HTML::_scripts_({}); - end 'body'; - end 'html'; -} - -1; diff --git a/lib/VNDB/Util/Misc.pm b/lib/VNDB/Util/Misc.pm deleted file mode 100644 index 6342c0c5..00000000 --- a/lib/VNDB/Util/Misc.pm +++ /dev/null @@ -1,93 +0,0 @@ - -package VNDB::Util::Misc; - -use strict; -use warnings; -use Exporter 'import'; -use TUWF ':html'; -use VNDB::Func; -use VNDB::Types; - -our @EXPORT = qw|filFetchDB filCompat|; - - -our %filfields = ( - vn => [qw|date_before date_after released length hasani hasshot tag_inc tag_exc taginc tagexc tagspoil lang olang plat staff_inc staff_exc ul_notblack ul_onwish ul_voted ul_onlist|], - release => [qw|type patch freeware doujin uncensored date_before date_after released minage lang olang resolution plat prod_inc prod_exc med voiced ani_story ani_ero engine|], - char => [qw|gender bloodt bust_min bust_max waist_min waist_max hip_min hip_max height_min height_max va_inc va_exc weight_min weight_max cup_min cup_max trait_inc trait_exc tagspoil role|], - staff => [qw|gender role truename lang|], -); - - -# Arguments: -# type ('vn', 'release' or 'char'), -# filter overwrite (string or undef), -# when defined, these filters will be used instead of the preferences, -# must point to a variable, will be modified in-place with the actually used filters -# options to pass to db*Get() before the filters (hashref or undef) -# these options can be overwritten by the filters or the next option -# options to pass to db*Get() after the filters (hashref or undef) -# these options overwrite all other options (pre-options and filters) - -sub filFetchDB { - my($self, $type, $overwrite, $pre, $post) = @_; - $pre = {} if !$pre; - $post = {} if !$post; - my $dbfunc = $self->can($type eq 'vn' ? 'dbVNGet' : $type eq 'release' ? 'dbReleaseGet' : $type eq 'char' ? 'dbCharGet' : 'dbStaffGet'); - my $prefname = 'filter_'.$type; - my $pref = $self->authPref($prefname); - - my $filters = fil_parse $overwrite // $pref, @{$filfields{$type}}; - - VNWeb::Filters::debug_validate($type, $filters); - - # compatibility - my $compat = $self->filCompat($type, $filters); - $self->authPref($prefname => fil_serialize $filters) if $compat && !defined $overwrite; - - # write the definite filter string in $overwrite - $_[2] = fil_serialize({map +( - exists($post->{$_}) ? ($_ => $post->{$_}) : - exists($filters->{$_}) ? ($_ => $filters->{$_}) : - exists($pre->{$_}) ? ($_ => $pre->{$_}) : (), - ), @{$filfields{$type}}}) if defined $overwrite; - - return $dbfunc->($self, %$pre, %$filters, %$post) if defined $overwrite or !keys %$filters;; - - # since incorrect filters can throw a database error, we have to special-case - # filters that originate from a preference setting, so that in case these are - # the cause of an error, they are removed. Not doing this will result in VNDB - # throwing 500's even for non-browse pages. We have to do some low-level - # PostgreSQL stuff with savepoints to ensure that an error won't affect our - # existing transaction. - my $dbh = $self->dbh; - $dbh->pg_savepoint('filter'); - my($r, $np); - my $OK = eval { - ($r, $np) = $dbfunc->($self, %$pre, %$filters, %$post); - 1; - }; - $dbh->pg_rollback_to('filter') if !$OK; - $dbh->pg_release('filter'); - - # error occured, let's try again without filters. if that succeeds we know - # it's the fault of the filter preference, and we should remove it. - if(!$OK) { - ($r, $np) = $dbfunc->($self, %$pre, %$post); - # if we're here, it means the previous function didn't die() (duh!) - $self->authPref($prefname => ''); - warn sprintf "Reset filter preference for userid %d. Old: %s\n", $self->authInfo->{id}||0, $pref; - } - return wantarray ? ($r, $np) : $r; -} - - -# Compatibility with old filters. Modifies the filter in-place and returns the number of changes made. -sub filCompat { - my($self, $type, $fil) = @_; - $type eq 'vn' ? VNWeb::Filters::filter_vn_compat($fil) : 0 -} - - -1; - diff --git a/lib/VNDB/Util/ValidateTemplates.pm b/lib/VNDB/Util/ValidateTemplates.pm deleted file mode 100644 index e28abcb2..00000000 --- a/lib/VNDB/Util/ValidateTemplates.pm +++ /dev/null @@ -1,16 +0,0 @@ -# This module implements various templates for formValidate() - -package VNDB::Util::ValidateTemplates; - -use strict; -use warnings; - - -TUWF::set( - validate_templates => { - id => { template => 'uint', max => 1<<40 }, - page => { template => 'uint', max => 1000 }, - } -); - -1; diff --git a/lib/VNWeb/Filters.pm b/lib/VNWeb/Filters.pm index 73f37e99..429a10f5 100644 --- a/lib/VNWeb/Filters.pm +++ b/lib/VNWeb/Filters.pm @@ -1,13 +1,13 @@ package VNWeb::Filters; -# This module implements validating and querying the old search filters. These -# filters are replaced with the new AdvSearch framework and this code only -# exists to convert old URLs. +# This module implements validating old search filters and converting them to +# the new AdvSearch system. It only exists for compatibility with old URLs. -use VNWeb::Prelude; +use TUWF; +use VNWeb::Validation; use Exporter 'import'; -our @EXPORT = qw/filter_parse filter_vn_query filter_release_query filter_vn_adv filter_release_adv filter_char_adv filter_staff_adv/; +our @EXPORT = qw/filter_parse filter_vn_adv filter_release_adv filter_char_adv filter_staff_adv/; my $VN = form_compile any => { @@ -87,17 +87,6 @@ my $STAFF = form_compile any => { }; -sub debug_validate { - my($type, $data) = @_; - my $s = {vn => $VN, release => $RELEASE, char => $CHAR, staff => $STAFF}->{$type}; - my $v = $s->validate($data); - if(!$v) { - warn sprintf "Filter validation failed!\nData: %s\nError: %s", JSON::XS->new->canonical->pretty->encode($data), JSON::XS->new->canonical->pretty->encode($v->err); - } else { - #warn sprintf "Filter validated: %sSerialized: %s", JSON::XS->new->canonical->pretty->encode($v->data), VNDB::Func::fil_serialize($v->data); - } -} - # Compatibility with old VN filters. Modifies the filter in-place and returns the number of changes made. sub filter_vn_compat { @@ -131,12 +120,29 @@ sub filter_release_compat { } + +my @fil_escape = split //, '_ !"#$%&\'()*+,-./:;<=>?@[\]^`{}~'; + +sub _fil_parse { + my $str = shift; + my %r; + for (split /\./, $str) { + next if !/^([a-z0-9_]+)-([a-zA-Z0-9_~\x81-\x{ffffff}]+)$/; + my($f, $v) = ($1, $2); + my @v = split /~/, $v; + s/_([0-9]{2})/$1 > $#fil_escape ? '' : $fil_escape[$1]/eg for(@v); + $r{$f} = @v > 1 ? \@v : $v[0] + } + return \%r; +} + + # Throws error on failure. sub filter_parse { my($type, $str) = @_; return {} if !$str; my $s = {v => $VN, r => $RELEASE, c => $CHAR, s => $STAFF}->{$type}; - my $data = ref $str ? $str : $str =~ /^{/ ? JSON::XS->new->decode($str) : VNDB::Func::fil_parse $str, keys $s->{known_keys}->%*; + my $data = ref $str ? $str : $str =~ /^{/ ? JSON::XS->new->decode($str) : _fil_parse $str; die "Invalid filter data: $str\n" if !$data; my $f = $s->validate($data)->data; filter_vn_compat $f if $type eq 'v'; @@ -145,67 +151,6 @@ sub filter_parse { } -# Returns an SQL expression for use in a WHERE clause. Assumption: 'v' is an alias to the vn table being queried. -sub filter_vn_query { - my($fil) = @_; - sql_and - defined $fil->{date_before} ? sql 'v.c_released <=', \$fil->{date_before} : (), - defined $fil->{date_after} ? sql 'v.c_released >=', \$fil->{date_after} : (), - defined $fil->{released} ? sql 'v.c_released', $fil->{released} ? '<=' : '>', \strftime('%Y%m%d', gmtime) : (), - defined $fil->{length} ? sql 'v.length IN', $fil->{length} : (), - defined $fil->{hasani} ? sql($fil->{hasani} ?'':'NOT', 'EXISTS(SELECT 1 FROM vn_anime iva WHERE iva.id = v.id)') : (), - defined $fil->{hasshot} ? sql($fil->{hasshot}?'':'NOT', 'EXISTS(SELECT 1 FROM vn_screenshots ivs WHERE ivs.id = v.id)') : (), - defined $fil->{tag_inc} ? sql - 'v.id IN(SELECT vid FROM tags_vn_inherit WHERE tag IN', $fil->{tag_inc}, 'AND spoiler <=', \$fil->{tagspoil}, 'GROUP BY vid HAVING COUNT(tag) =', scalar $fil->{tag_inc}->@*, ')' : (), - defined $fil->{tag_exc} ? sql 'v.id NOT IN(SELECT vid FROM tags_vn_inherit WHERE tag IN', $fil->{tag_exc}, ')' : (), - defined $fil->{lang} ? sql 'v.c_languages && ARRAY', $fil->{lang}, '::language[]' : (), - defined $fil->{olang} ? sql 'v.c_olang && ARRAY', $fil->{olang}, '::language[]' : (), - defined $fil->{plat} ? sql 'v.c_platforms && ARRAY', $fil->{plat}, '::platform[]' : (), - defined $fil->{staff_inc} ? sql 'v.id IN(SELECT ivs.id FROM vn_staff ivs JOIN staff_alias isa ON isa.aid = ivs.aid WHERE isa.id IN', $fil->{staff_inc}, ')' : (), - defined $fil->{staff_exc} ? sql 'v.id NOT IN(SELECT ivs.id FROM vn_staff ivs JOIN staff_alias isa ON isa.aid = ivs.aid WHERE isa.id IN', $fil->{staff_exc}, ')' : (), - auth ? ( - # TODO: onwish, voted and onlist should respect the label filters in users.ulist_* - defined $fil->{ul_notblack} ? sql 'v.id NOT IN(SELECT vid FROM ulist_vns_labels WHERE uid =', \auth->uid, 'AND lbl =', \6, ')' : (), - defined $fil->{ul_onwish} ? sql 'v.id', $fil->{ul_onwish}?'':'NOT', 'IN(SELECT vid FROM ulist_vns_labels WHERE uid =', \auth->uid, 'AND lbl =', \5, ')' : (), - defined $fil->{ul_voted} ? sql 'v.id', $fil->{ul_voted} ?'':'NOT', 'IN(SELECT vid FROM ulist_vns_labels WHERE uid =', \auth->uid, 'AND lbl =', \7, ')' : (), - defined $fil->{ul_onlist} ? sql 'v.id', $fil->{ul_onlist}?'':'NOT', 'IN(SELECT vid FROM ulist_vns WHERE uid =', \auth->uid, ')' : (), - ) : (), -} - - -# Assumption: 'r' is an alias to the release table being queried. -sub filter_release_query { - my($fil) = @_; - sql_and - defined $fil->{type} ? sql 'r.type =', \$fil->{type} : (), - defined $fil->{patch} ? sql($fil->{patch} ?'':'NOT', 'r.patch' ) : (), - defined $fil->{freeware} ? sql($fil->{freeware} ?'':'NOT', 'r.freeware' ) : (), - defined $fil->{doujin} ? sql($fil->{doujin} ?'':'NOT', 'r.doujin AND NOT r.patch') : (), - defined $fil->{uncensored} ? sql($fil->{uncensored}?'':'NOT', 'r.uncensored') : (), - defined $fil->{date_before} ? sql 'r.released <=', \$fil->{date_before} : (), - defined $fil->{date_after} ? sql 'r.released >=', \$fil->{date_after} : (), - defined $fil->{released} ? sql 'r.released', $fil->{released} ? '<=' : '>', \strftime('%Y%m%d', gmtime) : (), - defined $fil->{minage} ? sql 'r.minage IN', $fil->{minage} : (), - defined $fil->{lang} ? sql 'r.id IN(SELECT irl.id FROM releases_lang irl WHERE irl.lang IN', $fil->{lang}, ')' : (), - defined $fil->{olang} ? sql 'r.id IN(SELECT irv.id FROM releases_vn irv JOIN vn iv ON irv.vid = iv.id WHERE iv.c_olang && ARRAY', $fil->{olang}, '::language[])' : (), - defined $fil->{resolution} ? sql 'NOT r.patch AND ARRAY[r.reso_x,r.reso_y] IN', [ map $_ eq 'unknown' ? '{0,0}' : $_ eq 'nonstandard' ? '{0,1}' : '{'.(s/x/,/r).'}', $fil->{resolution}->@* ] : (), - defined $fil->{plat} ? sql_or( - grep( /^unk$/, $fil->{plat}->@*) ? sql 'NOT EXISTS(SELECT 1 FROM releases_platforms irp WHERE irp.id = r.id)' : (), - grep(!/^unk$/, $fil->{plat}->@*) ? sql 'r.id IN(SELECT irp.id FROM releases_platforms irp WHERE irp.platform IN', [grep !/^unk$/, $fil->{plat}->@*], ')' : (), - ) : (), - defined $fil->{prod_inc} ? sql 'r.id IN(SELECT irp.id FROM releases_producers irp WHERE irp.pid IN', $fil->{prod_inc}, ')' : (), - defined $fil->{prod_exc} ? sql 'r.id NOT IN(SELECT irp.id FROM releases_producers irp WHERE irp.pid IN', $fil->{prod_exc}, ')' : (), - defined $fil->{med} ? sql_or( - grep( /^unk$/, $fil->{med}->@*) ? sql 'NOT EXISTS(SELECT 1 FROM releases_media irm WHERE irm.id = r.id)' : (), - grep(!/^unk$/, $fil->{med}->@*) ? sql 'r.id IN(SELECT irm.id FROM releases_media irm WHERE irm.medium IN', [grep !/^unk$/, $fil->{med}->@*], ')' : (), - ) : (), - defined $fil->{voiced} ? sql 'NOT r.patch AND r.voiced IN', $fil->{voiced} : (), - defined $fil->{ani_story} ? sql 'NOT r.patch AND r.ani_story IN', $fil->{ani_story} : (), - defined $fil->{ani_ero} ? sql 'NOT r.patch AND r.ani_ero IN', $fil->{ani_ero} : (), - defined $fil->{engine} ? sql 'r.engine =', \$fil->{engine} : (), -} - - sub filter_vn_adv { my($fil) = @_; [ 'and', diff --git a/lib/VNWeb/HTML.pm b/lib/VNWeb/HTML.pm index a52aacce..7d8751c3 100644 --- a/lib/VNWeb/HTML.pm +++ b/lib/VNWeb/HTML.pm @@ -473,17 +473,6 @@ sub _hidden_msg_ { } -sub _scripts_ { - my($o) = @_; - script_ type => 'application/json', id => 'pagevars', sub { - # Escaping rules for a JSON <script> context are kinda weird, but more efficient than regular xml_escape(). - lit_(JSON::XS->new->canonical->encode(tuwf->req->{pagevars}) =~ s{</}{<\\/}rg =~ s/<!--/<\\u0021--/rg); - } if keys tuwf->req->{pagevars}->%*; - script_ type => 'application/javascript', src => config->{url_static}.'/g/elm.js?'.config->{version}, '' if tuwf->req->{pagevars}{elm}; - script_ type => 'application/javascript', src => config->{url_static}.'/g/plain.js?'.config->{version}, '' if tuwf->req->{js} || tuwf->req->{pagevars}{elm}; -} - - # Options: # title => $title # index => 1/0, default 0 @@ -515,7 +504,12 @@ sub framework_ { $cont->() unless $o{hiddenmsg} && _hidden_msg_ \%o; div_ id => 'footer', \&_footer_; }; - _scripts_ \%o; + script_ type => 'application/json', id => 'pagevars', sub { + # Escaping rules for a JSON <script> context are kinda weird, but more efficient than regular xml_escape(). + lit_(JSON::XS->new->canonical->encode(tuwf->req->{pagevars}) =~ s{</}{<\\/}rg =~ s/<!--/<\\u0021--/rg); + } if keys tuwf->req->{pagevars}->%*; + script_ type => 'application/javascript', src => config->{url_static}.'/g/elm.js?'.config->{version}, '' if tuwf->req->{pagevars}{elm}; + script_ type => 'application/javascript', src => config->{url_static}.'/g/plain.js?'.config->{version}, '' if tuwf->req->{js} || tuwf->req->{pagevars}{elm}; } } } diff --git a/util/jsgen.pl b/util/jsgen.pl deleted file mode 100755 index a4023ff8..00000000 --- a/util/jsgen.pl +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/perl - -use strict; -use warnings; -use Encode 'encode_utf8'; -use Cwd 'abs_path'; -use JSON::XS; - -my $ROOT; -BEGIN { ($ROOT = abs_path $0) =~ s{/util/jsgen\.pl$}{}; } - -use lib "$ROOT/lib"; -use VNDB::Config; -use VNDB::Types; - - -sub vars { - my %vars = ( - rlist_status => [ map [ $_, $RLIST_STATUS{$_} ], keys %RLIST_STATUS ], - cookie_prefix => config->{tuwf}{cookie_prefix}, - age_ratings => [ map [ $_, $AGE_RATING{$_}{txt}], keys %AGE_RATING ], - languages => [ map [ $_, $LANGUAGE{$_} ], sort { $LANGUAGE{$a} cmp $LANGUAGE{$b} } keys %LANGUAGE ], - platforms => [ map [ $_, $PLATFORM{$_} ], keys %PLATFORM ], - char_roles => [ map [ $_, $CHAR_ROLE{$_}{txt} ], keys %CHAR_ROLE ], - media => [ map [ $_, $MEDIUM{$_}{txt}, $MEDIUM{$_}{qty} ], keys %MEDIUM ], - release_types => [ map [ $_, $RELEASE_TYPE{$_} ], keys %RELEASE_TYPE ], - animated => [ map [ $_, $ANIMATED{$_}{txt} ], keys %ANIMATED ], - voiced => [ map [ $_, $VOICED{$_}{txt} ], keys %VOICED ], - vn_lengths => [ map [ $_, $VN_LENGTH{$_}{txt} ], keys %VN_LENGTH ], - blood_types => [ map [ $_, $BLOOD_TYPE{$_} ], keys %BLOOD_TYPE ], - genders => [ map [ $_, $GENDER{$_} ], keys %GENDER ], - credit_type => [ map [ $_, $CREDIT_TYPE{$_} ], keys %CREDIT_TYPE ], - cup_size => [ grep $_, keys %CUP_SIZE ], - ); - JSON::XS->new->encode(\%vars); -} - - -# Reads main.js and any included files. -sub readjs { - my $f = shift || 'main.js'; - open my $JS, '<:utf8', "$ROOT/data/js/$f" or die $!; - local $/ = undef; - local $_ = <$JS>; - close $JS; - s{^//include (.+)$}{'(function(){'.readjs($1).'})();'}meg; - $_; -} - - -sub save { - my($f, $body) = @_; - open my $F, '>', "$f~" or die $!; - print $F encode_utf8($body); - close $F; - rename "$f~", $f or die $!; -} - - -my $js = readjs; -$js =~ s{/\*VARS\*/}{vars()}eg; -save "$ROOT/static/g/vndb.js", $js; diff --git a/util/sql b/util/sql deleted file mode 120000 index 44657b95..00000000 --- a/util/sql +++ /dev/null @@ -1 +0,0 @@ -../sql
\ No newline at end of file diff --git a/util/vndb.pl b/util/vndb.pl index 29ef026b..f7c94044 100755 --- a/util/vndb.pl +++ b/util/vndb.pl @@ -91,27 +91,22 @@ sub TUWF::Object::resDenied { # Intercept TUWF::any() and TUWF::register() to figure out which module is processing the request. if(config->{trace_log}) { - my sub wrap { - my $f = shift; - sub { - my $i = 0; - my $loc = ['',0]; - while(my($pack, undef, $line) = caller($i++)) { - if($pack !~ '^(?:main|TUWF|VNWeb::Elm)') { - $loc = [$pack,$line]; - last; - } + no warnings 'redefine'; + my $f = \&TUWF::any; + *TUWF::any = sub { + my($meth, $path, $sub) = @_; + my $i = 0; + my $loc = ['',0]; + while(my($pack, undef, $line) = caller($i++)) { + if($pack !~ '^(?:main|TUWF|VNWeb::Elm)') { + $loc = [$pack,$line]; + last; } - my sub subwrap { my $sub = shift; sub { tuwf->req->{trace_loc} = $loc; $sub->(@_) } } - $f->(map ref($_) eq 'CODE' ? subwrap($_) : $_, @_) } - } - no warnings 'redefine'; - my $x = \&TUWF::register; *TUWF::register = wrap($x); - my $y = \&TUWF::any; *TUWF::any = wrap($y); + $f->($meth, $path, sub { tuwf->req->{trace_loc} = $loc; $sub->(@_) }); + }; } -TUWF::load_recursive('VNDB::Util', 'VNDB::DB', 'VNDB::Handler'); TUWF::set import_modules => 0; TUWF::load_recursive('VNWeb'); |