summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2019-12-30 15:20:00 +0100
committerYorhel <git@yorhel.nl>2019-12-30 15:20:00 +0100
commitd1bb5b82255c764edecc659c78d5b9f4e36555e8 (patch)
tree53058ac9a25d0e82968da77e28f46d1137204e6b
parent13287329e70cbaf155c85e3054f2496411e21b21 (diff)
parentddb0d385eeb112de6e544adefbbac1cb0b8a957a (diff)
Merge branch 'ulist'
-rw-r--r--Makefile49
-rw-r--r--README.md22
-rw-r--r--css3/framework/base.css84
-rw-r--r--css3/framework/elements.css962
-rw-r--r--css3/framework/grid.css102
-rw-r--r--css3/framework/helpers.css127
-rw-r--r--css3/vndb.css1838
-rw-r--r--data/js/misc.js15
-rw-r--r--data/style.css12
-rw-r--r--elm/ColSelect.elm6
-rw-r--r--elm/ColSelect.js5
-rw-r--r--elm/UList/LabelEdit.elm8
-rw-r--r--elm/UList/VNPage.elm127
-rw-r--r--elm/UList/VoteEdit.elm3
-rw-r--r--elm/User/Edit.elm8
-rw-r--r--elm3/CharEdit/General.elm260
-rw-r--r--elm3/CharEdit/Main.elm130
-rw-r--r--elm3/CharEdit/New.elm19
-rw-r--r--elm3/CharEdit/Traits.elm81
-rw-r--r--elm3/CharEdit/VN.elm186
-rw-r--r--elm3/DocEdit.elm107
-rw-r--r--elm3/Lib/Api.elm110
-rw-r--r--elm3/Lib/Autocomplete.elm292
-rw-r--r--elm3/Lib/Editsum.elm59
-rw-r--r--elm3/Lib/Ffi.elm29
-rw-r--r--elm3/Lib/Html.elm182
-rw-r--r--elm3/Lib/RDate.elm84
-rw-r--r--elm3/Lib/Util.elm76
-rw-r--r--elm3/Lightbox.elm178
-rw-r--r--elm3/ProdEdit/General.elm78
-rw-r--r--elm3/ProdEdit/Main.elm158
-rw-r--r--elm3/ProdEdit/Names.elm81
-rw-r--r--elm3/ProdEdit/New.elm12
-rw-r--r--elm3/ProdEdit/Relations.elm79
-rw-r--r--elm3/RelEdit/General.elm272
-rw-r--r--elm3/RelEdit/Main.elm137
-rw-r--r--elm3/RelEdit/New.elm19
-rw-r--r--elm3/RelEdit/Producers.elm92
-rw-r--r--elm3/RelEdit/Vn.elm77
-rw-r--r--elm3/StaffEdit/Main.elm220
-rw-r--r--elm3/StaffEdit/New.elm12
-rw-r--r--elm3/UVNList/Options.elm37
-rw-r--r--elm3/UVNList/Status.elm77
-rw-r--r--elm3/UVNList/Vote.elm103
-rw-r--r--elm3/User/Login.elm86
-rw-r--r--elm3/User/PassReset.elm90
-rw-r--r--elm3/User/PassSet.elm94
-rw-r--r--elm3/User/Register.elm108
-rw-r--r--elm3/User/Settings.elm206
-rw-r--r--elm3/VNEdit/General.elm155
-rw-r--r--elm3/VNEdit/Main.elm199
-rw-r--r--elm3/VNEdit/New.elm12
-rw-r--r--elm3/VNEdit/Relations.elm89
-rw-r--r--elm3/VNEdit/Screenshots.elm182
-rw-r--r--elm3/VNEdit/Seiyuu.elm104
-rw-r--r--elm3/VNEdit/Staff.elm95
-rw-r--r--elm3/VNEdit/Titles.elm103
-rw-r--r--elm3/elm.json30
-rw-r--r--lib/Multi/API.pm119
-rw-r--r--lib/Multi/Maintenance.pm41
-rw-r--r--lib/VN3/BBCode.pm300
-rw-r--r--lib/VN3/Char/Edit.pm168
-rw-r--r--lib/VN3/Char/JS.pm55
-rw-r--r--lib/VN3/Char/Page.pm330
-rw-r--r--lib/VN3/DB.pm287
-rw-r--r--lib/VN3/Docs/Edit.pm54
-rw-r--r--lib/VN3/Docs/JS.pm15
-rw-r--r--lib/VN3/Docs/Lib.pm86
-rw-r--r--lib/VN3/Docs/Page.pm23
-rw-r--r--lib/VN3/ElmGen.pm197
-rw-r--r--lib/VN3/HTML.pm375
-rw-r--r--lib/VN3/Misc/Homepage.pm31
-rw-r--r--lib/VN3/Misc/ImageUpload.pm70
-rw-r--r--lib/VN3/Prelude.pm104
-rw-r--r--lib/VN3/Producer/Edit.pm136
-rw-r--r--lib/VN3/Producer/JS.pm47
-rw-r--r--lib/VN3/Producer/Page.pm117
-rw-r--r--lib/VN3/Release/Edit.pm130
-rw-r--r--lib/VN3/Release/JS.pm32
-rw-r--r--lib/VN3/Release/Page.pm184
-rw-r--r--lib/VN3/Staff/Edit.pm108
-rw-r--r--lib/VN3/Staff/JS.pm43
-rw-r--r--lib/VN3/Staff/Page.pm213
-rw-r--r--lib/VN3/Trait/JS.pm44
-rw-r--r--lib/VN3/Types.pm171
-rw-r--r--lib/VN3/User/Lib.pm31
-rw-r--r--lib/VN3/User/Login.pm50
-rw-r--r--lib/VN3/User/Page.pm207
-rw-r--r--lib/VN3/User/RegReset.pm137
-rw-r--r--lib/VN3/User/Settings.pm98
-rw-r--r--lib/VN3/User/VNList.pm325
-rw-r--r--lib/VN3/VN/Edit.pm187
-rw-r--r--lib/VN3/VN/JS.pm46
-rw-r--r--lib/VN3/VN/Lib.pm20
-rw-r--r--lib/VN3/VN/Page.pm631
-rw-r--r--lib/VN3/Validation.pm168
-rw-r--r--lib/VNDB/DB/ULists.pm292
-rw-r--r--lib/VNDB/DB/VN.pm23
-rw-r--r--lib/VNDB/Handler/ULists.pm482
-rw-r--r--lib/VNDB/Handler/VNBrowse.pm7
-rw-r--r--lib/VNDB/Handler/VNPage.pm53
-rw-r--r--lib/VNDB/Types.pm17
-rw-r--r--lib/VNDB/Util/BrowseHTML.pm3
-rw-r--r--lib/VNDB/Util/CommonHTML.pm20
-rw-r--r--lib/VNDB/Util/LayoutHTML.pm2
-rw-r--r--lib/VNWeb/Auth.pm2
-rw-r--r--lib/VNWeb/HTML.pm34
-rw-r--r--lib/VNWeb/User/Edit.pm5
-rw-r--r--lib/VNWeb/User/List.pm37
-rw-r--r--lib/VNWeb/User/Lists.pm174
-rw-r--r--lib/VNWeb/User/Page.pm70
-rw-r--r--lib/VNWeb/VN/Votes.pm69
-rw-r--r--static/v3/apple.svg1
-rw-r--r--static/v3/bell.svg1
-rw-r--r--static/v3/camera-alt.svg1
-rw-r--r--static/v3/edit.svg1
-rw-r--r--static/v3/external-link-square-alt.svg1
-rw-r--r--static/v3/globe.svg1
-rw-r--r--static/v3/heavy/comment.svg1
-rw-r--r--static/v3/heavy/random.svg1
-rw-r--r--static/v3/heavy/search.svg1
-rw-r--r--static/v3/linux.svg1
-rw-r--r--static/v3/nsfw.svg8
-rw-r--r--static/v3/plus-circle.svg1
-rw-r--r--static/v3/plus.svg1
-rw-r--r--static/v3/popularity.svg9
-rw-r--r--static/v3/star.svg9
-rw-r--r--static/v3/vndb.js420
-rw-r--r--static/v3/windows.svg1
-rwxr-xr-xutil/dbdump.pl53
-rwxr-xr-xutil/devdump.pl14
-rwxr-xr-xutil/docker-init.sh9
-rw-r--r--util/sql/func.sql161
-rw-r--r--util/sql/perms.sql27
-rw-r--r--util/sql/schema.sql69
-rw-r--r--util/sql/tableattrs.sql25
-rw-r--r--util/updates/update_wip_lists.sql47
-rwxr-xr-xutil/vndb-dev-server.pl7
-rwxr-xr-xutil/vndb3.pl74
139 files changed, 784 insertions, 14999 deletions
diff --git a/Makefile b/Makefile
index 24f71507..c50a5cda 100644
--- a/Makefile
+++ b/Makefile
@@ -24,7 +24,7 @@
ALL_KEEP=\
static/ch static/cv static/sf static/st \
- data/log static/f static/v3 www www/feeds www/api \
+ data/log static/f www www/feeds www/api \
data/conf.pl \
www/robots.txt static/robots.txt
@@ -32,15 +32,10 @@ ALL_CLEAN=\
static/f/vndb.js \
static/f/v2rw.js \
data/icons/icons.css \
- static/v3/elm.js \
- static/v3/style.css \
util/sql/editfunc.sql \
$(shell ls static/s | sed -e 's/\(.\+\)/static\/s\/\1\/style.css/g')
PROD=\
- static/v3/elm-opt.js \
- static/v3/min.js static/v3/min.js.gz \
- static/v3/min.css static/v3/min.css.gz \
static/f/vndb.min.js static/f/vndb.min.js.gz \
static/f/v2rw.min.js static/f/v2rw.min.js.gz \
static/f/icons.opt.png \
@@ -54,13 +49,10 @@ clean:
rm -f ${ALL_CLEAN} ${PROD}
rm -f static/f/icons.png
rm -rf elm/Gen/
- rm -f elm3/Lib/Gen.elm
rm -rf elm/elm-stuff/build-artifacts
- rm -rf elm3/elm-stuff/build-artifacts
cleaner: clean
rm -rf elm/elm-stuff
- rm -rf elm3/elm-stuff
util/sql/editfunc.sql: util/sqleditfunc.pl util/sql/schema.sql
util/sqleditfunc.pl
@@ -69,7 +61,7 @@ static/ch static/cv static/sf static/st:
mkdir -p $@;
for i in $$(seq -w 0 1 99); do mkdir -p "$@/$$i"; done
-data/log www www/feeds www/api static/f static/v3:
+data/log www www/feeds www/api static/f:
mkdir -p $@
data/conf.pl:
@@ -164,43 +156,6 @@ static/f/v2rw.min.js: ${ELM_FILES} ${JS_FILES} elm/Gen/.generated | static/f
-# v3
-
-ELM3_FILES=elm3/*.elm elm3/*/*.elm elm3/Lib/Gen.elm
-ELM3_MODULES=$(shell grep -l '^main =' ${ELM3_FILES} | sed 's/^elm3\///')
-
-elm3/Lib/Gen.elm: lib/VN3/*.pm lib/VN3/*/*.pm data/conf.pl
- util/vndb3.pl elmgen >$@
-
-static/v3/elm.js: ${ELM3_FILES}
- cd elm3 && ELM_HOME=elm-stuff elm make ${ELM3_MODULES} --output ../$@
- sed -i 's/var \$$author\$$project\$$Lib\$$Ffi\$$/var __unused__/g' $@
- sed -Ei 's/\$$author\$$project\$$Lib\$$Ffi\$$([a-zA-Z0-9_]+)/window.elmFfi_\1(_Json_wrap)/g' $@
-
-static/v3/elm-opt.js: ${ELM3_FILES}
- cd elm3 && ELM_HOME=elm-stuff elm make --optimize ${ELM3_MODULES} --output ../$@
- sed -i 's/var \$$author\$$project\$$Lib\$$Ffi\$$/var __unused__/g' $@
- sed -Ei 's/\$$author\$$project\$$Lib\$$Ffi\$$([a-zA-Z0-9_]+)/window.elmFfi_\1(_Json_wrap)/g' $@
-
-static/v3/min.js: static/v3/elm-opt.js static/v3/vndb.js
- uglifyjs $^ --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' | uglifyjs --mangle -o $@
-
-
-CSS=\
- css3/framework/base.css\
- css3/framework/helpers.css\
- css3/framework/grid.css\
- css3/framework/elements.css\
- css3/vndb.css
-
-static/v3/style.css: ${CSS} | static/f
- cat $^ >$@
-
-static/v3/min.css: static/v3/style.css
- perl -MCSS::Minifier::XS -e 'undef $$/; print CSS::Minifier::XS::minify(scalar <>)' <$< >$@
-
-
-
# Multi
# may wait indefinitely, ^C and kill -9 in that case
diff --git a/README.md b/README.md
index 4644c41f..0a965040 100644
--- a/README.md
+++ b/README.md
@@ -147,24 +147,10 @@ to Elm and JSON APIs.
**Version 3**
-This is (or was) an attempt at a full rewrite of the entire website, both
-backend and frontend. It lives in `lib/VN3/` and uses `util/vndb3.pl` as entry
-point. Its frontend assets live inside `css3/`, `elm3/` and `static/v3/`. Most
-of the ideas from version 3 will be gradually backported into version 2-rw.
-Version 3 also comes with a completely different and much better layout, which
-I hope will also be integrated in version 2-rw at some point. Version 3 is not
-actively maintained at this point and is more of a playground for the new
-layout.
-
-To run version 3 instead of 2:
-
-```
- # When not using Docker
- util/vndb-dev-server.pl 3
-
- # Or when using Docker, start the container as follows:
- docker run -ti --name vndb -p 3000:3000 -v "`pwd`":/var/www --rm vndb /var/www/util/docker-init.sh 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**
diff --git a/css3/framework/base.css b/css3/framework/base.css
deleted file mode 100644
index d2124e1a..00000000
--- a/css3/framework/base.css
+++ /dev/null
@@ -1,84 +0,0 @@
-* {
- box-sizing: border-box;
-}
-
-body {
- margin: 0;
- color: #171717;
- font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
- font-size: 1rem;
- line-height: 1.5;
-}
-
-h1, h2, h3, h4, h5, h6 {
- margin-top: 0;
- margin-bottom: 0.5rem;
- font-weight: 500;
-}
-h1, .h1 {font-size: 1.75rem; }
-h2 { font-size: 1.5rem; }
-h3 { font-size: 1.25rem; }
-h4 { font-size: 1.125rem; }
-h5, h6 { font-size: 1rem; }
-
-p, dl, ol, ul {
- margin-top: 0;
- margin-bottom: 1rem;
-}
-
-dl { margin: 0; padding-top: -0.7em; }
-dt { margin: 0.7em 0 0 0; font-weight: 600; }
-dd { margin: 0; color: #3f3f3f;}
-
-dd > dl { padding: 0; }
-dd > dl > dt { color: #3f3f3f; font-weight: 400; margin: 0; }
-dd > dl > dd { margin-left: 1.4em; }
-
-.dl--horizontal { display: flex; flex-wrap: wrap; align-content: flex-start; }
-.dl--horizontal > dt { margin: 0 0 0.7em 0; font-weight: 600; width: 35%; }
-.dl--horizontal > dd { margin: 0 0 0.7em 0; color: #3f3f3f; width: 65%; }
-
-a {
- color: #005ec3;
- text-decoration: none;
- background-color: transparent;
- -webkit-text-decoration-skip: objects;
-}
-
-a:hover {
- color: #004ea2;
- text-decoration: underline;
-}
-
-a.link--subtle {
- color: #3f3f3f;
-}
-
-a.link--subtle:hover {
- color: #004ea2;
- text-decoration: none;
-}
-
-dt > a { color: #004ea2; }
-
-fieldset {
- border: none;
- margin: 0;
- padding: 0;
-}
-
-table {
- border-collapse: collapse;
-}
-
-th {
- text-align: inherit;
-}
-
-hr {
- box-sizing: content-box;
- height: 0;
- overflow: visible;
- border: 0;
- border-top: 1px solid rgba(0, 0, 0, 0.2);
-}
diff --git a/css3/framework/elements.css b/css3/framework/elements.css
deleted file mode 100644
index 6cefa7ed..00000000
--- a/css3/framework/elements.css
+++ /dev/null
@@ -1,962 +0,0 @@
-.form-control {
- display: block;
- width: 100%;
- padding: 4px 7px;
- font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
- font-size: 14px;
- line-height: 1.5;
- color: #495057;
- background-color: #fff;
- background-image: none;
- background-clip: padding-box;
- border: 1px solid #cecece;
- border-radius: 4px;
- box-shadow:
- 0 0 0 2px rgba(0, 123, 255, 0),
- 0 1px 0 0 rgba(0, 0, 0, 0.05);
- transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
-}
-
-select.form-control {
- background-image: linear-gradient(#ffffff, #fbfbfb);
- border-radius: 4px;
- padding: 4px;
- height: 31px;
- box-shadow:
- 0 0 0 2px rgba(0, 123, 255, 0),
- 0 1px 2px 0 rgba(0, 0, 0, 0.05);
-}
-
-select.form-control:hover {
- background-image: linear-gradient(#fbfbfb, #f5f5f5);
-}
-
-.form-control--compact {
- padding: 2px 5px;
-}
-
-select.form-control--compact {
- padding: 2px;
- height: 26px;
-}
-
-.form-control--stealth {
- border: 1px solid transparent;
- box-shadow:
- 0 0 0 2px rgba(0, 123, 255, 0),
- 0 1px 0 0 rgba(0, 0, 0, 0);
- transition: border-color ease-in-out 0.07s, box-shadow ease-in-out 0.07s;
-}
-
-.form-control--stealth:hover, .form-control--stealth:focus {
- border: 1px solid #cecece;
- box-shadow:
- 0 0 0 2px rgba(0, 123, 255, 0),
- 0 1px 0 0 rgba(0, 0, 0, 0.05);
-}
-
-.form-control:focus {
- color: #495057;
- background-color: #fff;
- border-color: #80bdff;
- outline: none;
- box-shadow:
- 0 0 0 2px rgba(0, 123, 255, 0.25),
- 0 1px 0 0 rgba(0, 0, 0, 0);
-}
-
-.is-invalid .form-control, .form-control.is-invalid {
- border: 1px solid #dc3545;
- box-shadow:
- 0 0 0 2px rgba(220, 53, 69, 0),
- 0 1px 0 0 rgba(0, 0, 0, 0.05);
-}
-
-.is-invalid .form-control:focus, .form-control.is-invalid:focus {
- box-shadow:
- 0 0 0 2px rgba(220, 53, 69, 0.25),
- 0 1px 0 0 rgba(0, 0, 0, 0);
-}
-
-.form-control::placeholder {
- color: #8d8d8d;
-}
-
-.form-control--inline {
- display: inline-block;
- width: auto;
-}
-
-.form-control-wrap {
- position: relative;
-}
-
-.form-control-wrap--loading .form-control {
- padding-right: 26px;
-}
-
-.form-control-wrap--loading::after {
- content: '';
- width: 12px;
- height: 12px;
- position: absolute;
- top: 0;
- right: 7px;
- bottom: 0;
- margin: auto 0;
- border: 2px solid #9eaebd;
- border-bottom-color: transparent;
- border-radius: 100%;
- animation: spin 1s infinite linear;
-}
-
-.col-form-label, .col-form-value {
- height: 31px;
- line-height: 31px;
-}
-
-.caret {
- display: inline-block;
- width: 7px;
- height: 7px;
- border-right: 2px solid #171717;
- border-bottom: 2px solid #171717;
- transform: rotate(45deg);
- vertical-align: middle;
- vertical-align: 0.16em;
- margin-left: 0.3em;
-}
-
-.caret--pre {
- margin-left: 0;
- margin-right: 0.3em;
-}
-
-.caret--up {
- transform: rotate(-135deg);
- vertical-align: 0;
-}
-
-.caret--left {
- transform: rotate(135deg);
-}
-
-.dropdown {
- position: relative;
-}
-
-.dropdown-menu {
- display: none;
- position: absolute;
- top: 100%;
- left: 0;
- z-index: 1000;
- padding: 6px 0;
- margin: 2px 0 0;
- font-size: 1rem;
- min-width: 160px;
- color: #171717;
- background-color: #fff;
- border-radius: 4px;
- flex-direction: column;
- box-shadow: 0 15px 35px rgba(50, 50, 93, 0.2), 0 5px 15px rgba(0, 0, 0, 0.2);
- text-align: left;
-}
-
-.dropdown-menu--right {
- left: auto;
- right: 0;
-}
-
-.dropdown--open .dropdown-menu, .dropdown-menu--open {
- display: flex;
-}
-
-.dropdown-menu__item {
- color: #171717;
- padding: 4px 16px;
- white-space: nowrap;
- font-size: 14px;
-}
-
-a.dropdown-menu__item:hover, a.dropdown-menu__item--active {
- text-decoration: none;
- background: #f1f1f1;
- color: #171717;
-}
-
-.dropdown__separator {
- height: 1px;
- background: #e0e0e0;
- margin: 6px 0;
-}
-
-.nav {
- display: flex;
- flex-wrap: wrap;
- padding: 0;
- margin: 0;
- list-style: none;
-}
-
-.nav__item {
- display: block;
-}
-
-.nav__link {
- padding: .5rem 1rem;
- display: block;
- white-space: nowrap;
- color: #6b6b6b;
-}
-.nav__item--active > .nav__link {
- color: #3f3f3f;
-}
-a.nav__link:hover {
- color: #3f3f3f;
- text-decoration: none;
-}
-
-.nav__item:first-child > .nav__link {
- padding-left: 0;
-}
-
-.nav__item:last-child > .nav__link {
- padding-right: 0;
-}
-
-.nav--vertical {
- flex-direction: column;
-}
-
-.nav--vertical .nav__item {
- display: flex;
- flex-direction: column;
- position: relative;
- font-size: 14px;
- line-height: 30px;
- padding: 0;
- font-weight: 500;
- text-overflow: ellipsis;
- overflow: hidden;
-}
-.nav--vertical .nav__item > .nav__item {
- margin-left: 20px;
-}
-
-.nav--vertical .nav__link {
- padding: 0;
- color: #555555;
-}
-.nav__item--active > .nav__link {
- color: #171717;
- font-weight: 600;
-}
-.nav--vertical a.nav__link:hover {
- color: #004ea2;
- text-decoration: none;
-}
-
-.nav--navbar .dropdown-menu {
- margin-left: 1rem;
-}
-
-.card {
- margin-bottom: 16px;
- border-radius: 4px;
- background: white;
- box-shadow: 0 7px 14px 0 rgba(50,50,93,.1), 0 3px 6px 0 rgba(0,0,0,.07);
- box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2);
- box-shadow: 0 7px 14px 0 rgba(50,50,93,.1), 0 3px 6px 0 rgba(0,0,0,.07), 0 1px 5px 0 rgba(0,0,0,0.2);
-}
-
-a.card {
- text-decoration: none;
- color: inherit;
- transition: box-shadow 0.2s;
-}
-
-a.card:hover {
- box-shadow: 0 9px 20px 0 rgba(50,50,93,.15), 0 3px 9px 0 rgba(0,0,0,.07), 0 1px 7px 0 rgba(0,0,0,0.2);
-}
-
-.card__header {
- padding: 16px 20px;
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.card__title {
- font-size: 18px;
- font-weight: 500;
- color: #3f3f3f;
-}
-
-.card__subheading {
- font-size: 14px;
- color: #6b6b6b;
- margin-top: 5px;
-}
-
-.card__body, .card__section {
- border-top: 1px solid #e0e0e0;
- padding: 16px 20px;
-}
-
-.card--no-separators .card__body, .card--no-separators .card__section {
- border: none;
-}
-
-.card__section {
- background: #f8f8f8;
-}
-
-.card--white .card__section {
- background: #fff;
-}
-
-.card__section--error, .card--white .card__section--error {
- color: #721c24;
- background-color: #f8d7da;
- border-top: 1px solid #f5c6cb;
- border-bottom: 1px solid #f5c6cb;
-}
-
-.card__section:last-child {
- border-bottom: none;
-}
-
-.card__section--error + .card__body, .card__section--error + .card__section {
- border-top: none;
-}
-
-.card > .card__body:first-child, .card > .card__section:first-child {
- border-top: none;
-}
-
-.card > .card__body:first-child, .card > .card__section:first-child {
- border-top-left-radius: 4px;
- border-top-right-radius: 4px;
-}
-
-.card > .card__body:last-child, .card > .card__section:last-child {
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 4px;
-}
-
-.card__form-section-left {
- margin-bottom: 8px;
-}
-
-.card__form-section-title {
- font-size: 16px;
- font-weight: 500;
- color: #3f3f3f;
-}
-
-.card__form-section-subtitle {
- font-size: 14px;
- color: #6b6b6b;
-}
-
-
-.alert {
- padding: .75rem 1.25rem;
- margin-bottom: 16px;
- border: 1px solid transparent;
- border-radius: 4px;
-}
-
-.alert--error {
- color: #721c24;
- background-color: #f8d7da;
- border-color: #f5c6cb;
-}
-
-
-.form-group {
- margin-bottom: 12px;
- font-size: 14px;
-}
-
-.form-group__help {
- font-size: 12px;
- margin-top: 4px;
- color: #3f3f3f;
-}
-
-.card__section > .form-group:last-child {
- margin-bottom: 0;
-}
-
-.form-group > label {
- display: block;
- font-size: 14px;
- font-weight: 500;
- color: #3f3f3f;
- margin-bottom: 4px;
-}
-
-.invalid-feedback {
- font-size: 14px;
- margin-top: 4px;
- color: #dc3545;
-}
-
-label.checkbox {
- font-size: 14px;
- color: #3f3f3f;
- font-weight: 400;
-}
-
-.editable-list {
- margin-bottom: 12px;
-}
-
-.editable-list__row {
- font-size: 14px;
- padding: 4px 0 0 0;
-}
-
-.editable-list--sm .editable-list__row {
- margin-bottom: 8px;
-}
-
-.editable-list__field {
- margin-bottom: 4px;
-}
-
-.card__section .editable-list:last-child {
- margin-bottom: 0;
-}
-
-@media (min-width: 576px) {
- .editable-list--sm .editable-list__row {
- margin-bottom: 0;
- }
-}
-
-.btn {
- font-family: inherit; /* not needed if using normalize.css */
- margin: 0; /* not needed if using normalize.css */
- display: inline-block;
- cursor: pointer;
- overflow: visible; /* remove padding in IE */
- padding: 4px 10px;
- font-size: 14px;
- line-height: 1.5;
- text-decoration: none;
- color: #495057;
- background-color: #fff;
- background-image: linear-gradient(#ffffff, #fbfbfb);
- background-clip: padding-box;
- border: 1px solid #cecece;
- border-radius: 4px;
- box-shadow:
- 0 0 0 2px rgba(0, 123, 255, 0),
- 0 1px 2px 0 rgba(0, 0, 0, 0.05);
- transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
- user-select: none;
-}
-
-.btn:hover {
- color: #495057;
- text-decoration: none;
- background-image: linear-gradient(#fbfbfb, #f5f5f5);
-}
-
-.btn:focus {
- color: #495057;
- background-color: #fff;
- border-color: #80bdff;
- outline: none;
- box-shadow:
- 0 0 0 2px rgba(0, 123, 255, 0.25),
- 0 1px 0 0 rgba(0, 0, 0, 0);
-}
-
-.btn.active {
- background: #959595;
- border-color: #767676;
- color: #fff;
-}
-
-.btn-group {
- display: inline-flex;
-}
-
-.btn-group .btn + .btn {
- margin-left: -1px;
-}
-
-.btn-group > .btn:not(:last-child) {
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
-}
-
-.btn-group >.btn:not(:first-child) {
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
-}
-
-.btn-group > .btn:hover, .btn-group > .btn:focus {
- z-index: 1;
-}
-
-.btn-group > .btn.active {
- z-index: 2;
-}
-
-.btn--subtle {
- border: none;
- background: none;
- box-shadow: none;
- transition: background-color ease-in-out 0.1s, box-shadow ease-in-out 0.15s;
-}
-
-.btn--subtle:hover {
- background: rgba(0, 0, 0, 0.06);
- color: #3f3f3f;
-}
-
-.btn--subtle:focus {
- background: none;
- box-shadow: none;
-}
-
-.btn--subtle:active {
- background: rgba(0, 0, 0, 0.09);
- color: #171717;
- box-shadow: none;
-}
-
-.btn--disabled, .btn:disabled {
- opacity: .65;
-}
-
-a.btn--disabled, fieldset:disabled a.btn {
- pointer-events: none;
-}
-
-.text-row { display: flex; }
-.text-shrink { flex-shrink: 100000; }
-.text-expand { flex: 1; flex-basis: auto; }
-
-.spinner-padding {
- padding: 40px;
- display: flex;
- justify-content: center;
-}
-
-.spinner {
- content: '';
- border: 3px solid #9eaebd;
- border-bottom-color: transparent;
- border-radius: 100%;
- animation: spin 1s infinite linear;
-}
-
-.spinner--sm {
- width: 12px;
- height: 12px;
- border-width: 2px;
-}
-
-.spinner--md {
- width: 24px;
- height: 24px;
- border-width: 3px;
-}
-
-.spinner--lg {
- width: 32px;
- height: 32px;
- border-width: 4px;
-}
-
-@keyframes spin {
- from { transform:rotate(0deg); }
- to { transform:rotate(360deg); }
-}
-
-.table {
- width: 100%;
- background: transparent;
-}
-
-.table tbody + .table tbody {
- border-top: 2px solid #dee2e6;
-}
-
-.table td, .table th {
- padding: .3rem .75rem;
- vertical-align: top;
-}
-
-.table th {
- font-weight: 500;
-}
-
-.table thead th {
- padding: .6rem .75rem;
- vertical-align: bottom;
- border-top: 1px solid #dee2e6;
- border-bottom: 1px solid #dee2e6;
-}
-
-.table thead th.th--nopad {
- padding: 0;
-}
-
-.table thead th .table-header {
- display: block;
- padding: .6rem .75rem;
-}
-
-.form-control--table-edit {
- /* align field so it 's at the same position the text would be */
- height: 28px;
- margin: -4px -8px -4px -8px;
-}
-
-.table-edit-overlay-base {
- position: relative;
-}
-
-.form-control--table-edit-overlay {
- position: absolute;
- top: 3px;
- left: 11px;
- opacity: 0;
- transition: border-color ease-in-out 0.07s, box-shadow ease-in-out 0.07s, opacity ease-in-out 0.07s;
-}
-
-.card > .table > tbody > tr:first-child > td .form-control--table-edit-overlay {
- top: 7px;
-}
-
-.form-control--table-edit-overlay:hover, .form-control--table-edit-overlay:focus {
- opacity: 1;
-}
-
-@media (max-width: 575.98px) {
- .table--responsive-single-sm {
- display: block;
- }
- .table--responsive-single-sm tbody, .table--responsive-single-sm thead {
- display: block;
- }
- .table--responsive-single-sm tr {
- display: block;
- padding: 8px 15px !important;
- }
- .table--responsive-single-sm td, .table--responsive-single-sm th, .table--responsive-single-sm th .table-header {
- display: block;
- padding: 0 !important;
- }
- .table--responsive-single-sm td[width], .table--responsive-single-sm th[width] {
- width: auto !important;
- }
- .table--responsive-single-sm thead th {
- border: none;
- }
- .table--responsive-single-sm thead {
- border-top: 1px solid #dee2e6;
- border-bottom: 1px solid #dee2e6;
- }
- .card > .table--responsive-single-sm:first-child > thead {
- border-top: none;
- }
- .form-control--table-edit-overlay, .card > .table > tbody > tr:first-child > td .form-control--table-edit-overlay {
- top: 0px;
- left: 0px;
- }
- .table-edit-overlay-base {
- height: 26px;
- }
- .form-control--table-edit {
- margin: 0 -8px 0 -8px;
- }
-}
-
-.card > .table > thead {
- background-color: #fafafa;
-}
-
-.card > .table:first-child > thead {
- border-top-left-radius: 4px;
- border-top-right-radius: 4px;
-}
-
-.card > .table:first-child > thead > tr > th {
- border-top: none;
-}
-
-.card > .table > tbody > tr:first-child > td {
- padding-top: .5rem;
-}
-
-.card > .table > tbody > tr:last-child > td {
- padding-bottom: .5rem;
-}
-
-.card > .table > tbody > tr > td:first-child, .card > .table > thead > tr > th:first-child {
- padding-left: 20px;
-}
-
-.card > .table > thead > tr > th.th--nopad:first-child {
- padding-left: 0;
-}
-
-.card > .table > thead > tr > th:first-child > .table-header {
- padding-left: 20px;
-}
-
-.card > .table > tbody > tr > td:last-child, .card > .table > thead > tr > th:last-child {
- padding-right: 20px;
-}
-
-.table > thead > tr > th:last-child {
- padding-right: 0;
-}
-
-.card > .table > thead > tr > th.th--nopad:last-child {
- padding-right: 0;
-}
-
-.card > .table > thead > tr > th:last-child > .table-header {
- padding-right: 20px;
-}
-
-.td-editable {
- cursor: pointer;
-}
-
-a.td-editable {
- display: block;
- color: inherit;
- text-decoration: inherit;
-}
-
-.vertical-selector {
- font-size: 14px;
-}
-
-.vertical-selector__item {
- display: block;
- font-weight: 500;
- padding: 5px 10px;
- border-radius: 5px;
- color: #555555;
-}
-
-a.vertical-selector__item {
- text-decoration: none;
-}
-
-.vertical-selector__item:hover {
- background-color: rgba(255, 255, 255, 0.4);
-}
-
-.vertical-selector__item--active {
- background-color: rgba(255, 255, 255, 0.7);
- color: #171717;
- font-weight: bold
-}
-
-.vertical-selector-label {
- font-size: 14px;
- font-weight: 500;
- color: #3f3f3f;
- margin-bottom: 8px;
-}
-
-.expand-arrow {
- display: inline-block;
- width: 7px;
- height: 7px;
- border-right: 1px solid #171717;
- border-bottom: 1px solid #171717;
- transform: rotate(-45deg);
- vertical-align: middle;
- vertical-align: 0.1em;
-}
-
-.expand-arrow--open {
- transform: rotate(45deg);
- vertical-align: middle;
- vertical-align: 0.16em;
-}
-
-.modal-backdrop {
- z-index: 1040;
- background-color: rgba(0, 0, 0, 0.21);
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
-}
-
-.modal-scrollbar-measure {
- position: absolute;
- top: -9999px;
- width: 50px;
- height: 50px;
- overflow: scroll;
-}
-
-body.has-modal-open {
- overflow: hidden;
-}
-
-.modal {
- display: none;
- z-index: 1050;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- overflow-x: hidden;
- overflow-y: scroll;
- flex-direction: column;
- align-items: center;
- padding: 14px;
-}
-
-.modal--open {
- display: flex;
-}
-
-.modal__content {
- background: #f8f8f8;
- width: 100%;
- margin: auto;
- box-shadow: 0 15px 35px 0 rgba(0, 0, 0, 0.2);
- border-radius: 4px;
-}
-
-@media (min-width: 630px) {
- .modal__content {
- width: 600px;
- }
-}
-
-.modal__header {
- height: 72px;
- background-color: #fff;
- border-bottom: 1px solid #e0e0e0;
- display: flex;
- align-items: center;
- padding: 0 22px;
- border-top-left-radius: 4px;
- border-top-right-radius: 4px;
-}
-
-.modal__title {
- font-size: 20px;
- font-weight: 500;
- flex: 1;
-}
-
-.modal__footer {
- height: 72px;
- background-color: #fff;
- border-top: 1px solid #e0e0e0;
- padding: 0 19px;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 4px;
- display: flex;
- align-items: center;
-}
-
-.modal__footer-buttons {
- flex: 1;
- display: flex;
- justify-content: flex-end;
- align-items: center;
-}
-
-.modal__footer-buttons .btn {
- margin-left: 10px;
-}
-
-.modal__body {
- background-color: #f8f8f8;
- padding: 22px;
-}
-
-.modal__body + .modal__body, .modal__table + .modal__body {
- border-top: 1px solid #e0e0e0;
-}
-
-.modal__close {
- display: block;
- width: 40px;
- height: 40px;
- position: relative;
- background: transparent;
- border: none;
- margin-right: -8px;
- cursor: pointer;
-}
-
-.modal__close::before, .modal__close::after {
- position: absolute;
- top: 11px; /* (40 (container height) - 20 (stroke height)) / 2 */
- left: 19px; /* (40 (container width) - 2 (stroke width)) / 2 */
- content: '';
- height: 18px;
- width: 2px;
- background: #6b6b6b;
- transform: rotate(-45deg);
-}
-
-.modal__close::after {
- transform: rotate(45deg);
-}
-
-.modal__section-title {
- font-size: 18px;
- font-weight: 500;
- color: #3f3f3f;
- margin: -5px 0 15px 0;
-}
-
-.modal__section-title--solo {
- margin: -5px 0;
-}
-
-.modal__table > thead { background: #fafafa; }
-/* reduce padding to fit in modal better */
-.modal__table td, .modal__table th { padding: .3rem .5rem; }
-.modal__table thead th { padding: .6rem .5rem; }
-.modal__table > tbody > tr > td:first-child, .modal__table > thead > tr > th:first-child { padding-left: 22px; }
-.modal__table > thead > tr > th.th--nopad:first-child { padding-left: 0; }
-.modal__table > thead > tr > th:first-child > .table-header { padding-left: 22px; }
-.modal__table > tbody > tr > td:last-child, .modal__table > thead > tr > th:last-child { padding-right: 22px; }
-.modal__table > thead > tr > th.th--nopad:last-child { padding-right: 0; }
-.modal__table > thead > tr > th:last-child > .table-header { padding-right: 22px; }
-
-.btn--remove-row {
- height: 29px;
- width: 29px;
- position: relative;
-}
-
-.btn--remove-row::before, .btn--remove-row::after {
- position: absolute;
- top: 8px; /* (29 (container height) - 17 (stroke height)) / 2 */
- left: 13px; /* (29 (container width) - 2 (stroke width)) / 2 */
- content: '';
- height: 13px;
- width: 2px;
- background: #8d8d8d;
- transform: rotate(-45deg);
-}
-
-.btn--remove-row::after {
- transform: rotate(45deg);
-} \ No newline at end of file
diff --git a/css3/framework/grid.css b/css3/framework/grid.css
deleted file mode 100644
index c66ff5b2..00000000
--- a/css3/framework/grid.css
+++ /dev/null
@@ -1,102 +0,0 @@
-.container {
- width: 100%;
- padding: 0 15px;
- margin: 0 auto;
-}
-
-.row {
- margin: 0 -15px;
- display: flex;
- flex-wrap: wrap;
-}
-
-.row--compact {
- margin: 0 -5px;
-}
-
-.row--ai-center {
- align-items: center;
-}
-
-.col, .col-sm, .col-md, .col-lg, .col-xl, .col-xxl {
- padding-left: 15px;
- padding-right: 15px;
- width: 100%;
- min-width: 0;
-}
-.row--compact .col, .row--compact .col-sm, .row--compact .col-md,
-.row--compact .col-lg, .row--compact .col-xl, .row--compact .col-xxl {
- padding-left: 5px;
- padding-right: 5px;
-}
-
-.col { flex: 1; }
-.col--2 { flex: 2; }
-.col--3 { flex: 3; }
-.col--4 { flex: 4; }
-
-.col--auto { flex: 0 0 auto; width: auto; }
-
-@media (min-width: 576px) {
- /* .container { max-width: 540px; } */
- .col-sm {
- flex: 1;
- }
- .col-sm--2 { flex: 2; }
- .col-sm--3 { flex: 3; }
- .col-sm--4 { flex: 4; }
- .col-sm--auto { flex: 0 0 auto; width: auto; }
- .d-sm-block { display: block; }
- .d-sm-none { display: none; }
- .text-sm-right { text-align: right; }
-}
-@media (min-width: 768px) {
- /* .container { max-width: 720px; } */
- .col-md {
- flex: 1;
- }
- .col-md--2 { flex: 2; }
- .col-md--3 { flex: 3; }
- .col-md--4 { flex: 4; }
- .col-md--auto { flex: 0 0 auto; width: auto; }
- .d-md-block { display: block; }
- .d-md-none { display: none; }
-}
-@media (min-width: 992px) {
- .container { max-width: 960px; }
- .container--narrow { max-width: 912px; }
- .col-lg {
- flex: 1;
- }
- .col-lg--2 { flex: 2; }
- .col-lg--3 { flex: 3; }
- .col-lg--4 { flex: 4; }
- .col-lg--auto { flex: 0 0 auto; width: auto; }
- .d-lg-block { display: block; }
- .d-lg-none { display: none; }
-}
-@media (min-width: 1200px) {
- .container { max-width: 1140px; }
- .container--narrow { max-width: 912px; }
- .col-xl {
- flex: 1;
- }
- .col-xl--2 { flex: 2; }
- .col-xl--3 { flex: 3; }
- .col-xl--4 { flex: 4; }
- .col-xl--auto { flex: 0 0 auto; width: auto; }
- .d-xl-block { display: block; }
- .d-xl-none { display: none; }
-}
-@media (min-width: 1360px) {
- /* .container { max-width: 1300px; } */
- .col-xxl {
- flex: 1;
- }
- .col-xxl--2 { flex: 2; }
- .col-xxl--3 { flex: 3; }
- .col-xxl--4 { flex: 4; }
- .col-xxl--auto { flex: 0 0 auto; width: auto; }
- .d-xxl-block { display: block; }
- .d-xxl-none { display: none; }
-}
diff --git a/css3/framework/helpers.css b/css3/framework/helpers.css
deleted file mode 100644
index 47c14126..00000000
--- a/css3/framework/helpers.css
+++ /dev/null
@@ -1,127 +0,0 @@
-/* bootstrap 4 inspired css helpers */
-
-.d-none { display: none !important; }
-.d-block { display: block !important; }
-.d-inline { display: block !important; }
-.d-flex { display: flex !important; }
-
-.single-line { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
-.text-left { text-align: left; }
-.text-right { text-align: right; }
-.tabular-nums { font-variant-numeric: tabular-nums; }
-
-.pull-right { float: right; }
-
-.flex-expand { flex: 1; }
-.jc-start { justify-content: flex-start; }
-.jc-end { justify-content: flex-end; }
-.jc-center { justify-content: center; }
-.jc-between { justify-content: space-between; }
-.align-items-center { align-items: center; }
-
-.img--fit {
- width: 100%;
- height: auto;
-}
-
-.img--rounded {
- border-radius: 4px;
-}
-
-.elevation-1 {
- box-shadow: 0 15px 35px rgba(50, 50, 93, 0.18), 0 5px 15px rgba(0, 0, 0, 0.18), 0 1px 5px 0 rgba(0,0,0,0.1);
-}
-
-.svg-icon {
- display: inline-block;
- font-size: inherit;
- height: 1em;
- overflow: visible;
- vertical-align: -.125em;
-}
-
-.semi-muted { color: #3f3f3f; }
-.muted { color: #6b6b6b; }
-
-.opacity-muted { opacity: 0.6; }
-
-.semi-bold { font-weight: 500; }
-.bold { font-weight: bold; }
-
-.fs-body { font-size: 1rem; }
-.fs-medium { font-size: 0.875rem; }
-
-.mx-0 { margin-left: 0 !important; margin-right: 0 !important; }
-.mx-1 { margin-left: 0.25rem !important; margin-right: 0.25rem !important; }
-.mx-2 { margin-left: 0.5rem !important; margin-right: 0.5rem !important; }
-.mx-3 { margin-left: 1rem !important; margin-right: 1rem !important; }
-.mx-4 { margin-left: 1.5rem !important; margin-right: 1.5rem !important; }
-.mx-5 { margin-left: 2.5rem !important; margin-right: 2.5rem !important; }
-
-.my-0 { margin-top: 0 !important; margin-bottom: 0 !important; }
-.my-1 { margin-top: 0.25rem !important; margin-bottom: 0.25rem !important; }
-.my-2 { margin-top: 0.5rem !important; margin-bottom: 0.5rem !important; }
-.my-3 { margin-top: 1rem !important; margin-bottom: 1rem !important; }
-.my-4 { margin-top: 1.5rem !important; margin-bottom: 1.5rem !important; }
-.my-5 { margin-top: 2.5rem !important; margin-bottom: 2.5rem !important; }
-
-.mb-0 { margin-bottom: 0 !important; }
-.mb-1 { margin-bottom: 0.25rem !important; }
-.mb-2 { margin-bottom: 0.5rem !important; }
-.mb-3 { margin-bottom: 1rem !important; }
-.mb-4 { margin-bottom: 1.5rem !important; }
-.mb-5 { margin-bottom: 2.5rem !important; }
-
-.mt-0 { margin-top: 0 !important; }
-.mt-1 { margin-top: 0.25rem !important; }
-.mt-2 { margin-top: 0.5rem !important; }
-.mt-3 { margin-top: 1rem !important; }
-.mt-4 { margin-top: 1.5rem !important; }
-.mt-5 { margin-top: 2.5rem !important; }
-
-.ml-0 { margin-left: 0 !important; }
-.ml-1 { margin-left: 0.25rem !important; }
-.ml-2 { margin-left: 0.5rem !important; }
-.ml-3 { margin-left: 1rem !important; }
-.ml-4 { margin-left: 1.5rem !important; }
-.ml-5 { margin-left: 2.5rem !important; }
-
-.mr-0 { margin-right: 0 !important; }
-.mr-1 { margin-right: 0.25rem !important; }
-.mr-2 { margin-right: 0.5rem !important; }
-.mr-3 { margin-right: 1rem !important; }
-.mr-4 { margin-right: 1.5rem !important; }
-.mr-5 { margin-right: 2.5rem !important; }
-
-@media (min-width: 576px) {
- .d-sm-block { display: block !important; }
- .d-sm-inline { display: inline !important; }
- .d-sm-flex { display: flex !important; }
- .d-sm-none { display: none !important; }
- .text-sm-right { text-align: right; }
-}
-@media (min-width: 768px) {
- .d-md-block { display: block !important; }
- .d-md-inline { display: inline !important; }
- .d-md-flex { display: flex !important; }
- .d-md-none { display: none !important; }
- .single-line-md { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
-}
-@media (min-width: 992px) {
- .d-lg-block { display: block !important; }
- .d-lg-inline { display: inline !important; }
- .d-lg-flex { display: flex !important; }
- .d-lg-none { display: none !important; }
-}
-@media (min-width: 1200px) {
- .d-xl-block { display: block !important; }
- .d-xl-inline { display: inline !important; }
- .d-xl-flex { display: flex !important; }
- .d-xl-none { display: none !important; }
-}
-@media (min-width: 1360px) {
- .d-xxl-block { display: block !important; }
- .d-xxl-inline { display: inline !important; }
- .d-xxl-flex { display: flex !important; }
- .d-xxl-none { display: none !important; }
-}
diff --git a/css3/vndb.css b/css3/vndb.css
deleted file mode 100644
index 72d55873..00000000
--- a/css3/vndb.css
+++ /dev/null
@@ -1,1838 +0,0 @@
-/**
- * Common colors:
- * #171717 - body text
- * #3f3f3f - muted 1
- * #6b6b6b - muted 2
- * #8d8d8d - muted 3
- * #5899bf - accent (teal-ish blue)
- */
-
-html {
- min-height: 100%;
- display: flex;
- flex-direction: column;
-}
-
-body {
- display: flex;
- flex-direction: column;
- flex: 1;
- background: hsla(200, 14%, 98%, 1);
-}
-
-.page-container {
- flex: 1;
- display: flex;
- flex-direction: column;
- background-image: linear-gradient(hsla(205, 35%, 93%, 1), hsla(200, 14%, 98%, 1));
- background-size: 100% 500px;
- background-repeat: repeat-x;
- padding-top: 26px;
-}
-
-.main-container {
- flex: 1;
-}
-
-.main-container--single-col {
- max-width: 865px;
-}
-
-.raised-top-container {
- background: hsla(205, 35%, 93%, 1);
-}
-
-.raised-top {
- background: #fcfdfd;
- box-shadow: rgba(33, 63, 80, 0.30) 0 2px 4px;
- margin-bottom: 4px;
-}
-
-.elevated-button {
- background: white;
- border-radius: 4px;
- padding: 0.8em 1em;
- font-weight: 500;
-}
-
-.elevated-button img {
- margin-right: 0.4em;
-}
-
-.serif {
- font-family: Georgia, Times, "Times New Roman", serif;
-}
-
-.switch {
- display: flex;
- font-size: 12px;
- font-weight: 500;
- color: #3f3f3f;
- align-items: center;
-}
-
-.switch:hover {
- color: #3f3f3f;
- text-decoration: none;
-}
-
-.switch__label + .switch__toggle {
- margin-left: 0.5em;
-}
-
-.switch__toggle {
- width: 26px;
- height: 16px;
- border-radius: 16px;
- background-color: #bebebe;
- position: relative;
- transition: background-color 0.1s;
-}
-
-.switch--on .switch__toggle {
- background-color: #5899bf;
-}
-
-.switch__toggle::after {
- content: '';
- position: absolute;
- top: 2px;
- left: 2px;
- width: 12px;
- height: 12px;
- border-radius: 12px;
- background: #f3f5f6;
- transition: left 0.1s;
- box-shadow: 0px 2px 3px rgba(50, 50, 93, 0.25);
-}
-
-.switch--on .switch__toggle::after {
- left: 12px;
-}
-
-.top-bar {
- height: 4px;
- background-color: #5899bf;
-}
-
-.icon-desc {
- margin-right: 0.2em;
-}
-
-.icon-group {
- display: inline-block;
- position: relative;
-}
-
-.navbar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- flex-wrap: wrap;
- padding: 15px;
-}
-
-.container > .navbar {
- margin-left: -15px;
- margin-right: -15px;
-}
-
-.navbar__toggler {
- flex: 1;
- display: flex;
- justify-content: flex-end;
- padding: 7px 0;
-}
-
-.navbar__toggler-icon {
- width: 16px;
- height: 10px;
- position: relative;
-}
-
-.navbar__toggler-icon::before, .navbar__toggler-icon::after {
- content: '';
- position: absolute;
- left: 0;
- right: 0;
- top: 4px;
- height: 2px;
- background-color: #171717;
- transition: transform 0.1s;
-}
-.navbar__toggler-icon::before { transform: translateY(-4px) rotate(0deg); }
-.navbar__toggler-icon::after { transform: translateY(4px) rotate(0deg); }
-
-.navbar--expanded .navbar__toggler-icon::before { transform: translateY(0) rotate(-45deg); }
-.navbar--expanded .navbar__toggler-icon::after { transform: translateY(0) rotate(45deg); }
-
-.navbar__collapse {
- flex: 1;
- display: none;
- margin-top: 10px;
-}
-
-.navbar__nav {
- flex-direction: column;
-}
-
-.navbar--expanded {
- box-shadow: rgba(33, 63, 80, 0.48) 0 2px 4px;
- height: auto;
- margin-bottom: 15px;
-}
-
-.navbar--expanded .navbar__collapse {
- display: block;
- flex-basis: 100%;
-}
-
-.navbar__nav .nav__link {
- padding-right: 0;
- padding-left: 0;
-}
-
-.navbar__main-nav {
- flex: 1;
-}
-
-.navbar__logo {
- font-weight: 500;
- font-size: 18px;
- padding-left: 0;
- margin-right: 30px;
-}
-
-.navbar__logo a {
- color: #171717;
- text-decoration: none;
-}
-
-.navbar__menu > .nav__link {
- font-weight: 500;
- color: #171717;
-}
-
-.navbar__menu > .nav__link .svg-icon {
- font-size: 14px;
-}
-
-.navbar .dropdown-menu {
- position: static;
- box-shadow: none;
-}
-
-.nav-sidebar {
- margin-bottom: 15px;
- background: #fff;
- box-shadow: 0 7px 14px 0 rgba(50,50,93,.1), 0 3px 6px 0 rgba(0,0,0,.07);
- border-radius: 4px;
- position: relative;
- display: flex;
- align-items: center;
- justify-content: space-between;
- flex-wrap: wrap;
-}
-
-.nav-sidebar__selection {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 10px 12px;
-}
-
-.nav-sidebar__selection, .nav-sidebar__selection:focus {
- color: #171717;
- text-decoration: none;
-}
-
-.nav-sidebar__selection .caret {
- margin-top: -2px;
-}
-
-.nav-sidebar .nav {
- padding: 2px 12px 10px 12px;
- display: none;
- flex-basis: 100%;
-}
-
-.nav-sidebar--expanded .nav {
- display: block;
-}
-
-@media (min-width: 768px) {
- .navbar--expand-md {
- justify-content: flex-start;
- height: 84px;
- box-shadow: none;
- margin-bottom: 0;
- }
-
- .navbar--expand-md .navbar__collapse {
- display: flex;
- flex-basis: auto;
- margin-top: 0;
- }
-
- .navbar--expand-md .navbar__nav .nav__link {
- padding: .5rem 1rem;
- }
-
- .navbar--expand-md .dropdown-menu {
- position: absolute;
- box-shadow: 0 15px 35px rgba(50, 50, 93, 0.2), 0 5px 15px rgba(0, 0, 0, 0.2);
- }
-
- .navbar--expand-md .navbar__toggler { display: none; }
- .navbar--expand-md .navbar__nav { flex-direction: row; }
- .navbar--expand-md .navbar__logo { margin-bottom: 0; }
-
- .nav-sidebar--expand-md {
- box-shadow: none;
- background: transparent;
- }
- .nav-sidebar--expand-md .nav-sidebar__selection {
- display: none;
- }
- .nav-sidebar--expand-md .nav {
- display: block;
- }
-}
-
-.footer {
- padding: 40px 0 30px 0;
- color: #6b6b6b;
- font-size: 14px;
-}
-
-.footer__logo {
- font-weight: 500;
- font-size: 18px;
-}
-
-.footer__logo a {
- color: #6b6b6b;
-}
-
-.footer__logo a:hover {
- text-decoration: none;
-}
-
-.footer__nav {
- margin: 3px 0 3px 0;
-}
-
-.footer__sep {
- margin: 0 0.2em;
-}
-
-.footer__random .svg-icon {
- opacity: 0.75;
- font-size: 0.8em;
-}
-
-.detail-page-title {
- font-size: 28px;
- font-weight: 500;
- line-height: 1.1;
- margin-bottom: 20px;
-}
-
-.detail-page-subtitle {
- font-size: 17px;
- margin-top: -12px;
- margin-bottom: 16px;
-}
-
-.detail-page-section-header {
- margin-top: 1rem;
- margin-bottom: 0.7rem;
-}
-
-.detail-page-sidebar-section-header {
- margin-top: 0.5rem;
-}
-
-.vn-header {
- padding: 5px 0 30px 0;
-}
-
-.vn-header__title {
- font-size: 30px;
- font-weight: 500;
- line-height: 1.1;
-}
-
-.vn-header__original-title {
- font-size: 17px;
- margin-top: 6px;
-}
-
-.vn-header__details {
- margin-top: 1em;
- font-size: 0.9em;
- font-weight: 500;
- display: flex;
- flex-direction: column;
- color: #3f3f3f;
-}
-
-.vn-header__sep {
- width: 20px;
- height: 1px;
- margin: 3px 0;
- background-color: rgba(0, 0, 0, 0.21);
-}
-
-.page-header-img-mobile {
- max-width: 100%;
- max-height: 200px;
- margin-bottom: 6px;
-}
-
-.detail-header-image-container {
- position: relative;
-}
-
-.detail-header-image {
- margin-bottom: 25px;
- position: absolute;
- left: 0;
- right: 0;
-}
-
-.detail-header-image-push {
- margin-top: -150px;
- margin-bottom: 25px;
- visibility: hidden;
-}
-
-.elevation-1-nsfw {
- box-shadow: 0 15px 35px rgba(255, 50, 50, 0.18), 0 5px 15px rgba(255, 0, 0, 0.18), 0 1px 5px 0 rgba(100,0,0,0.1);
-}
-
-.nsfw-outline {
- outline: 3px solid #F00;
-}
-
-@media (min-width: 768px) {
- .vn-header {
- padding: 20px 0 50px 0;
- }
- .vn-header__title { font-size: 35px; }
- .vn-header__original-title { font-size: 18px; }
-}
-
-@media (min-width: 992px) {
- .vn-header__title { font-size: 40px; }
- .vn-header__original-title { font-size: 20px; }
-}
-
-@media (min-width: 576px) {
- .vn-header__details {
- flex-direction: row;
- align-items: center;
- }
- .vn-header__sep {
- width: 1px;
- height: 1.5em;
- margin: 0 1em;
- background-color: rgba(0, 0, 0, 0.21);
- }
-}
-
-.vn-page {
- padding-bottom: 20px;
-}
-
-.vn-page__top {
- display: flex;
-}
-
-.vn-page__top-main {
- flex: 1;
-}
-
-.fixed-size-left-sidebar-md, .fixed-size-left-sidebar-xl {
- padding: 0 15px;
- width: 100%;
-}
-
-.vn-page__top-details > * {
- margin-bottom: 25px;
-}
-
-@media (min-width: 768px) {
- .fixed-size-left-sidebar-md {
- width: 275px;
- padding: 0 30px 0 15px;
- }
- .vn-page__top-details {
- position: absolute;
- top: 100px;
- }
-}
-
-@media (min-width: 1200px) {
- .fixed-size-left-sidebar-xl {
- width: 275px;
- padding: 0 30px 0 15px;
- }
-}
-
-.vn-image-placeholder {
- background: #efefef;
- padding-top: 136%;
- position: relative;
-}
-
-.vn-image-placeholder--wide {
- padding-top: 56.52%;
-}
-
-.vn-image-placeholder__icon {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
-}
-
-.vn-image-placeholder__icon .svg-icon {
- color: #3f3f3f;
- font-size: 32px;
- opacity: 0.3;
-}
-
-.vn-page__top-body {
- display: flex;
-}
-
-.description {
- line-height: 1.4;
-}
-
-@media (min-width: 992px) {
- .description {
- font-size: 1.1em;
- }
-}
-
-.raised-top-nav {
- flex-wrap: nowrap;
- overflow-x: auto;
-}
-
-.raised-top-nav > .nav__item > .nav__link {
- font-weight: 500;
- padding: 0 1.5em 0.75em 0;
-}
-
-.raised-top-nav > .nav__item--active > .nav__link {
- font-weight: 700;
-}
-
-.raised-top-nav-buttons {
- margin-top: -0.5em;
-}
-
-.vn-page__dl {
- font-size: 0.9em;
- margin-top: 35px;
-}
-
-.section {
- margin-top: 3em;
- padding-top: 0.9em;
-}
-
-.section__title {
- margin: 0 0 0.5em 0;
-}
-
-.section__title .switch {
- float: right;
- margin-top: 12px;
-}
-
-.spoiler {
- background-color: #ccc;
- color: #ccc;
- font-weight: normal;
-}
-
-.spoiler:hover {
- background-color: rgba(0, 0, 0, 0.05);
- color: inherit;
-}
-
-.gallery__section {
- display: flex;
- flex-wrap: wrap;
- margin: -7px;
- margin-bottom: 12px;
- align-items: flex-end;
-}
-
-.gallery__section:last-child {
- margin-bottom: 0;
-}
-
-.gallery__image-link {
- display: block;
- width: 50%;
- padding: 7px;
-}
-
-.gallery__image {
- display: block;
- /* margin: 7px;
- width: 170px; */
- width: 100%;
- border-radius: 4px;
-}
-
-.gallery__image--r18 {
- display: none;
-}
-
-.gallery--show-r18 .gallery__image--r18 {
- display: block;
-}
-
-@media (min-width: 535px) {
- .gallery__image-link { width: 33%; }
-}
-@media (min-width: 568px) {
- .gallery__image-link { width: 184px; }
-}
-
-.character-browser {
- font-size: 14px;
-}
-
-.character-browser__top-items {
- margin-top: 1em;
-}
-
-.character-browser__top-item {
- margin-top: 0.2em;
-}
-
-.character-browser__top-items .switch {
- font-weight: 400;
- font-size: 1em;
-}
-
-.character-browser__list {
- margin-top: 1em;
-}
-
-.character-browser__list-title {
- font-weight: 500;
-}
-
-.character {
- font-size: 14px;
-}
-
-.character-browser__char {
- display: block;
- color: #6b6b6b;
-}
-
-a.character-browser__char:hover, .character-browser__char--active {
- color: #171717;
- text-decoration: none;
-}
-
-.character__name {
- font-size: 24px;
-}
-
-.character__subtitle {
- margin-bottom: 10px;
- font-size: 12;
-}
-
-.character__description {
- font-size: 16px;
-}
-
-.character__image {
- width: 260px;
- flex: none;
-}
-
-.character__traits {
- max-width: 680px;
-}
-
-.staff-credits {
- font-size: 0.9em;
-}
-
-.staff-credits__section {
- margin-bottom: 0.7em;
-}
-
-.staff-credits__section-title {
- font-weight: 500;
-}
-
-.staff-credits__note {
- color: #8d8d8d;
-}
-
-.tag-summary {
- font-size: 14px;
-}
-
-.tag-summary__tags {
- display: flex;
- flex-wrap: wrap;
-}
-
-.tag-summary--collapsed {
- max-height: 50px;
- overflow: hidden;
-}
-
-.tag-summary__tag {
- font-size: 12px;
- font-weight: 500;
- margin-right: 1.5em;
- margin-bottom: 0.5em;
-}
-
-.tag-summary--hide-cont .tag-summary__tag--cont {
- display: none;
-}
-
-.tag-summary--hide-ero .tag-summary__tag--ero {
- display: none;
-}
-
-.tag-summary--hide-tech .tag-summary__tag--tech {
- display: none;
-}
-
-.tag-summary--hide-spoil-1 .tag-summary__tag--spoil-1 {
- display: none;
-}
-
-.tag-summary--hide-spoil-2 .tag-summary__tag--spoil-2 {
- display: none;
-}
-
-.tag-summary__tag-meter {
- height: 1px;
- background: #6b6b6b;
-}
-
-.tag-summary__options {
- margin-top: 4px;
- border-top: 1px solid rgba(0, 0, 0, 0.1);
- padding-top: 4px;
-}
-
-.tag-summary__option {
- display: inline-block;
- font-size: 14px;
- font-weight: 400;
- margin: 0 14px 0 0;
-}
-
-.tag-summary__options-right {
- margin-top: 4px;
-}
-
-@media (min-width: 576px) {
- .tag-summary__options {
- display: flex;
- }
- .tag-summary__options-right {
- flex: 1;
- text-align: right;
- margin-top: 0;
- }
- .tag-summary__option {
- margin: 0 0 0 14px;
- }
-}
-
-@media (min-width: 768px) {
- .navbar--expand-md .dropdown-menu.database-menu {
- width: 284px;
- flex-direction: row;
- flex-wrap: wrap;
- }
-
- .navbar--expand-md .dropdown-menu.database-menu .dropdown-menu__item {
- width: 130px;
- padding: 6px 13px;
- margin: 0 6px;
- }
-
- .navbar--expand-md .dropdown-menu.database-menu .dropdown-menu__item:hover {
- border-radius: 2px;
- }
-}
-
-.relsm { font-size: 14px; }
-.relsm__language { font-weight: 500; }
-.relsm__table { margin-bottom: 0.5em; }
-.relsm__rel { display: flex; flex-wrap: wrap; margin-top: 0.2em; }
-.relsm__rel-col { display: block;}
-.relsm__rel-date { flex: 1; color: #6b6b6b; }
-.relsm__rel-name { flex: 0 1 auto; order: -1; width: 100%; }
-.relsm__rel-platforms, .relsm__rel-mylist, .relsm__rel-link { padding: 0 0.7em; }
-.relsm__rel-platforms img, .relsm__rel-mylist img { opacity: 0.75; }
-.relsm__rel-link { padding-right: 0; }
-.relsm__rel-link--none { visibility: hidden; }
-
-@media (min-width: 768px) {
- .relsm__rel { flex-wrap: nowrap; margin-top: 0; }
- .relsm__rel-date { flex: 0 1 auto; width: 7em; color: #6b6b6b; }
- .relsm__rel-name { flex: 1; order: 0; width: auto; }
-}
-
-.lang-badge {
- border: 1px solid #a5a5a5;
- color: #a5a5a5;
- display: inline-block;
- padding: .35em .6em;
- font-size: 0.6em;
- font-weight: 700;
- line-height: 1;
- text-align: center;
- white-space: nowrap;
- vertical-align: middle;
- border-radius: 3px;
- min-width: 2.8em;
- margin-right: .1em;
-}
-
-.final-text {
- margin-top: .8em;
-}
-
-.notification-icon__indicator {
- position: absolute;
- top: -1px;
- right: -9px;
- font-size: 8px;
- background: #e63131;
- color: white;
- padding: 3px 4px;
- font-weight: 500;
- line-height: 1;
- border-radius: 100px;
- min-width: 14px;
- text-align: center;
-}
-
-.stats {
- font-size: 12px;
- max-width: 1000px;
-}
-
-.stats__col {
- margin-bottom: 20px;
-}
-
-.stats__ranking > dt {
- font-size: 14px;
-}
-
-.vote-graph {
- max-width: 320px;
- font-size: 12px;
- line-height: 1.3;
- display: flex;
- background: #e8e8e8;
- padding: 1.25em 1em;
- border-radius: 3px;
-}
-
-.vote-graph__scores {
- margin-right: 0.5em;
- text-align: right;
-}
-
-.vote-graph__score {
- height: 1.3em;
-}
-
-.vote-graph__bars {
- padding-right: 2.5em;
- flex: 1;
-}
-
-.vote-graph__bar {
- background: #6b6b6b;
- position: relative;
- height: 1.3em;
-}
-
-.vote-graph__bar-label {
- position: absolute;
- right: -2.2em;
- width: 2em;
-}
-
-.recent-votes__table {
- width: 100%;
- max-width: 100%;
- table-layout: fixed;
- border-spacing: 0;
-}
-
-.recent-votes__table td {
- padding: 0;
-}
-
-.recent-votes__table td:nth-child(1) { width: 100%; }
-.recent-votes__table td:nth-child(2) { width: 2em; text-align: right; }
-.recent-votes__table td:nth-child(3) { width: 7em; text-align: right; }
-
-body.lightbox-open {
- overflow: hidden;
-}
-
-.lightbox {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.9);
- display: flex;
- align-items: center;
- overflow: hidden;
-}
-
-.lightbox__image-current {
- width: 84%;
- display: flex;
- justify-content: center;
-}
-
-.lightbox__image-left {
- width: 8%;
- position: relative;
-}
-
-.lightbox__image-left .lightbox__img {
- position: absolute;
- right: 5px;
-}
-
-.lightbox__image-right {
- width: 8%;
- position: relative;
-}
-
-.lightbox__image-right .lightbox__img {
- position: absolute;
- left: 5px;
-}
-
-@media (min-width: 576px) {
- .lightbox__image-left .lightbox__img { right: 30px; }
- .lightbox__image-right .lightbox__img { left: 30px; }
-}
-
-.lightbox__img {
- display: block;
- border-radius: 4px;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
-}
-
-.lightbox__close {
- display: block;
- width: 40px;
- height: 40px;
- position: absolute;
- top: 0;
- right: 0;
-}
-
-.lightbox__close::before, .lightbox__close::after {
- position: absolute;
- top: 10px; /* (40 (container height) - 20 (stroke height)) / 2 */
- left: 19px; /* (40 (container width) - 2 (stroke width)) / 2 */
- content: '';
- height: 20px;
- width: 2px;
- background: #fff;
- transform: rotate(-45deg);
-}
-
-.lightbox__close::after {
- transform: rotate(45deg);
-}
-.lightbox__meta {
- display: block;
- width: 100%;
- height: 30px;
- position: absolute;
- bottom: 0;
- left: 0;
- background: #fff;
- text-align: center;
-}
-.lightbox__dims {
- font-weight: bold;
- margin-right: 25px;
-}
-
-.doc-list {
- font-size: 14px;
-}
-
-.doc-list__title {
- margin-top: 20px;
- font-weight: bold;
- font-size: 16px;
-}
-
-.doc-list__doc {
- margin-top: 5px;
- display: block;
- color: #6b6b6b;
-}
-
-a.doc-list__doc:hover, .doc-list__doc--active {
- color: #171717;
- text-decoration: none;
-}
-
-.doc h1 {
- margin-top: 20px;
- font-size: 30px;
- font-weight: bold;
- line-height: 1.1;
-}
-.doc h2 {
- margin-top: 20px;
-}
-.doc h3 {
- margin-top: 15px;
-}
-.doc h2 a, .doc h3 a {
- color: #000;
-}
-.doc dt {
- font-weight: normal;
- font-style: italic;
-}
-.doc dd {
- padding-left: 1em;
-}
-.doc pre {
- overflow-x: auto;
- font-size: 12px;
-}
-.doc td, .doc th {
- padding: 2px 4px;
- font-size: 12px;
-}
-
-.screenshot-edit { font-size: 14px; }
-.screenshot-edit__title { font-size: 16px; font-weight: 500; }
-.screenshot-edit__row { padding: 4px 0; }
-.screenshot-edit__screenshot { max-width: 200px; margin-bottom: 4px; }
-.screenshot-edit__fields { width: 100%; margin-bottom: 4px; }
-.screenshot-edit__remove { float: right; }
-.screenshot-edit__options { margin-bottom: 8px;}
-.screenshot-edit__upload-options { margin-top: 8px; }
-.screenshot-edit__upload-option { margin-bottom: 8px; }
-
-@media (min-width: 576px) {
- .screenshot-edit__row { display: flex; align-items: center; }
- .screenshot-edit__screenshot { width: 200px; }
- .screenshot-edit__fields { flex: 1; margin-left: 20px; }
-}
-
-@media (min-width: 992px) {
- .screenshot-edit__upload-options { display: flex; align-items: center; }
- .screenshot-edit__upload-option { margin-bottom: 0; }
- .screenshot-edit__upload-nsfw-label { margin-left: 8px; }
-}
-
-.flex-center-container {
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.small-card {
- max-width: 500px;
-}
-
-.character-card {
- display: flex;
- flex-direction: column;
-}
-
-.character-card__image {
- height: 100px;
- width: auto;
-}
-
-.character-card__left {
- flex: 1;
- display: flex;
-}
-
-.character-card__right {
- flex: 1;
- margin: 14px 20px;
- text-overflow: ellipsis;
- overflow: hidden;
- line-height: 24px;
- height: 72px;
-}
-
-.character-card__main {
- flex: 1;
- overflow: hidden;
- padding: 10px;
-}
-
-.character-card__image-container {
- width: 100px;
- overflow: hidden;
- flex: 0 0 auto;
-}
-
-.character-card__name {
- font-size: 20px;
-}
-
-.character-card__vns {
- font-size: 14px;
- margin-top: 4px;
- margin-bottom: 4px;
- width: 100%;
-}
-
-@media (min-width: 576px) {
- .character-card {
- height: 100px;
- flex-direction: row;
- }
-
- .character-card__left {
- width: 50%;
- }
-}
-
-.page-inner-controls {
- display: flex;
- justify-content: flex-end;
- align-items: center;
- margin-top: -20px;
- margin-bottom: 16px;
-}
-
-.page-inner-controls--left {
- justify-content: flex-start;
-}
-
-.page-inner-controls__option {
- display: inline-block;
- font-size: 14px;
- font-weight: 400;
- margin: 0 14px 0 0;
-}
-
-@media (min-width: 576px) {
- .page-inner-controls__option {
- margin: 0 0 0 14px;
- }
-
- .page-inner-controls--left .page-inner-controls__option {
- margin: 0 14px 0 0;
- }
-}
-
-.charpage--hide-ero .charpage--ero {
- display: none;
-}
-
-.charpage--hide-spoil-1 .charpage--spoil-1 {
- display: none;
-}
-
-.charpage--hide-spoil-2 .charpage--spoil-2 {
- display: none;
-}
-
-.trait-summary--trait {
- padding-right: 15px;
- white-space: nowrap;
-}
-
-@media (min-width: 768px) {
- .user-stats {
- display: flex;
- }
-}
-
-.user-stats__left {
- flex: 1 1 60%;
-}
-
-.user-stats__right {
- flex: 1 1 40%;
-}
-
-.big-stats {
- display: flex;
- font-size: 16px;
- font-weight: 500;
-}
-
-.big-stats__stat {
- display: block;
- flex: 1;
- margin-right: 25px;
- color: #3f3f3f;
-}
-
-a.big-stats__stat:hover {
- text-decoration: none;
-}
-
-.big-stats__value {
- font-size: 36px;
- font-weight: normal;
- line-height: 1.2;
-}
-
-.more-button {
- display: inline-block;
- width: 30px;
- height: 16px;
- background-color: #9c9c9c;
- border-radius: 2px;
- position: relative;
-}
-
-.more-button:hover {
- background-color: #8d8d8d;
-}
-
-.more-button--light {
- background-color: #fff;
-}
-
-.more-button--light:hover {
- background-color: #ddd;
-}
-
-.more-button__dots, .more-button__dots::before, .more-button__dots::after {
- position: absolute;
- width: 4px;
- height: 4px;
- border-radius: 4px;
- background-color: #fff;
-}
-
-.more-button--light .more-button__dots, .more-button--light .more-button__dots::before, .more-button--light .more-button__dots::after {
- background-color: #171717;
-}
-
-.more-button__dots {
- top: 6px;
- left: 13px;
-}
-
-.more-button__dots::before, .more-button__dots::after {
- content: '';
- top: 0;
-}
-
-.more-button__dots::before {
- left: -6px;
-}
-
-.more-button__dots::after {
- left: 6px;
-}
-
-.vn-list__expand-releases {
- color: #3F3F3F;
-}
-
-.vn-list__expand-comment .svg-icon {
- opacity: 0.39;
-}
-
-.vn-list__expand-comment--empty {
- opacity: 0.4;
-}
-
-.vn-list__expand-comment:hover .svg-icon {
- opacity: 0.45;
-}
-
-.vn-list__releases > table > tbody > tr > td:first-child {
- padding-left: 0;
-}
-
-.with-sort-icon {
- position: relative;
-}
-
-.with-sort-icon::after {
- content: '';
- position: absolute;
- right: 12px;
- top: 50%;
- visibility: hidden;
-}
-
-.with-sort-icon:hover::after, .with-sort-icon--active::after, .with-sort-icon:focus::after {
- visibility: visible;
-}
-
-.with-sort-icon--up::after {
- margin-top: -7px;
- border: 5px solid #999;
- border-color: transparent transparent #999 transparent;
-}
-
-.with-sort-icon--up.with-sort-icon--active::after {
- border-color: transparent transparent #171717 transparent;
-}
-
-.with-sort-icon--down::after {
- margin-top: -2px;
- margin-bottom: -5px;
- border: 5px solid #999;
- border-color: #999 transparent transparent transparent;
-}
-
-.with-sort-icon--down.with-sort-icon--active::after {
- border-color: #171717 transparent transparent transparent;
-}
-
-.table thead th a.table-header {
- color: #171717;
- text-decoration: none;
-}
-
-.vn-grid {
- /* will still work on browsers that don't support css grid, just not as nicely spaced/centered */
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
- justify-items: center;
-}
-
-.vn-grid__item {
- display: inline-block;
- width: 140px;
- height: 190px;
- background: #aaa;
- margin: 10px 5px;
- position: relative;
- border-radius: 4px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
-}
-
-.vn-grid__item-bg {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- background-size: cover;
- background-position: center;
- border-radius: 4px;
-}
-
-.vn-grid__item-overlay {
- position: relative;
- /* background: rgba(0, 0, 0, 0.5); */
- background: linear-gradient(to top, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0) 70%) repeat-x;
- height: 100%;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- border-radius: 4px;
-}
-
-.vn-grid__item-overlay:hover {
- background: rgba(0, 0, 0, 0.3);
-}
-
-.vn-grid__item-link {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
-}
-
-.vn-grid__item-top {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 9px 9px 4px 14px;
-}
-
-.vn-grid__item-top .more-button {
- display: none !important;
-}
-
-.vn-grid__item:hover .vn-grid__item-top .more-button, .vn-grid__item-top .dropdown--open .more-button {
- display: block !important;
-}
-
-.vn-grid__item-rating {
- background: white;
- padding: 1px 10px;
- height: 26px;
- line-height: 26px;
- border-radius: 100px;
- display: flex;
- align-items: center;
-}
-
-.vn-grid__item-rating .svg-icon {
- margin-right: 5px;
-}
-
-.vn-grid__item-name {
- color: white;
- padding: 9px 11px;
- font-size: 14px;
- font-weight: 500;
- line-height: 1.25;
- text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
-}
-
-/* list entry modal */
-.lem__release-remove {
- margin-top: 1px;
-}
-
-.raised-top-search {
- display: block;
- position: relative;
- padding-bottom: 16px;
-}
-
-.raised-top-search__field {
- font-size: 1.25em;
- padding: 10px 52px 10px 15px;
-}
-
-.raised-top-search__button {
- position: absolute;
- top: 1px;
- right: 1px;
- bottom: 17px;
- width: 50px;
- font-size: 18px;
- border-radius: 3px;
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
-}
-
-.raised-top-search__button .svg-icon {
- opacity: 0.5;
-}
-
-.filter-sidebar {
- background-color: #F5F8FA;
- margin: -30px 0 30px 0;
- box-shadow: inset rgba(33, 63, 80, 0.30) 0 12px 4px -10px;
- padding: 0;
-}
-
-.filter-sidebar__header {
- display: block;
- padding: 15px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- border-bottom: 1px solid #bebebe;
- font-weight: 500;
-}
-
-.filter-sidebar.open {
- box-shadow:
- inset rgba(33, 63, 80, 0.30) 0 12px 4px -10px,
- rgba(33, 63, 80, 0.2) 0 2px 4px;
-}
-
-.filter-sidebar__header, a.filter-sidebar__header {
- color: #3f3f3f;
- text-decoration: none;
-}
-
-.filter-sidebar .filter-sidebar__header .caret {
- transform: rotate(135deg);
-}
-
-.filter-sidebar.open .filter-sidebar__header .caret {
- transform: rotate(45deg);
-}
-
-.filter-sidebar__section {
- border-bottom: 1px solid #bebebe;
- display: none;
-}
-
-.filter-sidebar.open .filter-sidebar__section {
- display: block;
-}
-
-.filter-sidebar__section-header {
- padding: 13px 15px;
- font-weight: 500;
- border: none;
- background: transparent;
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-.filter-sidebar__section-header, a.filter-sidebar__section-header {
- color: inherit;
- text-decoration: none;
-}
-
-.filter-sidebar__section-header:hover {
- background-color: rgba(0, 0, 0, .03);
-}
-
-.filter-sidebar__section-header-text {
- color: #3f3f3f;
- position: relative;
-}
-
-.filter-sidebar__section.modified .filter-sidebar__section-header-text::after {
- content: '';
- width: 6px;
- height: 6px;
- border-radius: 3px;
- background-color: #5899bf;
- position: absolute;
- top: 10px;
- margin-left: 10px;
-}
-
-.filter-sidebar__section-header .caret {
- transform: rotate(135deg);
- margin-top: -2px;
-}
-
-.filter-sidebar__section.open .filter-sidebar__section-header .caret {
- transform: rotate(45deg);
-}
-
-.filter-sidebar__section-content {
- font-size: 14px;
- padding: 9px 15px 15px 15px;
- display: none;
-}
-
-.filter-sidebar__section.open .filter-sidebar__section-content {
- display: block;
-}
-
-@media (min-width: 992px) {
- .filter-sidebar {
- border-left: 1px solid #bebebe;
- border-right: 1px solid #bebebe;
- margin-bottom: -30px;
- padding-bottom: 55px;
- min-height: 100vh;
- /* fade out at bottom */
- -webkit-mask-image: linear-gradient(0deg, transparent, black 30px);
- mask-image: linear-gradient(0deg, transparent, black 30px);
- }
-
- .filter-sidebar.open {
- box-shadow: inset rgba(33, 63, 80, 0.30) 0 12px 4px -10px;
- }
-
- .filter-sidebar__header {
- display: none;
- }
-
- .filter-sidebar__section {
- display: block;
- }
-}
-
-.filter {
- color: #3f3f3f;
- margin-bottom: 8px;
-}
-
-.filter__header {
- display: flex;
- align-items: center;
- height: 30px;
- justify-content: space-between;
- align-items: center;
-}
-
-.filter__title {
- font-weight: 500;
-}
-
-.filter__actions {
- font-size: 12px;
- font-weight: 500;
-}
-
-.filter-select-list__option {
- margin: 0 -15px;
- padding: 0 15px 0 28px;
- height: 30px;
- display: flex;
- align-items: center;
- position: relative;
-}
-
-.filter-select-list__option, a.filter-select-list__option {
- text-decoration: none;
- color: inherit;
-}
-
-.filter-select-list__option:hover {
- background-color: #ecf1f5;
-}
-
-.filter-select-list__option.on {
- background-color: #e6edf2;
-}
-
-.filter-select-list__option.on::after {
- content: '✓';
- position: absolute;
- right: 15px;
-}
-
-.filter-select-inline {
- display: flex;
- align-items: center;
-}
-
-.filter-select-inline__options {
- display: flex;
- flex: 1;
- justify-content: flex-end;
-}
-
-.filter-select-inline__option {
- padding: 2px 8px;
- font-weight: 500;
- border-radius: 3px;
- margin-left: 2px;
-}
-
-.filter-select-inline__option, a.filter-select-inline__option {
- color: #6b6b6b;
- text-decoration: none;
-}
-
-.filter-select-inline__option:hover {
- background-color: #e6edf2;
-}
-
-.filter-select-inline__option.on {
- background-color: #dfe9f0;
- color: #3f3f3f;
-}
-
-.filter-date__row {
- display: flex;
-}
-
-.filter-date__row > select {
- flex: 1;
- margin-right: 10px;
-}
-
-.filter-date__row > select:last-child {
- margin-right: 0;
-}
-
-.filter-searchlist__row {
- display: flex;
- align-items: center;
- padding: 2px 0 2px 10px;
-}
-
-.filter-searchlist__row-title {
- flex: 1;
-}
-
-.filter-searchlist__row-remove {
- width: 21px;
- height: 21px;
-}
-
-.filter-searchlist__row-remove::before, .filter-searchlist__row-remove::after {
- top: 5px;
- left: 10px;
- height: 11px;
-}
-
-.filter-searchlist__add-container {
- padding: 5px 0 0 10px;
-}
-
-.card-list--dense > .card {
- margin-bottom: 7px;
-}
-
-.vn-card {
- display: flex;
- flex-direction: column;
- font-size: 14px;
- flex-direction: row;
-}
-
-.vn-card__left {
- display: flex;
- min-width: 0;
-}
-
-.vn-card__right {
- text-overflow: ellipsis;
- overflow: hidden;
- min-width: 70px;
- display: flex;
- flex-direction: column;
- justify-content: center;
- padding-right: 10px;
- flex: 1;
-}
-
-.vn-card__image-container {
- width: 50px;
- overflow: hidden;
- flex: 0 0 auto;
- display: flex;
- align-items: center;
- margin: 6px 2px 6px 6px;
-}
-
-.vn-card__image {
- height: 58px;
- width: auto;
- border-radius: 4px;
-}
-
-.vn-card__main {
- flex: 1;
- overflow: hidden;
- padding: 0 5px;
- display: flex;
- flex-direction: column;
- justify-content: center;
- padding: 4px;
-}
-
-.vn-card__name {
- margin-bottom: 2px;
- color: #003E86;
-}
-
-.vn-card__sub {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
-}
-
-.vn-card__date {
- color: #3f3f3f;
- margin-right: 20px;
- width: 100%;
- font-size: 12px;
-}
-
-.vn-card__langs {
- display: flex;
- margin-right: 20px;
-}
-
-.vn-card__langs .lang-badge {
- margin-right: 2px;
- padding: 0 .6em;
- height: 16px;
- line-height: 14px;
- font-size: 8px;
-}
-
-.vn-card__langs .lang-badge:last-child {
- margin-right: 0;
-}
-
-.vn-card__platforms {
- opacity: 0.58;
-}
-
-.vn-card__right .svg-icon {
- font-size: 14px;
- opacity: 0.45;
- margin-left: 7px;
-}
-
-.vn-card__rating {
- display: flex;
- justify-content: flex-end;
- align-items: center;
- font-size: 16px;
- color: #3f3f3f;
- font-weight: 500;
- line-height: 1;
- margin-bottom: 7px;
-}
-
-.vn-card__popularity {
- display: flex;
- justify-content: flex-end;
- align-items: center;
- color: #8d8d8d;
- line-height: 1;
-}
-
-@media (min-width: 576px) {
- .vn-card {
- height: 58px;
- }
-
- .vn-card__image-container {
- margin: 0;
- }
-
- .vn-card__image {
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- }
-
- .vn-card__main {
- padding: 0px;
- }
-
- .vn-card__date {
- color: #3f3f3f;
- margin-right: 20px;
- width: auto;
- font-size: inherit;
- }
-}
diff --git a/data/js/misc.js b/data/js/misc.js
index e5b135d0..fd042524 100644
--- a/data/js/misc.js
+++ b/data/js/misc.js
@@ -248,21 +248,6 @@ if(byId('not') && byId('vns'))
})();
-// "check all" checkbox
-(function(){
- function set() {
- var l = byName('input');
- for(var i=0; i<l.length; i++)
- if(l[i].type == this.type && l[i].name == this.name && !hasClass(l[i], 'hidden'))
- l[i].checked = this.checked;
- }
- var l = byClass('input', 'checkall');
- for(var i=0; i<l.length; i++)
- if(l[i].type == 'checkbox')
- l[i].onclick = set;
-})();
-
-
// search tabs
(function(){
function click() {
diff --git a/data/style.css b/data/style.css
index d9736401..dd0df17e 100644
--- a/data/style.css
+++ b/data/style.css
@@ -418,10 +418,10 @@ div.vnimg p { text-align: center; padding: 0px; margin: 0; }
.vndesc h2 { margin: 5px 0 0 0; }
.vndesc p { padding: 0 0 0 5px; }
p#nsfw_hid { display: block; cursor: pointer; }
-div.vndetails table { float: left; width: 500px; }
-div.vndetails table td.key { width: 90px; }
-div.vndetails table dt { float: left; font-style: italic; }
-div.vndetails table dd { margin-left: 90px; }
+div.vndetails > table { float: left; width: 500px; }
+div.vndetails > table td.key { width: 90px; }
+div.vndetails > table dt { float: left; font-style: italic; }
+div.vndetails > table dd { margin-left: 90px; }
div.vndetails td.relations dt { float: none; font-style: normal; }
div.vndetails td.relations dd { margin-left: 15px; }
div.vndetails td.anime b { font-size: 10px; font-weight: normal; padding-right: 4px; }
@@ -564,8 +564,8 @@ div.scr_uploader { visibility: hidden; overflow: hidden; width: 1px; height:
.vnbrowse .tc3 { padding: 0; }
.vnbrowse .tc5 { text-align: right; padding-right: 10px }
.vnbrowse .tc6 { width: 80px }
-.vnbrowse .tc7 { text-align: right; width: 8px }
-.vnbrowse .tc8 { width: 8px }
+.vnbrowse .tc7 { text-align: right; width: 8px; white-space: nowrap }
+.vnbrowse .tc7 abbr { display: inline-block; width: 20px; }
diff --git a/elm/ColSelect.elm b/elm/ColSelect.elm
index 049cc369..93c9a093 100644
--- a/elm/ColSelect.elm
+++ b/elm/ColSelect.elm
@@ -3,13 +3,13 @@
--
-- ?c=column_id&c=modified&...
--
--- Accepts a list of columns from Perl, e.g.:
+-- Accepts a [ $current_url, [ list of columns ] ] from Perl, e.g.:
--
--- [
+-- [ '?c=column_id', [
-- [ 'column_id', 'Column Label' ],
-- [ 'modified', 'Date modified' ],
-- ...
--- ]
+-- ] ]
module ColSelect exposing (main)
import Html exposing (..)
diff --git a/elm/ColSelect.js b/elm/ColSelect.js
deleted file mode 100644
index e6812fab..00000000
--- a/elm/ColSelect.js
+++ /dev/null
@@ -1,5 +0,0 @@
-var init = Elm.ColSelect.init;
-Elm.ColSelect.init = function(opt) {
- opt.flags = [ location.href, opt.flags ];
- return init(opt);
-};
diff --git a/elm/UList/LabelEdit.elm b/elm/UList/LabelEdit.elm
index 28f2cc68..471f6eb6 100644
--- a/elm/UList/LabelEdit.elm
+++ b/elm/UList/LabelEdit.elm
@@ -1,4 +1,4 @@
-port module UList.LabelEdit exposing (main)
+port module UList.LabelEdit exposing (main, init, update, view, isPublic, Model, Msg)
import Html exposing (..)
import Html.Attributes exposing (..)
@@ -50,6 +50,9 @@ type Msg
| Saved Int Bool GApi.Response
+isPublic : Model -> Bool
+isPublic model = List.any (\lb -> lb.id /= 7 && not lb.private && Set.member lb.id model.sel) model.labels
+
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
@@ -65,8 +68,7 @@ update msg model =
Saved l b (GApi.Success) ->
let nmodel = { model | sel = if b then Set.insert l model.sel else Set.remove l model.sel, state = Dict.remove l model.state }
- public = List.any (\lb -> lb.id /= 7 && not lb.private && Set.member lb.id nmodel.sel) nmodel.labels
- in (nmodel, ulistLabelChanged public)
+ in (nmodel, ulistLabelChanged (isPublic nmodel))
Saved l b e -> ({ model | state = Dict.insert l (Api.Error e) model.state }, Cmd.none)
diff --git a/elm/UList/VNPage.elm b/elm/UList/VNPage.elm
new file mode 100644
index 00000000..03fbaf6b
--- /dev/null
+++ b/elm/UList/VNPage.elm
@@ -0,0 +1,127 @@
+module UList.VNPage exposing (main)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Browser
+import Lib.Html exposing (..)
+import Lib.Api as Api
+import Lib.DropDown as DD
+import Gen.Api as GApi
+import Gen.UListDel as GDE
+import Gen.UListAdd as GAD
+import UList.LabelEdit as LE
+import UList.VoteEdit as VE
+
+-- We don't have a Gen.* module for this (yet), so define these manually
+type alias RecvLabels =
+ { id : Int
+ , label : String
+ , private : Bool
+ }
+
+type alias Recv =
+ { uid : Int
+ , vid : Int
+ , onlist : Bool
+ , canvote : Bool
+ , vote : Maybe String
+ , labels : List RecvLabels
+ , selected : List Int
+ }
+
+
+main : Program Recv Model Msg
+main = Browser.element
+ { init = \f -> (init f, Cmd.none)
+ , subscriptions = \model -> Sub.map Labels (DD.sub model.labels.dd)
+ , view = view
+ , update = update
+ }
+
+type alias Model =
+ { flags : Recv
+ , onlist : Bool
+ , del : Bool
+ , state : Api.State -- For adding/deleting; Vote and label edit widgets have their own state
+ , labels : LE.Model
+ , vote : VE.Model
+ }
+
+init : Recv -> Model
+init f =
+ { flags = f
+ , onlist = f.onlist
+ , del = False
+ , state = Api.Normal
+ , labels = LE.init { uid = f.uid, vid = f.vid, labels = f.labels, selected = f.selected }
+ , vote = VE.init { uid = f.uid, vid = f.vid, vote = f.vote }
+ }
+
+type Msg
+ = Add
+ | Added GApi.Response
+ | Labels LE.Msg
+ | Vote VE.Msg
+ | Del Bool
+ | Delete
+ | Deleted GApi.Response
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+ case msg of
+ Labels m -> let (nm, cmd) = LE.update m model.labels in ({ model | labels = nm}, Cmd.map Labels cmd)
+ Vote m -> let (nm, cmd) = VE.update m model.vote in ({ model | vote = nm}, Cmd.map Vote cmd)
+
+ Add -> ({ model | state = Api.Loading }, Api.post "/u/ulist/add.json" (GAD.encode { uid = model.flags.uid, vid = model.flags.vid }) Added)
+ Added GApi.Success -> ({ model | state = Api.Normal, onlist = True }, Cmd.none)
+ Added e -> ({ model | state = Api.Error e }, Cmd.none)
+
+ Del b -> ({ model | del = b }, Cmd.none)
+ Delete -> ({ model | state = Api.Loading }, Api.post "/u/ulist/del.json" (GDE.encode { uid = model.flags.uid, vid = model.flags.vid }) Deleted)
+ Deleted GApi.Success -> ({ model | state = Api.Normal, onlist = False, del = False }, Cmd.none)
+ Deleted e -> ({ model | state = Api.Error e }, Cmd.none)
+
+
+isPublic : Model -> Bool
+isPublic model =
+ LE.isPublic model.labels
+ || (model.vote.text /= "" && model.vote.text /= "-" && List.any (\l -> l.id == 7 && not l.private) model.labels.labels)
+
+
+view : Model -> Html Msg
+view model =
+ case model.state of
+ Api.Loading -> div [ class "spinner" ] []
+ Api.Error e -> b [ class "standout" ] [ text <| Api.showResponse e ]
+ Api.Normal ->
+ if not model.onlist
+ then a [ href "#", onClickD Add ] [ text "Add to list" ]
+ else if model.del
+ then
+ span []
+ [ text "Sure you want to remove this VN from your list? "
+ , a [ onClickD Delete ] [ text "Yes" ]
+ , text " | "
+ , a [ onClickD (Del False) ] [ text "Cancel" ]
+ ]
+ else
+ table [ style "width" "100%" ]
+ [ tr [ class "nostripe" ]
+ [ td [ style "width" "70px" ] [ text "Labels:" ]
+ , td [] [ Html.map Labels (LE.view model.labels) ]
+ ]
+ , if model.flags.canvote || (Maybe.withDefault "-" model.flags.vote /= "-")
+ then tr [ class "nostripe" ]
+ [ td [] [ text "Vote:" ]
+ , td [ class "compact stealth" ] [ Html.map Vote (VE.view model.vote) ]
+ ]
+ else text ""
+ , tr [ class "nostripe" ]
+ [ td [ colspan 2 ]
+ [ span [ classList [("invisible", not (isPublic model))], title "This visual novel is on your public list" ] [ text "👁 " ]
+ , a [ onClickD (Del True) ] [ text "Remove from list" ]
+ ]
+ ]
+ ]
diff --git a/elm/UList/VoteEdit.elm b/elm/UList/VoteEdit.elm
index 380cb6c8..058eb5aa 100644
--- a/elm/UList/VoteEdit.elm
+++ b/elm/UList/VoteEdit.elm
@@ -1,4 +1,4 @@
-port module UList.VoteEdit exposing (main)
+port module UList.VoteEdit exposing (main, init, update, view, Model, Msg)
import Html exposing (..)
import Html.Attributes exposing (..)
@@ -85,6 +85,7 @@ view model =
, onBlur Save
, onFocus Focus
, placeholder "7.5"
+ , style "width" "55px"
, custom "keydown" -- Grab enter key
<| JD.andThen (\c -> if c == "Enter" then JD.succeed { preventDefault = True, stopPropagation = True, message = Save } else JD.fail "")
<| JD.field "key" JD.string
diff --git a/elm/User/Edit.elm b/elm/User/Edit.elm
index 47d7d1cb..121e283a 100644
--- a/elm/User/Edit.elm
+++ b/elm/User/Edit.elm
@@ -52,7 +52,6 @@ type Data
| EMail String
| Perm Int Bool
| IgnVotes Bool
- | HideList Bool
| ShowNsfw Bool
| TraitsSexual Bool
| Spoilers Int
@@ -76,7 +75,6 @@ updateData msg model =
EMail n -> { model | email = n }
Perm n b -> { model | perm = if b then or model.perm n else and model.perm (complement n) }
IgnVotes n -> { model | ign_votes = n }
- HideList b -> { model | hide_list = b }
ShowNsfw b -> { model | show_nsfw = b }
TraitsSexual b -> { model | traits_sexual = b }
Spoilers n -> { model | spoilers = n }
@@ -189,12 +187,6 @@ view model =
] ++ (if model.cpass then passform else [])
++
[ tr [ class "newpart" ] [ td [ colspan 2 ] [ text "Preferences" ] ]
- , formField "Privacy"
- [ label []
- [ inputCheck "" data.hide_list (Set << HideList)
- , text " Don't allow others to see my visual novel list, vote list and wishlist and exclude these lists from the database dumps and API."
- ]
- ]
, formField "NSFW" [ label [] [ inputCheck "" data.show_nsfw (Set << ShowNsfw), text " Show NSFW images by default" ] ]
, formField "" [ label [] [ inputCheck "" data.traits_sexual (Set << TraitsSexual), text " Show sexual traits by default on character pages" ] ]
, formField "Tags" [ label [] [ inputCheck "" data.tags_all (Set << TagsAll), text " Show all tags by default on visual novel pages (don't summarize)" ] ]
diff --git a/elm3/CharEdit/General.elm b/elm3/CharEdit/General.elm
deleted file mode 100644
index ee0dbd77..00000000
--- a/elm3/CharEdit/General.elm
+++ /dev/null
@@ -1,260 +0,0 @@
-module CharEdit.General exposing (..)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import File exposing (File)
-import Lib.Html exposing (..)
-import Lib.Autocomplete as A
-import Lib.Util exposing (..)
-import Lib.Gen as Gen
-import Lib.Api as Api
-
-
-type alias Model =
- { alias : String
- , aliasDuplicates : Bool
- , bDay : Int
- , bMonth : Int
- , bloodt : String
- , desc : String
- , gender : String
- , height : Int
- , image : Int
- , imgState : Api.State
- , name : String
- , original : String
- , sBust : Int
- , sHip : Int
- , sWaist : Int
- , weight : Maybe Int
- , mainIs : Bool
- , mainInstance : Bool
- , mainId : Int
- , mainSpoil : Int
- , mainName : String
- , mainSearch : A.Model Gen.ApiCharResult
- }
-
-
-init : Gen.CharEdit -> Model
-init d =
- { alias = d.alias
- , aliasDuplicates = False
- , bDay = d.b_day
- , bMonth = d.b_month
- , bloodt = d.bloodt
- , desc = d.desc
- , gender = d.gender
- , height = d.height
- , image = d.image
- , imgState = Api.Normal
- , name = d.name
- , original = d.original
- , sBust = d.s_bust
- , sHip = d.s_hip
- , sWaist = d.s_waist
- , weight = d.weight
- , mainIs = d.main_is
- , mainInstance = isJust d.main
- , mainId = Maybe.withDefault 0 d.main
- , mainSpoil = d.main_spoil
- , mainName = d.main_name
- , mainSearch = A.init
- }
-
-
-new : Model
-new =
- { alias = ""
- , aliasDuplicates = False
- , bDay = 0
- , bMonth = 0
- , bloodt = "unknown"
- , desc = ""
- , gender = "unknown"
- , height = 0
- , image = 0
- , imgState = Api.Normal
- , name = ""
- , original = ""
- , sBust = 0
- , sHip = 0
- , sWaist = 0
- , weight = Nothing
- , mainIs = False
- , mainInstance = False
- , mainId = 0
- , mainSpoil = 0
- , mainName = ""
- , mainSearch = A.init
- }
-
-
-searchConfig : A.Config Msg Gen.ApiCharResult
-searchConfig = { wrap = MainSearch, id = "add-main", source = A.charSource }
-
-
-type Msg
- = Name String
- | Original String
- | Alias String
- | Desc String
- | Image String
- | Gender String
- | Bloodt String
- | BMonth String
- | BDay String
- | SBust String
- | SWaist String
- | SHip String
- | Height String
- | Weight String
- | MainInstance Bool
- | MainSpoil String
- | MainSearch (A.Msg Gen.ApiCharResult)
- | ImgUpload (List File)
- | ImgDone Api.Response
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Name s -> ({ model | name = s }, Cmd.none)
- Original s -> ({ model | original = s }, Cmd.none)
- Alias s -> ({ model | alias = s, aliasDuplicates = hasDuplicates (model.name :: model.original :: splitLn s) }, Cmd.none)
- Desc s -> ({ model | desc = s }, Cmd.none)
- Image s -> ({ model | image = if s == "" then 0 else Maybe.withDefault model.image (String.toInt s) }, Cmd.none)
- Gender s -> ({ model | gender = s }, Cmd.none)
- Bloodt s -> ({ model | bloodt = s }, Cmd.none)
- BMonth s -> ({ model | bMonth = if s == "" then 0 else Maybe.withDefault model.bMonth (String.toInt s) }, Cmd.none)
- BDay s -> ({ model | bDay = if s == "" then 0 else Maybe.withDefault model.bDay (String.toInt s) }, Cmd.none)
- SBust s -> ({ model | sBust = if s == "" then 0 else Maybe.withDefault model.sBust (String.toInt s) }, Cmd.none)
- SWaist s -> ({ model | sWaist = if s == "" then 0 else Maybe.withDefault model.sWaist (String.toInt s) }, Cmd.none)
- SHip s -> ({ model | sHip = if s == "" then 0 else Maybe.withDefault model.sHip (String.toInt s) }, Cmd.none)
- Height s -> ({ model | height = if s == "" then 0 else Maybe.withDefault model.height (String.toInt s) }, Cmd.none)
- Weight s -> ({ model | weight = String.toInt s }, Cmd.none)
- MainInstance b->({ model | mainInstance = b }, Cmd.none)
- MainSpoil s -> ({ model | mainSpoil = Maybe.withDefault model.mainSpoil (String.toInt s) }, Cmd.none)
- MainSearch m ->
- let (nm, c, res) = A.update searchConfig m model.mainSearch
- in case res of
- Nothing -> ({ model | mainSearch = nm }, c)
- Just r ->
- -- If the selected char has a main, automatically select that as our main
- let chr = Maybe.withDefault {id = r.id, name = r.name, original = r.original } r.main
- in ({ model | mainId = chr.id, mainName = chr.name, mainSearch = A.clear nm }, c)
-
- ImgUpload [i] -> ({ model | imgState = Api.Loading }, Api.postImage Api.Ch i ImgDone)
- ImgUpload _ -> (model, Cmd.none)
-
- ImgDone (Gen.Image id _ _) -> ({ model | image = id, imgState = Api.Normal }, Cmd.none)
- ImgDone r -> ({ model | image = 0, imgState = Api.Error r }, Cmd.none)
-
-
-zeroEmpty : Int -> String
-zeroEmpty i = if i == 0 then "" else String.fromInt i
-
-
-view : Model -> Html Msg
-view model = card "general" "General info" []
-
- [ cardRow "Name" Nothing <| formGroups
- [ [ label [for "name"] [text "Name (romaji)"]
- , inputText "name" model.name Name [required True, maxlength 200]
- ]
- , [ label [for "original"] [text "Original"]
- , inputText "original" model.original Original [maxlength 200]
- , div [class "form-group__help"] [text "The character's name in the language of the visual novel, leave blank if it already is in the Latin alphabet."]
- ]
- , [ inputTextArea "aliases" model.alias Alias
- [ rows 4, maxlength 500
- , classList [("is-invalid", model.aliasDuplicates)]
- ]
- , if model.aliasDuplicates
- then div [class "invalid-feedback"]
- [ text "There are duplicate aliases." ]
- else text ""
- , div [class "form-group__help"] [ text "(Un)official aliases, separated by a newline." ]
- ]
- ]
-
- , cardRow "Description" (Just "English please!") <| formGroup
- [ inputTextArea "desc" model.desc Desc [rows 8] ]
-
- , cardRow "Image" Nothing
- [ div [class "row"]
- [ div [class "col-md col-md--1"]
- [ div [style "max-width" "200px", style "margin-bottom" "8px"]
- [ dbImg "ch" (if model.imgState == Api.Loading then -1 else model.image) [] Nothing ]
- ]
- , div [class "col-md col-md--2"] <| formGroups
- [ [ label [for "img"] [ text "Upload new image" ]
- , input [type_ "file", class "text", name "img", id "img", Api.onFileChange ImgUpload, disabled (model.imgState == Api.Loading) ] []
- , case model.imgState of
- Api.Error r -> div [class "invalid-feedback"] [text <| Api.showResponse r]
- _ -> text ""
- , div [class "form-group__help"]
- [ text "Image must be in JPEG or PNG format and at most 1MiB. Images larger than 256x300 will be resized automatically. Image must be safe for work!" ]
- ]
- , [ label [for "img_id"] [ text "Image ID" ]
- , inputText "img_id" (String.fromInt model.image) Image [pattern "^[0-9]+$", disabled (model.imgState == Api.Loading)]
- , div [class "form-group__help"]
- [ text "Use a character image that is already on the server. Set to '0' to remove the current image." ]
- ]
- ]
- ]
- ]
-
- , cardRow "Meta" Nothing <| formGroups
- [ [ label [for "sex"] [text "Sex"]
- , inputSelect [id "sex", onInput Gender] model.gender Gen.genders
- ]
- , [ label [for "bloodt"] [text "Blood type"]
- , inputSelect [id "bloodt", onInput Bloodt] model.bloodt Gen.bloodTypes
- ]
- -- TODO: Enforce that both or neither are set
- , [ label [for "b_month"] [text "Birthday"]
- , inputSelect [id "b_month", onInput BMonth, class "form-control--inline"] (String.fromInt model.bMonth)
- <| ("0", "--month--") :: List.map (\i -> (String.fromInt i, String.fromInt i)) (List.range 1 12)
- , inputSelect [id "b_day", onInput BDay, class "form-control--inline"] (String.fromInt model.bDay)
- <| ("0", "--day--" ) :: List.map (\i -> (String.fromInt i, String.fromInt i)) (List.range 1 31)
- ]
- -- XXX: This looks messy
- , [ label [] [ text "Measurements" ]
- , p []
- [ text "Bust (cm): ", inputText "s_bust" (zeroEmpty model.sBust ) SBust [class "form-control--inline", style "width" "4em", pattern "^[0-9]{0,5}$"]
- , text " Waist (cm): ", inputText "s_waist" (zeroEmpty model.sWaist) SWaist [class "form-control--inline", style "width" "4em", pattern "^[0-9]{0,5}$"]
- , text " Hip (cm): ", inputText "s_hip" (zeroEmpty model.sHip ) SHip [class "form-control--inline", style "width" "4em", pattern "^[0-9]{0,5}$"]
- ]
- , p []
- [ text "Height (cm): ", inputText "height" (zeroEmpty model.height) Height [class "form-control--inline", style "width" "5em", pattern "^[0-9]{0,5}$"]
- , text " Weight (kg): ",inputText "weight" (Maybe.withDefault "" <| Maybe.map String.fromInt model.weight) Weight [class "form-control--inline", style "width" "5em", pattern "^[0-9]{0,5}$"]
- ]
- ]
- ]
-
- , cardRow "Instance" Nothing <|
- if model.mainIs
- then formGroup [ div [class "form-group__help"]
- [ text "This character is already referenced as \"main\" from another character entry."
- , text " If you want link this entry to another character, please edit that other character instead."
- ]
- ]
- else formGroups <|
- [ label [class "checkbox"] [ inputCheck "" model.mainInstance MainInstance, text " This character is an instance of another character" ] ]
- :: if not model.mainInstance then [] else
- [ [ if model.mainId == 0
- then div [] [ text "No character selected." ]
- else div []
- [ text "Main character: "
- , span [class "muted"] [ text <| "c" ++ String.fromInt model.mainId ++ ":" ]
- , a [href <| "/c" ++ String.fromInt model.mainId, target "_blank"] [ text model.mainName ]
- ]
- ]
- , if model.mainId == 0 then [] else
- [ inputSelect [id "mainspoil", onInput MainSpoil, class "form-control--inline"] (String.fromInt model.mainSpoil) spoilLevels ]
- , A.view searchConfig model.mainSearch [placeholder "Character name...", style "max-width" "400px"]
- ]
-
- ]
diff --git a/elm3/CharEdit/Main.elm b/elm3/CharEdit/Main.elm
deleted file mode 100644
index dbb88788..00000000
--- a/elm3/CharEdit/Main.elm
+++ /dev/null
@@ -1,130 +0,0 @@
-module CharEdit.Main exposing (Model,Msg,main,new,update,view)
-
-import Html exposing (..)
-import Html.Lazy exposing (..)
-import Browser
-import Browser.Navigation exposing (load)
-import Lib.Html exposing (..)
-import Lib.Gen as Gen
-import Lib.Api as Api
-import Lib.Editsum as Editsum
-import CharEdit.General as General
-import CharEdit.Traits as Traits
-import CharEdit.VN as VN
-
-
-main : Program Gen.CharEdit Model Msg
-main = Browser.element
- { init = \e -> (init e, Cmd.none)
- , view = view
- , update = update
- , subscriptions = always Sub.none
- }
-
-
-type alias Model =
- { state : Api.State
- , editsum : Editsum.Model
- , general : General.Model
- , traits : Traits.Model
- , vn : VN.Model
- , id : Maybe Int
- }
-
-
-init : Gen.CharEdit -> Model
-init d =
- { state = Api.Normal
- , editsum = { authmod = d.authmod, editsum = d.editsum, locked = d.locked, hidden = d.hidden }
- , general = General.init d
- , traits = Traits.init d.traits
- , vn = VN.init d.vns d.vnrels
- , id = d.id
- }
-
-
-new : List Gen.CharEditVns -> List Gen.CharEditVnrels -> Model
-new vns vnrels =
- { state = Api.Normal
- , editsum = Editsum.new
- , general = General.new
- , traits = Traits.init []
- , vn = VN.init vns vnrels
- , id = Nothing
- }
-
-
-encode : Model -> Gen.CharEditSend
-encode model =
- { editsum = model.editsum.editsum
- , hidden = model.editsum.hidden
- , locked = model.editsum.locked
- , alias = model.general.alias
- , b_day = model.general.bDay
- , b_month = model.general.bMonth
- , bloodt = model.general.bloodt
- , desc = model.general.desc
- , gender = model.general.gender
- , height = model.general.height
- , image = model.general.image
- , name = model.general.name
- , original = model.general.original
- , s_bust = model.general.sBust
- , s_hip = model.general.sHip
- , s_waist = model.general.sWaist
- , weight = model.general.weight
- , main = if not model.general.mainInstance || model.general.mainId == 0 then Nothing else Just model.general.mainId
- , main_spoil = model.general.mainSpoil
- , traits = List.map (\e -> { tid = e.tid, spoil = e.spoil }) model.traits.traits
- , vns = VN.encode model.vn
- }
-
-
-type Msg
- = Editsum Editsum.Msg
- | General General.Msg
- | Traits Traits.Msg
- | VN VN.Msg
- | Submit
- | Submitted Api.Response
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Editsum m -> ({ model | editsum = Editsum.update m model.editsum }, Cmd.none)
- General m -> let (nm, c) = General.update m model.general in ({ model | general = nm }, Cmd.map General c)
- Traits m -> let (nm, c) = Traits.update m model.traits in ({ model | traits = nm }, Cmd.map Traits c)
- VN m -> let (nm, c) = VN.update m model.vn in ({ model | vn = nm }, Cmd.map VN c)
-
- Submit ->
- let
- path =
- case model.id of
- Just id -> "/c" ++ String.fromInt id ++ "/edit"
- Nothing -> "/c/add"
- body = Gen.chareditSendEncode (encode model)
- in ({ model | state = Api.Loading }, Api.post path body Submitted)
-
- Submitted (Gen.Changed id rev) -> (model, load <| "/c" ++ String.fromInt id ++ "." ++ String.fromInt rev)
- Submitted r -> ({ model | state = Api.Error r }, Cmd.none)
-
-
-isValid : Model -> Bool
-isValid model = not
- ( model.general.aliasDuplicates
- || (model.general.mainInstance && model.general.mainId == 0)
- || model.traits.duplicates
- || model.vn.duplicates
- )
-
-
-view : Model -> Html Msg
-view model =
- form_ Submit (model.state == Api.Loading)
- [ Html.map General <| lazy General.view model.general
- , Html.map Traits <| lazy Traits.view model.traits
- , Html.map VN <| lazy VN.view model.vn
- , Html.map Editsum <| lazy Editsum.view model.editsum
- , submitButton "Submit" model.state (isValid model) False
- ]
diff --git a/elm3/CharEdit/New.elm b/elm3/CharEdit/New.elm
deleted file mode 100644
index 1a43341a..00000000
--- a/elm3/CharEdit/New.elm
+++ /dev/null
@@ -1,19 +0,0 @@
-module CharEdit.New exposing (main)
-
-import Browser
-import Lib.Gen exposing (CharEditVnrels, CharEditVns)
-import CharEdit.Main as Main
-
-
-type alias Flags =
- { vnrels : List CharEditVnrels
- , vns : List CharEditVns
- }
-
-main : Program Flags Main.Model Main.Msg
-main = Browser.element
- { init = \f -> (Main.new f.vns f.vnrels, Cmd.none)
- , view = Main.view
- , update = Main.update
- , subscriptions = always Sub.none
- }
diff --git a/elm3/CharEdit/Traits.elm b/elm3/CharEdit/Traits.elm
deleted file mode 100644
index 60399452..00000000
--- a/elm3/CharEdit/Traits.elm
+++ /dev/null
@@ -1,81 +0,0 @@
-module CharEdit.Traits exposing (Model, Msg, init, update, view)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Lib.Html exposing (..)
-import Lib.Autocomplete as A
-import Lib.Gen as Gen
-import Lib.Util exposing (..)
-
-
-type alias Model =
- { traits : List Gen.CharEditTraits
- , search : A.Model Gen.ApiTraitResult
- , duplicates : Bool
- }
-
-
-init : List Gen.CharEditTraits -> Model
-init l =
- { traits = l
- , search = A.init
- , duplicates = False
- }
-
-
-type Msg
- = Del Int
- | SetSpoil Int String
- | Search (A.Msg Gen.ApiTraitResult)
-
-
-searchConfig : A.Config Msg Gen.ApiTraitResult
-searchConfig = { wrap = Search, id = "add-trait", source = A.traitSource }
-
-
-validate : Model -> Model
-validate model = { model | duplicates = hasDuplicates <| List.map .tid model.traits }
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Del i -> (validate { model | traits = delidx i model.traits }, Cmd.none)
- SetSpoil i s -> (validate { model | traits = modidx i (\e -> { e | spoil = Maybe.withDefault e.spoil (String.toInt s) }) model.traits }
- , Cmd.none )
-
- Search m ->
- let (nm, c, res) = A.update searchConfig m model.search
- in case res of
- Nothing -> ({ model | search = nm }, c)
- Just r ->
- let nrow = { tid = r.id, name = r.name, group = Maybe.withDefault "" r.group, spoil = 0 }
- in (validate { model | search = A.clear nm, traits = model.traits ++ [nrow] }, c)
-
-
-
-view : Model -> Html Msg
-view model =
- let
- entry n e = editListRow ""
- [ editListField 2 "col-form-label single-line"
- [ span [ class "muted" ] [ text <| e.group ++ " / " ]
- , a [href <| "/i" ++ String.fromInt e.tid, title e.name, target "_blank" ] [ text e.name ] ]
- , editListField 1 ""
- [ inputSelect [onInput (SetSpoil n)] (String.fromInt e.spoil) spoilLevels ]
- , editListField 0 "" [ removeButton (Del n) ]
- ]
-
- in card "traits" "Traits" []
- <| editList (List.indexedMap entry model.traits)
- ++ formGroups (
- (if model.duplicates
- then [ [ div [ class "invalid-feedback" ]
- [ text "There are duplicate traits." ] ] ]
- else []
- ) ++
- [ label [for "add-trait"] [text "Add trait"]
- :: A.view searchConfig model.search [placeholder "Trait", style "max-width" "400px"]
- ]
- )
diff --git a/elm3/CharEdit/VN.elm b/elm3/CharEdit/VN.elm
deleted file mode 100644
index 57a50c88..00000000
--- a/elm3/CharEdit/VN.elm
+++ /dev/null
@@ -1,186 +0,0 @@
-module CharEdit.VN exposing (Model, Msg, init, encode, update, view)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Json.Encode as JE
-import Dict exposing (Dict)
-import Lib.Html exposing (..)
-import Lib.Autocomplete as A
-import Lib.Gen as Gen
-import Lib.Util exposing (..)
-import Lib.Api as Api
-
-
-type alias VNRel =
- { id : Int
- , title : String
- , role : String
- , spoil : Int
- , relsel : Bool
- , rel : Dict Int { role : String, spoil : Int }
- }
-
-type alias Model =
- { vn : List VNRel
- , releases : Dict Int (List Gen.CharEditVnrelsReleases) -- Mapping from VN id -> list of releases
- , search : A.Model Gen.ApiVNResult
- , duplicates : Bool
- }
-
-
-init : List Gen.CharEditVns -> List Gen.CharEditVnrels -> Model
-init vns rels =
- -- Turn the array from the server into a more usable data structure. This assumes that the array is ordered by VN id.
- let
- merge o n = case n.rid of
- Nothing -> { o | role = n.role, spoil = n.spoil }
- Just i -> { o | relsel = True, rel = Dict.insert i { role = n.role, spoil = n.spoil } o.rel }
-
- new n = case n.rid of
- Nothing -> { id = n.vid, title = n.title, relsel = False, role = n.role, spoil = n.spoil, rel = Dict.empty }
- Just i -> { id = n.vid, title = n.title, relsel = True, role = "", spoil = 0, rel = Dict.fromList [(i, { role = n.role, spoil = n.spoil })] }
-
- step n l =
- case l of
- [] -> [ new n ]
- i::xs ->
- if i.id == n.vid
- then merge i n :: xs
- else new n :: l
- in
- { vn = List.foldr step [] vns
- , releases = Dict.fromList <| List.map (\n -> (n.id, n.releases)) rels
- , search = A.init
- , duplicates = False
- }
-
-
--- XXX: The model and the UI allow an invalid state: VN is present, but
--- role="". This isn't too obvious to trigger, I hope, so in this case we'll
--- just be lazy and not send the VN to the server.
-encode : Model -> List Gen.CharEditSendVns
-encode model =
- let
- vn e =
- (if e.role == "" then [] else [{ vid = e.id, rid = Nothing, role = e.role, spoil = e.spoil }])
- ++
- (if e.relsel then Dict.foldl (\id r l -> { vid = e.id, rid = Just id, role = r.role, spoil = r.spoil } :: l) [] e.rel else [])
- in List.concat <| List.map vn model.vn
-
-
-
-type Msg
- = Del Int
- | SetSel Int Bool
- | SetRole Int String
- | SetSpoil Int String
- | SetRRole Int Int String
- | SetRSpoil Int Int String
- | Search (A.Msg Gen.ApiVNResult)
- | ReleaseInfo Int Api.Response
-
-
-searchConfig : A.Config Msg Gen.ApiVNResult
-searchConfig = { wrap = Search, id = "add-vn", source = A.vnSource }
-
-
-validate : Model -> Model
-validate model = { model | duplicates = hasDuplicates <| List.map .id model.vn }
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- let
- rrole s o_ = if s == "" then Nothing
- else Just <| case o_ of
- Nothing -> { role = s, spoil = 0 }
- Just o -> { o | role = s }
- rspoil s = Maybe.map (\o -> { o | spoil = Maybe.withDefault o.spoil (String.toInt s) })
- in case msg of
- Del i -> (validate { model | vn = delidx i model.vn }, Cmd.none)
- SetSel i b -> ({ model | vn = modidx i (\e -> { e | relsel = b }) model.vn }, Cmd.none)
- SetRole i s -> ({ model | vn = modidx i (\e -> { e | role = s }) model.vn }, Cmd.none)
- SetSpoil i s -> ({ model | vn = modidx i (\e -> { e | spoil = Maybe.withDefault e.spoil (String.toInt s) }) model.vn }, Cmd.none)
- SetRRole i id s -> ({ model | vn = modidx i (\e -> { e | rel = Dict.update id (rrole s) e.rel }) model.vn }, Cmd.none)
- SetRSpoil i id s -> ({ model | vn = modidx i (\e -> { e | rel = Dict.update id (rspoil s) e.rel }) model.vn }, Cmd.none)
-
- Search m ->
- let (nm, c, res) = A.update searchConfig m model.search
- in case res of
- Nothing -> ({ model | search = nm }, c)
- Just r ->
- let
- nrow = { id = r.id, title = r.title, relsel = False, role = "primary", spoil = 0, rel = Dict.empty }
- nc = case Dict.get r.id model.releases of
- Nothing -> Api.post "/js/release.json" (JE.object [("vid", JE.int r.id)]) (ReleaseInfo r.id)
- Just _ -> Cmd.none
- in (validate { model | search = A.clear nm, vn = model.vn ++ [nrow] }, Cmd.batch [c, nc])
-
- ReleaseInfo vid (Gen.ReleaseResult r) -> ({ model | releases = Dict.insert vid r model.releases}, Cmd.none)
- ReleaseInfo _ _ -> (model, Cmd.none)
-
-
-
-view : Model -> Html Msg
-view model =
- let
- vn n e = editList <|
- editListRow ""
- [ editListField 3 "col-form-label single-line"
- [ span [ class "muted" ] [ text <| "v" ++ String.fromInt e.id ++ ":" ]
- , a [ href <| "/v" ++ String.fromInt e.id, target "_blank" ] [ text e.title ]
- ]
- , editListField 0 "" [ removeButton (Del n) ]
- ]
- :: case Dict.get e.id model.releases of
- Nothing -> [ div [ class "spinner spinner--md" ] [] ]
- Just l -> default n e :: if e.relsel then List.map (rel n e.rel) l else []
-
- default n e =
- editListRow ""
- [ editListField 2 "ml-3"
- [ label [class "checkbox"] [ inputCheck "" e.relsel (SetSel n), text " Per release" ] ]
- , editListField 1 "col-form-label single-line text-right" [ text <| if e.relsel then "Default:" else "All releases:" ]
- , editListField 1 ""
- [ inputSelect [onInput (SetRole n)] e.role <|
- (if e.relsel then [("", "Not involved")] else [])
- ++ Gen.charRoles
- ]
- , editListField 1 ""
- [ if e.role == ""
- then text ""
- else inputSelect [onInput (SetSpoil n)] (String.fromInt e.spoil) spoilLevels
- ]
- ]
-
- rel n rels e =
- let
- sel = Maybe.withDefault { role = "", spoil = 0 } <| Dict.get e.id rels
- in editListRow ""
- [ editListField 3 "col-form-label single-line ml-3" <|
- span [ class "muted" ] [ text <| "r" ++ String.fromInt e.id ++ ": " ]
- :: List.map iconLanguage e.lang
- ++
- [ a [href <| "/r" ++ String.fromInt e.id, title e.title, target "_blank" ] [ text e.title ] ]
- , editListField 1 ""
- [ inputSelect [onInput (SetRRole n e.id)] sel.role (("", "-default-") :: Gen.charRoles) ]
- , editListField 1 ""
- [ if sel.role == ""
- then text ""
- else inputSelect [onInput (SetRSpoil n e.id)] (String.fromInt sel.spoil) spoilLevels
- ]
- ]
-
- in card "vns" "Visual Novels" [] <|
- List.concat (List.indexedMap vn model.vn)
- ++ formGroups (
- (if model.duplicates
- then [ [ div [ class "invalid-feedback" ]
- [ text "There are duplicate visual novels." ] ] ]
- else []
- ) ++
- [ label [for "add-vn"] [text "Add visual novel"]
- :: A.view searchConfig model.search [placeholder "VIsual novel", style "max-width" "400px"]
- ]
- )
diff --git a/elm3/DocEdit.elm b/elm3/DocEdit.elm
deleted file mode 100644
index 4461c522..00000000
--- a/elm3/DocEdit.elm
+++ /dev/null
@@ -1,107 +0,0 @@
-module DocEdit exposing (main)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Browser
-import Browser.Navigation exposing (load)
-import Json.Encode as JE
-import Lib.Html exposing (..)
-import Lib.Gen as Gen
-import Lib.Api as Api
-import Lib.Ffi as Ffi
-import Lib.Editsum as Editsum
-
-
-main : Program Gen.DocEdit Model Msg
-main = Browser.element
- { init = \e -> (init e, Cmd.none)
- , view = view
- , update = update
- , subscriptions = always Sub.none
- }
-
-
-type alias Model =
- { state : Api.State
- , editsum : Editsum.Model
- , title : String
- , content : String
- , id : Int
- , preview : String
- }
-
-
-init : Gen.DocEdit -> Model
-init d =
- { state = Api.Normal
- , editsum = { authmod = True, editsum = d.editsum, locked = d.locked, hidden = d.hidden }
- , title = d.title
- , content = d.content
- , id = d.id
- , preview = ""
- }
-
-
-encode : Model -> Gen.DocEditSend
-encode model =
- { editsum = model.editsum.editsum
- , hidden = model.editsum.hidden
- , locked = model.editsum.locked
- , title = model.title
- , content = model.content
- }
-
-
-type Msg
- = Editsum Editsum.Msg
- | Submit
- | Submitted Api.Response
- | Title String
- | Content String
- | Preview
- | HandlePreview Api.Response
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Editsum e -> ({ model | editsum = Editsum.update e model.editsum }, Cmd.none)
- Title s -> ({ model | title = s }, Cmd.none)
- Content s -> ({ model | content = s }, Cmd.none)
-
- Submit ->
- let
- path = "/d" ++ String.fromInt model.id ++ "/edit"
- body = Gen.doceditSendEncode (encode model)
- in ({ model | state = Api.Loading }, Api.post path body Submitted)
-
- Submitted (Gen.Changed id rev) -> (model, load <| "/d" ++ String.fromInt id ++ "." ++ String.fromInt rev)
- Submitted r -> ({ model | state = Api.Error r }, Cmd.none)
-
- Preview ->
- ( { model | state = Api.Loading, preview = "" }
- , Api.post "/js/markdown.json" (JE.object [("content", JE.string model.content)]) HandlePreview
- )
-
- HandlePreview (Gen.Content s) -> ({ model | state = Api.Normal, preview = s }, Cmd.none)
- HandlePreview r -> ({ model | state = Api.Error r }, Cmd.none)
-
-
-view : Model -> Html Msg
-view model =
- form_ Submit (model.state == Api.Loading)
- [ card "general" "General" [] <| formGroups
- [ [ label [ for "title" ] [ text "Title" ]
- , inputText "title" model.title Title [required True, maxlength 200]
- ]
- , [ label [ for "content" ] [ text "Content" ]
- , inputTextArea "content" model.content Content [rows 100, required True]
- ]
- , [ button [ type_ "button", class "btn", onClick Preview ] [ text "Preview" ]
- , div [ class "doc", Ffi.innerHtml model.preview ] []
- ]
- ]
- , Html.map Editsum (Editsum.view model.editsum)
- , submitButton "Submit" model.state True False
- ]
diff --git a/elm3/Lib/Api.elm b/elm3/Lib/Api.elm
deleted file mode 100644
index c1e0ddcb..00000000
--- a/elm3/Lib/Api.elm
+++ /dev/null
@@ -1,110 +0,0 @@
-module Lib.Api exposing (..)
-
-import Json.Encode as JE
-import Json.Decode as JD
-import File exposing (File)
-import Http
-import Html exposing (Attribute)
-import Html.Events exposing (on)
-
-import Lib.Gen exposing (ApiResponse(..), decodeApiResponse)
-
-
--- Handy state enum for forms
-type State
- = Normal
- | Loading
- | Error Response
-
-
-
-type alias Response = ApiResponse
-
-
--- User-friendly error message if the response isn't what the code expected.
--- (Technically a good chunk of this function could also be automatically
--- generated by ElmGen.pm, but that wouldn't really have all that much value).
-showResponse : Response -> String
-showResponse res =
- let unexp = "Unexpected response, please report a bug."
- in case res of
- HTTPError (Http.Timeout) -> "Network timeout, please try again later."
- HTTPError (Http.NetworkError) -> "Network error, please try again later."
- HTTPError (Http.BadStatus r) -> "Server error " ++ String.fromInt r ++ ", please try again later, or report an issue if this persists."
- HTTPError (Http.BadBody r) -> "Invalid response from the server, please report a bug (debug info: " ++ r ++")."
- HTTPError (Http.BadUrl _) -> unexp
- Success -> unexp
- CSRF -> "Invalid CSRF token, please refresh the page and try again."
- Throttled -> "Action throttled."
- Invalid _ -> "Invalid form data, please report a bug." -- This error is already logged server-side, no debug info necessary
- Unauth -> "You do not have the permission to perform this action."
- BadEmail -> "Unknown email address."
- BadLogin -> "Invalid username or password."
- BadPass -> "Your chosen password is in a database of leaked passwords, please choose another one."
- Bot -> "Invalid answer to the anti-bot question."
- Taken -> "Username already taken, please choose a different name."
- DoubleEmail -> "Email address already used for another account."
- DoubleIP -> "You can only register one account from the same IP within 24 hours."
- Unchanged -> "No changes"
- Changed _ _ -> unexp
- VNResult _ -> unexp
- StaffResult _ -> unexp
- ProducerResult _ -> unexp
- CharResult _ -> unexp
- TraitResult _ -> unexp
- ReleaseResult _ -> unexp
- ImgFormat -> "Unrecognized image format, please upload a JPG or PNG file."
- Image _ _ _ -> unexp
- Content _ -> unexp
-
-
-expectResponse : (Response -> msg) -> Http.Expect msg
-expectResponse msg =
- let
- res r = msg <| case r of
- Err e -> HTTPError e
- Ok v -> v
- in Http.expectJson res decodeApiResponse
-
-
--- Send a POST request with a JSON body to the VNDB API and get a Response back.
-post : String -> JE.Value -> (Response -> msg) -> Cmd msg
-post url body msg =
- Http.post
- { url = url
- , body = Http.jsonBody body
- , expect = expectResponse msg
- }
-
-
-
--- Simple image upload API
-
-type ImageType
- = Cv
- | Sf
- | Ch
-
-
-onFileChange : (List File -> m) -> Attribute m
-onFileChange msg = on "change" <| JD.map msg <| JD.at ["target","files"] <| JD.list File.decoder
-
-
--- Upload an image to /js/imageupload.json
-postImage : ImageType -> File -> (Response -> msg) -> Cmd msg
-postImage ty file msg =
- let
- tys = case ty of
- Cv -> "cv"
- Sf -> "sf"
- Ch -> "ch"
-
- body = Http.multipartBody
- [ Http.stringPart "type" tys
- , Http.filePart "img" file
- ]
- in Http.post
- { url = "/js/imageupload.json"
- , body = body
- , expect = expectResponse msg
- }
diff --git a/elm3/Lib/Autocomplete.elm b/elm3/Lib/Autocomplete.elm
deleted file mode 100644
index facbb8d2..00000000
--- a/elm3/Lib/Autocomplete.elm
+++ /dev/null
@@ -1,292 +0,0 @@
-module Lib.Autocomplete exposing
- ( Config
- , SourceConfig
- , Model
- , Msg
- , staffSource
- , vnSource
- , producerSource
- , charSource
- , traitSource
- , init
- , clear
- , update
- , view
- )
-
-import Html exposing (..)
-import Html.Events exposing (..)
-import Html.Attributes exposing (..)
-import Html.Keyed as Keyed
-import Json.Encode as JE
-import Json.Decode as JD
-import Task
-import Process
-import Browser.Dom as Dom
-import Lib.Html exposing (..)
-import Lib.Api as Api
-import Lib.Gen as Gen
-
-
-type alias Config m a =
- -- How to wrap a Msg from this model into a Msg of the using model
- { wrap : Msg a -> m
- -- A unique 'id' of the input box (necessary for the blur/focus events)
- , id : String
- -- The source defines where to get autocomplete results from and how to display them
- , source : SourceConfig m a
- }
-
-
-type alias SourceConfig m a =
- -- API path to query for completion results.
- -- (The API must accept POST requests with {"search":".."} as body)
- { path : String
- -- How to decode results from the API
- , decode : Api.Response -> Maybe (List a)
- -- How to display the decoded results
- , view : a -> List (Html m)
- -- Unique ID of an item (must not be an empty string).
- -- This is used to remember selection across data refreshes and to optimize
- -- HTML generation.
- , key : a -> String
- }
-
-
-
-staffSource : SourceConfig m Gen.ApiStaffResult
-staffSource =
- { path = "/js/staff.json"
- , decode = \x -> case x of
- Gen.StaffResult e -> Just e
- _ -> Nothing
- , view = (\i -> [ div [ class "row row-compact" ]
- [ div [ class "col single-line muted" ] [ text <| "s" ++ String.fromInt i.id ]
- , div [ class "col col--2 single-line semi-bold" ] [ text i.name ]
- , div [ class "col col--2 single-line" ] [ text i.original ]
- ] ] )
- , key = .aid >> String.fromInt
- }
-
-
-vnSource : SourceConfig m Gen.ApiVNResult
-vnSource =
- { path = "/js/vn.json"
- , decode = \x -> case x of
- Gen.VNResult e -> Just e
- _ -> Nothing
- , view = (\i -> [ div [ class "row row-compact" ]
- [ div [ class "col single-line muted" ] [ text <| "v" ++ String.fromInt i.id ]
- , div [ class "col col--4 single-line semi-bold" ] [ text i.title ]
- ] ] )
- , key = .id >> String.fromInt
- }
-
-
-producerSource : SourceConfig m Gen.ApiProducerResult
-producerSource =
- { path = "/js/producer.json"
- , decode = \x -> case x of
- Gen.ProducerResult e -> Just e
- _ -> Nothing
- , view = (\i -> [ div [ class "row row-compact" ]
- [ div [ class "col single-line muted" ] [ text <| "p" ++ String.fromInt i.id ]
- , div [ class "col col--4 single-line semi-bold" ] [ text i.name ]
- ] ] )
- , key = .id >> String.fromInt
- }
-
-
-charSource : SourceConfig m Gen.ApiCharResult
-charSource =
- { path = "/js/char.json"
- , decode = \x -> case x of
- Gen.CharResult e -> Just e
- _ -> Nothing
- , view = (\i -> [ div [ class "row row-compact" ]
- [ div [ class "col single-line muted" ] [ text <| "c" ++ String.fromInt i.id ]
- , div [ class "col col--2 single-line semi-bold" ] [ text i.name ]
- , div [ class "col col--2 single-line" ] [ text i.original ]
- ] ] )
- , key = .id >> String.fromInt
- }
-
-
-traitSource : SourceConfig m Gen.ApiTraitResult
-traitSource =
- { path = "/js/trait.json"
- , decode = \x -> case x of
- Gen.TraitResult e -> Just e
- _ -> Nothing
- , view = (\i -> [ div [ class "row row-compact" ]
- [ div [ class "col single-line muted" ] [ text <| "i" ++ String.fromInt i.id ]
- , div [ class "col col--4 single-line" ]
- [ span [ class "muted" ] [ text <| (Maybe.withDefault "" i.group) ++ " / " ]
- , span [ class "semi-bold" ] [ text i.name ]
- ]
- ] ] )
- , key = .id >> String.fromInt
- }
-
-
-
-type alias Model a =
- { position : Maybe Dom.Element
- , value : String
- , results : List a
- , sel : String
- , loading : Bool
- , wait : Int
- }
-
-
-init : Model a
-init =
- { position = Nothing
- , value = ""
- , results = []
- , sel = ""
- , loading = False
- , wait = 0
- }
-
-
-clear : Model a -> Model a
-clear m = { m
- | value = ""
- , results = []
- , sel = ""
- , loading = False
- }
-
-
-type Msg a
- = Noop
- | Focus
- | Blur
- | Pos (Result Dom.Error Dom.Element)
- | Input String
- | Search Int
- | Key String
- | Sel String
- | Enter a
- | Results String Api.Response
-
-
-select : Config m a -> Int -> Model a -> Model a
-select cfg offset model =
- let
- get n = List.drop n model.results |> List.head
- count = List.length model.results
- find (n,i) = if cfg.source.key i == model.sel then Just n else Nothing
- curidx = List.indexedMap (\a b -> (a,b)) model.results |> List.filterMap find |> List.head
- nextidx = (Maybe.withDefault -1 curidx) + offset
- nextsel = if nextidx < 0 then 0 else if nextidx >= count then count-1 else nextidx
- in
- { model | sel = Maybe.withDefault "" <| Maybe.map cfg.source.key <| get nextsel }
-
-
-update : Config m a -> Msg a -> Model a -> (Model a, Cmd m, Maybe a)
-update cfg msg model =
- let
- mod m = (m, Cmd.none, Nothing)
- -- Ugly hack: blur and focus the input on enter. This does two things:
- -- 1. If the user clicked on an entry (resulting in the 'Enter' message),
- -- then this will cause the input to be focussed again. This is
- -- convenient when adding multiple entries.
- -- 2. If, as a result of the enter key ('Key Enter' message), the input box
- -- position was moved (likely, because the input box is usually below
- -- the data being added), then this blur + focus causes the 'Focus'
- -- message to be triggered again, updating the position of the dropdown
- -- div. Without this hack the div positioning will be incorrect.
- -- (This hack does rely on the view being updated before these tasks
- -- are executed - but the Dom package seems to guarantee this)
- refocus = Dom.blur cfg.id
- |> Task.andThen (always (Dom.focus cfg.id))
- |> Task.attempt (always (cfg.wrap Noop))
- in
- case msg of
- Noop -> mod model
- Blur -> mod { model | position = Nothing }
- Focus -> ({ model | loading = False }, Task.attempt (cfg.wrap << Pos) (Dom.getElement cfg.id), Nothing)
- Pos (Ok p) -> mod { model | position = Just p }
- Pos _ -> mod model
- Sel s -> mod { model | sel = s }
- Enter r -> (model, refocus, Just r)
-
- Key "Enter" -> (model, refocus, List.filter (\i -> cfg.source.key i == model.sel) model.results |> List.head)
- Key "ArrowUp" -> mod <| select cfg -1 model
- Key "ArrowDown" -> mod <| select cfg 1 model
- Key _ -> mod model
-
- Input s ->
- if s == ""
- then mod { model | value = s, loading = False, results = [] }
- else ( { model | value = s, loading = True, wait = model.wait + 1 }
- , Task.perform (always <| cfg.wrap <| Search <| model.wait + 1) (Process.sleep 500)
- , Nothing )
-
- Search i ->
- if model.value == "" || model.wait /= i
- then mod model
- else ( model
- , Api.post cfg.source.path (JE.object [("search", JE.string model.value)]) (cfg.wrap << Results model.value)
- , Nothing )
-
- Results s r -> mod <|
- if s == model.value
- then { model | loading = False, results = cfg.source.decode r |> Maybe.withDefault [] }
- else model -- Discard stale results
-
-
-view : Config m a -> Model a -> List (Attribute m) -> List (Html m)
-view cfg model attrs =
- let
- input =
- inputText cfg.id model.value (cfg.wrap << Input)
- [ onFocus <| cfg.wrap Focus
- , onBlur <| cfg.wrap Blur
- , custom "keydown" <| JD.map (\c ->
- if c == "Enter" || c == "ArrowUp" || c == "ArrowDown"
- then { preventDefault = True, stopPropagation = True, message = cfg.wrap (Key c) }
- else { preventDefault = False, stopPropagation = False, message = cfg.wrap (Key c) }
- ) <| JD.field "key" JD.string
- ]
-
- inputDiv = div
- (classList [("form-control-wrap",True), ("form-control-wrap--loading",model.loading)] :: attrs)
- [ input ]
-
- msg = [("",
- if List.isEmpty model.results
- then b [] [text "No results"]
- else text ""
- )]
-
- box p =
- Keyed.node "div"
- [ style "top" <| String.fromFloat (p.element.y + p.element.height) ++ "px"
- , style "left" <| String.fromFloat p.element.x ++ "px"
- , style "width" <| String.fromFloat p.element.width ++ "px"
- , class "dropdown-menu dropdown-menu--open"
- ] <| msg ++ List.map item model.results
-
- item i =
- ( cfg.source.key i
- , a
- [ href "#"
- , classList [("dropdown-menu__item", True), ("dropdown-menu__item--active", cfg.source.key i == model.sel) ]
- , onMouseOver <| cfg.wrap <| Sel <| cfg.source.key i
- , onMouseDown <| cfg.wrap <| Enter i
- ] <| cfg.source.view i
- )
-
- in
- [ inputDiv
- , case model.position of
- Nothing -> text ""
- Just p ->
- if model.value == "" || (model.loading && List.isEmpty model.results)
- then text ""
- else box p
- ]
diff --git a/elm3/Lib/Editsum.elm b/elm3/Lib/Editsum.elm
deleted file mode 100644
index 3ddc1506..00000000
--- a/elm3/Lib/Editsum.elm
+++ /dev/null
@@ -1,59 +0,0 @@
--- This module provides an the 'Edit summary' box, including the 'hidden' and
--- 'locked' moderation checkboxes.
-
-module Lib.Editsum exposing (Model, Msg, new, update, view)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Lib.Html exposing (..)
-
-
-type alias Model =
- { authmod : Bool
- , locked : Bool
- , hidden : Bool
- , editsum : String
- }
-
-
-type Msg
- = Locked Bool
- | Hidden Bool
- | Editsum String
-
-
-new : Model
-new =
- { authmod = False
- , locked = False
- , hidden = False
- , editsum = ""
- }
-
-
-update : Msg -> Model -> Model
-update msg model =
- case msg of
- Locked b -> { model | locked = b }
- Hidden b -> { model | hidden = b }
- Editsum s -> { model | editsum = s }
-
-
-view : Model -> Html Msg
-view model =
- let
- lockhid = cardRow "Mod actions" Nothing <| formGroups
- [ [ label [ class "checkbox" ]
- [ inputCheck "" model.locked Locked
- , text " Locked" ]
- ]
- , [ label [ class "checkbox" ]
- [ inputCheck "" model.hidden Hidden
- , text " Hidden" ]
- ]
- ]
- in card_
- [ lockhid
- , cardRow "Edit summary" (Just "English please!")
- <| formGroup [ inputTextArea "" model.editsum Editsum [rows 4, minlength 2, maxlength 5000, required True] ]
- ]
diff --git a/elm3/Lib/Ffi.elm b/elm3/Lib/Ffi.elm
deleted file mode 100644
index 6c3cbf46..00000000
--- a/elm3/Lib/Ffi.elm
+++ /dev/null
@@ -1,29 +0,0 @@
--- Elm 0.19: "We've removed all Native modules and plugged all XSS vectors,
--- it's now impossible to talk with Javascript other than with ports!"
--- Me: "Oh yeah? I'll just run sed over the generated Javascript!"
-
--- This module is a hack to work around the lack of an FFI (Foreign Function
--- Interface) in Elm. The functions in this module are stubs, their
--- implementations are replaced by the Makefile with calls to
--- window.elmFfi_<name> and the actual implementations are in static/v3/vndb.js.
---
--- Use sparingly, all of this will likely break in future Elm versions.
-module Lib.Ffi exposing (..)
-
-import Html exposing (Attribute)
-import Html.Attributes exposing (title)
-
-
--- This is an "onclick = openLightbox(this)" attribute
-openLightbox : Attribute msg
-openLightbox = title ""
-
-
--- Set the innerHTML attribute of a node
-innerHtml : String -> Attribute msg
-innerHtml = always (title "")
-
-
--- The current year
-curYear : Int
-curYear = 2018
diff --git a/elm3/Lib/Html.elm b/elm3/Lib/Html.elm
deleted file mode 100644
index b811f9fd..00000000
--- a/elm3/Lib/Html.elm
+++ /dev/null
@@ -1,182 +0,0 @@
-module Lib.Html exposing (..)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import List
-import Lib.Api as Api
-import Lib.Gen exposing (urlStatic)
-import Lib.Ffi as Ffi
-import Json.Encode as JE
-import String exposing (padLeft)
-
--- Quick short-hand way of creating a form that can be disabled.
--- Usage:
--- form Submit_msg (state == Disabled) [contents]
-form_ : msg -> Bool -> List (Html msg) -> Html msg
-form_ sub dis cont = Html.form [ onSubmit sub ]
- [ fieldset [disabled dis] cont ]
-
-
--- Submit button with loading indicator and error message display
--- TODO: This use of pull-right is ugly.
-submitButton : String -> Api.State -> Bool -> Bool -> Html m
-submitButton val state valid load = div []
- [ input [ type_ "submit", class "btn pull-right", tabindex 10, value val, disabled (state == Api.Loading || not valid || load) ] []
- , case state of
- Api.Error r -> div [class "invalid-feedback pull-right" ] [ text <| Api.showResponse r ]
- _ -> if valid
- then text ""
- else div [class "invalid-feedback pull-right" ] [ text "The form contains errors, please fix these before submitting. " ]
- , if state == Api.Loading || load
- then div [ class "spinner spinner--md pull-right" ] []
- else text ""
- ]
-
-
-inputSelect : List (Attribute m) -> String -> List (String, String) -> Html m
-inputSelect attrs sel lst =
- let opt (id, name) = option [ value id, selected (id == sel) ] [ text name ]
- in select ([class "form-control", tabindex 10] ++ attrs) <| List.map opt lst
-
-
-inputText : String -> String -> (String -> m) -> List (Attribute m) -> Html m
-inputText nam val onch attrs = input (
- [ type_ "text"
- , class "form-control"
- , tabindex 10
- , value val
- , onInput onch
- ]
- ++ attrs
- ++ (if nam == "" then [] else [ id nam, name nam ])
- ) []
-
-inputTextArea : String -> String -> (String -> m) -> List (Attribute m) -> Html m
-inputTextArea nam val onch attrs = textarea (
- [ class "form-control"
- , tabindex 10
- , onInput onch
- ]
- ++ attrs
- ++ (if nam == "" then [] else [ id nam, name nam ])
- ) [ text val ]
-
-inputCheck : String -> Bool -> (Bool -> m) -> Html m
-inputCheck nam val onch = input (
- [ type_ "checkbox"
- , tabindex 10
- , onCheck onch
- , checked val
- ]
- ++ (if nam == "" then [] else [ id nam, name nam ])
- ) []
-
-inputRadio : String -> Bool -> (Bool -> m) -> Html m
-inputRadio nam val onch = input (
- [ type_ "radio"
- , tabindex 10
- , onCheck onch
- , checked val
- ]
- ++ (if nam == "" then [] else [ name nam ])
- ) []
-
--- Generate a card with: Id, Title, [Header stuff], [Sections]
--- TODO: Also abstract "small-card"s (many of the User/ things) into this
-card : String -> String -> List (Html m) -> List (Html m) -> Html m
-card i t h sections = div
- ([class "card"] ++ if i == "" then [] else [id i])
- <|
- [ div [class "card__header"] ([ div [class "card__title"] [text t] ] ++ h)
- ] ++ List.map (\c -> div [class "card__section"] [c]) sections
-
--- Card without header
-card_ : List (Html m) -> Html m
-card_ c = div [class "card"] [ div [class "card__body"] c ]
-
--- Generate a 2-column row for use within a card section: Title, Subtitle, Content
-cardRow : String -> Maybe String -> List (Html m) -> Html m
-cardRow t s c = div [class "row"]
- [ div [class "col-md col-md--1 card__form-section-left"]
- [ div [class "card__form-section-title"] [text t]
- , case s of
- Just n -> div [class "card__form-section-subtitle"] [text n]
- Nothing -> text ""
- ]
- , div [class "col-md col-md--2"] c
- ]
-
-formGroup : List (Html m) -> List (Html m)
-formGroup c = [div [class "form-group"] c]
-
-formGroups : List (List (Html m)) -> List (Html m)
-formGroups groups = List.map (\c -> div [class "form-group"] c) groups
-
-
-removeButton : m -> Html m
-removeButton cmd = button [type_ "button", class "btn", tabindex 10, onClick cmd]
- [ span [class "d-none d-sm-inline"] [text "x"]
- , span [class "d-sm-none"] [text "Remove"]
- ]
-
-
-
-editList : List (Html m) -> List (Html m)
-editList ct =
- if List.isEmpty ct
- then []
- else [ div [class "editable-list editable-list--sm"] ct ]
-
-editListRow : String -> List (Html m) -> Html m
-editListRow cl ct = div [class ("editable-list__row row row--compact " ++ cl)] ct
-
-editListField : Int -> String -> List (Html m) -> Html m
-editListField sm cl ct = div
- [ classList <|
- [ ("editable-list__field", True)
- , ("col-sm", True )
- , ("col-sm--auto", sm == 0)
- , ("col-sm--1", sm == 1)
- , ("col-sm--2", sm == 2)
- , ("col-sm--3", sm == 3)
- , (cl, cl /= "")
- ]
- ] ct
-
-
--- Special arguments,
--- id == -1 -> spinner
--- id == 0 -> camera-alt.svg
-dbImg : String -> Int -> List (Attribute m) -> Maybe { id: String, width: Int, height: Int } -> Html m
-dbImg dir id attrs full =
- if id == 0 then
- div (class "vn-image-placeholder img--rounded" :: attrs)
- [ div [ class "vn-image-placeholder__icon" ]
- [ img [ src (urlStatic ++ "/v3/camera-alt.svg"), class "svg-icon" ] [] ]
- ]
- else if id == -1 then
- div (class "vn-image-placeholder img--rounded" :: attrs)
- [ div [ class "vn-image-placeholder__icon" ]
- [ div [ class "spinner spinner--md" ] [] ]
- ]
- else
- let
- url d = urlStatic ++ "/" ++ d ++ "/" ++ (padLeft 2 '0' (String.fromInt (modBy 100 id))) ++ "/" ++ (String.fromInt id) ++ ".jpg"
- i = img [src (url dir), class "img--fit img--rounded" ] []
- fdir = if dir == "st" then "sf" else dir
- in case full of
- Nothing -> i
- Just f -> a
- [ href (url fdir)
- , Ffi.openLightbox
- , attribute "data-lightbox-id" f.id
- , attribute "data-lightbox-nfo" <| JE.encode 0 <| JE.object [("width", JE.int f.width), ("height", JE.int f.height)]
- ] [ i ]
-
-
-iconLanguage : String -> Html msg
-iconLanguage lang = span [ class "lang-badge" ] [ text lang ]
-
-iconPlatform : String -> Html msg
-iconPlatform plat = img [ class "svg-icon", src (urlStatic ++ "/v3/windows.svg"), title "Windows" ] []
diff --git a/elm3/Lib/RDate.elm b/elm3/Lib/RDate.elm
deleted file mode 100644
index 397f7243..00000000
--- a/elm3/Lib/RDate.elm
+++ /dev/null
@@ -1,84 +0,0 @@
--- Utility module and UI widget for handling release dates.
---
--- Release dates are integers with the following format: 0 or yyyymmdd
--- Special values
--- 0 -> unknown
--- 99999999 -> TBA
--- yyyy9999 -> year known, month & day unknown
--- yyyymm99 -> year & month known, day unknown
---
--- I'm not a big fan of the UI widget. It's functional, but could be much more
--- convenient and intuitive.
-module Lib.RDate exposing (..)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Date
-import Lib.Html exposing (..)
-import Lib.Ffi exposing (curYear)
-
-
-type alias RDate = Int
-
-type alias RDateComp =
- { y : Int
- , m : Int
- , d : Int
- }
-
-
-expand : RDate -> RDateComp
-expand r =
- { y = r // 10000
- , m = modBy 100 (r // 100)
- , d = modBy 100 r
- }
-
-
-compact : RDateComp -> RDate
-compact r = r.y * 10000 + r.m * 100 + r.d
-
-
-normalize : RDateComp -> RDateComp
-normalize r =
- if r.y == 0 then { y = 0, m = 0, d = 0 }
- else if r.y == 9999 then { y = 9999, m = 99, d = 99 }
- else if r.m == 99 then { y = r.y, m = 99, d = 99 }
- else r
-
-
-type Msg
- = Year String
- | Month String
- | Day String
-
-
-update : Msg -> RDate -> RDate
-update msg ro =
- let r = expand ro
- in compact <| normalize <| case msg of
- Year s -> { r | y = Maybe.withDefault r.y <| String.toInt s }
- Month s -> { r | m = Maybe.withDefault r.m <| String.toInt s }
- Day s -> { r | d = Maybe.withDefault r.d <| String.toInt s }
-
-
-view : RDate -> Bool -> Html Msg
-view ro permitUnknown =
- let r = expand ro
- range s = List.range s >> List.map (\n -> (String.fromInt n, String.fromInt n))
- yl = (if permitUnknown then [("0", "Unknown")] else [])
- ++ List.reverse (range 1980 (curYear + 5))
- ++ [("9999", "TBA")]
- ml = ("99", "- month -") :: (range 1 12)
- maxDay = Date.fromCalendarDate r.y (Date.numberToMonth r.m) 1 |> Date.add Date.Months 1 |> Date.add Date.Days -1 |> Date.day
- dl = ("99", "- day -") :: (range 1 maxDay)
- in div []
- [ inputSelect [class "form-control--inline", onInput Year] (String.fromInt r.y) yl
- , if r.y == 0 || r.y == 9999
- then text ""
- else inputSelect [class "form-control--inline", onInput Month] (String.fromInt r.m) ml
- , if r.m == 0 || r.m == 99
- then text ""
- else inputSelect [class "form-control--inline", onInput Day] (String.fromInt r.d) dl
- ]
diff --git a/elm3/Lib/Util.elm b/elm3/Lib/Util.elm
deleted file mode 100644
index f6b39188..00000000
--- a/elm3/Lib/Util.elm
+++ /dev/null
@@ -1,76 +0,0 @@
-module Lib.Util exposing (..)
-
-import Char
-import Dict
-
--- Delete an element from a List
-delidx : Int -> List a -> List a
-delidx n l = List.take n l ++ List.drop (n+1) l
-
-
--- Modify an element in a List
-modidx : Int -> (a -> a) -> List a -> List a
-modidx n f = List.indexedMap (\i e -> if i == n then f e else e)
-
-
-isJust : Maybe a -> Bool
-isJust m = case m of
- Just _ -> True
- _ -> False
-
-
--- Split by newline, trim whitespace and remove empty lines
-splitLn : String -> List String
-splitLn = String.lines >> List.map String.trim >> List.filter ((/=)"")
-
--- Returns true if the list contains duplicates
-hasDuplicates : List comparable -> Bool
-hasDuplicates l =
- let
- step e acc =
- case acc of
- Nothing -> Nothing
- Just m -> if Dict.member e m then Nothing else Just (Dict.insert e True m)
- in
- case List.foldr step (Just Dict.empty) l of
- Nothing -> True
- Just _ -> False
-
-
--- Similar to perl's ucfirst() (not terribly efficient)
-toUpperFirst : String -> String
-toUpperFirst s = String.toList s |> List.indexedMap (\i c -> if i == 0 then Char.toUpper c else c) |> String.fromList
-
-
--- Haskell's 'lookup' - find an entry in an association list
-lookup : a -> List (a,b) -> Maybe b
-lookup n l = List.filter (\(a,_) -> a == n) l |> List.head |> Maybe.map Tuple.second
-
-
-formatGtin : Int -> String
-formatGtin n = if n == 0 then "" else String.fromInt n |> String.padLeft 12 '0'
-
-
--- Based on VNDBUtil::gtintype()
-validateGtin : String -> Maybe Int
-validateGtin =
- let check = String.fromInt
- >> String.reverse
- >> String.toList
- >> List.indexedMap (\i c -> (Char.toCode c - Char.toCode '0') * if modBy 2 i == 0 then 1 else 3)
- >> List.sum
- inval n =
- n < 1000000000
- || (n >= 200000000000 && n < 600000000000)
- || (n >= 2000000000000 && n < 3000000000000)
- || n >= 9770000000000
- || modBy 10 (check n) /= 0
- in String.filter Char.isDigit >> String.toInt >> Maybe.andThen (\n -> if inval n then Nothing else Just n)
-
-
-spoilLevels : List (String, String)
-spoilLevels =
- [ ("0", "No spoiler")
- , ("1", "Minor spoiler")
- , ("2", "Major spoiler")
- ]
diff --git a/elm3/Lightbox.elm b/elm3/Lightbox.elm
deleted file mode 100644
index db19fc78..00000000
--- a/elm3/Lightbox.elm
+++ /dev/null
@@ -1,178 +0,0 @@
-port module Lightbox exposing (main)
-
--- TODO: Display quick-select thumbnails below the image if there's enough room?
--- TODO: The first image in a gallery is not aligned properly
--- TODO: Indicate which images are NSFW
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Array
-import Task
-import List
-import Browser
-import Browser.Events as EV
-import Browser.Dom as DOM
-import Json.Decode as JD
-import Lib.Html exposing (..)
-
-
-main : Program () (Maybe Model) Msg
-main = Browser.element
- { init = always (Nothing, Cmd.none)
- , view = view
- , update = update
- , subscriptions = \m ->
- if m == Nothing
- then open Open
- else Sub.batch
- [ EV.onResize Resize
- , EV.onKeyDown <| JD.map Keydown <| JD.field "key" JD.string
- , preloaded Preloaded
- ]
- }
-
-port close : Bool -> Cmd msg
-port open : (Model -> msg) -> Sub msg
-port preload : String -> Cmd msg
-port preloaded : (String -> msg) -> Sub msg
-
-type alias Release =
- { id : Int
- , title : String
- , lang : List String
- , plat : List String
- }
-
-type alias Image =
- { thumb : String
- , full : String
- , width : Int
- , height : Int
- , load : Bool
- , rel : Maybe Release
- }
-
-type alias Model =
- { images : Array.Array Image
- , current : Int
- , width : Int
- , height : Int
- }
-
-type Msg
- = Noop
- | Next
- | Prev
- | Open Model
- | Close
- | Resize Int Int
- | Viewport DOM.Viewport
- | Preloaded String
- | Keydown String
-
-
-setPreload : Model -> Cmd Msg
-setPreload model =
- let cmd n =
- case Array.get (model.current+n) model.images of
- Nothing -> Cmd.none
- Just i -> if i.load then Cmd.none else preload i.full
- in if cmd 0 /= Cmd.none then cmd 0 else Cmd.batch [cmd -1, cmd 1]
-
-
-update_ : Msg -> Model -> (Model, Cmd Msg)
-update_ msg model =
- let move n =
- case Array.get (model.current+n) model.images of
- Nothing -> (model, Cmd.none)
- Just i -> let m = { model | current = model.current+n } in (m, setPreload m)
- in
- case msg of
- Noop -> (model, Cmd.none)
- Next -> move 1
- Prev -> move -1
- Keydown "ArrowLeft" -> move -1
- Keydown "ArrowRight" -> move 1
- Keydown _ -> (model, Cmd.none)
- Resize width height -> ({ model | width = width, height = height }, Cmd.none)
- Viewport v -> ({ model | width = round v.viewport.width, height = round v.viewport.height }, Cmd.none)
- Preloaded url ->
- let m = { model | images = Array.map (\img -> if img.full == url then { img | load = True } else img) model.images }
- in (m, setPreload m)
- _ -> (model, Cmd.none)
-
-
-update : Msg -> Maybe Model -> (Maybe Model, Cmd Msg)
-update msg model =
- case (msg, model) of
- (Open m , _) -> ( Just m
- , Cmd.batch [setPreload m, Task.perform Viewport DOM.getViewport]
- )
- (Close , _) -> (Nothing, close True)
- (Keydown "Escape", _) -> (Nothing, close True)
- (_ , Just m) -> let (newm, cmd) = update_ msg m in (Just newm, cmd)
- _ -> (model, Cmd.none)
-
-
-
-view_ : Model -> Html Msg
-view_ model =
- let
- -- 'onClick' with stopPropagation and preventDefault
- onClickN action = custom "click" (JD.succeed { message = action, stopPropagation = True, preventDefault = True})
- -- 'onClick' with stopPropagation
- onClickP action = custom "click" (JD.succeed { message = action, stopPropagation = True, preventDefault = False})
-
- -- Maximum image dimensions
- awidth = toFloat model.width * 0.84
- aheight = toFloat model.height - 80
-
- full_img action position i =
- -- Scale image down to fit inside awidth/aheight
- let swidth = awidth / toFloat i.width
- sheight = aheight / toFloat i.height
- scale = Basics.min 1 <| if swidth < sheight then swidth else sheight
- iwidth = round <| scale * toFloat i.width
- iheight = round <| scale * toFloat i.height
- cwidth = style "width" <| String.fromInt iwidth ++ "px"
- cheight = style "height" <| String.fromInt iheight ++ "px"
- imgsrc = if i.load then i.full else i.thumb
- in
- a [ href "#", onClickN action, cheight
- , class <| "lightbox__image lightbox__image-" ++ position ]
- [ img [ class "lightbox__img", src imgsrc, cwidth, cheight ] [] ]
-
- full offset action position =
- case Array.get (model.current + offset) model.images of
- Nothing -> text ""
- Just i -> full_img action position i
-
- meta img = div [ class "lightbox__meta", onClickP Noop ] <|
- [ a [ href img.full, class "lightbox__dims" ] [ text <| String.fromInt img.width ++ "x" ++ String.fromInt img.height ]
- ] ++ relMeta img.rel
-
- relMeta r = case r of
- Nothing -> []
- Just rel ->
- (List.map iconPlatform rel.plat)
- ++ (List.map iconLanguage rel.lang)
- ++ [ a [ href ("/r" ++ String.fromInt rel.id) ] [ text rel.title ] ]
-
- container img = div [ class "lightbox", onClick Close ]
- [ a [ href "#", onClickN Close, class "lightbox__close" ] []
- , full -1 Prev "left"
- , full 0 Close "current"
- , full 1 Next "right"
- , meta img
- ]
-
- in case Array.get model.current model.images of
- Just img -> container img
- Nothing -> text ""
-
-
-view : (Maybe Model) -> Html Msg
-view m = case m of
- Just mod -> view_ mod
- Nothing -> text ""
diff --git a/elm3/ProdEdit/General.elm b/elm3/ProdEdit/General.elm
deleted file mode 100644
index 19ca79b7..00000000
--- a/elm3/ProdEdit/General.elm
+++ /dev/null
@@ -1,78 +0,0 @@
-module ProdEdit.General exposing (..)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Lib.Html exposing (..)
-import Lib.Gen exposing (languages, weburlPattern, producerTypes, ProdEdit)
-import Lib.Util exposing (..)
-
-
-type alias Model =
- { desc : String
- , l_wp : String
- , lang : String
- , ptype : String
- , website : String
- }
-
-
-init : ProdEdit -> Model
-init d =
- { desc = d.desc
- , l_wp = d.l_wp
- , lang = d.lang
- , ptype = d.ptype
- , website = d.website
- }
-
-
-new : Model
-new =
- { desc = ""
- , l_wp = ""
- , lang = "ja"
- , ptype = "co"
- , website = ""
- }
-
-
-type Msg
- = Desc String
- | LWP String
- | Lang String
- | PType String
- | Website String
-
-
-update : Msg -> Model -> Model
-update msg model =
- case msg of
- Desc s -> { model | desc = s }
- LWP s -> { model | l_wp = s }
- Lang s -> { model | lang = s }
- PType s -> { model | ptype = s }
- Website s -> { model | website = s }
-
-
-view : Model -> (Msg -> a) -> List (Html a) -> Html a
-view model wrap names = card "general" "General info" [] <|
- names ++ List.map (Html.map wrap)
- [ cardRow "Meta" Nothing <| formGroups
- [ [ label [for "ptype"] [ text "Type" ]
- , inputSelect [id "ptype", name "ptype", onInput PType] model.ptype producerTypes
- ]
- , [ label [for "lang"] [ text "Primary language" ]
- , inputSelect [id "lang", name "lang", onInput Lang] model.lang languages
- ]
- , [ label [for "website"] [ text "Official Website" ]
- , inputText "website" model.website Website [pattern weburlPattern]
- ]
- , [ label [] [ text "Wikipedia" ]
- , p [] [ text "https://en.wikipedia.org/wiki/", inputText "l_wp" model.l_wp LWP [class "form-control--inline", maxlength 100] ]
- ]
- ]
-
- , cardRow "Description" (Just "English please!") <| formGroup
- [ inputTextArea "desc" model.desc Desc [rows 8] ]
- ]
diff --git a/elm3/ProdEdit/Main.elm b/elm3/ProdEdit/Main.elm
deleted file mode 100644
index 06328f53..00000000
--- a/elm3/ProdEdit/Main.elm
+++ /dev/null
@@ -1,158 +0,0 @@
-module ProdEdit.Main exposing (Model, Msg, main, new, view, update)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Json.Encode as JE
-import Browser
-import Browser.Navigation exposing (load)
-import Lib.Util exposing (splitLn)
-import Lib.Html exposing (..)
-import Lib.Gen as Gen
-import Lib.Api as Api
-import Lib.Editsum as Editsum
-import ProdEdit.Names as Names
-import ProdEdit.General as General
-import ProdEdit.Relations as Rel
-
-
-main : Program Gen.ProdEdit Model Msg
-main = Browser.element
- { init = \e -> (init e, Cmd.none)
- , view = view
- , update = update
- , subscriptions = always Sub.none
- }
-
-
-type alias Model =
- { state : Api.State
- , new : Bool
- , editsum : Editsum.Model
- , names : Names.Model
- , general : General.Model
- , relations : Rel.Model
- , id : Maybe Int
- , dupProds : List Gen.ApiProducerResult
- }
-
-
-init : Gen.ProdEdit -> Model
-init d =
- { state = Api.Normal
- , new = False
- , editsum = { authmod = d.authmod, editsum = d.editsum, locked = d.locked, hidden = d.hidden }
- , names = Names.init d
- , general = General.init d
- , relations = Rel.init d.relations
- , id = d.id
- , dupProds = []
- }
-
-
-new : Model
-new =
- { state = Api.Normal
- , new = True
- , editsum = Editsum.new
- , names = Names.new
- , general = General.new
- , relations = Rel.init []
- , id = Nothing
- , dupProds = []
- }
-
-
-encode : Model -> Gen.ProdEditSend
-encode model =
- { editsum = model.editsum.editsum
- , hidden = model.editsum.hidden
- , locked = model.editsum.locked
- , name = model.names.name
- , original = model.names.original
- , alias = model.names.alias
- , desc = model.general.desc
- , lang = model.general.lang
- , ptype = model.general.ptype
- , l_wp = model.general.l_wp
- , website = model.general.website
- , relations = List.map (\e -> { pid = e.pid, relation = e.relation }) model.relations.relations
- }
-
-
-type Msg
- = Editsum Editsum.Msg
- | Submit
- | Submitted Api.Response
- | Names Names.Msg
- | General General.Msg
- | Relations Rel.Msg
- | CheckDup
- | RecvDup Api.Response
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Names m -> ({ model | names = Names.update m model.names, dupProds = [] }, Cmd.none)
- General m -> ({ model | general = General.update m model.general }, Cmd.none)
- Editsum m -> ({ model | editsum = Editsum.update m model.editsum }, Cmd.none)
- Relations m -> let (nm, c) = Rel.update m model.relations in ({ model | relations = nm }, Cmd.map Relations c)
-
- Submit ->
- let
- path =
- case model.id of
- Just id -> "/p" ++ String.fromInt id ++ "/edit"
- Nothing -> "/p/add"
- body = Gen.prodeditSendEncode (encode model)
- in ({ model | state = Api.Loading }, Api.post path body Submitted)
-
- Submitted (Gen.Changed id rev) -> (model, load <| "/p" ++ String.fromInt id ++ "." ++ String.fromInt rev)
- Submitted r -> ({ model | state = Api.Error r }, Cmd.none)
-
- CheckDup ->
- let body = JE.object
- [ ("search", JE.list JE.string <| List.filter ((/=)"") <| model.names.name :: model.names.original :: model.names.aliasList)
- , ("hidden", JE.bool True) ]
- in
- if List.isEmpty model.dupProds
- then ({ model | state = Api.Loading }, Api.post "/js/producer.json" body RecvDup)
- else ({ model | new = False }, Cmd.none)
-
- RecvDup (Gen.ProducerResult dup) ->
- ({ model | state = Api.Normal, dupProds = dup, new = not (List.isEmpty dup) }, Cmd.none)
- RecvDup r -> ({ model | state = Api.Error r }, Cmd.none)
-
-
-
-isValid : Model -> Bool
-isValid model = not
- ( model.names.aliasDuplicates
- || model.relations.duplicates
- )
-
-
-view : Model -> Html Msg
-view model =
- if model.new
- then form_ CheckDup (model.state == Api.Loading)
- [ card "new" "Add a new producer" []
- <| List.map (Html.map Names) <| Names.view model.names
- , if List.isEmpty model.dupProds
- then text ""
- else card "dup" "Possible duplicates" [ div [ class "card__subheading" ] [ text "Please check the list below for possible duplicates." ] ]
- [ cardRow "" Nothing <| formGroup [ div [ class "form-group__help" ] [
- ul [] <| List.map (\e ->
- li [] [ a [ href <| "/p" ++ String.fromInt e.id, title e.original, target "_black" ] [ text e.name ]
- , text <| if e.hidden then " (deleted)" else "" ]
- ) model.dupProds
- ] ] ]
- , submitButton "Continue" model.state (isValid model) False
- ]
-
- else form_ Submit (model.state == Api.Loading)
- [ General.view model.general General <| List.map (Html.map Names) <| Names.view model.names
- , Html.map Relations <| Rel.view model.relations
- , Html.map Editsum <| Editsum.view model.editsum
- , submitButton "Submit" model.state (isValid model) False
- ]
diff --git a/elm3/ProdEdit/Names.elm b/elm3/ProdEdit/Names.elm
deleted file mode 100644
index e28f4916..00000000
--- a/elm3/ProdEdit/Names.elm
+++ /dev/null
@@ -1,81 +0,0 @@
-module ProdEdit.Names exposing (..)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Dict
-import Lib.Html exposing (..)
-import Lib.Gen exposing (ProdEdit)
-import Lib.Util exposing (..)
-
-
-type alias Model =
- { name : String
- , original : String
- , alias : String
- , aliasList : List String
- , aliasDuplicates : Bool
- }
-
-
-init : ProdEdit -> Model
-init d =
- { name = d.name
- , original = d.original
- , alias = d.alias
- , aliasList = splitLn d.alias
- , aliasDuplicates = False
- }
-
-
-new : Model
-new =
- { name = ""
- , original = ""
- , alias = ""
- , aliasList = []
- , aliasDuplicates = False
- }
-
-
-type Msg
- = Name String
- | Original String
- | Alias String
-
-
-update : Msg -> Model -> Model
-update msg model =
- case msg of
- Name s -> { model | name = s }
- Original s -> { model | original = s }
- Alias s ->
- { model
- | alias = s
- , aliasList = splitLn s
- , aliasDuplicates = hasDuplicates (model.name :: model.original :: splitLn s)
- }
-
-
-view : Model -> List (Html Msg)
-view model =
- [ cardRow "Name" Nothing <| formGroups
- [ [ label [for "name"] [text "Name (romaji)"]
- , inputText "name" model.name Name [required True, maxlength 200]
- ]
- , [ label [for "original"] [text "Original"]
- , inputText "original" model.original Original [maxlength 200]
- , div [class "form-group__help"] [text "The original name of this producer, leave blank if it already is in the Latin alphabet."]
- ]
- ]
- , cardRow "Aliases" Nothing <| formGroup
- [ inputTextArea "aliases" model.alias Alias
- [ rows 4, maxlength 500
- , classList [("is-invalid", model.aliasDuplicates)]
- ]
- , if model.aliasDuplicates
- then div [class "invalid-feedback"]
- [ text "There are duplicate aliases." ]
- else text ""
- , div [class "form-group__help"] [ text "(Un)official aliases, separated by a newline." ]
- ]
- ]
diff --git a/elm3/ProdEdit/New.elm b/elm3/ProdEdit/New.elm
deleted file mode 100644
index c1df2ac4..00000000
--- a/elm3/ProdEdit/New.elm
+++ /dev/null
@@ -1,12 +0,0 @@
-module ProdEdit.New exposing (main)
-
-import Browser
-import ProdEdit.Main as Main
-
-main : Program () Main.Model Main.Msg
-main = Browser.element
- { init = always (Main.new, Cmd.none)
- , view = Main.view
- , update = Main.update
- , subscriptions = always Sub.none
- }
diff --git a/elm3/ProdEdit/Relations.elm b/elm3/ProdEdit/Relations.elm
deleted file mode 100644
index b1887684..00000000
--- a/elm3/ProdEdit/Relations.elm
+++ /dev/null
@@ -1,79 +0,0 @@
-module ProdEdit.Relations exposing (Model, Msg, init, update, view)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Lib.Html exposing (..)
-import Lib.Gen as Gen
-import Lib.Util exposing (..)
-import Lib.Autocomplete as A
-
-
-type alias Model =
- { relations : List Gen.ProdEditRelations
- , search : A.Model Gen.ApiProducerResult
- , duplicates : Bool
- }
-
-
-init : List Gen.ProdEditRelations -> Model
-init l =
- { relations = l
- , search = A.init
- , duplicates = False
- }
-
-
-type Msg
- = Del Int
- | Rel Int String
- | Search (A.Msg Gen.ApiProducerResult)
-
-
-searchConfig : A.Config Msg Gen.ApiProducerResult
-searchConfig = { wrap = Search, id = "add-relation", source = A.producerSource }
-
-
-validate : Model -> Model
-validate model = { model | duplicates = hasDuplicates <| List.map .pid model.relations }
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Del i -> (validate { model | relations = delidx i model.relations }, Cmd.none)
- Rel i s -> (validate { model | relations = modidx i (\e -> { e | relation = s }) model.relations }, Cmd.none)
- Search m ->
- let (nm, c, res) = A.update searchConfig m model.search
- in case res of
- Nothing -> ({ model | search = nm }, c)
- Just r ->
- let
- rel = List.head Gen.producerRelations |> Maybe.map Tuple.first |> Maybe.withDefault ""
- nrow = { pid = r.id, relation = rel, name = r.name }
- in (validate { model | search = A.clear nm, relations = model.relations ++ [nrow] }, c)
-
-
-view : Model -> Html Msg
-view model =
- let
- entry n e = editListRow "row--ai-center"
- [ editListField 1 "single-line"
- [ a [href <| "/p" ++ String.fromInt e.pid, title e.name, target "_blank" ] [text e.name ] ]
- , editListField 1 ""
- [ inputSelect [onInput (Rel n)] e.relation Gen.producerRelations ]
- , editListField 0 "" [ removeButton (Del n) ]
- ]
-
- in card "relations" "Relations" [] <|
- editList (List.indexedMap entry model.relations)
- ++ formGroups (
- (if model.duplicates
- then [ [ div [ class "invalid-feedback" ]
- [ text "The list contains duplicates. Make sure that the same producer is not listed multiple times." ] ] ]
- else []
- ) ++
- [ label [for "add-relation"] [text "Add relation"]
- :: A.view searchConfig model.search [placeholder "Producer...", style "max-width" "400px"]
- ]
- )
diff --git a/elm3/RelEdit/General.elm b/elm3/RelEdit/General.elm
deleted file mode 100644
index 698c9f99..00000000
--- a/elm3/RelEdit/General.elm
+++ /dev/null
@@ -1,272 +0,0 @@
-module RelEdit.General exposing (..)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Lib.Html exposing (..)
-import Lib.Gen exposing (..)
-import Lib.Util exposing (..)
-import Lib.RDate as RDate
-
-
-type alias Model =
- { aniEro : Int
- , aniStory : Int
- , catalog : String
- , doujin : Bool
- , freeware : Bool
- , gtinInput : String
- , gtinVal : Maybe Int
- , lang : List { lang : String }
- , media : List { medium : String, qty : Int }
- , minage : Maybe Int
- , notes : String
- , original : String
- , patch : Bool
- , platforms : List { platform : String }
- , released : RDate.RDate
- , resolution : String
- , rtype : String
- , title : String
- , uncensored : Bool
- , voiced : Int
- , website : String
- }
-
-
-init : RelEdit -> Model
-init d =
- { aniEro = d.ani_ero
- , aniStory = d.ani_story
- , catalog = d.catalog
- , doujin = d.doujin
- , freeware = d.freeware
- , gtinInput = formatGtin d.gtin
- , gtinVal = Just d.gtin
- , lang = d.lang
- , media = d.media
- , minage = d.minage
- , notes = d.notes
- , original = d.original
- , patch = d.patch
- , platforms = d.platforms
- , released = d.released
- , resolution = d.resolution
- , rtype = d.rtype
- , title = d.title
- , uncensored = d.uncensored
- , voiced = d.voiced
- , website = d.website
- }
-
-
-new : String -> String -> Model
-new title orig =
- { aniEro = 0
- , aniStory = 0
- , catalog = ""
- , doujin = False
- , freeware = False
- , gtinInput = ""
- , gtinVal = Just 0
- , lang = [{lang = "ja"}]
- , media = []
- , minage = Nothing
- , notes = ""
- , original = orig
- , patch = False
- , platforms = []
- , released = 99999999
- , resolution = "unknown"
- , rtype = "complete"
- , title = title
- , uncensored = False
- , voiced = 0
- , website = ""
- }
-
-
-type Msg
- = Title String
- | Original String
- | RType String
- | Patch Bool
- | Freeware Bool
- | Doujin Bool
- | LangDel Int
- | LangAdd String
- | Notes String
- | Released RDate.Msg
- | Gtin String
- | Catalog String
- | Website String
- | Minage String
- | Uncensored Bool
- | Resolution String
- | Voiced String
- | AniStory String
- | AniEro String
- | PlatDel Int
- | PlatAdd String
- | MedDel Int
- | MedQty Int String
- | MedAdd String
-
-
-update : Msg -> Model -> Model
-update msg model =
- case msg of
- Title s -> { model | title = s }
- Original s -> { model | original = s }
- RType s -> { model | rtype = s }
- Patch b -> { model | patch = b }
- Freeware b -> { model | freeware = b }
- Doujin b -> { model | doujin = b }
- LangDel n -> { model | lang = delidx n model.lang }
- LangAdd s -> if s == "" then model else { model | lang = model.lang ++ [{ lang = s }] }
- Notes s -> { model | notes = s }
- Released m -> { model | released = RDate.update m model.released }
- Gtin s -> { model | gtinInput= s, gtinVal = if s == "" then Just 0 else validateGtin s }
- Catalog s -> { model | catalog = s }
- Website s -> { model | website = s }
- Minage s -> { model | minage = String.toInt s }
- Uncensored b -> { model | uncensored = b }
- Resolution s -> { model | resolution = s }
- Voiced s -> { model | voiced = Maybe.withDefault model.voiced <| String.toInt s }
- AniStory s -> { model | aniStory = Maybe.withDefault model.aniStory <| String.toInt s }
- AniEro s -> { model | aniEro = Maybe.withDefault model.aniEro <| String.toInt s }
- PlatDel n -> { model | platforms = delidx n model.platforms }
- PlatAdd s -> if s == "" then model else { model | platforms = model.platforms ++ [{ platform = s }] }
- MedDel n -> { model | media = delidx n model.media }
- MedQty i s -> { model | media = modidx i (\e -> { e | qty = Maybe.withDefault 0 (String.toInt s) }) model.media }
- MedAdd s -> if s == "" then model else { model | media = model.media ++ [{ medium = s, qty = 1 }] }
-
-
-general : Model -> Html Msg
-general model = card "general" "General info" []
-
- [ cardRow "Title" Nothing <| formGroups
- [ [ label [for "title"] [text "Title (romaji)"]
- , inputText "title" model.title Title [required True, maxlength 250]
- ]
- , [ label [for "original"] [text "Original"]
- , inputText "original" model.original Original [maxlength 250]
- , div [class "form-group__help"] [text "The original title of this release, leave blank if it already is in the Latin alphabet."]
- ]
- ]
-
- , cardRow "Type" Nothing <| formGroups <|
- [ [ inputSelect [id "type", onInput RType] model.rtype <| List.map (\s -> (s, toUpperFirst s)) releaseTypes ]
- , [ label [class "checkbox"] [ inputCheck "" model.patch Patch , text " This release is a patch to another release" ] ]
- , [ label [class "checkbox"] [ inputCheck "" model.freeware Freeware, text " Freeware (i.e. available at no cost)" ] ]
- ] ++ if model.patch
- then []
- else [ [ label [class "checkbox"] [ inputCheck "" model.doujin Doujin, text " Doujin (self-published, not by a company)" ] ] ]
-
- , cardRow "Languages" Nothing <| formGroups
- [ editList <| List.indexedMap (\n l ->
- editListRow ""
- [ editListField 3 "" [ iconLanguage l.lang, text <| " " ++ (Maybe.withDefault l.lang <| lookup l.lang languages) ]
- , editListField 0 "" [ removeButton (LangDel n) ]
- ]
- ) model.lang
- , [ if List.isEmpty model.lang
- then div [class "invalid-feedback"] [ text "No language selected." ]
- else text ""
- , label [for "addlang"] [ text "Add language" ]
- -- TODO: Move selection back to "" when a new language has been added
- , inputSelect [id "addlang", onInput LangAdd] ""
- <| ("", "-- Add language --")
- :: List.filter (\(n,_) -> not <| List.any (\l -> l.lang == n) model.lang) languages
- ]
- ]
-
- , cardRow "Meta" Nothing <| formGroups <|
- [ [ label [for "released"] [text "Release date"]
- , Html.map Released <| RDate.view model.released False
- , div [class "form-group__help"] [text "Leave month or day blank if they are unknown"]
- ]
- , [ label [for "gtin"] [text "JAN/EAN/UPC"]
- , inputText "gtin" model.gtinInput Gtin [pattern "[0-9]+"]
- , if model.gtinVal == Nothing
- then div [class "invalid-feedback"] [ text "Invalid bar code." ]
- else text ""
- ]
- , [ label [for "catalog"] [text "Catalog number"]
- , inputText "catalog" model.catalog Catalog [maxlength 50]
- ]
- , [ label [for "website"] [text "Official website"]
- , inputText "website" model.website Website [pattern weburlPattern]
- ]
- , [ label [for "minage"] [text "Age rating"]
- , inputSelect [id "minage", onInput Minage] (Maybe.withDefault "" (Maybe.map String.fromInt model.minage)) (List.map (\(a,b) -> (String.fromInt a, b)) minAges)
- ]
- ] ++ if model.minage /= Just 18
- then []
- else [ [ label [class "checkbox"] [ inputCheck "" model.uncensored Uncensored, text " No mosaic or other optical censoring (only check if this release has erotic content)" ] ] ]
-
- , cardRow "Notes" (Just "English please!") <| formGroup
- [ inputTextArea "" model.notes Notes [rows 5, maxlength 10240]
- , div [class "form-group__help"]
- [ text "Miscellaneous notes/comments, information that does not fit in the other fields."
- , text " For example, types of censoring or for which other releases this patch applies."
- ]
- ]
- ]
-
-
-format : Model -> Html Msg
-format model = card "format" "Format" [] <|
-
- (if model.patch then [] else [ cardRow "Technical" Nothing <| formGroups
- [ [ label [for "resolution"] [text "Native screen resolution"]
- , inputSelect [id "resolution", onInput Resolution] model.resolution resolutions
- ]
- , [ label [for "voiced"] [text "Voiced"]
- , inputSelect [id "voiced", onInput Voiced] (String.fromInt model.voiced) <| List.map (\(a,b) -> (String.fromInt a, b)) voiced
- ]
- , [ label [for "ani_story"] [text "Story animation"]
- , inputSelect [id "ani_story", onInput AniStory] (String.fromInt model.aniStory) <| List.map (\(a,b) -> (String.fromInt a, b)) animated
- ]
- , [ label [for "ani_ero"] [text "Ere scene animation"]
- , inputSelect [id "ani_ero", onInput AniEro] (String.fromInt model.aniEro) <| List.map (\(a,b) -> (String.fromInt a, if a == 0 then "Unknown / no ero scenes" else b)) animated
- ]
- ]
- ]) ++
-
- [ cardRow "Platforms" Nothing <| formGroups
- [ editList <| List.indexedMap (\n p ->
- editListRow ""
- [ editListField 3 "" [ iconPlatform p.platform, text <| " " ++ (Maybe.withDefault p.platform <| lookup p.platform platforms) ]
- , editListField 0 "" [ removeButton (PlatDel n) ]
- ]
- ) model.platforms
- , [ label [for "addplat"] [ text "Add platform" ]
- -- TODO: Move selection back to "" when a new platform has been added
- , inputSelect [id "addplat", onInput PlatAdd] ""
- <| ("", "-- Add platform --")
- :: List.filter (\(n,_) -> not <| List.any (\p -> p.platform == n) model.platforms) platforms
- ]
- ]
-
- , cardRow "Media" Nothing <| formGroups
- [ editList <| List.indexedMap (\n m ->
- let md = Maybe.withDefault { qty = False, single = "", plural = "" } <| lookup m.medium Lib.Gen.media
- in editListRow ""
- [ editListField 2 "" [ text md.single ] -- TODO: Add icon
- , editListField 2 ""
- [ if md.qty
- then inputSelect [ onInput (MedQty n) ] (String.fromInt m.qty) <| List.map (\i -> (String.fromInt i, String.fromInt i)) <| List.range 1 20
- else text ""
- ]
- , editListField 0 "" [ removeButton (MedDel n) ]
- ]
- ) model.media
- , [ label [for "addmed"] [ text "Add medium" ]
- -- TODO: Move selection back to "" when a new medium has been added
- , inputSelect [id "addmed", onInput MedAdd] ""
- <| ("", "-- Add medium --")
- :: List.map (\(n,m) -> (n,m.single)) Lib.Gen.media
- ]
- ]
- ]
diff --git a/elm3/RelEdit/Main.elm b/elm3/RelEdit/Main.elm
deleted file mode 100644
index 16e317f0..00000000
--- a/elm3/RelEdit/Main.elm
+++ /dev/null
@@ -1,137 +0,0 @@
-module RelEdit.Main exposing (..)
-
-import Html exposing (..)
-import Browser
-import Browser.Navigation exposing (load)
-import Lib.Html exposing (..)
-import Lib.Gen as Gen
-import Lib.Api as Api
-import Lib.Editsum as Editsum
-import RelEdit.General as General
-import RelEdit.Producers as Producers
-import RelEdit.Vn as Vn
-
-
-main : Program Gen.RelEdit Model Msg
-main = Browser.element
- { init = \e -> (init e, Cmd.none)
- , view = view
- , update = update
- , subscriptions = always Sub.none
- }
-
-
-type alias Model =
- { state : Api.State
- , editsum : Editsum.Model
- , general : General.Model
- , producers : Producers.Model
- , vn : Vn.Model
- , id : Maybe Int
- }
-
-
-init : Gen.RelEdit -> Model
-init d =
- { state = Api.Normal
- , editsum = { authmod = d.authmod, editsum = d.editsum, locked = d.locked, hidden = d.hidden }
- , general = General.init d
- , producers = Producers.init d.producers
- , vn = Vn.init d.vn
- , id = d.id
- }
-
-
-new : Int -> String -> String -> Model
-new vid title orig =
- { state = Api.Normal
- , editsum = Editsum.new
- , general = General.new title orig
- , producers = Producers.init []
- , vn = Vn.init [{vid = vid, title = title}]
- , id = Nothing
- }
-
-
-encode : Model -> Gen.RelEditSend
-encode model =
- { editsum = model.editsum.editsum
- , hidden = model.editsum.hidden
- , locked = model.editsum.locked
- , catalog = model.general.catalog
- , doujin = model.general.doujin
- , freeware = model.general.freeware
- , gtin = Maybe.withDefault 0 model.general.gtinVal
- , lang = model.general.lang
- , minage = model.general.minage
- , notes = model.general.notes
- , original = model.general.original
- , patch = model.general.patch
- , rtype = model.general.rtype
- , released = model.general.released
- , title = model.general.title
- , uncensored = model.general.uncensored
- , website = model.general.website
- , resolution = model.general.resolution
- , voiced = model.general.voiced
- , ani_story = model.general.aniStory
- , ani_ero = model.general.aniEro
- , platforms = model.general.platforms
- , media = model.general.media
- , producers = List.map (\e -> { pid = e.pid, developer = e.developer, publisher = e.publisher }) model.producers.producers
- , vn = List.map (\e -> { vid = e.vid }) model.vn.vn
- }
-
-
-type Msg
- = Editsum Editsum.Msg
- | General General.Msg
- | Producers Producers.Msg
- | Vn Vn.Msg
- | Submit
- | Submitted Api.Response
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Editsum m -> ({ model | editsum = Editsum.update m model.editsum }, Cmd.none)
- General m -> ({ model | general = General.update m model.general }, Cmd.none)
- Producers m -> let (nm, c) = Producers.update m model.producers in ({ model | producers = nm }, Cmd.map Producers c)
- Vn m -> let (nm, c) = Vn.update m model.vn in ({ model | vn = nm }, Cmd.map Vn c)
-
- Submit ->
- let
- path =
- case model.id of
- Just id -> "/r" ++ String.fromInt id ++ "/edit"
- Nothing -> "/r/add"
- body = Gen.releditSendEncode (encode model)
- in ({ model | state = Api.Loading }, Api.post path body Submitted)
-
- Submitted (Gen.Changed id rev) -> (model, load <| "/r" ++ String.fromInt id ++ "." ++ String.fromInt rev)
- Submitted r -> ({ model | state = Api.Error r }, Cmd.none)
-
-
-isValid : Model -> Bool
-isValid model = not
- ( List.isEmpty model.general.lang
- || model.general.gtinVal == Nothing
- || model.producers.duplicates
- || model.vn.duplicates
- || List.isEmpty model.vn.vn
- )
-
-
-view : Model -> Html Msg
-view model =
- form_ Submit (model.state == Api.Loading)
- [ Html.map General <| General.general model.general
- , Html.map General <| General.format model.general
- , card "relations" "Relations" []
- [ Html.map Producers <| Producers.view model.producers
- , Html.map Vn <| Vn.view model.vn
- ]
- , Html.map Editsum <| Editsum.view model.editsum
- , submitButton "Submit" model.state (isValid model) False
- ]
diff --git a/elm3/RelEdit/New.elm b/elm3/RelEdit/New.elm
deleted file mode 100644
index 699e3c29..00000000
--- a/elm3/RelEdit/New.elm
+++ /dev/null
@@ -1,19 +0,0 @@
-module RelEdit.New exposing (main)
-
-import Browser
-import RelEdit.Main as Main
-
-
-type alias Flags =
- { id : Int
- , title : String
- , original : String
- }
-
-main : Program Flags Main.Model Main.Msg
-main = Browser.element
- { init = \f -> (Main.new f.id f.title f.original, Cmd.none)
- , view = Main.view
- , update = Main.update
- , subscriptions = always Sub.none
- }
diff --git a/elm3/RelEdit/Producers.elm b/elm3/RelEdit/Producers.elm
deleted file mode 100644
index 1af1f2ba..00000000
--- a/elm3/RelEdit/Producers.elm
+++ /dev/null
@@ -1,92 +0,0 @@
-module RelEdit.Producers exposing (Model, Msg, init, update, view)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Lib.Html exposing (..)
-import Lib.Autocomplete as A
-import Lib.Gen as Gen
-import Lib.Util exposing (..)
-
-
-type alias Model =
- { producers : List Gen.RelEditProducers
- , search : A.Model Gen.ApiProducerResult
- , duplicates : Bool
- }
-
-
-init : List Gen.RelEditProducers -> Model
-init l =
- { producers = l
- , search = A.init
- , duplicates = False
- }
-
-
-type Msg
- = Del Int
- | SetRole Int String
- | Search (A.Msg Gen.ApiProducerResult)
-
-
-searchConfig : A.Config Msg Gen.ApiProducerResult
-searchConfig = { wrap = Search, id = "add-producer", source = A.producerSource }
-
-
-validate : Model -> Model
-validate model = { model | duplicates = hasDuplicates <| List.map .pid model.producers }
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Del i -> (validate { model | producers = delidx i model.producers }, Cmd.none)
- SetRole i s -> (validate { model | producers = modidx i (\e -> { e | developer = s == "d" || s == "b", publisher = s == "p" || s == "b" }) model.producers }
- , Cmd.none )
-
- Search m ->
- let (nm, c, res) = A.update searchConfig m model.search
- in case res of
- Nothing -> ({ model | search = nm }, c)
- Just r ->
- let nrow = { pid = r.id, name = r.name, developer = False, publisher = True }
- in (validate { model | search = A.clear nm, producers = model.producers ++ [nrow] }, c)
-
-
-
-view : Model -> Html Msg
-view model =
- let
- role e =
- case (e.developer, e.publisher) of
- (True, False) -> "d"
- (False, True) -> "p"
- _ -> "b"
-
- roles =
- [ ("d", "Developer")
- , ("p", "Publisher")
- , ("b", "Both")
- ]
-
- entry n e = editListRow ""
- [ editListField 1 "col-form-label single-line"
- [ a [href <| "/p" ++ String.fromInt e.pid, title e.name, target "_blank" ] [text e.name ] ]
- , editListField 1 ""
- [ inputSelect [onInput (SetRole n)] (role e) roles ]
- , editListField 0 "" [ removeButton (Del n) ]
- ]
-
- in cardRow "Producers" Nothing
- <| editList (List.indexedMap entry model.producers)
- ++ formGroups (
- (if model.duplicates
- then [ [ div [ class "invalid-feedback" ]
- [ text "The producers list contains duplicates." ] ] ]
- else []
- ) ++
- [ label [for "add-producer"] [text "Add producer"]
- :: A.view searchConfig model.search [placeholder "Producer", style "max-width" "400px"]
- ]
- )
diff --git a/elm3/RelEdit/Vn.elm b/elm3/RelEdit/Vn.elm
deleted file mode 100644
index 9f32aff4..00000000
--- a/elm3/RelEdit/Vn.elm
+++ /dev/null
@@ -1,77 +0,0 @@
-module RelEdit.Vn exposing (Model, Msg, init, update, view)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Lib.Html exposing (..)
-import Lib.Gen as Gen
-import Lib.Util exposing (..)
-import Lib.Autocomplete as A
-
-
-type alias Model =
- { vn : List Gen.RelEditVn
- , search : A.Model Gen.ApiVNResult
- , duplicates : Bool
- }
-
-
-init : List Gen.RelEditVn -> Model
-init l =
- { vn = l
- , search = A.init
- , duplicates = False
- }
-
-
-type Msg
- = Del Int
- | Search (A.Msg Gen.ApiVNResult)
-
-
-searchConfig : A.Config Msg Gen.ApiVNResult
-searchConfig = { wrap = Search, id = "add-vn", source = A.vnSource }
-
-
-validate : Model -> Model
-validate model = { model | duplicates = hasDuplicates <| List.map .vid model.vn }
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Del i -> (validate { model | vn = delidx i model.vn }, Cmd.none)
- Search m ->
- let (nm, c, res) = A.update searchConfig m model.search
- in case res of
- Nothing -> ({ model | search = nm }, c)
- Just r ->
- let nrow = { vid = r.id, title = r.title }
- in (validate { model | search = A.clear nm, vn = model.vn ++ [nrow] }, c)
-
-
-view : Model -> Html Msg
-view model =
- let
- entry n e = editListRow "row--ai-center"
- [ editListField 1 "col-form-label single-line"
- [ a [href <| "/v" ++ String.fromInt e.vid, title e.title, target "_blank" ] [text e.title ] ]
- , editListField 0 "" [ removeButton (Del n) ]
- ]
-
- in cardRow "Visual Novels" Nothing
- <| editList (List.indexedMap entry model.vn)
- ++ formGroups (
- (if model.duplicates
- then [ [ div [ class "invalid-feedback" ]
- [ text "The list contains duplicates. Make sure that the same visual novel is not listed multiple times." ] ] ]
- else []
- ) ++
- (if List.isEmpty model.vn
- then [ [ div [ class "invalid-feedback" ]
- [ text "Please make sure that at least one visual novel is selected." ] ] ]
- else []
- ) ++
- [ label [for "add-vn"] [text "Add visual novel"]
- :: A.view searchConfig model.search [placeholder "Visual Novel...", style "max-width" "400px"]
- ]
- )
diff --git a/elm3/StaffEdit/Main.elm b/elm3/StaffEdit/Main.elm
deleted file mode 100644
index 96d90343..00000000
--- a/elm3/StaffEdit/Main.elm
+++ /dev/null
@@ -1,220 +0,0 @@
-module StaffEdit.Main exposing (Model, Msg, main, new, view, update)
-
-import Html exposing (..)
-import Html.Events exposing (..)
-import Html.Attributes exposing (..)
-import Json.Encode as JE
-import Browser
-import Browser.Navigation exposing (load)
-import Lib.Util exposing (..)
-import Lib.Html exposing (..)
-import Lib.Gen as Gen
-import Lib.Api as Api
-import Lib.Editsum as Editsum
-
-
-main : Program Gen.StaffEdit Model Msg
-main = Browser.element
- { init = \e -> (init e, Cmd.none)
- , view = view
- , update = update
- , subscriptions = always Sub.none
- }
-
-
-type alias Model =
- { state : Api.State
- , editsum : Editsum.Model
- , alias : List Gen.StaffEditAlias
- , aliasDup : Bool
- , aid : Int
- , desc : String
- , gender : String
- , l_site : String
- , l_wp : String
- , l_twitter : String
- , l_anidb : Maybe Int
- , lang : String
- , id : Maybe Int
- }
-
-
-init : Gen.StaffEdit -> Model
-init d =
- { state = Api.Normal
- , editsum = { authmod = d.authmod, editsum = d.editsum, locked = d.locked, hidden = d.hidden }
- , alias = d.alias
- , aliasDup = False
- , aid = d.aid
- , desc = d.desc
- , gender = d.gender
- , l_site = d.l_site
- , l_wp = d.l_wp
- , l_twitter = d.l_twitter
- , l_anidb = d.l_anidb
- , lang = "ja"
- , id = d.id
- }
-
-
-new : Model
-new =
- { state = Api.Normal
- , editsum = Editsum.new
- , alias = [ { aid = -1, name = "", original = "", inuse = False } ]
- , aliasDup = False
- , aid = -1
- , desc = ""
- , gender = "unknown"
- , l_site = ""
- , l_wp = ""
- , l_twitter = ""
- , l_anidb = Nothing
- , lang = "ja"
- , id = Nothing
- }
-
-
-encode : Model -> Gen.StaffEditSend
-encode model =
- { editsum = model.editsum.editsum
- , hidden = model.editsum.hidden
- , locked = model.editsum.locked
- , aid = model.aid
- , alias = List.map (\e -> { aid = e.aid, name = e.name, original = e.original }) model.alias
- , desc = model.desc
- , gender = model.gender
- , l_anidb = model.l_anidb
- , l_site = model.l_site
- , l_twitter = model.l_twitter
- , l_wp = model.l_wp
- , lang = model.lang
- }
-
-
-newAid : Model -> Int
-newAid model =
- let id = Maybe.withDefault 0 <| List.minimum <| List.map .aid model.alias
- in if id >= 0 then -1 else id - 1
-
-
-type Msg
- = Editsum Editsum.Msg
- | Submit
- | Submitted Api.Response
- | Lang String
- | Website String
- | LWP String
- | LTwitter String
- | LAnidb String
- | Desc String
- | AliasDel Int
- | AliasName Int String
- | AliasOrig Int String
- | AliasMain Int Bool
- | AliasAdd
-
-
-validate : Model -> Model
-validate model = { model | aliasDup = hasDuplicates <| List.map (\e -> (e.name, e.original)) model.alias }
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Editsum m -> ({ model | editsum = Editsum.update m model.editsum }, Cmd.none)
- Lang s -> ({ model | lang = s }, Cmd.none)
- Website s -> ({ model | l_site = s }, Cmd.none)
- LWP s -> ({ model | l_wp = s }, Cmd.none)
- LTwitter s -> ({ model | l_twitter = s }, Cmd.none)
- LAnidb s -> ({ model | l_anidb = if s == "" then Nothing else String.toInt s }, Cmd.none)
- Desc s -> ({ model | desc = s }, Cmd.none)
-
- AliasDel i -> (validate { model | alias = delidx i model.alias }, Cmd.none)
- AliasName i s -> (validate { model | alias = modidx i (\e -> { e | name = s }) model.alias }, Cmd.none)
- AliasOrig i s -> (validate { model | alias = modidx i (\e -> { e | original = s }) model.alias }, Cmd.none)
- AliasMain n _ -> ({ model | aid = n }, Cmd.none)
- AliasAdd -> ({ model | alias = model.alias ++ [{ aid = newAid model, name = "", original = "", inuse = False }] }, Cmd.none)
-
- Submit ->
- let
- path =
- case model.id of
- Just id -> "/s" ++ String.fromInt id ++ "/edit"
- Nothing -> "/s/add"
- body = Gen.staffeditSendEncode (encode model)
- in ({ model | state = Api.Loading }, Api.post path body Submitted)
-
- Submitted (Gen.Changed id rev) -> (model, load <| "/s" ++ String.fromInt id ++ "." ++ String.fromInt rev)
- Submitted r -> ({ model | state = Api.Error r }, Cmd.none)
-
-
-isValid : Model -> Bool
-isValid model = not
- ( model.aliasDup
- || List.any (\e -> e.name == "") model.alias
- )
-
-
-view : Model -> Html Msg
-view model =
- let
- nameEntry n e = editListRow ""
- [ editListField 0 ""
- [ inputRadio "main" (e.aid == model.aid) (AliasMain e.aid) ]
- , editListField 1 ""
- [ inputText "" e.name (AliasName n) <| (if e.name == "" then [class "is-invalid"] else []) ++ [placeholder "Name (romaji)"] ]
- , editListField 1 ""
- [ inputText "" e.original (AliasOrig n) [placeholder "Original name"] ]
- , editListField 0 ""
- [ if model.aid == e.aid || e.inuse then text "" else removeButton (AliasDel n) ]
- ]
-
- names = cardRow "Name(s)" (Just "Selected name = primary name.")
- <| editList (List.indexedMap nameEntry model.alias)
- ++ formGroups (
- (if model.aliasDup
- then [ [ div [ class "invalid-feedback" ]
- [ text "The list contains duplicate aliases." ] ] ]
- else []
- ) ++
- [ [ button [type_ "button", class "btn", tabindex 10, onClick AliasAdd] [ text "Add alias" ] ]
- , [ div [ class "form-group__help" ]
- [ text "Aliases can only be removed if they are not selected as this entry's primary name and if they are not credited in visual novel entries."
- , text " In some cases it happens that an alias can not be removed even when there are no visible credits for it."
- , text " This means that the alias is still credited from a deleted entry. A moderator can fix this for you."
- ]
- ]
- ]
- )
-
- meta = cardRow "Meta" Nothing <| formGroups
- [ [ label [for "lang"] [ text "Primary language" ]
- , inputSelect [id "lang", name "lang", onInput Lang] model.lang Gen.languages
- ]
- , [ label [for "website"] [ text "Official Website" ]
- , inputText "website" model.l_site Website [pattern Gen.weburlPattern]
- ]
- , [ label [] [ text "Wikipedia" ]
- , p [] [ text "https://en.wikipedia.org/wiki/", inputText "l_wp" model.l_wp LWP [class "form-control--inline", maxlength 100] ]
- ]
- , [ label [] [ text "Twitter username" ]
- , p [] [ text "https://twitter.com/", inputText "l_twitter" model.l_twitter LTwitter [class "form-control--inline", maxlength 100] ]
- ]
- , [ label [] [ text "AniDB creator ID" ]
- , p []
- [ text "https://anidb.net/cr"
- , inputText "l_anidb" (Maybe.withDefault "" (Maybe.map String.fromInt model.l_anidb))
- LAnidb [class "form-control--inline", maxlength 10, pattern "^[0-9]*$"]
- ]
- ]
- ]
-
- desc = cardRow "Description" (Just "English please!") <| formGroup
- [ inputTextArea "desc" model.desc Desc [rows 8] ]
-
- in form_ Submit (model.state == Api.Loading)
- [ card "general" "General info" [] [ names, meta, desc ]
- , Html.map Editsum <| Editsum.view model.editsum
- , submitButton "Submit" model.state (isValid model) False
- ]
diff --git a/elm3/StaffEdit/New.elm b/elm3/StaffEdit/New.elm
deleted file mode 100644
index 64e58517..00000000
--- a/elm3/StaffEdit/New.elm
+++ /dev/null
@@ -1,12 +0,0 @@
-module StaffEdit.New exposing (main)
-
-import Browser
-import StaffEdit.Main as Main
-
-main : Program () Main.Model Main.Msg
-main = Browser.element
- { init = always (Main.new, Cmd.none)
- , view = Main.view
- , update = Main.update
- , subscriptions = always Sub.none
- }
diff --git a/elm3/UVNList/Options.elm b/elm3/UVNList/Options.elm
deleted file mode 100644
index 2b7f0c1a..00000000
--- a/elm3/UVNList/Options.elm
+++ /dev/null
@@ -1,37 +0,0 @@
-module UVNList.Options exposing (main)
-
--- TODO: Actually implement the Edit form & remove functionality
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Browser
-import Lib.Html exposing (..)
-
-
-main : Program () Model Msg
-main = Browser.element
- { init = always ((), Cmd.none)
- , subscriptions = always Sub.none
- , view = view
- , update = \_ _ -> ((), Cmd.none)
- }
-
-type alias Msg = ()
-type alias Model = ()
-
--- XXX: This dropdown thing relies on the fact that the JS code to find and
--- update dropdowns is run *after* all Elm objects have initialized, but this
--- is pretty fragile and may break if we ever update our view. This should be
--- made more reliable - either by making sure the dropdown-JS can handle DOM
--- changes or by moving the handling into Elm.
-view : Model -> Html Msg
-view model =
- div [class "dropdown"]
- [ a [href "#", class "more-button more-button--light dropdown__toggle d-block"]
- [ span [ class "more-button__dots" ] [] ]
- , div [class "dropdown-menu"]
- [ a [href "#", class "dropdown-menu__item"] [ text "Edit" ]
- , a [href "#", class "dropdown-menu__item"] [ text "Remove" ]
- ]
- ]
diff --git a/elm3/UVNList/Status.elm b/elm3/UVNList/Status.elm
deleted file mode 100644
index 7a2782d2..00000000
--- a/elm3/UVNList/Status.elm
+++ /dev/null
@@ -1,77 +0,0 @@
-module UVNList.Status exposing (main)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Json.Encode as JE
-import Browser
-import Lib.Api as Api
-import Lib.Html exposing (..)
-import Lib.Util exposing (lookup)
-import Lib.Gen as Gen
-
-
-main : Program Flags Model Msg
-main = Browser.element
- { init = \f -> (init f, Cmd.none)
- , subscriptions = always Sub.none
- , view = view
- , update = update
- }
-
-type alias Flags =
- { uid : Int
- , vid : Int
- , status : Int
- }
-
-type alias Model =
- { state : Api.State
- , flags : Flags
- }
-
-init : Flags -> Model
-init f =
- { state = Api.Normal
- , flags = f
- }
-
-
-encodeForm : Model -> JE.Value
-encodeForm o = JE.object
- [ ("uid", JE.int o.flags.uid)
- , ("vid", JE.int o.flags.vid)
- , ("status", JE.int o.flags.status) ]
-
-
-type Msg
- = Input String
- | Saved Api.Response
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Input s ->
- let flags = model.flags
- nflags = { flags | status = Maybe.withDefault 0 <| String.toInt s }
- nmodel = { model | flags = nflags, state = Api.Loading }
- in ( nmodel
- , Api.post "/u/setvnstatus" (encodeForm nmodel) Saved )
-
- Saved Gen.Success -> ({ model | state = Api.Normal }, Cmd.none)
- Saved e -> ({ model | state = Api.Error e }, Cmd.none)
-
-
-view : Model -> Html Msg
-view model =
- -- TODO: Display error somewhere
- if model.state == Api.Loading
- then div [ class "spinner spinner--md" ] []
- else div []
- [ text <| Maybe.withDefault "" <| lookup model.flags.status Gen.vnlistStatus
- , inputSelect
- [ class "form-control--table-edit form-control--table-edit-overlay", onInput Input ]
- (String.fromInt model.flags.status)
- (List.map (\(a,b) -> (String.fromInt a, b)) Gen.vnlistStatus)
- ]
diff --git a/elm3/UVNList/Vote.elm b/elm3/UVNList/Vote.elm
deleted file mode 100644
index 2834e25e..00000000
--- a/elm3/UVNList/Vote.elm
+++ /dev/null
@@ -1,103 +0,0 @@
-module UVNList.Vote exposing (main)
-
--- XXX: There's some unobvious and unintuitive behavior when removing a vote:
--- If the VN isn't also in the user's 'vnlist', then the VN entry will be
--- removed from the user's list and this is only visible on a page refresh. A
--- clean solution to this is to merge the 'votes' and 'vnlist' tables so that
--- there's always a 'vnlist' entry that remains. This is best done after VNDBv2
--- has been decommissioned.
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Json.Encode as JE
-import Browser
-import Regex
-import Lib.Api as Api
-import Lib.Gen as Gen
-
-
-main : Program Flags Model Msg
-main = Browser.element
- { init = \f -> (init f, Cmd.none)
- , subscriptions = always Sub.none
- , view = view
- , update = update
- }
-
-type alias Flags =
- { uid : Int
- , vid : Int
- , vote : String
- }
-
-type alias Model =
- { state : Api.State
- , flags : Flags
- , text : String
- , valid : Bool
- }
-
-init : Flags -> Model
-init f =
- { state = Api.Normal
- , flags = f
- , text = f.vote
- , valid = True
- }
-
-
-encodeForm : Model -> JE.Value
-encodeForm o = JE.object
- [ ("uid", JE.int o.flags.uid)
- , ("vid", JE.int o.flags.vid)
- , ("vote", JE.string o.text) ]
-
-
-type Msg
- = Input String
- | Save
- | Saved Api.Response
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Input s ->
- ( { model | text = s
- , valid = Regex.contains (Maybe.withDefault Regex.never <| Regex.fromString Gen.vnvotePattern) s
- }
- , Cmd.none
- )
-
- Save ->
- if model.valid && model.text /= model.flags.vote
- then ( { model | state = Api.Loading }
- , Api.post "/u/setvote" (encodeForm model) Saved )
- else (model, Cmd.none)
-
- Saved Gen.Success ->
- let flags = model.flags
- nflags = { flags | vote = model.text }
- in ({ model | flags = nflags, state = Api.Normal }, Cmd.none)
-
- Saved e -> ({ model | state = Api.Error e }, Cmd.none)
-
-
-view : Model -> Html Msg
-view model =
- -- TODO: Display error somewhere
- -- TODO: Save when pressing enter
- if model.state == Api.Loading
- then
- div [ class "spinner spinner--md" ] []
- else
- input
- [ type_ "text"
- , pattern Gen.vnvotePattern
- , class "form-control form-control--table-edit form-control--stealth"
- , classList [("is-invalid", not model.valid)]
- , value model.text
- , onInput Input
- , onBlur Save
- ] []
diff --git a/elm3/User/Login.elm b/elm3/User/Login.elm
deleted file mode 100644
index 09aa0aa7..00000000
--- a/elm3/User/Login.elm
+++ /dev/null
@@ -1,86 +0,0 @@
-module User.Login exposing (main)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Json.Encode as JE
-import Browser
-import Browser.Navigation exposing (load)
-import Lib.Api as Api
-import Lib.Gen as Gen
-import Lib.Html exposing (..)
-
-
-main : Program () Model Msg
-main = Browser.element
- { init = always (Model "" "" Api.Normal, Cmd.none)
- , subscriptions = always Sub.none
- , view = view
- , update = update
- }
-
-
-encodeForm : Model -> JE.Value
-encodeForm o = JE.object
- [ ("username", JE.string o.username)
- , ("password", JE.string o.password) ]
-
-
-type alias Model =
- { username : String
- , password : String
- , state : Api.State
- }
-
-
-type Msg
- = Username String
- | Password String
- | Submit
- | Submitted Api.Response
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Username n -> ({ model | username = n }, Cmd.none)
- Password n -> ({ model | password = n }, Cmd.none)
-
- Submit -> ( { model | state = Api.Loading }
- , Api.post "/u/login" (encodeForm model) Submitted
- )
-
- Submitted Gen.Success -> (model, load "/")
- Submitted e -> ({ model | state = Api.Error e }, Cmd.none)
-
-
-view : Model -> Html Msg
-view model = form_ Submit (model.state == Api.Loading)
- [ div [ class "card card--white card--no-separators flex-expand small-card mb-5" ]
- [ div [ class "card__header" ] [ div [ class "card__title" ] [ text "Log in" ]]
- , case model.state of
- Api.Error e ->
- div [ class "card__section card__section--error fs-medium" ]
- [ h5 [] [ text "Error" ]
- , ul []
- [ li [] [ text <| Api.showResponse e ]
- , li [] [ text "If you have not used this login form since October 2014, your account has likely been disabled. You can reset your password to regain access." ]
- ]
- ]
- _ -> text ""
- , div [ class "card__section fs-medium" ]
- [ div [ class "form-group" ] [ inputText "username" model.username Username [placeholder "Username", required True, pattern "[a-z0-9-]{2,15}"] ]
- , div [ class "form-group" ] [ inputText "password" model.password Password [placeholder "Password", required True, minlength 4, maxlength 500, type_ "password"] ]
- , div [ class "d-flex jc-between" ] [ a [ href "/u/newpass" ] [ text "Forgot your password?" ] ]
- ]
- , div [ class "card__section" ]
- [ div [ class "d-flex jc-between" ]
- [ a [ class "btn btn--subtle", href "/u/register" ] [ text "Create an account" ]
- , if model.state == Api.Loading
- then div [ class "spinner spinner--md pull-right" ] []
- else text ""
- , input [ type_ "submit", class "btn", tabindex 10, value "Log in" ] []
- ]
- ]
- ]
- ]
diff --git a/elm3/User/PassReset.elm b/elm3/User/PassReset.elm
deleted file mode 100644
index 9d6130fa..00000000
--- a/elm3/User/PassReset.elm
+++ /dev/null
@@ -1,90 +0,0 @@
-module User.PassReset exposing (main)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Browser
-import Json.Encode as JE
-import Lib.Api as Api
-import Lib.Gen as Gen
-import Lib.Html exposing (..)
-
-
-main : Program () Model Msg
-main = Browser.element
- { init = always (Model "" False Api.Normal, Cmd.none)
- , subscriptions = always Sub.none
- , view = view
- , update = update
- }
-
-
-encodeForm : Model -> JE.Value
-encodeForm o = JE.object
- [ ("email", JE.string o.email) ]
-
-
-type alias Model =
- { email : String
- , success : Bool
- , state : Api.State
- }
-
-
-type Msg
- = EMail String
- | Submit
- | Submitted Api.Response
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- EMail n -> ({ model | email = n }, Cmd.none)
-
- Submit -> ( { model | state = Api.Loading }
- , Api.post "/u/newpass" (encodeForm model) Submitted
- )
-
- Submitted Gen.Success -> ({ model | success = True }, Cmd.none)
- Submitted e -> ({ model | state = Api.Error e }, Cmd.none)
-
-
-view : Model -> Html Msg
-view model = form_ Submit (model.state == Api.Loading)
- [ div [ class "card card--white card--no-separators flex-expand small-card mb-5" ] <|
- [ div [ class "card__header" ] [ div [ class "card__title" ] [ text "Reset password" ]]
- , case model.state of
- Api.Error e ->
- div [ class "card__section card__section--error fs-medium" ]
- [ h5 [] [ text "Error" ]
- , text <| Api.showResponse e
- ]
- _ -> text ""
- ] ++ if model.success
- then
- [ div [ class "card__section fs-medium" ]
- [ text "Your password has been reset and instructions to set a new one should reach your mailbox in a few minutes." ]
- ]
- else
- [
- div [ class "card__section fs-medium" ]
- [ div [ class "form-group" ]
- [ div [ class "form-group__help" ]
- [ text "Forgot your password and can\'t login to VNDB anymore?"
- , br [] []
- , text "Don't worry! Just fill in the email address you used to register on VNDB, and you'll receive instructions to set a new password within a few minutes!"
- ]
- , inputText "email" model.email EMail [required True, type_ "email"]
- ]
- ]
- , div [ class "card__section" ]
- [ div [ class "d-flex jc-end" ]
- [ if model.state == Api.Loading
- then div [ class "spinner spinner--md pull-right" ] []
- else text ""
- , input [ type_ "submit", class "btn", tabindex 10, value "Submit" ] []
- ]
- ]
- ]
- ]
diff --git a/elm3/User/PassSet.elm b/elm3/User/PassSet.elm
deleted file mode 100644
index b85f715a..00000000
--- a/elm3/User/PassSet.elm
+++ /dev/null
@@ -1,94 +0,0 @@
-module User.PassSet exposing (main)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Json.Encode as JE
-import Browser
-import Browser.Navigation exposing (load)
-import Lib.Api as Api
-import Lib.Gen as Gen
-import Lib.Html exposing (..)
-
-
-main : Program String Model Msg
-main = Browser.element
- { init = \s ->
- ( { url = s
- , pass1 = ""
- , pass2 = ""
- , badPass = False
- , state = Api.Normal
- }
- , Cmd.none)
- , subscriptions = always Sub.none
- , view = view
- , update = update
- }
-
-
-encodeForm : Model -> JE.Value
-encodeForm o = JE.object
- [ ("pass", JE.string o.pass1) ]
-
-
-type alias Model =
- { url : String
- , pass1 : String
- , pass2 : String
- , badPass : Bool
- , state : Api.State
- }
-
-
-type Msg
- = Pass1 String
- | Pass2 String
- | Submit
- | Submitted Api.Response
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Pass1 n -> ({ model | pass1 = n, badPass = False }, Cmd.none)
- Pass2 n -> ({ model | pass2 = n, badPass = False }, Cmd.none)
-
- Submit ->
- if model.pass1 /= model.pass2
- then ({ model | badPass = True }, Cmd.none)
- else ( { model | state = Api.Loading }
- , Api.post model.url (encodeForm model) Submitted)
-
- Submitted Gen.Success -> (model, load "/")
- Submitted e -> ({ model | state = Api.Error e }, Cmd.none)
-
-
-view : Model -> Html Msg
-view model =
- let err s =
- div [ class "card__section card__section--error fs-medium" ]
- [ h5 [] [ text "Error" ]
- , text s
- ]
- in form_ Submit (model.state == Api.Loading)
- [ div [ class "card card--white card--no-separators flex-expand small-card mb-5" ]
- [ div [ class "card__header" ] [ div [ class "card__title" ] [ text "Set password" ]]
- , case model.state of
- Api.Error e -> err <| Api.showResponse e
- _ -> text ""
- , if model.badPass then err "Passwords to not match" else text ""
- , div [ class "card__section fs-medium" ]
- [ div [ class "form-group" ] [ inputText "pass1" model.pass1 Pass1 [placeholder "New password", required True, minlength 4, maxlength 500, type_ "password"] ]
- , div [ class "form-group" ] [ inputText "pass2" model.pass2 Pass2 [placeholder "Repeat", required True, minlength 4, maxlength 500, type_ "password"] ]
- ]
- , div [ class "card__section" ]
- [ div [ class "d-flex jc-end" ]
- [ if model.state == Api.Loading
- then div [ class "spinner spinner--md pull-right" ] []
- else text ""
- , input [ type_ "submit", class "btn", tabindex 10, value "Submit" ] []
- ]
- ]
- ]
- ]
diff --git a/elm3/User/Register.elm b/elm3/User/Register.elm
deleted file mode 100644
index e214cef1..00000000
--- a/elm3/User/Register.elm
+++ /dev/null
@@ -1,108 +0,0 @@
-module User.Register exposing (main)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Browser
-import Json.Encode as JE
-import Lib.Gen as Gen
-import Lib.Api as Api
-import Lib.Html exposing (..)
-
-
-main : Program () Model Msg
-main = Browser.element
- { init = always (Model "" "" 0 False Api.Normal, Cmd.none)
- , subscriptions = always Sub.none
- , view = view
- , update = update
- }
-
-
-encodeForm : Model -> JE.Value
-encodeForm o = JE.object
- [ ("username", JE.string o.username)
- , ("email", JE.string o.email )
- , ("vns", JE.int o.vns )]
-
-
-type alias Model =
- { username : String
- , email : String
- , vns : Int
- , success : Bool
- , state : Api.State
- }
-
-type Msg
- = Username String
- | EMail String
- | VNs String
- | Submit
- | Submitted Api.Response
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Username n -> ({ model | username = n }, Cmd.none)
- EMail n -> ({ model | email = n }, Cmd.none)
- VNs n -> ({ model | vns = Maybe.withDefault 0 (String.toInt n) }, Cmd.none)
-
- Submit -> ( { model | state = Api.Loading }
- , Api.post "/u/register" (encodeForm model) Submitted)
-
- Submitted Gen.Success -> ({ model | state = Api.Normal, success = True }, Cmd.none)
- Submitted e -> ({ model | state = Api.Error e}, Cmd.none)
-
-
-view : Model -> Html Msg
-view model = form_ Submit (model.state == Api.Loading)
- [ div [ class "card card--white card--no-separators flex-expand small-card mb-5" ] <|
- [ div [ class "card__header" ] [ div [ class "card__title" ] [ text "Register" ]]
- , case model.state of
- Api.Error e ->
- div [ class "card__section card__section--error fs-medium" ]
- [ h5 [] [ text "Error" ]
- , text <| Api.showResponse e
- ]
- _ -> text ""
- ] ++ if model.success
- then
- [ div [ class "card__section fs-medium" ]
- [ text "Your account has been created! In a few minutes, you should receive an email with instructions to set a password and activate your account." ]
- ]
- else
- [
- div [ class "card__section fs-medium" ]
- [ div [ class "form-group" ]
- [ label [ for "username" ] [ text "Username" ]
- , inputText "username" model.username Username [required True, pattern "[a-z0-9-]{2,15}"]
- , div [ class "form-group__help" ] [ text "Preferred username. Must be lowercase and can only consist of alphanumeric characters." ]
- ]
- , div [ class "form-group" ]
- [ label [ for "email" ] [ text "Email" ]
- , inputText "email" model.email EMail [required True, type_ "email", pattern Gen.emailPattern]
- , div [ class "form-group__help" ]
- [ text "Your email address will only be used in case you lose your password. We will never send spam or newsletters unless you explicitly ask us for it or we get hacked." ]
- ]
- , div [ class "form-group" ]
- [ label [ for "vns" ] [ text "How many visual novels are there in the database?" ]
- , inputText "vns" (String.fromInt model.vns) VNs [required True, pattern "[0-9]+"]
- , div [ class "form-group__help" ]
- [ text "Anti-bot question, you can find the answer on the "
- , a [ href "/", target "_blank" ] [ text "main page" ]
- , text "."
- ]
- ]
- ]
- , div [ class "card__section" ]
- [ div [ class "d-flex jc-end" ]
- [ if model.state == Api.Loading
- then div [ class "spinner spinner--md pull-right" ] []
- else text ""
- , input [ type_ "submit", class "btn", tabindex 10, value "Submit" ] []
- ]
- ]
- ]
- ]
diff --git a/elm3/User/Settings.elm b/elm3/User/Settings.elm
deleted file mode 100644
index 43ab941a..00000000
--- a/elm3/User/Settings.elm
+++ /dev/null
@@ -1,206 +0,0 @@
-module User.Settings exposing (main)
-
-import Bitwise exposing (..)
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Browser
-import Browser.Navigation exposing (reload)
-import Lib.Html exposing (..)
-import Lib.Gen as Gen
-import Lib.Api as Api
-
-
-main : Program Gen.UserEdit Model Msg
-main = Browser.element
- { init = init
- , view = view
- , update = update
- , subscriptions = always Sub.none
- }
-
-
-type alias Model =
- { state : Api.State
- , saved : Bool
- , data : Gen.UserEdit
- , cpass : Bool
- , pass1 : String
- , pass2 : String
- , opass : String
- , passNeq : Bool
- }
-
-
-init : Gen.UserEdit -> (Model, Cmd Msg)
-init d =
- ({state = Api.Normal
- , saved = False
- , data = d
- , cpass = False
- , pass1 = ""
- , pass2 = ""
- , opass = ""
- , passNeq = False
- }, Cmd.none)
-
-
-encode : Model -> Gen.UserEditSend
-encode model =
- { hide_list = model.data.hide_list
- , ign_votes = model.data.ign_votes
- , mail = model.data.mail
- , password = if model.cpass then Just { old = model.opass, new = model.pass1 } else Nothing
- , perm = model.data.perm
- , show_nsfw = model.data.show_nsfw
- , spoilers = model.data.spoilers
- , tags_all = model.data.tags_all
- , tags_cont = model.data.tags_cont
- , tags_ero = model.data.tags_ero
- , tags_tech = model.data.tags_tech
- , traits_sexual = model.data.traits_sexual
- , username = model.data.username
- }
-
-
-type UpdateMsg
- = Username String
- | Email String
- | Perm Int Bool
- | IgnVotes Bool
- | HideList Bool
- | ShowNsfw Bool
- | TraitsSexual Bool
- | Spoilers String
- | TagsAll Bool
- | TagsCont Bool
- | TagsEro Bool
- | TagsTech Bool
-
-type Msg
- = Submit
- | Submitted Api.Response
- | Set UpdateMsg
- | CPass Bool
- | OPass String
- | Pass1 String
- | Pass2 String
-
-
-updateField : UpdateMsg -> Gen.UserEdit -> Gen.UserEdit
-updateField msg model =
- case msg of
- Username s -> { model | username = s }
- Email s -> { model | mail = s }
- Perm n b -> { model | perm = if b then or model.perm n else and model.perm (complement n) }
- IgnVotes b -> { model | ign_votes = b }
- HideList b -> { model | hide_list = b }
- ShowNsfw b -> { model | show_nsfw = b }
- TraitsSexual b -> { model | traits_sexual = b }
- Spoilers s -> { model | spoilers = Maybe.withDefault model.spoilers (String.toInt s) }
- TagsAll b -> { model | tags_all = b }
- TagsCont b -> { model | tags_cont = b }
- TagsEro b -> { model | tags_ero = b }
- TagsTech b -> { model | tags_tech = b }
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Set m -> ({ model | saved = False, data = updateField m model.data }, Cmd.none)
- CPass b -> ({ model | saved = False, cpass = b }, Cmd.none)
- OPass s -> ({ model | saved = False, opass = s }, Cmd.none)
- Pass1 s -> ({ model | saved = False, pass1 = s, passNeq = s /= model.pass2 }, Cmd.none)
- Pass2 s -> ({ model | saved = False, pass2 = s, passNeq = s /= model.pass1 }, Cmd.none)
-
- Submit ->
- let
- path = "/u" ++ String.fromInt model.data.id ++ "/edit"
- body = Gen.usereditSendEncode (encode model)
- in ({ model | state = Api.Loading }, Api.post path body Submitted)
-
- Submitted (Gen.Success) ->
- ( { model | state = Api.Normal, saved = True, cpass = False, opass = "", pass1 = "", pass2 = "" }
- , if model.cpass then reload else Cmd.none
- )
- Submitted r -> ({ model | state = Api.Error r }, Cmd.none)
-
-
-view : Model -> Html Msg
-view model =
- form_ Submit (model.state == Api.Loading)
- [ card "account" "Account info" [] <|
-
- [ cardRow "General" Nothing <| formGroups
- [ [ label [ for "username" ] [ text "Username" ]
- , inputText "username" model.data.username (Set << Username) [required True, maxlength 200, pattern "[a-z0-9-]{2,15}", disabled (not model.data.authmod)]
- ]
- , [ label [ for "email" ] [ text "Email address" ]
- , inputText "email" model.data.mail (Set << Email) [type_ "email", required True, pattern Gen.emailPattern]
- ]
- ]
-
- , cardRow "Password" Nothing <| formGroups <|
- [ label [ class "checkbox" ]
- [ inputCheck "" model.cpass CPass
- , text " Change password" ]
- ]
- :: if not model.cpass then [] else
- [ [ label [ class "opass" ] [ text "Current password" ]
- , inputText "opass" model.opass OPass [type_ "password", required True, minlength 4, maxlength 500]
- ]
- , [ label [ class "pass1" ] [ text "New password" ]
- , inputText "pass1" model.pass1 Pass1 [type_ "password", required True, minlength 4, maxlength 500]
- ]
- , [ label [ class "pass2" ] [ text "Repeat" ]
- , inputText "pass2" model.pass2 Pass2 [type_ "password", required True, minlength 4, maxlength 500, classList [("is-invalid", model.passNeq)]]
- , if model.passNeq
- then div [class "invalid-feedback"]
- [ text "Passwords do not match." ]
- else text ""
- ]
- ]
-
- ] ++ if not model.data.authmod then [] else
- [ cardRow "Mod options" Nothing <| formGroups
- [ [ label [] [ text "Permissions" ]
- ] ++ List.map (\(n,s) ->
- label [ class "checkbox" ] [ inputCheck "" (and model.data.perm n > 0) (Set << Perm n), text (" " ++ s) ]
- ) Gen.userPerms
- , [ label [] [ text "Other" ]
- , label [ class "checkbox" ] [ inputCheck "" model.data.ign_votes (Set << IgnVotes), text "Ignore votes in VN statistics" ]
- ]
- ]
- ]
-
- , card "preferences" "Preferences" [] <|
-
- [ cardRow "Privacy" Nothing <| formGroup
- [ label [ class "checkbox" ] [ inputCheck "" model.data.hide_list (Set << HideList), text "Hide my visual novel list, vote list and wishlist and exclude these lists from the database dumps and API" ] ]
-
- , cardRow "NSFW" Nothing <| formGroups
- [ [ label [ class "checkbox" ] [ inputCheck "" model.data.show_nsfw (Set << ShowNsfw), text "Disable warnings for images that are not safe for work" ] ]
- , [ label [ class "checkbox" ] [ inputCheck "" model.data.traits_sexual (Set << TraitsSexual), text "Show sexual traits by default on character pages" ] ]
- ]
-
- , cardRow "Spoilers" Nothing <| formGroup
- [ label [ for "spoilers" ] [ text "Default spoiler level" ]
- , inputSelect [onInput (Set << Spoilers)] (String.fromInt model.data.spoilers)
- [ ("0", "Hide spoilers")
- , ("1", "Show only minor spoilers")
- , ("2", "Show all spoilers")
- ]
- ]
-
- , cardRow "Tags" Nothing <| formGroups
- [ [ label [ class "checkbox" ] [ inputCheck "" model.data.tags_all (Set << TagsAll), text "Show all tags by default on visual novel pages (don't summarize)" ] ]
- , [ label [] [ text "Default tag categories on visual novel pages:" ]
- , label [ class "chexkbox" ] [ inputCheck "" model.data.tags_cont (Set << TagsCont), text "Content" ]
- , label [ class "chexkbox" ] [ inputCheck "" model.data.tags_ero (Set << TagsEro ), text "Sexual content" ]
- , label [ class "chexkbox" ] [ inputCheck "" model.data.tags_tech (Set << TagsTech), text "Technical" ]
- ]
- ]
- ]
-
- , submitButton (if model.saved then "Saved!" else "Save") model.state (not model.passNeq) False
- ]
diff --git a/elm3/VNEdit/General.elm b/elm3/VNEdit/General.elm
deleted file mode 100644
index 4a0ebaa2..00000000
--- a/elm3/VNEdit/General.elm
+++ /dev/null
@@ -1,155 +0,0 @@
-module VNEdit.General exposing (..)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import File exposing (File)
-import Json.Decode as JD
-import Lib.Html exposing (..)
-import Lib.Gen as Gen
-import Lib.Util exposing (..)
-import Lib.Api as Api
-
-
-type alias Model =
- { desc : String
- , image : Int
- , imgState : Api.State
- , img_nsfw : Bool
- , length : Int
- , l_renai : String
- , l_wp : String
- , anime : String
- , animeList : List { aid : Int }
- , animeDuplicates : Bool
- }
-
-
-init : Gen.VNEdit -> Model
-init d =
- { desc = d.desc
- , image = d.image
- , imgState = Api.Normal
- , img_nsfw = d.img_nsfw
- , length = d.length
- , l_renai = d.l_renai
- , l_wp = d.l_wp
- , anime = String.join " " (List.map (.aid >> String.fromInt) d.anime)
- , animeList = d.anime
- , animeDuplicates = False
- }
-
-
-new : Model
-new =
- { desc = ""
- , image = 0
- , imgState = Api.Normal
- , img_nsfw = False
- , length = 0
- , l_renai = ""
- , l_wp = ""
- , anime = ""
- , animeList = []
- , animeDuplicates = False
- }
-
-
-type Msg
- = Desc String
- | Image String
- | ImgNSFW Bool
- | Length String
- | LWP String
- | LRenai String
- | Anime String
- | ImgUpload (List File)
- | ImgDone Api.Response
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Desc s -> ({ model | desc = s }, Cmd.none)
- Image s -> ({ model | image = if s == "" then 0 else Maybe.withDefault model.image (String.toInt s) }, Cmd.none)
- ImgNSFW b -> ({ model | img_nsfw = b }, Cmd.none)
- Length s -> ({ model | length = Maybe.withDefault 0 (String.toInt s) }, Cmd.none)
- LWP s -> ({ model | l_wp = s }, Cmd.none)
- LRenai s -> ({ model | l_renai = s }, Cmd.none)
-
- Anime s ->
- let lst = List.map (\e -> { aid = Maybe.withDefault 0 (String.toInt e) }) (String.words s)
- in ({ model | anime = s, animeList = lst, animeDuplicates = hasDuplicates <| List.map .aid lst }, Cmd.none)
-
- ImgUpload [i] -> ({ model | imgState = Api.Loading }, Api.postImage Api.Cv i ImgDone)
- ImgUpload _ -> (model, Cmd.none)
-
- ImgDone (Gen.Image id _ _) -> ({ model | image = id, imgState = Api.Normal }, Cmd.none)
- ImgDone r -> ({ model | image = 0, imgState = Api.Error r }, Cmd.none)
-
-
-view : Model -> (Msg -> a) -> List (Html a) -> Html a
-view model wrap titles = card "general" "General info" [] <|
- titles ++ List.map (Html.map wrap)
- [ cardRow "Description" (Just "English please!") <| formGroup
- [ inputTextArea "desc" model.desc Desc [rows 8]
- , div [class "form-group__help"]
- [ text "Short description of the main story. Please do not include untagged spoilers,"
- , text " and don't forget to list the source in case you didn't write the description yourself."
- , text " Formatting codes are allowed."
- ]
- ]
- , cardRow "Image" Nothing
- [ div [class "row"]
- [ div [class "col-md col-md--1"]
- [ div [style "max-width" "200px", style "margin-bottom" "8px"]
- [ dbImg "cv" (if model.imgState == Api.Loading then -1 else model.image) [] Nothing ]
- ]
- , div [class "col-md col-md--2"] <| formGroups
- [ [ label [for "img"] [ text "Upload new image" ]
- , input [type_ "file", class "text", name "img", id "img", Api.onFileChange ImgUpload, disabled (model.imgState == Api.Loading) ] []
- , case model.imgState of
- Api.Error r -> div [class "invalid-feedback"] [text <| Api.showResponse r]
- _ -> text ""
- , div [class "form-group__help"]
- [ text "Preferably the cover of the CD/DVD/package. Image must be in JPEG or PNG format and at most 5MB. Images larger than 256x400 will automatically be resized." ]
- ]
- , [ label [for "img_id"] [ text "Image ID" ]
- , inputText "img_id" (String.fromInt model.image) Image [pattern "^[0-9]+$", disabled (model.imgState == Api.Loading)]
- , div [class "form-group__help"]
- [ text "Use a VN image that is already on the server. Set to '0' to remove the current image." ]
- ]
- , [ label [for "img_nsfw"] [ text "NSFW" ]
- , label [class "checkbox"]
- [ inputCheck "img_nsfw" model.img_nsfw ImgNSFW
- , text " Not safe for work" ]
- , div [class "form-group__help"]
- [ text "Please check this option if the image contains nudity, gore, or is otherwise not safe in a work-friendly environment." ]
- ]
- ]
- ]
- ]
- , cardRow "Properties" Nothing <| formGroups
- [ [ label [for "length"] [ text "Length" ]
- , inputSelect [id "length", name "length", onInput Length]
- (String.fromInt model.length)
- (List.map (\(a,b) -> (String.fromInt a, b)) Gen.vnLengths)
- ]
- , [ label [] [ text "External links" ]
- , p [] [ text "http://en.wikipedia.org/wiki/", inputText "l_wp" model.l_wp LWP [class "form-control--inline", maxlength 100] ]
- , p [] [ text "http://renai.us/game/", inputText "l_renai" model.l_renai LRenai [class "form-control--inline", maxlength 100], text ".shtml" ]
- ]
- -- TODO: Nicer list-editing and search suggestions for anime
- , [ label [ for "anime" ] [ text "Anime" ]
- , inputText "anime" model.anime Anime [pattern "^[ 0-9]*$"]
- , if model.animeDuplicates
- then div [class "invalid-feedback"] [ text "There are duplicate anime." ]
- else text ""
- , div [class "form-group__help"]
- [ text "Whitespace separated list of AniDB anime IDs. E.g. \"1015 3348\" will add Shingetsutan Tsukihime and Fate/stay night as related anime."
- , br [] []
- , text "Note: It can take a few minutes for the anime titles to appear on the VN page."
- ]
- ]
- ]
- ]
diff --git a/elm3/VNEdit/Main.elm b/elm3/VNEdit/Main.elm
deleted file mode 100644
index f613e62e..00000000
--- a/elm3/VNEdit/Main.elm
+++ /dev/null
@@ -1,199 +0,0 @@
-module VNEdit.Main exposing (Model, Msg, main, new, view, update)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Lazy exposing (..)
-import Json.Encode as JE
-import Browser
-import Browser.Navigation exposing (load)
-import Lib.Html exposing (..)
-import Lib.Gen as Gen
-import Lib.Api as Api
-import Lib.Editsum as Editsum
-import VNEdit.Titles as Titles
-import VNEdit.General as General
-import VNEdit.Seiyuu as Seiyuu
-import VNEdit.Staff as Staff
-import VNEdit.Screenshots as Scr
-import VNEdit.Relations as Rel
-
-
-main : Program Gen.VNEdit Model Msg
-main = Browser.element
- { init = \e -> (init e, Cmd.none)
- , view = view
- , update = update
- , subscriptions = always Sub.none
- }
-
-
-type alias Model =
- { state : Api.State
- , new : Bool
- , editsum : Editsum.Model
- , l_encubed : String
- , titles : Titles.Model
- , general : General.Model
- , staff : Staff.Model
- , seiyuu : Seiyuu.Model
- , relations : Rel.Model
- , screenshots : Scr.Model
- , id : Maybe Int
- , dupVNs : List Gen.ApiVNResult
- }
-
-
-init : Gen.VNEdit -> Model
-init d =
- { state = Api.Normal
- , new = False
- , editsum = { authmod = d.authmod, editsum = d.editsum, locked = d.locked, hidden = d.hidden }
- , l_encubed = d.l_encubed
- , titles = Titles.init d
- , general = General.init d
- , staff = Staff.init d.staff
- , seiyuu = Seiyuu.init d.seiyuu d.chars
- , relations = Rel.init d.relations
- , screenshots = Scr.init d.screenshots d.releases
- , id = d.id
- , dupVNs = []
- }
-
-
-new : Model
-new =
- { state = Api.Normal
- , new = True
- , editsum = Editsum.new
- , l_encubed = ""
- , titles = Titles.new
- , general = General.new
- , staff = Staff.init []
- , seiyuu = Seiyuu.init [] []
- , relations = Rel.init []
- , screenshots = Scr.init [] []
- , id = Nothing
- , dupVNs = []
- }
-
-
-encode : Model -> Gen.VNEditSend
-encode model =
- { editsum = model.editsum.editsum
- , hidden = model.editsum.hidden
- , locked = model.editsum.locked
- , l_encubed = model.l_encubed
- , title = model.titles.title
- , original = model.titles.original
- , alias = model.titles.alias
- , desc = model.general.desc
- , image = model.general.image
- , img_nsfw = model.general.img_nsfw
- , length = model.general.length
- , l_renai = model.general.l_renai
- , l_wp = model.general.l_wp
- , anime = model.general.animeList
- , staff = List.map (\e -> { aid = e.aid, role = e.role, note = e.note }) model.staff.staff
- , seiyuu = List.map (\e -> { aid = e.aid, cid = e.cid, note = e.note }) model.seiyuu.seiyuu
- , screenshots = List.map (\e -> { scr = e.scr, rid = e.rid, nsfw = e.nsfw }) model.screenshots.screenshots
- , relations = List.map (\e -> { vid = e.vid, relation = e.relation, official = e.official }) model.relations.relations
- }
-
-
-type Msg
- = Editsum Editsum.Msg
- | Submit
- | Submitted Api.Response
- | Titles Titles.Msg
- | General General.Msg
- | Staff Staff.Msg
- | Seiyuu Seiyuu.Msg
- | Relations Rel.Msg
- | Screenshots Scr.Msg
- | CheckDup
- | RecvDup Api.Response
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Editsum m -> ({ model | editsum = Editsum.update m model.editsum }, Cmd.none)
- Titles m -> ({ model | titles = Titles.update m model.titles, dupVNs = [] }, Cmd.none)
-
- Submit ->
- let
- path =
- case model.id of
- Just id -> "/v" ++ String.fromInt id ++ "/edit"
- Nothing -> "/v/add"
- body = Gen.vneditSendEncode (encode model)
- in ({ model | state = Api.Loading }, Api.post path body Submitted)
-
- Submitted (Gen.Changed id rev) -> (model, load <| "/v" ++ String.fromInt id ++ "." ++ String.fromInt rev)
- Submitted r -> ({ model | state = Api.Error r }, Cmd.none)
-
- General m -> let (nm, c) = General.update m model.general in ({ model | general = nm }, Cmd.map General c)
- Staff m -> let (nm, c) = Staff.update m model.staff in ({ model | staff = nm }, Cmd.map Staff c)
- Seiyuu m -> let (nm, c) = Seiyuu.update m model.seiyuu in ({ model | seiyuu = nm }, Cmd.map Seiyuu c)
- Screenshots m -> let (nm, c) = Scr.update m model.screenshots in ({ model | screenshots = nm }, Cmd.map Screenshots c)
- Relations m -> let (nm, c) = Rel.update m model.relations in ({ model | relations = nm }, Cmd.map Relations c)
-
- CheckDup ->
- let body = JE.object
- [ ("search", JE.list JE.string <| List.filter ((/=)"") <| model.titles.title :: model.titles.original :: model.titles.aliasList)
- , ("hidden", JE.bool True) ]
- in
- if List.isEmpty model.dupVNs
- then ({ model | state = Api.Loading }, Api.post "/js/vn.json" body RecvDup)
- else ({ model | new = False }, Cmd.none)
-
- RecvDup (Gen.VNResult dup) ->
- ({ model | state = Api.Normal, dupVNs = dup, new = not (List.isEmpty dup) }, Cmd.none)
- RecvDup r -> ({ model | state = Api.Error r }, Cmd.none)
-
-
-
-isValid : Model -> Bool
-isValid model = not
- ( model.titles.aliasDuplicates
- || not (List.isEmpty model.titles.aliasBad)
- || model.general.animeDuplicates
- || model.staff.duplicates
- || model.seiyuu.duplicates
- || model.relations.duplicates
- )
-
-
-view : Model -> Html Msg
-view model =
- if model.new
- then form_ CheckDup (model.state == Api.Loading)
- [ card "new" "Add a new visual novel"
- [ div [class "card__subheading"]
- [ text "Carefully read the "
- , a [ href "/d2" ] [ text "guidelines" ]
- , text " before creating a new visual novel entry, to make sure that the game indeed conforms to our inclusion criteria."
- ]
- ] <|
- List.map (Html.map Titles) <| Titles.view model.titles
- , if List.isEmpty model.dupVNs
- then text ""
- else card "dup" "Possible duplicates" [ div [ class "card__subheading" ] [ text "Please check the list below for possible duplicates." ] ]
- [ cardRow "" Nothing <| formGroup [ div [ class "form-group__help" ] [
- ul [] <| List.map (\e ->
- li [] [ a [ href <| "/v" ++ String.fromInt e.id, title e.original, target "_black" ] [ text e.title ]
- , text <| if e.hidden then " (deleted)" else "" ]
- ) model.dupVNs
- ] ] ]
- , submitButton "Continue" model.state (isValid model) False
- ]
-
- else form_ Submit (model.state == Api.Loading)
- [ General.view model.general General <| List.map (Html.map Titles) <| Titles.view model.titles
- , Html.map Staff <| lazy Staff.view model.staff
- , Html.map Seiyuu <| lazy2 Seiyuu.view model.seiyuu model.id
- , Html.map Relations <| lazy Rel.view model.relations
- , Html.map Screenshots <| lazy2 Scr.view model.screenshots model.id
- , Html.map Editsum <| lazy Editsum.view model.editsum
- , submitButton "Submit" model.state (isValid model) (model.general.imgState == Api.Loading || Scr.loading model.screenshots)
- ]
diff --git a/elm3/VNEdit/New.elm b/elm3/VNEdit/New.elm
deleted file mode 100644
index eee9ade6..00000000
--- a/elm3/VNEdit/New.elm
+++ /dev/null
@@ -1,12 +0,0 @@
-module VNEdit.New exposing (main)
-
-import Browser
-import VNEdit.Main as Main
-
-main : Program () Main.Model Main.Msg
-main = Browser.element
- { init = always (Main.new, Cmd.none)
- , view = Main.view
- , update = Main.update
- , subscriptions = always Sub.none
- }
diff --git a/elm3/VNEdit/Relations.elm b/elm3/VNEdit/Relations.elm
deleted file mode 100644
index c9f82ec6..00000000
--- a/elm3/VNEdit/Relations.elm
+++ /dev/null
@@ -1,89 +0,0 @@
-module VNEdit.Relations exposing (Model, Msg, init, update, view)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Lib.Html exposing (..)
-import Lib.Gen as Gen
-import Lib.Util exposing (..)
-import Lib.Autocomplete as A
-
-
-type alias Model =
- { relations : List Gen.VNEditRelations
- , search : A.Model Gen.ApiVNResult
- , duplicates : Bool
- }
-
-
-init : List Gen.VNEditRelations -> Model
-init l =
- { relations = l
- , search = A.init
- , duplicates = False
- }
-
-
-type Msg
- = Del Int
- | Official Int Bool
- | Rel Int String
- | Search (A.Msg Gen.ApiVNResult)
-
-
-searchConfig : A.Config Msg Gen.ApiVNResult
-searchConfig = { wrap = Search, id = "add-relation", source = A.vnSource }
-
-
-validate : Model -> Model
-validate model = { model | duplicates = hasDuplicates <| List.map .vid model.relations }
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Del i -> (validate { model | relations = delidx i model.relations }, Cmd.none)
- Official i b -> (validate { model | relations = modidx i (\e -> { e | official = b }) model.relations }, Cmd.none)
- Rel i s -> (validate { model | relations = modidx i (\e -> { e | relation = s }) model.relations }, Cmd.none)
- Search m ->
- let (nm, c, res) = A.update searchConfig m model.search
- in case res of
- Nothing -> ({ model | search = nm }, c)
- Just r ->
- let
- rel = List.head Gen.vnRelations |> Maybe.map Tuple.first |> Maybe.withDefault ""
- nrow = { vid = r.id, relation = rel, title = r.title, official = True }
- in (validate { model | search = A.clear nm, relations = model.relations ++ [nrow] }, c)
-
-
-view : Model -> Html Msg
-view model =
- let
- entry n e = editListRow "row--ai-center"
- [ editListField 1 "text-sm-right single-line"
- [ a [href <| "/v" ++ String.fromInt e.vid, title e.title, target "_blank" ] [text e.title ] ]
- , editListField 0 ""
- [ text "is an "
- , label [class "checkbox"]
- [ inputCheck "" e.official (Official n)
- , text " official"
- ]
- ]
- , editListField 1 ""
- [ inputSelect [onInput (Rel n)] e.relation Gen.vnRelations ]
- , editListField 0 "single-line" [ text " of this VN" ]
- , editListField 0 "" [ removeButton (Del n) ]
- ]
-
- in card "relations" "Relations" [] <|
- editList (List.indexedMap entry model.relations)
- ++ formGroups (
- (if model.duplicates
- then [ [ div [ class "invalid-feedback" ]
- [ text "The list contains duplicates. Make sure that the same visual novel is not listed multiple times." ] ] ]
- else []
- ) ++
- [ label [for "add-relation"] [text "Add relation"]
- :: A.view searchConfig model.search [placeholder "Visual Novel...", style "max-width" "400px"]
- ]
- )
diff --git a/elm3/VNEdit/Screenshots.elm b/elm3/VNEdit/Screenshots.elm
deleted file mode 100644
index f0888c8e..00000000
--- a/elm3/VNEdit/Screenshots.elm
+++ /dev/null
@@ -1,182 +0,0 @@
-module VNEdit.Screenshots exposing (Model, Msg, loading, init, update, view)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import File exposing (File)
-import Lib.Html exposing (..)
-import Lib.Util exposing (..)
-import Lib.Api as Api
-import Lib.Gen as Gen
-import Lib.Util exposing (lookup, isJust)
-
-
-type alias Model =
- { screenshots : List Gen.VNEditScreenshots
- , releases : List Gen.VNEditReleases
- , state : List Api.State
- , id : Int -- Temporary negative internal screenshot identifier, until the image has been uploaded and the actual ID is known
- , rel : Int
- , nsfw : Bool
- , files : List File
- }
-
-
-init : List Gen.VNEditScreenshots -> List Gen.VNEditReleases -> Model
-init scr rels =
- { screenshots = scr
- , releases = rels
- , state = List.map (always Api.Normal) scr
- , id = -1
- , rel = Maybe.withDefault 0 <| Maybe.map .id <| List.head rels
- , nsfw = False
- , files = []
- }
-
-
-loading : Model -> Bool
-loading model = List.any (\s -> s /= Api.Normal) model.state
-
-
-type Msg
- = Del Int
- | SetNSFW Int Bool
- | SetRel Int String
- | DefNSFW Bool
- | DefRel String
- | DefFiles (List File)
- | Upload
- | Done Int Api.Response
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Del i -> ({ model | screenshots = delidx i model.screenshots, state = delidx i model.state }, Cmd.none)
- SetNSFW i b -> ({ model | screenshots = modidx i (\e -> { e | nsfw = b }) model.screenshots }, Cmd.none)
- SetRel i s -> ({ model | screenshots = modidx i (\e -> { e | rid = Maybe.withDefault e.rid (String.toInt s) }) model.screenshots }, Cmd.none)
- DefNSFW b -> ({ model | nsfw = b }, Cmd.none)
- DefRel s -> ({ model | rel = Maybe.withDefault 0 (String.toInt s) }, Cmd.none)
- DefFiles l -> ({ model | files = l }, Cmd.none)
-
- Upload ->
- let
- st = model.state ++ List.map (always Api.Loading) model.files
- scr i _ = { scr = model.id - i, rid = model.rel, nsfw = model.nsfw, width = 0, height = 0 }
- alst = List.indexedMap scr model.files
- lst = model.screenshots ++ alst
- nid = model.id - List.length model.files
- cmd f i = Api.postImage Api.Sf f (Done i.scr)
- cmds = List.map2 cmd model.files alst
- in ({ model | screenshots = lst, id = nid, state = st, files = [] }, Cmd.batch cmds)
-
- Done id r ->
- case List.head <| List.filter (\(_,i) -> i.scr == id) <| List.indexedMap (\a b -> (a,b)) model.screenshots of
- Nothing -> (model, Cmd.none)
- Just (n,_) ->
- let
- st _ = case r of
- Gen.Image _ _ _ -> Api.Normal
- re -> Api.Error re
- scr s = case r of
- Gen.Image nid width height -> { s | scr = nid, width = width, height = height }
- _ -> s
- in ({ model | screenshots = modidx n scr model.screenshots, state = modidx n st model.state }, Cmd.none)
-
-
-
-view : Model -> Maybe Int -> Html Msg
-view model vid =
- let
- row image remove titl opts after = div [class "screenshot-edit__row"]
- [ div [ class "screenshot-edit__screenshot" ] [ image ]
- , div [ class "screenshot-edit__fields" ] <|
- [ remove
- , div [ class "screenshot-edit__title" ] [ text titl ]
- , div [ class "screenshot-edit__options" ] opts
- ] ++ after
- ]
-
- rm n = div [ class "screenshot-edit__remove" ] [ removeButton (Del n) ]
- img n f = dbImg "st" n [class "vn-image-placeholder--wide"] f
-
- commonRes res =
- -- NDS resolution, not in the database
- res == "256x384" || isJust (lookup res Gen.resolutions)
-
- resWarn e =
- let res = String.fromInt e.width ++ "x" ++ String.fromInt e.height
- in case List.filter (\r -> r.id == e.rid) model.releases |> List.head of
- Nothing -> text "" -- Shouldn't happen
- Just r ->
- -- If the release resolution is known and does *not* match the image resolution, warn about that
- if r.resolution /= "unknown" && r.resolution /= "nonstandard" && r.resolution /= res
- then div [ class "invalid-feedback" ]
- [ text <| "Screenshot resolution is not the same as that of the selected release (" ++ r.resolution ++ "). Please make sure take screenshots in that *exact* resolution!" ]
- -- Otherwise, if this isn't a non-standard resolution, check for common ones
- else if r.resolution == "nonstandard" || commonRes res
- then text ""
- else div [ class "invalid-feedback" ]
- [ text <| "Odd screenshot resolution. Please make sure take screenshots in the correct resolution!" ]
-
- entry n (s,e) = case s of
- Api.Loading -> row (img -1 Nothing) (rm n) "Uploading screenshot" [] []
- Api.Error r -> row
- (img 0 Nothing) (rm n) "Upload failed"
- [ div [ class "invalid-feedback" ] [ text <| Api.showResponse r ] ]
- []
- Api.Normal -> row
- (img e.scr <| Just { width = e.width, height = e.height, id = "scr" })
- (rm n) ("Screenshot #" ++ String.fromInt e.scr)
- [ span [ class "muted" ] [ text <| String.fromInt e.width ++ "x" ++ String.fromInt e.height ]
- , label [ class "checkbox" ]
- [ inputCheck "" e.nsfw (SetNSFW n)
- , text " Not safe for work"
- ]
- ]
- [ resWarn e
- , releaseSelect e.rid (SetRel n) ]
-
- add = if List.length model.screenshots == 10 then text "" else row
- (text "")
- (text "")
- "Add screenshot"
- [ span [ class "muted" ] [ text "Image must be smaller than 5MB and in PNG or JPEG format. No more than 10 screenshots can be uploaded." ] ]
- [ releaseSelect model.rel DefRel
- , div [ class "screenshot-edit__upload-options" ]
- [ div [ class "screenshot-edit__upload-option" ] [ input [ type_ "file", id "addscr", tabindex 10, multiple True, Api.onFileChange DefFiles ] [] ]
- , div [ class "screenshot-edit__upload-option" ]
- [ label [ class "checkbox screenshot-edit__upload-nsfw-label" ]
- [ inputCheck "" model.nsfw DefNSFW
- , text " Not safe for work" ] ]
- , div [ class "flex-expand" ] []
- , div [ class "screenshot-edit__upload-option" ]
- [ button
- [ type_ "button", class "btn screenshot-edit__upload-btn", tabindex 10, onClick Upload
- , disabled <| List.isEmpty model.files || (List.length model.files + List.length model.screenshots) > 10
- ] [ text "Upload!" ] ]
- ]
- ]
-
- releaseSelect rid msg = inputSelect [onInput msg] (String.fromInt rid)
- <| List.map (\s -> (String.fromInt s.id, s.display)) model.releases
-
- norel =
- case vid of
- Nothing -> [ text "Screenshots can be uploaded after adding releases to this visual novel." ]
- Just i ->
- [ text "Screenshots can be added after "
- , a [ href <| "/v" ++ (String.fromInt i) ++ "/add", target "_blank" ] [ text "adding a release entry" ]
- , text "."
- ]
-
- in if List.isEmpty model.releases
- then card "screenshots" "Screenshots" [ div [class "card__subheading"] norel ] []
- else card "screenshots" "Screenshots"
- [ div [class "card__subheading"]
- [ text "Keep in mind that all screenshots must conform to "
- , a [href "/d2#6", target "blank"] [ text "strict guidelines" ]
- , text ", read those carefully!"
- ]
- ]
- [ div [class "screenshot-edit"] <| List.indexedMap entry (List.map2 (\a b -> (a,b)) model.state model.screenshots) ++ [ add ] ]
diff --git a/elm3/VNEdit/Seiyuu.elm b/elm3/VNEdit/Seiyuu.elm
deleted file mode 100644
index 3473bb7b..00000000
--- a/elm3/VNEdit/Seiyuu.elm
+++ /dev/null
@@ -1,104 +0,0 @@
-module VNEdit.Seiyuu exposing (Model, Msg, init, update, view)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Lib.Util exposing (..)
-import Lib.Html exposing (..)
-import Lib.Gen as Gen
-import Lib.Autocomplete as A
-
-
-type alias Model =
- { chars : List Gen.VNEditChars
- , seiyuu : List Gen.VNEditSeiyuu
- , search : A.Model Gen.ApiStaffResult
- , duplicates : Bool
- }
-
-
-init : List Gen.VNEditSeiyuu -> List Gen.VNEditChars -> Model
-init s c =
- { chars = c
- , seiyuu = s
- , search = A.init
- , duplicates = False
- }
-
-
-type Msg
- = Del Int
- | SetNote Int String
- | SetChar Int String
- | Search (A.Msg Gen.ApiStaffResult)
-
-
-searchConfig : A.Config Msg Gen.ApiStaffResult
-searchConfig = { wrap = Search, id = "add-seiyuu", source = A.staffSource }
-
-
-validate : Model -> Model
-validate model = { model | duplicates = hasDuplicates <| List.map (\e -> (e.aid,e.cid )) model.seiyuu }
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Del i -> (validate { model | seiyuu = delidx i model.seiyuu }, Cmd.none)
- SetNote i s -> (validate { model | seiyuu = modidx i (\e -> { e | note = s }) model.seiyuu }, Cmd.none)
- SetChar i s -> (validate { model | seiyuu = modidx i (\e -> { e | cid = Maybe.withDefault e.cid (String.toInt s) }) model.seiyuu }, Cmd.none)
-
- Search m ->
- let (nm, c, res) = A.update searchConfig m model.search
- in case res of
- Nothing -> ({ model | search = nm }, c)
- Just r ->
- let
- char = List.head model.chars |> Maybe.map .id |> Maybe.withDefault 0
- nrow = { aid = r.aid, cid = char, id = r.id, name = r.name, note = "" }
- nmod = { model | search = A.clear nm, seiyuu = model.seiyuu ++ [nrow] }
- in (validate nmod, c)
-
-
-
-view : Model -> Maybe Int -> Html Msg
-view model id =
- let
- entry n e = editListRow ""
- [ editListField 1 "col-form-label single-line"
- [ a [href <| "/s" ++ String.fromInt e.id, target "_blank" ] [ text e.name ] ]
- , editListField 1 ""
- [ inputSelect
- [onInput (SetChar n)]
- (String.fromInt e.cid)
- (List.map (\c -> (String.fromInt c.id, c.name)) model.chars)
- ]
- , editListField 2 "" [ inputText "" e.note (SetNote n) [placeholder "Note", maxlength 250] ]
- , editListField 0 "" [ removeButton (Del n) ]
- ]
-
- nochars =
- case id of
- Nothing -> [ text "Cast can be added when the visual novel entry has characters linked to it." ]
- Just n ->
- [ text "Cast can be added after "
- , a [ href <| "/c/new?vid=" ++ (String.fromInt n), target "_blank" ] [ text "creating" ]
- , text " the appropriate character entries, or after linking "
- , a [ href "/c/all" ] [ text "existing characters" ]
- , text " to this visual novel entry."
- ]
-
- in if List.isEmpty model.chars
- then card "cast" "Cast" [ div [class "card__subheading"] nochars ] []
- else card "cast" "Cast" [] <|
- editList (List.indexedMap entry model.seiyuu)
- ++ formGroups (
- (if model.duplicates
- then [ [ div [ class "invalid-feedback" ]
- [ text "The cast list contains duplicates. Make sure that each person is only listed at most once for the same character" ] ] ]
- else []
- ) ++
- [ label [for "add-seiyuu"] [text "Add cast"]
- :: A.view searchConfig model.search [placeholder "Cast name", style "max-width" "400px"]
- ]
- )
diff --git a/elm3/VNEdit/Staff.elm b/elm3/VNEdit/Staff.elm
deleted file mode 100644
index 595b1eb0..00000000
--- a/elm3/VNEdit/Staff.elm
+++ /dev/null
@@ -1,95 +0,0 @@
-module VNEdit.Staff exposing (Model, Msg, init, update, view)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Html.Events exposing (..)
-import Lib.Html exposing (..)
-import Lib.Autocomplete as A
-import Lib.Gen as Gen
-import Lib.Util exposing (..)
-
-
-type alias Model =
- { staff : List Gen.VNEditStaff
- , search : A.Model Gen.ApiStaffResult
- , duplicates : Bool
- }
-
-
-init : List Gen.VNEditStaff -> Model
-init l =
- { staff = l
- , search = A.init
- , duplicates = False
- }
-
-
-type Msg
- = Del Int
- | SetNote Int String
- | SetRole Int String
- | Search (A.Msg Gen.ApiStaffResult)
-
-
-searchConfig : A.Config Msg Gen.ApiStaffResult
-searchConfig = { wrap = Search, id = "add-staff", source = A.staffSource }
-
-
-validate : Model -> Model
-validate model = { model | duplicates = hasDuplicates <| List.map (\e -> (e.aid,e.role)) model.staff }
-
-
-update : Msg -> Model -> (Model, Cmd Msg)
-update msg model =
- case msg of
- Del i -> (validate { model | staff = delidx i model.staff }, Cmd.none)
- SetNote i s -> (validate { model | staff = modidx i (\e -> { e | note = s }) model.staff }, Cmd.none)
- SetRole i s -> (validate { model | staff = modidx i (\e -> { e | role = s }) model.staff }, Cmd.none)
-
- Search m ->
- let (nm, c, res) = A.update searchConfig m model.search
- in case res of
- Nothing -> ({ model | search = nm }, c)
- Just r ->
- let
- role = List.head Gen.creditType |> Maybe.map Tuple.first |> Maybe.withDefault ""
- nrow = { aid = r.aid, id = r.id, name = r.name, role = role, note = "" }
- in (validate { model | search = A.clear nm, staff = model.staff ++ [nrow] }, c)
-
-
-
-view : Model -> Html Msg
-view model =
- let
- entry n e = editListRow ""
- [ editListField 1 "col-form-label single-line"
- [ a [href <| "/s" ++ String.fromInt e.id, target "_blank" ] [text e.name ] ]
- , editListField 1 ""
- [ inputSelect [onInput (SetRole n)] e.role Gen.creditType ]
- , editListField 2 ""
- [ inputText "" e.note (SetNote n) [placeholder "Note", maxlength 250] ]
- , editListField 0 "" [ removeButton (Del n) ]
- ]
-
- in card "staff" "Staff"
- [ div [class "card__subheading"]
- [ text "For information, check the "
- , a [href "/d2#3", target "_blank"] [text "staff editing guidelines"]
- , text ". You can "
- , a [href "/s/new", target "_blank"] [text "create a new staff entry"]
- , text " if it is not in the database yet, but please "
- , a [href "/s/all", target "_blank"] [text "check for aliases first"]
- , text "."
- ]
- ] <|
- editList (List.indexedMap entry model.staff)
- ++ formGroups (
- (if model.duplicates
- then [ [ div [ class "invalid-feedback" ]
- [ text "The staff list contains duplicates. Make sure that each person is only listed at most once with the same role" ] ] ]
- else []
- ) ++
- [ label [for "add-staff"] [text "Add staff"]
- :: A.view searchConfig model.search [placeholder "Staff name", style "max-width" "400px"]
- ]
- )
diff --git a/elm3/VNEdit/Titles.elm b/elm3/VNEdit/Titles.elm
deleted file mode 100644
index 9dad830d..00000000
--- a/elm3/VNEdit/Titles.elm
+++ /dev/null
@@ -1,103 +0,0 @@
-module VNEdit.Titles exposing (..)
-
-import Html exposing (..)
-import Html.Attributes exposing (..)
-import Dict
-import Lib.Html exposing (..)
-import Lib.Gen exposing (..)
-import Lib.Util exposing (..)
-
-
-type alias Model =
- { title : String
- , original : String
- , alias : String
- , aliasList : List String
- , aliasDuplicates : Bool
- , aliasBad : List String
- , aliasRel : Dict.Dict String Bool
- }
-
-
-init : VNEdit -> Model
-init d =
- { title = d.title
- , original = d.original
- , alias = d.alias
- , aliasList = splitLn d.alias
- , aliasDuplicates = False
- , aliasBad = []
- , aliasRel = Dict.fromList <| List.map (\e -> (e,True)) <| List.map .title d.releases ++ List.map .original d.releases
- }
-
-
-new : Model
-new =
- { title = ""
- , original = ""
- , alias = ""
- , aliasList = []
- , aliasDuplicates = False
- , aliasBad = []
- , aliasRel = Dict.empty
- }
-
-
-type Msg
- = Title String
- | Original String
- | Alias String
-
-
-update : Msg -> Model -> Model
-update msg model =
- case msg of
- Title s -> { model | title = s }
- Original s -> { model | original = s }
- Alias s ->
- let
- lst = splitLn s
- check a = a == model.title || a == model.original || Dict.member a model.aliasRel
- in
- { model
- | alias = s
- , aliasList = lst
- , aliasDuplicates = hasDuplicates lst
- , aliasBad = List.filter check lst
- }
-
-
-view : Model -> List (Html Msg)
-view model =
- [ cardRow "Title" Nothing <| formGroups
- [ [ label [for "title"] [text "Title (romaji)"]
- , inputText "title" model.title Title [required True, maxlength 250]
- ]
- , [ label [for "original"] [text "Original"]
- , inputText "original" model.original Original [maxlength 250]
- , div [class "form-group__help"] [text "The original title of this visual novel, leave blank if it already is in the Latin alphabet."]
- ]
- ]
- , cardRow "Aliases" Nothing <| formGroup
- [ inputTextArea "aliases" model.alias Alias
- [ rows 4, maxlength 500
- , classList [("is-invalid", model.aliasDuplicates || not (List.isEmpty model.aliasBad))]
- ]
- , if model.aliasDuplicates
- then div [class "invalid-feedback"]
- [ text "There are duplicate aliases." ]
- else text ""
- , if List.isEmpty model.aliasBad
- then text ""
- else div [class "invalid-feedback"]
- [ text
- <| "The following aliases are already listed elsewhere and should be removed: "
- ++ String.join ", " model.aliasBad
- ]
- , div [class "form-group__help"]
- [ text "List of alternative titles or abbreviations. One line for each alias. Can include both official (japanese/english) titles and unofficial titles used around net."
- , br [] []
- , text "Titles that are listed in the releases should not be added here!"
- ]
- ]
- ]
diff --git a/elm3/elm.json b/elm3/elm.json
deleted file mode 100644
index 6867e2b1..00000000
--- a/elm3/elm.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "type": "application",
- "source-directories": [
- "."
- ],
- "elm-version": "0.19.1",
- "dependencies": {
- "direct": {
- "elm/browser": "1.0.1",
- "elm/core": "1.0.2",
- "elm/file": "1.0.1",
- "elm/html": "1.0.0",
- "elm/http": "2.0.0",
- "elm/json": "1.1.2",
- "elm/regex": "1.0.0",
- "justinmimbs/date": "3.1.2"
- },
- "indirect": {
- "elm/bytes": "1.0.3",
- "elm/parser": "1.1.0",
- "elm/time": "1.0.0",
- "elm/url": "1.0.0",
- "elm/virtual-dom": "1.0.2"
- }
- },
- "test-dependencies": {
- "direct": {},
- "indirect": {}
- }
-}
diff --git a/lib/Multi/API.pm b/lib/Multi/API.pm
index 5cb68c1b..120c3341 100644
--- a/lib/Multi/API.pm
+++ b/lib/Multi/API.pm
@@ -1036,20 +1036,22 @@ my %GET_USER = (
# the uid filter for votelist/vnlist/wishlist
-my $UID_FILTER = [ 'int' => 'uid :op: :value:', {qw|= =|}, range => [0,1e6], process => \&subst_user_id ];
+my $UID_FILTER = [ 'int' => 'uv.uid :op: :value:', {qw|= =|}, range => [0,1e6], process => \&subst_user_id ];
# Similarly, a filter for 'vid'
my $VN_FILTER = [
- [ 'int' => 'vid :op: :value:', {qw|= = != <> > > < < <= <= >= >=|}, range => [1,1e6] ],
- [ inta => 'vid :op:(:value:)', {'=' => 'IN', '!=' => 'NOT IN'}, range => [1,1e6], join => ',' ],
+ [ 'int' => 'uv.vid :op: :value:', {qw|= = != <> > > < < <= <= >= >=|}, range => [1,1e6] ],
+ [ inta => 'uv.vid :op:(:value:)', {'=' => 'IN', '!=' => 'NOT IN'}, range => [1,1e6], join => ',' ],
];
+my $UV_PUBLIC = 'EXISTS(SELECT 1 FROM ulist_vns_labels uvl JOIN ulist_labels ul ON ul.uid = uvl.uid AND ul.id = uvl.lbl WHERE uvl.uid = uv.uid AND uvl.vid = uv.vid AND NOT ul.private)';
+
my %GET_VOTELIST = (
islist => 1,
- sql => "SELECT %s FROM votes v WHERE (%s) AND NOT EXISTS(SELECT 1 FROM users WHERE id = v.uid AND hide_list) %s",
- sqluser => q{SELECT %1$s FROM votes v WHERE (%2$s) AND (uid = %4$d OR NOT EXISTS(SELECT 1 FROM users WHERE id = v.uid AND hide_list)) %3$s},
- select => "uid, vid as vn, vote, extract('epoch' from date) AS added",
+ sql => "SELECT %s FROM ulist_vns uv WHERE uv.vote IS NOT NULL AND (%s) AND $UV_PUBLIC %s",
+ sqluser => "SELECT %1\$s FROM ulist_vns uv WHERE uv.vote IS NOT NULL AND (%2\$s) AND (uid = %4\$d OR $UV_PUBLIC) %3\$s",
+ select => "uid, vid as vn, vote, extract('epoch' from vote_date) AS added",
proc => sub {
$_[0]{uid}*=1;
$_[0]{vn}*=1;
@@ -1062,37 +1064,44 @@ my %GET_VOTELIST = (
filters => { uid => [ $UID_FILTER ], vn => $VN_FILTER }
);
+my $SQL_VNLIST = 'FROM ulist_vns uv LEFT JOIN ulist_vns_labels uvl ON uvl.uid = uv.uid AND uvl.vid = uv.vid AND uvl.lbl IN(1,2,3,4)'
+ .' WHERE (EXISTS(SELECT 1 FROM ulist_vns_labels uvl WHERE uvl.uid = uv.uid AND uvl.vid = uv.vid AND uvl.lbl IN(1,2,3,4))'
+ .' OR NOT EXISTS(SELECT 1 FROM ulist_vns_labels uvl WHERE uvl.uid = uv.uid AND uvl.vid = uv.vid))';
+
my %GET_VNLIST = (
islist => 1,
- sql => "SELECT %s FROM vnlists v WHERE (%s) AND NOT EXISTS(SELECT 1 FROM users WHERE id = v.uid AND hide_list) %s",
- sqluser => q{SELECT %1$s FROM vnlists v WHERE (%2$s) AND (uid = %4$d OR NOT EXISTS(SELECT 1 FROM users WHERE id = v.uid AND hide_list)) %3$s},
- select => "uid, vid as vn, status, extract('epoch' from added) AS added, notes",
+ sql => "SELECT %s $SQL_VNLIST AND (%s) AND $UV_PUBLIC GROUP BY uv.uid, uv.vid, uv.added, uv.notes %s",
+ sqluser => "SELECT %1\$s $SQL_VNLIST AND (%2\$s) AND (uv.uid = %4\$d OR $UV_PUBLIC) GROUP BY uv.uid, uv.vid, uv.added, uv.notes %3\$s",
+ select => "uv.uid, uv.vid as vn, MAX(uvl.lbl) AS status, extract('epoch' from uv.added) AS added, uv.notes",
proc => sub {
$_[0]{uid}*=1;
$_[0]{vn}*=1;
- $_[0]{status}*=1;
+ $_[0]{status} = defined $_[0]{status} ? $_[0]{status}*1 : undef;
$_[0]{added} = int $_[0]{added};
$_[0]{notes} ||= undef;
},
sortdef => 'vn',
- sorts => { vn => 'vid %s' },
+ sorts => { vn => 'uv.vid %s' },
flags => { basic => {} },
filters => { uid => [ $UID_FILTER ], vn => $VN_FILTER }
);
+my $SQL_WISHLIST = "FROM ulist_vns uv JOIN ulist_vns_labels uvl ON uvl.uid = uv.uid AND uvl.vid = uv.vid JOIN ulist_labels ul ON ul.uid = uv.uid AND ul.id = uvl.lbl"
+ ." WHERE (uvl.lbl IN(5,6) OR ul.label IN('Wishlist-Low','Wishlist-Medium','Wishlist-High'))";
+
my %GET_WISHLIST = (
islist => 1,
- sql => "SELECT %s FROM wlists w WHERE (%s) AND NOT EXISTS(SELECT 1 FROM users WHERE id = w.uid AND hide_list) %s",
- sqluser => q{SELECT %1$s FROM wlists w WHERE (%2$s) AND (uid = %4$d OR NOT EXISTS(SELECT 1 FROM users WHERE id = w.uid AND hide_list)) %3$s},
- select => "uid, vid AS vn, wstat AS priority, extract('epoch' from added) AS added",
+ sql => "SELECT %s $SQL_WISHLIST AND (%s) AND NOT ul.private GROUP BY uv.uid, uv.vid, uv.added %s",
+ sqluser => "SELECT %1\$s $SQL_WISHLIST AND (%2\$s) AND (uv.uid = %4\$d OR NOT ul.private) GROUP BY uv.uid, uv.vid, uv.added %3\$s",
+ select => "uv.uid, uv.vid AS vn, MAX(ul.label) AS priority, extract('epoch' from uv.added) AS added",
proc => sub {
$_[0]{uid}*=1;
$_[0]{vn}*=1;
- $_[0]{priority}*=1;
+ $_[0]{priority} = {'Wishlist-High' => 0, 'Wishlist-Medium' => 1, 'Wishlist-Low' => 2, 'Blacklist' => 3}->{$_[0]{priority}}//1;
$_[0]{added} = int $_[0]{added};
},
sortdef => 'vn',
- sorts => { vn => 'vid %s' },
+ sorts => { vn => 'uv.vid %s' },
flags => { basic => {} },
filters => { uid => [ $UID_FILTER ], vn => $VN_FILTER }
);
@@ -1427,28 +1436,35 @@ sub setpg {
};
}
+sub set_ulist_ret {
+ my($c, $obj) = @_;
+ setpg $obj, 'SELECT update_users_ulist_stats($1)', [ $c->{uid} ]; # XXX: This can be deferred, to speed up batch updates over the same connection
+}
+
sub set_votelist {
my($c, $obj) = @_;
- return setpg $obj, 'DELETE FROM votes WHERE uid = $1 AND vid = $2',
- [ $c->{uid}, $obj->{id} ] if !$obj->{opt};
+ return cpg $c, 'UPDATE ulist_vns SET vote = NULL, vote_date = NULL WHERE uid = $1 AND vid = $2', [ $c->{uid}, $obj->{id} ], sub {
+ set_ulist_ret $c, $obj
+ } if !$obj->{opt};
my($ev, $vv) = (exists($obj->{opt}{vote}), $obj->{opt}{vote});
return cerr $c, missing => 'No vote given', field => 'vote' if !$ev;
return cerr $c, badarg => 'Invalid vote', field => 'vote' if ref($vv) || !defined($vv) || $vv !~ /^\d+$/ || $vv < 10 || $vv > 100;
- setpg $obj, 'WITH upsert AS (UPDATE votes SET vote = $1 WHERE uid = $2 AND vid = $3 RETURNING vid)
- INSERT INTO votes (vote, uid, vid) SELECT $1, $2, $3 WHERE EXISTS(SELECT 1 FROM vn v WHERE v.id = $3) AND NOT EXISTS(SELECT 1 FROM upsert)',
- [ $vv, $c->{uid}, $obj->{id} ];
+ cpg $c, 'INSERT INTO ulist_vns (uid, vid, vote, vote_date) VALUES ($1, $2, $3, NOW()) ON CONFLICT (uid, vid) DO UPDATE SET vote = $3, vote_date = NOW(), lastmod = NOW()',
+ [ $c->{uid}, $obj->{id}, $vv ], sub { set_ulist_ret $c, $obj; }
}
sub set_vnlist {
my($c, $obj) = @_;
- return setpg $obj, 'DELETE FROM vnlists WHERE uid = $1 AND vid = $2',
- [ $c->{uid}, $obj->{id} ] if !$obj->{opt};
+ # Bug: Also removes from wishlist and votelist.
+ return cpg $c, 'DELETE FROM ulist_vns WHERE uid = $1 AND vid = $2', [ $c->{uid}, $obj->{id} ], sub {
+ set_ulist_ret $c, $obj;
+ } if !$obj->{opt};
my($es, $en, $vs, $vn) = (exists($obj->{opt}{status}), exists($obj->{opt}{notes}), $obj->{opt}{status}, $obj->{opt}{notes});
return cerr $c, missing => 'No status or notes given', field => 'status,notes' if !$es && !$en;
@@ -1458,34 +1474,63 @@ sub set_vnlist {
$vs ||= 0;
$vn ||= '';
- my $set = join ', ', $es ? 'status = $3' : (), $en ? 'notes = $4' : ();
- setpg $obj, 'WITH upsert AS (UPDATE vnlists SET '.$set.' WHERE uid = $1 AND vid = $2 RETURNING vid)
- INSERT INTO vnlists (uid, vid, status, notes) SELECT $1, $2, $3, $4 WHERE EXISTS(SELECT 1 FROM vn v WHERE v.id = $2) AND NOT EXISTS(SELECT 1 FROM upsert)',
- [ $c->{uid}, $obj->{id}, $vs, $vn ];
+ cpg $c, 'INSERT INTO ulist_vns (uid, vid, notes) VALUES ($1, $2, $3) ON CONFLICT (uid, vid) DO UPDATE SET lastmod = NOW()'.($en ? ', notes = $3' : ''),
+ [ $c->{uid}, $obj->{id}, $vn ], sub {
+ if($es) {
+ cpg $c, 'DELETE FROM ulist_vns_labels WHERE uid = $1 AND vid = $2 AND lbl IN(1,2,3,4)', [ $c->{uid}, $obj->{id} ], sub {
+ if($vs) {
+ cpg $c, 'INSERT INTO ulist_vns_labels (uid, vid, lbl) VALUES($1, $2, $3)', [ $c->{uid}, $obj->{id}, $vs ], sub {
+ set_ulist_ret $c, $obj;
+ }
+ } else {
+ set_ulist_ret $c, $obj;
+ }
+ }
+ } else {
+ set_ulist_ret $c, $obj;
+ }
+ }
}
sub set_wishlist {
my($c, $obj) = @_;
- return setpg $obj, 'DELETE FROM wlists WHERE uid = $1 AND vid = $2',
- [ $c->{uid}, $obj->{id} ] if !$obj->{opt};
+ my $sql_label = "(lbl IN(5,6) OR lbl IN(SELECT id FROM ulist_labels WHERE uid = \$1 AND label IN('Wishlist-Low','Wishlist-High','Wishlist-Medium')))";
+
+ # Bug: This will make it appear in the vnlist
+ return cpg $c, "DELETE FROM ulist_vns_labels WHERE uid = \$1 AND vid = \$2 AND $sql_label",
+ [ $c->{uid}, $obj->{id} ], sub {
+ set_ulist_ret $c, $obj;
+ } if !$obj->{opt};
my($ep, $vp) = (exists($obj->{opt}{priority}), $obj->{opt}{priority});
return cerr $c, missing => 'No priority given', field => 'priority' if !$ep;
return cerr $c, badarg => 'Invalid priority', field => 'priority' if ref($vp) || !defined($vp) || $vp !~ /^[0-3]$/;
- setpg $obj, 'WITH upsert AS (UPDATE wlists SET wstat = $1 WHERE uid = $2 AND vid = $3 RETURNING vid)
- INSERT INTO wlists (wstat, uid, vid) SELECT $1, $2, $3 WHERE EXISTS(SELECT 1 FROM vn v WHERE v.id = $3) AND NOT EXISTS(SELECT 1 FROM upsert)',
- [ $vp, $c->{uid}, $obj->{id} ];
+ # Bug: High/Med/Low statuses are only set if a Wishlist-(High|Medium|Low) label exists; These should probably be created if they don't.
+ cpg $c, 'INSERT INTO ulist_vns (uid, vid) VALUES ($1, $2) ON CONFLICT DO NOTHING', [ $c->{uid}, $obj->{id} ], sub {
+ cpg $c, "DELETE FROM ulist_vns_labels WHERE uid = \$1 AND vid = \$2 AND $sql_label", [ $c->{uid}, $obj->{id} ], sub {
+ cpg $c, 'INSERT INTO ulist_vns_labels (uid, vid, lbl) VALUES($1, $2, $3)', [ $c->{uid}, $obj->{id}, $vp == 3 ? 6 : 5 ], sub {
+ if($vp != 3) {
+ cpg $c, 'INSERT INTO ulist_vns_labels (uid, vid, lbl) SELECT $1, $2, id FROM ulist_labels WHERE uid = $1 AND label = $3',
+ [ $c->{uid}, $obj->{id}, ['Wishlist-High', 'Wishlist-Medium', 'Wishlist-Low']->[$vp] ], sub {
+ set_ulist_ret $c, $obj;
+ }
+ } else {
+ set_ulist_ret $c, $obj;
+ }
+ }
+ }
+ }
}
-
sub set_ulist {
my($c, $obj) = @_;
- return setpg $obj, 'DELETE FROM ulist_vns WHERE uid = $1 AND vid = $2',
- [ $c->{uid}, $obj->{id} ] if !$obj->{opt};
+ return cpg $c, 'DELETE FROM ulist_vns WHERE uid = $1 AND vid = $2', [ $c->{uid}, $obj->{id} ], sub {
+ set_ulist_ret $c, $obj;
+ } if !$obj->{opt};
my $opt = $obj->{opt};
my @set;
@@ -1533,7 +1578,9 @@ sub set_ulist {
return cerr $c, missing => 'No fields to change' if !@set;
cpg $c, 'INSERT INTO ulist_vns (uid, vid) VALUES ($1, $2) ON CONFLICT (uid, vid) DO NOTHING', [ $c->{uid}, $obj->{id} ], sub {
- setpg $obj, 'UPDATE ulist_vns SET '.join(',', @set).' WHERE uid = $1 AND vid = $2', \@bind;
+ cpg $c, 'UPDATE ulist_vns SET '.join(',', @set).' WHERE uid = $1 AND vid = $2', \@bind, sub {
+ set_ulist_ret $c, $obj;
+ }
};
}
diff --git a/lib/Multi/Maintenance.pm b/lib/Multi/Maintenance.pm
index e19c1890..abed87a6 100644
--- a/lib/Multi/Maintenance.pm
+++ b/lib/Multi/Maintenance.pm
@@ -71,18 +71,8 @@ my %dailies = (
# takes about 25 seconds, OK
traitcache => 'SELECT traits_chars_calc(NULL)',
- # takes about 140 seconds, not really OK
- vnpopularity => 'SELECT update_vnpopularity()',
-
- # takes about 3 seconds, can be performed in ranges as well when necessary
- vnrating => q|
- UPDATE vn SET
- c_rating = (SELECT (
- ((SELECT COUNT(vote)::real/COUNT(DISTINCT vid)::real FROM votes)*(SELECT AVG(a)::real FROM (SELECT AVG(vote) FROM votes GROUP BY vid) AS v(a)) + SUM(vote)::real) /
- ((SELECT COUNT(vote)::real/COUNT(DISTINCT vid)::real FROM votes) + COUNT(uid)::real)
- ) FROM votes WHERE vid = id AND uid NOT IN(SELECT id FROM users WHERE ign_votes)
- ),
- c_votecount = COALESCE((SELECT count(*) FROM votes WHERE vid = id AND uid NOT IN(SELECT id FROM users WHERE ign_votes)), 0)|,
+ # takes about 4 seconds, OK
+ vnstats => 'SELECT update_vnvotestats()',
# should be pretty fast
cleangraphs => q|
@@ -214,30 +204,3 @@ sub vnsearch_update { # id, res, time
1;
-
-__END__
-
-# Shouldn't really be necessary, except c_changes could be slightly off when
-# hiding/unhiding DB items.
-# This query takes almost two hours to complete and tends to bring the entire
-# site down with it, so it's been disabled for now. Can be performed in
-# ranges though.
-UPDATE users SET
- c_votes = COALESCE(
- (SELECT COUNT(vid)
- FROM votes
- WHERE uid = users.id
- GROUP BY uid
- ), 0),
- c_changes = COALESCE(
- (SELECT COUNT(id)
- FROM changes
- WHERE requester = users.id
- GROUP BY requester
- ), 0),
- c_tags = COALESCE(
- (SELECT COUNT(tag)
- FROM tags_vn
- WHERE uid = users.id
- GROUP BY uid
- ), 0)
diff --git a/lib/VN3/BBCode.pm b/lib/VN3/BBCode.pm
deleted file mode 100644
index a9922b4c..00000000
--- a/lib/VN3/BBCode.pm
+++ /dev/null
@@ -1,300 +0,0 @@
-package VN3::BBCode;
-
-use strict;
-use warnings;
-use v5.10;
-use Exporter 'import';
-use TUWF::XML 'xml_escape';
-
-our @EXPORT = qw/bb2html bb2text bb_subst_links/;
-
-# Supported BBCode:
-# [spoiler] .. [/spoiler]
-# [quote] .. [/quote]
-# [code] .. [/code]
-# [url=..] [/url]
-# [raw] .. [/raw]
-# link: http://../
-# dblink: v+, v+.+, d+#+, d+#+.+
-#
-# Permitted nesting of formatting codes:
-# spoiler -> url, raw, link, dblink
-# quote -> anything
-# code -> nothing
-# url -> raw
-# raw -> nothing
-
-
-# State action function usage:
-# _state_action \@stack, $match, $char_pre, $char_post
-# Returns: ($token, @arg) on successful parse, () otherwise.
-
-# Trivial open and close actions
-sub _spoiler_start { if(lc$_[1] eq '[spoiler]') { push @{$_[0]}, 'spoiler'; ('spoiler_start') } else { () } }
-sub _quote_start { if(lc$_[1] eq '[quote]') { push @{$_[0]}, 'quote'; ('quote_start') } else { () } }
-sub _code_start { if(lc$_[1] eq '[code]') { push @{$_[0]}, 'code'; ('code_start') } else { () } }
-sub _raw_start { if(lc$_[1] eq '[raw]') { push @{$_[0]}, 'raw'; ('raw_start') } else { () } }
-sub _spoiler_end { if(lc$_[1] eq '[/spoiler]') { pop @{$_[0]}; ('spoiler_end') } else { () } }
-sub _quote_end { if(lc$_[1] eq '[/quote]' ) { pop @{$_[0]}; ('quote_end' ) } else { () } }
-sub _code_end { if(lc$_[1] eq '[/code]' ) { pop @{$_[0]}; ('code_end' ) } else { () } }
-sub _raw_end { if(lc$_[1] eq '[/raw]' ) { pop @{$_[0]}; ('raw_end' ) } else { () } }
-sub _url_end { if(lc$_[1] eq '[/url]' ) { pop @{$_[0]}; ('url_end' ) } else { () } }
-
-sub _url_start {
- if($_[1] =~ m{^\[url=((https?://|/)[^\]>]+)\]$}i) {
- push @{$_[0]}, 'url';
- (url_start => $1)
- } else { () }
-}
-
-sub _link {
- my(undef, $match, $char_pre, $char_post) = @_;
-
- # Tags arent links
- return () if $match =~ /^\[/;
-
- # URLs (already "validated" in the parsing regex)
- return ('link') if $match =~ /^[hf]t/;
-
- # Now we're left with various forms of IDs, just need to make sure it's not surrounded by word characters
- return ('dblink') if $char_pre !~ /\w/ && $char_post !~ /\w/;
-
- ();
-}
-
-
-# Permitted actions to take in each state. The actions are run in order, if
-# none succeed then the token is passed through as text.
-# The "current state" is the most recent tag in the stack, or '' if no tags are open.
-my %STATE = (
- '' => [ \&_link, \&_url_start, \&_raw_start, \&_spoiler_start, \&_quote_start, \&_code_start],
- spoiler => [\&_spoiler_end, \&_link, \&_url_start, \&_raw_start],
- quote => [\&_quote_end, \&_link, \&_url_start, \&_raw_start, \&_spoiler_start, \&_quote_start, \&_code_start],
- code => [\&_code_end ],
- url => [\&_url_end, \&_raw_start],
- raw => [\&_raw_end ],
-);
-
-
-# Usage:
-#
-# parse $input, sub {
-# my($raw, $token, @arg) = @_;
-# return 1; # to continue processing, 0 to stop. (Note that _close tokens may still follow after stopping)
-# };
-#
-# $raw = the raw part that has been parsed
-# $token = name of the parsed bbcode token, with some special cases (see below)
-# @arg = $token-specific arguments.
-#
-# Tags:
-# text -> literal text, $raw is the text to display
-# spoiler_start -> start a spoiler
-# spoiler_end -> end
-# quote_start -> start a quote
-# quote_end -> end
-# code_start -> code block
-# code_end -> end
-# url_start -> [url=..], $arg[0] contains the url
-# url_end -> [/url]
-# raw_start -> [raw]
-# raw_end -> [/raw]
-# link -> http://.../, $raw is the link
-# dblink -> v123, t13.1, etc. $raw is the dblink
-#
-# This function will ensure correct nesting of _start and _end tokens.
-sub parse {
- my($raw, $sub) = @_;
- $raw =~ s/\r//g;
- return if !$raw && $raw ne '0';
-
- my $last = 0;
- my @stack;
-
- while($raw =~ m{(?:
- \[ \/? (?i: spoiler|quote|code|url|raw ) [^\s\]]* \] | # tag
- d[1-9][0-9]* \# [1-9][0-9]* (?: \.[1-9][0-9]* )? | # d+#+[.+]
- [tdvprcs][1-9][0-9]*\.[1-9][0-9]* | # v+.+
- [tdvprcsugi][1-9][0-9]* | # v+
- (?:https?|ftp)://[^><"\n\s\]\[]+[\d\w=/-] # link
- )}xg) {
- my $token = $&;
- my $pre = substr $raw, $last, $-[0]-$last;
- my $char_pre = $-[0] ? substr $raw, $-[0]-1, 1 : '';
- $last = pos $raw;
- my $char_post = substr $raw, $last, 1;
-
- # Pass through the unformatted text before the match
- $sub->($pre, 'text') || goto FINAL if length $pre;
-
- # Call the state functions. Arguments to these functions are implicitely
- # passed through @_, which avoids allocating a new stack for each function
- # call.
- my $state = $STATE{ $stack[$#stack]||'' };
- my @ret;
- @_ = (\@stack, $token, $char_pre, $char_post);
- for(@$state) {
- @ret = &$_;
- last if @ret;
- }
- $sub->($token, @ret ? @ret : ('text')) || goto FINAL;
- }
-
- $sub->(substr($raw, $last), 'text') if $last < length $raw;
-
-FINAL:
- # Close all tags. This code is a bit of a hack, as it bypasses the state actions.
- $sub->('', "${_}_end") for reverse @stack;
-}
-
-
-sub bb2html {
- my($input, $maxlength, $charspoil) = @_;
-
- my $incode = 0;
- my $rmnewline = 0;
- my $length = 0;
- my $ret = '';
-
- # escapes, returns string, and takes care of $length and $maxlength; also
- # takes care to remove newlines and double spaces when necessary
- my $e = sub {
- local $_ = shift;
-
- s/^\n// if $rmnewline && $rmnewline--;
- s/\n{5,}/\n\n/g if !$incode;
- s/ +/ /g if !$incode;
- $length += length $_;
- if($maxlength && $length > $maxlength) {
- $_ = substr($_, 0, $maxlength-$length);
- s/\W+\w*$//; # cleanly cut off on word boundary
- }
- s/&/&amp;/g;
- s/>/&gt;/g;
- s/</&lt;/g;
- s/\n/<br>/g if !$maxlength;
- s/\n/ /g if $maxlength;
- $_;
- };
-
- parse $input, sub {
- my($raw, $tag, @arg) = @_;
-
- #$ret .= "$tag {$raw}\n";
- #return 1;
-
- if($tag eq 'text') {
- $ret .= $e->($raw);
-
- } elsif($tag eq 'spoiler_start') {
- $ret .= !$charspoil
- ? '<b class="spoiler">'
- : '<b class="grayedout charspoil charspoil_-1">&lt;hidden by spoiler settings&gt;</b><span class="charspoil charspoil_2 hidden">';
- } elsif($tag eq 'spoiler_end') {
- $ret .= !$charspoil ? '</b>' : '</span>';
-
- } elsif($tag eq 'quote_start') {
- $ret .= '<div class="quote">' if !$maxlength;
- $rmnewline = 1;
- } elsif($tag eq 'quote_end') {
- $ret .= '</div>' if !$maxlength;
- $rmnewline = 1;
-
- } elsif($tag eq 'code_start') {
- $ret .= '<pre>' if !$maxlength;
- $rmnewline = 1;
- $incode = 1;
- } elsif($tag eq 'code_end') {
- $ret .= '</pre>' if !$maxlength;
- $rmnewline = 1;
- $incode = 0;
-
- } elsif($tag eq 'url_start') {
- $ret .= sprintf '<a href="%s" rel="nofollow">', xml_escape($arg[0]);
- } elsif($tag eq 'url_end') {
- $ret .= '</a>';
-
- } elsif($tag eq 'link') {
- $ret .= sprintf '<a href="%s" rel="nofollow">%s</a>', xml_escape($raw), $e->('link');
-
- } elsif($tag eq 'dblink') {
- (my $link = $raw) =~ s/^d(\d+)\.(\d+)\.(\d+)$/d$1#$2.$3/;
- $ret .= sprintf '<a href="/%s">%s</a>', $link, $e->($raw);
- }
-
- !$maxlength || $length < $maxlength;
- };
- $ret;
-}
-
-
-# Convert bbcode into plain text, stripping all tags and spoilers. [url] tags
-# only display the title.
-sub bb2text {
- my $input = shift;
-
- my $inspoil = 0;
- my $ret = '';
- parse $input, sub {
- my($raw, $tag, @arg) = @_;
- if($tag eq 'spoiler_start') {
- $inspoil = 1;
- } elsif($tag eq 'spoiler_end') {
- $inspoil = 0;
- } else {
- $ret .= $raw if !$inspoil && $tag !~ /_(start|end)$/;
- }
- 1;
- };
- $ret;
-}
-
-
-# Turn (most) 'dblink's into [url=..] links. This function relies on TUWF to do
-# the database querying, so can't be used from Multi.
-# Doesn't handle:
-# - d+, t+, r+ and u+ links
-# - item revisions
-sub bb_subst_links {
- my $msg = shift;
-
- # Parse a message and create an index of links to resolve
- my %lookup;
- parse $msg, sub {
- my($code, $tag) = @_;
- $lookup{$1}{$2} = 1 if $tag eq 'dblink' && $code =~ /^(.)(\d+)/;
- 1;
- };
- return $msg unless %lookup;
-
- # Now resolve the links
- state $types = { # Query must return 'id' and 'name' columns, list of IDs will be appended to it.
- v => 'SELECT id, title AS name FROM vn WHERE id IN',
- c => 'SELECT id, name FROM chars WHERE id IN',
- p => 'SELECT id, name FROM producers WHERE id IN',
- g => 'SELECT id, name FROM tags WHERE id IN',
- i => 'SELECT id, name FROM traits WHERE id IN',
- s => 'SELECT s.id, sa.name FROM staff_alias sa JOIN staff s ON s.aid = sa.id WHERE s.id IN',
- };
- my %links;
- for my $type (keys %$types) {
- next if !$lookup{$type};
- my $lst = $TUWF::OBJ->dbAlli($types->{$type}, [keys %{$lookup{$type}}]);
- $links{$type . $_->{id}} = $_->{name} for @$lst;
- }
- return $msg unless %links;
-
- # Now substitute
- my $result = '';
- parse $msg, sub {
- my($code, $tag) = @_;
- $result .= $tag eq 'dblink' && $links{$code}
- ? sprintf '[url=/%s]%s[/url]', $code, $links{$code}
- : $code;
- 1;
- };
- return $result;
-}
-
-
-1;
diff --git a/lib/VN3/Char/Edit.pm b/lib/VN3/Char/Edit.pm
deleted file mode 100644
index e711eb17..00000000
--- a/lib/VN3/Char/Edit.pm
+++ /dev/null
@@ -1,168 +0,0 @@
-package VN3::Char::Edit;
-
-use VN3::Prelude;
-
-
-my $FORM = {
- alias => { required => 0, default => '', maxlength => 500 },
- desc => { required => 0, default => '', maxlength => 5000 },
- hidden => { anybool => 1 },
- locked => { anybool => 1 },
- original => { required => 0, default => '', maxlength => 200 },
- name => { maxlength => 200 },
- b_day => { uint => 1, range => [ 0, 31 ] },
- b_month => { uint => 1, range => [ 0, 12 ] },
- s_waist => { uint => 1, range => [ 0, 99999 ] },
- s_bust => { uint => 1, range => [ 0, 99999 ] },
- s_hip => { uint => 1, range => [ 0, 99999 ] },
- height => { uint => 1, range => [ 0, 99999 ] },
- weight => { uint => 1, range => [ 0, 99999 ], required => 0 },
- gender => { gender => 1 },
- bloodt => { blood_type => 1 },
- image => { required => 0, default => 0, id => 1 }, # X
- main => { id => 1, required => 0 }, # X
- main_spoil => { spoiler => 1 },
- main_name => { _when => 'out' },
- main_is => { _when => 'out', anybool => 1 }, # If true, this character is already a "main" character for other character(s)
- traits => { maxlength => 200, sort_keys => 'tid', aoh => {
- tid => { id => 1 }, # X
- spoil => { spoiler => 1 },
- group => { _when => 'out' },
- name => { _when => 'out' },
- } },
- vns => { maxlength => 50, sort_keys => ['vid', 'rid'], aoh => {
- vid => { id => 1 }, # X
- rid => { id => 1, required => 0 }, # X
- role => { char_role => 1 },
- spoil => { spoiler => 1 },
- title => { _when => 'out' },
- } },
-
- vnrels => { _when => 'out', aoh => {
- id => { id => 1 },
- releases => { aoh => {
- id => { id => 1 },
- title => { },
- lang => { type => 'array', values => {} },
- } }
- } },
-
- id => { _when => 'out', required => 0, id => 1 },
- authmod => { _when => 'out', anybool => 1 },
- editsum => { _when => 'in out', editsum => 1 },
-};
-
-my $FORM_OUT = form_compile out => $FORM;
-my $FORM_IN = form_compile in => $FORM;
-my $FORM_CMP = form_compile cmp => $FORM;
-
-elm_form CharEdit => $FORM_OUT, $FORM_IN;
-
-
-sub vnrels {
- my @vns = @_;
- my $v = [ map +{ id => $_ }, @vns ];
- enrich_list releases => id => vid => sub {
- sql q{SELECT rv.vid, r.id, r.title FROM releases r JOIN releases_vn rv ON rv.id = r.id WHERE NOT r.hidden AND rv.vid IN}, $_[0], q{ORDER BY r.id}
- }, $v;
- enrich_list1 lang => id => id => sub { sql 'SELECT id, lang FROM releases_lang WHERE id IN', $_[0], 'ORDER BY id, lang' }, map $_->{releases}, @$v;
- $v
-}
-
-
-TUWF::get qr{/$CREV_RE/(?<type>edit|copy)} => sub {
- my $c = entry c => tuwf->capture('id'), tuwf->capture('rev') or return tuwf->resNotFound;
- return tuwf->resDenied if !can_edit c => $c;
- my $copy = tuwf->capture('type') eq 'copy';
-
- $c->{main_name} = $c->{main} ? tuwf->dbVali('SELECT name FROM chars WHERE id =', \$c->{main}) : '';
- $c->{main_is} = !$copy && tuwf->dbVali('SELECT 1 FROM chars WHERE main =', \$c->{id})||0;
-
- enrich tid => q{SELECT t.id AS tid, t.name, g.name AS group, g.order FROM traits t JOIN traits g ON g.id = t.group WHERE t.id IN} => $c->{traits};
- $c->{traits} = [ sort { $a->{order} <=> $b->{order} || $a->{name} cmp $b->{name} } @{$c->{traits}} ];
-
- enrich vid => q{SELECT id AS vid, title FROM vn WHERE id IN} => $c->{vns};
- $c->{vns} = [ sort { $a->{vid} <=> $b->{vid} } @{$c->{vns}} ];
-
- my %vids = map +($_->{vid}, 1), @{$c->{vns}};
- $c->{vnrels} = vnrels keys %vids;
-
- $c->{authmod} = auth->permDbmod;
- $c->{editsum} = $copy ? "Copied from c$c->{id}.$c->{chrev}" : $c->{chrev} == $c->{maxrev} ? '' : "Reverted to revision c$c->{id}.$c->{chrev}";
-
- my $title = sprintf '%s %s', $copy ? 'Copy' : 'Edit', $c->{name};
- Framework index => 0, title => $title,
- top => sub {
- Div class => 'col-md', sub {
- EntryEdit c => $c;
- Div class => 'detail-page-title', sub {
- Txt $title;
- Debug $c;
- };
- };
- }, sub {
- FullPageForm module => 'CharEdit.Main', schema => $FORM_OUT, data => { %$c, $copy ? (id => undef) : () }, sections => [
- general => 'General info',
- traits => 'Traits',
- vns => 'Visual novels',
- ];
- };
-};
-
-
-TUWF::get qr{/$VID_RE/addchar}, sub {
- return tuwf->resDenied if !auth->permEdit;
-
- my $vn = tuwf->dbRowi('SELECT id, title FROM vn WHERE NOT hidden AND id =', \tuwf->capture('id'));
- return tuwf->resNotFound if !$vn->{id};
-
- my $data = {
- vns => [ { vid => $vn->{id}, rid => undef, role => 'primary', spoil => 0, title => $vn->{title} } ],
- vnrels => vnrels $vn->{id}
- };
-
- Framework index => 0, title => "Add a new character to $vn->{title}", narrow => 1, sub {
- FullPageForm module => 'CharEdit.New', schema => $FORM_OUT, data => $data, sections => [
- general => 'General info',
- format => 'Format',
- relations => 'Relations'
- ];
- };
-};
-
-
-json_api qr{/(?:$CID_RE/edit|c/add)}, $FORM_IN, sub {
- my $data = shift;
- my $new = !tuwf->capture('id');
- my $c = $new ? { id => 0 } : entry c => tuwf->capture('id') or return tuwf->resNotFound;
-
- return $elm_Unauth->() if !can_edit c => $c;
-
- if(!auth->permDbmod) {
- $data->{hidden} = $c->{hidden}||0;
- $data->{locked} = $c->{locked}||0;
- }
- $data->{main} = undef if $data->{hidden};
- $data->{main_spoil} = 0 if !$data->{main};
-
- die "Image not found" if $data->{image} && !-e tuwf->imgpath(ch => $data->{image});
- if($data->{main}) {
- die "Relation with self" if $data->{main} == $c->{id};
- die "Invalid main" if !tuwf->dbVali('SELECT 1 FROM chars WHERE main IS NULL AND id =', \$data->{main});
- die "Main set when self is main" if $c->{id} && tuwf->dbVali('SELECT 1 FROM chars WHERE main =', \$c->{id});
- }
- validate_dbid 'SELECT id FROM traits WHERE id IN', map $_->{tid}, @{$data->{traits}};
- validate_dbid 'SELECT id FROM vn WHERE id IN', map $_->{vid}, @{$data->{vns}};
- for (grep $_->{rid}, @{$data->{vns}}) {
- die "Invalid release $_->{rid}" if !tuwf->dbVali('SELECT 1 FROM releases_vn WHERE', { id => $_->{rid}, vid => $_->{vid} });
- }
-
- $data->{desc} = bb_subst_links $data->{desc};
-
- return $elm_Unchanged->() if !$new && !form_changed $FORM_CMP, $data, $c;
-
- my($id,undef,$rev) = update_entry c => $c->{id}, $data;
- $elm_Changed->($id, $rev);
-};
-
-1;
diff --git a/lib/VN3/Char/JS.pm b/lib/VN3/Char/JS.pm
deleted file mode 100644
index eafda3ad..00000000
--- a/lib/VN3/Char/JS.pm
+++ /dev/null
@@ -1,55 +0,0 @@
-package VN3::Char::JS;
-
-use VN3::Prelude;
-
-
-my $elm_CharResult = elm_api CharResult => { aoh => {
- id => { id => 1 },
- name => {},
- original => {},
- main => { type => 'hash', required => 0, keys => {
- id => { id => 1 },
- name => {},
- original => {},
- }},
-}};
-
-json_api '/js/char.json', {
- search => { maxlength => 500 }
-}, sub {
- my $q = shift->{search};
-
- # XXX: This query is kinda slow
- my $qs = $q =~ s/[%_]//gr;
- my $r = tuwf->dbAlli(
- 'SELECT c.id, c.name, c.original, c.main, c2.name AS main_name, c2.original AS main_original',
- 'FROM (',
- # ID search
- $q =~ /^$CID_RE$/ ? ('SELECT 1, id FROM chars WHERE id =', \"$1", 'UNION ALL') : (),
- # exact match
- 'SELECT 2, id FROM chars WHERE lower(name) = lower(', \$q, ") OR lower(translate(original,' ', '')) = lower(", \($q =~ s/\s//gr), ')',
- 'UNION ALL',
- # prefix match
- 'SELECT 3, id FROM chars WHERE name ILIKE', \"$qs%", ' OR original ILIKE', \"$qs%",
- 'UNION ALL',
- # substring match
- 'SELECT 4, id FROM chars WHERE name ILIKE', \"%$qs%", ' OR original ILIKE', \"%$qs%",
- ') AS ct (ord, id)',
- 'JOIN chars c ON c.id = ct.id',
- 'LEFT JOIN chars c2 ON c2.id = c.main',
- 'WHERE NOT c.hidden',
- 'GROUP BY c.id, c.name, c.original, c.main, c2.name, c2.original',
- 'ORDER BY MIN(ct.ord), c.name',
- 'LIMIT 20'
- );
-
- for (@$r) {
- $_->{main} = $_->{main} ? { id => $_->{main}, name => $_->{main_name}, original => $_->{main_original} } : undef;
- delete $_->{main_name};
- delete $_->{main_original};
- }
-
- $elm_CharResult->($r);
-};
-
-1;
diff --git a/lib/VN3/Char/Page.pm b/lib/VN3/Char/Page.pm
deleted file mode 100644
index 11939060..00000000
--- a/lib/VN3/Char/Page.pm
+++ /dev/null
@@ -1,330 +0,0 @@
-package VN3::Char::Page;
-
-use VN3::Prelude;
-use List::Util 'all', 'min';
-
-sub Top {
- my $e = shift;
-
- my $img = $e->{image} && tuwf->imgurl(ch => $e->{image});
-
- Div class => 'fixed-size-left-sidebar-md', sub {
- Img class => 'page-header-img-mobile img img--rounded d-md-none', src => $img;
- Div class => 'detail-header-image-container', sub {
- Img class => 'img img--fit img--rounded elevation-1 d-none d-md-block detail-header-image', src => $img;
- };
- } if $img;
-
- Div class => 'col-md', sub {
- EntryEdit c => $e;
- Div class => 'detail-page-title', sub {
- Txt $e->{name};
- Txt ' '.gender_icon $e->{gender};
- Txt ' '.blood_type_display $e->{bloodt} if $e->{bloodt} ne 'unknown';
- Debug $e;
- };
- Div class => 'detail-page-subtitle', $e->{original} if $e->{original};
- };
-}
-
-
-sub Settings {
- my $spoil = auth->pref('spoilers') || 0;
- my $ero = auth->pref('traits_sexual');
-
- Div class => 'page-inner-controls', id => 'charpage_settings', sub {
- Div class => 'page-inner-controls__option dropdown', sub {
- A href => 'javascript:;', class => 'link--subtle dropdown__toggle', sub {
- Span class => 'page-inner-controls__option-spoil', spoil_display $spoil;
- Lit ' ';
- Span class => 'caret', '';
- };
- Div class => 'dropdown-menu', sub {
- A class => 'dropdown-menu__item page-inner-controls__option-spoil-0', href => 'javascript:;', spoil_display 0;
- A class => 'dropdown-menu__item page-inner-controls__option-spoil-1', href => 'javascript:;', spoil_display 1;
- A class => 'dropdown-menu__item page-inner-controls__option-spoil-2', href => 'javascript:;', spoil_display 2;
- };
- };
- Div class => 'page-inner-controls__option', sub {
- Switch 'Sexual traits', $ero, 'page-inner-controls__option-ero' => 1;
- };
- };
-}
-
-
-sub Description {
- my $e = shift;
-
- Div class => 'row', sub {
- Div class => 'fixed-size-left-sidebar-md', sub {
- if($e->{image}) {
- # second copy of image to ensure there's enough space (uh, mkay)
- Img class => 'img img--fit d-none d-md-block detail-header-image-push', src => tuwf->imgurl(ch => $e->{image});
- } else {
- H3 class => 'detail-page-sidebar-section-header', 'Description';
- }
- };
- Div class => 'col-md', sub {
- Div class => 'description serif mb-5', sub {
- P sub { Lit bb2html $e->{desc} };
- };
- };
- } if $e->{desc};
-}
-
-
-sub DetailsTable {
- my $e = shift;
-
- my(%groups, @groups);
- for(@{$e->{traits}}) {
- push @groups, $_->{gid} if !$groups{$_->{gid}};
- push @{$groups{$_->{gid}}}, $_;
- }
-
- # TODO: This was copy-pasted from VN::Page, need to consolidate (...once we figure out how to actually display chars on the VN page)
- my @list = (
- $e->{alias} ? sub {
- Dt 'Aliases';
- Dd $e->{alias} =~ s/\n/, /gr;
- } : (),
-
- defined $e->{weight} || $e->{height} || $e->{s_bust} || $e->{s_waist} || $e->{s_hip} ? sub {
- Dt 'Measurements';
- Dd join ', ',
- $e->{height} ? "Height: $e->{height}cm" : (),
- defined $e->{weight} ? "Weight: $e->{weight}kg" : (),
- $e->{s_bust} || $e->{s_waist} || $e->{s_hip} ?
- sprintf 'Bust-Waist-Hips: %s-%s-%scm', $e->{s_bust}||'??', $e->{s_waist}||'??', $e->{s_hip}||'??' : ();
- } : (),
-
- $e->{b_month} && $e->{b_day} ? sub {
- Dt 'Birthday';
- Dd sprintf '%d %s', $e->{b_day}, [qw{January February March April May June July August September October November December}]->[$e->{b_month}-1];
- } : (),
-
- # XXX: Group visibility is determined by the same 'charpage--x' classes
- # as the individual traits (group is considered 'ero' if all traits are
- # ero, and the lowest trait spoiler determines group spoiler level).
- # But this has an unfortunate special case that isn't handled: A trait
- # with (ero && spoil>0) in a group that isn't itself (ero && spoil>0)
- # will display an empty group if settings are (ero && spoil==0).
- # XXX#2: I'd rather have the traits delimited by a comma, but that's a
- # hard problem to solve in combination with the dynamic hiding of
- # traits.
- (map { my $g = $_; sub {
- my @c = mkclass
- 'charpage--ero' => (all { $_->{sexual} } @{$groups{$g}}),
- sprintf('charpage--spoil-%d', min map $_->{spoil}, @{$groups{$g}}) => 1;
-
- Dt @c, sub { A href => "/i$g", $groups{$g}[0]{group} };
- Dd @c, sub {
- Join ' ', sub {
- A mkclass('trait-summary--trait' => 1, 'charpage--ero' => $_[0]{sexual}, sprintf('charpage--spoil-%d', $_[0]{spoil}), 1),
- style => 'padding-right: 15px; white-space: nowrap',
- href => "/i$_[0]{tid}", $_[0]{name}
- }, @{$groups{$g}};
- };
- } } @groups),
- );
-
- Div class => 'row', sub {
- Div class => 'fixed-size-left-sidebar-md', sub {
- H2 class => 'detail-page-sidebar-section-header', 'Details';
- };
- Div class => 'col-md', sub {
- Div class => 'card card--white mb-5', sub {
- Div class => 'card__section fs-medium', sub {
- Div class => 'row', sub {
- Dl class => 'col-md dl--horizontal', sub { $_->() for @list[0..$#list/2] };
- Dl class => 'col-md dl--horizontal', sub { $_->() for @list[$#list/2+1..$#list] };
- }
- }
- }
- }
- } if @list;
-}
-
-
-sub VNs {
- my $e = shift;
-
- # TODO: Maybe this table should be full-width?
- # TODO: Improved styling of release rows
-
- my $rows = sub {
- for my $vn (@{$e->{vns}}) {
- Tr class => sprintf('charpage--spoil-%d', $vn->{spoil}), sub {
- Td class => 'tabular-nums muted', sub { ReleaseDate $vn->{c_released} };
- Td sub {
- A href => "/v$vn->{vid}", title => $vn->{original}||$vn->{title}, $vn->{title};
- };
- Td $vn->{releases}[0]{rid} ? '' : join ', ', map char_role_display($_->{role}), @{$vn->{releases}};
- Td sub {
- Join ', ', sub {
- A href => "/s$_[0]{sid}", title => $_[0]{original}||$_[0]{name}, $_[0]{name};
- Span class => 'muted', " ($_[0]{note})" if $_[0]{note};
- }, @{$vn->{seiyuu}};
- }
- };
- for my $rel ($vn->{releases}[0]{rid} ? @{$vn->{releases}} : ()) {
- Tr class => sprintf('charpage--spoil-%d', $rel->{spoil}), sub {
- Td class => 'tabular-nums muted', $rel->{rid} ? sub { Lit '&nbsp;&nbsp;'; ReleaseDate $rel->{released} } : '';
- Td sub {
- Span class => 'muted', '» ';
- A href => "/r$rel->{rid}", title => $rel->{title}||$rel->{original}, $rel->{title} if $rel->{rid};
- Span class => 'muted', 'Other releases' if !$rel->{rid};
- };
- Td char_role_display $rel->{role};
- Td '';
- };
- }
- }
- };
-
- Div class => 'row', sub {
- Div class => 'fixed-size-left-sidebar-md', sub {
- H2 class => 'detail-page-sidebar-section-header', 'Visual Novels';
- };
- Div class => 'col-md', sub {
- Div class => 'card card--white mb-5', sub {
- Table class => 'table table--responsive-single-sm fs-medium', sub {
- Thead sub {
- Tr sub {
- Th width => '15%', 'Date';
- Th width => '40%', 'Title';
- Th width => '20%', 'Role';
- Th width => '25%', 'Voiced by';
- };
- };
- Tbody $rows;
- };
- }
- }
- }
-}
-
-
-sub Instances {
- my $e = shift;
-
- return if !@{$e->{instances}};
-
- my $minspoil = min map $_->{spoiler}, @{$e->{instances}};
-
- Div class => sprintf('row charpage--spoil-%d', $minspoil), sub {
- Div class => 'fixed-size-left-sidebar-md', sub {
- H2 class => 'detail-page-sidebar-section-header', 'Other instances';
- };
- Div class => 'col-md', sub {
- for my $c (@{$e->{instances}}) {
- A class => sprintf('card card--white character-card mb-3 charpage--spoil-%d', $c->{spoiler}), href => "/c$c->{id}", sub {
- Div class => 'character-card__left', sub {
- Div class => 'character-card__image-container', sub {
- Img class => 'character-card__image', src => tuwf->imgurl(ch => $c->{image}) if $c->{image};
- };
- Div class => 'character-card__main', sub {
- Div class => 'character-card__name', sub {
- Txt $c->{name};
- Txt ' '.gender_icon $c->{gender};
- Txt ' '.blood_type_display $c->{bloodt} if $c->{bloodt} ne 'unknown';
- };
- Div class => 'character-card__sub-name', $c->{original} if $c->{original};
- Div class => 'character-card__vns muted single-line', join ', ', map $_->{title}, @{$c->{vns}} if @{$c->{vns}};
- };
- Div class => 'character-card__right serif semi-muted', sub {
- Lit bb2text $c->{desc}; # TODO: maxlength?
- };
- }
- }
- }
- };
- };
-}
-
-
-TUWF::get qr{/$CREV_RE}, sub {
- my $e = entry c => tuwf->capture('id'), tuwf->capture('rev') or return tuwf->resNotFound;
- return tuwf->resNotFound if !$e->{id} || $e->{hidden};
-
- enrich tid => q{
- SELECT t.id AS tid, t.name, t.sexual, g.id AS gid, g.name AS group, g.order
- FROM traits t
- JOIN traits g ON g.id = t.group
- WHERE t.id IN
- }, $e->{traits};
-
- $e->{traits} = [ sort { $a->{order} <=> $b->{order} || $a->{name} cmp $b->{name} } @{$e->{traits}} ];
-
- $e->{vns} = tuwf->dbAlli(q{
- SELECT cv.vid, v.title, v.original, v.c_released, MIN(cv.spoil) AS spoil
- FROM chars_vns_hist cv
- JOIN vn v ON cv.vid = v.id
- WHERE cv.chid =}, \$e->{chid}, q{
- GROUP BY v.c_released, cv.vid, v.title, v.original
- ORDER BY v.c_released, cv.vid
- });
-
- enrich_list releases => vid => vid => sub {sql q{
- SELECT cv.rid, cv.vid, cv.role, cv.spoil, r.title, r.original, r.released
- FROM chars_vns_hist cv
- LEFT JOIN releases r ON r.id = cv.rid
- WHERE cv.chid =}, \$e->{chid}, q{
- ORDER BY r.released, r.id
- }}, $e->{vns};
-
- enrich_list seiyuu => vid => vid => sub {sql q{
- SELECT vs.id AS vid, vs.note, sa.id AS sid, sa.aid, sa.name, sa.original
- FROM vn_seiyuu vs
- JOIN staff_alias sa ON vs.aid = sa.aid
- WHERE vs.cid =}, \$e->{id}, q{
- ORDER BY sa.name, sa.aid
- }}, $e->{vns};
-
- $e->{instances} = tuwf->dbAlli(q{
- SELECT id, name, original, image, gender, bloodt, "desc",
- (CASE WHEN id =}, \$e->{main}, THEN => \$e->{main_spoil}, q{ELSE main_spoil END) AS spoiler
- FROM chars
- WHERE NOT hidden
- AND id <>}, \$e->{id}, q{
- AND ( main =}, \$e->{id}, q{
- OR main =}, \$e->{main}, q{
- OR id =}, \$e->{main}, q{
- )
- ORDER BY name, id
- });
- enrich_list vns => id => cid => sub {sql q{
- SELECT cv.id AS cid, v.id, v.title
- FROM chars_vns cv
- JOIN vn v ON v.id = cv.vid
- WHERE cv.id IN}, $_[0], q{
- AND cv.spoil = 0
- GROUP BY v.id, cv.id, v.title
- ORDER BY MIN(cv.role), v.title, v.id
- }}, $e->{instances};
-
- my $spoil = auth->pref('spoilers') || 0;
- my $ero = auth->pref('traits_sexual');
-
- Framework
- og => {
- description => bb2text($e->{desc}),
- $e->{image} ? (image => tuwf->imgurl(ch => $e->{image})) : ()
- },
- title => $e->{name},
- main_classes => {
- 'charpage--hide-spoil-1' => $spoil < 1,
- 'charpage--hide-spoil-2' => $spoil < 2,
- 'charpage--hide-ero' => !$ero
- },
- top => sub { Top $e },
- sub {
- Settings $e;
- Description $e;
- DetailsTable $e;
- VNs $e;
- Instances $e;
- };
-};
-
-1;
diff --git a/lib/VN3/DB.pm b/lib/VN3/DB.pm
deleted file mode 100644
index 35b31660..00000000
--- a/lib/VN3/DB.pm
+++ /dev/null
@@ -1,287 +0,0 @@
-package VN3::DB;
-
-use v5.10;
-use strict;
-use warnings;
-use TUWF;
-use SQL::Interp ':all';
-use Carp 'carp';
-use VNWeb::DB (); # For the tuwf->dbVali etc methods
-use base 'Exporter';
-
-our @EXPORT = qw/
- sql
- sql_join sql_comma sql_and sql_array sql_func sql_fromhex sql_tohex sql_fromtime sql_totime
- enrich enrich_list enrich_list1
- entry update_entry
-/;
-
-
-
-# sql_* are macros for SQL::Interp use
-
-# join(), but for sql objects.
-sub sql_join {
- my $sep = shift;
- my @args = map +($sep, $_), @_;
- shift @args;
- return @args;
-}
-
-# Join multiple arguments together with a comma, for use in a SELECT or IN
-# clause or function arguments.
-sub sql_comma { sql_join ',', @_ }
-
-sub sql_and { sql_join 'AND', map sql('(', $_, ')'), @_ }
-
-# Construct a PostgreSQL array type from the function arguments.
-sub sql_array { 'ARRAY[', sql_join(',', map \$_, @_), ']' }
-
-# Call an SQL function
-sub sql_func {
- my($funcname, @args) = @_;
- sql $funcname, '(', sql_comma(@args), ')';
-}
-
-# Convert a Perl hex value into Postgres bytea
-sub sql_fromhex($) {
- sql_func decode => \$_[0], "'hex'";
-}
-
-# Convert a Postgres bytea into a Perl hex value
-sub sql_tohex($) {
- sql_func encode => $_[0], "'hex'";
-}
-
-# Convert a Perl time value (UNIX timestamp) into a Postgres timestamp
-sub sql_fromtime($) {
- sql_func to_timestamp => \$_[0];
-}
-
-# Convert a Postgres timestamp into a Perl time value
-sub sql_totime($) {
- sql "extract('epoch' from ", $_[0], ')';
-}
-
-
-
-# Helper function for the enrich functions below.
-sub _enrich {
- my($merge, $key, $sql, @array) = @_;
-
- # 'flatten' the given array, so that you can also give arrayrefs as argument
- @array = map +(ref $_ eq 'ARRAY' ? @$_ : $_), @array;
-
- # Create a list of unique identifiers to fetch, do nothing if there's nothing to fetch
- my %ids = map +($_->{$key},1), @array;
- return if !keys %ids;
-
- # Fetch the data
- $sql = ref $sql eq 'CODE' ? $sql->([keys %ids]) : sql $sql, [keys %ids];
- my $data = tuwf->dbAlli($sql);
-
- # And merge
- $merge->($data, \@array);
-}
-
-
-# This function is slightly magical: It is used to fetch information from the
-# database and add it to an existing data structure. Usage:
-#
-# enrich $key, $sql, $object1, $object2, [$more_objects], ..;
-#
-# Where each $object is an hashref that will be modified in-place. $key is the
-# name of a key that should be present in each $object, and indicates the value
-# that should be used as database identifier to fetch more information. $sql is
-# the SQL query that is used to fetch more information for each identifier. If
-# $sql is a subroutine, then it is given an arrayref of keys (to be used in an
-# WHERE x IN() clause), and should return a sql() query. If $sql is a string
-# or sql() query itself, then the arrayref of keys is appended to it. The
-# generated SQL query should return a column named $key, so that the other
-# columns can be merged back into the $objects.
-sub enrich {
- my($key, $sql, @array) = @_;
- _enrich sub {
- my($data, $array) = @_;
- my %ids = map +(delete($_->{$key}), $_), @$data;
- # Copy the key to a temp variable to prevent stringifycation of integer keys
- %$_ = (%$_, %{$ids{ (my $v = $_->{$key}) }}) for @$array;
- }, $key, $sql, @array;
-}
-
-
-# Similar to enrich(), but instead of requiring a one-to-one mapping between
-# $object->{$key} and the row returned by $sql, this function allows multiple
-# rows to be returned by $sql. $object->{$key} is compared with $merge_col
-# returned by the SQL query, the rows are stored as an arrayref in
-# $object->{$name}.
-sub enrich_list {
- my($name, $key, $merge_col, $sql, @array) = @_;
- _enrich sub {
- my($data, $array) = @_;
- my %ids = ();
- push @{$ids{ delete $_->{$merge_col} }}, $_ for @$data;
- $_->{$name} = $ids{ (my $v = $_->{$key}) }||[] for @$array;
- }, $key, $sql, @array;
-}
-
-
-# Similar to enrich_list(), instead of returning each row as a hash, each row
-# is taken to be a single value.
-sub enrich_list1 {
- my($name, $key, $merge_col, $sql, @array) = @_;
- _enrich sub {
- my($data, $array) = @_;
- my %ids = ();
- push @{$ids{ delete $_->{$merge_col} }}, values %$_ for @$data;
- $_->{$name} = $ids{ (my $v = $_->{$key}) }||[] for @$array;
- }, $key, $sql, @array;
-}
-
-
-
-
-# Database entry API: Intended to provide a low-level read/write interface for
-# versioned database entires. The same data structure is used for reading and
-# updating entries, and should support easy diffing/comparison.
-# Probably not very convenient for general querying & searching, but we'll see.
-
-my %entry_prefixes = (qw{
- c chars
- d docs
- p producers
- r releases
- s staff
- v vn
-});
-
-# Reads the database schema and creates a hash of
-# 'table' => [versioned item-specific columns]
-# for a particular entry prefix, where each column is a hash.
-#
-# These functions assume a specific table layout for versioned database
-# entries, as documented in util/sql/schema.sql.
-sub _entry_tables {
- my $prefix = shift;
- my $tables = tuwf->dbh->column_info(undef, undef, "$prefix%_hist", undef)->fetchall_arrayref({});
- my %tables;
- for (@$tables) {
- (my $t = $_->{TABLE_NAME}) =~ s/_hist$//;
- next if $_->{COLUMN_NAME} eq 'chid';
- push @{$tables{$t}}, {
- name => $_->{pg_column}, # Raw name, as it appears in the data structure
- type => $_->{TYPE_NAME}, # Postgres type name
- sql_ref => $_->{COLUMN_NAME}, # SQL to refer to this column
- sql_read => $_->{COLUMN_NAME}, # SQL to read this column (could be used to transform the data to something perl likes)
- sql_write => sub { \$_[0] }, # SQL to convert Perl data into something that can be assigned to the column
- };
- }
- \%tables;
-}
-
-
-sub _entry_type {
- # Store the cached result of _entry_tables() for each entry type
- state $types = {
- map +($_, _entry_tables $entry_prefixes{$_}),
- keys %entry_prefixes
- };
- $types->{ shift() };
-}
-
-
-# Returns everything for a specific entry ID. The top-level hash also includes
-# the following keys:
-#
-# id, chid, rev, maxrev, hidden, locked, entry_hidden, entry_locked
-#
-# (Ordering of arrays is unspecified)
-sub entry {
- my($type, $id, $rev) = @_;
-
- my $prefix = $entry_prefixes{$type}||die;
- my $t = _entry_type $type;
-
- my $maxrev = tuwf->dbVali('SELECT MAX(rev) FROM changes WHERE type =', \$type, ' AND itemid =', \$id);
- return undef if !$maxrev;
- $rev ||= $maxrev;
- my $entry = tuwf->dbRowi(q{
- SELECT itemid AS id, id AS chid, rev AS chrev, ihid AS hidden, ilock AS locked
- FROM changes
- WHERE}, { type => $type, itemid => $id, rev => $rev }
- );
- return undef if !$entry->{id};
- $entry->{maxrev} = $maxrev;
-
- if($maxrev == $rev) {
- $entry->{entry_hidden} = $entry->{hidden};
- $entry->{entry_locked} = $entry->{locked};
- } else {
- enrich id => "SELECT id, hidden AS entry_hidden, locked AS entry_locked FROM $prefix WHERE id IN", $entry;
- }
-
- enrich chid => sql(
- SELECT => sql_comma(chid => map $_->{sql_read}, @{$t->{$prefix}}),
- FROM => "${prefix}_hist",
- 'WHERE chid IN'
- ), $entry;
-
- for my $tbl (grep /^${prefix}_/, keys %$t) {
- (my $name = $tbl) =~ s/^${prefix}_//;
- $entry->{$name} = tuwf->dbAlli(
- SELECT => sql_comma(map $_->{sql_read}, @{$t->{$tbl}}),
- FROM => "${tbl}_hist",
- WHERE => { chid => $entry->{chid} });
- }
- $entry
-}
-
-
-# Update or create an entry, usage:
-# ($id, $chid, $rev) = update_entry $type, $id, $data, $uid;
-#
-# $id should be undef to create a new entry.
-# $uid should be undef to use the currently logged in user.
-# $data should have the same format as returned by entry(), but instead with
-# the following additional keys in the top-level hash:
-#
-# hidden, locked, editsum
-sub update_entry {
- my($type, $id, $data, $uid) = @_;
- $id ||= undef;
-
- my $prefix = $entry_prefixes{$type}||die;
- my $t = _entry_type $type;
-
- tuwf->dbExeci("SELECT edit_${type}_init(", \$id, ', (SELECT MAX(rev) FROM changes WHERE type = ', \$type, ' AND itemid = ', \$id, '))');
- tuwf->dbExeci('UPDATE edit_revision SET', {
- requester => $uid // scalar VNWeb::Auth::auth()->uid(),
- ip => scalar tuwf->reqIP(),
- comments => $data->{editsum},
- ihid => $data->{hidden},
- ilock => $data->{locked},
- });
-
- tuwf->dbExeci("UPDATE edit_${prefix} SET ",
- sql_comma(map sql($_->{sql_ref}, ' = ', $_->{sql_write}->($data->{$_->{name}})), @{$t->{$prefix}}));
-
- for my $tbl (grep /^${prefix}_/, keys %$t) {
- (my $name = $tbl) =~ s/^${prefix}_//;
-
- my @rows = map {
- my $d = $_;
- sql '(', sql_comma(map $_->{sql_write}->($d->{$_->{name}}), @{$t->{$tbl}}), ')'
- } @{$data->{$name}};
-
- tuwf->dbExeci("DELETE FROM edit_${tbl}");
- tuwf->dbExeci("INSERT INTO edit_${tbl} ",
- '(', sql_comma(map $_->{sql_ref}, @{$t->{$tbl}}), ')',
- ' VALUES ', sql_comma(@rows)
- ) if @rows;
- }
-
- my $r = tuwf->dbRow("SELECT * FROM edit_${type}_commit()");
- ($r->{itemid}, $r->{chid}, $r->{rev})
-}
-
-1;
diff --git a/lib/VN3/Docs/Edit.pm b/lib/VN3/Docs/Edit.pm
deleted file mode 100644
index a93be5b2..00000000
--- a/lib/VN3/Docs/Edit.pm
+++ /dev/null
@@ -1,54 +0,0 @@
-package VN3::Docs::Edit;
-
-use VN3::Prelude;
-use VN3::Docs::Lib;
-
-
-my $FORM = {
- title => { maxlength => 200 },
- content => { required => 0, default => '' },
- hidden => { anybool => 1 },
- locked => { anybool => 1 },
-
- editsum => { _when => 'in out', editsum => 1 },
- id => { _when => 'out', id => 1 },
-};
-
-my $FORM_OUT = form_compile out => $FORM;
-my $FORM_IN = form_compile in => $FORM;
-my $FORM_CMP = form_compile cmp => $FORM;
-
-elm_form DocEdit => $FORM_OUT, $FORM_IN;
-
-
-TUWF::get qr{/$DREV_RE/edit} => sub {
- my $d = entry d => tuwf->capture('id'), tuwf->capture('rev') or return tuwf->resNotFound;
- return tuwf->resDenied if !can_edit d => $d;
-
- $d->{editsum} = $d->{chrev} == $d->{maxrev} ? '' : "Reverted to revision d$d->{id}.$d->{chrev}";
-
- Framework title => "Edit $d->{title}", index => 0,
- sub {
- Div class => 'row', sub {
- Div class => 'fixed-size-left-sidebar-md doc-list', \&Sidebar;
- Div class => 'col-md col-md--4', sub {
- Div 'data-elm-module' => 'DocEdit',
- 'data-elm-flags' => JSON::XS->new->encode($FORM_OUT->analyze->coerce_for_json($d)), '';
- };
- };
- };
-};
-
-
-json_api qr{/$DOC_RE/edit}, $FORM_IN, sub {
- my $data = shift;
- my $doc = entry d => tuwf->capture('id') or return tuwf->resNotFound;
-
- return $elm_Unauth->() if !can_edit d => $doc;
- return $elm_Unchanged->() if !form_changed $FORM_CMP, $data, $doc;
-
- my($id,undef,$rev) = update_entry d => $doc->{id}, $data;
- $elm_Changed->($id, $rev);
-};
-
-1;
diff --git a/lib/VN3/Docs/JS.pm b/lib/VN3/Docs/JS.pm
deleted file mode 100644
index 397842fd..00000000
--- a/lib/VN3/Docs/JS.pm
+++ /dev/null
@@ -1,15 +0,0 @@
-package Docs::JS;
-
-use VN3::Prelude;
-use VN3::Docs::Lib;
-
-my $elm_Content = elm_api Content => {};
-
-json_api '/js/markdown.json', {
- content => { required => 0, default => '' }
-}, sub {
- return $elm_Unauth->() if !auth->permDbmod;
- $elm_Content->(md2html shift->{content});
-};
-
-1;
diff --git a/lib/VN3/Docs/Lib.pm b/lib/VN3/Docs/Lib.pm
deleted file mode 100644
index e9239499..00000000
--- a/lib/VN3/Docs/Lib.pm
+++ /dev/null
@@ -1,86 +0,0 @@
-package VN3::Docs::Lib;
-
-use VN3::Prelude;
-use Text::MultiMarkdown 'markdown';
-
-our @EXPORT = qw/md2html Sidebar/;
-
-
-sub md2html {
- my $content = shift;
-
- $content =~ s{^:MODERATORS:$}{
- my %modperms = map auth->listPerms->{$_} & auth->defaultPerms ? () : ($_, auth->listPerms->{$_}), keys %{ auth->listPerms };
- my $l = tuwf->dbAlli('SELECT id, username, perm FROM users WHERE (perm & ', \(auth->allPerms &~ auth->defaultPerms), ') > 0 ORDER BY id LIMIT 100');
- '<dl>'.join('', map {
- my $u = $_;
- my $p = $u->{perm} >= auth->allPerms ? 'admin'
- : join ', ', sort grep $u->{perm} & $modperms{$_}, keys %modperms;
- sprintf '<dt><a href="/u%d">%s</a></dt><dd>%s</dd>', $_->{id}, $_->{username}, $p;
- } @$l).'</dl>';
- }me;
-
- my $html = markdown $content, {
- strip_metadata => 1,
- img_ids => 0,
- disable_footnotes => 1,
- disable_bibliography => 1,
- };
-
- # Number sections and turn them into links
- my($sec, $subsec) = (0,0);
- $html =~ s{<h([1-2])[^>]+>(.*?)</h\1>}{
- if($1 == 1) {
- $sec++;
- $subsec = 0;
- qq{<h2><a href="#$sec" name="$sec">$sec. $2</a></h2>}
- } elsif($1 == 2) {
- $subsec++;
- qq|<h3><a href="#$sec.$subsec" name="$sec.$subsec">$sec.$subsec. $2</a></h3>\n|
- }
- }ge;
-
- # Text::MultiMarkdown doesn't handle fenced code blocks properly. The
- # following solution breaks inline code blocks, but I don't use those anyway.
- $html =~ s/<code>/<pre>/g;
- $html =~ s#</code>#</pre>#g;
-
- $html
-}
-
-
-sub Cat {
- Div class => 'doc-list__title', $_[0];
-}
-
-sub Doc {
- A mkclass('doc-list__doc' => 1, 'doc-list__doc--active' => tuwf->capture('id') == $_[0]),
- href => "/d$_[0]", $_[1];
-}
-
-
-sub Sidebar {
- # TODO: Turn this into a nav-sidebar for better mobile viewing?
- Cat 'About VNDB';
- Doc 7, 'About us';
- Doc 6, 'FAQ';
- Doc 9, 'Discussion board';
- Doc 17, 'Privacy Policy & Licensing';
- Doc 11, 'Database API';
- Doc 14, 'Database Dumps';
- Doc 18, 'Database Querying';
- Doc 8, 'Development';
-
- Cat 'Guidelines';
- Doc 5, 'Editing guidelines';
- Doc 2, 'Visual novels';
- Doc 15, 'Special games';
- Doc 3, 'Releases';
- Doc 4, 'Producers';
- Doc 16, 'Staff';
- Doc 12, 'Characters';
- Doc 10, 'Tags & Traits';
- Doc 13, 'Capturing screenshots';
-}
-
-1;
diff --git a/lib/VN3/Docs/Page.pm b/lib/VN3/Docs/Page.pm
deleted file mode 100644
index 0392434b..00000000
--- a/lib/VN3/Docs/Page.pm
+++ /dev/null
@@ -1,23 +0,0 @@
-package VN3::Docs::Page;
-
-use VN3::Prelude;
-use VN3::Docs::Lib;
-
-TUWF::get qr{/$DREV_RE} => sub {
- my $d = entry d => tuwf->capture('id'), tuwf->capture('rev');
- return tuwf->resNotFound if !$d || $d->{hidden};
-
- Framework title => $d->{title},
- sub {
- Div class => 'row', sub {
- Div class => 'fixed-size-left-sidebar-md doc-list', \&Sidebar;
- Div class => 'col-md doc', sub {
- EntryEdit d => $d;
- H1 $d->{title};
- Lit md2html $d->{content};
- };
- };
- };
-};
-
-1;
diff --git a/lib/VN3/ElmGen.pm b/lib/VN3/ElmGen.pm
deleted file mode 100644
index fefc154e..00000000
--- a/lib/VN3/ElmGen.pm
+++ /dev/null
@@ -1,197 +0,0 @@
-# This module is responsible for generating elm3/Lib/Gen.elm. Variables and
-# type definitions can be added from any Perl module by calling def(),
-# elm_form() and elm_api() at file load time.
-
-package VN3::ElmGen;
-
-use strict;
-use warnings;
-use TUWF;
-use Exporter 'import';
-use List::Util 'max';
-use VNWeb::Auth;
-use VN3::Types;
-use VNDB::Types;
-
-our @EXPORT = qw/
- elm_form elm_api
- $elm_Unauth $elm_Unchanged $elm_Changed $elm_Success $elm_CSRF
-/;
-
-
-my $data = <<_;
--- This file is automatically generated from lib/VN3/ElmGen.pm
--- DO NOT EDIT!
-module Lib.Gen exposing (..)
-
-import Http
-import Json.Encode as JE
-import Json.Decode as JD
-
-type alias Medium =
- { qty : Bool
- , single : String
- , plural : String
- }
-_
-
-
-
-# Formatting functions
-sub indent($) { $_[0] =~ s/\n/\n /gr }
-sub list { indent "\n[ ".join("\n, ", @_)."\n]" }
-sub string($) { '"'.($_[0] =~ s/([\\"])/\\$1/gr).'"' }
-sub tuple { '('.join(', ', @_).')' }
-sub bool($) { $_[0] ? 'True' : 'False' }
-sub to_camel { (ucfirst $_[0]) =~ s/_([a-z])/'_'.uc $1/egr; }
-
-# Output a variable definition: name, type, value
-sub def($$$) { $data .= sprintf "\n%s : %s\n%1\$s = %s\n", @_; }
-
-
-# Define an Elm type corresponding to a TUWF::Validate schema
-sub def_type {
- my($name, $obj) = @_;
- my @keys = $obj->{keys} ? grep $obj->{keys}{$_}{keys}||($obj->{keys}{$_}{values}&&$obj->{keys}{$_}{values}{keys}), sort keys %{$obj->{keys}} : ();
-
- def_type($name . to_camel($_), $obj->{keys}{$_}{values} || $obj->{keys}{$_}) for @keys;
-
- $data .= sprintf "\ntype alias %s = %s\n\n", $name, $obj->elm_type(
- keys => +{ map +($_, ($obj->{keys}{$_}{values} ? 'List ' : '') . $name . to_camel($_)), @keys }
- );
-}
-
-
-# Define an Elm JSON encoder taking a corresponding def_type() as input
-sub encoder {
- my($name, $type, $obj) = @_;
- def $name, "$type -> JE.Value", $obj->elm_encoder(json_encode => 'JE.');
-}
-
-
-# Create type definitions and a JSON encoder for a typical form.
-# Usage:
-#
-# elm_form 'FormName', $TO_ELM_SCHEMA, $TO_SERVER_SCHEMA;
-#
-# That will define:
-#
-# type alias FormName = { .. }
-# type alias FormNameSend = { .. }
-# formnameSendEncode : FormNameSend -> JE.Value
-#
-sub elm_form {
- my($name, $out, $in) = @_;
- def_type $name, $out->analyze;
- def_type $name.'Send', $in->analyze;
- encoder lc($name).'SendEncode', $name.'Send', $in->analyze;
-}
-
-
-my %apis;
-
-# Define an API response. This will be added to the 'Lib.Api.Response' union type.
-# Usage:
-#
-# # At file scope:
-# my $json_generator = elm_api_response UnionName => $SCHEMA1, $SCHEMA2, ..;
-#
-# # Later, to actually generate a JSON response:
-# $json_generator->($data1, $data2, ..);
-#
-# Limitation: There may be only a single $SCHEMA with an embedded {type => 'hash'}.
-sub elm_api {
- my($name, @schema) = @_;
- @schema = map tuwf->compile($_), @schema;
- $apis{$name} = \@schema;
- sub {
- # TODO: Validate $data? Easier to catch bugs that way
- tuwf->resJSON({$name, @schema ? [map $schema[$_]->analyze->coerce_for_json($_[$_], unknown => 'reject'), 0..$#schema] : 1})
- }
-}
-
-# Common API responses.
-our $elm_Unauth = elm_api 'Unauth';
-our $elm_Unchanged = elm_api 'Unchanged';
-our $elm_Changed = elm_api 'Changed', { id => 1 }, { uint => 1 };
-our $elm_Success = elm_api 'Success';
-our $elm_CSRF = elm_api 'CSRF';
-
-
-sub print {
- # Generate the ApiResponse type and decoder.
- #
- # Extract all { type => 'hash' } schemas and give them their own
- # definition, so that it's easy to refer to those records in other places
- # of the Elm code, similar to def_type().
- my(@union, @decode);
- my $len = max map length, keys %apis;
- for (sort keys %apis) {
- my($name, $schema) = ($_, $apis{$_});
- my $def = $name;
- my $dec = sprintf 'JD.field "%s"%s <| %s', $name,
- ' 'x($len-(length $name)),
- @$schema == 0 ? "JD.succeed $name" :
- @$schema == 1 ? "JD.map $name" : sprintf 'JD.map%d %s', scalar @$schema, $name;
- my $tname = "Api$name";
- for my $argn (0..$#$schema) {
- my $arg = $schema->[$argn]->analyze();
- my $jd = $arg->elm_decoder(json_decode => 'JD.', level => 3);
- $dec .= " (JD.index $argn $jd)";
- if($arg->{keys}) {
- def_type $tname, $arg;
- $def .= " $tname";
- #$dec .= $jd;
- } elsif($arg->{values} && $arg->{values}{keys}) {
- def_type $tname, $arg->{values};
- $def .= " (List $tname)";
- #$dec .= "(JD.list $jd)";
- } else {
- $def .= ' '.$arg->elm_type();
- #$dec .= $jd;
- }
- #$dec .= ')';
- }
- push @union, $def;
- push @decode, $dec;
- }
- $data .= sprintf "\ntype ApiResponse\n = HTTPError Http.Error\n | %s\n", join "\n | ", @union;
- $data .= sprintf "\ndecodeApiResponse : JD.Decoder ApiResponse\ndecodeApiResponse = JD.oneOf\n [ %s\n ]", join "\n , ", @decode;
-
- print $data;
-};
-
-
-my $perms = VNWeb::Auth::listPerms();
-
-def urlStatic => String => string tuwf->conf->{url_static};
-def userPerms => 'List (Int, String)' => list map tuple($perms->{$_}, string $_), sort keys %$perms;
-def vnLengths => 'List (Int, String)' => list map tuple($_, string vn_length_display $_), keys %VN_LENGTH;
-def vnRelations => 'List (String, String)' => list map tuple(string $_, string vn_relation_display $_), keys %VN_RELATION;
-def producerRelations => 'List (String, String)' => list map tuple(string $_, string producer_relation_display $_), keys %PRODUCER_RELATION;
-def creditType => 'List (String, String)' => list map tuple(string $_, string $CREDIT_TYPE{$_}), keys %CREDIT_TYPE;
-def languages => 'List (String, String)' => list map tuple(string $_, string $LANGUAGE{$_}), sort { $LANGUAGE{$a} cmp $LANGUAGE{$b} } keys %LANGUAGE;
-def platforms => 'List (String, String)' => list map tuple(string $_, string $PLATFORM{$_}), keys %PLATFORM;
-def releaseTypes => 'List String' => list map string($_), release_types;
-def producerTypes => 'List (String, String)' => list map tuple(string $_, string $PRODUCER_TYPE{$_}), keys %PRODUCER_TYPE;
-def minAges => 'List (Int, String)' => list map tuple($_, string minage_display_full $_), keys %AGE_RATING;
-def resolutions => 'List (String, String)' => list map tuple(string $_, string resolution_display_full $_), keys %RESOLUTION;
-def voiced => 'List (Int, String)' => list map tuple($_, string($VOICED{$_})), keys %VOICED;
-def animated => 'List (Int, String)' => list map tuple($_, string($ANIMATED{$_})), keys %ANIMATED;
-def genders => 'List (String, String)' => list map tuple(string $_, string gender_display $_), keys %GENDER;
-def bloodTypes => 'List (String, String)' => list map tuple(string $_, string blood_type_display $_), keys %BLOOD_TYPE;
-def charRoles => 'List (String, String)' => list map tuple(string $_, string char_role_display $_), keys %CHAR_ROLE;
-def vnlistStatus => 'List (Int, String)' => list map tuple($_, string $VNLIST_STATUS{$_}), keys %VNLIST_STATUS;
-
-def emailPattern => String => string { tuwf->compile({ email => 1 })->analyze->html5_validation() }->{pattern};
-def weburlPattern => String => string { tuwf->compile({ weburl => 1 })->analyze->html5_validation() }->{pattern};
-def vnvotePattern => String => string { tuwf->compile({ vnvote => 1 })->analyze->html5_validation() }->{pattern};
-
-def media => 'List (String, Medium)' =>
- list map tuple(
- string($_),
- sprintf('{ qty = %s, single = %s, plural = %s }', bool($MEDIUM{$_}{qty}), string($MEDIUM{$_}{txt}), string($MEDIUM{$_}{plural}))
- ), keys %MEDIUM;
-
-
-1;
diff --git a/lib/VN3/HTML.pm b/lib/VN3/HTML.pm
deleted file mode 100644
index 0dcd7241..00000000
--- a/lib/VN3/HTML.pm
+++ /dev/null
@@ -1,375 +0,0 @@
-# Convention:
-# All HTML-generating functions are in CamelCase
-#
-# TODO: HTML generation for dropdowns can be abstracted more nicely.
-
-package VN3::HTML;
-
-use strict;
-use warnings;
-use v5.10;
-use utf8;
-use List::Util 'pairs', 'max', 'sum';
-use TUWF ':Html5', 'mkclass', 'uri_escape';
-use VNWeb::Auth;
-use VN3::Types;
-use VN3::Validation;
-use base 'Exporter';
-
-our @EXPORT = qw/Framework EntryEdit Switch Debug Join FullPageForm VoteGraph ListIcon GridIcon/;
-
-
-sub Navbar {
- Div class => 'nav navbar__nav navbar__main-nav', sub {
- Div class => 'nav__item navbar__menu dropdown', sub {
- A href => 'javascript:;', class => 'nav__link dropdown__toggle', sub { Txt 'Database '; Span class => 'caret', '' };
- Div class => 'dropdown-menu database-menu', sub {
- A class => 'dropdown-menu__item', href => '/v/all', 'Visual novels';
- A class => 'dropdown-menu__item', href => '/g', 'Tags';
- A class => 'dropdown-menu__item', href => '/c/all', 'Characters';
- A class => 'dropdown-menu__item', href => '/i', 'Traits';
- A class => 'dropdown-menu__item', href => '/p/all', 'Producers';
- A class => 'dropdown-menu__item', href => '/s/all', 'Staff';
- A class => 'dropdown-menu__item', href => '/r', 'Releases';
- };
- };
- Div class => 'nav__item navbar__menu', sub { A class => 'nav__link', href => '/d6', 'FAQ' };
- Div class => 'nav__item navbar__menu', sub { A class => 'nav__link', href => '/t', 'Forums' };
- Div class => 'nav__item navbar__menu dropdown', sub {
- A href => 'javascript:;', class => 'nav__link dropdown__toggle', sub { Txt 'Contribute '; Span class => 'caret', '' };
- Div class => 'dropdown-menu', sub {
- A class => 'dropdown-menu__item', href => '/hist', 'Recent changes';
- A class => 'dropdown-menu__item', href => '/v/add', 'Add Visual Novel';
- A class => 'dropdown-menu__item', href => '/p/add', 'Add Producer';
- A class => 'dropdown-menu__item', href => '/s/new', 'Add Staff';
- };
- };
- Div class => 'nav__item navbar__menu', sub {
- A href => '/v/all', class => 'nav__link', sub {
- Span class => 'icon-desc d-md-none', 'Search ';
- Img class => 'svg-icon', src => tuwf->conf->{url_static}.'/v3/heavy/search.svg';
- };
- };
- };
-
- Div class => 'nav navbar__nav', sub {
- my $notifies = auth && tuwf->dbVali('SELECT count(*) FROM notifications WHERE uid =', \auth->uid, 'AND read IS NULL');
- Div class => 'nav__item navbar__menu', sub {
- A href => '/'.auth->uid.'/notifies', class => 'nav__link notification-icon', sub {
- Span class => 'icon-desc d-md-none', 'Notifications ';
- Div class => 'icon-group', sub {
- Img class => 'svg-icon', src => tuwf->conf->{url_static}.'/v3/bell.svg';
- Div class => 'notification-icon__indicator', $notifies;
- };
- };
- } if $notifies;
- Div class => 'nav__item navbar__menu dropdown', sub {
- A href => 'javascript:;', class => 'nav__link dropdown__toggle', sub { Txt auth->username.' '; Span class => 'caret'; };
- Div class => 'dropdown-menu dropdown-menu--right', sub {
- my $id = auth->uid;
- A class => 'dropdown-menu__item', href => "/u$id", 'Profile';
- A class => 'dropdown-menu__item', href => "/u$id/edit", 'Settings';
- A class => 'dropdown-menu__item', href => "/u$id/list", 'List';
- A class => 'dropdown-menu__item', href => "/u$id/wish", 'Wishlist';
- A class => 'dropdown-menu__item', href => "/u$id/hist", 'Recent changes';
- A class => 'dropdown-menu__item', href => "/g/links?u=$id", 'Tags';
- Div class => 'dropdown__separator', '';
- A class => 'dropdown-menu__item', href => "/u$id/logout", 'Log out';
- };
- } if auth;
- if(!auth) {
- Div class => 'nav__item navbar__menu', sub { A class => 'nav__link', href => '/u/register', 'Register'; };
- Div class => 'nav__item navbar__menu', sub { A class => 'nav__link', href => '/u/login', 'Login'; };
- }
- };
-}
-
-
-sub Top {
- my($opt) = @_;
- Div class => 'raised-top-container', sub {
- Div class => 'raised-top', sub {
- Div class => 'container', sub {
- Div class => 'navbar navbar--expand-md', sub {
- Div class => 'navbar__logo', sub {
- A href => '/', 'vndb';
- };
- A href => 'javascript:;', class => 'navbar__toggler', sub {
- Div class => 'navbar__toggler-icon', '';
- };
- Div class => 'navbar__collapse', \&Navbar;
- };
- Div class => 'row', $opt->{top} if $opt->{top};
- };
- };
- };
-}
-
-
-sub Bottom {
- Div class => 'col-md col-md--1', sub {
- Div class => 'footer__logo', sub {
- A href => '/', class => 'link-subtle', 'vndb';
- };
- };
-
- state $sep = sub { Span class => 'footer__sep', sub { Lit '&middot;'; }; };
- state $lnk = sub { A href => $_[0], class => 'link--subtle', $_[1]; };
- state $root = tuwf->root;
- state $ver = `git -C "$root" describe` =~ /^(.+)$/ ? $1 : '';
-
- Div class => 'col-md col-md--4', sub {
- Div class => 'footer__nav', sub {
- $lnk->('/d7', 'about us');
- $sep->();
- $lnk->('irc://irc.synirc.net/vndb', '#vndb');
- $sep->();
- $lnk->('mailto:contact@vndb.org', 'contact@vndb.org');
- $sep->();
- $lnk->('https://code.blicky.net/yorhel/vndb/src/branch/v3', 'source');
- $sep->();
- A href => '/v/rand', class => 'link--subtle footer__random', sub {
- Txt 'random vn ';
- Img class => 'svg-icon', src => tuwf->conf->{url_static}.'/v3/heavy/random.svg';
- };
- $sep->();
- Txt $ver;
- };
-
- my $q = tuwf->dbRow('SELECT vid, quote FROM quotes ORDER BY random() LIMIT 1');
- Div class => 'footer__quote', sub {
- $lnk->('/v'.$q->{vid}, $q->{quote});
- } if $q;
- };
-}
-
-
-sub Framework {
- my $body = pop;
- my %opt = @_;
- Html sub {
- Head prefix => 'og: http://ogp.me/ns#', sub {
- Meta name => 'viewport', content => 'width=device-width, initial-scale=1, shrink-to-fit=no';
- Meta name => 'csrf-token', content => auth->csrftoken;
- Meta charset => 'utf-8';
- Meta name => 'robots', content => 'noindex, follow' if exists $opt{index} && !$opt{index};
- Title $opt{title} . ' | vndb';
- Link rel => 'stylesheet', href => tuwf->conf->{url_static}.'/v3/style.css';
- Link rel => 'shortcut icon', href => '/favicon.ico', type => 'image/x-icon';
- Link rel => 'search', type => 'application/opensearchdescription+xml', title => 'VNDB VN Search', href => tuwf->reqBaseURI().'/opensearch.xml';
-
- # TODO: Link to RSS feeds.
-
- # Opengraph metadata
- if($opt{og}) {
- $opt{og}{site_name} ||= 'The Visual Novel Database';
- $opt{og}{type} ||= 'object';
- $opt{og}{image} ||= 'https://s.vndb.org/s/angel/bg.jpg'; # TODO: Something better
- $opt{og}{url} ||= tuwf->reqURI;
- $opt{og}{title} ||= $opt{title};
- Meta property => "og:$_", content => ($opt{og}{$_} =~ s/\n/ /gr) for sort keys %{$opt{og}};
- }
- };
- Body sub {
- Div class => 'top-bar', id => 'top', '';
- Top \%opt;
- Div class => 'page-container', sub {
- Div mkclass(
- container => 1,
- 'main-container' => 1,
- 'container--narrow' => $opt{narrow},
- 'flex-center-container' => $opt{center},
- 'main-container--single-col' => $opt{single_col},
- $opt{main_classes} ? %{$opt{main_classes}} :()
- ), $body;
- Div class => 'container', sub {
- Div class => 'footer', sub {
- Div class => 'row', \&Bottom;
- };
- };
- };
- Script type => 'text/javascript', src => tuwf->conf->{url_static}.'/v3/elm.js', '';
- Script type => 'text/javascript', src => tuwf->conf->{url_static}.'/v3/vndb.js', '';
- #Script type => 'text/javascript', src => tuwf->conf->{url_static}.'/v3/min.js', '';
- };
- };
- if(tuwf->debug) {
- tuwf->dbCommit; # Hack to measure the commit time
-
- my $sql = uri_escape join "\n", map {
- my($sql, $params, $time) = @$_;
- sprintf " [%6.2fms] %s | %s", $time*1000, $sql,
- join ', ', map "$_:".DBI::neat($params->{$_}),
- sort { $a =~ /^[0-9]+$/ && $b =~ /^[0-9]+$/ ? $a <=> $b : $a cmp $b }
- keys %$params;
- } @{ tuwf->{_TUWF}{DB}{queries} };
- A href => 'data:text/plain,'.$sql, 'SQL';
-
- my $modules = uri_escape join "\n", sort keys %INC;
- A href => 'data:text/plain,'.$modules, 'Modules';
- }
-}
-
-
-sub EntryEdit {
- my($type, $e) = @_;
-
- return if $type eq 'u' && !auth->permUsermod;
-
- Div class => 'dropdown pull-right', sub {
- A href => 'javascript:;', class => 'btn d-block dropdown__toggle', sub {
- Div class => 'opacity-muted', sub {
- Img class => 'svg-icon', src => tuwf->conf->{url_static}.'/v3/edit.svg';
- Span class => 'caret', '';
- };
- };
- Div class => 'dropdown-menu dropdown-menu--right database-menu', sub {
- A class => 'dropdown-menu__item', href => "/$type$e->{id}", 'Details';
- A class => 'dropdown-menu__item', href => "/$type$e->{id}/hist", 'History' if $type ne 'u';
- A class => 'dropdown-menu__item', href => "/$type$e->{id}/edit", 'Edit' if can_edit $type, $e;
- A class => 'dropdown-menu__item', href => "/$type$e->{id}/add", 'Add release' if $type eq 'v' && can_edit $type, $e;
- A class => 'dropdown-menu__item', href => "/$type$e->{id}/addchar",'Add character' if $type eq 'v' && can_edit $type, $e;
- A class => 'dropdown-menu__item', href => "/$type$e->{id}/copy", 'Copy' if $type =~ /[cr]/ && can_edit $type, $e;
- };
- }
-}
-
-
-sub Switch {
- my $label = shift;
- my $on = shift;
- my @class = mkclass
- 'switch' => 1,
- 'switch--on' => $on,
- @_;
-
- A @class, href => 'javascript:;', sub {
- Div class => 'switch__label', $label;
- Div class => 'switch__toggle', '';
- };
-}
-
-
-# Throw any data structure on the page for inspection.
-sub Debug {
- return if !tuwf->debug;
- require JSON::XS;
- # This provides a nice JSON browser in FF, not sure how other browsers render it.
- my $data = uri_escape(JSON::XS->new->canonical->encode($_[0]));
- A style => 'margin: 0 5px', title => 'Debug', href => 'data:application/json,'.$data, ' ⚙ ';
-}
-
-
-# Similar to join($sep, map $item->($_), @list), but works for HTML generation functions.
-# Join ', ', sub { A href => '#', $_[0] }, @list;
-# Join \&Br, \&Txt, @list;
-sub Join {
- my($sep, $item, @list) = @_;
- for my $i (0..$#list) {
- ref $sep ? $sep->() : Txt $sep if $i > 0;
- $item->($list[$i]);
- }
-}
-
-
-# Full-page form, optionally with sections. Options:
-#
-# module => '', # Elm module to load
-# data => $form_data,
-# schema => $tuwf_validate_schema, # Optional TUWF::Validate schema to use to encode the data
-# sections => [ # Optional list of sections
-# anchor1 => 'Section 1',
-# ..
-# ]
-#
-# If no sections are given, the parent Framework() should have narrow => 1.
-sub FullPageForm {
- my %o = @_;
-
- my $form = sub { Div
- 'data-elm-module' => $o{module},
- 'data-elm-flags' => JSON::XS->new->encode($o{schema} ? $o{schema}->analyze->coerce_for_json($o{data}) : $o{data}),
- ''
- };
-
- Div class => 'row', $o{sections} ? sub {
-
- Div class => 'col-md col-md--1', sub {
- Div class => 'nav-sidebar nav-sidebar--expand-md', sub {
- A href => 'javascript:;', class => 'nav-sidebar__selection', sub {
- Txt $o{sections}[1];
- Div class => 'caret', '';
- };
- Div class => 'nav nav--vertical', sub {
- my $x = 0;
- for my $s (pairs @{$o{sections}}) {
- Div mkclass(nav__item => 1, 'nav__item--active' => !$x++), sub {
- A class => 'nav__link', href => '#'.$s->key, $s->value;
- }
- }
- };
- }
- };
- Div class => 'col-md col-md--4', $form;
- } : sub {
- Div class => 'col-md col-md--1', $form;
- };
-}
-
-
-sub VoteGraph {
- my($type, $id) = @_;
-
- my %histogram = map +($_->{vote}, $_), @{ tuwf->dbAlli(q{
- SELECT (vote::numeric/10)::int AS vote, COUNT(vote) as votes, SUM(vote) AS total
- FROM votes},
- $type eq 'v' ? (q{
- JOIN users ON id = uid AND NOT ign_votes
- WHERE vid =}, \$id
- ) : (q{
- WHERE uid =}, \$id
- ), q{
- GROUP BY (vote::numeric/10)::int
- })};
-
- my $max = max map $_->{votes}, values %histogram;
- my $count = sum map $_->{votes}, values %histogram;
- my $sum = sum map $_->{total}, values %histogram;
-
- my $Graph = sub {
- Div class => 'vote-graph', sub {
- Div class => 'vote-graph__scores', sub {
- Div class => 'vote-graph__score', $_ for (reverse 1..10);
- };
- Div class => 'vote-graph__bars', sub {
- Div class => 'vote-graph__bar', style => sprintf('width: %.2f%%', ($histogram{$_}{votes}||0)/$max*100), sub {
- Div class => 'vote-graph__bar-label', $histogram{$_}{votes}||'1';
- } for (reverse 1..10);
- };
- };
- Div class => 'final-text',
- sprintf '%d vote%s total, average %.2f%s',
- $count, $count == 1 ? '' : 's', $sum/$count/10,
- $type eq 'v' ? ' ('.vote_string($sum/$count).')' : '';
- };
- return ($count, $Graph);
-}
-
-sub ListIcon {
- Lit q{<svg class="svg-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" version="1">}
- .q{<g fill="currentColor" fill-rule="nonzero">}
- .q{<path d="M0 2h14v2H0zM0 6h14v2H0zM0 10h14v2H0z"/>}
- .q{</g>}
- .q{</svg>};
-}
-
-
-sub GridIcon {
- Lit q{<svg class="svg-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" version="1">}
- .q{<g fill="currentColor" fill-rule="nonzero">}
- .q{<path d="M0 0h3v3H0zM0 5h3v3H0zM0 10h3v3H0zM5 0h3v3H5zM5 5h3v3H5zM5 10h3v3H5zM10 0h3v3h-3zM10 5h3v3h-3zM10 10h3v3h-3z"/>}
- .q{</g>}
- .q{</svg>};
-}
-
-1;
diff --git a/lib/VN3/Misc/Homepage.pm b/lib/VN3/Misc/Homepage.pm
deleted file mode 100644
index b9939b07..00000000
--- a/lib/VN3/Misc/Homepage.pm
+++ /dev/null
@@ -1,31 +0,0 @@
-package VN3::User::Login;
-
-use VN3::Prelude;
-
-
-TUWF::get '/' => sub {
- Framework title => 'VNDB', sub {
- H1 'Hello, World!';
- P sub {
- Txt 'This is the place where version 3 of ';
- A href => 'https://vndb.org/', 'VNDB.org';
- Txt ' is being developed. Some random notes:';
- Ul sub {
- Li 'This test site interfaces directly with the same database as the main site, which makes it easier to test all the functionality and find odd test cases.';
- Li 'This test site is very incomplete, don\'t be surprised to see 404\'s or other things that don\'t work.';
- Li 'This is a long-term project, don\'t expect this new design to replace the main site anytime soon.';
- Li sub {
- Txt 'Feedback/comments/ideas or want to help out? Post in ';
- A href => 'https://code.blicky.net/yorhel/vndb/issues/2', 'this issue';
- Txt ' or create a new one.';
- };
- Li sub {
- Txt 'You can follow development activity on the ';
- A href => 'https://code.blicky.net/yorhel/vndb/src/branch/v3', 'git repo.';
- };
- };
- };
- };
-};
-
-1;
diff --git a/lib/VN3/Misc/ImageUpload.pm b/lib/VN3/Misc/ImageUpload.pm
deleted file mode 100644
index 76a07975..00000000
--- a/lib/VN3/Misc/ImageUpload.pm
+++ /dev/null
@@ -1,70 +0,0 @@
-package VN3::Misc::ImageUpload;
-
-use VN3::Prelude;
-use Image::Magick;
-
-
-sub save_img {
- my($im, $dir, $id, $ow, $oh, $pw, $ph) = @_;
-
- if($pw) {
- my($nw, $nh) = imgsize($ow, $oh, $pw, $ph);
- if($ow != $nw || $oh != $nh) {
- $im->GaussianBlur(geometry => '0.5x0.5');
- $im->Resize(width => $nw, height => $nh);
- $im->UnsharpMask(radius => 0, sigma => 0.75, amount => 0.75, threshold => 0.008);
- }
- }
-
- my $fn = tuwf->imgpath($dir, $id);
- $im->Write($fn);
- chmod 0666, $fn;
-}
-
-my $elm_ImgFormat = elm_api 'ImgFormat';
-my $elm_Image = elm_api 'Image', {id=>1}, {uint=>1}, {uint=>1}; # id, width, height
-
-
-TUWF::post '/js/imageupload.json', sub {
- if(!auth->csrfcheck(tuwf->reqHeader('X-CSRF-Token')||'')) {
- warn "Invalid CSRF token in request";
- return $elm_CSRF->();
- }
- return $elm_Unauth->() if !auth->permEdit;
-
- my $type = tuwf->validate(post => type => { enum => [qw/cv ch sf/] })->data;
- my $imgdata = tuwf->reqUploadRaw('img');
- return $elm_ImgFormat->() if $imgdata !~ /^(\xff\xd8|\x89\x50)/; # JPG or PNG header
-
- my $im = Image::Magick->new;
- $im->BlobToImage($imgdata);
- $im->Set(magick => 'JPEG');
- $im->Set(background => '#ffffff');
- $im->Set(alpha => 'Remove');
- $im->Set(quality => 90);
- my($ow, $oh) = ($im->Get('width'), $im->Get('height'));
- my $id;
-
-
- # VN cover image
- if($type eq 'cv') {
- $id = tuwf->dbVali("SELECT nextval('covers_seq')");
- save_img $im, cv => $id, $ow, $oh, 256, 400;
-
- # Screenshot
- } elsif($type eq 'sf') {
- $id = tuwf->dbVali('INSERT INTO screenshots', { width => $ow, height => $oh }, 'RETURNING id');
- save_img $im, sf => $id;
- save_img $im, st => $id, $ow, $oh, 136, 102;
-
- # Character image
- } elsif($type eq 'ch') {
- $id = tuwf->dbVali("SELECT nextval('charimg_seq')");
- save_img $im, ch => $id, $ow, $oh, 256, 300;
- }
-
- $elm_Image->($id, $ow, $oh);
-};
-
-
-1;
diff --git a/lib/VN3/Prelude.pm b/lib/VN3/Prelude.pm
deleted file mode 100644
index a10a66ac..00000000
--- a/lib/VN3/Prelude.pm
+++ /dev/null
@@ -1,104 +0,0 @@
-# Importing this module is equivalent to:
-#
-# use strict;
-# use warnings;
-# use v5.10;
-# use utf8;
-#
-# use TUWF ':Html5', 'mkclass';
-# use Exporter 'import';
-# use Time::HiRes 'time';
-#
-# use VNDBUtil;
-# use VNDB::Types;
-# use VNWeb::Auth;
-# use VN3::HTML;
-# use VN3::DB;
-# use VN3::Types;
-# use VN3::Validation;
-# use VN3::BBCode;
-# use VN3::ElmGen;
-#
-# WARNING: This should not be used from the above modules.
-#
-# This module also exports a few utility functions for writing URI handlers.
-package VN3::Prelude;
-
-use strict;
-use warnings;
-use utf8;
-use feature ':5.10';
-use TUWF;
-use VNWeb::Auth;
-use VN3::ElmGen;
-
-sub import {
- my $c = caller;
-
- strict->import;
- warnings->import;
- feature->import(':5.10');
- utf8->import;
-
- die $@ if !eval <<" EOM;";
- package $c;
-
- use TUWF ':Html5', 'mkclass';
- use Exporter 'import';
- use Time::HiRes 'time';
-
- use VNDBUtil;
- use VNDB::Types;
- use VNWeb::Auth;
- use VN3::HTML;
- use VN3::DB;
- use VN3::Types;
- use VN3::Validation;
- use VN3::BBCode;
- use VN3::ElmGen;
- 1;
- EOM;
-
- no strict 'refs';
- *{$c.'::json_api'} = \&json_api;
-}
-
-
-
-# Easy wrapper to create a simple API that accepts JSON data on POST requests.
-# The CSRF token and the input data are validated before the subroutine is
-# called.
-#
-# Usage:
-#
-# json_api '/some/url', {
-# username => { maxlength => 10 },
-# }, sub {
-# my $validated_data = shift;
-# };
-my $elm_Invalid = elm_api 'Invalid', {};
-sub json_api {
- my($path, $keys, $sub) = @_;
-
- my $schema = ref $keys eq 'HASH' ? tuwf->compile({ type => 'hash', keys => $keys }) : $keys;
-
- TUWF::post $path => sub {
- if(!auth->csrfcheck(tuwf->reqHeader('X-CSRF-Token')||'')) {
- warn "Invalid CSRF token in request\n";
- $elm_CSRF->();
- return;
- }
-
- my $data = tuwf->validate(json => $schema);
- if(!$data) {
- warn "JSON validation failed\ninput: " . JSON::XS->new->allow_nonref->pretty->canonical->encode(tuwf->reqJSON) . "\nerror: " . JSON::XS->new->encode($data->err) . "\n";
- $elm_Invalid->($data->err);
- return;
- }
-
- $sub->($data->data);
- warn "Non-JSON response to a json_api request, is this intended?\n" if tuwf->resHeader('Content-Type') !~ /^application\/json/;
- };
-}
-
-1;
diff --git a/lib/VN3/Producer/Edit.pm b/lib/VN3/Producer/Edit.pm
deleted file mode 100644
index 3643d771..00000000
--- a/lib/VN3/Producer/Edit.pm
+++ /dev/null
@@ -1,136 +0,0 @@
-package VN3::Producer::Edit;
-
-use VN3::Prelude;
-
-
-my $FORM = {
- alias => { required => 0, default => '', maxlength => 500 },
- desc => { required => 0, default => '', maxlength => 5000 },
- hidden => { anybool => 1 },
- l_wp => { required => 0, default => '', maxlength => 150 },
- lang => { language => 1 },
- locked => { anybool => 1 },
- original => { required => 0, default => '', maxlength => 200 },
- name => { maxlength => 200 },
- ptype => { enum => \%PRODUCER_TYPE }, # This is 'type' in the database, but renamed for Elm compat
- relations => { maxlength => 50, sort_keys => 'pid', aoh => {
- pid => { id => 1 }, # X
- relation => { producer_relation => 1 },
- name => { _when => 'out' },
- } },
- website => { required => 0, default => '', weburl => 1 },
-
- id => { _when => 'out', required => 0, id => 1 },
- authmod => { _when => 'out', anybool => 1 },
- editsum => { _when => 'in out', editsum => 1 },
-};
-
-my $FORM_OUT = form_compile out => $FORM;
-my $FORM_IN = form_compile in => $FORM;
-my $FORM_CMP = form_compile cmp => $FORM;
-
-elm_form ProdEdit => $FORM_OUT, $FORM_IN;
-
-
-TUWF::get qr{/$PREV_RE/edit} => sub {
- my $p = entry p => tuwf->capture('id'), tuwf->capture('rev') or return tuwf->resNotFound;
- return tuwf->resDenied if !can_edit p => $p;
-
- enrich pid => q{SELECT id AS pid, name FROM producers WHERE id IN} => $p->{relations};
-
- $p->{l_wp} //= ''; # TODO: The DB currently uses NULL when no wp link is provided, this should be an empty string instead to be consistent with most other fields.
- $p->{ptype} = delete $p->{type};
- $p->{authmod} = auth->permDbmod;
- $p->{editsum} = $p->{chrev} == $p->{maxrev} ? '' : "Reverted to revision p$p->{id}.$p->{chrev}";
-
- Framework index => 0, title => "Edit $p->{name}",
- top => sub {
- Div class => 'col-md', sub {
- EntryEdit p => $p;
- Div class => 'detail-page-title', sub {
- Txt $p->{name};
- Debug $p;
- };
- };
- }, sub {
- FullPageForm module => 'ProdEdit.Main', data => $p, schema => $FORM_OUT, sections => [
- general => 'General info',
- relations => 'Relations',
- ];
- };
-};
-
-
-TUWF::get '/p/add', sub {
- return tuwf->resDenied if !auth->permEdit;
- Framework index => 0, title => 'Add a new producer', narrow => 1, sub {
- Div class => 'row', sub {
- Div class => 'col-md col-md--1', sub { Div 'data-elm-module' => 'ProdEdit.New', '' };
- };
- };
-};
-
-
-json_api qr{/(?:$PID_RE/edit|p/add)}, $FORM_IN, sub {
- my $data = shift;
- my $new = !tuwf->capture('id');
- my $p = $new ? { id => 0 } : entry p => tuwf->capture('id') or return tuwf->resNotFound;
-
- return $elm_Unauth->() if !can_edit p => $p;
-
- $data->{l_wp} ||= undef;
- if(!auth->permDbmod) {
- $data->{hidden} = $p->{hidden}||0;
- $data->{locked} = $p->{locked}||0;
- }
- $data->{relations} = [] if $data->{hidden};
-
- die "Relation with self" if grep $_->{pid} == $p->{id}, @{$data->{relations}};
- validate_dbid 'SELECT id FROM producers WHERE id IN', map $_->{pid}, @{$data->{relations}};
-
- $data->{desc} = bb_subst_links $data->{desc};
-
- $p->{ptype} = delete $p->{type};
- return $elm_Unchanged->() if !$new && !form_changed $FORM_CMP, $data, $p;
- $data->{type} = delete $data->{ptype};
-
- my($id,undef,$rev) = update_entry p => $p->{id}, $data;
-
- update_reverse($id, $rev, $p, $data);
-
- $elm_Changed->($id, $rev);
-};
-
-
-sub update_reverse {
- my($id, $rev, $old, $new) = @_;
-
- my %old = map +($_->{pid}, $_), $old->{relations} ? @{$old->{relations}} : ();
- my %new = map +($_->{pid}, $_), @{$new->{relations}};
-
- # Updates to be performed, pid => { pid => x, relation => y } or undef if the relation should be removed.
- my %upd;
-
- for my $i (keys %old, keys %new) {
- if($old{$i} && !$new{$i}) {
- $upd{$i} = undef;
- } elsif(!$old{$i} || $old{$i}{relation} ne $new{$i}{relation}) {
- $upd{$i} = {
- pid => $id,
- relation => producer_relation_reverse($new{$i}{relation}),
- };
- }
- }
-
- for my $i (keys %upd) {
- my $p = entry p => $i;
- $p->{relations} = [
- $upd{$i} ? $upd{$i} : (),
- grep $_->{pid} != $id, @{$p->{relations}}
- ];
- $p->{editsum} = "Reverse relation update caused by revision p$id.$rev";
- update_entry p => $i, $p, 1;
- }
-}
-
-1;
diff --git a/lib/VN3/Producer/JS.pm b/lib/VN3/Producer/JS.pm
deleted file mode 100644
index 50161ce5..00000000
--- a/lib/VN3/Producer/JS.pm
+++ /dev/null
@@ -1,47 +0,0 @@
-package VN3::Producer::JS;
-
-use VN3::Prelude;
-
-
-my $elm_ProducerResult = elm_api ProducerResult => { aoh => {
- id => { id => 1 },
- name => {},
- original => {},
- hidden => { anybool => 1 },
-}};
-
-
-json_api '/js/producer.json', {
- search => { type => 'array', scalar => 1, minlength => 1, values => { maxlength => 500 } },
- hidden => { anybool => 1 }
-}, sub {
- my $data = shift;
-
- my $r = tuwf->dbAlli(
- 'SELECT p.id, p.name, p.original, p.hidden',
- 'FROM (', (sql_join 'UNION ALL', map {
- my $q = $_;
- my $qs = s/[%_]//gr;
- +(
- # ID search
- /^$PID_RE$/ ? (sql 'SELECT 1, id FROM producers WHERE id =', \"$1") : (),
- # exact match
- sql('SELECT 2, id FROM producers WHERE lower(name) = lower(', \$q, ") OR lower(translate(original,' ', '')) = lower(", \($q =~ s/\s//gr), ')'),
- # prefix match
- sql('SELECT 3, id FROM producers WHERE name ILIKE', \"$qs%", ' OR original ILIKE', \"$qs%"),
- # substring match
- sql('SELECT 4, id FROM producers WHERE name ILIKE', \"%$qs%", ' OR original ILIKE', \"%$qs%", ' OR alias ILIKE', \"%$qs%")
- )
- } @{$data->{search}}),
- ') AS pt (ord, id)',
- 'JOIN producers p ON p.id = pt.id',
- $data->{hidden} ? () : ('WHERE NOT p.hidden'),
- 'GROUP BY p.id',
- 'ORDER BY MIN(pt.ord), p.name',
- 'LIMIT 20'
- );
-
- $elm_ProducerResult->($r);
-};
-
-1;
diff --git a/lib/VN3/Producer/Page.pm b/lib/VN3/Producer/Page.pm
deleted file mode 100644
index 89cd9dd8..00000000
--- a/lib/VN3/Producer/Page.pm
+++ /dev/null
@@ -1,117 +0,0 @@
-package VN3::Producer::Page;
-
-use VN3::Prelude;
-
-# TODO: Releases/VNs
-# TODO: Relation graph
-
-sub Notes {
- my $e = shift;
-
- Div class => 'row', sub {
- Div class => 'fixed-size-left-sidebar-md', sub {
- H2 class => 'detail-page-sidebar-section-header', 'Notes';
- };
- Div class => 'col-md', sub {
- Div class => 'description serif mb-5', sub {
- P sub { Lit bb2html $e->{desc} };
- };
- };
- } if $e->{desc};
-}
-
-
-sub DetailsTable {
- my $e = shift;
-
- my @links = (
- $e->{website} ? [ 'Official website', $e->{website} ] : (),
- $e->{l_wp} ? [ 'Wikipedia', "https://en.wikipedia.org/wiki/$e->{l_wp}" ] : (),
- );
-
- my %rel;
- push @{$rel{$_->{relation}}}, $_ for (sort { $a->{name} cmp $b->{name} } @{$e->{relations}});
-
- my @list = (
- $e->{alias} ? sub {
- Dt $e->{alias} =~ /\n/ ? 'Aliases' : 'Alias';
- Dd $e->{alias} =~ s/\n/, /gr;
- } : (),
-
- sub {
- Dt 'Type';
- Dd $PRODUCER_TYPE{$e->{type}};
- },
-
- sub {
- Dt 'Language';
- Dd sub {
- Lang $e->{lang};
- Txt " $LANGUAGE{$e->{lang}}";
- }
- },
-
- @links ? sub {
- Dt 'Links';
- Dd sub {
- Join ', ', sub { A href => $_[0][1], rel => 'nofollow', $_[0][0] }, @links;
- };
- } : (),
-
- (map {
- my $r = $_;
- sub {
- Dt producer_relation_display $r;
- Dd sub {
- Join ', ', sub {
- A href => "/p$_[0]{pid}", title => $_[0]{original}||$_[0]{name}, $_[0]{name};
- }, @{$rel{$r}}
- }
- }
- } grep $rel{$_}, keys %PRODUCER_RELATION)
- );
-
- Div class => 'row', sub {
- Div class => 'fixed-size-left-sidebar-md', sub {
- H2 class => 'detail-page-sidebar-section-header', 'Details';
- };
- Div class => 'col-md', sub {
- Div class => 'card card--white mb-5', sub {
- Div class => 'card__section fs-medium', sub {
- Div class => 'row', sub {
- Dl class => 'col-md dl--horizontal', sub { $_->() for @list[0..$#list/2] };
- Dl class => 'col-md dl--horizontal', sub { $_->() for @list[$#list/2+1..$#list] };
- }
- }
- }
- }
- } if @list;
-}
-
-
-TUWF::get qr{/$PREV_RE}, sub {
- my $e = entry p => tuwf->capture('id'), tuwf->capture('rev') or return tuwf->resNotFound;
- return tuwf->resNotFound if !$e->{id} || $e->{hidden};
-
- enrich pid => q{SELECT id AS pid, name, original FROM producers WHERE id IN}, $e->{relations};
-
- Framework
- title => $e->{name},
- top => sub {
- Div class => 'col-md', sub {
- EntryEdit p => $e;
- Div class => 'detail-page-title', sub {
- Txt $e->{name};
- Debug $e;
- };
- Div class => 'detail-page-subtitle', $e->{original} if $e->{original};
- # TODO: link to discussions page. Prolly needs a TopNav
- }
- },
- sub {
- DetailsTable $e;
- Notes $e;
- };
-};
-
-1;
diff --git a/lib/VN3/Release/Edit.pm b/lib/VN3/Release/Edit.pm
deleted file mode 100644
index 030c0711..00000000
--- a/lib/VN3/Release/Edit.pm
+++ /dev/null
@@ -1,130 +0,0 @@
-package VN3::Release::Edit;
-
-use VN3::Prelude;
-
-my $FORM = {
- hidden => { anybool => 1 },
- locked => { anybool => 1 },
- title => { maxlength => 250 },
- original => { required => 0, default => '', maxlength => 250 },
- rtype => { enum => [ release_types ] }, # This is 'type' in the database, but renamed for Elm compat
- patch => { anybool => 1 },
- freeware => { anybool => 1 },
- doujin => { anybool => 1 },
- lang => { minlength => 1, sort_keys => 'lang', aoh => { lang => { language => 1 } } },
- gtin => { gtin => 1 },
- catalog => { required => 0, default => '', maxlength => 50 },
- website => { required => 0, default => '', weburl => 1 },
- released => { rdate => 1, min => 1 },
- minage => { required => 0, minage => 1 },
- uncensored => { anybool => 1 },
- notes => { required => 0, default => '', maxlength => 10240 },
- resolution => { resolution => 1 },
- voiced => { voiced => 1 },
- ani_story => { animated => 1 },
- ani_ero => { animated => 1 },
- platforms => { sort_keys => 'platform', aoh => { platform => { platform => 1 } } },
- media => { sort_keys => ['media', 'qty'], aoh => {
- medium => { medium => 1 },
- qty => { uint => 1, range => [0,20] },
- } },
- vn => { length => [1,50], sort_keys => 'vid', aoh => {
- vid => { id => 1 }, # X
- title => { _when => 'out' },
- } },
- producers => { maxlength => 50, sort_keys => 'pid', aoh => {
- pid => { id => 1 }, # X
- developer => { anybool => 1 },
- publisher => { anybool => 1 },
- name => { _when => 'out' },
- } },
-
- id => { _when => 'out', required => 0, id => 1 },
- authmod => { _when => 'out', anybool => 1 },
- editsum => { _when => 'in out', editsum => 1 },
-};
-
-my $FORM_OUT = form_compile out => $FORM;
-my $FORM_IN = form_compile in => $FORM;
-my $FORM_CMP = form_compile cmp => $FORM;
-
-elm_form RelEdit => $FORM_OUT, $FORM_IN;
-
-TUWF::get qr{/$RREV_RE/(?<type>edit|copy)}, sub {
- my $r = entry r => tuwf->capture('id'), tuwf->capture('rev') or return tuwf->resNotFound;
- return tuwf->resDenied if !can_edit r => $r;
- my $copy = tuwf->capture('type') eq 'copy';
-
- enrich vid => q{SELECT id AS vid, title FROM vn WHERE id IN} => $r->{vn};
- enrich pid => q{SELECT id AS pid, name FROM producers WHERE id IN} => $r->{producers};
-
- $r->{rtype} = delete $r->{type};
- $r->{authmod} = auth->permDbmod;
- $r->{editsum} = $copy ? "Copied from r$r->{id}.$r->{chrev}" : $r->{chrev} == $r->{maxrev} ? '' : "Reverted to revision r$r->{id}.$r->{chrev}";
-
- my $title = sprintf '%s %s', $copy ? 'Copy' : 'Edit', $r->{title};
- Framework title => $title,
- top => sub {
- Div class => 'col-md', sub {
- EntryEdit r => $r;
- Div class => 'detail-page-title', sub {
- Txt $title;
- Debug $r;
- };
- };
- }, sub {
- FullPageForm module => 'RelEdit.Main', schema => $FORM_OUT, data => { %$r, $copy ? (id => undef) : () }, sections => [
- general => 'General info',
- format => 'Format',
- relations => 'Relations'
- ];
- };
-};
-
-
-TUWF::get qr{/$VID_RE/add}, sub {
- return tuwf->resDenied if !auth->permEdit;
-
- my $vn = tuwf->dbRowi('SELECT id, title, original FROM vn WHERE NOT hidden AND id =', \tuwf->capture('id'));
- return tuwf->resNotFound if !$vn->{id};
-
- Framework index => 0, title => "Add a new release to $vn->{title}", narrow => 1, sub {
- FullPageForm module => 'RelEdit.New', data => $vn, sections => [
- general => 'General info',
- format => 'Format',
- relations => 'Relations'
- ];
- };
-};
-
-
-json_api qr{/(?:$RID_RE/edit|r/add)}, $FORM_IN, sub {
- my $data = shift;
- my $new = !tuwf->capture('id');
- my $rel = $new ? { id => 0 } : entry r => tuwf->capture('id') or return tuwf->resNotFound;
-
- return $elm_Unauth->() if !can_edit r => $rel;
-
- if(!auth->permDbmod) {
- $data->{hidden} = $rel->{hidden}||0;
- $data->{locked} = $rel->{locked}||0;
- }
- $data->{doujin} = $data->{voiced} = $data->{ani_story} = $data->{ani_ero} = 0 if $data->{patch};
- $data->{resolution} = 'unknown' if $data->{patch};
- $data->{uncensored} = 0 if !$data->{minage} || $data->{minage} != 18;
- $_->{qty} = $MEDIUM{$_->{medium}}{qty} ? $_->{qty}||1 : 0 for @{$data->{media}};
-
- validate_dbid 'SELECT id FROM vn WHERE id IN', map $_->{vid}, @{$data->{vn}};
- validate_dbid 'SELECT id FROM producers WHERE id IN', map $_->{pid}, @{$data->{producers}};
-
- $data->{notes} = bb_subst_links $data->{notes};
-
- $rel->{rtype} = delete $rel->{type};
- return $elm_Unchanged() if !$new && !form_changed $FORM_CMP, $data, $rel;
- $data->{type} = delete $data->{rtype};
-
- my($id,undef,$rev) = update_entry r => $rel->{id}, $data;
- $elm_Changed->($id, $rev);
-};
-
-1;
diff --git a/lib/VN3/Release/JS.pm b/lib/VN3/Release/JS.pm
deleted file mode 100644
index 152fd69a..00000000
--- a/lib/VN3/Release/JS.pm
+++ /dev/null
@@ -1,32 +0,0 @@
-package VN3::Release::JS;
-
-use VN3::Prelude;
-
-
-my $elm_ReleaseResult = elm_api ReleaseResult => { aoh => {
- id => { id => 1 },
- title => {},
- lang => { type => 'array', values => {} },
-}};
-
-
-# Fetch all releases assigned to a VN
-json_api '/js/release.json', {
- vid => { id => 1 },
-}, sub {
- my $vid = shift->{vid};
-
- my $r = tuwf->dbAlli(q{
- SELECT r.id, r.title
- FROM releases r
- JOIN releases_vn rv ON rv.id = r.id
- WHERE NOT r.hidden
- AND rv.vid =}, \$vid, q{
- ORDER BY r.id
- });
- enrich_list1 lang => id => id => sub { sql 'SELECT id, lang FROM releases_lang WHERE id IN', $_[0], 'ORDER BY id, lang' }, $r;
-
- $elm_ReleaseResult->($r);
-};
-
-1;
diff --git a/lib/VN3/Release/Page.pm b/lib/VN3/Release/Page.pm
deleted file mode 100644
index 03d3bd5c..00000000
--- a/lib/VN3/Release/Page.pm
+++ /dev/null
@@ -1,184 +0,0 @@
-package VN3::Release::Page;
-
-use VN3::Prelude;
-
-# TODO: Userlist options
-
-
-sub Notes {
- my $e = shift;
-
- Div class => 'row', sub {
- Div class => 'fixed-size-left-sidebar-md', sub {
- H2 class => 'detail-page-sidebar-section-header', 'Notes';
- };
- Div class => 'col-md', sub {
- Div class => 'description serif mb-5', sub {
- P sub { Lit bb2html $e->{notes} };
- };
- };
- } if $e->{notes};
-}
-
-
-sub DetailsTable {
- my $e = shift;
-
- # TODO: Some of these properties could be moved into the title header thing
- # (type and languages, in particular)
- # (Not even sure this table format makes sense for all properties, there's gotta be a nicer way)
- my @list = (
- @{$e->{vn}} ? sub {
- Dt @{$e->{vn}} == 1 ? 'Visual Novel' : 'Visual Novels';
- Dd sub {
- Join \&Br, sub {
- A href => "/v$_[0]{vid}", title => $_[0]{original}||$_[0]{title}, $_[0]{title};
- }, @{$e->{vn}};
- }
- } : (),
-
- sub {
- Dt 'Type';
- Dd sub {
- Txt ucfirst $e->{type};
- Txt ", patch" if $e->{patch};
- }
- },
-
- sub {
- Dt 'Released';
- Dd sub { ReleaseDate $e->{released} };
- },
-
- sub {
- Dt @{$e->{lang}} > 1 ? 'Languages' : 'Language';
- Dd sub {
- Join \&Br, sub {
- Lang $_[0]{lang};
- Txt " $LANGUAGE{$_[0]{lang}}";
- }, @{$e->{lang}};
- }
- },
-
- sub {
- Dt 'Publication';
- Dd join ', ',
- $e->{freeware} ? 'Freeware' : 'Non-free',
- $e->{patch} ? () : ($e->{doujin} ? 'doujin' : 'commercial')
- },
-
- $e->{minage} && $e->{minage} >= 0 ? sub {
- Dt 'Age rating';
- Dd minage_display $e->{minage};
- } : (),
-
- @{$e->{platforms}} ? sub {
- Dt @{$e->{platforms}} == 1 ? 'Platform' : 'Platforms';
- Dd sub {
- Join \&Br, sub {
- Platform $_[0]{platform};
- Txt " $PLATFORM{$_[0]{platform}}";
- }, @{$e->{platforms}};
- }
- } : (),
-
- @{$e->{media}} ? sub {
- Dt @{$e->{media}} == 1 ? 'Medium' : 'Media';
- Dd join ', ', map media_display($_->{medium}, $_->{qty}), @{$e->{media}};
- } : (),
-
- $e->{voiced} ? sub {
- Dt 'Voiced';
- Dd $VOICED{$e->{voiced}}{txt};
- } : (),
-
- $e->{ani_story} ? sub {
- Dt 'Story animation';
- Dd $ANIMATED{$e->{ani_story}}{txt};
- } : (),
-
- $e->{ani_ero} ? sub {
- Dt 'Ero animation';
- Dd $ANIMATED{$e->{ani_ero}}{txt};
- } : (),
-
- $e->{minage} && $e->{minage} == 18 ? sub {
- Dt 'Censoring';
- Dd $e->{uncensored} ? 'No optical censoring (e.g. mosaics)' : 'May include optical censoring (e.g. mosaics)';
- } : (),
-
- $e->{gtin} ? sub {
- Dt gtintype($e->{gtin}) || 'GTIN';
- Dd $e->{gtin};
- } : (),
-
- $e->{catalog} ? sub {
- Dt 'Catalog no.';
- Dd $e->{catalog};
- } : (),
-
- (map {
- my $type = $_;
- my @prod = grep $_->{$type}, @{$e->{producers}};
- @prod ? sub {
- Dt ucfirst($type) . (@prod == 1 ? '' : 's');
- Dd sub {
- Join \&Br, sub {
- A href => "/p$_[0]{pid}", title => $_[0]{original}||$_[0]{name}, $_[0]{name};
- }, @prod;
- }
- } : ()
- } 'developer', 'publisher'),
-
- $e->{website} ? sub {
- Dt 'Links';
- Dd sub {
- A href => $e->{website}, rel => 'nofollow', 'Official website';
- };
- } : (),
- );
-
- Div class => 'row', sub {
- Div class => 'fixed-size-left-sidebar-md', sub {
- H2 class => 'detail-page-sidebar-section-header', 'Details';
- };
- Div class => 'col-md', sub {
- Div class => 'card card--white mb-5', sub {
- Div class => 'card__section fs-medium', sub {
- Div class => 'row', sub {
- Dl class => 'col-md dl--horizontal', sub { $_->() for @list[0..$#list/2] };
- Dl class => 'col-md dl--horizontal', sub { $_->() for @list[$#list/2+1..$#list] };
- }
- }
- }
- }
- } if @list;
-}
-
-
-TUWF::get qr{/$RREV_RE}, sub {
- my $e = entry r => tuwf->capture('id'), tuwf->capture('rev') or return tuwf->resNotFound;
- return tuwf->resNotFound if !$e->{id} || $e->{hidden};
-
- enrich vid => q{SELECT id AS vid, title, original FROM vn WHERE id IN}, $e->{vn};
- enrich pid => q{SELECT id AS pid, name, original FROM producers WHERE id IN}, $e->{producers};
-
- Framework
- title => $e->{title},
- top => sub {
- Div class => 'col-md', sub {
- EntryEdit r => $e;
- Div class => 'detail-page-title', sub {
- Txt $e->{title};
- Debug $e;
- };
- Div class => 'detail-page-subtitle', $e->{original} if $e->{original};
- }
- },
- sub {
- DetailsTable $e;
- Notes $e;
- };
-};
-
-1;
diff --git a/lib/VN3/Staff/Edit.pm b/lib/VN3/Staff/Edit.pm
deleted file mode 100644
index 0b9c3af4..00000000
--- a/lib/VN3/Staff/Edit.pm
+++ /dev/null
@@ -1,108 +0,0 @@
-package VN3::Staff::Edit;
-
-use VN3::Prelude;
-
-
-my $FORM = {
- aid => { int => 1, range => [ -1000, 1<<40 ] }, # X
- alias => { maxlength => 100, sort_keys => 'aid', aoh => {
- aid => { int => 1, range => [ -1000, 1<<40 ] }, # X, negative IDs are for new aliases
- name => { maxlength => 200 },
- original => { maxlength => 200, required => 0, default => '' },
- inuse => { anybool => 1, _when => 'out' },
- } },
- desc => { required => 0, default => '', maxlength => 5000 },
- gender => { gender => 1 },
- hidden => { anybool => 1 },
- l_site => { required => 0, default => '', weburl => 1 },
- l_wp => { required => 0, default => '', maxlength => 150 },
- l_twitter => { required => 0, default => '', maxlength => 150 },
- l_anidb => { required => 0, id => 1 },
- lang => { language => 1 },
- locked => { anybool => 1 },
-
- id => { _when => 'out', required => 0, id => 1 },
- authmod => { _when => 'out', anybool => 1 },
- editsum => { _when => 'in out', editsum => 1 },
-};
-
-my $FORM_OUT = form_compile out => $FORM;
-my $FORM_IN = form_compile in => $FORM;
-my $FORM_CMP = form_compile cmp => $FORM;
-
-elm_form StaffEdit => $FORM_OUT, $FORM_IN;
-
-
-TUWF::get qr{/$SREV_RE/edit} => sub {
- my $e = entry s => tuwf->capture('id'), tuwf->capture('rev') or return tuwf->resNotFound;
- return tuwf->resDenied if !can_edit s => $e;
-
- $e->{authmod} = auth->permDbmod;
- $e->{editsum} = $e->{chrev} == $e->{maxrev} ? '' : "Reverted to revision s$e->{id}.$e->{chrev}";
-
- enrich aid => sub { sql '
- SELECT aid, EXISTS(SELECT 1 FROM vn_staff WHERE aid = x.aid UNION ALL SELECT 1 FROM vn_seiyuu WHERE aid = x.aid) AS inuse
- FROM unnest(', sql_array(@{$_[0]}), '::int[]) AS x(aid)'
- }, $e->{alias};
-
- my $name = (grep $_->{aid} == $e->{aid}, @{$e->{alias}})[0]{name};
- Framework index => 0, narrow => 1, title => "Edit $name",
- top => sub {
- Div class => 'col-md', sub {
- EntryEdit s => $e;
- Div class => 'detail-page-title', sub {
- Txt $name,
- Debug $e;
- };
- };
- }, sub {
- FullPageForm module => 'StaffEdit.Main', data => $e, schema => $FORM_OUT;
- };
-};
-
-
-TUWF::get '/s/new', sub {
- return tuwf->resDenied if !auth->permEdit;
- Framework index => 0, title => 'Add a new staff entry', narrow => 1, sub {
- Div class => 'row', sub {
- Div class => 'col-md col-md--1', sub { Div 'data-elm-module' => 'StaffEdit.New', '' };
- };
- };
-};
-
-
-json_api qr{/(?:$SID_RE/edit|s/add)}, $FORM_IN, sub {
- my $data = shift;
- my $new = !tuwf->capture('id');
- my $e = $new ? { id => 0 } : entry s => tuwf->capture('id') or return tuwf->resNotFound;
-
- return $elm_Unauth->() if !can_edit s => $e;
-
- if(!auth->permDbmod) {
- $data->{hidden} = $e->{hidden}||0;
- $data->{locked} = $e->{locked}||0;
- }
-
- # For positive alias IDs: Make sure they exist and are owned by this entry.
- validate_dbid
- sub { sql 'SELECT aid FROM staff_alias WHERE id =', \$e->{id}, ' AND aid IN', $_[0] },
- grep $_>=0, map $_->{aid}, @{$data->{alias}};
-
- # For negative alias IDs: Assign a new ID.
- for my $alias (@{$data->{alias}}) {
- if($alias->{aid} < 0) {
- my $new = tuwf->dbVali(select => sql_func nextval => \'staff_alias_aid_seq');
- $data->{aid} = $new if $alias->{aid} == $data->{aid};
- $alias->{aid} = $new;
- }
- }
- # We rely on Postgres to throw an error if we attempt to delete an alias that is still being referenced.
-
- $data->{desc} = bb_subst_links $data->{desc};
-
- return $elm_Unchanged->() if !$new && !form_changed $FORM_CMP, $data, $e;
- my($id,undef,$rev) = update_entry s => $e->{id}, $data;
- $elm_Changed->($id, $rev);
-};
-
-1;
diff --git a/lib/VN3/Staff/JS.pm b/lib/VN3/Staff/JS.pm
deleted file mode 100644
index 58ce947b..00000000
--- a/lib/VN3/Staff/JS.pm
+++ /dev/null
@@ -1,43 +0,0 @@
-package Staff::JS;
-
-use VN3::Prelude;
-
-my $elm_StaffResult = elm_api StaffResult => { aoh => {
- id => { id => 1 },
- aid => { id => 1 },
- name => {},
- original => {},
-}};
-
-json_api '/js/staff.json', {
- search => { maxlength => 500 }
-}, sub {
- my $q = shift->{search};
-
- # XXX: This query is kinda slow
- my $qs = $q =~ s/[%_]//gr;
- my $r = tuwf->dbAlli(
- 'SELECT s.id, st.aid, st.name, st.original',
- 'FROM (',
- # ID search
- $q =~ /^$SID_RE$/ ? ('SELECT 1, id, aid, name, original FROM staff_alias WHERE id =', \"$1", 'UNION ALL') : (),
- # exact match
- 'SELECT 2, id, aid, name, original FROM staff_alias WHERE lower(name) = lower(', \$q, ") OR lower(translate(original,' ', '')) = lower(", \($q =~ s/\s//gr), ')',
- 'UNION ALL',
- # prefix match
- 'SELECT 3, id, aid, name, original FROM staff_alias WHERE name ILIKE', \"$qs%", ' OR original ILIKE', \"$qs%",
- 'UNION ALL',
- # substring match
- 'SELECT 4, id, aid, name, original FROM staff_alias WHERE name ILIKE', \"%$qs%", ' OR original ILIKE', \"%$qs%",
- ') AS st (ord, id, aid, name, original)',
- 'JOIN staff s ON s.id = st.id',
- 'WHERE NOT s.hidden',
- 'GROUP BY s.id, st.aid, st.name, st.original',
- 'ORDER BY MIN(st.ord), st.name',
- 'LIMIT 20'
- );
-
- $elm_StaffResult->($r);
-};
-
-1;
diff --git a/lib/VN3/Staff/Page.pm b/lib/VN3/Staff/Page.pm
deleted file mode 100644
index 2d8cd349..00000000
--- a/lib/VN3/Staff/Page.pm
+++ /dev/null
@@ -1,213 +0,0 @@
-package VN3::Staff::Page;
-
-use VN3::Prelude;
-
-sub Notes {
- my $e = shift;
-
- Div class => 'row', sub {
- Div class => 'fixed-size-left-sidebar-md', sub {
- H2 class => 'detail-page-sidebar-section-header', 'Notes';
- };
- Div class => 'col-md', sub {
- Div class => 'description serif mb-5', sub {
- P sub { Lit bb2html $e->{desc} };
- };
- };
- } if $e->{desc};
-}
-
-
-sub DetailsTable {
- my $e = shift;
-
- my @links = (
- $e->{l_site} ? [ 'Official website', $e->{l_site} ] : (),
- $e->{l_wp} ? [ 'Wikipedia', "https://en.wikipedia.org/wiki/$e->{l_wp}" ] : (),
- $e->{l_twitter} ? [ 'Twitter', "https://twitter.com/$e->{l_twitter}" ] : (),
- $e->{l_anidb} ? [ 'AniDB', "http://anidb.net/cr$e->{l_anidb}" ] : (),
- );
- my @alias = grep $_->{aid} != $e->{aid}, @{$e->{alias}};
-
- my @list = (
- @alias ? sub {
- Dt @alias > 1 ? 'Aliases' : 'Alias';
- Dd sub {
- Join \&Br, sub {
- Txt $_[0]{name};
- Txt " ($_[0]{original})" if $_[0]{original};
- }, sort { $a->{name} cmp $b->{name} || $a->{original} cmp $b->{original} } @alias;
- }
- } : (),
-
- sub {
- Dt 'Language';
- Dd sub {
- Lang $e->{lang};
- Txt " $LANGUAGE{$e->{lang}}";
- }
- },
-
- @links ? sub {
- Dt 'Links';
- Dd sub {
- Join ', ', sub { A href => $_[0][1], rel => 'nofollow', $_[0][0] }, @links;
- };
- } : (),
- );
-
- Div class => 'row', sub {
- Div class => 'fixed-size-left-sidebar-md', sub {
- H2 class => 'detail-page-sidebar-section-header', 'Details';
- };
- Div class => 'col-md', sub {
- Div class => 'card card--white mb-5', sub {
- Div class => 'card__section fs-medium', sub {
- Div class => 'row', sub {
- Dl class => 'col-md dl--horizontal', sub { $_->() for @list[0..$#list/2] };
- Dl class => 'col-md dl--horizontal', sub { $_->() for @list[$#list/2+1..$#list] };
- }
- }
- }
- }
- } if @list;
-}
-
-
-sub Roles {
- my $e = shift;
-
- my $roles = tuwf->dbAlli(q{
- SELECT sa.id, sa.aid, v.id AS vid, sa.name, sa.original, v.c_released, v.title, v.original AS t_original, vs.role, vs.note
- FROM vn_staff vs
- JOIN vn v ON v.id = vs.id
- JOIN staff_alias sa ON vs.aid = sa.aid
- WHERE sa.id =}, \$e->{id}, q{ AND NOT v.hidden
- ORDER BY v.c_released ASC, v.title ASC, vs.role ASC
- });
- return if !@$roles;
-
- my $rows = sub {
- for my $r (@$roles) {
- Tr sub {
- Td class => 'tabular-nums muted', sub { ReleaseDate $r->{c_released} };
- Td sub {
- A href => "/v$r->{vid}", title => $r->{t_original}||$r->{title}, $r->{title};
- };
- Td $CREDIT_TYPE{$r->{role}};
- Td title => $r->{original}||$r->{name}, $r->{name};
- Td $r->{note};
- };
- }
- };
-
- # TODO: Full-width table? It's pretty dense
- Div class => 'row', sub {
- Div class => 'fixed-size-left-sidebar-md', sub {
- H2 class => 'detail-page-sidebar-section-header', 'Credits';
- Debug $roles;
- };
- Div class => 'col-md', sub {
- Div class => 'card card--white mb-5', sub {
- Table class => 'table table--responsive-single-sm fs-medium', sub {
- Thead sub {
- Tr sub {
- Th width => '15%', 'Date';
- Th width => '30%', 'Title';
- Th width => '20%', 'Role';
- Th width => '20%', 'As';
- Th width => '15%', 'Note';
- };
- };
- Tbody $rows;
- };
- }
- }
- }
-}
-
-
-sub Cast {
- my $e = shift;
-
- my $cast = tuwf->dbAlli(q{
- SELECT sa.id, sa.aid, v.id AS vid, sa.name, sa.original, v.c_released, v.title, v.original AS t_original, c.id AS cid, c.name AS c_name, c.original AS c_original, vs.note
- FROM vn_seiyuu vs
- JOIN vn v ON v.id = vs.id
- JOIN chars c ON c.id = vs.cid
- JOIN staff_alias sa ON vs.aid = sa.aid
- WHERE sa.id =}, \$e->{id}, q{ AND NOT v.hidden
- ORDER BY v.c_released ASC, v.title ASC
- });
- return if !@$cast;
-
- my $rows = sub {
- for my $c (@$cast) {
- Tr sub {
- Td class => 'tabular-nums muted', sub { ReleaseDate $c->{c_released} };
- Td sub {
- A href => "/v$c->{vid}", title => $c->{t_original}||$c->{title}, $c->{title};
- };
- Td sub {
- A href => "/c$c->{cid}", title => $c->{c_original}||$c->{c_name}, $c->{c_name};
- };
- Td title => $c->{original}||$c->{name}, $c->{name};
- Td $c->{note};
- };
- }
- };
-
- # TODO: Full-width table? It's pretty dense
- Div class => 'row', sub {
- Div class => 'fixed-size-left-sidebar-md', sub {
- H2 class => 'detail-page-sidebar-section-header', 'Voiced Characters';
- Debug $cast;
- };
- Div class => 'col-md', sub {
- Div class => 'card card--white mb-5', sub {
- Table class => 'table table--responsive-single-sm fs-medium', sub {
- Thead sub {
- Tr sub {
- Th width => '15%', 'Date';
- Th width => '30%', 'Title';
- Th width => '20%', 'Cast';
- Th width => '20%', 'As';
- Th width => '15%', 'Note';
- };
- };
- Tbody $rows;
- };
- }
- }
- }
-}
-
-
-TUWF::get qr{/$SREV_RE}, sub {
- my $e = entry s => tuwf->capture('id'), tuwf->capture('rev') or return tuwf->resNotFound;
- return tuwf->resNotFound if !$e->{id} || $e->{hidden};
-
- ($e->{name}, $e->{original}) = @{(grep $_->{aid} == $e->{aid}, @{$e->{alias}})[0]}{'name', 'original'};
-
- Framework
- title => $e->{name},
- top => sub {
- Div class => 'col-md', sub {
- EntryEdit s => $e;
- Div class => 'detail-page-title', sub {
- Txt $e->{name};
- Txt ' '.gender_icon $e->{gender};
- Debug $e;
- };
- Div class => 'detail-page-subtitle', $e->{original} if $e->{original};
- }
- },
- sub {
- DetailsTable $e;
- Notes $e;
- Roles $e;
- Cast $e;
- };
-};
-
-1;
diff --git a/lib/VN3/Trait/JS.pm b/lib/VN3/Trait/JS.pm
deleted file mode 100644
index 05e1d03d..00000000
--- a/lib/VN3/Trait/JS.pm
+++ /dev/null
@@ -1,44 +0,0 @@
-package VN3::Trait::JS;
-
-use VN3::Prelude;
-
-my $elm_TraitResult = elm_api TraitResult => { aoh => {
- id => { id => 1 },
- name => {},
- gid => { id => 1, required => 0 },
- group => { required => 0 }
-}};
-
-# Returns only approved and applicable traits
-json_api '/js/trait.json', {
- search => { maxlength => 500 }
-}, sub {
- my $q = shift->{search};
-
- my $qs = $q =~ s/[%_]//gr;
- my $r = tuwf->dbAlli(
- 'SELECT t.id, t.name, g.id AS gid, g.name AS group',
- 'FROM (',
- # ID search
- $q =~ /^$IID_RE$/ ? ('SELECT 1, id FROM traits WHERE id =', \"$1", 'UNION ALL') : (),
- # exact match
- 'SELECT 2, id FROM traits WHERE lower(name) = lower(', \$q, ")",
- 'UNION ALL',
- # prefix match
- 'SELECT 3, id FROM traits WHERE name ILIKE', \"$qs%",
- 'UNION ALL',
- # substring match + alias search
- 'SELECT 4, id FROM traits WHERE name ILIKE', \"%$qs%", ' OR alias ILIKE', \"%$qs%",
- ') AS tt (ord, id)',
- 'JOIN traits t ON t.id = tt.id',
- 'LEFT JOIN traits g ON g.id = t.group',
- 'WHERE t.state = 2 AND t.applicable',
- 'GROUP BY t.id, t.name, g.id, g.name',
- 'ORDER BY MIN(tt.ord), t.name',
- 'LIMIT 20'
- );
-
- $elm_TraitResult->($r);
-};
-
-1;
diff --git a/lib/VN3/Types.pm b/lib/VN3/Types.pm
deleted file mode 100644
index 273f8b79..00000000
--- a/lib/VN3/Types.pm
+++ /dev/null
@@ -1,171 +0,0 @@
-# Listings and formatting functions for various data types in the database.
-
-package VN3::Types;
-
-use strict;
-use warnings;
-use utf8;
-use TUWF ':Html5';
-use POSIX 'strftime', 'ceil';
-use Exporter 'import';
-use VNDB::Types;
-
-our @EXPORT = qw/
- $UID_RE $VID_RE $RID_RE $SID_RE $CID_RE $PID_RE $IID_RE $DOC_RE
- $VREV_RE $RREV_RE $PREV_RE $SREV_RE $CREV_RE $DREV_RE
- Lang
- Platform
- media_display
- ReleaseDate
- vn_length_time vn_length_display
- char_roles char_role_display
- vote_display vote_string
- date_display
- vn_relation_reverse vn_relation_display
- producer_relation_reverse producer_relation_display
- spoil_display
- release_types
- minage_display minage_display_full
- resolution_display_full
- gender_display gender_icon
- blood_type_display
-/;
-
-
-# Regular expressions for use in path registration
-my $num = qr{[1-9][0-9]{0,6}};
-our $UID_RE = qr{u(?<id>$num)};
-our $VID_RE = qr{v(?<id>$num)};
-our $RID_RE = qr{r(?<id>$num)};
-our $SID_RE = qr{s(?<id>$num)};
-our $CID_RE = qr{c(?<id>$num)};
-our $PID_RE = qr{p(?<id>$num)};
-our $IID_RE = qr{i(?<id>$num)};
-our $DOC_RE = qr{d(?<id>$num)};
-our $VREV_RE = qr{$VID_RE(?:\.(?<rev>$num))?};
-our $RREV_RE = qr{$RID_RE(?:\.(?<rev>$num))?};
-our $PREV_RE = qr{$PID_RE(?:\.(?<rev>$num))?};
-our $SREV_RE = qr{$SID_RE(?:\.(?<rev>$num))?};
-our $CREV_RE = qr{$CID_RE(?:\.(?<rev>$num))?};
-our $DREV_RE = qr{$DOC_RE(?:\.(?<rev>$num))?};
-
-
-sub Lang {
- Span class => 'lang-badge', uc $_[0];
-}
-
-
-
-sub Platform {
- # TODO: Icons
- Img class => 'svg-icon', src => tuwf->conf->{url_static}.'/v3/windows.svg', title => $PLATFORM{$_[0]};
-}
-
-
-sub media_display {
- my($media, $qty) = @_;
- my $med = $MEDIUM{$media};
- return $med->{txt} if !$med->{qty};
- sprintf '%d %s', $qty, $qty == 1 ? $med->{txt} : $med->{plural};
-}
-
-
-
-
-sub ReleaseDate {
- 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);
-
- Txt $str if !$future;
- B class => 'future', $str if $future;
-}
-
-
-sub vn_length_time {
- my $l = $VN_LENGTH{$_[0]};
- $l->{time} || $l->{txt};
-}
-
-sub vn_length_display {
- my $l = $VN_LENGTH{$_[0]};
- $l->{txt}.($l->{time} ? " ($l->{time})" : '')
-}
-
-
-
-sub char_role_display {
- my($role, $num) = @_;
- $CHAR_ROLE{$role}{!$num || $num == 1 ? 'txt' : 'plural'};
-}
-
-
-
-sub vote_display {
- !$_[0] ? '-' : $_[0] % 10 == 0 ? $_[0]/10 : sprintf '%.1f', $_[0]/10;
-}
-
-sub vote_string {
- ['worst ever',
- 'awful',
- 'bad',
- 'weak',
- 'so-so',
- 'decent',
- 'good',
- 'very good',
- 'excellent',
- 'masterpiece']->[ceil(shift()/10)-2];
-}
-
-
-
-sub date_display {
- strftime '%Y-%m-%d', gmtime $_[0];
-}
-
-
-
-sub vn_relation_reverse { $VN_RELATION{$_[0]}{reverse} }
-sub vn_relation_display { $VN_RELATION{$_[0]}{txt} }
-
-
-
-sub producer_relation_reverse { $PRODUCER_RELATION{$_[0]}{reverse} }
-sub producer_relation_display { $PRODUCER_RELATION{$_[0]}{txt} }
-
-
-
-sub spoil_display {
- ['No spoilers'
- ,'Minor spoilers'
- ,'Spoil me!']->[$_[0]];
-}
-
-
-
-sub release_types { keys %RELEASE_TYPE }
-
-
-sub minage_display { $AGE_RATING{$_[0]}{txt} }
-sub minage_display_full { my $e = $AGE_RATING{$_[0]}; $e->{txt}.($e->{ex} ? " (e.g. $e->{ex})" : '') };
-
-
-
-sub resolution_display_full { my $e = $RESOLUTION{$_[0]}; ($e->{cat} ? ucfirst "$e->{cat}: " : '').$e->{txt} }
-
-
-sub gender_display { $GENDER{$_[0]} }
-sub gender_icon { +{qw/m ♂ f ♀ mf ♂♀/}->{$_[0]}||'' }
-
-
-
-sub blood_type_display { $BLOOD_TYPE{$_[0]} }
-
-
-1;
diff --git a/lib/VN3/User/Lib.pm b/lib/VN3/User/Lib.pm
deleted file mode 100644
index c63e4286..00000000
--- a/lib/VN3/User/Lib.pm
+++ /dev/null
@@ -1,31 +0,0 @@
-package VN3::User::Lib;
-
-use VN3::Prelude;
-
-our @EXPORT = qw/show_list TopNav/;
-
-
-# Whether we can see the user's list
-sub show_list {
- my $u = shift;
- die "Can't determine show_list() when hide_list preference is not known" if !exists $u->{hide_list};
- auth->permUsermod || !$u->{hide_list} || $u->{id} == (auth->uid||0);
-}
-
-
-sub TopNav {
- my($page, $u) = @_;
-
- Div class => 'nav raised-top-nav', sub {
- Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'details'), sub { A href => "/u$u->{id}", class => 'nav__link', 'Details'; };
- Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'list'), sub { A href => "/u$u->{id}/list", class => 'nav__link', 'List'; } if show_list $u;
- Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'wish'), sub { A href => "/u$u->{id}/wish", class => 'nav__link', 'Wishlist'; } if show_list $u;
- Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'posts'), sub { A href => "/u$u->{id}/posts", class => 'nav__link', 'Posts'; };
- Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'discussions'),sub { A href => "/t/u$u->{id}", class => 'nav__link', 'Discussions'; };
- Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'tags'), sub { A href => "/g/links?uid=$u->{id}", class => 'nav__link', 'Tags'; };
- Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'hist'), sub { A href => "/u$u->{id}/hist", class => 'nav__link', 'Contributions'; };
- };
-}
-
-1;
-
diff --git a/lib/VN3/User/Login.pm b/lib/VN3/User/Login.pm
deleted file mode 100644
index 7660762a..00000000
--- a/lib/VN3/User/Login.pm
+++ /dev/null
@@ -1,50 +0,0 @@
-package VN3::User::Login;
-
-use VN3::Prelude;
-
-# TODO: Redirect to a password change form when a user logs in with an insecure password.
-
-TUWF::get '/u/login' => sub {
- return tuwf->resRedirect('/', 'temp') if auth;
- Framework title => 'Login', center => 1, sub {
- Div 'data-elm-module' => 'User.Login', '';
- };
-};
-
-
-my $elm_Throttled = elm_api 'Throttled';
-my $elm_BadLogin = elm_api 'BadLogin';
-
-json_api '/u/login', {
- username => { username => 1 },
- password => { password => 1 }
-}, sub {
- my $data = shift;
-
- my $conf = tuwf->conf->{login_throttle} || [ 24*3600/10, 24*3600 ];
- my $ip = norm_ip tuwf->reqIP;
-
- my $tm = tuwf->dbVali(
- 'SELECT', sql_totime('greatest(timeout, now())'), 'FROM login_throttle WHERE ip =', \$ip
- ) || time;
-
- return $elm_Throttled->() if $tm-time() > $conf->[1];
- return $elm_Success->() if auth->login($data->{username}, $data->{password});
-
- # Failed login, update throttle.
- my $upd = {
- ip => \$ip,
- timeout => sql_fromtime $tm+$conf->[0]
- };
- tuwf->dbExeci('INSERT INTO login_throttle', $upd, 'ON CONFLICT (ip) DO UPDATE SET', $upd);
- $elm_BadLogin->()
-};
-
-
-TUWF::get qr{/$UID_RE/logout}, sub {
- return tuwf->resNotFound if !auth || auth->uid != tuwf->capture('id');
- auth->logout;
- tuwf->resRedirect('/', 'temp');
-};
-
-1;
diff --git a/lib/VN3/User/Page.pm b/lib/VN3/User/Page.pm
deleted file mode 100644
index 886ad39a..00000000
--- a/lib/VN3/User/Page.pm
+++ /dev/null
@@ -1,207 +0,0 @@
-package VN3::User::Page;
-
-use VN3::Prelude;
-use VN3::User::Lib;
-
-
-sub StatsLeft {
- my $u = shift;
- my $vns = show_list($u) && tuwf->dbVali('SELECT COUNT(*) FROM vnlists WHERE uid =', \$u->{id});
- my $rel = show_list($u) && tuwf->dbVali('SELECT COUNT(*) FROM rlists WHERE uid =', \$u->{id});
- my $posts = tuwf->dbVali('SELECT COUNT(*) FROM threads_posts WHERE uid =', \$u->{id});
- my $threads = tuwf->dbVali('SELECT COUNT(*) FROM threads_posts WHERE num = 1 AND uid =', \$u->{id});
-
- Div class => 'card__title mb-4', 'Stats';
- Div class => 'big-stats mb-5', sub {
- A href => "/u$u->{id}/list", class => 'big-stats__stat', sub {
- Txt 'Votes';
- Div class => 'big-stats__value', show_list($u) ? $u->{c_votes} : '-';
- };
- A href => "/u$u->{id}/hist", class => 'big-stats__stat', sub {
- Txt 'Edits';
- Div class => 'big-stats__value', $u->{c_changes};
- };
- A href => "/g/links?u=$u->{id}", class => 'big-stats__stat', sub {
- Txt 'Tags';
- Div class => 'big-stats__value', $u->{c_tags};
- };
- };
- Div class => 'user-stats__text', sub {
- Dl class => 'dl--horizontal', sub {
- if(show_list $u) {
- Dt 'List stats';
- Dd sprintf '%d release%s of %d visual novel%s', $rel, $rel == 1 ? '' : 's', $vns, $vns == 1 ? '' : 's';
- }
- Dt 'Forum stats';
- Dd sprintf '%d post%s, %d new thread%s', $posts, $posts == 1 ? '' : 's', $threads, $threads == 1 ? '' : 's';
- Dt 'Registered';
- Dd date_display $u->{registered};
- };
- };
-}
-
-
-sub Stats {
- my $u = shift;
-
- my($count, $Graph) = show_list($u) ? VoteGraph u => $u->{id} : ();
-
- Div class => 'card card--white card--no-separators flex-expand mb-5', sub {
- Div class => 'card__section fs-medium', sub {
- Div class => 'user-stats', sub {
- Div class => 'user-stats__left', sub { StatsLeft $u };
- Div class => 'user-stats__right', sub {
- Div class => 'card__title mb-2', 'Vote distribution';
- $Graph->();
- } if $count;
- }
- }
- }
-}
-
-
-sub List {
- my $u = shift;
- return if !show_list $u;
-
- # XXX: This query doesn't catch vote or list *changes*, only new entries.
- # We don't store the modification date in the DB at the moment.
- my $l = tuwf->dbAlli(q{
- SELECT il.vid, EXTRACT('epoch' FROM GREATEST(v.date, l.added)) AS date, vn.title, vn.original, v.vote, l.status
- FROM (
- SELECT vid FROM votes WHERE uid = }, \$u->{id}, q{
- UNION SELECT vid FROM vnlists WHERE uid = }, \$u->{id}, q{
- ) AS il (vid)
- LEFT JOIN votes v ON v.vid = il.vid
- LEFT JOIN vnlists l ON l.vid = il.vid
- JOIN vn ON vn.id = il.vid
- WHERE v.uid = }, \$u->{id}, q{
- AND l.uid = }, \$u->{id}, q{
- ORDER BY GREATEST(v.date, l.added) DESC
- LIMIT 10
- });
- return if !@$l;
-
- Div class => 'card card--white card--no-separators mb-5', sub {
- Div class => 'card__header', sub {
- Div class => 'card__title', 'Recent list additions';
- };
- Table class => 'table table--responsive-single-sm fs-medium', sub {
- Thead sub {
- Tr sub {
- Th width => '15%', 'Date';
- Th width => '50%', 'Visual novel';
- Th width => '10%', 'Vote';
- Th width => '25%', 'Status';
- };
- };
- Tbody sub {
- for my $i (@$l) {
- Tr sub {
- Td class => 'tabular-nums muted', date_display $i->{date};
- Td sub {
- A href => "/v$i->{vid}", title => $i->{original}||$i->{title}, $i->{title};
- };
- Td vote_display $i->{vote};
- Td $i->{status} ? $VNLIST_STATUS{$i->{status}} : '';
- };
- }
- };
- };
- Div class => 'card__section fs-medium', sub {
- A href => "/u$u->{id}/list", 'View full list';
- }
- };
-}
-
-
-sub Edits {
- my $u = shift;
- # XXX: This is a lazy implementation, could probably share code/UI with the database entry history tables (as in VNDB 2)
-
- my $l = tuwf->dbAlli(q{
- SELECT ch.id, ch.itemid, ch.rev, ch.type, EXTRACT('epoch' FROM ch.added) AS added
- FROM changes ch
- WHERE ch.requester =}, \$u->{id}, q{
- ORDER BY ch.added DESC LIMIT 10
- });
- return if !@$l;
-
- # This can also be written as a UNION, haven't done any benchmarking yet.
- # It doesn't matter much with only 10 entries, but it will matter if this
- # query is re-used for other history browsing purposes.
- enrich id => q{
- SELECT ch.id, COALESCE(d.title, v.title, p.name, r.title, c.name, sa.name) AS title
- FROM changes ch
- LEFT JOIN docs_hist d ON ch.type = 'd' AND d.chid = ch.id
- LEFT JOIN vn_hist v ON ch.type = 'v' AND v.chid = ch.id
- LEFT JOIN producers_hist p ON ch.type = 'p' AND p.chid = ch.id
- LEFT JOIN releases_hist r ON ch.type = 'r' AND r.chid = ch.id
- LEFT JOIN chars_hist c ON ch.type = 'c' AND c.chid = ch.id
- LEFT JOIN staff_hist s ON ch.type = 's' AND s.chid = ch.id
- LEFT JOIN staff_alias_hist sa ON ch.type = 's' AND sa.chid = ch.id AND s.aid = sa.aid
- WHERE ch.id IN}, $l;
-
- Div class => 'card card--white card--no-separators mb-5', sub {
- Div class => 'card__header', sub {
- Div class => 'card__title', 'Recent database contributions';
- };
- Table class => 'table table--responsive-single-sm fs-medium', sub {
- Thead sub {
- Tr sub {
- Th width => '15%', 'Date';
- Th width => '10%', 'Rev.';
- Th width => '75%', 'Entry';
- };
- };
- Tbody sub {
- for my $i (@$l) {
- my $id = "$i->{type}$i->{itemid}.$i->{rev}";
- Tr sub {
- Td class => 'tabular-nums muted', date_display $i->{added};
- Td sub {
- A href => "/$id", $id;
- };
- Td sub {
- A href => "/$id", $i->{title};
- };
- }
- }
- }
- };
- Div class => 'card__section fs-medium', sub {
- A href => "/u$u->{id}/hist", 'View all';
- }
- };
-}
-
-
-TUWF::get qr{/$UID_RE}, sub {
- my $uid = tuwf->capture('id');
- my $u = tuwf->dbRowi(q{
- SELECT u.id, u.username, EXTRACT('epoch' FROM u.registered) AS registered, u.c_votes, u.c_changes, u.c_tags, hd.value AS hide_list
- FROM users u
- LEFT JOIN users_prefs hd ON hd.uid = u.id AND hd.key = 'hide_list'
- WHERE u.id =}, \$uid
- );
- return tuwf->resNotFound if !$u->{id};
-
- Framework
- title => lcfirst($u->{username}),
- index => 0,
- single_col => 1,
- top => sub {
- Div class => 'col-md', sub {
- EntryEdit u => $u;
- Div class => 'detail-page-title', ucfirst $u->{username};
- TopNav details => $u;
- }
- },
- sub {
- Stats $u;
- List $u;
- Edits $u;
- };
-};
-
-1;
diff --git a/lib/VN3/User/RegReset.pm b/lib/VN3/User/RegReset.pm
deleted file mode 100644
index ed815547..00000000
--- a/lib/VN3/User/RegReset.pm
+++ /dev/null
@@ -1,137 +0,0 @@
-# User registration and password reset. These functions share some common code.
-package VN3::User::RegReset;
-
-use VN3::Prelude;
-
-
-TUWF::get '/u/newpass' => sub {
- return tuwf->resRedirect('/', 'temp') if auth;
- Framework title => 'Password reset', center => 1, sub {
- Div 'data-elm-module' => 'User.PassReset', '';
- };
-};
-
-
-my $elm_BadEmail = elm_api 'BadEmail';
-my $elm_BadPass = elm_api 'BadPass';
-my $elm_Bot = elm_api 'Bot';
-my $elm_Taken = elm_api 'Taken';
-my $elm_DoubleEmail = elm_api 'DoubleEmail';
-my $elm_DoubleIP = elm_api 'DoubleIP';
-
-
-json_api '/u/newpass', {
- email => { email => 1 },
-}, sub {
- my $data = shift;
-
- my($id, $token) = auth->resetpass($data->{email});
- return $elm_BadEmail->() if !$id;
-
- my $name = tuwf->dbVali('SELECT username FROM users WHERE id =', \$id);
- my $body = sprintf
- "Hello %s,"
- ."\n\n"
- ."Your VNDB.org login has been disabled, you can now set a new password by following the link below:"
- ."\n\n"
- ."%s"
- ."\n\n"
- ."Now don't forget your password again! :-)"
- ."\n\n"
- ."vndb.org",
- $name, tuwf->reqBaseURI()."/u$id/setpass/$token";
-
- tuwf->mail($body,
- To => $data->{email},
- From => 'VNDB <noreply@vndb.org>',
- Subject => "Password reset for $name",
- );
- $elm_Success->();
-};
-
-
-my $reset_url = qr{/$UID_RE/setpass/(?<token>[a-f0-9]{40})};
-
-TUWF::get $reset_url, sub {
- return tuwf->resRedirect('/', 'temp') if auth;
-
- my $id = tuwf->capture('id');
- my $token = tuwf->capture('token');
- my $name = tuwf->dbVali('SELECT username FROM users WHERE id =', \$id);
-
- return tuwf->resNotFound if !$name || !auth->isvalidtoken($id, $token);
-
- Framework title => 'Set password', center => 1, sub {
- Div 'data-elm-module' => 'User.PassSet', 'data-elm-flags' => '"'.tuwf->reqPath().'"', '';
- };
-};
-
-
-json_api $reset_url, {
- pass => { password => 1 },
-}, sub {
- my $data = shift;
- my $id = tuwf->capture('id');
- my $token = tuwf->capture('token');
-
- return $elm_BadPass->() if tuwf->isUnsafePass($data->{pass});
- die "Invalid reset token" if !auth->setpass($id, $token, undef, $data->{pass});
- tuwf->dbExeci('UPDATE users SET email_confirmed = true WHERE id =', \$id);
- $elm_Success->()
-};
-
-
-TUWF::get '/u/register', sub {
- return tuwf->resRedirect('/', 'temp') if auth;
- Framework title => 'Register', center => 1, sub {
- Div 'data-elm-module' => 'User.Register', '';
- };
-};
-
-
-json_api '/u/register', {
- username => { username => 1 },
- email => { email => 1 },
- vns => { int => 1 },
-}, sub {
- my $data = shift;
-
- my $num = tuwf->dbVali("SELECT count FROM stats_cache WHERE section = 'vn'");
- return $elm_Bot->() if $data->{vns} < $num*0.995 || $data->{vns} > $num*1.005;
- return $elm_Taken->() if tuwf->dbVali('SELECT 1 FROM users WHERE username =', \$data->{username});
- return $elm_DoubleEmail->() if tuwf->dbVali(select => sql_func user_emailexists => \$data->{email});
-
- my $ip = tuwf->reqIP;
- return $elm_DoubleIP->() if tuwf->dbVali(
- q{SELECT 1 FROM users WHERE registered >= NOW()-'1 day'::interval AND ip <<},
- $ip =~ /:/ ? \"$ip/48" : \"$ip/30"
- );
-
- my $id = tuwf->dbVali('INSERT INTO users', {
- username => $data->{username},
- mail => $data->{email},
- ip => $ip,
- }, 'RETURNING id');
- my(undef, $token) = auth->resetpass($data->{email});
-
- my $body = sprintf
- "Hello %s,"
- ."\n\n"
- ."Someone has registered an account on VNDB.org with your email address. To confirm your registration, follow the link below."
- ."\n\n"
- ."%s"
- ."\n\n"
- ."If you don't remember creating an account on VNDB.org recently, please ignore this e-mail."
- ."\n\n"
- ."vndb.org",
- $data->{username}, tuwf->reqBaseURI()."/u$id/setpass/$token";
-
- tuwf->mail($body,
- To => $data->{email},
- From => 'VNDB <noreply@vndb.org>',
- Subject => "Confirm registration for $data->{username}",
- );
- $elm_Success->()
-};
-
-1;
diff --git a/lib/VN3/User/Settings.pm b/lib/VN3/User/Settings.pm
deleted file mode 100644
index a63de232..00000000
--- a/lib/VN3/User/Settings.pm
+++ /dev/null
@@ -1,98 +0,0 @@
-package VN3::User::Settings;
-
-use VN3::Prelude;
-
-
-my $FORM = {
- username => { username => 1 },
- mail => { email => 1 },
- perm => { uint => 1, func => sub { ($_[0] & ~auth->allPerms) == 0 } },
- ign_votes => { anybool => 1 },
- hide_list => { anybool => 1 },
- show_nsfw => { anybool => 1 },
- traits_sexual => { anybool => 1 },
- tags_all => { anybool => 1 },
- tags_cont => { anybool => 1 },
- tags_ero => { anybool => 1 },
- tags_tech => { anybool => 1 },
- spoilers => { uint => 1, range => [ 0, 2 ] },
-
- password => { _when => 'in', required => 0, type => 'hash', keys => {
- old => { password => 1 },
- new => { password => 1 }
- } },
-
- id => { _when => 'out', uint => 1 },
- authmod => { _when => 'out', anybool => 1 },
-};
-
-my $FORM_OUT = form_compile out => $FORM;
-my $FORM_IN = form_compile in => $FORM;
-
-elm_form UserEdit => $FORM_OUT, $FORM_IN;
-
-my $elm_BadPass = elm_api 'BadPass';
-my $elm_BadLogin = elm_api 'BadLogin';
-
-TUWF::get qr{/$UID_RE/edit}, sub {
- my $u = tuwf->dbRowi('SELECT id, username, perm, ign_votes FROM users WHERE id =', \tuwf->capture('id'));
-
- return tuwf->resNotFound if !can_edit u => $u;
-
- $u->{mail} = tuwf->dbVali(select => sql_func user_getmail => \$u->{id}, \auth->uid, sql_fromhex auth->token);
- $u->{authmod} = auth->permUsermod;
-
- # Let's not disclose this (though it's not hard to find out through other means)
- if(!auth->permUsermod) {
- $u->{ign_votes} = 0;
- $u->{perm} = auth->defaultPerms;
- }
-
- my $prefs = { map +($_->{key}, $_->{value}), @{ tuwf->dbAlli('SELECT key, value FROM users_prefs WHERE uid =', \$u->{id}) }};
- $u->{$_} = $prefs->{$_}||'' for qw/hide_list show_nsfw traits_sexual tags_all spoilers/;
- $u->{spoilers} ||= 0;
- $u->{"tags_$_"} = (($prefs->{tags_cat}||'cont,tech') =~ /$_/) for qw/cont ero tech/;
-
- my $title = $u->{id} == auth->uid ? 'My Preferences' : "Edit $u->{username}";
- Framework title => $title, noindex => 1, narrow => 1, sub {
- FullPageForm module => 'User.Settings', data => $u, schema => $FORM_OUT;
- };
-};
-
-
-json_api qr{/$UID_RE/edit}, $FORM_IN, sub {
- my $data = shift;
- my $id = tuwf->capture('id');
-
- return $elm_Unauth->() if !can_edit u => { id => $id };
-
- if(auth->permUsermod) {
- tuwf->dbExeci(update => users => set => {
- username => $data->{username},
- ign_votes => $data->{ign_votes},
- email_confirmed => 1,
- }, where => { id => $id });
- tuwf->dbExeci(select => sql_func user_setperm => \$id, \auth->uid, sql_fromhex(auth->token), \$data->{perm});
- }
-
- if($data->{password}) {
- return $elm_BadPass->() if tuwf->isUnsafePass($data->{password}{new});
-
- if(auth->uid == $id) {
- return $elm_BadLogin->() if !auth->setpass($id, undef, $data->{password}{old}, $data->{password}{new});
- } else {
- tuwf->dbExeci(select => sql_func user_admin_setpass => \$id, \auth->uid,
- sql_fromhex(auth->token), sql_fromhex auth->_preparepass($data->{password}{new})
- );
- }
- }
-
- tuwf->dbExeci(select => sql_func user_setmail => \$id, \auth->uid, sql_fromhex(auth->token), \$data->{mail});
-
- auth->prefSet($_, $data->{$_}, $id) for qw/hide_list show_nsfw traits_sexual tags_all spoilers/;
- auth->prefSet(tags_cat => join(',', map $data->{"tags_$_"} ? $_ : (), qw/cont ero tech/), $id);
-
- $elm_Success->();
-};
-
-1;
diff --git a/lib/VN3/User/VNList.pm b/lib/VN3/User/VNList.pm
deleted file mode 100644
index 922f81d6..00000000
--- a/lib/VN3/User/VNList.pm
+++ /dev/null
@@ -1,325 +0,0 @@
-package VN3::User::VNList;
-
-use POSIX 'ceil';
-use VN3::Prelude;
-use VN3::User::Lib;
-
-
-sub mkurl {
- my $opt = shift;
- $opt = { %$opt, @_ };
- delete $opt->{t} if $opt->{t} == -1;
- delete $opt->{g} if !$opt->{g};
- '?'.join ';', map "$_=$opt->{$_}", sort keys %$opt;
-}
-
-
-sub SideBar {
- my $opt = shift;
-
- Div class => 'fixed-size-left-sidebar-xl', sub {
- Div class => 'vertical-selector-label', 'Status';
- Div class => 'vertical-selector', sub {
- for (-1, keys %VNLIST_STATUS) {
- A href => mkurl($opt, t => $_, p => 1), mkclass(
- 'vertical-selector__item' => 1,
- 'vertical-selector__item--active' => $_ == $opt->{t}
- ), $_ < 0 ? 'All' : $VNLIST_STATUS{$_};
- }
- };
- };
-}
-
-
-sub NextPrev {
- my($opt, $count) = @_;
- my $numpage = ceil($count/50);
-
- Div class => 'd-lg-flex jc-between align-items-center', sub {
- Div class => 'd-flex align-items-center', '';
- Div class => 'd-block d-lg-none mb-2', '';
- Div class => 'd-flex jc-right align-items-center', sub {
- A href => mkurl($opt, p => $opt->{p}-1), mkclass(btn => 1, 'btn--disabled' => $opt->{p} <= 1), '< Prev';
- Div class => 'mx-3 semi-muted', sprintf 'page %d of %d', $opt->{p}, $numpage;
- A href => mkurl($opt, p => $opt->{p}+1), mkclass(btn => 1, 'btn--disabled' => $opt->{p} >= $numpage), 'Next >';
- };
- };
-}
-
-
-sub EditDropDown {
- my($u, $opt, $item) = @_;
- return if $u->{id} != (auth->uid||0);
- Div 'data-elm-module' => 'UVNList.Options',
- 'data-elm-flags' => JSON::XS->new->encode({uid => $u->{id}, item => $item}),
- '';
-}
-
-
-sub VNTable {
- my($u, $lst, $opt) = @_;
-
- my $SortHeader = sub {
- my($id, $label) = @_;
- my $isasc = $opt->{s} eq $id && $opt->{o} eq 'a';
- A mkclass(
- 'table-header' => 1,
- 'with-sort-icon' => 1,
- 'with-sort-icon--down' => !$isasc,
- 'with-sort-icon--up' => $isasc,
- 'with-sort-icon--active' => $opt->{s} eq $id,
- ), href => mkurl($opt, p => 1, s => $id, o => $isasc ? 'd' : 'a'), $label;
- };
-
- Table class => 'table table--responsive-single-sm fs-medium vn-list', sub {
- Thead sub {
- Tr sub {
- Th width => '15%', class => 'th--nopad', sub { $SortHeader->(date => 'Date' ) };
- Th width => '40%', class => 'th--nopad', sub { $SortHeader->(title => 'Title') };
- Th width => '10%', class => 'th--nopad', sub { $SortHeader->(vote => 'Vote' ) };
- Th width => '13%', 'Status';
- Th width => '7.33%', '';
- Th width => '7.33%', '';
- Th width => '7.33%', '';
- };
- };
- Tbody sub {
- for my $l (@$lst) {
- Tr sub {
- Td class => 'tabular-nums muted', date_display $l->{date};
- Td sub {
- A href => "/v$l->{id}", title => $l->{original}||$l->{title}, $l->{title};
- };
-
- if($u->{id} == (auth->uid||0)) {
- Td class => 'table-edit-overlay-base', sub {
- Div 'data-elm-module' => 'UVNList.Vote',
- 'data-elm-flags' => JSON::XS->new->encode({uid => int $u->{id}, vid => int $l->{id}, vote => ''.vote_display $l->{vote}}),
- vote_display $l->{vote};
- };
- Td class => 'table-edit-overlay-base', sub {
- Div 'data-elm-module' => 'UVNList.Status',
- 'data-elm-flags' => JSON::XS->new->encode({uid => int $u->{id}, vid => int $l->{id}, status => int $l->{status}||0}),
- $VNLIST_STATUS{$l->{status}||0};
- };
- } else {
- Td vote_display $l->{vote};
- Td $VNLIST_STATUS{$l->{status}||0};
- }
-
- # Release info
- Td sub {
- A href => 'javascript:;', class => 'vn-list__expand-releases', sub {
- Span class => 'expand-arrow mr-2', '';
- Txt sprintf '%d/%d', (scalar grep $_->{status}==2, @{$l->{rel}}), scalar @{$l->{rel}};
- } if @{$l->{rel}};
- };
-
- # Notes
- Td sub {
- # TODO: vn-list__expand-comment--empty for 'add comment' things
- A href => 'javascript:;', class => 'vn-list__expand-comment', sub {
- Span class => 'expand-arrow mr-2', '';
- Img class => 'svg-icon', src => tuwf->conf->{url_static}.'/v3/heavy/comment.svg';
- } if $l->{notes};
- };
-
- Td sub { EditDropDown $u, $opt, $l };
- };
-
- # Release info
- Tr class => 'vn-list__releases-row d-none', sub {
- Td colspan => '6', sub {
- Div class => 'vn-list__releases', sub {
- Table class => 'table table--responsive-single-sm ml-3', sub {
- Tbody sub {
- for my $r (@{$l->{rel}}) {
- Tr sub {
- Td width => '15%', class => 'tabular-nums muted pl-0', date_display $r->{date};
- Td width => '50%', sub {
- A href => "/v$r->{rid}", title => $r->{original}||$r->{title}, $r->{title};
- };
- # TODO: Editabe
- Td width => '20%', $RLIST_STATUS{$l->{status}};
- Td width => '15%', ''; # TODO: Edit menu
- }
- }
- }
- }
- }
- }
- } if @{$l->{rel}};
-
- # Notes
- Tr class => 'vn-list__comment-row d-none', sub {
- Td colspan => '6', sub {
- # TODO: Editable
- Div class => 'vn-list__comment ml-3', $l->{notes};
- }
- } if $l->{notes};
- };
- };
- };
-}
-
-
-sub VNGrid {
- my($u, $lst, $opt) = @_;
-
- Div class => 'vn-grid mb-4', sub {
- for my $l (@$lst) {
- Div class => 'vn-grid__item', sub {
- # TODO: NSFW hiding? What about missing images?
- Div class => 'vn-grid__item-bg', style => sprintf("background-image: url('%s')", tuwf->imgurl(cv => $l->{image})), '';
- Div class => 'vn-grid__item-overlay', sub {
- A href => 'javascript:;', class => 'vn-grid__item-link', ''; # TODO: Open modal on click
- Div class => 'vn-grid__item-top', sub {
- EditDropDown $u, $opt, $l;
- Div class => 'vn-grid__item-rating', sub {
- Img class => 'svg-icon', src => tuwf->conf->{url_static}.'/v3/heavy/comment.svg' if $l->{notes};
- Lit ' ';
- Txt vote_display $l->{vote};
- }
- };
- Div class => 'vn-grid__item-name', $l->{title};
- }
- }
- }
- }
-}
-
-
-sub List {
- my($u, $opt) = @_;
-
- my $lst = tuwf->dbAlli(q{
- SELECT v.id, v.title, v.original, vl.status, vl.notes, vo.vote, v.image, },
- sql_totime('LEAST(vl.added, vo.date)'), q{AS date,
- count(*) OVER() AS full_count
- FROM vn v
- LEFT JOIN votes vo ON vo.vid = v.id AND vo.uid =}, \$u->{id}, q{
- LEFT JOIN vnlists vl ON vl.vid = v.id AND vl.uid =}, \$u->{id}, q{
- WHERE }, sql_and(
- 'vo.vid IS NOT NULL OR vl.vid IS NOT NULL',
- $opt->{t} >= 1 ? sql('vl.status =', \$opt->{t}) : $opt->{t} == 0 ? 'vl.status = 0 OR vl.status IS NULL' : ()
- ),
- 'ORDER BY', {
- title => 'v.title',
- date => 'LEAST(vl.added, vo.date)',
- vote => 'vo.vote',
- }->{$opt->{s}},
- $opt->{o} eq 'a' ? 'ASC' : 'DESC',
- 'NULLS LAST',
- 'LIMIT', \50,
- 'OFFSET', \(($opt->{p}-1)*50)
- );
- my $count = @$lst ? $lst->[0]{full_count} : 0;
- delete $_->{full_count} for @$lst;
-
- enrich_list rel => id => vid => sub { sql q{
- SELECT rv.vid, rl.rid, rl.status, r.title, r.original, }, sql_totime('rl.added'), q{ AS date
- FROM rlists rl
- JOIN releases r ON r.id = rl.rid
- JOIN releases_vn rv ON rv.id = r.id
- WHERE rl.uid =}, \$u->{id}, q{AND rv.vid IN}, $_[0]
- }, $lst;
-
- Div class => 'col-md', sub {
- Div class => 'card card--white card--no-separators mb-5', sub {
- Div class => 'card__header', sub {
- Div class => 'card__title', 'List';
- Debug $lst;
- Div class => 'card__header-buttons', sub {
- Div class => 'btn-group', sub {
- A href => mkurl($opt, g => 0), mkclass(btn => 1, active => !$opt->{g}, 'js-show-vn-list' => 1), \&ListIcon;
- A href => mkurl($opt, g => 1), mkclass(btn => 1, active => $opt->{g}, 'js-show-vn-grid' => 1), \&GridIcon;
- };
- };
- };
-
- VNTable $u, $lst, $opt unless $opt->{g};
- Div class => 'card__body fs-medium', sub {
- VNGrid $u, $lst, $opt if $opt->{g};
- NextPrev $opt, $count;
- };
- }
- };
-}
-
-
-TUWF::get qr{/$UID_RE/list}, sub {
- my $uid = tuwf->capture('id');
- my $u = tuwf->dbRowi(q{
- SELECT u.id, u.username, hd.value AS hide_list
- FROM users u
- LEFT JOIN users_prefs hd ON hd.uid = u.id AND hd.key = 'hide_list'
- WHERE u.id =}, \$uid
- );
- return tuwf->resNotFound if !$u->{id} || !show_list $u;
-
- my $opt = tuwf->validate(get =>
- t => { vnlist_status => 1, required => 0, default => -1 }, # status
- p => { page => 1 }, # page
- o => { enum => ['d','a'], required => 0, default => 'a' }, # order (asc/desc)
- s => { enum => ['title', 'date', 'vote'], required => 0, default => 'title' }, # sort column
- g => { anybool => 1 }, # grid
- )->data;
-
- Framework
- title => $u->{username},
- index => 0,
- top => sub {
- Div class => 'col-md', sub {
- Div class => 'detail-page-title', ucfirst $u->{username};
- TopNav list => $u;
- }
- },
- sub {
- Div class => 'row', sub {
- SideBar $opt;
- List $u, $opt;
- };
- };
-};
-
-
-json_api '/u/setvote', {
- uid => { id => 1 },
- vid => { id => 1 },
- vote => { vnvote => 1 }
-}, sub {
- my $data = shift;
- return $elm_Unauth->() if (auth->uid||0) != $data->{uid};
-
- tuwf->dbExeci(
- 'DELETE FROM votes WHERE',
- { vid => $data->{vid}, uid => $data->{uid} }
- ) if !$data->{vote};
-
- tuwf->dbExeci(
- 'INSERT INTO votes',
- { vid => $data->{vid}, uid => $data->{uid}, vote => $data->{vote} },
- 'ON CONFLICT (vid, uid) DO UPDATE SET',
- { vote => $data->{vote} }
- ) if $data->{vote};
-
- $elm_Success->()
-};
-
-
-json_api '/u/setvnstatus', {
- uid => { id => 1 },
- vid => { id => 1 },
- status => { vnlist_status => 1 }
-}, sub {
- my $data = shift;
- return $elm_Unauth->() if (auth->uid||0) != $data->{uid};
-
- tuwf->dbExeci(
- 'INSERT INTO vnlists',
- { vid => $data->{vid}, uid => $data->{uid}, status => $data->{status} },
- 'ON CONFLICT (vid, uid) DO UPDATE SET',
- { status => $data->{status} }
- );
- $elm_Success->();
-};
diff --git a/lib/VN3/VN/Edit.pm b/lib/VN3/VN/Edit.pm
deleted file mode 100644
index bee48a5f..00000000
--- a/lib/VN3/VN/Edit.pm
+++ /dev/null
@@ -1,187 +0,0 @@
-package VN3::VN::Edit;
-
-use VN3::Prelude;
-use VN3::VN::Lib;
-
-
-my $FORM = {
- alias => { required => 0, default => '', maxlength => 500 },
- anime => { maxlength => 50, sort_keys => 'aid', aoh =>{
- aid => { id => 1 }
- } },
- desc => { required => 0, default => '', maxlength => 10240 },
- image => { required => 0, default => 0, id => 1 }, # X
- img_nsfw => { anybool => 1 },
- hidden => { anybool => 1 },
- l_encubed => { required => 0, default => '', maxlength => 100 },
- l_renai => { required => 0, default => '', maxlength => 100 },
- l_wp => { required => 0, default => '', maxlength => 150 },
- length => { vn_length => 1 },
- locked => { anybool => 1 },
- original => { required => 0, default => '', maxlength => 250 },
- relations => { maxlength => 50, sort_keys => 'vid', aoh => {
- vid => { id => 1 }, # X
- relation => { vn_relation => 1 },
- official => { anybool => 1 },
- title => { _when => 'out' },
- } },
- screenshots => { maxlength => 10, sort_keys => 'scr', aoh => {
- scr => { id => 1 }, # X
- rid => { id => 1 }, # X
- nsfw => { anybool => 1 },
- width => { _when => 'out', uint => 1 },
- height => { _when => 'out', uint => 1 },
- } },
- seiyuu => { sort_keys => ['aid','cid'], aoh => {
- aid => { id => 1 }, # X
- cid => { id => 1 }, # X
- note => { required => 0, default => '', maxlength => 250 },
- id => { _when => 'out', id => 1 },
- name => { _when => 'out' },
- } },
- staff => { sort_keys => ['aid','role'], aoh => {
- aid => { id => 1 }, # X
- role => { staff_role => 1 },
- note => { required => 0, default => '', maxlength => 250 },
- id => { _when => 'out', id => 1 },
- name => { _when => 'out' },
- } },
- title => { maxlength => 250 },
-
- id => { _when => 'out', required => 0, id => 1 },
- authmod => { _when => 'out', anybool => 1 },
- editsum => { _when => 'in out', editsum => 1 },
- chars => { _when => 'out', aoh => {
- id => { id => 1 },
- name => {},
- } },
- releases => { _when => 'out', aoh => {
- id => { id => 1 },
- title => {},
- original => {},
- display => {},
- resolution=> {},
- } },
-};
-
-my $FORM_OUT = form_compile out => $FORM;
-my $FORM_IN = form_compile in => $FORM;
-my $FORM_CMP = form_compile cmp => $FORM;
-
-elm_form VNEdit => $FORM_OUT, $FORM_IN;
-
-
-TUWF::get qr{/$VREV_RE/edit} => sub {
- my $vn = entry v => tuwf->capture('id'), tuwf->capture('rev') or return tuwf->resNotFound;
- return tuwf->resDenied if !can_edit v => $vn;
-
- enrich aid => q{SELECT id, aid, name FROM staff_alias WHERE aid IN} => $vn->{staff}, $vn->{seiyuu};
- enrich vid => q{SELECT id AS vid, title FROM vn WHERE id IN} => $vn->{relations};
- enrich scr => q{SELECT id AS scr, width, height FROM screenshots WHERE id IN}, $vn->{screenshots};
- $vn->{chars} = tuwf->dbAlli('SELECT id, name FROM chars c WHERE id IN(SELECT id FROM chars_vns WHERE vid =', \$vn->{id}, ') ORDER BY name');
-
- $vn->{releases} = tuwf->dbAlli('SELECT id, title, original, resolution FROM releases WHERE id IN(SELECT id FROM releases_vn WHERE vid =', \$vn->{id}, ') ORDER BY id');
- enrich_list1 lang => id => id => q{SELECT id, lang FROM releases_lang WHERE id IN}, $vn->{releases};
- $_->{display} = sprintf '[%s] %s (r%d)', join(',', @{ delete $_->{lang} }), $_->{title}, $_->{id} for @{$vn->{releases}};
-
- $vn->{authmod} = auth->permDbmod;
- $vn->{editsum} = $vn->{chrev} == $vn->{maxrev} ? '' : "Reverted to revision v$vn->{id}.$vn->{chrev}";
-
- Framework index => 0, title => "Edit $vn->{title}",
- top => sub {
- Div class => 'col-md', sub {
- EntryEdit v => $vn;
- Div class => 'detail-page-title', sub {
- Txt $vn->{title};
- Debug $vn;
- };
- TopNav edit => $vn;
- };
- }, sub {
- FullPageForm module => 'VNEdit.Main', data => $vn, schema => $FORM_OUT, sections => [
- general => 'General info',
- staff => 'Staff',
- cast => 'Cast',
- relations => 'Relations',
- screenshots => 'Screenshots',
- ];
- };
-};
-
-
-TUWF::get '/v/add', sub {
- return tuwf->resDenied if !auth->permEdit;
- Framework index => 0, title => 'Add a new visual novel', narrow => 1, sub {
- Div class => 'row', sub {
- Div class => 'col-md col-md--1', sub { Div 'data-elm-module' => 'VNEdit.New', '' };
- };
- };
-};
-
-
-json_api qr{/(?:$VID_RE/edit|v/add)}, $FORM_IN, sub {
- my $data = shift;
- my $new = !tuwf->capture('id');
- my $vn = $new ? { id => 0 } : entry v => tuwf->capture('id') or return tuwf->resNotFound;
-
- return $elm_Unauth->() if !can_edit v => $vn;
-
- if(!auth->permDbmod) {
- $data->{hidden} = $vn->{hidden}||0;
- $data->{locked} = $vn->{locked}||0;
- }
-
- # Elm doesn't actually verify this one
- die "Image not found" if $data->{image} && !-e tuwf->imgpath(cv => $data->{image});
-
- die "Relation with self" if grep $_->{vid} == $vn->{id}, @{$data->{relations}};
- validate_dbid 'SELECT id FROM vn WHERE id IN', map $_->{vid}, @{$data->{relations}};
- validate_dbid 'SELECT id FROM screenshots WHERE id IN', map $_->{scr}, @{$data->{screenshots}};
- validate_dbid sql('SELECT DISTINCT id FROM releases_vn WHERE vid =', \$vn->{id}, ' AND id IN'), map $_->{rid}, @{$data->{screenshots}};
- validate_dbid 'SELECT aid FROM staff_alias WHERE aid IN', map $_->{aid}, @{$data->{seiyuu}}, @{$data->{staff}};
- validate_dbid sql('SELECT DISTINCT id FROM chars_vns WHERE vid =', \$vn->{id}, ' AND id IN'), map $_->{cid}, @{$data->{seiyuu}};
-
- $data->{desc} = bb_subst_links $data->{desc};
- return $elm_Unchanged->() if !$new && !form_changed $FORM_CMP, $data, $vn;
-
- my($id,undef,$rev) = update_entry v => $vn->{id}, $data;
-
- update_reverse($id, $rev, $vn, $data);
-
- $elm_Changed->($id, $rev);
-};
-
-
-sub update_reverse {
- my($id, $rev, $old, $new) = @_;
-
- my %old = map +($_->{vid}, $_), $old->{relations} ? @{$old->{relations}} : ();
- my %new = map +($_->{vid}, $_), @{$new->{relations}};
-
- # Updates to be performed, vid => { vid => x, relation => y, official => z } or undef if the relation should be removed.
- my %upd;
-
- for my $i (keys %old, keys %new) {
- if($old{$i} && !$new{$i}) {
- $upd{$i} = undef;
- } elsif(!$old{$i} || $old{$i}{relation} ne $new{$i}{relation} || !$old{$i}{official} != !$new{$i}{official}) {
- $upd{$i} = {
- vid => $id,
- relation => vn_relation_reverse($new{$i}{relation}),
- official => $new{$i}{official}
- };
- }
- }
-
- for my $i (keys %upd) {
- my $v = entry v => $i;
- $v->{relations} = [
- $upd{$i} ? $upd{$i} : (),
- grep $_->{vid} != $id, @{$v->{relations}}
- ];
- $v->{editsum} = "Reverse relation update caused by revision v$id.$rev";
- update_entry v => $i, $v, 1;
- }
-}
-
-1;
diff --git a/lib/VN3/VN/JS.pm b/lib/VN3/VN/JS.pm
deleted file mode 100644
index ec98b768..00000000
--- a/lib/VN3/VN/JS.pm
+++ /dev/null
@@ -1,46 +0,0 @@
-package VN3::VN::JS;
-
-use VN3::Prelude;
-
-
-my $elm_VNResult = elm_api VNResult => { aoh => {
- id => { id => 1 },
- title => {},
- original => {},
- hidden => { anybool => 1 },
-}};
-
-
-json_api '/js/vn.json', {
- search => { type => 'array', scalar => 1, minlength => 1, values => { maxlength => 500 } },
- hidden => { anybool => 1 }
-}, sub {
- my $data = shift;
-
- my $r = tuwf->dbAlli(
- 'SELECT v.id, v.title, v.original, v.hidden',
- 'FROM (', (sql_join 'UNION ALL', map {
- my $qs = s/[%_]//gr;
- my @q = normalize_query $_;
- +(
- # ID search
- /^$VID_RE$/ ? (sql 'SELECT 1, id FROM vn WHERE id =', \"$1") : (),
- # prefix match
- sql('SELECT 2, id FROM vn WHERE title ILIKE', \"$qs%"),
- # substring match
- @q ? (sql 'SELECT 3, id FROM vn WHERE', sql_and map sql('c_search ILIKE', \"%$_%"), @q) : ()
- )
- } @{$data->{search}}),
- ') AS vt (ord, id)',
- 'JOIN vn v ON v.id = vt.id',
- $data->{hidden} ? () : ('WHERE NOT v.hidden'),
- 'GROUP BY v.id, v.title, v.original',
- 'ORDER BY MIN(vt.ord), v.title',
- 'LIMIT 20'
- );
-
- $elm_VNResult->($r);
-};
-
-1;
-
diff --git a/lib/VN3/VN/Lib.pm b/lib/VN3/VN/Lib.pm
deleted file mode 100644
index 9571cef8..00000000
--- a/lib/VN3/VN/Lib.pm
+++ /dev/null
@@ -1,20 +0,0 @@
-package VN3::VN::Lib;
-
-use VN3::Prelude;
-
-our @EXPORT = qw/TopNav/;
-
-
-sub TopNav {
- my($page, $v) = @_;
-
- my $rg = exists $v->{rgraph} ? $v->{rgraph} : tuwf->dbVali('SELECT rgraph FROM vn WHERE id=', \$v->{id});
-
- Div class => 'nav raised-top-nav', sub {
- Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'details'), sub { A href => "/v$v->{id}", class => 'nav__link', 'Details'; };
- Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'discussions'),sub { A href => "/t/v$v->{id}", class => 'nav__link', 'Discussions'; }; # TODO: count
- Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'relations'), sub { A href => "/v$v->{id}/rg", class => 'nav__link', 'Relations'; } if $rg;
- };
-}
-
-1;
diff --git a/lib/VN3/VN/Page.pm b/lib/VN3/VN/Page.pm
deleted file mode 100644
index a09bbeb9..00000000
--- a/lib/VN3/VN/Page.pm
+++ /dev/null
@@ -1,631 +0,0 @@
-package VN3::VN::Page;
-
-use VN3::Prelude;
-use VN3::VN::Lib;
-
-
-TUWF::get '/v/rand', sub {
- # TODO: Apply stored filters?
- my $vid = tuwf->dbVal('SELECT id FROM vn WHERE NOT hidden ORDER BY RANDOM() LIMIT 1');
- tuwf->resRedirect("/v$vid", 'temp');
-};
-
-
-sub CVImage {
- my($vn, $class, $class_sfw, $class_nsfw) = @_;
- return if !$vn->{image};
-
- my $img = tuwf->imgurl(cv => $vn->{image});
- my $nsfw = tuwf->conf->{url_static}.'/v3/nsfw.svg';
- Img class => $class.' '.($vn->{img_nsfw} ? $class_nsfw : $class_sfw),
- !$vn->{img_nsfw} ? (src => $img)
- : auth->pref('show_nsfw') ? (src => $img, 'data-toggle-img' => $nsfw)
- : (src => $nsfw, 'data-toggle-img' => $img);
-}
-
-
-sub Top {
- my $vn = shift;
- Div class => 'fixed-size-left-sidebar-md', '';
- Div class => 'col-md', sub {
- Div class => 'vn-header', sub {
- EntryEdit v => $vn;
- CVImage $vn, 'page-header-img-mobile img img--rounded d-md-none', '', 'nsfw-outline';
- Div class => 'vn-header__title', $vn->{title};
- Div class => 'vn-header__original-title', $vn->{original} if $vn->{original};
- Div class => 'vn-header__details', sub {
- Txt $vn->{c_rating} ? sprintf '%.1f ', $vn->{c_rating}/10 : '-';
- Div class => 'vn-header__sep', '';
- Txt vn_length_time $vn->{length};
- Div class => 'vn-header__sep', '';
- Txt join ', ', map $LANGUAGE{$_}, @{$vn->{c_languages}};
- Debug $vn;
- };
- };
- TopNav details => $vn;
- };
-}
-
-
-sub SidebarProd {
- my $vn = shift;
-
- my $prod = tuwf->dbAlli(q{
- SELECT p.id, p.name, p.original, bool_or(rp.developer) AS dev, bool_or(rp.publisher) AS pub
- FROM releases r
- JOIN releases_producers rp ON rp.id = r.id
- JOIN releases_vn rv ON rv.id = r.id
- JOIN producers p ON rp.pid = p.id
- WHERE rv.vid =}, \$vn->{id}, q{
- AND NOT r.hidden
- GROUP BY p.id, p.name, p.original
- ORDER BY p.name
- });
-
- my $Fmt = sub {
- my($single, $multi, @lst) = @_;
-
- Dt @lst == 1 ? $single : $multi;
- Dd sub {
- Join ', ', sub {
- A href => "/p$_[0]{id}", title => $_[0]{original}||$_[0]{name}, $_[0]{name}
- }, @lst;
- };
- };
-
- $Fmt->('Developer', 'Developers', grep $_->{dev}, @$prod);
- $Fmt->('Publisher', 'Publishers', grep $_->{pub}, @$prod);
-}
-
-
-sub SidebarRel {
- my $vn = shift;
- return if !@{$vn->{relations}};
-
- Dt 'Relations';
- Dd sub {
- Dl sub {
- for my $type (keys %VN_RELATION) {
- my @rel = grep $_->{relation} eq $type, @{$vn->{relations}};
- next if !@rel;
- Dt vn_relation_display $type;
- Dd class => 'single-line-md', sub {
- Span 'unofficial ' if !$_->{official};
- A href => "/v$_->{vid}", title => $_->{original}||$_->{title}, $_->{title};
- } for @rel;
- }
- }
- }
-}
-
-
-sub Sidebar {
- my $vn = shift;
-
- CVImage $vn, 'img img--fit img--rounded d-none d-md-block vn-img-desktop', 'elevation-1', 'elevation-1-nsfw' if $vn->{image};
- Div class => 'vn-image-placeholder img--rounded elevation-1 d-none d-md-block vn-img-desktop', sub {
- Div class => 'vn-image-placeholder__icon', sub {
- Img class => 'svg-icon', src => tuwf->conf->{url_static}.'/v3/camera-alt.svg';
- }
- } if !$vn->{image};
-
- Div class => 'add-to-list elevated-button elevation-1', sub {
- Img class => 'svg-icon', src => tuwf->conf->{url_static}.'/v3/plus.svg';
- Txt 'Add to my list';
- };
-
- Dl class => 'vn-page__dl', sub {
- if($vn->{original}) {
- Dt 'Original Title';
- Dd $vn->{original};
- }
-
- Dt 'Main Title';
- Dd $vn->{title};
-
- if($vn->{alias}) {
- Dt 'Aliases';
- Dd $vn->{alias} =~ s/\n/, /gr;
- }
-
- if($vn->{length}) {
- Dt 'Length';
- Dd vn_length_display $vn->{length};
- }
-
- SidebarProd $vn;
- SidebarRel $vn;
-
- # TODO: Affiliate links
- # TODO: Anime
- };
-}
-
-
-sub Tags {
- my $vn = shift;
-
- my $tag_rating = 'avg(CASE WHEN tv.ignore THEN NULL ELSE tv.vote END)';
- my $tags = tuwf->dbAlli(qq{
- SELECT tv.tag, t.name, t.cat, count(*) as cnt, $tag_rating as rating,
- COALESCE(avg(CASE WHEN tv.ignore THEN NULL ELSE tv.spoiler END), t.defaultspoil) as spoiler
- FROM tags_vn tv
- JOIN tags t ON tv.tag = t.id
- WHERE tv.vid =}, \$vn->{id}, qq{
- AND t.state = 1+1
- GROUP BY tv.tag, t.name, t.cat, t.defaultspoil
- HAVING $tag_rating > 0
- ORDER BY $tag_rating DESC
- });
-
- my $spoil = auth->pref('spoilers') || 0;
- my $cat = auth->pref('tags_cat') || 'cont,tech';
- my %cat = map +($_, !!($cat =~ /$_/)), qw/cont ero tech/;
-
- Div mkclass(
- 'tag-summary__tags' => 1,
- 'tag-summary--collapsed' => 1,
- 'tag-summary--hide-spoil-1' => $spoil < 1,
- 'tag-summary--hide-spoil-2' => $spoil < 2,
- map +("tag-summary--hide-$_", !$cat{$_}), keys %cat
- ), sub {
- for my $tag (@$tags) {
- Div class => sprintf(
- 'tag-summary__tag tag-summary__tag--%s tag-summary__tag--spoil-%d',
- $tag->{cat}, $tag->{spoiler} > 1.3 ? 2 : $tag->{spoiler} > 0.4 ? 1 : 0
- ), sub {
- A href => "/g$tag->{tag}", class => 'link--subtle', $tag->{name};
- Div class => 'tag-summary__tag-meter', style => sprintf('width: %dpx', $tag->{rating}*10), '';
- };
- }
- };
-
- Div class => 'tag-summary__options', sub {
- Div class => 'tag-summary__options-left', sub {
- A href => 'javascript:;', class => 'link--subtle d-none tag-summary__show-all', sub {
- Span class => 'caret caret--pre', '';
- Txt ' Show all tags';
- };
- Debug $tags;
- };
- Div class => 'tag-summary__options-right', sub {
- Div class => 'tag-summary__option dropdown', sub {
- A href => 'javascript:;', class => 'link--subtle dropdown__toggle', sub {
- Span class => 'tag-summary_option--spoil', spoil_display $spoil;
- Lit ' ';
- Span class => 'caret', '';
- };
- Div class => 'dropdown-menu', sub {
- A class => 'dropdown-menu__item tag-summary_option--spoil-0', href => 'javascript:;', spoil_display 0;
- A class => 'dropdown-menu__item tag-summary_option--spoil-1', href => 'javascript:;', spoil_display 1;
- A class => 'dropdown-menu__item tag-summary_option--spoil-2', href => 'javascript:;', spoil_display 2;
- };
- };
- Div class => 'tag-summary__option', sub { Switch 'Content', $cat{cont}, 'tag-summary__option--cont' => 1; };
- Div class => 'tag-summary__option', sub { Switch 'Sexual', $cat{ero}, 'tag-summary__option--ero' => 1; };
- Div class => 'tag-summary__option', sub { Switch 'Technical', $cat{tech}, 'tag-summary__option--tech' => 1; };
- };
- };
-}
-
-
-sub Releases {
- my $vn = shift;
-
- my %lang;
- my @lang = grep !$lang{$_}++, map @{$_->{lang}}, @{$vn->{releases}};
-
- for my $lang (@lang) {
- Div class => 'relsm__language', sub {
- Lang $lang;
- Txt " $LANGUAGE{$lang}";
- };
- Div class => 'relsm__table', sub {
- Div class => 'relsm__rel', sub {
- my $rel = $_;
-
- Div class => 'relsm__rel-col relsm__rel-date tabular-nums', sub { ReleaseDate $rel->{released}; };
- A class => 'relsm__rel-col relsm__rel-name', href => "/r$rel->{id}", title => $rel->{original}||$rel->{title}, $rel->{title};
- Div class => 'relsm__rel-col relsm__rel-platforms', sub { Platform $_ for @{$rel->{platforms}} };
- Div class => 'relsm__rel-col relsm__rel-mylist', sub {
- # TODO: Make this do something
- Img class => 'svg-icon', src => tuwf->conf->{url_static}.'/v3/plus-circle.svg';
- };
- if($rel->{website}) {
- Div class => 'relsm__rel-col relsm__rel-link', sub {
- A href => $rel->{website}, 'Link';
- };
- } else {
- Div class => 'relsm__rel-col relsm__rel-link relsm__rel-link--none', 'Link';
- }
-
- # TODO: Age rating
- # TODO: Release type
- # TODO: Release icons
- } for grep grep($_ eq $lang, @{$_->{lang}}), @{$vn->{releases}};
- }
- }
-}
-
-
-sub Staff {
- my $vn = shift;
- return if !@{$vn->{staff}};
-
- my $Role = sub {
- my $role = shift;
- my @staff = grep $_->{role} eq $role, @{$vn->{staff}};
- return if !@staff;
-
- Div class => 'staff-credits__section', sub {
- Div class => 'staff-credits__section-title', $CREDIT_TYPE{$role};
- Div class => 'staff-credits__item', sub {
- A href => "/s$_->{id}", title => $_->{original}||$_->{name}, $_->{name};
- Span class => 'staff-credits__note', " $_->{note}" if $_->{note};
- } for (@staff);
- };
- };
-
- Div class => 'section', id => 'staff', sub {
- H2 class => 'section__title', 'Staff';
- Div class => 'staff-credits js-columnize', 'data-columns' => 3, sub {
- $Role->($_) for keys %CREDIT_TYPE;
- };
- };
-}
-
-
-sub Gallery {
- my $vn = shift;
-
- return if !@{$vn->{screenshots}};
- my $show = auth->pref('show_nsfw');
-
- Div mkclass(section => 1, gallery => 1, 'gallery--show-r18' => $show), id => 'gallery', sub {
- H2 class => 'section__title', sub {
- Switch '18+', $show, 'gallery-r18-toggle' => 1 if grep $_->{nsfw}, @{$vn->{screenshots}};
- Txt 'Gallery';
- };
-
- # TODO: Thumbnails are being upscaled, we should probably recreate all thumbnails at higher resolution
-
- Div class => 'gallery__section', sub {
- for my $s (@{$vn->{screenshots}}) {
- my $r = (grep $_->{id} == $s->{rid}, @{$vn->{releases}})[0];
- my $meta = {
- width => 1*$s->{width},
- height => 1*$s->{height},
- rel => {
- id => 1*$s->{rid},
- title => $r->{title},
- lang => $r->{lang},
- plat => $r->{platforms},
- }
- };
-
- A mkclass('gallery__image-link' => 1, 'gallery__image--r18' => $s->{nsfw}),
- 'data-lightbox-nfo' => JSON::XS->new->encode($meta),
- href => tuwf->imgurl(sf => $s->{scr}),
- sub {
- Img mkclass(gallery__image => 1, 'nsfw-outline' => $s->{nsfw}), src => tuwf->imgurl(st => $s->{scr});
- }
- }
- }
- };
-}
-
-
-sub CharacterList {
- my($vn, $roles, $first_char) = @_;
-
- # TODO: Implement spoiler & sexual stuff settings
- # TODO: Make long character lists collapsable
-
- Div class => 'character-browser__top-item dropdown', sub {
- A href => 'javascript:;', class => 'link--subtle dropdown__toggle', sub {
- Txt spoil_display 0;
- Lit ' ';
- Span class => 'caret', '';
- };
- Div class => 'dropdown-menu', sub {
- A class => 'dropdown-menu__item', href => 'javascript:;', spoil_display 0;
- A class => 'dropdown-menu__item', href => 'javascript:;', spoil_display 1;
- A class => 'dropdown-menu__item', href => 'javascript:;', spoil_display 2;
- };
- };
- Div class => 'character-browser__top-item d-none d-md-block', sub { Switch 'Sexual traits', 0 };
- Div class => 'character-browser__top-item', sub {
- A href => "/v$vn->{id}/chars", 'View all on one page';
- };
-
- Div class => 'character-browser__list', sub {
- Div class => 'character-browser__list-title', char_role_display $_, scalar @{$roles->{$_}};
- A mkclass('character-browser__char' => 1, 'character-browser__char--active' => $_->{id} == $first_char),
- href => "/c$_->{id}", title => $_->{original}||$_->{name}, 'data-character' => $_->{id}, $_->{name}
- for @{$roles->{$_}};
- } for grep @{$roles->{$_}}, keys %CHAR_ROLE;
-}
-
-
-sub CharacterInfo {
- my $char = shift;
-
- Div class => 'row', sub {
- Div class => 'col-md', sub {
- # TODO: Gender & blood type
- Div class => 'character__name', $char->{name};
- Div class => 'character__subtitle', $char->{original} if $char->{original};
- Div class => 'character__description serif', sub {
- P sub { Lit bb2html $char->{desc}, 0, 1 };
- };
- };
- Div class => 'col-md character__image', sub {
- Img class => 'img img--fit img--rounded',
- src => tuwf->imgurl(ch => $char->{image})
- } if $char->{image};
- };
-
- my(%groups, @groups);
- for(@{$char->{traits}}) {
- push @groups, $_->{gid} if !$groups{$_->{gid}};
- push @{$groups{$_->{gid}}}, $_;
- }
-
- # Create a list of key/value things, so that we can neatly split them in
- # two. The split occurs on the number of sections, so long sections can
- # still cause some imbalance.
- # TODO: Date of birth?
- my @traits = (
- $char->{alias} ? sub {
- Dt 'Aliases';
- Dd $char->{alias} =~ s/\n/, /gr;
- } : (),
-
- $char->{weight} || $char->{height} || $char->{s_bust} || $char->{s_waist} || $char->{s_hip} ? sub {
- Dt 'Measurements';
- Dd join ', ',
- $char->{height} ? "Height: $char->{height}cm" : (),
- $char->{weight} ? "Weight: $char->{weight}kg" : (),
- $char->{s_bust} || $char->{s_waist} || $char->{s_hip} ?
- sprintf 'Bust-Waist-Hips: %s-%s-%scm', $char->{s_bust}||'??', $char->{s_waist}||'??', $char->{s_hip}||'??' : ();
- } : (),
-
- # TODO: Do something with spoiler settings.
- (map { my $g = $_; sub {
- Dt sub { A href => "/i$g", $groups{$g}[0]{group} };
- Dd sub {
- Join ', ', sub {
- A href => "/i$_[0]{tid}", $_[0]{name};
- }, @{$groups{$g}};
- };
- } } @groups),
-
- @{$char->{seiyuu}} ? sub {
- Dt 'Voiced by';
- Dd sub {
- my $prev = '';
- for my $s (sort { $a->{name} cmp $b->{name} } @{$char->{seiyuu}}) {
- next if $s->{name} eq $prev;
- A href => "/s$s->{id}", title => $s->{original}||$s->{name}, $s->{name};
- Txt ' ('.$s->{note}.')' if $s->{note};
- }
- };
- } : (),
- );
-
- Div class => 'character__traits row mt-4', sub {
- Dl class => 'col-md dl--horizontal', sub { $_->() for @traits[0..$#traits/2]; };
- Dl class => 'col-md dl--horizontal', sub { $_->() for @traits[$#traits/2+1..$#traits]; };
- } if @traits;
-}
-
-
-sub Characters {
- my $vn = shift;
-
- # XXX: Fetching and rendering all character details on the VN page is a bit
- # inefficient and bloats the HTML. We should probably load data from other
- # characters on demand.
-
- my $chars = tuwf->dbAlli(q{
- SELECT id, name, original, alias, image, "desc", gender, s_bust, s_waist, s_hip,
- b_month, b_day, height, weight, bloodt
- FROM chars
- WHERE NOT hidden
- AND id IN(SELECT id FROM chars_vns WHERE vid =}, \$vn->{id}, q{)
- ORDER BY name
- });
- return if !@$chars;
-
- enrich_list releases => id => id =>
- sql('SELECT id, rid, spoil, role FROM chars_vns WHERE vid =', \$vn->{id}, ' AND id IN'),
- $chars;
-
- # XXX: Just fetching this list takes ~10ms for a large VN (v92). I worry
- # about formatting and displaying it on every page view. (This query can
- # probably be sped up by grabbing/caching the group tag names separately,
- # there are only 12 groups in the DB anyway).
- enrich_list traits => id => id => sub {sql q{
- SELECT ct.id, ct.tid, ct.spoil, t.name, t.sexual, g.id AS gid, g.name AS group, g.order
- FROM chars_traits ct
- JOIN traits t ON t.id = ct.tid
- JOIN traits g ON g.id = t.group
- WHERE ct.id IN}, $_[0], q{
- ORDER BY g.order, t.name
- }}, $chars;
-
- enrich_list seiyuu => id => cid => sub{sql q{
- SELECT va.id, vs.aid, vs.cid, vs.note, va.name, va.original
- FROM vn_seiyuu_hist vs JOIN staff_alias va ON va.aid = vs.aid
- WHERE vs.chid =}, \$vn->{chid}
- }, $chars;
-
- my %done;
- my %roles = map {
- my $r = $_;
- ($r, [ grep grep($_->{role} eq $r, @{$_->{releases}}) && !$done{$_->{id}}++, @$chars ]);
- } keys %CHAR_ROLE;
-
- my($first_char) = map @{$roles{$_}} ? $roles{$_}[0]{id} : (), keys %CHAR_ROLE;
-
- Div class => 'section', id => 'characters', sub {
- H2 class => 'section__title', sub { Txt 'Characters'; Debug \%roles };
- Div class => 'character-browser', sub {
- Div class => 'row', sub {
- Div class => 'fixed-size-left-sidebar-md', sub {
- Div class => 'character-browser__top-items', sub { CharacterList $vn, \%roles, $first_char; }
- };
- Div class => 'col-md col-md--3 d-none d-md-block', sub {
- Div mkclass(character => 1, 'd-none' => $_->{id} != $first_char), 'data-character' => $_->{id},
- sub { CharacterInfo $_ }
- for @$chars;
- };
- };
- };
- };
-}
-
-
-sub Stats {
- my $vn = shift;
-
- my($has_data, $Dist) = VoteGraph v => $vn->{id};
- return if !$has_data;
-
- my $recent_votes = tuwf->dbAlli(q{
- SELECT v.vid, v.vote,}, sql_totime('v.date'), q{AS date, u.id, u.username
- FROM votes v JOIN users u ON u.id = v.uid
- WHERE NOT EXISTS(SELECT 1 FROM users_prefs WHERE uid = u.id AND key = 'hide_list')
- AND NOT u.ign_votes
- AND v.vid =}, \$vn->{id}, q{
- ORDER BY v.date DESC LIMIT 10
- });
- my $Recent = sub {
- H4 'Recent votes';
- Div class => 'recent-votes', sub {
- Table class => 'recent-votes__table tabular-numbs', sub {
- Tbody sub {
- Tr sub {
- Td sub { A href => "/u$_->{id}", $_->{username}; };
- Td vote_display $_->{vote};
- Td date_display $_->{date};
- } for @$recent_votes;
- };
- };
- Div class => 'final-text', sub {
- A href => "/v$vn->{id}/votes", 'All votes';
- };
- };
- };
-
-
- my $popularity_rank = tuwf->dbVali(
- 'SELECT COUNT(*)+1 FROM vn WHERE NOT hidden AND c_popularity >',
- \($vn->{c_popularity}||0)
- );
- my $rating_rank = tuwf->dbVali(
- 'SELECT COUNT(*)+1 FROM vn WHERE NOT hidden AND c_rating >',
- \($vn->{c_rating}||0)
- );
-
- my $Popularity = sub {
- H4 'Ranking';
- Dl class => 'stats__ranking', sub {
- Dt 'Popularity';
- Dd sprintf 'ranked #%d with a score of %.2f', $popularity_rank, 100*($vn->{c_popularity}||0);
- Dt 'Bayesian rating';
- Dd sprintf 'ranked #%d with a rating of %.2f', $rating_rank, $vn->{c_rating}/10;
- };
- Div class => 'final-text', sub {
- A href => '/v/all', 'See best rated games';
- };
- };
-
-
- Div class => 'section stats', id => 'stats', sub {
- H2 class => 'section__title', 'Stats';
- Div class => 'row semi-muted', sub {
- Div class => 'stats__col col-md col-md-1', sub {
- H4 'Vote distribution';
- $Dist->();
- };
- Div class => 'stats__col col-md col-md-1', $Recent if @$recent_votes;
- Div class => 'stats__col col-md col-md-1', $Popularity;
- };
- };
-}
-
-
-sub Contents {
- my $vn = shift;
-
- Div class => 'vn-page', sub {
- Div class => 'row', sub {
- Div class => 'col-md', sub {
- Div class => 'row', sub {
- Div class => 'fixed-size-left-sidebar-md vn-page__top-details', sub { Sidebar $vn };
- Div class => 'fixed-size-left-sidebar-md', '';
- Div class => 'col-md', sub {
- Div class => 'description serif', id => 'about', sub {
- P sub { Lit bb2html $vn->{desc}||'No description.' };
- };
- Div class => 'section', id => 'tags', sub {
- Div class => 'tag-summary', sub { Tags $vn };
- };
- Div class => 'section', id => 'releases', sub {
- H2 class => 'section__title', 'Releases';
- Div class => 'relsm', sub { Releases $vn };
- };
- Staff $vn;
- Gallery $vn;
- };
- };
- };
- };
- Div class => 'row', sub {
- Div class => 'col-xxl', sub {
- Characters $vn;
- Stats $vn;
- };
- };
- };
-}
-
-
-TUWF::get qr{/$VREV_RE}, sub {
- my $vn = entry v => tuwf->capture('id'), tuwf->capture('rev') or return tuwf->resNotFound;
- return tuwf->resNotFound if !$vn->{id} || $vn->{hidden};
-
- enrich id => q{SELECT id, rgraph, c_languages::text[], c_popularity, c_rating, c_votecount FROM vn WHERE id IN}, $vn;
- enrich scr => q{SELECT id AS scr, width, height FROM screenshots WHERE id IN}, $vn->{screenshots};
- enrich vid => q{SELECT id AS vid, title, original FROM vn WHERE id IN}, $vn->{relations};
- enrich aid => q{SELECT aid, id, name, original FROM staff_alias WHERE aid IN}, $vn->{staff};
-
- enrich_list releases => id => vid => sub {sql q{
- SELECT rv.vid, r.id, r.title, r.original, r.type, r.website, r.released, r.notes,
- r.minage, r.patch, r.freeware, r.doujin, r.resolution, r.voiced, r.ani_story, r.ani_ero
- FROM releases r
- JOIN releases_vn rv ON r.id = rv.id
- WHERE NOT r.hidden AND rv.vid IN}, $_[0], q{
- ORDER BY r.released
- }}, $vn;
-
- enrich_list1 platforms => id => id => 'SELECT id, platform FROM releases_platforms WHERE id IN', $vn->{releases};
- enrich_list1 lang => id => id => 'SELECT id, lang FROM releases_lang WHERE id IN', $vn->{releases};
- enrich_list media => id => id => 'SELECT id, medium, qty FROM releases_media WHERE id IN', $vn->{releases};
-
- Framework
- og => {
- description => bb2text($vn->{desc}),
- $vn->{image} && !$vn->{img_nsfw} ? (
- image => tuwf->imgurl(cv => $vn->{image})
- ) : (($_) = grep !$_->{nsfw}, @{$vn->{screenshots}}) ? (
- image => tuwf->imgurl(st => $_->{scr})
- ) : ()
- },
- title => $vn->{title},
- top => sub { Top $vn },
- sub { Contents $vn };
-};
-
-1;
diff --git a/lib/VN3/Validation.pm b/lib/VN3/Validation.pm
deleted file mode 100644
index 73bf7d62..00000000
--- a/lib/VN3/Validation.pm
+++ /dev/null
@@ -1,168 +0,0 @@
-# This module provides additional validations for tuwf->validate(), and exports
-# a few convenient form handling/validation functions.
-package VN3::Validation;
-
-use strict;
-use warnings;
-use TUWF;
-use VNDBUtil;
-use VNDB::Types;
-use VNWeb::Auth;
-use VN3::DB;
-use VN3::Types;
-use JSON::XS;
-use Exporter 'import';
-use Time::Local 'timegm';
-use Carp 'croak';
-our @EXPORT = ('form_compile', 'form_changed', 'validate_dbid', 'can_edit');
-
-
-TUWF::set custom_validations => {
- id => { uint => 1, max => 1<<40 },
- page => { uint => 1, min => 1, max => 1000, required => 0, default => 1 },
- username => { regex => qr/^[a-z0-9-]{2,15}$/ },
- password => { length => [ 4, 500 ] },
- editsum => { required => 1, length => [ 2, 5000 ] },
- vn_length => { required => 0, default => 0, uint => 1, enum => \%VN_LENGTH },
- vn_relation => { enum => \%VN_RELATION },
- producer_relation => { enum => \%PRODUCER_RELATION },
- staff_role => { enum => \%CREDIT_TYPE },
- char_role => { enum => \%CHAR_ROLE },
- language => { enum => \%LANGUAGE },
- platform => { enum => \%PLATFORM },
- medium => { enum => \%MEDIUM },
- resolution => { enum => \%RESOLUTION },
- gender => { enum => \%GENDER },
- blood_type => { enum => \%BLOOD_TYPE },
- gtin => { uint => 1, func => sub { $_[0] eq 0 || gtintype($_[0]) } },
- minage => { uint => 1, enum => \%AGE_RATING },
- animated => { uint => 1, enum => \%ANIMATED },
- voiced => { uint => 1, enum => \%VOICED },
- rdate => { uint => 1, func => \&_validate_rdate },
- spoiler => { uint => 1, range => [ 0, 2 ] },
- vnlist_status=>{ enum => \%VNLIST_STATUS },
- # Accepts a user-entered vote string (or '-' or empty) and converts that into a DB vote number (or undef)
- vnvote => { regex => qr/^(?:|-|[1-9]|10|[1-9]\.[0-9]|10\.0)$/, required => 0, func => sub { $_[0] = $_[0] eq '-' ? undef : 10*$_[0]; 1 } },
- # Sort an array by the listed hash keys, using string comparison on each key
- sort_keys => sub {
- my @keys = ref $_[0] eq 'ARRAY' ? @{$_[0]} : $_[0];
- +{ type => 'array', sort => sub {
- for(@keys) {
- my $c = defined($_[0]{$_}) cmp defined($_[1]{$_}) || (defined($_[0]{$_}) && $_[0]{$_} cmp $_[1]{$_});
- return $c if $c;
- }
- 0
- } }
- },
- # Sorted and unique array-of-hashes (default order is sort_keys on the sorted keys...)
- aoh => sub { +{ type => 'array', unique => 1, sort_keys => [sort keys %{$_[0]}], values => { type => 'hash', keys => $_[0] } } },
-};
-
-
-sub _validate_rdate {
- return 0 if $_[0] ne 0 && $_[0] !~ /^([0-9]{4})([0-9]{2})([0-9]{2})$/;
- my($y, $m, $d) = $_[0] eq 0 ? (0,0,0) : ($1, $2, $3);
-
- # Re-normalize
- ($m, $d) = (0, 0) if $y == 0;
- $m = 99 if $y == 9999;
- $d = 99 if $m == 99;
- $_[0] = $y*10000 + $m*100 + $d;
-
- return 0 if $y && $y != 9999 && ($y < 1980 || $y > 2100);
- return 0 if $y && $m != 99 && (!$m || $m > 12);
- return 0 if $y && $d != 99 && !eval { timegm(0, 0, 0, $d, $m-1, $y) };
- return 1;
-}
-
-
-# Recursively remove keys from hashes that have a '_when' key that doesn't
-# match $when. This is a quick and dirty way to create multiple validation
-# schemas from a single schema. For example:
-#
-# {
-# title => { _when => 'input' },
-# name => { },
-# }
-#
-# If $when is 'input', then this function returns:
-# { title => {}, name => {} }
-# Otherwise, it returns:
-# { name => {} }
-sub stripwhen {
- my($when, $o) = @_;
- return $o if ref $o ne 'HASH';
- +{ map $_ eq '_when' || (ref $o->{$_} eq 'HASH' && defined $o->{$_}{_when} && $o->{$_}{_when} !~ $when) ? () : ($_, stripwhen($when, $o->{$_})), keys %$o }
-}
-
-
-# Short-hand to compile a validation schema for a form. Usage:
-#
-# form_compile $when, {
-# title => { _when => 'input' },
-# name => { },
-# ..
-# };
-sub form_compile {
- tuwf->compile({ type => 'hash', keys => stripwhen @_ });
-}
-
-
-sub eq_deep {
- my($a, $b) = @_;
- return 0 if ref $a ne ref $b;
- return 0 if defined $a != defined $b;
- return 1 if !defined $a;
- return 1 if !ref $a && $a eq $b;
- return 1 if ref $a eq 'ARRAY' && (@$a == @$b && !grep !eq_deep($a->[$_], $b->[$_]), 0..$#$a);
- return 1 if ref $a eq 'HASH' && eq_deep([sort keys %$a], [sort keys %$b]) && !grep !eq_deep($a->{$_}, $b->{$_}), keys %$a;
- 0
-}
-
-
-# Usage: form_changed $schema, $a, $b
-# Returns 1 if there is a difference between the data ($a) and the form input
-# ($b), using the normalization defined in $schema. The $schema must validate.
-sub form_changed {
- my($schema, $a, $b) = @_;
- my $na = $schema->validate($a)->data;
- my $nb = $schema->validate($b)->data;
-
- #warn "a=".JSON::XS->new->pretty->canonical->encode($na);
- #warn "b=".JSON::XS->new->pretty->canonical->encode($nb);
- !eq_deep $na, $nb;
-}
-
-
-# Validate identifiers against an SQL query. The query must end with a 'id IN'
-# clause, where the @ids array is appended. The query must return exactly 1
-# column, the id of each entry. This function throws an error if an id is
-# missing from the query. For example, to test for non-hidden VNs:
-#
-# validate_dbid 'SELECT id FROM vn WHERE NOT hidden AND id IN', 2,3,5,7,...;
-#
-# If any of those ids is hidden or not in the database, an error is thrown.
-sub validate_dbid {
- my($sql, @ids) = @_;
- return if !@ids;
- $sql = ref $sql eq 'CODE' ? sql $sql->(\@ids) : sql $sql, \@ids;
- my %dbids = map +((values %$_)[0],1), @{ tuwf->dbAlli($sql) };
- my @missing = grep !$dbids{$_}, @ids;
- croak "Invalid database IDs: ".join(',', @missing) if @missing;
-}
-
-
-# Returns whether the current user can edit the given database entry.
-sub can_edit {
- my($type, $entry) = @_;
-
- return auth->permUsermod || $entry->{id} == (auth->uid||0) if $type eq 'u';
- return auth->permDbmod if $type eq 'd';
-
- die "Can't do authorization test when entry_hidden/entry_locked fields aren't present"
- if $entry->{id} && (!exists $entry->{entry_hidden} || !exists $entry->{entry_locked});
-
- auth->permDbmod || (auth->permEdit && !($entry->{entry_hidden} || $entry->{entry_locked}));
-}
-
-1;
diff --git a/lib/VNDB/DB/ULists.pm b/lib/VNDB/DB/ULists.pm
index 6f061e97..4c1d10ae 100644
--- a/lib/VNDB/DB/ULists.pm
+++ b/lib/VNDB/DB/ULists.pm
@@ -7,9 +7,8 @@ use Exporter 'import';
our @EXPORT = qw|
- dbRListGet dbVNListGet dbVNListList dbVNListAdd dbVNListDel dbRListAdd dbRListDel
- dbVoteGet dbVoteStats dbVoteAdd dbVoteDel
- dbWishListGet dbWishListAdd dbWishListDel
+ dbRListGet dbRListAdd dbRListDel
+ dbVoteStats
|;
@@ -30,121 +29,6 @@ sub dbRListGet {
);
}
-# Options: uid vid
-sub dbVNListGet {
- my($self, %o) = @_;
-
- my %where = (
- 'uid = ?' => $o{uid},
- $o{vid} ? ('vid IN(!l)' => [ ref $o{vid} ? $o{vid} : [$o{vid}] ]) : (),
- );
-
- return $self->dbAll(q|
- SELECT uid, vid, status
- FROM vnlists
- !W|,
- \%where
- );
-}
-
-
-# Options: uid char voted page results sort reverse
-# sort: title vote
-sub dbVNListList {
- my($self, %o) = @_;
- $o{results} ||= 50;
- $o{page} ||= 1;
-
- my %where = (
- 'vl.uid = ?' => $o{uid},
- defined($o{voted}) ? ('vo.vote !s NULL' => $o{voted} ? 'IS NOT' : 'IS') : (),
- defined($o{status})? ('vl.status = ?' => $o{status}) : (),
- $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 ) : (),
- );
-
- my $order = sprintf {
- title => 'v.title %s',
- vote => 'vo.vote %s NULLS LAST, v.title ASC',
- }->{ $o{sort}||'title' }, $o{reverse} ? 'DESC' : 'ASC';
-
- # execute query
- my($r, $np) = $self->dbPage(\%o, qq|
- SELECT vl.vid, v.title, v.original, vl.status, vl.notes, COALESCE(vo.vote, 0) AS vote
- FROM vnlists vl
- JOIN vn v ON v.id = vl.vid
- LEFT JOIN votes vo ON vo.vid = vl.vid AND vo.uid = vl.uid
- !W
- ORDER BY !s|,
- \%where, $order
- );
-
- # fetch releases and link to VNs
- if(@$r) {
- my %vns = map {
- $_->{rels}=[];
- $_->{vid}, $_->{rels}
- } @$r;
-
- my $rel = $self->dbAll(q|
- SELECT rv.vid, rl.rid, r.title, r.original, r.released, r.type, rl.status
- FROM rlists rl
- JOIN releases r ON rl.rid = r.id
- JOIN releases_vn rv ON rv.id = r.id
- WHERE rl.uid = ?
- AND rv.vid IN(!l)
- ORDER BY r.released ASC|,
- $o{uid}, [ keys %vns ]
- );
-
- if(@$rel) {
- my %rel = map { $_->{rid} => [] } @$rel;
- push(@{$rel{$_->{id}}}, $_->{lang}) for (@{$self->dbAll(q|
- SELECT id, lang
- FROM releases_lang
- WHERE id IN(!l)|,
- [ keys %rel ]
- )});
- for(@$rel) {
- $_->{languages} = $rel{$_->{rid}};
- push @{$vns{$_->{vid}}}, $_;
- }
- }
- }
-
- return wantarray ? ($r, $np) : $r;
-}
-
-
-# Arguments: uid vid status notes
-# vid can be an arrayref only when the rows are already present, in which case an update is done
-# status and notes can be undef when an update is done, in which case these fields aren't updated
-sub dbVNListAdd {
- my($self, $uid, $vid, $stat, $notes) = @_;
- $self->dbExec(
- 'UPDATE vnlists !H WHERE uid = ? AND vid IN(!l)',
- {defined($stat) ? ('status = ?' => $stat ):(),
- defined($notes)? ('notes = ?' => $notes):()},
- $uid, ref($vid) ? $vid : [ $vid ]
- )
- ||
- $self->dbExec(
- 'INSERT INTO vnlists (uid, vid, status, notes) VALUES(?, ?, ?, ?)',
- $uid, $vid, $stat||0, $notes||''
- );
-}
-
-
-# Arguments: uid, vid
-sub dbVNListDel {
- my($self, $uid, $vid) = @_;
- $self->dbExec(
- 'DELETE FROM vnlists WHERE uid = ? AND vid IN(!l)',
- $uid, ref($vid) ? $vid : [ $vid ]
- );
-}
-
# Arguments: uid rid status
# rid can be an arrayref only when the rows are already present, in which case an update is done
@@ -172,180 +56,22 @@ sub dbRListDel {
}
-# Options: uid vid hide_ign results page what sort reverse
-# what: user, vn, hide_list
-sub dbVoteGet {
- my($self, %o) = @_;
- $o{results} ||= 50;
- $o{page} ||= 1;
- $o{what} ||= '';
- $o{sort} ||= 'date';
- $o{reverse} //= 1;
-
- my %where = (
- $o{uid} ? ( 'n.uid = ?' => $o{uid} ) : (),
- $o{vid} ? ( 'n.vid = ?' => $o{vid} ) : (),
- $o{hide_ign} ? ( '(NOT u.ign_votes OR u.id = ?)' => $self->authInfo->{id}||0 ) : (),
- $o{vn_char} ? ( 'LOWER(SUBSTR(v.title, 1, 1)) = ?' => $o{vn_char} ) : (),
- defined $o{vn_char} && !$o{vn_char} ? (
- '(ASCII(v.title) < 97 OR ASCII(v.title) > 122) AND (ASCII(v.title) < 65 OR ASCII(v.title) > 90)' => 1 ) : (),
- );
-
- my @select = (
- qw|n.vid n.vote n.uid|, q|extract('epoch' from n.date) as date|,
- $o{what} =~ /user/ ? (VNWeb::DB::sql_user()) : (),
- $o{what} =~ /vn/ ? (qw|v.title v.original|) : (),
- $o{what} =~ /hide_list/ ? ('u.hide_list') : (),
- );
-
- my @join = (
- $o{what} =~ /vn/ ? (
- 'JOIN vn v ON v.id = n.vid',
- ) : (),
- $o{what} =~ /user/ || $o{hide} || $o{what} =~ /hide_list/ ? (
- 'JOIN users u ON u.id = n.uid'
- ) : (),
- );
-
- my $order = sprintf {
- date => 'n.date %s',
- # Hidden users should not be sorted among the rest. as that would still give them away
- username => $o{what} =~ /hide_list/ ? '(CASE WHEN u.hide_list THEN NULL ELSE u.username END) %s, n.date' : 'u.username %s',
- title => 'v.title %s',
- vote => 'n.vote %s'.($o{what} =~ /vn/ ? ', v.title ASC' : $o{what} =~ /user/ ? ', u.username ASC' : ''),
- }->{$o{sort}}, $o{reverse} ? 'DESC' : 'ASC';
-
- my($r, $np) = $self->dbPage(\%o, q|
- SELECT !s
- FROM votes n
- !s
- !W
- ORDER BY !s|,
- join(',', @select), join(' ', @join), \%where, $order
- );
-
- return wantarray ? ($r, $np) : $r;
-}
-
-
-# Arguments: (uid|vid), id, use_ignore_list
+# Arguments: 'vid', id
# Returns an arrayref with 10 elements containing the [ count(vote), sum(vote) ]
# for votes in the range of ($index+0.5) .. ($index+1.4)
sub dbVoteStats {
my($self, $col, $id, $ign) = @_;
- my $u = $self->authInfo->{id};
my $r = [ map [0,0], 0..9 ];
$r->[$_->{idx}] = [ $_->{votes}, $_->{total} ] for (@{$self->dbAll(q|
- SELECT (vote::numeric/10)::int-1 AS idx, COUNT(vote) as votes, SUM(vote) AS total
- FROM votes
- !s
- !W
- GROUP BY (vote::numeric/10)::int|,
- $ign ? 'JOIN users ON id = uid AND (NOT ign_votes'.($u?sprintf(' OR id = %d',$u):'').')' : '',
- $col ? { '!s = ?' => [ $col, $id ] } : {},
+ SELECT (vote::numeric/10)::int-1 AS idx, COUNT(vote) as votes, SUM(vote) AS total
+ FROM ulist_vns uv
+ WHERE uv.vote IS NOT NULL AND NOT EXISTS(SELECT 1 FROM users u WHERE u.id = uv.uid AND u.ign_votes)
+ AND uv.vid = ?
+ GROUP BY (vote::numeric/10)::int|,
+ $id
)});
return $r;
}
-
-# Adds a new vote or updates an existing one
-# Arguments: vid, uid, vote
-# vid can be an arrayref only when the rows are already present, in which case an update is done
-sub dbVoteAdd {
- my($self, $vid, $uid, $vote) = @_;
- $self->dbExec(q|
- UPDATE votes
- SET vote = ?
- WHERE vid IN(!l)
- AND uid = ?|,
- $vote, ref($vid) ? $vid : [$vid], $uid
- ) || $self->dbExec(q|
- INSERT INTO votes
- (vid, uid, vote)
- VALUES (!l)|,
- [ $vid, $uid, $vote ]
- );
-}
-
-
-# Arguments: uid, vid
-# vid can be an arrayref
-sub dbVoteDel {
- my($self, $uid, $vid) = @_;
- $self->dbExec('DELETE FROM votes !W',
- { 'vid IN(!l)' => [ref($vid)?$vid:[$vid]], 'uid = ?' => $uid }
- );
-}
-
-
-# %options->{ uid vid wstat what page results sort reverse }
-# what: vn
-# sort: title added wstat
-sub dbWishListGet {
- my($self, %o) = @_;
-
- $o{page} ||= 1;
- $o{results} ||= 50;
- $o{what} ||= '';
-
- my %where = (
- 'wl.uid = ?' => $o{uid},
- $o{vid} ? ( 'wl.vid = ?' => $o{vid} ) : (),
- defined $o{wstat} ? ( 'wl.wstat = ?' => $o{wstat} ) : (),
- );
-
- my $select = q|wl.vid, wl.wstat, extract('epoch' from wl.added) AS added|;
- my @join;
- if($o{what} =~ /vn/) {
- $select .= ', v.title, v.original';
- push @join, 'JOIN vn v ON v.id = wl.vid';
- }
-
- no if $] >= 5.022, warnings => 'redundant';
- my $order = sprintf {
- title => 'v.title %s',
- added => 'wl.added %s',
- wstat => 'wl.wstat %2$s, v.title ASC',
- }->{ $o{sort}||'added' }, $o{reverse} ? 'DESC' : 'ASC', $o{reverse} ? 'ASC' : 'DESC';
-
- my($r, $np) = $self->dbPage(\%o, q|
- SELECT !s
- FROM wlists wl
- !s
- !W
- ORDER BY !s|,
- $select, join(' ', @join), \%where, $order,
- );
-
- return wantarray ? ($r, $np) : $r;
-}
-
-
-# Updates or adds a whishlist item
-# Arguments: vid, uid, wstat
-sub dbWishListAdd {
- my($self, $vid, $uid, $wstat) = @_;
- $self->dbExec(
- 'UPDATE wlists SET wstat = ? WHERE uid = ? AND vid IN(!l)',
- $wstat, $uid, ref($vid) eq 'ARRAY' ? $vid : [ $vid ]
- )
- ||
- $self->dbExec(
- 'INSERT INTO wlists (uid, vid, wstat) VALUES(!l)',
- [ $uid, $vid, $wstat ]
- );
-}
-
-
-# Arguments: uid, vids
-sub dbWishListDel {
- my($self, $uid, $vid) = @_;
- $self->dbExec(
- 'DELETE FROM wlists WHERE uid = ? AND vid IN(!l)',
- $uid, ref($vid) eq 'ARRAY' ? $vid : [ $vid ]
- );
-}
-
-
1;
diff --git a/lib/VNDB/DB/VN.pm b/lib/VNDB/DB/VN.pm
index c3ec2cc2..d099b6ff 100644
--- a/lib/VNDB/DB/VN.pm
+++ b/lib/VNDB/DB/VN.pm
@@ -14,8 +14,8 @@ our @EXPORT = qw|dbVNGet dbVNGetRev dbVNRevisionInsert dbVNImageId dbScreenshotA
# 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 screenshots relgraph rating ranking wishlist vnlist
-# Note: wishlist and vnlist are ignored (no db search) unless a user is logged in
+# What: extended anime staff seiyuu relations screenshots relgraph 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) = @_;
@@ -65,13 +65,13 @@ sub dbVNGet {
$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 wlists WHERE uid = ? AND wstat = 3)' => $uid ) : (),
+ '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 wlists WHERE uid = ?)' => [ $o{ul_onwish} ? '' : 'NOT', $uid ] ) : (),
+ '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 votes WHERE uid = ?)' => [ $o{ul_voted} ? '' : 'NOT', $uid ] ) : (),
+ '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 vnlists WHERE uid = ?)' => [ $o{ul_onlist} ? '' : 'NOT', $uid ] ) : (),
+ '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)
@@ -97,8 +97,6 @@ sub dbVNGet {
my @join = (
$o{what} =~ /relgraph/ ? 'JOIN relgraphs vg ON vg.id = v.rgraph' : (),
- $uid && $o{what} =~ /wishlist/ ?
- 'LEFT JOIN wlists wl ON wl.vid = v.id AND wl.uid = ' . $uid : (),
$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
@@ -120,7 +118,6 @@ sub dbVNGet {
'(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} =~ /wishlist/ ? 'wl.wstat' : (),
$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 ?
@@ -258,6 +255,14 @@ sub _enrich {
}
}
+ 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;
}
diff --git a/lib/VNDB/Handler/ULists.pm b/lib/VNDB/Handler/ULists.pm
index e42a41c0..03c079b1 100644
--- a/lib/VNDB/Handler/ULists.pm
+++ b/lib/VNDB/Handler/ULists.pm
@@ -3,83 +3,17 @@ package VNDB::Handler::ULists;
use strict;
use warnings;
-use TUWF ':html', ':xml';
+use TUWF ':xml';
use VNDB::Func;
use VNDB::Types;
TUWF::register(
- qr{v([1-9]\d*)/vote}, \&vnvote,
- qr{v([1-9]\d*)/wish}, \&vnwish,
- qr{v([1-9]\d*)/list}, \&vnlist_e,
qr{r([1-9]\d*)/list}, \&rlist_e,
qr{xml/rlist.xml}, \&rlist_e,
- qr{([uv])([1-9]\d*)/votes}, \&votelist,
- qr{u([1-9]\d*)/wish}, \&wishlist,
- qr{u([1-9]\d*)/list}, \&vnlist,
);
-sub vnvote {
- my($self, $id) = @_;
-
- my $uid = $self->authInfo->{id};
- return $self->htmlDenied() if !$uid;
-
- return if !$self->authCheckCode;
- my $f = $self->formValidate(
- { get => 'v', regex => qr/^(-1|([1-9]|10)(\.[0-9])?)$/ },
- { get => 'ref', required => 0, default => "/v$id" }
- );
- return $self->resNotFound if $f->{_err} || ($f->{v} != -1 && ($f->{v} > 10 || $f->{v} < 1));
-
- $self->dbVoteDel($uid, $id) if $f->{v} == -1;
- $self->dbVoteAdd($id, $uid, $f->{v}*10) if $f->{v} > 0;
-
- $self->resRedirect($f->{ref}, 'temp');
-}
-
-
-sub vnwish {
- my($self, $id) = @_;
-
- my $uid = $self->authInfo->{id};
- return $self->htmlDenied() if !$uid;
-
- return if !$self->authCheckCode;
- my $f = $self->formValidate(
- { get => 's', enum => [ -1, keys %WISHLIST_STATUS ] },
- { get => 'ref', required => 0, default => "/v$id" }
- );
- return $self->resNotFound if $f->{_err};
-
- $self->dbWishListDel($uid, $id) if $f->{s} == -1;
- $self->dbWishListAdd($id, $uid, $f->{s}) if $f->{s} != -1;
-
- $self->resRedirect($f->{ref}, 'temp');
-}
-
-
-sub vnlist_e {
- my($self, $id) = @_;
-
- my $uid = $self->authInfo->{id};
- return $self->htmlDenied() if !$uid;
-
- return if !$self->authCheckCode;
- my $f = $self->formValidate(
- { get => 'e', enum => [ -1, keys %VNLIST_STATUS ] },
- { get => 'ref', required => 0, default => "/v$id" }
- );
- return $self->resNotFound if $f->{_err};
-
- $self->dbVNListDel($uid, $id) if $f->{e} == -1;
- $self->dbVNListAdd($uid, $id, $f->{e}) if $f->{e} != -1;
-
- $self->resRedirect($f->{ref}, 'temp');
-}
-
-
sub rlist_e {
my($self, $id) = @_;
@@ -113,419 +47,5 @@ sub rlist_e {
}
}
-
-sub votelist {
- my($self, $type, $id) = @_;
-
- my $obj = $type eq 'v' ? $self->dbVNGet(id => $id)->[0] : $self->dbUserGet(uid => $id, what => 'hide_list')->[0];
- return $self->resNotFound if !$obj->{id};
-
- my $own = $type eq 'u' && $self->authInfo->{id} && $self->authInfo->{id} == $id;
- return $self->resNotFound if $type eq 'u' && !$own && !(!$obj->{hide_list} || $self->authCan('usermod'));
-
- my $f = $self->formValidate(
- { get => 'p', required => 0, default => 1, template => 'page' },
- { get => 'o', required => 0, default => 'd', enum => ['a', 'd'] },
- { get => 's', required => 0, default => 'date', enum => [qw|date title vote|] },
- { get => 'c', required => 0, default => 'all', enum => [ 'all', 'a'..'z', 0 ] },
- );
- return $self->resNotFound if $f->{_err};
-
- if($own && $self->reqMethod eq 'POST') {
- return if !$self->authCheckCode;
- my $frm = $self->formValidate(
- { post => 'vid', required => 1, multi => 1, template => 'id' },
- { post => 'batchvotes', required => 1, regex => qr/^(-1|([1-9]|10)(\.[0-9])?)$/ },
- );
- my @vid = grep $_ && $_ > 0, @{$frm->{vid}};
- if(!$frm->{_err} && @vid && $frm->{batchvotes} > -2) {
- $self->dbVoteDel($id, \@vid) if $frm->{batchvotes} == -1;
- $self->dbVoteAdd(\@vid, $id, $frm->{batchvotes}*10) if $frm->{batchvotes} > 0;
- }
- }
-
- my($list, $np) = $self->dbVoteGet(
- $type.'id' => $id,
- what => $type eq 'v' ? 'user hide_list' : 'vn',
- hide_ign => $type eq 'v',
- sort => $f->{s} eq 'title' && $type eq 'v' ? 'username' : $f->{s},
- reverse => $f->{o} eq 'd',
- results => 50,
- page => $f->{p},
- $type eq 'u' && $f->{c} ne 'all' ? (vn_char => $f->{c}) : (),
- );
-
- my $title = $type eq 'v' ? "Votes for $obj->{title}" : 'Votes by '.VNWeb::HTML::user_displayname($obj);
- $self->htmlHeader(noindex => 1, type => $type, dbobj => $obj, title => $title);
- $self->htmlMainTabs($type => $obj, 'votes');
- div class => 'mainbox';
- h1 $title;
- if($type eq 'u') {
- p class => 'browseopts';
- for ('all', 'a'..'z', 0) {
- a href => "/$type$id/votes?c=$_", $_ eq $f->{c} ? (class => 'optselected') : (), $_ eq 'all' ? 'ALL' : $_ ? uc $_ : '#';
- }
- end;
- }
- p 'No votes to list. :-(' if !@$list;
- end;
-
- if($own) {
- my $code = $self->authGetCode("/u$id/votes");
- form action => "/u$id/votes?formcode=$code;c=$f->{c};s=$f->{s};p=$f->{p}", method => 'post';
- }
-
- @$list && $self->htmlBrowse(
- class => 'votelist',
- items => $list,
- options => $f,
- nextpage => $np,
- pageurl => "/$type$id/votes?c=$f->{c};o=$f->{o};s=$f->{s}",
- sorturl => "/$type$id/votes?c=$f->{c}",
- header => [
- [ 'Cast', 'date' ],
- [ 'Vote', 'vote' ],
- [ $type eq 'v' ? 'User' : 'Visual novel', 'title' ],
- ],
- row => sub {
- my($s, $n, $l) = @_;
- Tr;
- td class => 'tc1';
- input type => 'checkbox', name => 'vid', value => $l->{vid} if $own;
- txt ' '.fmtdate $l->{date};
- end;
- td class => 'tc2', fmtvote $l->{vote};
- td class => 'tc3';
- if($type eq 'u') {
- a href => "/v$l->{vid}", title => $l->{original}||$l->{title}, shorten $l->{title}, 100;
- } elsif($l->{hide_list}) {
- b class => 'grayedout', 'hidden';
- } else {
- VNWeb::HTML::user_($l);
- }
- end;
- end;
- },
- $own ? (footer => sub {
- Tr;
- td colspan => 3, class => 'tc1';
- input type => 'checkbox', class => 'checkall', name => 'vid', value => 0;
- txt ' ';
- Select name => 'batchvotes', id => 'batchvotes';
- option value => -2, '-- with selected --';
- optgroup label => 'Change vote';
- option value => $_, sprintf '%d (%s)', $_, fmtrating $_ for (reverse 1..10);
- option value => -3, 'Other';
- end;
- option value => -1, 'revoke';
- end;
- end;
- end 'tr';
- }) : (),
- );
- end if $own;
- $self->htmlFooter;
-}
-
-
-sub wishlist {
- my($self, $uid) = @_;
-
- my $own = $self->authInfo->{id} && $self->authInfo->{id} == $uid;
- my $u = $self->dbUserGet(uid => $uid, what => 'hide_list')->[0];
- return $self->resNotFound if !$u || !$own && !(!$u->{hide_list} || $self->authCan('usermod'));
-
- my $f = $self->formValidate(
- { get => 'p', required => 0, default => 1, template => 'page' },
- { get => 'o', required => 0, default => 'd', enum => [ 'a', 'd' ] },
- { get => 's', required => 0, default => 'wstat', enum => [qw|title added wstat|] },
- { get => 'f', required => 0, default => -1, enum => [ -1, keys %WISHLIST_STATUS ] },
- );
- return $self->resNotFound if $f->{_err};
-
- if($own && $self->reqMethod eq 'POST') {
- return if !$self->authCheckCode;
- my $frm = $self->formValidate(
- { post => 'sel', required => 0, default => 0, multi => 1, template => 'id' },
- { post => 'batchedit', required => 1, enum => [ -1, keys %WISHLIST_STATUS ] },
- );
- $frm->{sel} = [ grep $_, @{$frm->{sel}} ]; # weed out "select all" checkbox
- if(!$frm->{_err} && @{$frm->{sel}} && $frm->{sel}[0]) {
- $self->dbWishListDel($uid, $frm->{sel}) if $frm->{batchedit} == -1;
- $self->dbWishListAdd($frm->{sel}, $uid, $frm->{batchedit}) if $frm->{batchedit} >= 0;
- }
- }
-
- my($list, $np) = $self->dbWishListGet(
- uid => $uid,
- sort => $f->{s}, reverse => $f->{o} eq 'd',
- $f->{f} != -1 ? (wstat => $f->{f}) : (),
- what => 'vn',
- results => 50,
- page => $f->{p},
- );
-
- my $title = $own ? 'My wishlist' : VNWeb::HTML::user_displayname($u)."'s wishlist";
- $self->htmlHeader(title => $title, noindex => 1, type => 'u', dbobj => $u);
- $self->htmlMainTabs('u', $u, 'wish');
- div class => 'mainbox';
- h1 $title;
- if(!@$list && $f->{f} == -1) {
- p 'Wishlist empty...';
- end;
- return $self->htmlFooter;
- }
- p class => 'browseopts';
- a $f->{f} == $_ ? (class => 'optselected') : (), href => "/u$uid/wish?f=$_",
- $_ == -1 ? 'All priorities' : $WISHLIST_STATUS{$_}
- for (-1, keys %WISHLIST_STATUS);
- end;
- end 'div';
-
- if($own) {
- my $code = $self->authGetCode("/u$uid/wish");
- form action => "/u$uid/wish?formcode=$code;f=$f->{f};o=$f->{o};s=$f->{s};p=$f->{p}", method => 'post';
- }
-
- $self->htmlBrowse(
- class => 'wishlist',
- items => $list,
- nextpage => $np,
- options => $f,
- pageurl => "/u$uid/wish?f=$f->{f};o=$f->{o};s=$f->{s}",
- sorturl => "/u$uid/wish?f=$f->{f}",
- header => [
- [ 'Title' => 'title' ],
- [ 'Priority' => 'wstat' ],
- [ 'Added' => 'added' ],
- ],
- row => sub {
- my($s, $n, $i) = @_;
- Tr;
- td class => 'tc1';
- input type => 'checkbox', name => 'sel', value => $i->{vid}
- if $own;
- a href => "/v$i->{vid}", title => $i->{original}||$i->{title}, ' '.shorten $i->{title}, 70;
- end;
- td class => 'tc2', $WISHLIST_STATUS{$i->{wstat}};
- td class => 'tc3', fmtdate $i->{added}, 'compact';
- end;
- },
- $own ? (footer => sub {
- Tr;
- td colspan => 3;
- input type => 'checkbox', class => 'checkall', name => 'sel', value => 0;
- txt ' ';
- Select name => 'batchedit', id => 'batchedit';
- option '-- with selected --';
- optgroup label => 'Change priority';
- option value => $_, $WISHLIST_STATUS{$_}
- for (keys %WISHLIST_STATUS);
- end;
- option value => -1, 'remove from wishlist';
- end;
- end;
- end;
- }) : (),
- );
- end 'form' if $own;
- $self->htmlFooter;
-}
-
-
-sub vnlist {
- my($self, $uid) = @_;
-
- my $own = $self->authInfo->{id} && $self->authInfo->{id} == $uid;
- my $u = $self->dbUserGet(uid => $uid, what => 'hide_list')->[0];
- return $self->resNotFound if !$u || !$own && !(!$u->{hide_list} || $self->authCan('usermod'));
-
- my $f = $self->formValidate(
- { get => 'p', required => 0, default => 1, template => 'page' },
- { get => 'o', required => 0, default => 'a', enum => [ 'a', 'd' ] },
- { get => 's', required => 0, default => 'title', enum => [ 'title', 'vote' ] },
- { get => 'c', required => 0, default => 'all', enum => [ 'all', 'a'..'z', 0 ] },
- { get => 'v', required => 0, default => 0, enum => [ -1..1 ] },
- { get => 't', required => 0, default => -1, enum => [ -1, keys %VNLIST_STATUS ] },
- );
- return $self->resNotFound if $f->{_err};
-
- if($own && $self->reqMethod eq 'POST') {
- return if !$self->authCheckCode;
- my $frm = $self->formValidate(
- { post => 'vid', required => 0, default => 0, multi => 1, template => 'id' },
- { post => 'rid', required => 0, default => 0, multi => 1, template => 'id' },
- { post => 'not', required => 0, default => '', maxlength => 2000 },
- { post => 'vns', required => 1, enum => [ -2, -1, keys %VNLIST_STATUS, 999 ] },
- { post => 'rel', required => 1, enum => [ -2, -1, keys %RLIST_STATUS ] },
- );
- my @vid = grep $_ > 0, @{$frm->{vid}};
- my @rid = grep $_ > 0, @{$frm->{rid}};
- if(!$frm->{_err} && @vid && $frm->{vns} > -2) {
- $self->dbVNListDel($uid, \@vid) if $frm->{vns} == -1;
- $self->dbVNListAdd($uid, \@vid, $frm->{vns}) if $frm->{vns} >= 0 && $frm->{vns} < 999;
- $self->dbVNListAdd($uid, \@vid, undef, $frm->{not}) if $frm->{vns} == 999;
- }
- if(!$frm->{_err} && @rid && $frm->{rel} > -2) {
- $self->dbRListDel($uid, \@rid) if $frm->{rel} == -1;
- $self->dbRListAdd($uid, \@rid, $frm->{rel}) if $frm->{rel} >= 0;
- }
- }
-
- my($list, $np) = $self->dbVNListList(
- uid => $uid,
- results => 50,
- page => $f->{p},
- sort => $f->{s}, reverse => $f->{o} eq 'd',
- voted => $f->{v} == 0 ? undef : $f->{v} < 0 ? 0 : $f->{v},
- $f->{c} ne 'all' ? (char => $f->{c}) : (),
- $f->{t} >= 0 ? (status => $f->{t}) : (),
- );
-
- my $title = $own ? 'My visual novel list' : VNWeb::HTML::user_displayname($u)."'s visual novel list";
- $self->htmlHeader(title => $title, noindex => 1, type => 'u', dbobj => $u);
- $self->htmlMainTabs('u', $u, 'list');
-
- # url generator
- my $url = sub {
- my($n, $v) = @_;
- $n ||= '';
- local $_ = "/u$uid/list";
- $_ .= '?c='.($n eq 'c' ? $v : $f->{c});
- $_ .= ';v='.($n eq 'v' ? $v : $f->{v});
- $_ .= ';t='.($n eq 't' ? $v : $f->{t});
- if($n eq 'page') {
- $_ .= ';o='.($n eq 'o' ? $v : $f->{o});
- $_ .= ';s='.($n eq 's' ? $v : $f->{s});
- }
- return $_;
- };
-
- div class => 'mainbox';
- h1 $title;
- p class => 'browseopts';
- for ('all', 'a'..'z', 0) {
- a href => $url->(c => $_), $_ eq $f->{c} ? (class => 'optselected') : (), $_ eq 'all' ? 'ALL' : $_ ? uc $_ : '#';
- }
- end;
- p class => 'browseopts';
- a href => $url->(v => 0), 0 == $f->{v} ? (class => 'optselected') : (), 'All';
- a href => $url->(v => 1), 1 == $f->{v} ? (class => 'optselected') : (), 'Only voted';
- a href => $url->(v => -1), -1 == $f->{v} ? (class => 'optselected') : (), 'Hide voted';
- end;
- p class => 'browseopts';
- a href => $url->(t => -1), -1 == $f->{t} ? (class => 'optselected') : (), 'All';
- a href => $url->(t => $_), $_ == $f->{t} ? (class => 'optselected') : (), $VNLIST_STATUS{$_} for keys %VNLIST_STATUS;
- end;
- end 'div';
-
- _vnlist_browse($self, $own, $list, $np, $f, $url, $uid);
- $self->htmlFooter;
-}
-
-sub _vnlist_browse {
- my($self, $own, $list, $np, $f, $url, $uid) = @_;
-
- if($own) {
- form action => $url->(), method => 'post';
- input type => 'hidden', class => 'hidden', name => 'not', id => 'not', value => '';
- input type => 'hidden', class => 'hidden', name => 'formcode', id => 'formcode', value => $self->authGetCode("/u$uid/list");
- }
-
- $self->htmlBrowse(
- class => 'rlist',
- items => $list,
- nextpage => $np,
- options => $f,
- sorturl => $url->(),
- pageurl => $url->('page'),
- header => [
- [ '' ],
- sub { td class => 'tc2', id => 'expandall'; lit '&#9656;'; end; },
- [ 'Title' => 'title' ],
- [ '' ], [ '' ],
- [ 'Status' ],
- [ 'Releases*' ],
- [ 'Vote' => 'vote' ],
- ],
- row => sub {
- my($s, $n, $i) = @_;
- Tr class => 'nostripe'.($n%2 ? ' odd' : '');
- td class => 'tc1'; input type => 'checkbox', name => 'vid', value => $i->{vid} if $own; end;
- if(@{$i->{rels}}) {
- td class => 'tc2 collapse_but', id => "vid$i->{vid}"; lit '&#9656;'; end;
- } else {
- td class => 'tc2', '';
- }
- td class => 'tc3_5', colspan => 3;
- a href => "/v$i->{vid}", title => $i->{original}||$i->{title}, shorten $i->{title}, 70;
- b class => 'grayedout', $i->{notes} if $i->{notes};
- end;
- td class => 'tc6', $i->{status} ? $VNLIST_STATUS{$i->{status}} : '';
- td class => 'tc7';
- my $obtained = grep $_->{status}==2, @{$i->{rels}};
- my $total = scalar @{$i->{rels}};
- my $txt = sprintf '%d/%d', $obtained, $total;
- $txt = qq|<b class="done">$txt</b>| if $total && $obtained == $total;
- $txt = qq|<b class="todo">$txt</b>| if $obtained < $total;
- lit $txt;
- end;
- td class => 'tc8', fmtvote $i->{vote};
- end 'tr';
-
- for (@{$i->{rels}}) {
- Tr class => "nostripe collapse relhid collapse_vid$i->{vid}".($n%2 ? ' odd':'');
- td class => 'tc1', '';
- td class => 'tc2';
- input type => 'checkbox', name => 'rid', value => $_->{rid} if $own;
- end;
- td class => 'tc3';
- lit fmtdatestr $_->{released};
- end;
- td class => 'tc4';
- cssicon "lang $_", $LANGUAGE{$_} for @{$_->{languages}};
- cssicon "rt$_->{type}", $_->{type};
- end;
- td class => 'tc5';
- a href => "/r$_->{rid}", title => $_->{original}||$_->{title}, shorten $_->{title}, 50;
- end;
- td class => 'tc6', $_->{status} ? $RLIST_STATUS{$_->{status}} : '';
- td class => 'tc7_8', colspan => 2, '';
- end 'tr';
- }
- },
-
- $own ? (footer => sub {
- Tr;
- td class => 'tc1'; input type => 'checkbox', name => 'vid', value => 0, class => 'checkall'; end;
- td class => 'tc2'; input type => 'checkbox', name => 'rid', value => 0, class => 'checkall'; end;
- td class => 'tc3_6', colspan => 4;
- Select id => 'vns', name => 'vns';
- option value => -2, '-- with selected VNs --';
- optgroup label => 'Change status';
- option value => $_, $VNLIST_STATUS{$_}
- for (keys %VNLIST_STATUS);
- end;
- option value => 999, 'Set note';
- option value => -1, 'remove from list';
- end;
- Select id => 'rel', name => 'rel';
- option value => -2, '-- with selected releases --';
- optgroup label => 'Change status';
- option value => $_, $RLIST_STATUS{$_}
- for (keys %RLIST_STATUS);
- end;
- option value => -1, 'remove from list';
- end;
- input type => 'submit', value => 'Update';
- end;
- td class => 'tc7_8', colspan => 2, '* Obtained/total';
- end 'tr';
- }) : (),
- );
-
- end 'form' if $own;
-}
-
1;
diff --git a/lib/VNDB/Handler/VNBrowse.pm b/lib/VNDB/Handler/VNBrowse.pm
index b3ec9dc6..64cc57d4 100644
--- a/lib/VNDB/Handler/VNBrowse.pm
+++ b/lib/VNDB/Handler/VNBrowse.pm
@@ -26,7 +26,6 @@ sub list {
{ get => 'rfil', required => 0, default => '' },
{ get => 'cfil', required => 0, default => '' },
{ get => 'vnlist', required => 0, default => 2, enum => [ '0', '1' ] }, # 2: use pref
- { get => 'wish', required => 0, default => 2, enum => [ '0', '1' ] }, # 2: use pref
);
return $self->resNotFound if $f->{_err};
$f->{q} ||= $f->{sq};
@@ -45,7 +44,6 @@ sub list {
};
$f->{vnlist} = $read_write_pref->('vnlist', 'vn_list_own');
- $f->{wish} = $read_write_pref->('wish', 'vn_list_wish');
return $self->resRedirect('/'.$1.$2.(!$3 ? '' : $1 eq 'd' ? '#'.$3 : '.'.$3), 'temp')
if $f->{q} && $f->{q} =~ /^([gvrptudcis])([0-9]+)(?:\.([0-9]+))?$/;
@@ -64,9 +62,7 @@ sub list {
%compat,
tagspoil => $self->authPref('spoilers')||0,
}, {
- what => ' rating' .
- ($f->{vnlist} ? ' vnlist' : '').
- ($f->{wish} ? ' wishlist' : ''),
+ what => ' rating'.($f->{vnlist} ? ' vnlist' : ''),
$char ne 'all' ? ( char => $char ) : (),
$f->{q} ? ( search => $f->{q} ) : (),
keys %$rfil ? ( release => $rfil ) : (),
@@ -103,7 +99,6 @@ sub list {
if($uid) {
p class => 'browseopts';
a href => $url->($char, 'vnlist'), $f->{vnlist} ? (class => 'optselected') : (), 'User VN list';
- a href => $url->($char, 'wish' ), $f->{wish} ? (class => 'optselected') : (), 'Wishlist';
end 'p';
}
diff --git a/lib/VNDB/Handler/VNPage.pm b/lib/VNDB/Handler/VNPage.pm
index 87c9244c..3556476c 100644
--- a/lib/VNDB/Handler/VNPage.pm
+++ b/lib/VNDB/Handler/VNPage.pm
@@ -531,7 +531,7 @@ sub page {
_screenshots($self, $v, $r) if @{$v->{screenshots}};
}
- $self->htmlFooter;
+ $self->htmlFooter(v2rwjs => $self->authInfo->{id});
}
@@ -715,45 +715,28 @@ sub _useroptions {
# Voting option is hidden if nothing has been released yet
my $minreleased = min grep $_, map $_->{released}, @$r;
- my $canvote = $minreleased && $minreleased < strftime '%Y%m%d', gmtime;
- my $vote = $self->dbVoteGet(uid => $self->authInfo->{id}, vid => $v->{id})->[0];
- my $list = $self->dbVNListGet(uid => $self->authInfo->{id}, vid => $v->{id})->[0];
- my $wish = $self->dbWishListGet(uid => $self->authInfo->{id}, vid => $v->{id})->[0];
+ my $labels = tuwf->dbAlli(
+ 'SELECT l.id, l.label, l.private, uvl.vid IS NOT NULL as assigned
+ FROM ulist_labels l
+ LEFT JOIN ulist_vns_labels uvl ON uvl.uid = l.uid AND uvl.lbl = l.id AND uvl.vid =', \$v->{id}, '
+ WHERE l.uid =', \$self->authInfo->{id}, '
+ ORDER BY CASE WHEN l.id < 10 THEN l.id ELSE 10 END, l.label'
+ );
+ my $lst = tuwf->dbRowi('SELECT vid, vote FROM ulist_vns WHERE uid =', \$self->authInfo->{id}, 'AND vid =', \$v->{id});
Tr;
td 'User options';
td;
- if($vote || ($canvote && !$wish)) {
- Select id => 'votesel', name => $self->authGetCode("/v$v->{id}/vote");
- option value => -3, $vote ? 'your vote: '.fmtvote($vote->{vote}) : 'not voted yet';
- optgroup label => $vote ? 'Change vote' : 'Vote';
- option value => $_, "$_ (".fmtrating($_).')' for (reverse 1..10);
- option value => -2, 'Other';
- end;
- option value => -1, 'revoke' if $vote;
- end;
- br;
- }
-
- Select id => 'listsel', name => $self->authGetCode("/v$v->{id}/list");
- option $list ? "VN list: $VNLIST_STATUS{$list->{status}}" : 'not on your VN list';
- optgroup label => $list ? 'Change status' : 'Add to VN list';
- option value => $_, $VNLIST_STATUS{$_} for (keys %VNLIST_STATUS);
- end;
- option value => -1, 'remove from VN list' if $list;
- end;
- br;
-
- if(!$vote || $wish) {
- Select id => 'wishsel', name => $self->authGetCode("/v$v->{id}/wish");
- option $wish ? "wishlist: $WISHLIST_STATUS{$wish->{wstat}}" : 'not on your wishlist';
- optgroup label => $wish ? 'Change status' : 'Add to wishlist';
- option value => $_, $WISHLIST_STATUS{$_} for (keys %WISHLIST_STATUS);
- end;
- option value => -1, 'remove from wishlist' if $wish;
- end;
- }
+ VNWeb::HTML::elm_('UList.VNPage', undef, {
+ uid => 1*$self->authInfo->{id},
+ vid => 1*$v->{id},
+ onlist => $lst->{vid}?\1:\0,
+ canvote => $minreleased && $minreleased < strftime('%Y%m%d', gmtime) ? \1 : \0,
+ vote => fmtvote($lst->{vote}).'',
+ labels => [ map +{ id => 1*$_->{id}, label => $_->{label}, private => $_->{private}?\1:\0 }, @$labels ],
+ selected => [ map $_->{id}, grep $_->{assigned}, @$labels ],
+ });
end;
end 'tr';
}
diff --git a/lib/VNDB/Types.pm b/lib/VNDB/Types.pm
index 8e5b26cf..3341343d 100644
--- a/lib/VNDB/Types.pm
+++ b/lib/VNDB/Types.pm
@@ -267,14 +267,6 @@ hash RELEASE_TYPE =>
-hash WISHLIST_STATUS =>
- 0 => 'High',
- 1 => 'Medium',
- 2 => 'Low',
- 3 => 'Blacklist';
-
-
-
# 0 = hardcoded "unknown", 2 = hardcoded 'OK'
hash RLIST_STATUS =>
0 => 'Unknown',
@@ -285,15 +277,6 @@ hash RLIST_STATUS =>
-hash VNLIST_STATUS =>
- 0 => 'Unknown',
- 1 => 'Playing',
- 2 => 'Finished',
- 3 => 'Stalled',
- 4 => 'Dropped';
-
-
-
# SQL: ENUM board_type
hash BOARD_TYPE =>
an => { txt => 'Announcements', post_perm => 'boardmod', index_rows => 5, dbitem => 0 },
diff --git a/lib/VNDB/Util/BrowseHTML.pm b/lib/VNDB/Util/BrowseHTML.pm
index 7846d5c0..1a7e3878 100644
--- a/lib/VNDB/Util/BrowseHTML.pm
+++ b/lib/VNDB/Util/BrowseHTML.pm
@@ -160,9 +160,10 @@ sub htmlBrowseVN {
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 => 'tc8', defined($l->{wstat}) ? $WISHLIST_STATUS{$l->{wstat}} : '' if $f->{wish};
td class => 'tc2';
$_ ne 'oth' && cssicon $_, $PLATFORM{$_}
for (sort @{$l->{c_platforms}});
diff --git a/lib/VNDB/Util/CommonHTML.pm b/lib/VNDB/Util/CommonHTML.pm
index 9472f53d..7a3d554c 100644
--- a/lib/VNDB/Util/CommonHTML.pm
+++ b/lib/VNDB/Util/CommonHTML.pm
@@ -215,6 +215,7 @@ sub htmlItemMessage {
# generates two tables, one with a vote graph, other with recent votes
+# Only supports $type eq 'v' now.
sub htmlVoteStats {
my($self, $type, $obj, $stats) = @_;
@@ -244,12 +245,17 @@ sub htmlVoteStats {
}
end 'table';
- my $recent = $self->dbVoteGet(
- $type.'id' => $obj->{id},
- results => 8,
- what => $type eq 'v' ? 'user hide_list' : 'vn',
- hide_ign => $type eq 'v',
+ my $recent = $self->dbAlli('
+ SELECT uv.vote,', VNWeb::DB::sql_totime('uv.vote_date '), 'as date, ', VNWeb::DB::sql_user(), '
+ , NOT EXISTS(SELECT 1 FROM ulist_vns_labels uvl JOIN ulist_labels ul ON ul.uid = uvl.uid AND ul.id = uvl.lbl WHERE uvl.uid = uv.uid AND uvl.vid = uv.vid AND NOT ul.private) AS hide_list
+ FROM ulist_vns uv
+ JOIN users u ON u.id = uv.uid
+ WHERE uv.vid =', \$obj->{id}, 'AND uv.vote IS NOT NULL
+ AND NOT EXISTS(SELECT 1 FROM users u WHERE u.id = uv.uid AND u.ign_votes)
+ ORDER BY uv.vote_date DESC
+ LIMIT', \8
);
+
if(@$recent) {
table class => 'recentvotes stripe';
thead; Tr;
@@ -265,9 +271,7 @@ sub htmlVoteStats {
for (@$recent) {
Tr;
td;
- if($type eq 'u') {
- a href => "/v$_->{vid}", title => $_->{original}||$_->{title}, shorten $_->{title}, 40;
- } elsif($_->{hide_list}) {
+ if($_->{hide_list}) {
b class => 'grayedout', 'hidden';
} else {
VNWeb::HTML::user_($_);
diff --git a/lib/VNDB/Util/LayoutHTML.pm b/lib/VNDB/Util/LayoutHTML.pm
index 1b6ce1de..6bafbeda 100644
--- a/lib/VNDB/Util/LayoutHTML.pm
+++ b/lib/VNDB/Util/LayoutHTML.pm
@@ -11,6 +11,7 @@ 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};
@@ -34,6 +35,7 @@ sub htmlFooter { # %options => { pref_code => 1 }
noscript id => 'pref_code', title => $self->authGetCode('/xml/prefs.xml'), ''
if $o{pref_code} && $self->authInfo->{id};
script type => 'text/javascript', src => $self->{url_static}.'/f/vndb.js?'.$self->{version}, '';
+ VNWeb::HTML::v2rwjs_() if $o{v2rwjs};
end 'body';
end 'html';
}
diff --git a/lib/VNWeb/Auth.pm b/lib/VNWeb/Auth.pm
index 56367397..35587c8d 100644
--- a/lib/VNWeb/Auth.pm
+++ b/lib/VNWeb/Auth.pm
@@ -273,7 +273,7 @@ sub csrfcheck {
# TODO: Measure global usage of the pref() and prefSet() calls to see if this cache is actually necessary.
my @pref_columns = qw/
- email_confirmed skin customcss filter_vn filter_release show_nsfw hide_list notify_dbedit notify_announce
+ email_confirmed skin customcss filter_vn filter_release show_nsfw notify_dbedit notify_announce
vn_list_own vn_list_wish tags_all tags_cont tags_ero tags_tech spoilers traits_sexual
nodistract_can nodistract_noads nodistract_nofancy
/;
diff --git a/lib/VNWeb/HTML.pm b/lib/VNWeb/HTML.pm
index 0492fc7d..1d6731c2 100644
--- a/lib/VNWeb/HTML.pm
+++ b/lib/VNWeb/HTML.pm
@@ -36,7 +36,7 @@ our @EXPORT = qw/
# Encoded as JSON and appended to the end of the page, to be read by pagevars.js.
-my %pagevars;
+our %pagevars;
# Ugly hack to move rendering down below the float object.
@@ -226,9 +226,9 @@ sub _menu_ {
h2_ sub { user_ auth->user, 'user_', 1 };
div_ sub {
a_ href => "$uid/edit", 'My Profile'; txt_ '⭐' if $support_opt && !auth->pref('nodistract_nofancy'); br_;
- a_ href => "$uid/list", 'My Visual Novel List'; br_;
- a_ href => "$uid/votes",'My Votes'; br_;
- a_ href => "$uid/wish", 'My Wishlist'; br_;
+ a_ href => "$uid/ulist?vnlist=1", 'My Visual Novel List'; br_;
+ a_ href => "$uid/ulist?votes=1",'My Votes'; br_;
+ a_ href => "$uid/ulist?wishlist=1", 'My Wishlist'; br_;
a_ href => "$uid/notifies", $nc ? (class => 'notifyget') : (), 'My Notifications'.($nc?" ($nc)":''); br_;
a_ href => "$uid/hist", 'My Recent Changes'; br_;
a_ href => '/g/links?u='.auth->uid, 'My Tags'; br_;
@@ -344,13 +344,10 @@ sub _maintabs_ {
t tagmod => "/$id/tagmod", 'modify tags' if $t eq 'v' && auth->permTag && !$o->{entry_hidden};
do {
- t list => "/$id/list", 'list';
- t votes => "/$id/votes", 'votes';
- t wish => "/$id/wish", 'wishlist';
- } if $t eq 'u' && (
- auth->permUsermod || (auth && auth->uid == $o->{id})
- || !($o->{hide_list} // tuwf->dbVali('SELECT hide_list FROM users WHERE id =', \$o->{id}))
- );
+ t list => "/$id/ulist?vnlist=1", 'list';
+ t votes => "/$id/ulist?votes=1", 'votes';
+ t wish => "/$id/ulist?wishlist=1", 'wishlist';
+ } if $t eq 'u';
t posts => "/$id/posts", 'posts' if $t eq 'u';
@@ -409,6 +406,15 @@ sub _hidden_msg_ {
}
+sub v2rwjs_ { # Also used by VNDB::Util::LayoutHTML.
+ 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(\%pagevars) =~ s{</}{<\\/}rg =~ s/<!--/<\\u0021--/rg);
+ } if keys %pagevars;
+ script_ type => 'application/javascript', src => config->{url_static}.'/f/v2rw.js?'.config->{version}, '';
+}
+
+
# Options:
# title => $title
# index => 1/0, default 0
@@ -438,11 +444,7 @@ sub framework_ {
$cont->() unless $o{hiddenmsg} && _hidden_msg_ \%o;
div_ id => 'footer', \&_footer_;
};
- 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(\%pagevars) =~ s{</}{<\\/}rg =~ s/<!--/<\\u0021--/rg);
- } if keys %pagevars;
- script_ type => 'application/javascript', src => config->{url_static}.'/f/v2rw.js?'.config->{version}, '';
+ v2rwjs_;
}
}
}
diff --git a/lib/VNWeb/User/Edit.pm b/lib/VNWeb/User/Edit.pm
index e34ef0ba..82e08729 100644
--- a/lib/VNWeb/User/Edit.pm
+++ b/lib/VNWeb/User/Edit.pm
@@ -8,7 +8,6 @@ my $FORM = form_compile in => {
email => { email => 1 },
perm => { uint => 1, func => sub { ($_[0] & ~auth->allPerms) == 0 } },
ign_votes => { anybool => 1 },
- hide_list => { anybool => 1 },
show_nsfw => { anybool => 1 },
traits_sexual => { anybool => 1 },
tags_all => { anybool => 1 },
@@ -51,7 +50,7 @@ sub _getmail {
TUWF::get qr{/$RE{uid}/edit}, sub {
my $u = tuwf->dbRowi(q{
- SELECT id, username, perm, ign_votes, hide_list, show_nsfw, traits_sexual
+ SELECT id, username, perm, ign_votes, show_nsfw, traits_sexual
, tags_all, tags_cont, tags_ero, tags_tech, spoilers, skin, customcss
, nodistract_can, nodistract_noads, nodistract_nofancy, support_can, support_enabled, uniname_can, uniname, pubskin_can, pubskin_enabled
FROM users WHERE id =}, \tuwf->capture('id')
@@ -140,7 +139,7 @@ json_api qr{/u/edit\.json}, $FORM, sub {
$data->{skin} = '' if $data->{skin} eq config->{skin_default};
$data->{uniname} = '' if $data->{uniname} eq $data->{username};
tuwf->dbExeci('UPDATE users SET', { %{$data}{qw/
- hide_list show_nsfw traits_sexual tags_all tags_cont tags_ero tags_tech spoilers skin customcss
+ show_nsfw traits_sexual tags_all tags_cont tags_ero tags_tech spoilers skin customcss
nodistract_noads nodistract_nofancy support_enabled uniname pubskin_enabled
/} },
'WHERE id =', \$data->{id}
diff --git a/lib/VNWeb/User/List.pm b/lib/VNWeb/User/List.pm
index c3694ddc..72da203d 100644
--- a/lib/VNWeb/User/List.pm
+++ b/lib/VNWeb/User/List.pm
@@ -14,28 +14,33 @@ sub listing_ {
thead_ sub { tr_ sub {
td_ class => 'tc1', sub { txt_ 'Username'; sortable_ 'username', $opt, \&url };
td_ class => 'tc2', sub { txt_ 'Registered'; sortable_ 'registered', $opt, \&url };
- td_ class => 'tc3', sub { txt_ 'Votes'; sortable_ 'votes', $opt, \&url };
- td_ class => 'tc4', sub { txt_ 'Edits'; sortable_ 'changes', $opt, \&url };
- td_ class => 'tc5', sub { txt_ 'Tags'; sortable_ 'tags', $opt, \&url };
+ td_ class => 'tc3', sub { txt_ 'VNs'; sortable_ 'vns', $opt, \&url };
+ td_ class => 'tc4', sub { txt_ 'Votes'; sortable_ 'votes', $opt, \&url };
+ td_ class => 'tc5', sub { txt_ 'Wishlist'; sortable_ 'wish', $opt, \&url };
+ td_ class => 'tc6', sub { txt_ 'Edits'; sortable_ 'changes', $opt, \&url };
+ td_ class => 'tc7', sub { txt_ 'Tags'; sortable_ 'tags', $opt, \&url };
} };
tr_ sub {
my $l = $_;
td_ class => 'tc1', sub { user_ $l };
td_ class => 'tc2', fmtdate $l->{registered};
- td_ mkclass(tc3 => 1, linethrough => $l->{hide_list} && auth->permUsermod), sub {
- if($l->{hide_list} && !auth->permUsermod) {
- txt_ '-';
- } elsif(!$l->{c_votes}) {
- txt_ '0';
- } else {
- a_ href => "/u$l->{user_id}/votes", $l->{c_votes};
- }
+ td_ class => 'tc3', sub {
+ txt_ '0' if !$l->{c_vns};
+ a_ href => "/u$l->{user_id}/ulist?vnlist=1", $l->{c_vns} if $l->{c_vns};
};
td_ class => 'tc4', sub {
+ txt_ '0' if !$l->{c_votes};
+ a_ href => "/u$l->{user_id}/ulist?votes=1", $l->{c_votes} if $l->{c_votes};
+ };
+ td_ class => 'tc5', sub {
+ txt_ '0' if !$l->{c_wish};
+ a_ href => "/u$l->{user_id}/ulist?wishlist=1", $l->{c_wish} if $l->{c_wish};
+ };
+ td_ class => 'tc6', sub {
txt_ '-' if !$l->{c_changes};
a_ href => "/u$l->{user_id}/hist", $l->{c_changes} if $l->{c_changes};
};
- td_ class => 'tc5', sub {
+ td_ class => 'tc7', sub {
txt_ '-' if !$l->{c_tags};
a_ href => "/g/links?u=$l->{user_id}", $l->{c_tags} if $l->{c_tags};
};
@@ -51,7 +56,7 @@ TUWF::get qr{/u/(?<char>[0a-z]|all)}, sub {
my $opt = eval { tuwf->validate(get =>
p => { upage => 1 },
- s => { required => 0, default => 'registered', enum => [qw[username registered votes changes tags]] },
+ s => { required => 0, default => 'registered', enum => [qw[username registered vns votes wish changes tags]] },
o => { required => 0, default => 'd', enum => [qw[a d]] },
q => { required => 0, default => '' },
)->data } || return tuwf->resNotFound;
@@ -65,13 +70,15 @@ TUWF::get qr{/u/(?<char>[0a-z]|all)}, sub {
);
my $list = tuwf->dbPagei({ results => 50, page => $opt->{p} },
- 'SELECT', sql_user(), ',', sql_totime('registered'), 'as registered, c_votes, c_changes, c_tags, hide_list
+ 'SELECT', sql_user(), ',', sql_totime('registered'), 'as registered, c_vns, c_votes, c_wish, c_changes, c_tags
FROM users u
WHERE', sql_and('id > 0', @where),
'ORDER BY', {
username => 'username',
registered => 'id',
- votes => auth->permUsermod ? 'c_votes' : 'hide_list, c_votes',
+ vns => 'c_vns',
+ votes => 'c_votes',
+ wish => 'c_wish',
changes => 'c_changes',
tags => 'c_tags'
}->{$opt->{s}}, $opt->{o} eq 'd' ? 'DESC' : 'ASC'
diff --git a/lib/VNWeb/User/Lists.pm b/lib/VNWeb/User/Lists.pm
index 4a8bb5d1..2c0548c1 100644
--- a/lib/VNWeb/User/Lists.pm
+++ b/lib/VNWeb/User/Lists.pm
@@ -3,6 +3,18 @@ package VNWeb::User::Lists;
use VNWeb::Prelude;
+# Do we have "ownership" access to this users' list (i.e. can we edit and see private stuff)?
+sub own {
+ auth->permUsermod || (auth && auth->uid == shift)
+}
+
+
+# Should be called after any change to the ulist_* tables.
+# (Normally I'd do this with triggers, but that seemed like a more complex and less efficient solution in this case)
+sub updcache {
+ tuwf->dbExeci(SELECT => sql_func update_users_ulist_stats => \shift);
+}
+
my $LABELS = form_compile any => {
uid => { id => 1 },
@@ -19,7 +31,7 @@ elm_form 'UListManageLabels', undef, $LABELS;
json_api qr{/u/ulist/labels\.json}, $LABELS, sub {
my($uid, $labels) = ($_[0]{uid}, $_[0]{labels});
- return elm_Unauth if !auth || auth->uid != $uid;
+ return elm_Unauth if !own $uid;
# Insert new labels
my @new = grep $_->{id} < 0 && !$_->{delete}, @$labels;
@@ -63,6 +75,7 @@ json_api qr{/u/ulist/labels\.json}, $LABELS, sub {
# (This will also delete all relevant vn<->label rows from ulist_vns_labels)
tuwf->dbExeci('DELETE FROM ulist_labels WHERE uid =', \$uid, 'AND id IN', [ map $_->{id}, @delete ]) if @delete;
+ updcache $uid;
elm_Success
};
@@ -79,7 +92,7 @@ elm_form 'UListVoteEdit', undef, $VNVOTE;
json_api qr{/u/ulist/setvote\.json}, $VNVOTE, sub {
my($data) = @_;
- return elm_Unauth if !auth || auth->uid != $data->{uid};
+ return elm_Unauth if !own $data->{uid};
tuwf->dbExeci(
'UPDATE ulist_vns
SET vote =', \$data->{vote},
@@ -87,6 +100,8 @@ json_api qr{/u/ulist/setvote\.json}, $VNVOTE, sub {
', lastmod = NOW()
WHERE uid =', \$data->{uid}, 'AND vid =', \$data->{vid}
);
+
+ updcache $data->{uid};
elm_Success
};
@@ -109,7 +124,7 @@ elm_form 'UListLabelEdit', $VNLABELS_OUT, $VNLABELS_IN;
json_api qr{/u/ulist/setlabel\.json}, $VNLABELS_IN, sub {
my($data) = @_;
- return elm_Unauth if !auth || auth->uid != $data->{uid};
+ return elm_Unauth if !own $data->{uid};
die "Attempt to set vote label" if $data->{label} == 7;
tuwf->dbExeci(
@@ -122,6 +137,7 @@ json_api qr{/u/ulist/setlabel\.json}, $VNLABELS_IN, sub {
) if $data->{applied};
tuwf->dbExeci('UPDATE ulist_vns SET lastmod = NOW() WHERE uid =', \$data->{uid}, 'AND vid =', \$data->{vid});
+ updcache $data->{uid};
elm_Success
};
@@ -139,11 +155,12 @@ elm_form 'UListDateEdit', undef, $VNDATE;
json_api qr{/u/ulist/setdate\.json}, $VNDATE, sub {
my($data) = @_;
- return elm_Unauth if !auth || auth->uid != $data->{uid};
+ return elm_Unauth if !own $data->{uid};
tuwf->dbExeci(
'UPDATE ulist_vns SET lastmod = NOW(), ', $data->{start} ? 'started' : 'finished', '=', \($data->{date}||undef),
'WHERE uid =', \$data->{uid}, 'AND vid =', \$data->{vid}
);
+ updcache $data->{uid};
elm_Success
};
@@ -181,11 +198,12 @@ elm_form 'UListVNNotes', undef, $VNNOTES;
json_api qr{/u/ulist/setnote\.json}, $VNNOTES, sub {
my($data) = @_;
- return elm_Unauth if !auth || auth->uid != $data->{uid};
+ return elm_Unauth if !own $data->{uid};
tuwf->dbExeci(
'UPDATE ulist_vns SET lastmod = NOW(), notes = ', \$data->{notes},
'WHERE uid =', \$data->{uid}, 'AND vid =', \$data->{vid}
);
+ # Doesn't need `updcache()`
elm_Success
};
@@ -201,13 +219,33 @@ elm_form 'UListDel', undef, $VNDEL;
json_api qr{/u/ulist/del\.json}, $VNDEL, sub {
my($data) = @_;
- return elm_Unauth if !auth || auth->uid != $data->{uid};
+ return elm_Unauth if !own $data->{uid};
tuwf->dbExeci('DELETE FROM ulist_vns WHERE uid =', \$data->{uid}, 'AND vid =', \$data->{vid});
+ updcache $data->{uid};
+ elm_Success
+};
+
+
+
+
+my $VNADD = form_compile any => {
+ uid => { id => 1 },
+ vid => { id => 1 },
+};
+
+elm_form 'UListAdd', undef, $VNADD;
+
+json_api qr{/u/ulist/add\.json}, $VNDEL, sub {
+ my($data) = @_;
+ return elm_Unauth if !own $data->{uid};
+ tuwf->dbExeci('INSERT INTO ulist_vns', $data, 'ON CONFLICT (uid, vid) DO NOTHING');
+ updcache $data->{uid};
elm_Success
};
+
my $RSTATUS = form_compile any => {
uid => { id => 1 },
rid => { id => 1 },
@@ -219,50 +257,51 @@ elm_form 'UListRStatus', undef, $RSTATUS;
# Adds the release when not in the list.
json_api qr{/u/ulist/rstatus\.json}, $RSTATUS, sub {
my($data) = @_;
- return elm_Unauth if !auth || auth->uid != $data->{uid};
+ return elm_Unauth if !own $data->{uid};
if($data->{status} == -1) {
tuwf->dbExeci('DELETE FROM rlists WHERE uid =', \$data->{uid}, 'AND rid =', \$data->{rid})
} else {
tuwf->dbExeci('INSERT INTO rlists', $data, 'ON CONFLICT (uid, rid) DO UPDATE SET status =', \$data->{status})
}
+ # Doesn't need `updcache()`
elm_Success
};
-sub filters_ {
- my($uid, $own, $labels) = @_;
-
- my @filtlabels = (
- @$labels,
- $own ? {
- id => -1, label => 'No label', count => tuwf->dbVali(
- 'SELECT count(*)
- FROM ulist_vns uv
- WHERE NOT EXISTS(SELECT 1 FROM ulist_vns_labels uvl WHERE uvl.uid = uv.uid AND uvl.vid = uv.vid AND uvl.lbl <>', \7, ')
- AND uid =', \$uid
- )
- } : (),
- );
+sub opt {
+ my($filtlabels) = @_;
- my $opt = eval { tuwf->validate(get =>
- p => { upage => 1 },
- l => { type => 'array', scalar => 1, required => 0, default => [], values => { int => 1 } },
- s => { required => 0, default => 'title', enum => [qw[ title label vote voted added modified started finished rel rating ]] },
- o => { required => 0, default => 'a', enum => ['a', 'd'] },
- c => { type => 'array', scalar => 1, required => 0, default => [], values => { enum => [qw[ vote voted added modified started finished rel rating ]] } },
- q => { required => 0 },
- )->data } || { p => 1, l => [], s => 'title', o => 'a', c => [] };
+ my $opt =
+ # Presets
+ tuwf->reqGet('vnlist') ? { p => 1, l => [1,2,3,4,7,-1,0], s => 'title', o => 'a', c => [qw/vote added started finished/] } :
+ tuwf->reqGet('votes') ? { p => 1, l => [7], s => 'voted', o => 'd', c => [qw/vote voted/] } :
+ tuwf->reqGet('wishlist') ? { p => 1, l => [5], s => 'title', o => 'a', c => [qw/added/] } :
+ # Full options
+ eval { tuwf->validate(get =>
+ p => { upage => 1 },
+ l => { type => 'array', scalar => 1, required => 0, default => [], values => { int => 1 } },
+ s => { required => 0, default => 'title', enum => [qw[ title label vote voted added modified started finished rel rating ]] },
+ o => { required => 0, default => 'a', enum => ['a', 'd'] },
+ c => { type => 'array', scalar => 1, required => 0, default => [], values => { enum => [qw[ vote voted added modified started finished rel rating ]] } },
+ q => { required => 0 },
+ )->data } || { p => 1, l => [], s => 'title', o => 'a', c => [] };
# $labels only includes labels we are allowed to see, getting rid of any labels in 'l' that aren't in $labels ensures we only filter on visible labels
- my %accessible_labels = map +($_->{id}, 1), @filtlabels;
+ my %accessible_labels = map +($_->{id}, 1), @$filtlabels;
my %opt_l = map +($_, 1), grep $accessible_labels{$_}, $opt->{l}->@*;
%opt_l = %accessible_labels if !keys %opt_l;
$opt->{l} = keys %opt_l == keys %accessible_labels ? [] : [ sort keys %opt_l ];
+ ($opt, \%opt_l)
+}
+
+
+sub filters_ {
+ my($own, $filtlabels, $opt, $opt_labels) = @_;
my sub lblfilt_ {
- input_ type => 'checkbox', name => 'l', value => $_->{id}, id => "form_l$_->{id}", tabindex => 10, $opt_l{$_->{id}} ? (checked => 'checked') : ();
+ input_ type => 'checkbox', name => 'l', value => $_->{id}, id => "form_l$_->{id}", tabindex => 10, $opt_labels->{$_->{id}} ? (checked => 'checked') : ();
label_ for => "form_l$_->{id}", "$_->{label} ";
txt_ " ($_->{count})";
}
@@ -275,14 +314,14 @@ sub filters_ {
input_ type => 'text', class => 'text', name => 'q', value => $opt->{q}||'', style => 'width: 500px', placeholder => 'Search', tabindex => 10;
br_;
span_ class => 'linkradio', sub {
- join_ sub { em_ ' / ' }, \&lblfilt_, grep $_->{id} < 10, @filtlabels;
+ join_ sub { em_ ' / ' }, \&lblfilt_, grep $_->{id} < 10, @$filtlabels;
em_ ' | ';
input_ type => 'checkbox', name => 'l', class => 'checkall', value => 0, id => 'form_l_all', tabindex => 10, $opt->{l}->@* == 0 ? (checked => 'checked') : ();
label_ for => 'form_l_all', 'Select all';
- debug_ $labels;
+ debug_ $filtlabels;
};
- my @cust = grep $_->{id} >= 10, @$labels;
+ my @cust = grep $_->{id} >= 10, @$filtlabels;
if(@cust) {
br_;
span_ class => 'linkradio', sub {
@@ -294,7 +333,6 @@ sub filters_ {
input_ type => 'button', class => 'submit', tabindex => 10, id => 'managelabels', value => 'Manage labels' if $own;
};
};
- $opt;
}
@@ -432,7 +470,7 @@ sub listing_ {
# TODO: Thumbnail view?
paginate_ \&url, $opt->{p}, [ $count, 50 ], 't', sub {
- elm_ ColSelect => undef, [
+ elm_ ColSelect => undef, [ url(), [
[ vote => 'Vote' ],
[ voted => 'Vote date' ],
[ added => 'Added' ],
@@ -441,7 +479,7 @@ sub listing_ {
[ finished => 'Finish date' ],
[ rel => 'Release date' ],
[ rating => 'Rating' ],
- ];
+ ] ];
};
div_ class => 'mainbox browse ulist', sub {
table_ sub {
@@ -473,10 +511,9 @@ TUWF::get qr{/$RE{uid}/ulist}, sub {
my $u = tuwf->dbRowi('SELECT id,', sql_user(), 'FROM users u WHERE id =', \tuwf->capture('id'));
return tuwf->resNotFound if !$u->{id};
- my $own = auth && $u->{id} == auth->uid;
-
- return tuwf->resNotFound if !$own; # TEMPORARY while in beta.
+ my $own = own $u->{id};
+ # Visible and selectable labels
my $labels = tuwf->dbAlli(
'SELECT l.id, l.label, l.private, count(vl.vid) as count, null as delete
FROM ulist_labels l LEFT JOIN ulist_vns_labels vl ON vl.uid = l.uid AND vl.lbl = l.id
@@ -485,38 +522,35 @@ TUWF::get qr{/$RE{uid}/ulist}, sub {
ORDER BY CASE WHEN l.id < 10 THEN l.id ELSE 10 END, l.label'
);
+ # All visible labels that can be filtered on, including "virtual" labels like 'No label'
+ my $filtlabels = [
+ @$labels,
+ $own ? {
+ id => -1, label => 'No label', count => tuwf->dbVali(
+ 'SELECT count(*)
+ FROM ulist_vns uv
+ WHERE NOT EXISTS(SELECT 1 FROM ulist_vns_labels uvl WHERE uvl.uid = uv.uid AND uvl.vid = uv.vid AND uvl.lbl <>', \7, ')
+ AND uid =', \$u->{id}
+ )
+ } : (),
+ ];
+
+ my($opt, $opt_labels) = opt $filtlabels;
+
+ # This page has 3 user tabs: list, wish and votes; Select the appropriate active tab based on label filters.
+ my $num_core_labels = grep $_ < 10, keys %$opt_labels;
+ my $tab = $num_core_labels == 1 && $opt_labels->{7} ? 'votes'
+ : $num_core_labels == 1 && $opt_labels->{5} ? 'wish' : 'list';
+
my $title = $own ? 'My list' : user_displayname($u)."'s list";
- framework_ title => $title, type => 'u', dbobj => $u, tab => 'list',
+ framework_ title => $title, type => 'u', dbobj => $u, tab => $tab,
$own ? ( pagevars => {
uid => $u->{id}*1,
labels => $LABELS->analyze->{keys}{labels}->coerce_for_json($labels),
voteprivate => (map \($_->{private}?1:0), grep $_->{id} == 7, @$labels),
} ) : (),
sub {
- div_ class => 'mainbox', sub {
- p_ class => 'center', sub { b_ class => 'standout', style => 'font-size: 30px', '!BETA BETA BETA BETA!'; };
- div_ class => 'warning', sub {
- p_ 'This is a prototype for the new lists feature. It should eventually replace your visual novel list, votes and wishlist. Feel free to play around, but keep the following in mind:';
- ul_ sub {
- li_ "Changes made on this page will be lost when the feature goes live, and possibly a few times before that as well. The old visual novel list, votes and wishlist are still your primary lists.";
- li_ "Exception to the above rule: The releases are synchronized with your visual novel list, so adding/removing/changing release status here will also affect your regular visual novel list and the other way around.";
- li_ "You can not share your list or browse other people's list while this is in beta.";
- li_ sub { txt_ "More info and feedback go to "; a_ href => '/t13136', 't13136' };
- };
- };
- p_ class => 'center', sub { b_ class => 'standout', style => 'font-size: 30px', '!BETA BETA BETA BETA!'; };
- p_ class => 'center', sub {
- txt_ 'Menu links: ';
- a_ href => '?l=1&l=2&l=3&l=4&l=7&l=-1&l=0&c=vote&c=added&c=started&c=finished', 'My Visual Novel list';
- txt_ ' - ';
- a_ href => '?l=7&c=vote&c=voted&s=voted&o=d', 'My Votes';
- txt_ ' - ';
- a_ href => '?l=5&c=added', 'My Wishlist';
- };
- };
-
- my $empty = !grep $_->{count}, @$labels;
- my $opt;
+ my $empty = !grep $_->{count}, @$filtlabels;
div_ class => 'mainbox', sub {
h1_ $title;
if($empty) {
@@ -524,7 +558,7 @@ TUWF::get qr{/$RE{uid}/ulist}, sub {
? 'Your list is empty! You can add visual novels to your list from the visual novel pages.'
: user_displayname($u).' does not have any visible visual novels in their list.';
} else {
- $opt = filters_ $u->{id}, $own, $labels;
+ filters_ $own, $filtlabels, $opt, $opt_labels;
elm_ 'UList.ManageLabels' if $own;
}
};
@@ -532,4 +566,12 @@ TUWF::get qr{/$RE{uid}/ulist}, sub {
};
};
+
+
+# Redirects for old URLs
+TUWF::get qr{/$RE{uid}/votes}, sub { tuwf->resRedirect("/u".tuwf->capture('id').'/ulist?votes=1', 'perm') };
+TUWF::get qr{/$RE{uid}/list}, sub { tuwf->resRedirect("/u".tuwf->capture('id').'/ulist?vnlist=1', 'perm') };
+TUWF::get qr{/$RE{uid}/wish}, sub { tuwf->resRedirect("/u".tuwf->capture('id').'/ulist?wishlist=1', 'perm') };
+
+
1;
diff --git a/lib/VNWeb/User/Page.pm b/lib/VNWeb/User/Page.pm
index 654e3d90..5ba088e6 100644
--- a/lib/VNWeb/User/Page.pm
+++ b/lib/VNWeb/User/Page.pm
@@ -5,7 +5,7 @@ use VNWeb::Misc::History;
sub _info_table_ {
- my($u, $vis) = @_;
+ my($u, $own) = @_;
my sub sup {
b_ ' ⭐supporter⭐' if $u->{user_support_can} && $u->{user_support_enabled};
@@ -39,22 +39,30 @@ sub _info_table_ {
};
};
tr_ sub {
+ my $num = sum map $_->{votes}, $u->{votes}->@*;
+ my $sum = sum map $_->{total}, $u->{votes}->@*;
td_ 'Votes';
- td_ !$vis ? 'hidden' : !$u->{c_votes} ? '-' : sub {
- my $sum = sum map $_->{total}, $u->{votes}->@*;
- txt_ sprintf '%d vote%s, %.2f average. ', $u->{c_votes}, $u->{c_votes} == 1 ? '' : 's', $sum/$u->{c_votes}/10;
- a_ href => "/u$u->{id}/votes", 'Browse votes »';
+ td_ !$num ? '-' : sub {
+ txt_ sprintf '%d vote%s, %.2f average. ', $num, $num == 1 ? '' : 's', $sum/$num/10;
+ a_ href => "/u$u->{id}/ulist?votes=1", 'Browse votes »';
}
};
tr_ sub {
- my $vns = tuwf->dbVali('SELECT COUNT(*) FROM vnlists WHERE uid =', \$u->{id})||0;
- my $rel = tuwf->dbVali('SELECT COUNT(*) FROM rlists WHERE uid =', \$u->{id})||0;
+ my $vns = tuwf->dbVali(
+ 'SELECT COUNT(DISTINCT uvl.vid) FROM ulist_vns_labels uvl',
+ $own ? () : ('JOIN ulist_labels ul ON ul.uid = uvl.uid AND ul.id = uvl.lbl AND NOT ul.private'),
+ 'WHERE uvl.lbl NOT IN(', \5, ',', \6, ') AND uvl.uid =', \$u->{id}
+ )||0;
+ my $privrel = $own ? '1=1' : 'EXISTS(
+ SELECT 1 FROM releases_vn rv JOIN ulist_vns_labels uvl ON uvl.vid = rv.vid JOIN ulist_labels ul ON ul.id = uvl.lbl AND ul.uid = uvl.uid WHERE rv.id = r.rid AND uvl.uid = r.uid AND NOT ul.private
+ )';
+ my $rel = tuwf->dbVali('SELECT COUNT(*) FROM rlists r WHERE', $privrel, 'AND r.uid =', \$u->{id})||0;
td_ 'List stats';
- td_ !$vis ? 'hidden' : !$vns && !$rel ? '-' : sub {
+ td_ !$vns && !$rel ? '-' : sub {
txt_ sprintf '%d release%s of %d visual novel%s. ',
$rel, $rel == 1 ? '' : 's',
$vns, $vns == 1 ? '' : 's';
- a_ href => "/u$u->{id}/list", 'Browse list »';
+ a_ href => "/u$u->{id}/ulist?vnlist=1", 'Browse list »';
};
};
tr_ sub {
@@ -82,14 +90,15 @@ sub _info_table_ {
sub _votestats_ {
- my($u) = @_;
+ my($u, $own) = @_;
my $sum = sum map $_->{total}, $u->{votes}->@*;
my $max = max map $_->{votes}, $u->{votes}->@*;
+ my $num = sum map $_->{votes}, $u->{votes}->@*;
table_ class => 'votegraph', sub {
thead_ sub { tr_ sub { td_ colspan => 2, 'Vote stats' } };
- tfoot_ sub { tr_ sub { td_ colspan => 2, sprintf '%d vote%s total, average %.2f', $u->{c_votes}, $u->{c_votes} == 1 ? '' : 's', $sum/$u->{c_votes}/10 } };
+ tfoot_ sub { tr_ sub { td_ colspan => 2, sprintf '%d vote%s total, average %.2f', $num, $num == 1 ? '' : 's', $sum/$num/10 } };
tr_ sub {
my $num = $_;
my $votes = [grep $num == $_->{idx}, $u->{votes}->@*]->[0]{votes} || 0;
@@ -101,15 +110,21 @@ sub _votestats_ {
} for (reverse 1..10);
};
- my $recent = tuwf->dbAlli(q{
- SELECT vn.id, vn.title, vn.original, v.vote,}, sql_totime('v.date'), q{AS date
- FROM votes v JOIN vn ON vn.id = v.vid WHERE v.uid =}, \$u->{id}, 'ORDER BY v.date DESC LIMIT', \8
+ my $recent = tuwf->dbAlli('
+ SELECT vn.id, vn.title, vn.original, uv.vote,', sql_totime('uv.vote_date'), 'AS date
+ FROM ulist_vns uv',
+ $own ? () : (
+ 'JOIN ulist_labels ul ON ul.uid = uv.uid AND ul.id =', \7, 'AND NOT ul.private'
+ ), '
+ JOIN vn ON vn.id = uv.vid
+ WHERE uv.vote IS NOT NULL AND uv.uid =', \$u->{id}, '
+ ORDER BY uv.vote_date DESC LIMIT', \8
);
table_ class => 'recentvotes stripe', sub {
thead_ sub { tr_ sub { td_ colspan => 3, sub {
txt_ 'Recent votes';
- b_ sub { txt_ ' ('; a_ href => "/u$u->{id}/votes", 'show all'; txt_ ')' };
+ b_ sub { txt_ ' ('; a_ href => "/u$u->{id}/ulist?votes=1", 'show all'; txt_ ')' };
} } };
tr_ sub {
my $v = $_;
@@ -125,7 +140,7 @@ sub _votestats_ {
TUWF::get qr{/$RE{uid}}, sub {
my $u = tuwf->dbRowi(q{
- SELECT id, hide_list, c_changes, c_votes, c_tags
+ SELECT id, c_changes, c_votes, c_tags
,}, sql_totime('registered'), q{ AS registered
,}, sql_user(), q{
FROM users u
@@ -133,27 +148,30 @@ TUWF::get qr{/$RE{uid}}, sub {
);
return tuwf->resNotFound if !$u->{id};
- my $vis = !$u->{hide_list} || (auth && auth->uid == $u->{id}) || auth->permUsermod;
+ my $own = (auth && auth->uid == $u->{id}) || auth->permUsermod;
- $u->{votes} = $vis && $u->{c_votes} && tuwf->dbAlli(q{
- SELECT (vote::numeric/10)::int AS idx, COUNT(vote) as votes, SUM(vote) AS total
- FROM votes
- WHERE uid =}, \$u->{id}, q{
- GROUP BY (vote::numeric/10)::int
- });
+ $u->{votes} = tuwf->dbAlli('
+ SELECT (uv.vote::numeric/10)::int AS idx, COUNT(uv.vote) as votes, SUM(uv.vote) AS total
+ FROM ulist_vns uv',
+ $own ? () : (
+ 'JOIN ulist_labels ul ON ul.uid = uv.uid AND ul.id =', \7, 'AND NOT ul.private'
+ ), '
+ WHERE uv.vote IS NOT NULL AND uv.uid =', \$u->{id}, '
+ GROUP BY (uv.vote::numeric/10)::int
+ ');
my $title = user_displayname($u)."'s profile";
framework_ title => $title, type => 'u', dbobj => $u,
sub {
div_ class => 'mainbox userpage', sub {
h1_ $title;
- table_ class => 'stripe', sub { _info_table_ $u, $vis };
+ table_ class => 'stripe', sub { _info_table_ $u, $own };
};
div_ class => 'mainbox', sub {
h1_ 'Vote statistics';
- div_ class => 'votestats', sub { _votestats_ $u };
- } if $vis && $u->{c_votes};
+ div_ class => 'votestats', sub { _votestats_ $u, $own };
+ } if grep $_->{votes} > 0, $u->{votes}->@*;
if($u->{c_changes}) {
h1_ class => 'boxtitle', sub { a_ href => "/u$u->{id}/hist", 'Recent changes' };
diff --git a/lib/VNWeb/VN/Votes.pm b/lib/VNWeb/VN/Votes.pm
new file mode 100644
index 00000000..1d4fe774
--- /dev/null
+++ b/lib/VNWeb/VN/Votes.pm
@@ -0,0 +1,69 @@
+package VNWeb::VN::Votes;
+
+use VNWeb::Prelude;
+
+
+sub listing_ {
+ my($opt, $count, $lst) = @_;
+
+ my sub url { '?'.query_encode %$opt, @_ }
+ paginate_ \&url, $opt->{p}, [ $count, 50 ], 't';
+ div_ class => 'mainbox browse votelist', sub {
+ table_ class => 'stripe', sub {
+ thead_ sub { tr_ sub {
+ td_ class => 'tc1', sub { txt_ 'Date'; sortable_ 'date', $opt, \&url; debug_ $lst };
+ td_ class => 'tc2', sub { txt_ 'Vote'; sortable_ 'vote', $opt, \&url; };
+ td_ class => 'tc3', sub { txt_ 'User'; sortable_ 'title', $opt, \&url; };
+ } };
+ tr_ sub {
+ td_ class => 'tc1', fmtdate $_->{date};
+ td_ class => 'tc2', fmtvote $_->{vote};
+ td_ class => 'tc3', sub {
+ b_ class => 'grayedout', 'hidden' if $_->{hide_list};
+ user_ $_ if !$_->{hide_list};
+ };
+ } for @$lst;
+ };
+ };
+ paginate_ \&url, $opt->{p}, [ $count, 50 ], 'b';
+}
+
+
+TUWF::get qr{/$RE{vid}/votes}, sub {
+ my $id = tuwf->capture('id');
+ my $v = tuwf->dbRowi('SELECT id, title, hidden AS entry_hidden, locked AS entry_locked FROM vn WHERE id =', \$id);
+ return tuwf->resNotFound if !$v->{id} || $v->{hidden};
+
+ my $opt = eval { tuwf->validate(get =>
+ p => { page => 1 },
+ o => { required => 0, default => 'd', enum => ['a','d'] },
+ s => { required => 0, default => 'date', enum => ['date', 'title', 'vote' ] }
+ )->data } || { p => 1, o => 'd', s => 'date' };
+
+ my $fromwhere = sql
+ 'FROM ulist_vns uv
+ JOIN users u ON u.id = uv.uid
+ WHERE uv.vid =', \$v->{id}, 'AND uv.vote IS NOT NULL
+ AND NOT EXISTS(SELECT 1 FROM users u WHERE u.id = uv.uid AND u.ign_votes)';
+
+ my $count = tuwf->dbVali('SELECT COUNT(*)', $fromwhere);
+
+ my $lst = tuwf->dbPagei({results => 50, page => $opt->{p}},
+ 'SELECT uv.vote,', sql_totime('uv.vote_date'), 'as date, ', sql_user(), '
+ , NOT EXISTS(SELECT 1 FROM ulist_vns_labels uvl JOIN ulist_labels ul ON ul.uid = uvl.uid AND ul.id = uvl.lbl WHERE uvl.uid = uv.uid AND uvl.vid = uv.vid AND NOT ul.private) AS hide_list
+ ', $fromwhere, 'ORDER BY', sprintf
+ { date => 'uv.vote_date %s', vote => 'uv.vote %s', title => '(CASE WHEN hide_list THEN NULL ELSE u.username END) %s, uv.vote_date' }->{$opt->{s}},
+ { a => 'ASC', d => 'DESC' }->{$opt->{o}}
+ );
+
+ framework_ title => "Votes for $v->{title}", type => 'v', dbobj => $v, sub {
+ div_ class => 'mainbox', sub {
+ h1_ "Votes for $v->{title}";
+ p_ 'No votes to list. :(' if !@$lst;
+ };
+ listing_ $opt, $count, $lst if @$lst;
+ };
+};
+
+
+1;
diff --git a/static/v3/apple.svg b/static/v3/apple.svg
deleted file mode 100644
index 0181ed8a..00000000
--- a/static/v3/apple.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M247.2 137.6c-6.2 1.9-15.3 3.5-27.9 4.6 1.1-56.7 29.9-96.6 88-110.1 9.3 41.6-26.1 94.1-60.1 105.5zm121.3 72.7c6.4-9.4 16.6-19.9 30.6-31.7-22.3-27.6-48.1-44.3-85.1-44.3-35.4 0-65.2 18.2-87 18.2-18.5 0-51.9-16.1-84.5-16.1-69.6 0-106.5 68.1-106.5 139C36 354.2 95.7 480 156.2 480c23.8 0 45.2-18 73.5-18 29.3 0 52.8 17.2 80.3 17.2 46 0 88.6-77.5 102-119.7-46.8-14.3-84.4-90.2-43.5-149.2z"/></svg> \ No newline at end of file
diff --git a/static/v3/bell.svg b/static/v3/bell.svg
deleted file mode 100644
index afcffe43..00000000
--- a/static/v3/bell.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M425 331c-17-17-34-34-34-116 0-83-61-152-141-165a32 32 0 0 0 6-18 32 32 0 0 0-64 0 32 32 0 0 0 6 18C118 63 57 132 57 215c0 82-17 99-34 116a67 67 0 0 0 44 117h93a64 64 0 0 0 128 0h93c57 0 92-70 44-117zM224 472c-13 0-24-11-24-24h48c0 13-11 24-24 24zm157-72H67c-17 0-25-20-13-32 28-29 51-56 51-153a119 119 0 0 1 238 0c0 98 23 124 51 153 12 12 4 32-13 32z"/></svg> \ No newline at end of file
diff --git a/static/v3/camera-alt.svg b/static/v3/camera-alt.svg
deleted file mode 100644
index 3c2e12aa..00000000
--- a/static/v3/camera-alt.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 408c-66.2 0-120-53.8-120-120s53.8-120 120-120 120 53.8 120 120-53.8 120-120 120zm0-208c-48.5 0-88 39.5-88 88s39.5 88 88 88 88-39.5 88-88-39.5-88-88-88zm-32 88c0-17.6 14.4-32 32-32 8.8 0 16-7.2 16-16s-7.2-16-16-16c-35.3 0-64 28.7-64 64 0 8.8 7.2 16 16 16s16-7.2 16-16zM324.3 64c3.3 0 6.3 2.1 7.5 5.2l22.1 58.8H464c8.8 0 16 7.2 16 16v288c0 8.8-7.2 16-16 16H48c-8.8 0-16-7.2-16-16V144c0-8.8 7.2-16 16-16h110.2l20.1-53.6c2.3-6.2 8.3-10.4 15-10.4h131m0-32h-131c-20 0-37.9 12.4-44.9 31.1L136 96H48c-26.5 0-48 21.5-48 48v288c0 26.5 21.5 48 48 48h416c26.5 0 48-21.5 48-48V144c0-26.5-21.5-48-48-48h-88l-14.3-38c-5.8-15.7-20.7-26-37.4-26z"/></svg> \ No newline at end of file
diff --git a/static/v3/edit.svg b/static/v3/edit.svg
deleted file mode 100644
index d7b19146..00000000
--- a/static/v3/edit.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M402.3 344.9l32-32c5-5 13.7-1.5 13.7 5.7V464c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V112c0-26.5 21.5-48 48-48h273.5c7.1 0 10.7 8.6 5.7 13.7l-32 32c-1.5 1.5-3.5 2.3-5.7 2.3H48v352h352V350.5c0-2.1.8-4.1 2.3-5.6zm156.6-201.8L296.3 405.7l-90.4 10c-26.2 2.9-48.5-19.2-45.6-45.6l10-90.4L432.9 17.1c22.9-22.9 59.9-22.9 82.7 0l43.2 43.2c22.9 22.9 22.9 60 .1 82.8zM460.1 174L402 115.9 216.2 301.8l-7.3 65.3 65.3-7.3L460.1 174zm64.8-79.7l-43.2-43.2c-4.1-4.1-10.8-4.1-14.8 0L436 82l58.1 58.1 30.9-30.9c4-4.2 4-10.8-.1-14.9z"/></svg> \ No newline at end of file
diff --git a/static/v3/external-link-square-alt.svg b/static/v3/external-link-square-alt.svg
deleted file mode 100644
index 131a7b6b..00000000
--- a/static/v3/external-link-square-alt.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M400 32H48C21 32 0 53 0 80v352c0 27 21 48 48 48h352c27 0 48-21 48-48V80c0-27-21-48-48-48zm-6 400H54a6 6 0 0 1-6-6V86a6 6 0 0 1 6-6h340a6 6 0 0 1 6 6v340a6 6 0 0 1-6 6zm-54-304H204c-11 0-16 13-8 20l48 49-144 144c-5 5-5 12 0 17l22 22c5 5 12 5 17 0l144-144 49 48c7 8 20 3 20-8V140c0-7-5-12-12-12z"/></svg> \ No newline at end of file
diff --git a/static/v3/globe.svg b/static/v3/globe.svg
deleted file mode 100644
index 422636b2..00000000
--- a/static/v3/globe.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 8a248 248 0 1 0 0 496 248 248 0 0 0 0-496zm180 160h-84c-8-37-20-72-36-103 54 17 96 55 120 103zm16 128h-94c2-26 3-53 0-80h94c5 26 5 54 0 80zm-250 0c-3-26-3-53 0-80h108c3 27 3 53 0 80H202zm100 48c-9 42-25 80-46 109-21-29-37-67-46-109h92zM60 216h94c-2 26-3 52 0 80H60c-5-26-5-54 0-80zm149-48c10-42 26-80 47-109 21 29 37 67 47 109h-94zM196 65c-16 31-28 66-36 103H76c24-48 66-86 120-103zM76 344h84c8 37 20 72 36 103-54-17-97-56-120-103zm240 103c16-31 28-66 36-103h84c-24 47-66 86-120 103z"/></svg> \ No newline at end of file
diff --git a/static/v3/heavy/comment.svg b/static/v3/heavy/comment.svg
deleted file mode 100644
index f6332836..00000000
--- a/static/v3/heavy/comment.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 32C114.6 32 0 125.1 0 240c0 49.6 21.4 95 57 130.7C44.5 421.1 2.7 466 2.2 466.5c-2.2 2.3-2.8 5.7-1.5 8.7S4.8 480 8 480c66.3 0 116-31.8 140.6-51.4 32.7 12.3 69 19.4 107.4 19.4 141.4 0 256-93.1 256-208S397.4 32 256 32z"/></svg> \ No newline at end of file
diff --git a/static/v3/heavy/random.svg b/static/v3/heavy/random.svg
deleted file mode 100644
index 4e720978..00000000
--- a/static/v3/heavy/random.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M505 359c9 9 9 25 0 34l-80 80c-15 15-41 4-41-17v-40h-59a12 12 0 0 1-9-4l-70-75 53-58 53 57h32v-40c0-21 26-32 41-17l80 80zM12 176h84l53 57 53-58-70-75a12 12 0 0 0-9-4H12c-7 0-12 5-12 12v56c0 7 5 12 12 12zm372 0v40c0 21 26 32 41 17l80-80c9-9 9-25 0-34l-80-80c-15-15-41-4-41 17v40h-59a12 12 0 0 0-9 4L96 336H12c-7 0-12 5-12 12v56c0 7 5 12 12 12h111c3 0 6-1 9-4l220-236h32z"/></svg> \ No newline at end of file
diff --git a/static/v3/heavy/search.svg b/static/v3/heavy/search.svg
deleted file mode 100644
index af3df931..00000000
--- a/static/v3/heavy/search.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M505 443L405 343c-4-4-10-7-17-7h-16a208 208 0 1 0-36 36v16c0 7 3 13 7 17l100 100c9 9 24 9 34 0l28-28c9-10 9-25 0-34zM208 336a128 128 0 1 1 0-256 128 128 0 0 1 0 256z"/></svg> \ No newline at end of file
diff --git a/static/v3/linux.svg b/static/v3/linux.svg
deleted file mode 100644
index 95c14f6b..00000000
--- a/static/v3/linux.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M196 124c0-2 2-3 3-3h6v1l-3 1-3 2-3-1zm25-1l3 2 3-1c0-2-2-3-4-3l-5-1-1 2 4 1zm214 311c0 8-6 13-13 18-15 9-38 16-51 32l-3-2 3 2a73 73 0 0 1-49 28c-16 1-32-6-40-23l-2-7c-22 1-41-5-55-4-22 1-36 6-49 7-5 10-14 17-26 20-16 3-36 0-56-11l2-3-2 3c-18-9-42-9-59-12-9-2-16-5-20-12-4-8-3-18 2-32 2-5 1-13-1-21l-1-12 3-12c5-9 12-12 19-15 6-2 12-4 17-8l16-20c-2-17 0-36 6-53 13-38 40-75 58-97 17-23 21-41 23-65 1-32-25-135 78-135 81 0 76 85 76 131-1 30 16 51 33 72 15 18 35 45 47 75 9 24 12 52 3 79l4 2 4 3c7 5 9 14 11 22s3 16 7 20c11 12 16 21 16 30zM222 109l13 5c-2-13 4-24 12-23 9 0 14 15 9 27l-4 4 12 5c8-9 11-26 5-40-10-21-34-22-44 0-3 8-4 15-3 22zm-46 19c7-6 7-5 6-6-8-6-7-27 1-28 7 0 11 11 10 20l10-5c2-19-9-33-19-33-19 0-24 37-8 52zm-10 21c2 5 6 10 15 15 8 5 12 12 20 15l10 2c18 1 27-11 38-15 11-3 20-11 22-18 4-8-2-14-10-18-11-5-16-5-23-9-10-7-19-9-26-9-14 0-23 10-28 14l-14 11c-4 3-5 7-4 12zm-33 253l-20-36c-7-9-14-15-22-16s-12 1-18 7c-4 5-8 12-14 18-8 6-9 6-19 10-7 2-12 4-15 11-3 5-2 12-1 20s3 16 0 24c-5 14-5 22-2 27 8 15 46 6 76 21 32 17 73 18 76-18 2-20-32-49-41-68zm154 35c3-11 6-21 6-29 1-15 2-28 5-39 3-13 9-24 21-28 2-21 19-21 38-12 19 8 27 16 23 26h5c5-17-15-28-31-35 3-12 2-24-1-36-6-25-22-47-35-59-2 0-2 2 3 7 11 11 37 49 23 85l-11-2c-5-29-17-53-23-64-12-22-30-66-38-96-4 6-12 12-22 15l-16 9c-14 8-30 9-42-1-5-4-8-8-13-10l-6-5c-2 38-27 86-39 113-9 20-13 41-14 62-22-29-6-67 3-83 9-17 11-22 8-21-8 14-22 37-27 60-3 12-3 24 0 35 4 11 11 21 25 30 0 0 25 14 38 32 8 10 10 19 8 25-3 7-10 9-17 9l14 20c38 25 83 15 115-8zm129-28c-10-12-7-34-17-42-7-6-14-6-23-5-7 9-26 19-38 16s-18-16-19-29h-1c-7 4-11 11-14 21l-4 39c-1 12-6 26-10 40-3 14-5 26-1 37 7 14 20 20 34 19s30-10 43-25c23-27 63-30 64-47 0-5-3-13-14-24zM173 149l8 7c7 5 16 10 28 10 11 0 22-6 31-10l15-11c4-3 6-6 3-6-2-1-2 2-6 5s-9 7-14 9c-7 5-19 11-29 11-11 0-19-5-25-10l-8-7c-2-1-2-5-4-5s-2 4 1 7z" class="st1"/></svg> \ No newline at end of file
diff --git a/static/v3/nsfw.svg b/static/v3/nsfw.svg
deleted file mode 100644
index b330811c..00000000
--- a/static/v3/nsfw.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 58.208333 31.750001" height="120" width="220">
- <g transform="translate(-4.998675,4.0015319)">
- <rect ry="2.3623512" y="4.9918275" x="21.408564" height="7.3399329" width="23.14588" style="fill:none;stroke:#ff0013;stroke-width:0.52899998;stroke-linejoin:round;stroke-miterlimit:4" />
- <text y="11.251631" x="22.492025" style="font-size:1.58749998px;font-family:Sans"><tspan style="font-size:7.05555534px;fill:#ff0000" y="11.251631" x="22.492025">NSFW</tspan></text>
- <text y="18.055201" x="17.540541" style="font-size:1.58749998px;font-family:Sans"><tspan style="font-size:4.93888903px" y="18.055201" x="17.540541">Click to show</tspan></text>
- </g>
-</svg>
diff --git a/static/v3/plus-circle.svg b/static/v3/plus-circle.svg
deleted file mode 100644
index a4e1614d..00000000
--- a/static/v3/plus-circle.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M384 240v32c0 7-5 12-12 12h-88v88c0 7-5 12-12 12h-32c-7 0-12-5-12-12v-88h-88c-7 0-12-5-12-12v-32c0-7 5-12 12-12h88v-88c0-7 5-12 12-12h32c7 0 12 5 12 12v88h88c7 0 12 5 12 12zm120 16a248 248 0 1 1-496 0 248 248 0 0 1 496 0zm-48 0a200 200 0 1 0-400 0 200 200 0 0 0 400 0z"/></svg> \ No newline at end of file
diff --git a/static/v3/plus.svg b/static/v3/plus.svg
deleted file mode 100644
index 6abdb9c1..00000000
--- a/static/v3/plus.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M436 228H252V44c0-7-5-12-12-12h-32c-7 0-12 5-12 12v184H12c-7 0-12 5-12 12v32c0 7 5 12 12 12h184v184c0 7 5 12 12 12h32c7 0 12-5 12-12V284h184c7 0 12-5 12-12v-32c0-7-5-12-12-12z"/></svg> \ No newline at end of file
diff --git a/static/v3/popularity.svg b/static/v3/popularity.svg
deleted file mode 100644
index b5d38cbc..00000000
--- a/static/v3/popularity.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
- <title>popularity</title>
- <desc>Created with Sketch.</desc>
- <g id="popularity" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
- <path d="M7.6433049,0.653105611 C7.6433049,2.10227637 8.84868284,2.95829665 10.0314144,4.08172755 C11.1340394,5.12906825 12.2620799,6.4030691 12.2620799,8.76017967 C12.2620799,11.3800892 10.1843491,14 6.97982033,14 C3.77529154,14 1.74,11.3800898 1.74,8.76017967 C1.74,7.38566311 2.34076885,5.93401954 3.3040627,5.00374915 C3.65529761,4.66455522 4.360198,4.94624673 4.360198,5.43556101 C4.360198,6.15939813 4.38173711,6.73597201 4.360198,7.58760037 C4.31163435,9.50774315 6.0268408,9.96797041 7.03876191,9.40124534 C7.47734681,9.15561645 7.9688702,8.49858249 7.87293082,7.71221561 C7.79216421,7.05021241 7.24107638,6.33873909 6.45278408,5.62655348 C5.0451431,4.35481502 5.25278079,3.34974837 5.25278079,3.19634055 C5.25278079,2.26589186 5.94332373,1.08107326 6.45278408,0.302862128 C6.7860135,-0.206152645 7.6433049,-0.0630522052 7.6433049,0.653105611 Z" id="Path" fill="#000000" fill-rule="nonzero"></path>
- </g>
-</svg> \ No newline at end of file
diff --git a/static/v3/star.svg b/static/v3/star.svg
deleted file mode 100644
index 665bfe3c..00000000
--- a/static/v3/star.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
- <title>star</title>
- <desc>Created with Sketch.</desc>
- <g id="star" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
- <path d="M6.33376841,1.07697421 C6.63337211,0.470154595 7.49412216,0.477766805 7.79119672,1.07697421 L9.44919563,4.43869486 L13.1587472,4.97950893 C13.8239849,5.07598811 14.0905859,5.89609838 13.6081651,6.36582383 L10.9243775,8.98105197 L11.5591416,12.6753791 C11.6733992,13.3431708 10.9701004,13.8408159 10.3810343,13.5310461 L7.06248257,11.7867093 L3.74393083,13.5310461 C3.15486478,13.8433451 2.45156598,13.3431708 2.56582352,12.6753791 L3.20058763,8.98105197 L0.516800006,6.36582383 C0.0343792811,5.89609838 0.300980208,5.07598811 0.966217956,4.97950893 L4.67576951,4.43869486 L6.33376841,1.07697421 Z" fill="#000000"></path>
- </g>
-</svg> \ No newline at end of file
diff --git a/static/v3/vndb.js b/static/v3/vndb.js
deleted file mode 100644
index 2a2eb3f2..00000000
--- a/static/v3/vndb.js
+++ /dev/null
@@ -1,420 +0,0 @@
-/* Polyfill for classList.toggle() (mainly for IE) */
-(function() {
- var historic = DOMTokenList.prototype.toggle;
- DOMTokenList.prototype.toggle = function(token, force) {
- if(arguments.length > 0 && this.contains(token) === force) {
- return force;
- }
- return historic.call(this, token);
- };
-})();
-
-
-/* Polyfill for Element.matches() and Element.closest() */
-if(!Element.prototype.matches)
- Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
-if(!Element.prototype.closest)
- Element.prototype.closest = function(s) {
- var el = this;
- if(!document.documentElement.contains(el)) return null;
- do {
- if(el.matches(s)) return el;
- el = el.parentElement || el.parentNode;
- } while(el !== null && el.nodeType === 1);
- return null;
- };
-
-
-function each(arr, cb) {
- Array.prototype.forEach.call(arr, cb);
-}
-
-
-/* Elm FFI, see elm/Lib/Ffi.elm.
- *
- * All functions are passed a reference to _Json_wrap() to wrap Javascript
- * values to values that Elm can work with; that function works differently
- * depending on whether the Elm code was compiled with --optimize.
- */
-window.elmFfi_openLightbox = function(wrap) { // _VirtualDom_property('onclick', _Json_wrap(function..))
- return {
- $: 'a2',
- n: 'onclick',
- o: wrap(function() { return window.openLightbox(this) })
- }
-};
-window.elmFfi_innerHtml = function(wrap) { // \s -> _VirtualDom_property('innerHTML', _Json_wrap(s)
- return function(s) {
- return {
- $: 'a2',
- n: 'innerHTML',
- o: wrap(s)
- }
- }
-};
-window.elmFfi_curYear = function() { return (new Date()).getFullYear() };
-
-
-/* Add the X-CSRF-Token header to every POST request. Based on:
- * https://stackoverflow.com/questions/24196140/adding-x-csrf-token-header-globally-to-all-instances-of-xmlhttprequest/24196317#24196317
- */
-(function() {
- var open = XMLHttpRequest.prototype.open,
- token = document.querySelector('meta[name=csrf-token]').content;
-
- XMLHttpRequest.prototype.open = function(method, url) {
- var ret = open.apply(this, arguments);
- this.dataUrl = url;
- if(method.toLowerCase() == 'post' && /^\//.test(url))
- this.setRequestHeader('X-CSRF-Token', token);
- return ret;
- };
-})();
-
-
-/* Find all divs with a data-elm-module, and embed the given Elm module in the div */
-each(document.querySelectorAll('div[data-elm-module]'), function(el) {
- var mod = el.getAttribute('data-elm-module').split('.').reduce(function(p, c) { return p[c] }, window.Elm);
- var flags = el.getAttribute('data-elm-flags');
- if(flags)
- mod.init({ node: el, flags: JSON.parse(flags)});
- else
- mod.init({ node: el });
-});
-
-
-/* Navbar toggles */
-each(document.querySelectorAll('.navbar__toggler'), function(el) {
- el.onclick = function() {
- el.closest('.navbar').classList.toggle('navbar--expanded');
- };
-});
-each(document.querySelectorAll('.nav-sidebar__selection'), function(el) {
- el.onclick = function() {
- el.closest('.nav-sidebar').classList.toggle('nav-sidebar--expanded');
- };
-});
-
-
-/* Dropdown menus */
-each(document.querySelectorAll('.dropdown'), function(el) {
- var visible = false;
-
- function cancel() { visible = false; update() }
-
- function update() {
- el.classList.toggle('dropdown--open', visible);
- setTimeout(function() {
- if(visible)
- document.body.addEventListener('click', cancel);
- else
- document.body.removeEventListener('click', cancel);
- });
- }
-
- var toggle = el.querySelector('.dropdown__toggle');
- if(toggle)
- toggle.onclick = function() {
- visible = !visible;
- update();
- return false;
- };
-});
-
-
-/* Measure the height of each element within the views and place them in approximately equal columns */
-each(document.querySelectorAll('.js-columnize'), function(el) {
- var columns = Number(el.dataset.columns || 2);
- var children = Array.prototype.slice.apply(el.children);
- var colHeight = children.reduce(function(a, n) { return a + n.offsetHeight }, 0) / columns;
-
- var col;
- var curHeight = 0;
- var row = document.createElement('row');
- row.className = 'row';
-
- children.forEach(function(child) {
- if(!col) {
- col = document.createElement('div');
- col.className = 'col-lg col-lg--1';
- row.appendChild(col);
- }
- curHeight += child.offsetHeight;
- col.appendChild(child);
- if(curHeight >= colHeight) {
- col = null;
- curHeight = 0;
- }
- });
- el.appendChild(row);
-});
-
-
-/* Ensure VN sidebar doesn't overlap header area */
-(function() {
- var raisedTop = document.querySelector('.raised-top');
- var sidebar = document.querySelector('.vn-page__top-details');
- if (!raisedTop || !sidebar) return;
-
- var img = sidebar.querySelector('.vn-img-desktop');
- var nextEl = img && img.nextElementSibling;
- if (!img || !nextEl) return;
-
- function addMargin() {
- // reset margin bottom, otherwise if we're called more than once, otherwise the numbers will be off
- img.style.marginBottom = '25px';
- // 29: default margin of img (25px) + .top-bar (4px)
- if (sidebar.offsetTop + nextEl.offsetTop < raisedTop.offsetHeight + 29) {
- img.style.marginBottom = (raisedTop.offsetHeight - (sidebar.offsetTop + img.offsetTop + img.offsetHeight) + 29) + 'px';
- }
- }
-
- addMargin();
- if (!img.complete) {
- img.addEventListener('load', addMargin);
- }
- window.addEventListener('resize', function() {
- setTimeout(addMargin, 0);
- });
-})();
-
-
-/* NSFW Image toggle */
-each(document.querySelectorAll('img[data-toggle-img]'), function(el) {
- el.onclick = function() {
- var cur = this.src;
- this.src = this.getAttribute('data-toggle-img');
- this.setAttribute('data-toggle-img', cur);
- return false;
- };
-});
-
-
-/* VN tag collapsing, category toggles & spoiler level */
-(function() {
- var tags = document.querySelector('.tag-summary__tags');
- if(!tags)
- return;
-
- var collapsed = true;
- var show_all = document.querySelector('.tag-summary__show-all');
- var check_collapsable = function() {
- show_all.classList.toggle('d-none', tags.scrollHeight <= 50);
- };
- check_collapsable();
-
- show_all.onclick = function() {
- collapsed = !collapsed;
- tags.classList.toggle('tag-summary--collapsed', collapsed);
- show_all.querySelector('.caret').classList.toggle('caret--up', !collapsed);
- return false;
- };
-
- var toggle = function(cat) {
- var sw = document.querySelector('.tag-summary__option--'+cat);
- sw.onclick = function() {
- sw.classList.toggle('switch--on');
- tags.classList.toggle('tag-summary--hide-'+cat);
- check_collapsable();
- return false;
- };
- };
- toggle('cont');
- toggle('ero');
- toggle('tech');
-
- var spoil_label = document.querySelector('.tag-summary_option--spoil');
- var spoil = function(lvl) {
- var lnk = document.querySelector('.tag-summary_option--spoil-'+lvl);
- lnk.onclick = function() {
- spoil_label.innerHTML = lnk.innerHTML;
- tags.classList.toggle('tag-summary--hide-spoil-1', lvl < 1);
- tags.classList.toggle('tag-summary--hide-spoil-2', lvl < 2);
- check_collapsable();
- return false;
- };
- };
- spoil(0);
- spoil(1);
- spoil(2);
-})();
-
-
-/* Char page spoiler level and sexual trait hiding */
-(function() {
- var ero = document.querySelector('.page-inner-controls__option-ero');
- if(!ero)
- return;
-
- var main = document.querySelector('.main-container');
-
- ero.onclick = function() {
- var on = main.classList.contains('charpage--hide-ero');
- main.classList.toggle('charpage--hide-ero', !on);
- ero.classList.toggle('switch--on', on);
- return false;
- };
-
- var spoil_label = document.querySelector('.page-inner-controls__option-spoil');
- var spoil = function(lvl) {
- var lnk = document.querySelector('.page-inner-controls__option-spoil-'+lvl);
- lnk.onclick = function() {
- spoil_label.innerHTML = lnk.innerHTML;
- main.classList.toggle('charpage--hide-spoil-1', lvl < 1);
- main.classList.toggle('charpage--hide-spoil-2', lvl < 2);
- return false;
- };
- };
- spoil(0);
- spoil(1);
- spoil(2);
-})();
-
-
-/* Lightbox driver.
- * Usage:
- *
- * <a href="dest-image" onclick="return openLightbox(this)" data-lightbox-id="x" data-lightbox-nfo="json">..</a>
- *
- * Similar links with the same id are grouped. nfo should be a JSON object
- * that matches the "Image" type in Lightbox.elm. "full", "thumb" and "load"
- * are inferred if not present.
- */
-(function(){
- var div;
- var app;
- var preload = {};
-
- var create = function() {
- if(div)
- return;
-
- div = document.createElement('div');
- document.body.appendChild(div);
- app = window.Elm.Lightbox.init({ node: div });
-
- app.ports.close.subscribe(function() {
- document.body.classList.remove('lightbox-open');
- });
-
- app.ports.preload.subscribe(function(url) {
- if(!preload[url]) {
- preload[url] = new Image();
- preload[url].onload = function() { app.ports.preloaded.send(url); };
- preload[url].src = url;
- }
- if(preload[url].complete)
- preload[url].onload();
- });
- };
-
- var model = function(ev) {
- var l = document.querySelectorAll("a[data-lightbox-id="+ev.getAttribute('data-lightbox-id')+"]");
- var mod = { width: 0, height: 0, images: [], current: 0 };
- for(var i=0; i<l.length; i++) {
- if(l[i] == ev)
- mod.current = i;
- var inf = JSON.parse(l[i].getAttribute('data-lightbox-nfo'));
- if(!inf.full) inf.full = l[i].href;
- if(!inf.thumb) inf.thumb = inf.full.replace("/sf/", "/st/");
- if(!inf.rel) inf.rel = null;
- inf.load = !!preload[inf.full];
- mod.images.push(inf);
- }
- return mod;
- };
-
- window.openLightbox = function(ev) {
- create();
- document.body.classList.add('lightbox-open');
- app.ports.open.send(model(ev));
- return false;
- };
-})();
-
-
-/* VN Gallery NSFW toggle */
-(function(){
- var gallery = document.querySelector('.gallery');
- if(!gallery)
- return;
-
- var show_nsfw = gallery.classList.contains('gallery--show-r18');
- var toggle = gallery.querySelector('.gallery-r18-toggle');
- if(toggle)
- toggle.onclick = function() {
- show_nsfw = !show_nsfw;
- toggle.classList.toggle('switch--on', show_nsfw);
- gallery.classList.toggle('gallery--show-r18', show_nsfw);
- return false;
- };
-
- var images = gallery.querySelectorAll('.gallery__image-link');
- each(images, function(el) {
- el.onclick = function() {
- // Fixup data-lightbox-id to exclude hidden images before opening the lightbox
- each(images, function(img) {
- img.setAttribute('data-lightbox-id', !show_nsfw && img.classList.contains('gallery__image--r18') ? 'scr-nsfw' : 'scr');
- });
- return openLightbox(this);
- };
- });
-})();
-
-
-/* VN character switcher.
- * TODO: Update URL on switch?
- */
-(function(){
- var chars = document.querySelectorAll('#characters .character');
- if(!chars)
- return;
- var links = document.querySelectorAll('.character-browser__top-items .character-browser__char');
-
- each(links, function(el) {
- el.onclick = function() {
- var id = el.getAttribute('data-character');
- each(chars, function(ch) { ch.classList.toggle('d-none', ch.getAttribute('data-character') != id); });
- each(links, function(lk) { lk.classList.toggle('character-browser__char--active', el == lk); });
- return false;
- };
- });
-})();
-
-
-/* User VN List */
-(function(){
- function toggleExpand(el, contentClass) {
- var arrow = el.querySelector('.expand-arrow');
- arrow.classList.toggle('expand-arrow--open');
-
- var nextRow = el.closest('tr').nextSibling;
- while (nextRow) {
- // skip over text nodes
- while (nextRow && nextRow.nodeType == Node.TEXT_NODE) {
- nextRow = nextRow.nextSibling;
- }
- if (nextRow) {
- if (nextRow.classList.contains(contentClass)) {
- // is this the row we're looking for?
- nextRow.classList.toggle('d-none');
- break;
- }
- // apparently not, so continue to next
- nextRow = nextRow.nextSibling;
- }
- }
- }
-
- each(document.querySelectorAll('.vn-list .vn-list__expand-releases'), function(el) {
- el.addEventListener('click', function() {
- toggleExpand(el, 'vn-list__releases-row');
- });
- });
-
- each(document.querySelectorAll('.vn-list .vn-list__expand-comment'), function(el) {
- el.addEventListener('click', function() {
- toggleExpand(el, 'vn-list__comment-row');
- });
- });
-})();
diff --git a/static/v3/windows.svg b/static/v3/windows.svg
deleted file mode 100644
index 586ba25d..00000000
--- a/static/v3/windows.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z"/></svg> \ No newline at end of file
diff --git a/util/dbdump.pl b/util/dbdump.pl
index dba08504..640ff6fc 100755
--- a/util/dbdump.pl
+++ b/util/dbdump.pl
@@ -28,6 +28,7 @@ use DBI;
use DBD::Pg;
use File::Copy 'cp';
use File::Find 'find';
+use Time::HiRes 'time';
use Cwd 'abs_path';
our $ROOT;
@@ -42,6 +43,10 @@ use VNDB::Schema;
# Tables are exported with an explicit ORDER BY to make them more deterministic
# and avoid potentially leaking information about internal state (such as when
# a user last updated their account).
+#
+# Hidden DB entries, private user lists and various other rows with no
+# interesting references are excluded from the dumps. Keeping all references
+# consistent with those omissions complicates the WHERE clauses somewhat.
my %tables = (
anime => { where => 'id IN(SELECT va.aid FROM vn_anime va JOIN vn v ON v.id = va.id WHERE NOT v.hidden)' },
chars => { where => 'NOT hidden' },
@@ -59,7 +64,12 @@ my %tables = (
releases_platforms => { where => 'id IN(SELECT id FROM releases WHERE NOT hidden)' },
releases_producers => { where => 'id IN(SELECT id FROM releases WHERE NOT hidden) AND pid IN(SELECT id FROM producers WHERE NOT hidden)' },
releases_vn => { where => 'id IN(SELECT id FROM releases WHERE NOT hidden) AND vid IN(SELECT id FROM vn WHERE NOT hidden)' },
- rlists => { where => 'uid NOT IN(SELECT id FROM users WHERE hide_list) AND rid IN(SELECT id FROM releases WHERE NOT hidden)' },
+ rlists => { where => 'EXISTS(SELECT 1 FROM releases r'
+ .' JOIN releases_vn rv ON rv.id = r.id'
+ .' JOIN vn v ON v.id = rv.vid'
+ .' JOIN ulist_vns_labels uvl ON uvl.vid = rv.vid'
+ .' JOIN ulist_labels ul ON ul.uid = uvl.uid AND ul.id = uvl.lbl'
+ .' WHERE r.id = rlists.rid AND uvl.uid = rlists.uid AND NOT r.hidden AND NOT v.hidden AND NOT ul.private)' },
screenshots => { where => 'id IN(SELECT scr FROM vn_screenshots vs JOIN vn v ON v.id = vs.id WHERE NOT v.hidden)' },
staff => { where => 'NOT hidden' },
staff_alias => { where => 'id IN(SELECT id FROM staff WHERE NOT hidden)' },
@@ -69,17 +79,15 @@ my %tables = (
tags_vn => { where => 'tag IN(SELECT id FROM tags WHERE state = 2) AND vid IN(SELECT id FROM vn WHERE NOT hidden)' },
traits => { where => 'state = 2' },
traits_parents => { where => 'trait IN(SELECT id FROM traits WHERE state = 2)' },
- # Only include users that are relevant for this dump.
- # (The 'DISTINCT' isn't necessary, but does make the query faster)
- # (Users with their votes ignored are still included. W/e)
- users => { where => q{
- ( id NOT IN(SELECT DISTINCT id FROM users WHERE hide_list)
- AND id IN(SELECT DISTINCT uid FROM rlists
- UNION SELECT DISTINCT uid FROM wlists
- UNION SELECT DISTINCT uid FROM vnlists
- UNION SELECT DISTINCT uid FROM votes)
- ) OR id IN(SELECT DISTINCT uid FROM tags_vn)
- } },
+ ulist_labels => { where => 'NOT private AND EXISTS(SELECT 1 FROM ulist_vns_labels uvl WHERE uvl.lbl = id AND ulist_labels.uid = uvl.uid)' },
+ ulist_vns => { where => 'vid IN(SELECT id FROM vn WHERE NOT hidden)'
+ .' AND EXISTS(SELECT 1 FROM ulist_vns_labels uvl'
+ .' JOIN ulist_labels ul ON ul.uid = uvl.uid AND ul.id = uvl.lbl'
+ .' WHERE ulist_vns.uid = uvl.uid AND ulist_vns.vid = uvl.vid AND NOT ul.private)' },
+ ulist_vns_labels => { where => 'vid IN(SELECT id FROM vn WHERE NOT hidden)'
+ .' AND EXISTS(SELECT 1 FROM ulist_labels ul WHERE ul.uid = ulist_vns_labels.uid AND id = lbl AND NOT ul.private)' },
+ users => { where => 'id IN(SELECT DISTINCT uvl.uid FROM ulist_vns_labels uvl JOIN ulist_labels ul ON ul.uid = uvl.uid AND ul.id = uvl.lbl WHERE NOT ul.private)'
+ .' OR id IN(SELECT DISTINCT uid FROM tags_vn)' },
vn => { where => 'NOT hidden' },
vn_anime => { where => 'id IN(SELECT id FROM vn WHERE NOT hidden)' },
vn_relations => { where => 'id IN(SELECT id FROM vn WHERE NOT hidden)' },
@@ -88,13 +96,9 @@ my %tables = (
.' AND aid IN(SELECT sa.aid FROM staff_alias sa JOIN staff s ON s.id = sa.id WHERE NOT s.hidden)'
.' AND cid IN(SELECT id FROM chars WHERE NOT hidden)' },
vn_staff => { where => 'id IN(SELECT id FROM vn WHERE NOT hidden) AND aid IN(SELECT sa.aid FROM staff_alias sa JOIN staff s ON s.id = sa.id WHERE NOT s.hidden)' },
- vnlists => { where => 'uid NOT IN(SELECT id FROM users WHERE hide_list) AND vid IN(SELECT id FROM vn WHERE NOT hidden)' },
- votes => { where => 'uid NOT IN(SELECT id FROM users WHERE hide_list OR ign_votes)'
- .' AND vid IN(SELECT id FROM vn WHERE NOT hidden)' },
wikidata => { where => q{id IN(SELECT l_wikidata FROM producers WHERE NOT hidden
UNION SELECT l_wikidata FROM staff WHERE NOT hidden
UNION SELECT l_wikidata FROM vn WHERE NOT hidden)} },
- wlists => { where => 'uid NOT IN(SELECT id FROM users WHERE hide_list) AND vid IN(SELECT id FROM vn WHERE NOT hidden)' },
);
my @tables = map +{ name => $_, %{$tables{$_}} }, sort keys %tables;
@@ -121,7 +125,6 @@ sub export_table {
my @cols = grep $_->{pub}, @{$schema->{cols}};
die "No columns to export for table '$table->{name}'\n" if !@cols;;
- #print "# Dumping $table->{name}\n";
my $fn = "$dest/$table->{name}";
# Truncate all timestamptz columns to a day, to avoid leaking privacy-sensitive info.
@@ -130,12 +133,15 @@ sub export_table {
my $order = $schema->{primary} ? join ', ', map "\"$_\"", @{$schema->{primary}} : $table->{order};
die "Table '$table->{name}' is missing an ORDER BY clause\n" if !$order;
+ my $start = time;
$db->do(qq{COPY (SELECT $cols FROM "$table->{name}" $where ORDER BY $order) TO STDOUT});
open my $F, '>:utf8', $fn;
my $v;
print $F $v while($db->pg_getcopydata($v) >= 0);
close $F;
+ #printf "# Dumped %s in %.3fs\n", $table->{name}, time-$start;
+
open $F, '>', "$fn.header";
print $F join "\t", map $_->{name}, @cols;
print $F "\n";
@@ -269,14 +275,15 @@ sub export_votes {
open my $F, '>:gzip:utf8', $dest;
$db->do(q{COPY (
- SELECT vv.vid||' '||vv.uid||' '||vv.vote||' '||to_char(vv.date, 'YYYY-MM-DD')
- FROM votes vv
- JOIN users u ON u.id = vv.uid
- JOIN vn v ON v.id = vv.vid
+ SELECT uv.vid||' '||uv.uid||' '||uv.vote||' '||to_char(uv.vote_date, 'YYYY-MM-DD')
+ FROM ulist_vns uv
+ JOIN users u ON u.id = uv.uid
+ JOIN vn v ON v.id = uv.vid
WHERE NOT v.hidden
AND NOT u.ign_votes
- AND NOT u.hide_list
- ORDER BY vv.vid, vv.uid
+ AND uv.vote IS NOT NULL
+ AND EXISTS(SELECT 1 FROM ulist_vns_labels uvl JOIN ulist_labels ul ON ul.id = uvl.lbl AND ul.uid = uvl.uid WHERE uv.uid = uvl.uid AND uv.vid = uvl.vid AND NOT ul.private)
+ ORDER BY uv.vid, uv.uid
) TO STDOUT
});
my $v;
diff --git a/util/devdump.pl b/util/devdump.pl
index 9b3480bf..e3d198c2 100755
--- a/util/devdump.pl
+++ b/util/devdump.pl
@@ -87,6 +87,8 @@ sub copy_entry {
print "\\set ON_ERROR_STOP 1\n";
print "\\i util/sql/schema.sql\n";
print "\\i util/sql/data.sql\n";
+ print "\\i util/sql/func.sql\n";
+ print "\\i util/sql/editfunc.sql\n";
# Copy over all sequence values
my @seq = sort @{ $db->selectcol_arrayref(
@@ -107,6 +109,7 @@ sub copy_entry {
[ 8, 'user6', 'user6@vndb.org', 21 ],
[ 9, 'user7', 'user7@vndb.org', 21 ],
);
+ print "SELECT ulist_labels_create(id) FROM users;\n";
# Tags & traits
copy tags => undef, {addedby => 'user'};
@@ -146,13 +149,13 @@ sub copy_entry {
# VN-related niceties
copy tags_vn => "SELECT DISTINCT ON (tag,vid,uid%10) * FROM tags_vn WHERE vid IN($vids)", {uid => 'user'};
copy quotes => "SELECT * FROM quotes WHERE vid IN($vids)";
- copy votes => "SELECT vid, uid%8+2 AS uid, (percentile_cont((uid%8+1)::float/9) WITHIN GROUP (ORDER BY vote))::smallint AS vote, MIN(date) AS date FROM votes WHERE vid IN($vids) GROUP BY vid, uid%8", {uid => 'user'};
+ my $votes = "SELECT vid, uid%8+2 AS uid, (percentile_cont((uid%8+1)::float/9) WITHIN GROUP (ORDER BY vote))::smallint AS vote, MIN(date) AS vote_date FROM votes WHERE vid IN($vids) GROUP BY vid, uid%8";
+ copy ulist_vns => $votes, {uid => 'user'};
+ copy ulist_vns_labels => "SELECT vid, uid, 7 AS lbl FROM ($votes) x", {uid => 'user'};
# Releases
copy_entry r => [qw/releases releases_lang releases_media releases_platforms releases_producers releases_vn/], $releases;
- print "\\i util/sql/func.sql\n";
- print "\\i util/sql/editfunc.sql\n";
print "\\i util/sql/tableattrs.sql\n";
# Update some caches
@@ -160,11 +163,10 @@ sub copy_entry {
print "SELECT traits_chars_calc(NULL);\n";
print "SELECT update_vncache(id) FROM vn;\n";
print "SELECT update_stats_cache_full();\n";
- print "SELECT update_vnpopularity();\n";
- print "UPDATE users u SET c_votes = (SELECT COUNT(*) FROM votes v WHERE v.uid = u.id);\n";
+ print "SELECT update_vnvotestats();\n";
+ print "SELECT update_users_ulist_stats(NULL);\n";
print "UPDATE users u SET c_tags = (SELECT COUNT(*) FROM tags_vn v WHERE v.uid = u.id);\n";
print "UPDATE users u SET c_changes = (SELECT COUNT(*) FROM changes c WHERE c.requester = u.id);\n";
- # TODO: The vn.c_rating and vn.c_votecount stats are still inconsistent
print "\\set ON_ERROR_STOP 0\n";
print "\\i util/sql/perms.sql\n";
diff --git a/util/docker-init.sh b/util/docker-init.sh
index c78c5e76..d6994583 100755
--- a/util/docker-init.sh
+++ b/util/docker-init.sh
@@ -89,7 +89,7 @@ pg_start() {
# Should run as devuser
devshell() {
cd /var/www
- util/vndb-dev-server.pl $1
+ util/vndb-dev-server.pl
bash
}
@@ -100,15 +100,10 @@ case "$1" in
su devuser -c '/var/www/util/docker-init.sh pg_start'
exec su devuser -c '/var/www/util/docker-init.sh devshell'
;;
- 3)
- mkdevuser
- su devuser -c '/var/www/util/docker-init.sh pg_start'
- exec su devuser -c '/var/www/util/docker-init.sh devshell 3'
- ;;
pg_start)
pg_start
;;
devshell)
- devshell $2
+ devshell
;;
esac
diff --git a/util/sql/func.sql b/util/sql/func.sql
index dab2ddbb..b9fa5ade 100644
--- a/util/sql/func.sql
+++ b/util/sql/func.sql
@@ -102,24 +102,59 @@ CREATE OR REPLACE FUNCTION update_vncache(integer) RETURNS void AS $$
$$ LANGUAGE sql;
-
--- recalculate vn.c_popularity
-CREATE OR REPLACE FUNCTION update_vnpopularity() RETURNS void AS $$
- -- the following querie only update VNs which have valid votes, so make sure to reset all rows first.
- UPDATE vn SET c_popularity = NULL;
- WITH t2(vid, win) AS (
+-- Update vn.c_popularity, c_rating and c_votecount
+CREATE OR REPLACE FUNCTION update_vnvotestats() RETURNS void AS $$
+ WITH votes(vid, uid, vote) AS ( -- List of all non-ignored VN votes
+ SELECT vid, uid, vote FROM ulist_vns WHERE vote IS NOT NULL AND uid NOT IN(SELECT id FROM users WHERE ign_votes)
+ ), avgcount(avgcount) AS ( -- Average number of votes per VN
+ SELECT COUNT(vote)::real/COUNT(DISTINCT vid)::real FROM votes
+ ), avgavg(avgavg) AS ( -- Average vote average
+ SELECT AVG(a)::real FROM (SELECT AVG(vote) FROM votes GROUP BY vid) x(a)
+ ), ratings(vid, count, rating) AS ( -- Ratings and vote counts
+ SELECT vid, COALESCE(COUNT(uid), 0),
+ COALESCE(
+ ((SELECT avgcount FROM avgcount) * (SELECT avgavg FROM avgavg) + SUM(vote)::real) /
+ ((SELECT avgcount FROM avgcount) + COUNT(uid)::real),
+ 0)
+ FROM votes
+ GROUP BY vid
+ ), popularities(vid, win) AS ( -- Popularity scores (before normalization)
SELECT vid, SUM(rank)
FROM (
- SELECT v.uid, v.vid, ((rank() OVER (PARTITION BY uid ORDER BY vote))::real - 1) ^ 0.36788
- FROM votes v
- JOIN users u ON u.id = v.uid AND NOT ign_votes
- ) t1(uid, vid, rank)
- GROUP BY vid
+ SELECT uid, vid, ((rank() OVER (PARTITION BY uid ORDER BY vote))::real - 1) ^ 0.36788 FROM votes
+ ) x(uid, vid, rank)
+ GROUP BY vid
+ ), stats(vid, rating, count, popularity) AS ( -- Combined stats
+ SELECT v.id, COALESCE(r.rating, 0), COALESCE(r.count, 0)
+ , p.win/(SELECT MAX(win) FROM popularities)
+ FROM vn v
+ LEFT JOIN ratings r ON r.vid = v.id
+ LEFT JOIN popularities p ON p.vid = v.id AND p.win > 0
)
- UPDATE vn SET c_popularity = s1.win/(SELECT MAX(win) FROM t2) FROM t2 s1 WHERE s1.vid = vn.id AND s1.win > 0;
+ UPDATE vn SET c_rating = rating, c_votecount = count, c_popularity = popularity FROM stats WHERE id = vid;
$$ LANGUAGE SQL;
+
+-- Update users.c_vns, c_votes and c_wish for one user (when given an id) or all users (when given NULL)
+CREATE OR REPLACE FUNCTION update_users_ulist_stats(integer) RETURNS void AS $$
+BEGIN
+ WITH cnt(uid, votes, vns, wish) AS (
+ SELECT u.id
+ , COUNT(*) FILTER (WHERE ul.id = 7) -- Voted
+ , COUNT(DISTINCT uvl.vid) FILTER (WHERE ul.id NOT IN(5,6)) -- Labelled, but not wishlish/blacklist
+ , COUNT(DISTINCT uvl.vid) FILTER (WHERE ul.id = 5) -- Wishlist
+ FROM users u
+ LEFT JOIN ulist_vns_labels uvl ON uvl.uid = u.id
+ LEFT JOIN ulist_labels ul ON ul.id = uvl.lbl AND ul.uid = u.id AND NOT ul.private
+ WHERE $1 IS NULL OR u.id = $1
+ GROUP BY u.id
+ ) UPDATE users SET c_votes = votes, c_vns = vns, c_wish = wish FROM cnt WHERE id = uid;
+END;
+$$ LANGUAGE plpgsql; -- Don't use "LANGUAGE SQL" here; Make sure to generate a new query plan at invocation time.
+
+
+
-- Recalculate tags_vn_inherit.
-- When a vid is given, only the tags for that vid will be updated. These
-- incremental updates do not affect tags.c_items, so that may still get
@@ -408,6 +443,16 @@ BEGIN
THEN
PERFORM notify_dbedit(xtype, xedit);
END IF;
+
+ -- Make sure all visual novels linked to a release have a corresponding entry
+ -- in ulist_vns for users who have the release in rlists. This is action (3) in
+ -- update_vnlist_rlist().
+ IF xtype = 'r' AND xoldchid IS NOT NULL
+ THEN
+ INSERT INTO ulist_vns (uid, vid)
+ SELECT rl.uid, rv.vid FROM rlists rl JOIN releases_vn rv ON rv.id = rl.rid WHERE rl.rid = xedit.itemid
+ ON CONFLICT (uid, vid) DO NOTHING;
+ END IF;
END;
$$ LANGUAGE plpgsql;
@@ -418,16 +463,10 @@ $$ LANGUAGE plpgsql;
----------------------------------------------------------
--- keep the c_* columns in the users table up to date
+-- keep the c_tags and c_changes columns in the users table up to date
CREATE OR REPLACE FUNCTION update_users_cache() RETURNS TRIGGER AS $$
BEGIN
- IF TG_TABLE_NAME = 'votes' THEN
- IF TG_OP = 'INSERT' THEN
- UPDATE users SET c_votes = c_votes + 1 WHERE id = NEW.uid;
- ELSE
- UPDATE users SET c_votes = c_votes - 1 WHERE id = OLD.uid;
- END IF;
- ELSIF TG_TABLE_NAME = 'changes' THEN
+ IF TG_TABLE_NAME = 'changes' THEN
IF TG_OP = 'INSERT' THEN
UPDATE users SET c_changes = c_changes + 1 WHERE id = NEW.requester;
ELSE
@@ -519,48 +558,64 @@ $$ LANGUAGE plpgsql;
-- For each row in rlists, there should be at least one corresponding row in
--- vnlists for at least one of the VNs linked to that release.
--- 1. When a row is deleted from vnlists, also remove all rows from rlists that
--- would otherwise not have a corresponding row in vnlists
+-- ulist_vns for each VN linked to that release.
+-- 1. When a row is deleted from ulist_vns, also remove all rows from rlists
+-- with that VN linked.
-- 2. When a row is inserted to rlists and there is not yet a corresponding row
--- in vnlists, add a row in vnlists (with status=unknown) for each vn linked
--- to the release.
+-- in ulist_vns, add a row to ulist_vns for each vn linked to the release.
+-- 3. When a release is edited to add another VN, add those VNs to ulist_vns
+-- for everyone who has the release in rlists.
+-- This is done in edit_committed().
+-- #. When a release is edited to remove a VN, that VN kinda should also be
+-- removed from ulist_vns, but only if that ulist_vns entry was
+-- automatically added as part of the rlists entry and the user has not
+-- changed anything in the ulist_vns row. This isn't currently done.
CREATE OR REPLACE FUNCTION update_vnlist_rlist() RETURNS trigger AS $$
BEGIN
-- 1.
- IF TG_TABLE_NAME = 'vnlists' THEN
- DELETE FROM rlists WHERE uid = OLD.uid AND rid IN(SELECT rv.id
- -- fetch all related rows in rlists
- FROM releases_vn rv
- JOIN rlists rl ON rl.rid = rv.id
- WHERE rv.vid = OLD.vid AND rl.uid = OLD.uid
- -- and test for a corresponding row in vnlists
- AND NOT EXISTS(
- SELECT 1
- FROM releases_vn rvi
- JOIN vnlists vl ON vl.vid = rvi.vid AND uid = OLD.uid
- WHERE rvi.id = rv.id
- ));
-
+ IF TG_TABLE_NAME = 'ulist_vns' THEN
+ DELETE FROM rlists WHERE uid = OLD.uid AND rid IN(SELECT id FROM releases_vn WHERE vid = OLD.vid);
-- 2.
ELSE
- INSERT INTO vnlists (uid, vid) SELECT NEW.uid, rv.vid
- -- all VNs linked to the release
- FROM releases_vn rv
- WHERE rv.id = NEW.rid
- -- but only if there are no corresponding rows in vnlists yet
- AND NOT EXISTS(
- SELECT 1
- FROM releases_vn rvi
- JOIN vnlists vl ON vl.vid = rvi.vid
- WHERE rvi.id = NEW.rid AND vl.uid = NEW.uid
- );
+ INSERT INTO ulist_vns (uid, vid)
+ SELECT NEW.uid, rv.vid FROM releases_vn rv WHERE rv.id = NEW.rid
+ ON CONFLICT (uid, vid) DO NOTHING;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
+-- Create ulist labels for new users.
+CREATE OR REPLACE FUNCTION ulist_labels_create(integer) RETURNS void AS $$
+ INSERT INTO ulist_labels (uid, id, label, private)
+ VALUES ($1, 1, 'Playing', false),
+ ($1, 2, 'Finished', false),
+ ($1, 3, 'Stalled', false),
+ ($1, 4, 'Dropped', false),
+ ($1, 5, 'Wishlist', false),
+ ($1, 6, 'Blacklist', false),
+ ($1, 7, 'Voted', false)
+ ON CONFLICT (uid, id) DO NOTHING;
+$$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION ulist_labels_create() RETURNS trigger AS 'BEGIN PERFORM ulist_labels_create(NEW.id); RETURN NULL; END' LANGUAGE plpgsql;
+
+
+
+-- Set/unset the 'Voted' label when voting.
+CREATE OR REPLACE FUNCTION ulist_voted_label() RETURNS trigger AS $$
+BEGIN
+ IF NEW.vote IS NULL THEN
+ DELETE FROM ulist_vns_labels WHERE uid = NEW.uid AND vid = NEW.vid AND lbl = 7;
+ ELSE
+ INSERT INTO ulist_vns_labels (uid, vid, lbl) VALUES (NEW.uid, NEW.vid, 7) ON CONFLICT (uid, vid, lbl) DO NOTHING;
+ END IF;
+ RETURN NULL;
+END
+$$ LANGUAGE plpgsql;
+
+
-- Send a notify whenever anime info should be fetched
CREATE OR REPLACE FUNCTION anime_fetch_notify() RETURNS trigger AS $$
@@ -707,10 +762,8 @@ CREATE OR REPLACE FUNCTION notify_listdel(xtype dbentry_type, xedit edit_rettype
SELECT DISTINCT 'listdel'::notification_ntype, xtype::text::notification_ltype, u.uid, xedit.itemid, xedit.rev, x.title, c.requester
-- look for users who should get this notify
FROM (
- SELECT uid FROM votes WHERE xtype = 'v' AND vid = xedit.itemid
- UNION SELECT uid FROM vnlists WHERE xtype = 'v' AND vid = xedit.itemid
- UNION SELECT uid FROM wlists WHERE xtype = 'v' AND vid = xedit.itemid
- UNION SELECT uid FROM rlists WHERE xtype = 'r' AND rid = xedit.itemid
+ SELECT uid FROM ulist_vns WHERE xtype = 'v' AND vid = xedit.itemid
+ UNION SELECT uid FROM rlists WHERE xtype = 'r' AND rid = xedit.itemid
) u
-- fetch info about this edit
JOIN changes c ON c.id = xedit.chid
diff --git a/util/sql/perms.sql b/util/sql/perms.sql
index a038103e..e649526f 100644
--- a/util/sql/perms.sql
+++ b/util/sql/perms.sql
@@ -61,15 +61,15 @@ GRANT SELECT, INSERT, UPDATE, DELETE ON threads_posts TO vndb_site;
GRANT SELECT, INSERT, UPDATE, DELETE ON traits TO vndb_site;
GRANT SELECT, INSERT, UPDATE, DELETE ON traits_chars TO vndb_site;
GRANT SELECT, INSERT, UPDATE, DELETE ON traits_parents TO vndb_site;
-GRANT SELECT, INSERT, UPDATE, DELETE ON ulists TO vndb_site;