summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--ChangeLog9
-rw-r--r--Makefile14
-rw-r--r--data/docs/125
-rw-r--r--data/docs/index1
-rw-r--r--data/global.pl18
-rw-r--r--data/lang.txt1412
-rw-r--r--data/notes/chardb578
-rw-r--r--data/script.js360
-rw-r--r--data/style.css74
-rw-r--r--lib/Multi/Feed.pm5
-rw-r--r--lib/Multi/IRC.pm32
-rw-r--r--lib/Multi/Image.pm40
-rw-r--r--lib/VNDB/DB/Chars.pm125
-rw-r--r--lib/VNDB/DB/Misc.pm9
-rw-r--r--lib/VNDB/DB/Releases.pm2
-rw-r--r--lib/VNDB/DB/Tags.pm43
-rw-r--r--lib/VNDB/DB/Traits.pm105
-rw-r--r--lib/VNDB/DB/VN.pm9
-rw-r--r--lib/VNDB/Func.pm77
-rw-r--r--lib/VNDB/Handler/Chars.pm514
-rw-r--r--lib/VNDB/Handler/Misc.pm6
-rw-r--r--lib/VNDB/Handler/Releases.pm34
-rw-r--r--lib/VNDB/Handler/Tags.pm86
-rw-r--r--lib/VNDB/Handler/Traits.pm435
-rw-r--r--lib/VNDB/Handler/VNPage.pm55
-rw-r--r--lib/VNDB/Util/CommonHTML.pm29
-rw-r--r--lib/VNDB/Util/LayoutHTML.pm5
-rw-r--r--lib/VNDBUtil.pm4
-rw-r--r--static/f/icons.pngbin10825 -> 11329 bytes
-rwxr-xr-xutil/dbgraph.pl2
-rwxr-xr-xutil/jsgen.pl1
-rw-r--r--util/sql/all.sql9
-rw-r--r--util/sql/func.sql99
-rw-r--r--util/sql/schema.sql99
-rw-r--r--util/updates/update_2.19.sql206
36 files changed, 4248 insertions, 255 deletions
diff --git a/.gitignore b/.gitignore
index c4732a40..0a0a346b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
/static/feeds/
/static/s/*/style.css
/static/s/*/boxbg.png
+/static/ch/
/static/cv/
/static/sf/
/static/st/
diff --git a/ChangeLog b/ChangeLog
index 23015f18..8f2fb2b6 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,4 +1,11 @@
-2.19 - ?
+2.19 - 2011-03-30
+ - Character database:
+ - New DB item (versioned): character, page: /c+
+ - New DB item (not versioned): traits (like tags), page: /i+
+ - New pages: trait listing/browser: /i/*
+ - New pages: character browser: /c/*
+ - VN pages updated with a characters tab
+ - Changed text color of inactive tabs
- Don't allow users to edit their post when it was deleted
- Auto-set category when creating new child tag
- Increased allowed size of VN cover image upload to 5MB
diff --git a/Makefile b/Makefile
index 16246871..d1ed33fe 100644
--- a/Makefile
+++ b/Makefile
@@ -38,11 +38,12 @@
.PHONY: all dirs js skins robots chmod chmod-tladmin multi-stop multi-start multi-restart sql-import\
- update-2.10 update-2.11 update-2.12 update-2.13 update-2.14 update-2.15 update-2.16 update-2.17 update-2.18
+ update-2.10 update-2.11 update-2.12 update-2.13 update-2.14 update-2.15 update-2.16 update-2.17\
+ update-2.18 update-2.19
all: dirs js skins robots data/config.pl
-dirs: static/f/js static/cv static/sf static/st data/log www www/feeds
+dirs: static/f/js static/ch static/cv static/sf static/st data/log www www/feeds
js: static/f/js/en.js
@@ -50,7 +51,7 @@ skins: static/s/*/style.css
robots: dirs www/robots.txt static/robots.txt
-static/cv static/sf static/st:
+static/ch static/cv static/sf static/st:
mkdir $@;
for i in $$(seq -w 0 1 99); do mkdir "$@/$$i"; done
@@ -69,7 +70,7 @@ static/s/%/style.css: static/s/%/conf util/skingen.pl data/style.css
chmod: all
chmod a+xrw static/f/js
- chmod -R a-x+rwX static/{cv,sf,st}
+ chmod -R a-x+rwX static/{ch,cv,sf,st}
chmod a-x+rw static/s/*/{style.css,boxbg.png}
chmod-tladmin:
@@ -173,3 +174,8 @@ update-2.18: all
$(multi-stop)
${runpsql} < util/updates/update_2.18.sql
$(multi-start)
+
+update-2.19: all
+ $(multi-stop)
+ ${runpsql} < util/updates/update_2.19.sql
+ $(multi-start)
diff --git a/data/docs/12 b/data/docs/12
new file mode 100644
index 00000000..50bd98bc
--- /dev/null
+++ b/data/docs/12
@@ -0,0 +1,5 @@
+:TITLE:Adding/Editing Characters
+:INC:index
+
+<i>to-do</i>
+
diff --git a/data/docs/index b/data/docs/index
index 02c8f769..0c30f664 100644
--- a/data/docs/index
+++ b/data/docs/index
@@ -4,6 +4,7 @@
<li><a href="/d2">Visual Novels</a></li>
<li><a href="/d3">Releases</a></li>
<li><a href="/d4">Producers</a></li>
+ <li><a href="/d12">Characters</a></li>
<li><a href="/d10">Tags</a></li>
<li><b>About VNDB</b></li>
<li><a href="/d9">Discussion board</a></li>
diff --git a/data/global.pl b/data/global.pl
index 30feb436..5fd2bf85 100644
--- a/data/global.pl
+++ b/data/global.pl
@@ -29,15 +29,16 @@ our %S = (%S,
source_url => 'http://git.blicky.net/vndb.git/?h=master',
admin_email => 'contact@vndb.org',
scr_size => [ 136, 102 ], # w*h of screenshot thumbnails
+ ch_size => [ 256, 300 ], # max. w*h of char images
cv_size => [ 256, 400 ], # max. w*h of cover images
user_ranks => [
- # allowed actions # DB number
- [qw| hist |], # 0
- [qw| hist |], # 1
- [qw| hist board |], # 2
- [qw| hist board edit tag |], # 3
- [qw| hist board boardmod edit tag mod lock del tagmod |], # 4
- [qw| hist board boardmod edit tag mod lock del tagmod usermod |], # 5
+ # allowed actions # DB number
+ [qw| hist |], # 0
+ [qw| hist |], # 1
+ [qw| hist board |], # 2
+ [qw| hist board edit tag |], # 3
+ [qw| hist board boardmod edit charedit tag mod lock del tagmod |], # 4
+ [qw| hist board boardmod edit charedit tag mod lock del tagmod usermod |], # 5
],
languages => [qw|cs da de en es fi fr hu it ja ko nl no pl pt-br pt-pt ru sk sv tr vi zh|],
producer_types => [qw|co in ng|],
@@ -105,6 +106,9 @@ our %S = (%S,
wishlist_status => [ 0..3 ],
rlist_status => [ 0..4 ], # 2 = hardcoded 'OK'
vnlist_status => [ 0..4 ],
+ blood_types => [qw| unknown a b ab o |],
+ genders => [qw| unknown m f b |],
+ char_roles => [qw| main primary side appears |],
atom_feeds => { # num_entries, title, id
announcements => [ 10, 'VNDB Site Announcements', '/t/an' ],
changes => [ 25, 'VNDB Recent Changes', '/hist' ],
diff --git a/data/lang.txt b/data/lang.txt
index 928740ae..f82dfcc2 100644
--- a/data/lang.txt
+++ b/data/lang.txt
@@ -1161,6 +1161,107 @@ hu : Technikai
nl : Technisch
+# Blood types
+
+:_bloodt_unknown
+en : Unknown
+ru*:
+cs*:
+hu*:
+nl : Onbekend
+
+:_bloodt_a
+en : A
+ru*:
+cs*:
+hu*:
+nl :
+
+:_bloodt_b
+en : B
+ru*:
+cs*:
+hu*:
+nl :
+
+:_bloodt_ab
+en : AB
+ru*:
+cs*:
+hu*:
+nl :
+
+:_bloodt_o
+en : O
+ru*:
+cs*:
+hu*:
+nl : O
+
+
+# Genders
+
+:_gender_unknown
+en : Unknown or N/A
+ru*:
+cs*:
+hu*:
+nl : Onbekend of n.v.t.
+
+:_gender_m
+en : Male
+ru*:
+cs*:
+hu*:
+nl : Man
+
+:_gender_f
+en : Female
+ru*:
+cs*:
+hu*:
+nl : Vrouw
+
+:_gender_b
+en : Both
+ru*:
+cs*:
+hu*:
+nl : Beide
+
+
+# Character roles
+
+:_charrole_main
+en : Protagonist
+ru*:
+cs*:
+hu*:
+nl :
+
+:_charrole_primary
+en : Main character
+ru*:
+cs*:
+hu*:
+nl : Belangrijke rol
+
+:_charrole_side
+en : Side character
+ru*:
+cs*:
+hu*:
+nl : Onbelangrijke rol
+
+:_charrole_appears
+en : Makes an appearance
+ru*:
+cs*:
+hu*:
+nl : Figurant
+
+
+
#############################################################################
## Global UI strings ##
@@ -1187,6 +1288,93 @@ hu : %Y-%m-%d - %R-kor
nl : %Y-%m-%d om %R
+# Months (used on char pages in _charp_bday_fmt)
+
+:_month_1
+en : January
+ru*:
+cs*:
+hu*:
+nl : januari
+
+:_month_2
+en : February
+ru*:
+cs*:
+hu*:
+nl : februari
+
+:_month_3
+en : March
+ru*:
+cs*:
+hu*:
+nl : maart
+
+:_month_4
+en : April
+ru*:
+cs*:
+hu*:
+nl : april
+
+:_month_5
+en : May
+ru*:
+cs*:
+hu*:
+nl : mei
+
+:_month_6
+en : June
+ru*:
+cs*:
+hu*:
+nl : juni
+
+:_month_7
+en : July
+ru*:
+cs*:
+hu*:
+nl : juli
+
+:_month_8
+en : August
+ru*:
+cs*:
+hu*:
+nl : augustus
+
+:_month_9
+en : September
+ru*:
+cs*:
+hu*:
+nl : september
+
+:_month_10
+en : October
+ru*:
+cs*:
+hu*:
+nl : oktober
+
+:_month_11
+en : November
+ru*:
+cs*:
+hu*:
+nl : november
+
+:_month_12
+en : December
+ru*:
+cs*:
+hu*:
+nl : december
+
+
# Age display
:_age_years
@@ -1508,6 +1696,37 @@ hu : Nincs engedélyed, ennek az oldalnak a szerkesztésére
nl : Je bent niet toegestaan om wijzigingen aan te brengen
+# Spoiler levels
+
+:_spoil_-1
+en : neutral
+ru : нейтральный
+cs : neutrální
+hu : semleges
+nl : neutraal
+
+:_spoil_0
+en : no spoiler
+ru : без спойлеров
+cs : bez spoilerů
+hu : nem spoiler
+nl : geen spoiler
+
+:_spoil_1
+en : minor spoiler
+ru : лёгкий спойлер
+cs : menší spoilery
+hu : kisebb spoiler
+nl : geringe spoiler
+
+:_spoil_2
+en : major spoiler
+ru : жёсткий спойлер
+cs : všechny spoilery
+hu : nagyobb spoiler
+nl : grote spoiler
+
+
#############################################################################
@@ -1559,6 +1778,13 @@ cs : Vizuální novely
hu : Visual Novellek
nl :
+:_menu_tags
+en : Tags
+ru : Теги
+cs : Tagy
+hu : Címkék
+nl :
+
:_menu_releases
en : Releases
ru : Выпуски
@@ -1573,12 +1799,19 @@ cs : Producenti
hu : Készítők
nl : Producenten
-:_menu_tags
-en : Tags
-ru : Теги
-cs : Tagy
-hu : Címkék
-nl :
+:_menu_characters
+en : Characters
+ru*:
+cs*:
+hu*:
+nl : Karakters
+
+:_menu_traits
+en : Traits
+ru*:
+cs*:
+hu*:
+nl : Kenmerken
:_menu_users
en : Users
@@ -1688,6 +1921,13 @@ cs : Přidat producenta
hu : Új készítő bejegyzése
nl : Nieuwe producent
+:_menu_addcharacter
+en : Add Character
+ru*:
+cs*:
+hu*:
+nl : Nieuw karakter
+
:_menu_logout
en : Logout
ru : Выйти
@@ -2012,6 +2252,13 @@ cs : Producenti
hu : Keszítők
nl : Producenten
+:_searchbox_chars
+en : Characters
+ru*:
+cs*:
+hu*:
+nl : Karakters
+
:_searchbox_tags
en : Tags
ru : Теги
@@ -2019,6 +2266,13 @@ cs : Tagy
hu : Címkék
nl :
+:_searchbox_traits
+en : Traits
+ru*:
+cs*:
+hu*:
+nl : Kenmerken
+
:_searchbox_users
en : Users
ru : Пользователи
@@ -2224,6 +2478,13 @@ cs : Pouze producenti
hu : Csak készítők
nl : Alleen producenten
+:_hist_filter_onlychars
+en : Only characters
+ru*:
+cs*:
+hu*:
+nl : Alleen karakters
+
:_hist_filter_allactions
en : Show all changes
ru : Показать все изменения
@@ -3974,13 +4235,6 @@ cs : Závislé tagy
hu : Alcímkék
nl : Subtags
-:_tagp_tree
-en : Tag tree
-ru : Древо тегов
-cs : Strom tagů
-hu : Címke fa
-nl : Taglijst
-
:_tagp_moretags
en : [_1] more [quant,_1,tag,tags]
ru : ещё [_1] [quant,_1,тег,тега,тегов]
@@ -4461,27 +4715,6 @@ cs*:
hu :
nl :
-:_taglink_spoil0
-en : No spoiler
-ru : Без спойлера
-cs : Bez spoilerů
-hu : Nem spoiler
-nl : Geen spoiler
-
-:_taglink_spoil1
-en : Minor spoiler
-ru : Лёгкий спойлер
-cs : Menší spoilery
-hu : Kisebb spoiler
-nl : Geringe spoiler
-
-:_taglink_spoil2
-en : Major spoiler
-ru : Жёсткий спойлер
-cs : Všechny spoilery
-hu : Nagyobb spoiler
-nl : Grote spoiler
-
# VN<->Tag voting (/v+/tagmod)
@@ -4609,34 +4842,6 @@ hu : Menj a [url,/g,cimke listára] ha szeretnél böngészni az összes rendelk
nl : Zie de [url,/g,tag lijst] om te browsen door alle beschikbare tags.[br]
Niet gevonden wat je zocht? [url,/g/new,Vraag een nieuwe tag aan].
-:_tagv_spoil0
-en : neutral
-ru : нейтральный
-cs : neutrální
-hu : semleges
-nl : neutraal
-
-:_tagv_spoil1
-en : no spoiler
-ru : без спойлеров
-cs : bez spoilerů
-hu : nem spoiler
-nl : geen spoiler
-
-:_tagv_spoil2
-en : minor spoiler
-ru : лёгкий спойлер
-cs : menší spoilery
-hu : kisebb spoiler
-nl : geringe spoiler
-
-:_tagv_spoil3
-en : major spoiler
-ru : жёсткий спойлер
-cs : všechny spoilery
-hu : nagyobb spoiler
-nl : grote spoiler
-
:_tagv_notfound
en : Item not found!
ru : Элемент не найден!
@@ -4675,6 +4880,13 @@ cs : Hledat tag
hu : Címkék keresése
nl : Zoek tags
+:_tagidx_tree
+en : Tag tree
+ru : Древо тега
+cs : Strom тегов
+hu : Címke fa
+nl : Taglijst
+
:_tagidx_browseall
en : Browse all tags
ru : Обзор всех тегов
@@ -4735,6 +4947,1010 @@ nl : Geweigerde tags
#############################################################################
+## Character pages ##
+#############################################################################
+# Handler::Chars
+
+
+# Character diff viewer (/c+.+)
+
+:_revfield_c_name
+en : Name
+ru*:
+cs*:
+hu*:
+nl : Naam
+
+:_revfield_c_original
+en : Original name
+ru*:
+cs*:
+hu*:
+nl : Originele naam
+
+:_revfield_c_alias
+en : Aliasses
+ru*:
+cs*:
+hu*:
+nl : Aliassen
+
+:_revfield_c_desc
+en : Description
+ru*:
+cs*:
+hu*:
+nl : Omschrijving
+
+:_revfield_c_gender
+en : Gender
+ru*:
+cs*:
+hu*:
+nl : Geslacht
+
+:_revfield_c_b_month
+en : Birthday - month
+ru*:
+cs*:
+hu*:
+nl : Verjaardag - maand
+
+:_revfield_c_b_day
+en : Birthday - day
+ru*:
+cs*:
+hu*:
+nl : Verjaardag - dag
+
+:_revfield_c_s_bust
+en : Bust
+ru*:
+cs*:
+hu*:
+nl : Borst
+
+:_revfield_c_s_waist
+en : Waist
+ru*:
+cs*:
+hu*:
+nl : Taille
+
+:_revfield_c_s_hip
+en : Hip
+ru*:
+cs*:
+hu*:
+nl :
+
+:_revfield_c_height
+en : Height
+ru*:
+cs*:
+hu*:
+nl : Lengte
+
+:_revfield_c_weight
+en : Weight
+ru*:
+cs*:
+hu*:
+nl : Gewicht
+
+:_revfield_c_bloodt
+en : Blood type
+ru*:
+cs*:
+hu*:
+nl : Bloedgroep
+
+:_revfield_c_main
+en : Main character
+ru*:
+cs*:
+hu*:
+nl : Hoofdkarakter
+
+:_revfield_c_main_spoil
+en : Spoiler
+ru*:
+cs*:
+hu*:
+nl :
+
+:_revfield_c_image
+en : Image
+ru*:
+cs*:
+hu*:
+nl : Plaatje
+
+:_revfield_c_traits
+en : Traits
+ru*:
+cs*:
+hu*:
+nl : Kenmerken
+
+:_revfield_c_vns
+en : Visual novels
+ru*:
+cs*:
+hu*:
+nl :
+
+:_chdiff_image_proc
+en : ~[processing~]
+ru : ~[обработка~]
+cs : ~[zpracovává se~]
+hu : ~[feldolgozás~]
+nl : ~[bewerken...~]
+
+:_chdiff_image_none
+en : No image
+ru : Нет изображения
+cs : Obrázek není
+hu : Nincs kép
+nl : Geen plaatje
+
+
+# Character page (/c+)
+
+:_charp_noimg
+en : No image uploaded yet
+ru : Нет загруженного изображения
+cs : Ještě nebyl nahrán žádný obrázek
+hu : Még nincs kép feltöltve
+nl : Nog geen plaatje geupload
+
+:_charp_imgproc
+en : ~[processing image, please return in a few minutes~]
+ru : ~[идёт обработка изображения, подождите несколько минут~]
+cs : ~[obrázek se zpracovává, prosíme, vraťte se za několik minut~]
+hu : ~[a kép feldolgozás alatt van, gyere vissza pár perc múlva~]
+nl : ~[plaatje wordt verwerkt, kom a.u.b. terug in een paar minuten~]
+
+:_charp_alias
+en : Aliases
+ru*:
+cs*:
+hu*:
+nl : Aliassen
+
+:_charp_meas
+en : Measurements
+ru*:
+cs*:
+hu*:
+nl : Afmetingen
+
+:_charp_meas_h
+en : Height: [_1]cm
+ru*:
+cs*:
+hu*:
+nl : Lengte: [_1]cm
+
+:_charp_meas_bwh
+en : Bust-Waist-Hips: [_1]-[_2]-[_3]cm
+ru*:
+cs*:
+hu*:
+nl : Borst-Taille-Heup: [_1]-[_2]-[_3]cm
+
+:_charp_meas_w
+en : Weight: [_1]kg
+ru*:
+cs*:
+hu*:
+nl : Gewicht: [_1]kg
+
+:_charp_bday
+en : Birthday
+ru*:
+cs*:
+hu*:
+nl : Verjaardag
+
+# [_1]=day-of-month, as a number, [_2]=month string, as in _month_<num>
+:_charp_bday_fmt
+en : [_1] [_2]
+ru*:
+cs*:
+hu*:
+nl :
+
+:_charp_vns
+en : Visual novels
+ru*:
+cs*:
+hu*:
+nl :
+
+:_charp_releases
+en : Releases
+ru*:
+cs*:
+hu*:
+nl : Uitgaven
+
+:_charp_vns_other
+en : All other releases
+ru*:
+cs*:
+hu*:
+nl : Overige uitgaven
+
+:_charp_description
+en : Description
+ru*:
+cs*:
+hu*:
+nl : Beschrijving
+
+:_charp_instances
+en : Other instances
+ru*:
+cs*:
+hu*:
+nl : Andere instanties
+
+
+
+# Add/edit character
+
+:_chare_title_edit
+en : Edit [_1]
+ru : Править [_1]
+cs : Editovat [_1]
+hu : [_1] szerkesztése
+nl : Wijzig [_1]
+
+:_chare_title_copy
+en : Copy [_1]
+ru : Копирование [_1]
+cs : Kopírovat [_1]
+hu : [_1] másolása
+nl : Kopieer [_1]
+
+:_chare_title_add
+en : Add new character
+ru*:
+cs*:
+hu*:
+nl : Nieuwe karakter
+
+:_chare_form_generalinfo
+en : General info
+ru : Основная информация
+cs : Obecné informace
+hu : Általános info
+nl : Algemene info
+
+:_chare_form_name
+en : Name (romaji)
+ru : Название (ромадзи)
+cs : Jméno (romaji)
+hu : Név (romaji)
+nl : Naam (romaji)
+
+:_chare_form_original
+en : Original name
+ru : Исходное название
+cs : Originální jméno
+hu : Eredeti név
+nl : Originele naam
+
+:_chare_form_original_note
+en : The original name of the character, leave blank if it is already in the Latin alphabet.
+ru*:
+cs*:
+hu*:
+nl : De werkelijke naam van het karakter, laat leeg als dit al in het Latijnse alphabet is.
+
+:_chare_form_alias
+en : Aliases
+ru : Прочие названия
+cs : Aliasy
+hu : Más nevek
+nl : Aliassen
+
+:_chare_form_alias_note
+en : (Un)official aliases, separated by a newline.
+ru*:
+cs*:
+hu*:
+nl : (In)officiele aliassen, elke alias op een aparte regel.
+
+:_chare_form_desc
+en : Description
+ru : Описание
+cs : Popis
+hu : Leírás
+nl : Omschrijving
+
+:_chare_form_gender
+en : Gender
+ru*:
+cs*:
+hu*:
+nl : Geslacht
+
+:_chare_form_bday
+en : Birthday
+ru*:
+cs*:
+hu*:
+nl : Verjaardag
+
+:_chare_form_bday_fmt
+en : MM-DD (e.g. "01-26" for the 26th of January)
+ru*:
+cs*:
+hu*:
+nl : MM-DD (b.v. "01-26" voor 26 januari)
+
+:_chare_form_bday_err
+en : Birthday must be in MM-DD format.
+ru*:
+cs*:
+hu*:
+nl : Verjaardag moet in het MM-DD formaat zijn.
+
+:_chare_form_bust
+en : Bust
+ru*:
+cs*:
+hu*:
+nl : Borst
+
+:_chare_form_waist
+en : Waist
+ru*:
+cs*:
+hu*:
+nl : Taille
+
+:_chare_form_hip
+en : Hips
+ru*:
+cs*:
+hu*:
+nl : Hippen
+
+:_chare_form_height
+en : Height
+ru*:
+cs*:
+hu*:
+nl : Lengte
+
+:_chare_form_weight
+en : Weight
+ru*:
+cs*:
+hu*:
+nl : Gewicht
+
+:_chare_form_bloodt
+en : Blood type
+ru*:
+cs*:
+hu*:
+nl : Bloedgroep
+
+:_chare_form_main
+en : Instance of
+ru*:
+cs*:
+hu*:
+nl : Instantie van
+
+:_chare_form_main_note
+en : ID of the main character - the character of which this is an instance of.
+ru*:
+cs*:
+hu*:
+nl : ID van het hoofdkarakter - het karakter waar dit een instantie van is.
+
+:_chare_form_main_spoil
+en : Spoiler
+ru*:
+cs*:
+hu*:
+nl :
+
+:_chare_image
+en : Image
+ru : Изображение
+cs : Obrázek
+hu : Kép
+nl : Plaatje
+
+:_chare_image_none
+en : No image uploaded yet
+ru : Изображения пока нет
+cs : Obrázek ještě nebyl nahrán
+hu : Még nincs kép feltöltve
+nl : Nog geen plaatje geupload
+
+:_chare_image_processing
+en : ~[processing image, please return in a few minutes~]
+ru : ~[обработка изображения, пожалуйста подождите несколько минут~]
+cs : ~[obrázek se zpracovává, vraťte se prosím za několik minut~]
+hu : ~[a kép feldolgozás alatt van, gyere vissza pár perc múlva~]
+nl : ~[bezig met het verwerken van het plaatje, kom a.u.b. terug in een paar minuten~]
+
+:_chare_image_id
+en : Image ID
+ru*:
+cs*:
+hu*:
+nl : Plaatje ID
+
+:_chare_image_id_msg
+en : Use a character image that is already on the server. Set to '0' to remove the current image.
+ru :
+cs :
+hu :
+nl : Gebruik een karakterplaatje dat al op de server staat. Gebruik '0' om een huidig plaatje te verwijderen.
+
+:_chare_image_upload
+en : Upload new image
+ru : Загрузить новое изображение
+cs : Nahrát nový obrázek
+hu : Tölts fel egy új képet
+nl : Upload nieuw plaatje
+
+:_chare_image_upload_msg
+en : Image must be in JPEG or PNG format and at most 1MiB. Images larger than 256x300 will automatically be resized. Image must be safe for work!
+ru*:
+cs*:
+hu*:
+nl : Plaatje moet in het JPEG of PNG formaat zijn, en moet kleiner zijn dan 1MiB. Plaatjes groter dan 256x300 worden automatisch verkleind. Het plaatje moet veilig zijn in een werkomgeving!
+
+:_chare_traits
+en : Traits
+ru*:
+cs*:
+hu*:
+nl : Kenmerken
+
+:_chare_traits_sel
+en : Current traits
+ru*:
+cs*:
+hu*:
+nl : Huidige kenmerken
+
+:_chare_traits_add
+en : Add trait
+ru*:
+cs*:
+hu*:
+nl : Voeg kenmerk toe
+
+:_chare_traits_present
+en : Selected trait is already present.
+ru*:
+cs*:
+hu*:
+nl : Geselecteerd kenmerk is al aanwezig.
+
+:_chare_traits_nometa
+en : Meta traits can't be used here.
+ru*:
+cs*:
+hu*:
+nl : Metakenmerken kunnen hier niet gebruikt worden.
+
+:_chare_traits_empty
+en : No traits present yet.
+ru*:
+cs*:
+hu*:
+nl : Nog geen kenmerken aanwezig.
+
+:_chare_vns
+en : Visual novels
+ru*:
+cs*:
+hu*:
+nl :
+
+:_chare_vns_sel
+en : Selected visual novels
+ru*:
+cs*:
+hu*:
+nl : Geselecteerde visual novels
+
+:_chare_vns_add
+en : Add visual novel
+ru*:
+cs*:
+hu*:
+nl : Voeg visual novel toe
+
+:_chare_vns_empty
+en : No visual novels selected.
+ru*:
+cs*:
+hu*:
+nl : Geen visual novels geselecteerd
+
+:_chare_vns_addrel
+en : add release
+ru*:
+cs*:
+hu*:
+nl : voeg uitgave toe
+
+:_chare_vns_other
+en : All / others
+ru*:
+cs*:
+hu*:
+nl : Alle / overige
+
+:_chare_vns_del
+en : remove
+ru*:
+cs*:
+hu*:
+nl : verwijder
+
+:_chare_vns_allrel
+en : All releases already selected.
+ru*:
+cs*:
+hu*:
+nl : Alle uitgaven al geselecteerd.
+
+:_chare_vns_exists
+en : VN already present.
+ru*:
+cs*:
+hu*:
+nl : VN is al geselecteerd.
+
+
+# Character browser (/c/*)
+
+:_charb_title
+en : Browse characters
+ru*:
+cs*:
+hu*:
+nl : Browse karakters
+
+:_charb_noresults
+en : No results
+ru*:
+cs*:
+hu*:
+nl : Geen resultaten
+
+:_charb_noresults_msg
+en : No characters found that matched your criteria.
+ru*:
+cs*:
+hu*:
+nl : Geen karakters gevonden die aan je kriteria voldoen.
+
+
+
+
+
+
+#############################################################################
+## Trait page ##
+#############################################################################
+# Handler::Traits
+
+
+# Trait page (/i+)
+
+:_traitp_title
+en : [index,_1,Meta trait,Trait]: [_2]
+ru*:
+cs*:
+hu*:
+nl : [index,_1,Metakenmerk,Kenmerk]: [_2]
+
+:_traitp_del_title
+en : Trait deleted
+ru*:
+cs*:
+hu*:
+nl : Verwijderd kenmerk
+
+:_traitp_del_msg
+en : This trait has been removed from the database, and cannot be used or re-added.
+ File a request on the [url,/t/db,discussion board] if you disagree with this.
+ru*:
+cs*:
+hu*:
+nl : Dit kenmerk is verwijderd uit de database, en kan hier niet gebruikt worden.
+ Open een topic op het [url,/t/db,forum] als je het hier niet mee eens bent.
+
+:_traitp_pending_title
+en : Waiting for approval
+ru : Ждёт одобрения
+cs : Čeká na schválení
+hu : Megerősítésre vár
+nl : Wacht op goedkeuring
+
+:_traitp_pending_msg
+en : This trait is waiting for a moderator to approve it.
+ru*:
+cs*:
+hu*:
+nl : Dit kenmerk is nog niet goedgekeurd door een moderator.
+
+:_traitp_addchild
+en : Create child trait
+ru*:
+cs*:
+hu*:
+nl : Voeg een subkenmerk toe
+
+:_traitp_indexlink
+en : Traits
+ru*:
+cs*:
+hu*:
+nl : Kenmerken
+
+:_traitp_sexual
+en : Sexual content
+ru*:
+cs*:
+hu*:
+nl : Erotisch
+
+:_traitp_aliases
+en : Aliases
+ru : Прочие названия
+cs : Aliasy
+hu : Más nevek
+nl : Aliassen
+
+:_traitp_childs
+en : Child traits
+ru*:
+cs*:
+hu*:
+nl : Subkenmerken
+
+:_traitp_more
+en : [_1] more [quant,_1,trait,traits]
+ru*:
+cs*:
+hu*:
+nl : nog [_1] [quant,_1,kenmerk,kenmerken]
+
+:_traitp_charlist
+en : Characters
+ru*:
+cs*:
+hu*:
+nl : Karakters
+
+:_traitp_nochars
+en : This trait has not been linked to any characters yet, or they were hidden because of your spoiler settings.
+ru*:
+cs*:
+hu*:
+nl : Dit kenmerk is nog niet gekoppeld aan een karakter, of deze worden niet weergegeven in verband met je spoilerinstelling.
+
+
+# Trait add/edit form (/i+/edit, /i+/add, /i/new)
+
+:_traite_title_add
+en : Add child trait to [_1]
+ru*:
+cs*:
+hu*:
+nl : Voeg subkenmerk toe aan [_1]
+
+:_traite_title_edit
+en : Edit trait: [_1]
+ru*:
+cs*:
+hu*:
+nl : Wijzig kenmerk [_1]
+
+:_traite_title_new
+en : Add new trait
+ru*:
+cs*:
+hu*:
+nl : Voeg nieuw kenmerk toe
+
+:_traite_req_title
+en : Requesting new trait
+ru*:
+cs*:
+hu*:
+nl : Vraag nieuwe trait aan
+
+:_traite_req_subtitle
+en : Your trait must be approved
+ru*:
+cs*:
+hu*:
+nl : Je kenmerk moet goedgekeurd worden
+
+:_traite_req_msg
+en : Because all traits have to be approved by moderators, it can take a while before your trait will show up in the listings or can be used on character entries.
+ru*:
+cs*:
+hu*:
+nl : Omdat alle kenmerken goedgekeurd moeten worden door een moderator, kan het even duren voordat je kenmerk te zien is op de site of gebruikt kan worden voor karakters.
+
+:_traite_frm_name
+en : Primary name
+ru : Основное название
+cs : Hlavní jméno
+hu : Elsődleges név
+nl : Primaire naam
+
+:_traite_frm_by
+en : Added by
+ru : Добавлено
+cs : Přidal
+hu : Hozzáadta
+nl : Toegevoegd door
+
+:_traite_frm_state
+en : State
+ru : Состояние
+cs : Stav
+hu : Állapot
+nl : Status
+
+:_traite_frm_state0
+en : Awaiting moderation
+ru : Ожидает модерации
+cs : Čeká na schválení
+hu : Moderálásra vár
+nl : Wacht op goedkeuring
+
+:_traite_frm_state1
+en : Deleted/hidden
+ru : Удалён/скрыт
+cs : Smazán/skryt
+hu : Rejtett/törölve
+nl : Verwijderd
+
+:_traite_frm_state2
+en : Approved
+ru : Одобрен
+cs : Schválen
+hu : Elfogadva
+nl : Goedgekeurd
+
+:_traite_frm_meta
+en : This is a meta trait (only to be used as parent for other traits, not for direct use with characters)
+ru*:
+cs*:
+hu*:
+nl : Dit is een metakenmerk (kan alleen gebruikt worden als houder voor andere kenmerken, niet voor karakters)
+
+:_traite_frm_sexual
+en : Indicates sexual content
+ru*:
+cs*:
+hu*:
+nl : Erotisch
+
+:_traite_frm_alias
+en : Aliases
+ (separated by newlines)
+ru : Прочие названия
+ (каждое с новой строки)
+cs : Aliasy
+ (odděleny novou řádkou)
+hu : Más nevek
+ (új sorokkal elválasztva)
+nl : Aliassen
+ (gescheiden door enters)
+
+:_traite_frm_desc
+en : Description
+ru : Описание
+cs : Popis
+hu : Leírás
+nl : Beschrijving
+
+:_traite_frm_parents
+en : Parent traits
+ru*:
+cs*:
+hu*:
+nl : Hoofdkenmerk
+
+:_traite_frm_parents_msg
+en : List of trait IDs to be used as parent for this trait, separated by a space.
+ru*:
+cs*:
+hu*:
+nl : Spatiegescheiden lijst van hoofdkenmerken (numerieke IDs).
+
+:_traite_frm_gorder
+en : Group number
+ru*:
+cs*:
+hu*:
+nl : Groepnummer
+
+:_traite_frm_gorder_msg
+en : (Only used if this trait is a group. Used for ordering, lowest first)
+ru*:
+cs*:
+hu*:
+nl : (Alleen gebruikt als dit kenmerk een groep is. Gebruikt voor sorteren, laagste getal eerst)
+
+
+
+# Plain trait browser (/i/list)
+
+:_traitb_title
+en : Browse traits
+ru*:
+cs*:
+hu*:
+nl : Browse kenmerken
+
+:_traitb_state-1
+en : All
+ru : Все
+cs : Všechny
+hu : Mind
+nl : Alle
+
+:_traitb_state0
+en : Awaiting moderation
+ru : Ожидающие модерации
+cs : Čekající na schválení
+hu : Moderálásra vár
+nl : Wachtend op goedkeuring
+
+:_traitb_state1
+en : Deleted
+ru : Удалённые
+cs : Smazané
+hu : Törölve
+nl : Verwijderd
+
+:_traitb_state2
+en : Accepted
+ru : Одобренные
+cs : Přijaté
+hu : Elfogadva
+nl : Geaccepteerd
+
+:_traitb_noresults
+en : No results found
+ru : Совпадений не найдено
+cs : Nenalezeny žádné výsledky
+hu : Keresés sikertelen
+nl : Geen resultaten gevonden
+
+:_traitb_col_added
+en : Created
+ru : Созданы
+cs : Vytvořené
+hu : Létrehozva
+nl : Aangemaakt
+
+:_traitb_col_name
+en : Trait
+ru*:
+cs*:
+hu*:
+nl : Kenmerk
+
+:_traitb_note_awaiting
+en : awaiting moderation
+ru : ожидает модерации
+cs : čeká na schválení
+hu : moderálásra vár
+nl : wachtend op goedkeuring
+
+:_traitb_note_del
+en : deleted
+ru : удалён
+cs : smazán
+hu : törölve
+nl : verwijderd
+
+
+# Trait index (/g)
+
+:_traiti_title
+en : Trait index
+ru*:
+cs*:
+hu*:
+nl : Kenmerkenindex
+
+:_traiti_create
+en : Create new trait
+ru*:
+cs*:
+hu*:
+nl : Nieuw kenmerk
+
+:_traiti_search
+en : Search traits
+ru*:
+cs*:
+hu*:
+nl : Zoek kenmerken
+
+:_traiti_tree
+en : Trait tree
+ru*:
+cs*:
+hu*:
+nl : Kenmerkenlijst
+
+:_traiti_browseall
+en : Browse all traits
+ru*:
+cs*:
+hu*:
+nl : Browse alle kenmerken
+
+:_traiti_recent
+en : Recently added
+ru : Недавно добавлены
+cs : Poslední přidané
+hu : Nemrég hozzáadva
+nl : Recentelijk toegevoegd
+
+:_traiti_popular
+en : Popular traits
+ru*:
+cs*:
+hu*:
+nl : Populaire kenmerken
+
+:_traiti_queue
+en : Awaiting moderation
+ru : Ожидают модерации
+cs : Čekající na schválení
+hu : Moderálásra vár
+nl : Wacht op goedkeuring
+
+:_traiti_queue_empty
+en : Moderation queue empty! yay!
+ru : В очереди модерации пусто! Няа!
+cs : Fronta ke schválení prázdná! Jej!
+hu : Moderálásra váró lista üres! Hurrá!
+nl : Alles is gemodereerd! yay!
+
+:_traiti_queue_link
+en : Moderation queue
+ru : Очередь модерации
+cs : Fronta ke schválení
+hu : Moderálásra váró lista
+nl : Moderatiewachtrij
+
+:_traiti_denied
+en : Denied traits
+ru*:
+cs*:
+hu*:
+nl : Geweigerde kenmerken
+
+
+
+
+#############################################################################
## Personal User Lists ##
#############################################################################
# Handler::ULists
@@ -6195,6 +7411,13 @@ nl : Spatiegescheiden lijst van [url,http://anidb.net/,AniDB] anime IDs.
en [url,http://anidb.net/a3348,Fate/stay night] toe als gerelateerde anime.[br]
Houd er rekening mee dat het een aantal minuten kan duren voordat de animetitels zichtbaar worden op de VN pagina.
+:_vnedit_form_desc
+en : Description
+ru : Описание
+cs : Popis
+hu : Leírás
+nl : Omschrijving
+
:_vnedit_image
en : Image
ru : Изображение
@@ -7062,6 +8285,20 @@ cs*:
hu : Eltávolítás a listából
nl : Verwijder van lijst
+:_vnpage_tab_main
+en : main
+ru*:
+cs*:
+hu*:
+nl : primaire info
+
+:_vnpage_tab_chars
+en : characters
+ru*:
+cs*:
+hu*:
+nl : karakters
+
:_vnpage_rel
en : Releases
ru : Выпуски
@@ -7263,23 +8500,23 @@ nl : Dit item is uit de database verwijderd. Als je het hier niet mee eens bent
# The warning/notice messages on edit pages
:_editmsg_copy_title
-en : You're not editing a release!
-ru : Вы не редактируете выпуск, а копируете его!
-cs : Needitujete vydání!
-hu : Te most nem szerkesztesz egy kiadást!
-nl : Je bent geen uitgave aan het wijzigen!
+en : You're not editing a entry!
+ru*:
+cs*:
+hu*:
+nl : Je bent geen item aan het wijzigen!
:_editmsg_copy_msg
-en : You're about to insert a new release into the database with information based on [_1].[br]
- Hit the 'edit' tab on the right-top if you intended to edit the release instead of creating a new one.
-ru : Вы собираетесь создать новый выпуск, основанный на информации из [_1].[br]
+en : You're about to insert a new entry into the database with information based on [_1].[br]
+ Hit the 'edit' tab on the right-top if you intended to edit the entry instead of creating a new one.
+ru*: Вы собираетесь создать новый выпуск, основанный на информации из [_1].[br]
Щёлкните вкладку 'правка' в правом верхнем углу, если собирались редактировать выпуск, а не создавать новый.
-cs : Chystáte se vložit nové vydání do databáze s informacemi založenými na vydání [_1].[br]
+cs*: Chystáte se vložit nové vydání do databáze s informacemi založenými na vydání [_1].[br]
Pokud chcete editovat vydání místo toho, abyste vložili další vydání, přejděte na záložku 'editovat' v pravém horním rohu.
-hu : Te most ép arra készülsz, hogy egy új kiadást hozzál létre az adatbázisban, [_1]-ra alapozva az információkat.[br]
+hu*: Te most ép arra készülsz, hogy egy új kiadást hozzál létre az adatbázisban, [_1]-ra alapozva az információkat.[br]
Klikkelj a 'szerkesztés' fülre a jobb felső sarokba ha egy új kiadás helyet, csak szerkeszteni akartad a meglévőt.
-nl : Je staat op het punt om een nieuwe uitgave toe te voegen aan de database, met informatie gebasseerd op [_1].[br]
- Klik op de 'wijzig' tab rechtsboven als je bedoeling is om deze uitgave te wijzigen in plaats van een nieuwe aan te maken.
+nl : Je staat op het punt om een nieuw item toe te voegen aan de database, met informatie gebasseerd op [_1].[br]
+ Klik op de 'wijzig' tab rechtsboven als je bedoeling is om dit item te wijzigen in plaats van een nieuwe aan te maken.
:_editmsg_msg_title
en : Before editing:
@@ -7310,11 +8547,11 @@ hu : Nézz bele a [url,_1,szerkesztési előzményekbe], hogy lásd milyen vált
nl : Browse de [url,_1,geschiedenis] van dit item voor wijzigingen gerelateerd aan wat je wil doen.
:_editmsg_msg_search
-en : [url,_1,Search the database] to see if we already have information about this [index,_2,visual novel,release,producer].
-ru : [url,_1,Воспользуйтесь поиском], ведь вполне возможно, что у нас уже есть информация об [index,_2,этой новелле,этом выпуске,этой компании].
-cs : [url,_1,Prohledejte databázi], zda již nemáme informaci o [index,_2,této vizuální novele,tomto vydání,tomto producentovi].
-hu : [url,_1,Nézd át az adatbázist], hogy van-e már információ erről a [index,_2,visual novelről, kiadásról, készítőről].
-nl : [url,_1,Doorzoek de database] om te kijken of we al informatie hebben over deze [index,_2,visual novel,uitgave,producent].
+en : [url,_1,Search the database] to see if we already have information about this [index,_2,visual novel,release,producer,character].
+ru*: [url,_1,Воспользуйтесь поиском], ведь вполне возможно, что у нас уже есть информация об [index,_2,этой новелле,этом выпуске,этой компании].
+cs*: [url,_1,Prohledejte databázi], zda již nemáme informaci o [index,_2,této vizuální novele,tomto vydání,tomto producentovi].
+hu*: [url,_1,Nézd át az adatbázist], hogy van-e már információ erről a [index,_2,visual novelről, kiadásról, készítőről].
+nl : [url,_1,Doorzoek de database] om te kijken of we al informatie hebben over [index,_2,deze visual novel,deze uitgave,deze producent,dit karakter].
:_editmsg_revert_title
en : Reverting
@@ -7324,15 +8561,15 @@ hu : Visszaállítás
nl : Terugdraaien
:_editmsg_revert_msg
-en : You are editing an old revision of this [index,_1,visual novel,release,producer].
+en : You are editing an old revision of this [index,_1,visual novel,release,producer,character].
If you save it, all changes made after this revision will be reverted!
-ru : Вы правите старую редакцию страницы [index,_1,новеллы,выпуска,компании].
+ru*: Вы правите старую редакцию страницы [index,_1,новеллы,выпуска,компании].
Если вы сохраните её, все изменения, сделанные после этой правки будут утеряны!
-cs : Editujete starší verzi [index,_1,této vizuální novely,tohoto vydání,tohoto producenta].
+cs*: Editujete starší verzi [index,_1,této vizuální novely,tohoto vydání,tohoto producenta].
Pokud ji uložíte, všechny změny provedené po této revizi budou vráceny!
-hu : Te egy régi átjavítását szerkeszted ennek a [index,_1,visual novelnek, kiadásnak, készítőnek].
+hu*: Te egy régi átjavítását szerkeszted ennek a [index,_1,visual novelnek, kiadásnak, készítőnek].
Ha lemented, akkor minden szerkesztés amit ezután a javítás után végeztek megsemmisül!
-nl : Je wijzigd een verouderde versie van deze [index,_1,visual novel,uitgave,producent].
+nl : Je wijzigd een verouderde versie van [index,_1,deze visual novel,deze uitgave,deze producent,dit karakter].
Als je dit opslaat, zullen alle wijzigingen na deze revisie teruggedraait worden.
@@ -7480,6 +8717,13 @@ cs*:
hu : Légyszíves olvasdd el a szerkesztési összegzés [url,/d5.4, útmutatóját].
nl : Lees a.u.b. [url,/d5.4,de richtlijnen] over het gebruik van de samenvatting.
+:_formerr_e_mainchar
+en : Invalid main character. Make sure the ID is correct, that the main character itself is not an instance of an other character, and that this entry is not used as a main character elsewhere.
+ru*:
+cs*:
+hu*:
+nl : Ongeldig hoofdkaracter. Zorg ervoor dat het ID correct is, dat het hoofdkarakter zelf geen instantie is van een ander karakter, en dat dit karakter zelf ook niet ergens gebruikt wordt als hoofdkarakter.
+
:_formerr_title
en : Error
ru : Ошибка
diff --git a/data/notes/chardb b/data/notes/chardb
new file mode 100644
index 00000000..37dfa80d
--- /dev/null
+++ b/data/notes/chardb
@@ -0,0 +1,578 @@
+Character Database
+
+Last modified: 2011-02-14
+Status: Draft / incomplete
+
+
+*GLOBAL* data layout (ignoring any UI stuff or implementation details):
+
+ Format: (in case it's not obvious)
+ - field with a single value
+ - field with multiple values
+ - subfield - field for each value in the above field
+
+ New DB item: Trait (not versioned, moderated; similar to tags)
+ - name
+ - aliases
+ - description
+ - parents (multiple parents; similar to tags)
+ - state (new/approved/deleted; similar to tags)
+ - added by (similar to tags)
+
+ New DB item: Character (versioned) (without instances)
+ - name
+ - original name
+ - aliases / nicknames
+ - image
+ - description
+ - 3 sizes
+ - height and weight
+ - birthday (day and month only; year rarely known and hardly practical)
+ - list of traits
+ - trait ID
+ - spoiler flag
+ - list of VNs
+ - VN id
+ - release id
+ - spoiler flag ("the fact that this character appears in this game is a spoiler")
+ - role
+ - link to "main" character + spoiler indication
+
+ (UNUSED) New DB item: Character (versioned) (instance idea)
+ - aliases / nicknames (misc. not names of instances)
+ - default instance
+ - instances (at least one for each character)
+ - name
+ - original name
+ - image
+ - description
+ - spoiler flag
+ - 3 sizes
+ - age? birthday?
+ - list of traits
+ - trait ID
+ - spoiler level
+ - list of VNs
+ - VN id
+ - role (protagonist, primary character, side character, appears in)
+ - list of releases (none implies all)
+
+
+ "Same character" spoiler problems:
+ Case 1: (relatively common)
+ The characters themselves are not spoilers, but the fact that they are the same is.
+ Examples (ROT13):
+ Symphonic Rain: Cubeav & Nevrggn
+ Fate/stay night: Nepure & Fuvebh
+ How to handle:
+ With instances: no solution found yet, other than not using instances
+ Without instances: character relation with spoiler flag set
+
+ Case 2: (not very uncommon)
+ The entire existence of a character is a spoiler.
+ Examples (ROT13):
+ Ever17: Oyvpx Jvaxry
+ Aoishiro: Arxngn? Lnfhuvzr?
+ How to handle:
+ With instances: make the instance a spoiler (would take care of everything)
+ Without instances: the relation with the VN should be marked a spoiler
+
+ Case 3: (pretty uncommon)
+ What appears to be a single character turns out to be multiple characters in the end.
+ (this one sucks...)
+ Examples:
+ Ever17: Xvq (Ubxhgb & Elbtb), Lbh ('Nxv naq 'Uneh)
+ How to handle:
+ With instances: (ugly, but does the trick)
+ make one character entry with two or three instances: one with what
+ the game wants you to believe and use other instances for the actual
+ characters (set to spoiler).
+ Without instances:
+ make a separate entry for each character that the game wants you to
+ believe is a single character, and separate entries for the actual
+ characters. link them together and to VNs with spoiler markins.
+
+
+ Traits vs. fields:
+ - Preferably, we'd put as much data in traits, since these are flexible
+ - However, we do want to have some basic information (e.g. gender, apparent
+ age) to be specified early on (e.g. with a dropdown selection). And we also
+ want them to be nicely ordered on the charpage (e.g. Gender: male).
+ - It would be nice if it were also possible to limit the selection of some
+ tags to only one for a specific category. E.g. A character can only have
+ one "Gender" trait. But this isn't all that important, since I doubt users
+ are *that* stupid and traits are part of the characters' revisioning
+ system, which means everything can be moderated quite easily.
+ - "Displaying everything nice on a characters' page"-solutions:
+ 1. Table-layout with a "Parent: trait1, trait2" listing, where parent is
+ the top trait (i.e. without parents). Traits with multiple parents will
+ have to be listed multiple times.
+ 2. Same as above, but add a boolean flag "category" to the traits. The
+ traits with this flag set will be used in the table instead of the
+ top-traits. This allows more flexibility of the trait tree, but is more
+ complex to implement.
+
+
+ Linking "same character"s together (without instances):
+ - It is possible to handle this with a regular char<->char "is the same as"
+ relation. This can become annoying when there are many entries that are
+ the same. For example, if there are four characters (A-D), then there are
+ many different ways to link these together with that relation:
+ "Linked list"-style: A=B; B=A,C; C=B,D; D=C
+ "Binary Tree"-style: A=B,C; B=A,D; C=A; D=B
+ "Everything"-style: A=B,C,D; B=A,C,D; C=A,B,D; D=A,B,C
+ That is quite annoying, both to the user and in the code. :-(
+ On the upside, since every relation has a "spoiler" option, this does allow
+ some flexibility: A=B and C=D may be spoilers, but B=C does not have to be.
+ - An alternative approach: "parent"->"child" relations (let's call them
+ main->guise relations (thank you AniDB), since the CS terminology fails
+ here).
+ To explain: each "guise" character can be linked to a single "main"
+ character (with spoiler flag). As a restriction, this "main" character can
+ not itself be linked to an other character as "guise" again. This gives us
+ an easy structure to work with. With the above example, using "A" as "main"
+ character (and "->" is "links to"): A; B->A; C->A, D->A.
+ This approach is actually extremely similar to the idea of using instances:
+ the data structure created with these links is equivalent to the structure
+ with instances. The main differences are the implementation and the idea
+ that the "instances" themselves are centric rather than their "main
+ character". This idea also prevents the issue of "same character spoiling:
+ case 1".
+
+
+ Misc. questions:
+ - How to handle cases where in an original release a character only played a
+ side role, while in a later release this character would get a route?
+ This isn't entirely uncommon... Possible solutions:
+ 1. Allow a character to be linked to the same VN more than once with
+ different role and different releases. This solves the problem, but would
+ make it hard to generate a nice overview of all characters in a VN
+ (covering all its releases).
+ 2. Move the "role" field as a subfield of the release links. This is
+ probably a better idea...
+ But I'm going with option 1 anyway, since is easier and more efficient.
+ - Similar to the above, are there cases where in an original release the
+ existance of a character is a spoiler, while in a later release it is not?
+ Can't think if any...
+ - "Has route" indication?
+ This obviously doesn't work for all VNs, since routes are not always linked
+ to characters. In the case that it does work, it should probably be a
+ subfield of the release links (see the "role" thing above).
+ Better yet, we should have a "route database". For the future. >_>
+ - "Age" field? Bad idea?
+ - May differ per release even when nothing else changed (hello JAST USA).
+ Can be handled by adding a new character and linking and stuff, but isn't
+ worth the trouble)
+ - Conflicting information. For example: When heroines are 1st grade high
+ school but it is stated in the beginning that "all characters are at
+ least 18 years old"
+ - Just a bad idea in general, since there are quite a few fuckable
+ 10-years-olds, and explicitely stating that is not a very nice thing.
+ - External links?
+ - Wikipedia (en)?
+ - AniDB
+ - Animecharacterdatabase.com (crappy site, but people seem to use it?)
+ - MLA?
+
+
+ Traits vs. VN tags (not very important for now):
+ - Ideally, some character traits would imply VN tags.
+ - For example: a VN that has a character linked to it with the "vampire"
+ trait, the VN should have the "Vampire" tag.
+ - Correctly implementing this would be hard, but it is possible to get Multi
+ to add auto-votes with some rules. This would require traits to have an
+ "implies" relation with tags.
+ - "Level" setting can be partly determined from the importance of the role of
+ the character. Though this will just be a wild guess.
+ - "Spoiler" setting is inherited, though unknown which level it should have.
+ The following might work:
+ tag spoiler = 0
+ tag spoiler +1 if the character-VN link is a spoiler
+ tag spoiler +1 if the tag-character link is a spoiler
+
+
+ Ever17 example (with instances, SPOILERS):
+ These examples are not technically correct, since the two "You"s are two
+ separate characters, and there is no such thing as "Kid". But the following
+ example would do the trick in a way that is neither spoilerous, nor
+ completely wrong.
+
+ You:
+ alias: You, Nakkyu
+ Instance #1:
+ | name: Yubiseiharukana Tanaka
+ | VNs: v17, all releases, primary character
+ | spoiler: no
+ Instance #2:
+ | name: Yubiseiakikana Tanaka
+ | VNs: v17, all releases, side character
+ | spoiler: yes
+
+ Kid:
+ alias: Kid
+ Instance #1:
+ | name: Shounen
+ | description: Doesn't even remember his own name, dumbass! Protagonist in the 2034 routes.
+ | VNs: v17, all releases, protagonist
+ | spoiler: no
+ Instance #2:
+ | name: Ryogo Kaburaki
+ | description: "kid" in 2017, "Takeshi" in 2034
+ | VNs: v17, all releases, primary character
+ | spoiler: yes
+ Instance #3:
+ | name: Hokuto
+ | description: "kid" in 2034, son of Tsugumi and Takeshi
+ | VNs: v17, all releases, protagonist
+ | spoiler: yes
+
+ Without instances: make a separate character entry for each of the above
+ instances and set the appropriate spoiler flags.
+
+
+
+
+The term "instance":
+ To make things extra confusing, the term "instance" has two meanings:
+ 1. In the above part (global data layout) and in Maou's original draft, it
+ refers to a special database entry of type "instance", separate from the type
+ "character".
+ 2. In the rest of this document, I'm using it to mean a character entry which
+ has the "main character" field set. In the global data layout I used the term
+ "guise", but I believe "instance" is better.
+
+
+
+
+User interface considerations:
+
+ Pages to consider:
+ - trait page
+ - trait edit
+ - trait listing / overview
+ - character page
+ - character edit
+ - character listing / search
+ - VN page (list of characters)
+
+ Trait page:
+ URI: /i+
+ Similar to tag page: basic description + listing of characters.
+ The listing of characters includes all characters linked to child traits.
+ (same as with tags)
+
+ Trait edit:
+ URI: /i+/edit, /i/new
+ The regular add/edit form.
+ - What to do with the linked characters when a trait is marked as deleted
+ or meta? Batch-edit all character entries to remove the trait? Sounds
+ painful...
+
+ Trait listing / overview:
+ URI: /i
+ Just be creative with this, can be similar to the tag overview.
+
+ Character page:
+ URI: /c+
+ If the character is not an instance (i.e. it has no "main character"):
+ Display the information of the requested character, followed by that of
+ all instances linked to it. (spoilerous instances are hidden by default).
+ If the character is an instance:
+ - Display the information of the requested character, followed by a link
+ to its main character? (if it's not a spoiler of course).
+ - Or make no distinction between "Main character" and "instance", and
+ simply display all information or the main character and its other
+ instances on the same page? (similar to the main character page). This
+ is sligtly counter-intuitive when the relation between the instance and
+ its main character is a spoiler: in that case all the related entries
+ would be hidden, rather than only those of which their relation is a
+ spoiler. (See global data layout -> same character linking above for a
+ discussion).
+ Information display for a single character:
+ Name
+ (orig name)
+ [image] [table]
+ [description]
+ table:
+ | Name: <name>
+ | Original name: <orig>
+ | ...
+ | Visual novels: Role - VN title
+ | VN title 2
+ | > Role - release title
+ | > Role - other releases
+ | Trait group #1: Trait1, trait2, ..
+ | Trait group #2: ..
+
+ Character edit:
+ URI: /c+/edit
+ This requires some thinking...
+ - Batch-edit-with-instances:
+ Instances more often than not share quite a bit of information with each
+ other. When editing a character that is an instance or has instances, the
+ edit page should preferably contain the char-edit-form for all related
+ characters, and automatically link fields that are the same for all
+ instances together. E.g. each field could have a checkbox indicating
+ "same as main character", in which case editing the field in the main
+ character would update that of the instances as well.
+ This is slightly more annoying with traits, since this should be done on
+ a per-trait basis.
+ - VN-linking:
+ Just mirror the structure of the chars_traits table:
+ <VN title> <release dropdown> <spoiler checkbox> <role dropdown>
+ A VN can be added more than once to select other releases. This isn't all
+ that intuitive, but is simple to implement and does the job.
+ - Trait linking interface? How will it work together with the
+ batch-edit-with-instances and copying over traits from the main
+ character?
+
+ Character listing / search:
+ URI: /c
+ Nothing special.
+
+ VN page (list of characters):
+ URI: /v+ (stays the same, obviously)
+ Add tabs above the "Releases" box with two items: "Main" and "Characters".
+ "Main": Displays the usual "Releases" / "User stats" / "Screenshots" boxes
+ "Characters": Displays a list of characters linked to that VN,
+ getchu-style. Spoilerous characters are hidden by default.
+ JS-tabs vs. new URI:
+ JS tabs are nicer, but require all character information to be sent with
+ each pageview on the VN page. This is heavy on the server and slows down
+ page loading. This can be avoided by loading the character data using
+ AJAX when the tab is opened, but I'm not very fond of using AJAX in this
+ way. So in that sense a separate URI may be a better idea. E.g. /v+/chars
+ Listing:
+ Order by role: protag -> main chars -> side -> etc
+ Display image + quite a bit of information for protag + main chars,
+ followed by a plain and simple (table) listing of "other characters".
+
+ Misc. stuff:
+ - Do we want to be able to search for VNs that have a character with a
+ certain trait? For example, to get a listing of all VNs that have a
+ "vampire" character. This will be very heavy on the server if it were
+ implemented without some form of caching, and may not be very useful if
+ you can't set other constraints as well (e.g. it must be a main character
+ in the VN).
+ People will definitely complain if they can't search on their "Genius
+ protagonist" tag anymore. >_>
+
+
+
+
+The SQL schema:
+ (outdated, see /util/updates/update_2.19.sql instead)
+
+ CREATE TABLE traits (
+ id SERIAL PRIMARY KEY,
+ name varchar(250) NOT NULL UNIQUE,
+ description text NOT NULL DEFAULT '',
+ meta boolean NOT NULL DEFAULT false,
+ added timestamptz NOT NULL DEFAULT NOW(),
+ state smallint NOT NULL DEFAULT 0,
+ addedby integer NOT NULL DEFAULT 0 REFERENCES users (id)
+ );
+
+ CREATE TABLE traits_aliases (
+ alias varchar(250) NOT NULL PRIMARY KEY,
+ trait integer NOT NULL REFERENCES traits (id)
+ );
+
+ CREATE TABLE traits_parents (
+ trait integer NOT NULL REFERENCES traits (id),
+ parent integer NOT NULL REFERENCES traits (id),
+ PRIMARY KEY(trait, parent)
+ );
+
+ CREATE TABLE chars (
+ id SERIAL PRIMARY KEY,
+ latest integer NOT NULL DEFAULT 0 REFERENCES chars_rev (id),
+ locked boolean NOT NULL DEFAULT FALSE,
+ hidden boolean NOT NULL DEFAULT FALSE
+ );
+
+ CREATE TABLE chars_rev (
+ id integer NOT NULL PRIMARY KEY REFERENCES changes (id),
+ cid integer NOT NULL REFERENCES chars (id),
+ name varchar(250) NOT NULL DEFAULT '',
+ original varchar(250) NOT NULL DEFAULT '',
+ alias varchar(500) NOT NULL DEFAULT '',
+ image integer NOT NULL DEFAULT 0,
+ "desc" text NOT NULL DEFAULT '',
+ s_bust smallint NOT NULL DEFAULT 0, -- in cm
+ s_waist smallint NOT NULL DEFAULT 0,
+ s_hip smallint NOT NULL DEFAULT 0,
+ b_month smallint NOT NULL DEFAULT 0, -- birthday
+ b_day smallint NOT NULL DEFAULT 0,
+ height smallint NOT NULL DEFAULT 0, -- in cm
+ weight smallint NOT NULL DEFAULT 0, -- in kg
+ main integer REFERENCES chars (id),
+ main_spoil boolean NOT NULL DEFAULT false
+ );
+
+ CREATE TABLE chars_traits (
+ cid integer NOT NULL REFERENCES chars_rev (id),
+ tid integer NOT NULL REFERENCES traits (id),
+ spoil boolean NOT NULL DEFAULT false, -- boolean or smallint?
+ PRIMARY KEY(cid, tid)
+ );
+
+ CREATE TABLE chars_vns (
+ cid integer NOT NULL REFERENCES chars_rev (id),
+ vid integer NOT NULL REFERENCES vn (id),
+ rid integer REFERENCES releases (id), -- NULL = "all releases"
+ spoil boolean NOT NULL DEFAULT false,
+ role char_role NOT NULL DEFAULT 'main',
+ PRIMARY KEY(cid, vid, rid)
+ );
+
+ -- this one is probably required to speed up character-by-trait search.
+ -- Similar to chars_traits, but has two differences:
+ -- 1. all parent tags are included here
+ -- 2. unversioned. i.e. it links to the chars table instead of chars_rev.
+ CREATE TABLE chars_traits_inherit (
+ cid integer NOT NULL REFERENCES chars (id),
+ tid integer NOT NULL REFERENCES traits (id),
+ spoil boolean NOT NULL DEFAULT false, -- boolean or smallint?
+ PRIMARY KEY(cid, tid)
+ );
+
+
+
+
+
+
+The original Maou draft (which I use as a sort of guideline / inspiration):
+
+ Each game has a list of characters.
+
+ Each "character" has:
+ 0. an ID (cXXX)
+ 1. a name (mandatory)
+ 2. original (kanji/kana)
+ 3. alias, nicknames
+ 4. a portray/pic (if so desired, move to instance)
+ 5. a list of instances (mandatory)
+
+ Instances:
+ 0. an ID (iXXX)
+ 1. a vn (mandatory)
+ 2. a list of releases
+ 3. a description - what it says on the label
+ 4. a traitlist
+ 5. a commentlist
+
+ Traits(have spoilerratings and inheritance, but are not votable):
+ 1st Trait (mandatory when creating): Role - protagonist, Heroine, Side Character, Antagonist
+ 2nd Trait ( " " " ): Sex - male, female, both?
+ following traits should describe the character, our current character tags could prolly be converted for that
+
+ commentlist: People can post (short) opinions about a character quickly...
+ Users can edit/delete their own entries, mods can edit/delete everything.
+
+ Q: Why instances?
+ A: Characters often reappear in other games by the same company - works by
+ age are just one example. More often that not, they change between their
+ appearances - most obviously, their role changes, but their other
+ characteristics may also change to do artistic license, different PoV or
+ (gasp) character development. The alternative to this would be to add each
+ version as a new character and then link them together, but the end-result
+ would look rather silly for longer series IMHO.
+
+ Q: How would adding a character work? How would adding new instances work?
+ A: When adding a character you'd create the first instance together with the
+ character. When you decide to add another instance, you start with the
+ currently selected instance as base - so you'd just have to make the
+ necessary adjustments.
+
+ Q: How would characters be displayed?
+ A: VN Characterlist (part of/accessible from the VN page): All characters of
+ the VN together, ordered Protagonist > Heroine > Sidekick > Antagonist, with
+ the applicable instance only.
+ Characterpage: Just a single character (think release page), toggle/dropdown
+ menu/whatever to switch between instances. Hide traits above a set
+ spoilerlevel (as we're doing with tags already).
+
+ Q: How would searching work?
+ A: Enter a combination of traits you're looking for (with exclusions - find
+ all swordwielding heroines that DO NOT have the "has rapescene" trait), get a
+ list of games that have the characters (instances) in question. If the trait
+ in question is a spoiler, the charactername shouldn't be displayed, else it
+ should be presented together with the VN.
+
+ Q: What about Seiyuu?
+ A: Just link to the corresponding entry in the StaffDB... <_<
+
+
+ Release-sensitive Instances:
+ Normally, the list contains all releases of the VN in question. (If a new
+ release is added it will be added to that list per default (if there are
+ multiple instances for that VN, it should be selectable from a dropdown
+ menu). Adding new instances for releases would work identical to adding a
+ new instance for another VN (with the difference that when you add an
+ instance to a release, all other instance remove said VN from their list).
+ If a character has multiple instances for a single VN, the applicable
+ releases would be displayed in the character list and the user would be
+ able to switch between the instances (similar to the character page).
+ The advantage of this is that it covers everything. The drawback is that it
+ is more work intensive and complex.
+ NOTE: Add only new instances for full versions, NOT for trials
+
+ List of Relations:
+ 5 possibilities:
+ Instance <-> Instance, display on a simple map
+ if they're multiple instances, they're all displayed together with (<->
+ same character) relations between them
+ Instance <-> Instance, display on dynamic map
+ display each character once, but allow to switch between the various
+ instances while viewing the map, with the relations being redrawn based on
+ the relations the new instance (and instances being replaced/added based
+ where necessary)
+ Instance <-> Instance. display release-centric
+ first, display the relations of the instances that belong to the release
+ and connect them. Ignore any that don't belong to the release. Then draw
+ relations to instances which aren't part of the current VN. Allow to switch
+ between the various releases with different relation map easily)
+ Instance <-> Instance, display instances as a single character,
+ colourcode relations depending on what instances it applies to (with legend
+ for which colour belongs to what release/instances)
+ Character <-> Character, what it says
+ simplistic, not suitable for longer series and more complex universes
+
+ Examples:
+ c1
+ NAME: SAKURAI KEI
+ ORIGINAL: 櫻井螢
+ ALIAS:
+ PORTRAY: (prolly taken from the getchu page)
+ INSTANCES: i1, i2
+
+ i1
+ VN: v548
+ RELEASES: r1132, r1133
+ DESCRIPTION: 5th Seat, Leonhard August.
+ TRAITS: Antagonist Heroine, Female, Long Hair, Black Hair, Coodere, Lacks
+ Ending, Has Optional Sex eroscene, Swordwielding, Immortal (Spoiler 1),
+ Maou's Harem
+ COMMENTS: Maou(One of my favourite heroines, sadly she's lacking a route.)
+
+ i2
+ VN: v548
+ RELEASES: r3228
+ DESCRIPTION: 5th Seat, Leonhard August.
+ TRAITS: Antagonist Heroine, Female, Long Hair, Black Hair, Coodere, Has
+ Ending, Has Sex Scenes, Swordwielding, Immortal (Spoiler 1), Maou's Harem
+ COMMENTS: Maou(Perfect), RandomPerson(Interesting character)
+
+ really short traittree:
+ Traits(Role(Protagonist, Heroine (Antagonist Heroine - also child of
+ Antagonist)), Sidekick, Antagonist), Gender (Male, Female, Both, Other),
+ Appearance(Hair(Long Hair, Black Hair)), Personality(Deretypes(classic
+ Tsundere, Tsundere, Deredere, Coodere)), Significance(Routes(Has End(Has
+ True End), Lacks End), Has Sex Scenes(Has Insignificant/Optional
+ eroscene, has rapescene (has unavoidable rapescene))), Relation(Sister,
+ Senpai, Osananajimi), Vocation(Fighting (Swordwielding, Knight), Hacker),
+ Other(Idiot Friend, Immortal, Maou's Harem)
+
+ Ok, just kidding about the Harem thing <_<
+
diff --git a/data/script.js b/data/script.js
index 45c42162..aa7b91a0 100644
--- a/data/script.js
+++ b/data/script.js
@@ -1,4 +1,6 @@
/* function/attribute prefixes:
+ * ctr -> Character <-> trait linking
+ * cvn -> Character <-> VN linking
* date -> Date selector
* dd -> dropdown
* ds -> dropdown search
@@ -1254,7 +1256,7 @@ var tglSpoilers = [];
function tglLoad() {
for(var i=0; i<=3; i++)
- tglSpoilers[i] = mt('_tagv_spoil'+i); // l10n /_tagv_spoil\d+/
+ tglSpoilers[i] = mt('_spoil_'+(i-1)); // l10n /_spoil_-?\d+/
// tag dropdown search
dsInit(byId('tagmod_tag'), '/xml/tags.xml?q=', function(item, tr) {
@@ -1752,6 +1754,338 @@ if(byId('prodrelations'))
+/* C H A R A C T E R T R A I T S (/c+/edit) */
+
+// l10n /_spoil_-?\d+/
+
+function ctrLoad() {
+ // load current traits
+ var l = byId('traits').value.split(' ');
+ var v = {}; // tag id -> spoiler lookup table
+ var q = []; // list of id=X parameters
+ for(var i=0; i<l.length; i++) {
+ if(l[i]) {
+ var m = l[i].split(/-/);
+ v[m[0]] = m[1];
+ q[i] = 'id='+m[0];
+ }
+ }
+ if(q.length > 0)
+ ajax('/xml/traits.xml?'+q.join(';'), function (ht) {
+ var t = ht.responseXML.getElementsByTagName('item');
+ for(var i=0; i<t.length; i++)
+ ctrAdd(t[i], v[t[i].getAttribute('id')]);
+ }, 1);
+ else
+ ctrEmpty();
+
+ // dropdown
+ dsInit(byId('trait_input'), '/xml/traits.xml?q=', function(item, tr) {
+ var g = item.getAttribute('groupname');
+ g = g ? g+' / ' : '';
+ tr.appendChild(tag('td', { style: 'text-align: right; padding-right: 5px'}, 'i'+item.getAttribute('id')));
+ tr.appendChild(tag('td',
+ tag('b', {'class':'grayedout'}, g), item.firstChild.nodeValue,
+ tag('b', {'class':'grayedout'}, item.getAttribute('meta')=='yes' ? mt('_js_ds_tag_meta') : '')));
+ }, ctrFormAdd);
+}
+
+function ctrEmpty() {
+ var x = byId('traits_loading');
+ var t = byId('traits_tbl');
+ if(x)
+ t.removeChild(x);
+ var l = byName(t, 'tr');
+ var e = byId('traits_empty');
+ if(e && l.length > 1)
+ t.removeChild(e);
+ else if(!e && l.length < 1)
+ t.appendChild(tag('tr', {id:'traits_empty',colspan:3}, tag('td', mt('_chare_traits_empty'))));
+}
+
+function ctrAdd(item, spoil) {
+ var id = item.getAttribute('id');
+ var name = item.firstChild.nodeValue;
+ var group = item.getAttribute('groupname');
+ var sp = tag('td', {'class':'tc_spoil', onclick:ctrSpoilNext, ctr_spoil:spoil}, mt('_spoil_'+spoil));
+ ddInit(sp, 'tagmod', ctrSpoilDD);
+ byId('traits_tbl').appendChild(tag('tr', {ctr_id:id, ctr_spoiler:spoil},
+ tag('td', {'class':'tc_name'},
+ tag('b', {'class':'grayedout'}, group?group+' / ':''),
+ tag('a', {'href':'/i'+id}, name)),
+ sp,
+ tag('td', {'class':'tc_del'}, tag('a', {href:'#', onclick:ctrDel}, 'del'))
+ ));
+ ctrEmpty();
+ ctrSerialize();
+}
+
+function ctrFormAdd(item) {
+ var l = byName(byId('traits_tbl'), 'tr');
+ for(var i=0; i<l.length; i++)
+ if(l[i].ctr_id && l[i].ctr_id == item.getAttribute('id'))
+ break;
+ if(i < l.length)
+ alert(mt('_chare_traits_present'));
+ else if(item.getAttribute('meta') == 'yes')
+ alert(mt('_chare_traits_nometa'));
+ else
+ ctrAdd(item, 0);
+ return '';
+}
+
+function ctrSpoilNext() {
+ if(++this.ctr_spoil > 2)
+ this.ctr_spoil = 0;
+ setText(this, mt('_spoil_'+this.ctr_spoil));
+ ddRefresh();
+ ctrSerialize();
+}
+
+function ctrSpoilDD(lnk) {
+ var lst = tag('ul', null);
+ for(var i=0; i<=2; i++)
+ lst.appendChild(tag('li', i == lnk.ctr_spoil
+ ? tag('i', mt('_spoil_'+i))
+ : tag('a', {href: '#', onclick:ctrSpoilSet, ctr_td:lnk, ctr_sp:i}, mt('_spoil_'+i))
+ ));
+ return lst;
+}
+
+function ctrSpoilSet() {
+ this.ctr_td.ctr_spoil = this.ctr_sp;
+ setText(this.ctr_td, mt('_spoil_'+this.ctr_sp));
+ ddHide();
+ ctrSerialize();
+ return false;
+}
+
+function ctrDel() {
+ var tr = this;
+ while(tr.nodeName.toLowerCase() != 'tr')
+ tr = tr.parentNode;
+ tr.parentNode.removeChild(tr);
+ ctrEmpty();
+ ctrSerialize();
+ return false
+}
+
+function ctrSerialize() {
+ var l = byName(byId('traits_tbl'), 'tr');
+ var v = [];
+ for(var i=0; i<l.length; i++)
+ if(l[i].ctr_id)
+ v.push(l[i].ctr_id+'-'+byClass(l[i], 'tc_spoil')[0].ctr_spoil);
+ byId('traits').value = v.join(' ');
+}
+
+if(byId('traits_tbl'))
+ ctrLoad();
+
+
+
+
+/* C H A R A C T E R < - > V N L I N K I N G (/c+/edit) */
+
+function cvnLoad() {
+ // load current links
+ var l = byId('vns').value.split(' ');
+ var v = {}; // vid -> { rid: [ role, spoil ], .. }
+ var q = []; // list of v=X parameters
+ for(var i=0; i<l.length; i++) {
+ if(!l[i])
+ continue;
+ var m = l[i].split(/-/); // vid, rid, spoil, role
+ if(!v[m[0]]) {
+ q.push('v='+m[0]);
+ v[m[0]] = {};
+ }
+ v[m[0]][m[1]] = [ m[3], m[2] ];
+ }
+ if(q.length > 0)
+ ajax('/xml/releases.xml?'+q.join(';'), function(hr) {
+ var vns = byName(hr.responseXML, 'vn');
+ for(var i=0; i<vns.length; i++) {
+ var vid = vns[i].getAttribute('id');
+ cvnVNAdd(vns[i]);
+ var rels = byName(vns[i], 'release');
+ for(var r=0; r<rels.length; r++) {
+ var rid = rels[r].getAttribute('id');
+ if(v[vid][rid])
+ cvnRelAdd(vid, rid, v[vid][rid][0], v[vid][rid][1]);
+ }
+ if(v[vid][0])
+ cvnRelAdd(vid, 0, v[vid][0][0], v[vid][0][1]);
+ }
+ }, 1);
+ else
+ cvnEmpty();
+
+ // dropdown search
+ dsInit(byId('vns_input'), '/xml/vn.xml?q=', function(item, tr) {
+ tr.appendChild(tag('td', { style: 'text-align: right; padding-right: 5px'}, 'v'+item.getAttribute('id')));
+ tr.appendChild(tag('td', shorten(item.firstChild.nodeValue, 40)));
+ }, cvnFormAdd);
+}
+
+function cvnEmpty() {
+ var x = byId('vns_loading');
+ var t = byId('vns_tbl');
+ if(x)
+ t.removeChild(x);
+ var l = byName(t, 'tr');
+ var e = byId('vns_empty');
+ if(e && l.length > 1)
+ t.removeChild(e);
+ else if(!e && l.length < 1)
+ t.appendChild(tag('tr', {id:'vns_empty',colspan:3}, tag('td', mt('_chare_vns_empty'))));
+}
+
+function cvnVNAdd(vn, rel) {
+ var vid = vn.getAttribute('id');
+ var rels = byName(vn, 'release');
+ byId('vns_tbl').appendChild(tag('tr', {id:'cvn_v'+vid, cvn_vid:vid, cvn_rels:rels},
+ tag('td', {'class':'tc_vn',colspan:4}, 'v'+vid+':',
+ tag('a', {href:'/v'+vid}, vn.getAttribute('title')),
+ tag('i', '(', tag('a', {href:'#', onclick:cvnRelNew}, mt('_chare_vns_addrel')), ')')
+ )
+ ));
+ if(rel)
+ cvnRelAdd(vid, 0, 'primary', 0);
+ cvnEmpty();
+}
+
+function cvnRelAdd(vid, rid, role, spoil) {
+ var rels = byId('cvn_v'+vid).cvn_rels;
+ var rsel = tag('select', {onchange:cvnRelChange}, tag('option', {value:0}, mt('_chare_vns_other')));
+ for(var i=0; i<rels.length; i++) {
+ var id = rels[i].getAttribute('id');
+ rsel.appendChild(tag('option', {value: id, selected:id==rid},
+ '['+rels[i].getAttribute('lang')+'] '+rels[i].firstChild.nodeValue+' (r'+id+')'));
+ }
+
+ var lsel = tag('select', {onchange:cvnSerialize});
+ for(var i=0; i<char_roles.length; i++)
+ lsel.appendChild(tag('option', {value: char_roles[i], selected:char_roles[i]==role}, char_roles[i]));
+
+ // l10n /_spoil_\d+/
+ var ssel = tag('select', {onchange:cvnSerialize});
+ for(var i=0; i<3; i++)
+ ssel.appendChild(tag('option', {value:i, selected:i==spoil}, mt('_spoil_'+i)));
+
+ var tbl = byId('vns_tbl');
+ var l = byName(tbl, 'tr');
+ var last = null;
+ for(var i=1; i<l.length; i++)
+ if(l[i-1].cvn_vid == vid && l[i].cvn_vid != vid)
+ last = l[i-1];
+ tbl.insertBefore(tag('tr', {id:'cvn_v'+vid+'r'+rid, cvn_vid:vid, cvn_rid:rid},
+ tag('td', {'class':'tc_rel'}, rsel),
+ tag('td', {'class':'tc_rol'}, lsel),
+ tag('td', {'class':'tc_spl'}, ssel),
+ tag('td', {'class':'tc_del'}, tag('a', {href:'#', onclick:cvnRelDel}, mt('_chare_vns_del')))
+ ), last);
+}
+
+function cvnRelChange() {
+ // look for duplicates and disallow the change
+ var val = this.options[this.selectedIndex].value;
+ var tr = this;
+ while(tr.nodeName.toLowerCase() != 'tr')
+ tr = tr.parentNode;
+ if(byId('cvn_v'+tr.cvn_vid+'r'+val)) {
+ alert('Release already selected.');
+ for(var i=0; i<this.options.length; i++)
+ this.options[i].selected = this.options[i].value == tr.cvn_rid;
+ return;
+ }
+ // otherwise, 'rename' this entry
+ tr.id = 'cvn_v'+tr.cvn_vid+'r'+val;
+ tr.cvn_rid = val;
+ cvnSerialize();
+}
+
+function cvnRelNew() {
+ var tr = this;
+ while(tr.nodeName.toLowerCase() != 'tr')
+ tr = tr.parentNode;
+ var id = 0;
+ if(byId('cvn_v'+tr.cvn_vid+'r0')) {
+ for(var i=0; i<tr.cvn_rels.length; i++) {
+ id = tr.cvn_rels[i].getAttribute('id');
+ if(!byId('cvn_v'+tr.cvn_vid+'r'+id))
+ break;
+ }
+ if(i == tr.cvn_rels.length) {
+ alert(mt('_chare_vns_allrel'));
+ return false;
+ }
+ }
+ cvnRelAdd(tr.cvn_vid, id, 'primary', 0);
+ cvnSerialize();
+ return false;
+}
+
+function cvnRelDel() {
+ var tbl = byId('vns_tbl');
+ var tr = this;
+ while(tr.nodeName.toLowerCase() != 'tr')
+ tr = tr.parentNode;
+ tbl.removeChild(tr);
+ var l = byName(tbl, 'tr');
+ var c = 0;
+ for(var i=0; i<l.length; i++)
+ if(l[i].cvn_vid == tr.cvn_vid)
+ c++;
+ if(c <= 1)
+ tbl.removeChild(byId('cvn_v'+tr.cvn_vid));
+ cvnSerialize();
+ cvnEmpty();
+ return false;
+}
+
+function cvnFormAdd(item) {
+ var inpt = byId('vns_input');
+ inpt.disabled = true;
+
+ ajax('/xml/releases.xml?v='+item.getAttribute('id'), function(hr) {
+ inpt.disabled = false;
+ inpt.value = '';
+
+ var items = byName(hr.responseXML, 'vn');
+ if(items.length < 1) // shouldn't happen
+ return alert('Oops! Error!');
+
+ var id = items[0].getAttribute('id');
+ if(byId('cvn_v'+id))
+ return alert(mt('_chare_vns_exists'));
+ cvnVNAdd(items[0], 1);
+ cvnSerialize();
+ }, 1);
+ return mt('_js_loading');
+}
+
+function cvnSerialize() {
+ var l = byName(byId('vns_tbl'), 'tr');
+ var v = [];
+ for(var i=0; i<l.length; i++)
+ if(l[i].cvn_rid != null) {
+ var rol = byName(byClass(l[i], 'tc_rol')[0], 'select')[0];
+ var spl = byName(byClass(l[i], 'tc_spl')[0], 'select')[0];
+ v.push(l[i].cvn_vid+'-'+l[i].cvn_rid+'-'+
+ spl.options[spl.selectedIndex].value+'-'+
+ rol.options[rol.selectedIndex].value);
+ }
+ byId('vns').value = v.join(' ');
+}
+
+if(byId('jt_box_chare_vns'))
+ cvnLoad();
+
+
+
+
+
/* F I L T E R S Y S T E M */
@@ -2404,6 +2738,30 @@ if(byId('expandall')) {
}
+// charspoil handling (ugly)
+if(byId('charspoil_sel')) {
+ var k = byClass('charspoil');
+ var h = byName(byId('charspoil_sel'), 'a');
+ var setall = function(spoil) {
+ for(var i=0; i<k.length; i++)
+ setClass(k[i], 'hidden',
+ hasClass(k[i], 'charspoil_0') ? false :
+ hasClass(k[i], 'charspoil_1') ? spoil < 1 : spoil < 2);
+ for(var i=0; i<h.length; i++)
+ setClass(h[i], 'sel', spoil == i);
+ };
+ for(var i=0; i<h.length; i++) {
+ h[i].num = i;
+ h[i].onclick = function() {
+ setall(this.num);
+ setCookie('tagspoil', this.num);
+ return false;
+ };
+ };
+ setall(getCookie('tagspoil'));
+}
+
+
// set note input box (/u+/list)
if(byId('not') && byId('vns'))
byId('vns').onchange = function () {
diff --git a/data/style.css b/data/style.css
index f5445c2a..08199de0 100644
--- a/data/style.css
+++ b/data/style.css
@@ -337,6 +337,7 @@ ul.maintabs li a {
border: 1px solid $border$;
border-bottom: none;
background-color: $tabbg$;
+ color: $grayedout$;
}
ul.maintabs.notfirst li a,
ul.maintabs.notfirst li b { margin-top: 20px }
@@ -348,10 +349,10 @@ ul.maintabs li.left b { float: left; margin-left: 0; margin-right: 10
ul.maintabs li b { margin-left: -2px; margin-right: -7px }
ul.maintabs li.left b { margin-left: -7px; margin-right: -2px }
ul.maintabs li.tabselected a,
-ul.maintabs li a:hover { background-color: $_blendbg$; padding-bottom: 6px }
+ul.maintabs li a:hover { background-color: $_blendbg$; color: $maintext$; padding-bottom: 6px }
ul.maintabs.bottom li.tabselected a,
ul.maintabs.bottom li a:hover { padding-bottom: 2px; padding-top: 5px; margin-top: 9px }
-ul.maintabs.browsetabs li a { margin-left: 5px }
+ul.maintabs.browsetabs li a { margin-left: 5px; color: $maintext$ }
ul.maintabs.browsetabs li.left a { margin-left: 0; margin-right: 5px }
@@ -459,7 +460,7 @@ p#searchtabs a:hover, p#searchtabs a.sel {
padding: 1px 5px 2px 5px;
background: $secbg$ url($_boxbg$) repeat;
}
-#q { width: 350px }
+#q { width: 450px }
/* history browser */
div.history table { table-layout: fixed }
@@ -853,6 +854,53 @@ div.scr_uploader { visibility: hidden; overflow: hidden; width: 1px; height: 1px
+/***** Char page (also used on VN page) *****/
+
+div.chardetails { margin: 0 auto; width: 800px; }
+div.charimg { float: left; width: 250px; margin: 0 10px; text-align: center }
+div.charimg p { text-align: center; padding: 0px; margin: 0; }
+.chardesc h2 { margin: 0!important; }
+.chardesc p { padding: 0 0 0 5px; }
+div.chardetails table { float: left; width: 530px; }
+div.chardetails table td.key { width: 80px; }
+div.chardetails.charsep { padding-top: 5px; margin-top: 5px; border-top: 1px solid $border$ }
+#charspoil_sel { clear: right; float: right; }
+#charspoil_sel a { margin: 0 0 0 10px; border: 0; outline: none }
+#maincontent #charspoil_sel a.sel { color: $maintext$; }
+
+
+
+/***** Char edit *****/
+#jt_box_chare_img div.img { float: left; height: 300px; padding-right: 20px; }
+#jt_box_chare_img h2 { margin: 0; }
+#jt_box_chare_traits table { margin-bottom: 10px; margin-left: 10px; }
+#jt_box_chare_traits h2 { margin: 0 0 3px 0px; }
+#jt_box_chare_traits td.tc_name { width: 200px }
+#jt_box_chare_traits td.tc_name input { width: 280px; }
+#jt_box_chare_traits td.tc_spoil { width: 80px; }
+#jt_box_chare_vns table { margin-bottom: 10px; margin-left: 10px; }
+#jt_box_chare_vns h2 { margin: 0 0 3px 0px; }
+#jt_box_chare_vns td.tc_vn { font-weight: bold; padding: 5px 0 3px 0 }
+#jt_box_chare_vns td.tc_vn i{ font-weight: normal; padding-left: 5px; font-style: normal }
+#jt_box_chare_vns td.tc_rel { width: 340px; padding-left: 15px }
+#jt_box_chare_vns td.tc_rel select { width: 340px; }
+#jt_box_chare_vns td.tc_rol,
+#jt_box_chare_vns td.tc_spl,
+#jt_box_chare_vns td.tc_rol select,
+#jt_box_chare_vns td.tc_spl select { width: 100px }
+#jt_box_chare_vns td.tc_del { padding-left: 5px }
+#jt_box_chare_vns td.tc_vnadd input { width: 280px }
+
+/***** Char browse *****/
+
+div.charb table { table-layout: fixed }
+div.charb td { white-space: nowrap }
+div.charb td.tc1 { text-align: right; width: 40px; padding-left: 0!important; padding-bottom: 0 }
+div.charb td.tc2 { overflow: hidden }
+div.charb td.tc2 b { margin-left: 10px }
+div.charb td.tc2 b a { color: $grayedout$!important }
+
+
/***** Documentation pages *****/
.docs { padding: 0 15% 20px 15%; }
@@ -944,7 +992,7 @@ div.uposts td.tc4 b { margin-left: 10px }
.tagvnlist .tc4 { padding: 0; }
.tagvnlist .tc6 { text-align: right; padding-right: 10px; }
-/***** Tag list *****/
+/***** Tag/trait list (/g/list, /i/list) *****/
.browse.taglist .tc1 { width: 80px }
@@ -1136,6 +1184,7 @@ div#fil_div {
width: 16px;
height: 14px;
margin: 0 2px 0 0;
+ margin-top: 0px!important;
overflow: hidden;
display:-moz-inline-stack;
display: inline-block;
@@ -1143,15 +1192,10 @@ div#fil_div {
border: 0;
text-decoration: none;
}
-.icons {
- margin-top: 0px!important;
-}
-.icons.lang {
- width: 13px;
- height: 11px;
- opacity: 0.5;
-}
-.icons.feed { width: 12px; height: 12px }
+.icons.lang { width: 13px; height: 11px; opacity: 0.5; }
+.icons.feed { width: 12px; height: 12px }
+.icons.gen { width: 14px; height: 14px }
+.icons.gen.b { width: 28px }
.icons.rtcomplete, .icons.rtpartial, .icons.rttrial { width: 11px; }
acronym.icons, acronym.uicons { cursor: default; }
a .icons { cursor: pointer }
@@ -1166,6 +1210,10 @@ a .icons { cursor: pointer }
.icons.p98 { background-position: 0px -98px; }
.icons.feed { background-position: 0px -112px; }
+.icons.gen.f,
+.icons.gen.b { background-position: -12px -112px; }
+.icons.gen.m { background-position: -26px -112px; }
+
.icons.dvd { background-position: -16px 0px; }
.icons.mac { background-position: -16px -14px; }
.icons.ps1 { background-position: -16px -28px; }
diff --git a/lib/Multi/Feed.pm b/lib/Multi/Feed.pm
index 12631497..483089bd 100644
--- a/lib/Multi/Feed.pm
+++ b/lib/Multi/Feed.pm
@@ -64,13 +64,14 @@ sub generate {
# changes
$_[KERNEL]->post(pg => query => q{
- SELECT '/'||c.type||COALESCE(vr.vid, rr.rid, pr.pid)||'.'||c.rev AS id,
- COALESCE(vr.title, rr.title, pr.name) AS title, extract('epoch' from c.added) AS updated,
+ SELECT '/'||c.type||COALESCE(vr.vid, rr.rid, pr.pid, cr.cid)||'.'||c.rev AS id,
+ COALESCE(vr.title, rr.title, pr.name, cr.name) AS title, extract('epoch' from c.added) AS updated,
u.username, u.id AS uid, c.comments AS summary
FROM changes c
LEFT JOIN vn_rev vr ON c.type = 'v' AND c.id = vr.id
LEFT JOIN releases_rev rr ON c.type = 'r' AND c.id = rr.id
LEFT JOIN producers_rev pr ON c.type = 'p' AND c.id = pr.id
+ LEFT JOIN chars_rev cr ON c.type = 'c' AND c.id = cr.id
JOIN users u ON u.id = c.requester
WHERE c.requester <> 1
ORDER BY c.id DESC
diff --git a/lib/Multi/IRC.pm b/lib/Multi/IRC.pm
index 2967724c..2cd2e813 100644
--- a/lib/Multi/IRC.pm
+++ b/lib/Multi/IRC.pm
@@ -165,12 +165,14 @@ sub _start {
newrevision => 'notify',
newpost => 'notify',
newtag => 'notify',
+ newtrait => 'notify',
);
$_[HEAP]{notify}{$_[HEAP]{channels}[0]} = 1;
# get last id/time for each notify item
$_[KERNEL]->post(pg => query => q|SELECT
(SELECT id FROM changes ORDER BY id DESC LIMIT 1) AS rev,
(SELECT id FROM tags ORDER BY id DESC LIMIT 1) AS tag,
+ (SELECT id FROM traits ORDER BY id DESC LIMIT 1) AS tag,
(SELECT date FROM threads_posts ORDER BY date DESC LIMIT 1) AS post|,
undef, 'notify_init');
@@ -182,7 +184,7 @@ sub _start {
sub shutdown {
$irc->yield(shutdown => $_[ARG1]);
- $_[KERNEL]->post(pg => unlisten => qw|newrevision newpost newtag|);
+ $_[KERNEL]->post(pg => unlisten => qw|newrevision newpost newtag newtrait|);
$_[KERNEL]->delay('throttle_gc');
$_[KERNEL]->delay('idlequote');
$_[KERNEL]->alias_remove('irc');
@@ -269,6 +271,7 @@ sub notify_init { # num, res
my $r = $_[ARG1][0];
$_[HEAP]{lastrev} = $r->{rev};
$_[HEAP]{lasttag} = $r->{tag};
+ $_[HEAP]{lasttrait} = $r->{trait};
$_[HEAP]{lastpost} = $r->{post};
}
@@ -279,11 +282,12 @@ sub notify { # name, pid, payload
my $q = $_[ARG0] eq 'newrevision' ? q|SELECT
c.type, c.rev, c.comments, c.id AS lastrev,
- COALESCE(vr.vid, rr.rid, pr.pid) AS id, COALESCE(vr.title, rr.title, pr.name) AS title, u.username
+ COALESCE(vr.vid, rr.rid, pr.pid, cr.cid) AS id, COALESCE(vr.title, rr.title, pr.name, cr.name) AS title, u.username
FROM changes c
LEFT JOIN vn_rev vr ON c.type = 'v' AND c.id = vr.id
LEFT JOIN releases_rev rr ON c.type = 'r' AND c.id = rr.id
LEFT JOIN producers_rev pr ON c.type = 'p' AND c.id = pr.id
+ LEFT JOIN chars_rev cr ON c.type = 'c' AND c.id = cr.id
JOIN users u ON u.id = c.requester
WHERE c.id > ? AND c.requester <> 1
ORDER BY c.added|
@@ -294,6 +298,12 @@ sub notify { # name, pid, payload
JOIN users u ON u.id = tp.uid
WHERE tp.date > ? AND tp.num = 1
ORDER BY tp.date|
+ : $_[ARG0] eq 'newtrait' ? q|SELECT
+ 'i' AS type, t.id, t.name AS title, u.username, t.id AS lastrait
+ FROM traits t
+ JOIN users u ON u.id = t.addedby
+ WHERE t.id > ?
+ ORDER BY t.added|
: q|SELECT
'g' AS type, t.id, t.name AS title, u.username, t.id AS lasttag
FROM tags t
@@ -308,9 +318,7 @@ sub notify { # name, pid, payload
sub notify_result { # num, res
return if $_[ARG0] < 1;
my $r = $_[ARG1][$#{$_[ARG1]}];
- $_[HEAP]{lastrev} = $r->{lastrev} if $r->{lastrev};
- $_[HEAP]{lastpost} = $r->{lastpost} if $r->{lastpost};
- $_[HEAP]{lasttag} = $r->{lasttag} if $r->{lasttag};
+ $r->{$_} and ($_[HEAP]{$_} = $r->{$_}) for (qw|lastrev lastpost lasttag lasttrait|);
return if !keys %{$_[HEAP]{notify}};
$_[KERNEL]->yield(formatid => $_[ARG0], $_[ARG1], [ [ keys %{$_[HEAP]{notify}} ], 1 ]);
}
@@ -530,33 +538,36 @@ sub vndbid { # dest, msg
my @id; # [ type, id, ref ]
for (split /[, ]/, $msg) {
next if length > 15 or m{[a-z]{3,6}://}i; # weed out URLs and too long things
- push @id, /^(?:.*[^\w]|)([dvprt])([1-9][0-9]*)\.([1-9][0-9]*)(?:[^\w].*|)$/ ? [ $1, $2, $3 ] # x+.+
- : /^(?:.*[^\w]|)([dvprtug])([1-9][0-9]*)(?:[^\w].*|)$/ ? [ $1, $2, 0 ] : (); # x+
+ push @id, /^(?:.*[^\w]|)([dvprtc])([1-9][0-9]*)\.([1-9][0-9]*)(?:[^\w].*|)$/ ? [ $1, $2, $3 ] # x+.+
+ : /^(?:.*[^\w]|)([dvprtugic])([1-9][0-9]*)(?:[^\w].*|)$/ ? [ $1, $2, 0 ] : (); # x+
}
for (@id) {
my($t, $id, $rev) = @$_;
next if throttle $_[HEAP], "$dest->[0].$t$id.$rev", 60;
- # plain vn/user/producer/thread/tag/release
+ # plain vn/user/producer/thread/tag/trait/release
$_[KERNEL]->post(pg => query => 'SELECT ?::text AS type, ?::integer AS id, '.(
$t eq 'v' ? 'vr.title FROM vn_rev vr JOIN vn v ON v.latest = vr.id WHERE v.id = ?' :
$t eq 'u' ? 'u.username AS title FROM users u WHERE u.id = ?' :
$t eq 'p' ? 'pr.name AS title FROM producers_rev pr JOIN producers p ON p.latest = pr.id WHERE p.id = ?' :
+ $t eq 'c' ? 'cr.name AS title FROM chars_rev cr JOIN chars c ON c.latest = cr.id WHERE c.id = ?' :
$t eq 't' ? 'title, '.GETBOARDS.' FROM threads t WHERE id = ?' :
$t eq 'g' ? 'name AS title FROM tags WHERE id = ?' :
+ $t eq 'i' ? 'name AS title FROM traits WHERE id = ?' :
'rr.title FROM releases_rev rr JOIN releases r ON r.latest = rr.id WHERE r.id = ?'),
[ $t, $id, $id ], 'formatid', [$dest]
- ) if !$rev && $t =~ /[vprtug]/;
+ ) if !$rev && $t =~ /[vprtugic]/;
# edit/insert of vn/release/producer or discussion board post
$_[KERNEL]->post(pg => query => 'SELECT ?::text AS type, ?::integer AS id, ?::integer AS rev, '.(
$t eq 'v' ? 'vr.title, u.username, c.comments FROM changes c JOIN vn_rev vr ON c.id = vr.id JOIN users u ON u.id = c.requester WHERE vr.vid = ? AND c.rev = ?' :
$t eq 'r' ? 'rr.title, u.username, c.comments FROM changes c JOIN releases_rev rr ON c.id = rr.id JOIN users u ON u.id = c.requester WHERE rr.rid = ? AND c.rev = ?' :
$t eq 'p' ? 'pr.name AS title, u.username, c.comments FROM changes c JOIN producers_rev pr ON c.id = pr.id JOIN users u ON u.id = c.requester WHERE pr.pid = ? AND c.rev = ?' :
+ $t eq 'c' ? 'cr.name AS title, u.username, h.comments FROM changes h JOIN chars_rev cr ON h.id = cr.id JOIN users u ON u.id = h.requester WHERE cr.cid = ? AND h.rev = ?' :
't.title, u.username, '.GETBOARDS.' FROM threads t JOIN threads_posts tp ON tp.tid = t.id JOIN users u ON u.id = tp.uid WHERE t.id = ? AND tp.num = ?'),
[ $t, $id, $rev, $id, $rev], 'formatid', [$dest]
- ) if $rev && $t =~ /[vprt]/;
+ ) if $rev && $t =~ /[vprtc]/;
# documentation page (need to parse the doc pages manually here)
if($t eq 'd') {
@@ -596,6 +607,7 @@ sub formatid {
p => 'producer',
r => 'release',
g => 'tag',
+ i => 'trait',
t => 'thread',
);
diff --git a/lib/Multi/Image.pm b/lib/Multi/Image.pm
index ad1d436c..8cd5bfc1 100644
--- a/lib/Multi/Image.pm
+++ b/lib/Multi/Image.pm
@@ -18,10 +18,11 @@ sub spawn {
POE::Session->create(
package_states => [
$p => [qw| _start
- _start shutdown cv_check cv_process scr_check scr_process
+ _start shutdown ch_check ch_process cv_check cv_process scr_check scr_process
|],
],
heap => {
+ chpath => $VNDB::ROOT.'/static/ch',
cvpath => $VNDB::ROOT.'/static/cv',
sfpath => $VNDB::ROOT.'/static/sf',
stpath => $VNDB::ROOT.'/static/st',
@@ -35,20 +36,53 @@ sub spawn {
sub _start {
$_[KERNEL]->alias_set('image');
$_[KERNEL]->sig(shutdown => 'shutdown');
- $_[KERNEL]->post(pg => listen => coverimage => 'cv_check', screenshot => 'scr_check');
+ $_[KERNEL]->post(pg => listen => charimage => 'ch_check', coverimage => 'cv_check', screenshot => 'scr_check');
+ $_[KERNEL]->yield('ch_check');
$_[KERNEL]->yield('cv_check');
$_[KERNEL]->yield('scr_check');
}
sub shutdown {
- $_[KERNEL]->post(pg => unlisten => 'coverimage', 'screenshot');
+ $_[KERNEL]->post(pg => unlisten => 'charimage', 'coverimage', 'screenshot');
+ $_[KERNEL]->delay('ch_check');
$_[KERNEL]->delay('cv_check');
$_[KERNEL]->delay('scr_check');
$_[KERNEL]->alias_remove('image');
}
+sub ch_check {
+ $_[KERNEL]->delay('ch_check');
+ $_[KERNEL]->post(pg => query => 'SELECT image FROM chars_rev WHERE image < 0 LIMIT 1', undef, 'ch_process');
+}
+
+
+sub ch_process { # num, res
+ return $_[KERNEL]->delay(ch_check => $_[HEAP]{check_delay}) if $_[ARG0] == 0;
+
+ my $id = -1*$_[ARG1][0]{image};
+ my $start = time;
+ my $img = sprintf '%s/%02d/%d.jpg', $_[HEAP]{chpath}, $id%100, $id;
+ my $os = -s $img;
+
+ my $im = Image::Magick->new;
+ $im->Read($img);
+ $im->Set(magick => 'JPEG');
+ my($ow, $oh) = ($im->Get('width'), $im->Get('height'));
+ my($nw, $nh) = imgsize($ow, $oh, @{$VNDB::S{ch_size}});
+ $im->Thumbnail(width => $nw, height => $nh);
+ $im->Set(quality => 90);
+ $im->Write($img);
+
+ $_[KERNEL]->post(pg => do => 'UPDATE chars_rev SET image = image*-1 WHERE image = ?', [ -1*$id ]);
+ $_[KERNEL]->call(core => log => 'Processed character image %d in %.2fs: %.2fkB (%dx%d) -> %.2fkB (%dx%d)',
+ $id, time-$start, $os/1024, $ow, $oh, (-s $img)/1024, $nw, $nh);
+
+ $_[KERNEL]->yield('ch_check');
+}
+
+
sub cv_check {
$_[KERNEL]->delay('cv_check');
$_[KERNEL]->post(pg => query => 'SELECT image FROM vn_rev WHERE image < 0 LIMIT 1', undef, 'cv_process');
diff --git a/lib/VNDB/DB/Chars.pm b/lib/VNDB/DB/Chars.pm
new file mode 100644
index 00000000..8fb01ebf
--- /dev/null
+++ b/lib/VNDB/DB/Chars.pm
@@ -0,0 +1,125 @@
+
+package VNDB::DB::Chars;
+
+use strict;
+use warnings;
+use Exporter 'import';
+
+our @EXPORT = qw|dbCharGet dbCharRevisionInsert dbCharImageId|;
+
+
+# options: id rev instance traitspoil trait_inc trait_exc char what results page
+# what: extended traits vns changes
+sub dbCharGet {
+ my $self = shift;
+ my %o = (
+ page => 1,
+ results => 10,
+ what => '',
+ traitspoil => 0,
+ @_
+ );
+
+ $o{search} =~ s/%//g if $o{search};
+
+ my %where = (
+ !$o{id} && !$o{rev} ? ( 'c.hidden = FALSE' => 1 ) : (),
+ $o{id} ? ( 'c.id = ?' => $o{id} ) : (),
+ $o{rev} ? ( 'h.rev = ?' => $o{rev} ) : (),
+ $o{notid} ? ( 'c.id <> ?' => $o{notid} ) : (),
+ $o{instance} ? ( 'cr.main = ?' => $o{instance} ) : (),
+ $o{vid} ? ( 'cr.id IN(SELECT cid FROM chars_vns WHERE vid = ?)' => $o{vid} ) : (),
+ $o{search} ? (
+ '(cr.name ILIKE ? OR cr.original ILIKE ? OR cr.alias ILIKE ?)', [ map '%%'.$o{search}.'%%', 1..3 ] ) : (),
+ $o{char} ? (
+ 'LOWER(SUBSTR(cr.name, 1, 1)) = ?' => $o{char} ) : (),
+ defined $o{char} && !$o{char} ? (
+ '(ASCII(cr.name) < 97 OR ASCII(cr.name) > 122) AND (ASCII(cr.name) < 65 OR ASCII(cr.name) > 90)' => 1 ) : (),
+ $o{trait_inc} ? (
+ 'c.id IN(SELECT cid FROM traits_chars WHERE tid IN(!l) AND spoil <= ? GROUP BY cid HAVING COUNT(tid) = ?)',
+ [ ref $o{trait_inc} ? $o{trait_inc} : [$o{trait_inc}], $o{traitspoil}, ref $o{trait_inc} ? $#{$o{trait_inc}}+1 : 1 ]) : (),
+ $o{trait_exc} ? (
+ 'c.id NOT IN(SELECT cid FROM traits_chars WHERE tid IN(!l))' => [ ref $o{trait_exc} ? $o{trait_exc} : [$o{trait_exc}] ] ) : (),
+ );
+
+ my @select = (qw|c.id cr.name cr.original cr.gender|, 'cr.id AS cid');
+ push @select, qw|c.hidden c.locked cr.alias cr.desc cr.image cr.b_month cr.b_day cr.s_bust cr.s_waist cr.s_hip cr.height cr.weight cr.bloodt cr.main cr.main_spoil| if $o{what} =~ /extended/;
+ push @select, qw|h.requester h.comments c.latest u.username h.rev h.ihid h.ilock|, "extract('epoch' from h.added) as added" if $o{what} =~ /changes/;
+
+ my @join;
+ push @join, $o{rev} ? 'JOIN chars c ON c.id = cr.cid' : 'JOIN chars c ON cr.id = c.latest';
+ push @join, 'JOIN changes h ON h.id = cr.id' if $o{what} =~ /changes/ || $o{rev};
+ push @join, 'JOIN users u ON u.id = h.requester' if $o{what} =~ /changes/;
+
+ my($r, $np) = $self->dbPage(\%o, q|
+ SELECT !s
+ FROM chars_rev cr
+ !s
+ !W
+ ORDER BY cr.name|,
+ join(', ', @select), join(' ', @join), \%where
+ );
+
+ if(@$r && $o{what} =~ /(vns|traits)/) {
+ my %r = map {
+ $_->{traits} = [];
+ $_->{vns} = [];
+ ($_->{cid}, $_)
+ } @$r;
+
+ if($o{what} =~ /traits/) {
+ push @{$r{ delete $_->{cid} }{traits}}, $_ for (@{$self->dbAll(q|
+ SELECT ct.cid, ct.tid, ct.spoil, t.name, t."group", tg.name AS groupname
+ FROM chars_traits ct
+ JOIN traits t ON t.id = ct.tid
+ LEFT JOIN traits tg ON tg.id = t."group"
+ WHERE cid IN(!l)
+ ORDER BY tg."order", t.name|, [ keys %r ]
+ )});
+ }
+
+ if($o{what} =~ /vns(?:\((\d+)\))?/) {
+ push @{$r{ delete $_->{cid} }{vns}}, $_ for (@{$self->dbAll(q|
+ SELECT cv.cid, cv.vid, cv.rid, cv.spoil, cv.role, vr.title AS vntitle, rr.title AS rtitle
+ FROM chars_vns cv
+ JOIN vn v ON cv.vid = v.id
+ JOIN vn_rev vr ON vr.id = v.latest
+ LEFT JOIN releases r ON cv.rid = r.id
+ LEFT JOIN releases_rev rr ON rr.id = r.latest
+ !W|, { 'cv.cid IN(!l)' => [[keys %r]], $1 ? ('cv.vid = ?', $1) : () }
+ )});
+ }
+ }
+ return wantarray ? ($r, $np) : $r;
+}
+
+
+# Updates the edit_* tables, used from dbItemEdit()
+# Arguments: { columns in chars_rev + traits + vns },
+sub dbCharRevisionInsert {
+ my($self, $o) = @_;
+
+ my %set = map exists($o->{$_}) ? (qq|"$_" = ?|, $o->{$_}) : (),
+ qw|name original alias desc image b_month b_day s_bust s_waist s_hip height weight bloodt gender main main_spoil|;
+ $self->dbExec('UPDATE edit_char !H', \%set) if keys %set;
+
+ if($o->{traits}) {
+ $self->dbExec('DELETE FROM edit_char_traits');
+ $self->dbExec('INSERT INTO edit_char_traits (tid, spoil) VALUES (?,?)', $_->[0],$_->[1]) for (@{$o->{traits}});
+ }
+ if($o->{vns}) {
+ $self->dbExec('DELETE FROM edit_char_vns');
+ $self->dbExec('INSERT INTO edit_char_vns (vid, rid, spoil, role) VALUES(!l)', $_) for (@{$o->{vns}});
+ }
+}
+
+
+# fetches an ID for a new image
+sub dbCharImageId {
+ return shift->dbRow("SELECT nextval('charimg_seq') AS ni")->{ni};
+}
+
+
+
+1;
+
diff --git a/lib/VNDB/DB/Misc.pm b/lib/VNDB/DB/Misc.pm
index f9eb459a..4b31ebb4 100644
--- a/lib/VNDB/DB/Misc.pm
+++ b/lib/VNDB/DB/Misc.pm
@@ -27,7 +27,7 @@ sub dbStats {
sub dbItemEdit {
my($self, $type, $oid, %o) = @_;
- my $fun = {qw|v vn r release p producer|}->{$type};
+ my $fun = {qw|v vn r release p producer c char|}->{$type};
$self->dbExec('SELECT edit_!s_init(?)', $fun, $oid);
$self->dbExec('UPDATE edit_revision !H', {
'requester = ?' => $o{uid}||$self->authInfo->{id},
@@ -40,6 +40,7 @@ sub dbItemEdit {
$self->dbVNRevisionInsert( \%o) if $type eq 'v';
$self->dbProducerRevisionInsert(\%o) if $type eq 'p';
$self->dbReleaseRevisionInsert( \%o) if $type eq 'r';
+ $self->dbCharRevisionInsert( \%o) if $type eq 'c';
return $self->dbRow('SELECT * FROM edit_!s_commit()', $fun);
}
@@ -59,10 +60,10 @@ sub dbRevisionGet {
$o{what} ||= '';
$o{releases} = 0 if !$o{type} || $o{type} ne 'v' || !$o{iid};
- my %tables = qw|v vn r releases p producers|;
+ my %tables = qw|v vn r releases p producers c chars|;
# what types should we join?
my @types = (
- !$o{type} ? ('v', 'r', 'p') :
+ !$o{type} ? ('v', 'r', 'p', 'c') :
$o{type} ne 'v' ? $o{type} :
$o{releases} ? ('v', 'r') : 'v'
);
@@ -102,7 +103,7 @@ sub dbRevisionGet {
$o{what} =~ /user/ ? 'u.username' : (),
$o{what} =~ /item/ ? (
'COALESCE('.join(', ', map "${_}r.${_}id", @types).') AS iid',
- 'COALESCE('.join(', ', map $_ eq 'p' ? 'pr.name' : "${_}r.title", @types).') AS ititle',
+ 'COALESCE('.join(', ', map /[pc]/ ? "${_}r.name" : "${_}r.title", @types).') AS ititle',
'COALESCE('.join(', ', map "${_}r.original", @types).') AS ioriginal',
) : (),
);
diff --git a/lib/VNDB/DB/Releases.pm b/lib/VNDB/DB/Releases.pm
index 87685ce6..bd2d5bd3 100644
--- a/lib/VNDB/DB/Releases.pm
+++ b/lib/VNDB/DB/Releases.pm
@@ -24,7 +24,7 @@ sub dbReleaseGet {
!$o{id} && !$o{rev} ? ( 'r.hidden = FALSE' => 0 ) : (),
$o{id} ? ( 'r.id = ?' => $o{id} ) : (),
$o{rev} ? ( 'c.rev = ?' => $o{rev} ) : (),
- $o{vid} ? ( 'rv.vid = ?' => $o{vid} ) : (),
+ $o{vid} ? ( 'rv.vid IN(!l)' => [ ref $o{vid} ? $o{vid} : [$o{vid}] ] ) : (),
$o{pid} ? ( 'rp.pid = ?' => $o{pid} ) : (),
defined $o{patch} ? ( 'rr.patch = ?' => $o{patch} == 1 ? 1 : 0) : (),
defined $o{freeware} ? ( 'rr.freeware = ?' => $o{freeware} == 1 ? 1 : 0) : (),
diff --git a/lib/VNDB/DB/Tags.pm b/lib/VNDB/DB/Tags.pm
index 8ed4cec6..eee73f10 100644
--- a/lib/VNDB/DB/Tags.pm
+++ b/lib/VNDB/DB/Tags.pm
@@ -5,12 +5,12 @@ use strict;
use warnings;
use Exporter 'import';
-our @EXPORT = qw|dbTagGet dbTagTree dbTagEdit dbTagAdd dbTagMerge dbTagLinks dbTagLinkEdit dbTagStats|;
+our @EXPORT = qw|dbTagGet dbTTTree dbTagEdit dbTagAdd dbTagMerge dbTagLinks dbTagLinkEdit dbTagStats|;
# %options->{ id noid name search state meta page results what sort reverse }
# what: parents childs(n) aliases addedby
-# sort: id name added vns
+# sort: id name added items
sub dbTagGet {
my $self = shift;
my %o = (
@@ -39,7 +39,7 @@ sub dbTagGet {
't.meta = ?' => $o{meta}?1:0 ) : (),
);
my @select = (
- qw|t.id t.meta t.name t.description t.state t.cat t.c_vns|,
+ qw|t.id t.meta t.name t.description t.state t.cat t.c_items|,
q|extract('epoch' from t.added) as added|,
$o{what} =~ /addedby/ ? ('t.addedby', 'u.username') : (),
);
@@ -49,7 +49,7 @@ sub dbTagGet {
id => 't.id %s',
name => 't.name %s',
added => 't.added %s',
- vns => 't.c_vns %s',
+ items => 't.c_items %s',
}->{ $o{sort}||'id' }, $o{reverse} ? 'DESC' : 'ASC';
my($r, $np) = $self->dbPage(\%o, q|
@@ -73,40 +73,41 @@ sub dbTagGet {
}
if($o{what} =~ /parents\((\d+)\)/) {
- $_->{parents} = $self->dbTagTree($_->{id}, $1, 1) for(@$r);
+ $_->{parents} = $self->dbTTTree(tag => $_->{id}, $1, 1) for(@$r);
}
if($o{what} =~ /childs\((\d+)\)/) {
- $_->{childs} = $self->dbTagTree($_->{id}, $1) for(@$r);
+ $_->{childs} = $self->dbTTTree(tag => $_->{id}, $1) for(@$r);
}
return wantarray ? ($r, $np) : $r;
}
-# Walks the tag tree
+# Walks the tag/trait tree
+# type = tag | trait
# id = tag to start with, or 0 to start with top-level tags
# lvl = max. recursion level
# back = false for parent->child, true for child->parent
-# Returns: [ { id, name, c_vns, sub => [ { id, name, c_vns, sub => [..] }, .. ] }, .. ]
-sub dbTagTree {
- my($self, $id, $lvl, $back) = @_;
+# Returns: [ { id, name, c_items, sub => [ { id, name, c_items, sub => [..] }, .. ] }, .. ]
+sub dbTTTree {
+ my($self, $type, $id, $lvl, $back) = @_;
$lvl ||= 15;
- my $r = $self->dbAll(q|
- WITH RECURSIVE tagtree(lvl, id, parent, name, c_vns) AS (
- SELECT ?::integer, id, 0, name, c_vns
- FROM tags
+ my $r = $self->dbAll(qq|
+ WITH RECURSIVE thetree(lvl, id, parent, name, c_items) AS (
+ SELECT ?::integer, id, 0, name, c_items
+ FROM ${type}s
!W
UNION ALL
- SELECT tt.lvl-1, t.id, tt.id, t.name, t.c_vns
- FROM tagtree tt
- JOIN tags_parents tp ON !s
- JOIN tags t ON !s
+ SELECT tt.lvl-1, t.id, tt.id, t.name, t.c_items
+ FROM thetree tt
+ JOIN ${type}s_parents tp ON !s
+ JOIN ${type}s t ON !s
WHERE tt.lvl > 0
AND t.state = 2
- ) SELECT DISTINCT id, parent, name, c_vns FROM tagtree ORDER BY name|, $lvl,
- $id ? {'id = ?' => $id} : {'NOT EXISTS(SELECT 1 FROM tags_parents WHERE tag = id)' => 1, 'state = 2' => 1},
- !$back ? ('tp.parent = tt.id', 't.id = tp.tag') : ('tp.tag = tt.id', 't.id = tp.parent')
+ ) SELECT DISTINCT id, parent, name, c_items FROM thetree ORDER BY name|, $lvl,
+ $id ? {'id = ?' => $id} : {"NOT EXISTS(SELECT 1 FROM ${type}s_parents WHERE $type = id)" => 1, 'state = 2' => 1},
+ !$back ? ('tp.parent = tt.id', "t.id = tp.$type") : ("tp.$type = tt.id", 't.id = tp.parent')
);
for my $i (@$r) {
$i->{'sub'} = [ grep $_->{parent} == $i->{id}, @$r ];
diff --git a/lib/VNDB/DB/Traits.pm b/lib/VNDB/DB/Traits.pm
new file mode 100644
index 00000000..15779391
--- /dev/null
+++ b/lib/VNDB/DB/Traits.pm
@@ -0,0 +1,105 @@
+
+package VNDB::DB::Traits;
+
+# This module is for a large part a copy of VNDB::DB::Tags. I could have chosen
+# to modify that module to work for both traits and tags but that would have
+# complicated the code, so I chose to maintain two versions with similar
+# functionality instead.
+
+use strict;
+use warnings;
+use Exporter 'import';
+
+our @EXPORT = qw|dbTraitGet dbTraitEdit dbTraitAdd|;
+
+
+# Options: id what results page sort reverse
+# what: parents childs(n) addedby
+# sort: id name name added items
+sub dbTraitGet {
+ my $self = shift;
+ my %o = (
+ page => 1,
+ results => 10,
+ what => '',
+ @_,
+ );
+
+ $o{search} =~ s/%//g if $o{search};
+
+ my %where = (
+ $o{id} ? ('t.id IN(!l)' => [ ref($o{id}) ? $o{id} : [$o{id}] ]) : (),
+ defined $o{state} && $o{state} != -1 ? (
+ 't.state = ?' => $o{state} ) : (),
+ !defined $o{state} && !$o{id} && !$o{name} ? (
+ 't.state = 2' => 1 ) : (),
+ $o{search} ? (
+ 't.name ILIKE ? OR t.alias ILIKE ?' => [ "%$o{search}%", "%$o{search}%" ] ) : (),
+ );
+
+ my @select = (
+ qw|t.id t.meta t.name t.description t.state t.alias t."group" t."order" t.sexual t.c_items|,
+ 'tg.name AS groupname', 'tg."order" AS grouporder', q|extract('epoch' from t.added) as added|,
+ $o{what} =~ /addedby/ ? ('t.addedby', 'u.username') : (),
+ );
+ my @join = $o{what} =~ /addedby/ ? 'JOIN users u ON u.id = t.addedby' : ();
+ push @join, 'LEFT JOIN traits tg ON tg.id = t."group"';
+
+ my $order = sprintf {
+ id => 't.id %s',
+ name => 't.name %s',
+ group => 'tg."order" %s, t.name %1$s',
+ added => 't.added %s',
+ items => 't.c_items %s',
+ }->{ $o{sort}||'id' }, $o{reverse} ? 'DESC' : 'ASC';
+
+ my($r, $np) = $self->dbPage(\%o, q|
+ SELECT !s
+ FROM traits t
+ !s
+ !W
+ ORDER BY !s|,
+ join(', ', @select), join(' ', @join), \%where, $order
+ );
+
+ if($o{what} =~ /parents\((\d+)\)/) {
+ $_->{parents} = $self->dbTTTree(trait => $_->{id}, $1, 1) for(@$r);
+ }
+
+ if($o{what} =~ /childs\((\d+)\)/) {
+ $_->{childs} = $self->dbTTTree(trait => $_->{id}, $1) for(@$r);
+ }
+
+ return wantarray ? ($r, $np) : $r;
+}
+
+
+# args: trait id, %options->{ columns in the traits table + parents }
+sub dbTraitEdit {
+ my($self, $id, %o) = @_;
+
+ $self->dbExec('UPDATE traits !H WHERE id = ?', {
+ $o{upddate} ? ('added = NOW()' => 1) : (),
+ map exists($o{$_}) ? ("\"$_\" = ?" => $o{$_}) : (), qw|name meta description state alias group order sexual|
+ }, $id);
+ if($o{parents}) {
+ $self->dbExec('DELETE FROM traits_parents WHERE trait = ?', $id);
+ $self->dbExec('INSERT INTO traits_parents (trait, parent) VALUES (?, ?)', $id, $_) for(@{$o{parents}});
+ }
+}
+
+
+# same args as dbTraitEdit, without the first trait id
+# returns the id of the new trait
+sub dbTraitAdd {
+ my($self, %o) = @_;
+ my $id = $self->dbRow('INSERT INTO traits (name, meta, description, state, alias, "group", "order", sexual, addedby) VALUES (!l, ?) RETURNING id',
+ [ map $o{$_}, qw|name meta description state alias group order sexual| ], $o{addedby}||$self->authInfo->{id}
+ )->{id};
+ $self->dbExec('INSERT INTO traits_parents (trait, parent) VALUES (?, ?)', $id, $_) for(@{$o{parents}});
+ return $id;
+}
+
+
+1;
+
diff --git a/lib/VNDB/DB/VN.pm b/lib/VNDB/DB/VN.pm
index 0f6c5731..474066ce 100644
--- a/lib/VNDB/DB/VN.pm
+++ b/lib/VNDB/DB/VN.pm
@@ -7,7 +7,7 @@ use Exporter 'import';
use VNDB::Func 'gtintype', 'normalize_query';
use Encode 'decode_utf8';
-our @EXPORT = qw|dbVNGet dbVNRevisionInsert dbVNImageId dbScreenshotAdd dbScreenshotGet dbScreenshotRandom|;
+our @EXPORT = qw|dbVNGet dbVNRevisionInsert dbVNImageId dbScreenshotAdd dbScreenshotGet dbScreenshotRandom dbVNHasChar|;
# Options: id, rev, char, search, length, lang, olang, plat, tag_inc, tag_exc, tagspoil,
@@ -269,5 +269,12 @@ sub dbScreenshotRandom {
}
+sub dbVNHasChar {
+ my($self, $vid) = @_;
+ return $self->dbRow(
+ 'SELECT 1 AS exists FROM chars c JOIN chars_vns cv ON c.latest = cv.cid WHERE cv.vid = ?', $vid
+ )->{exists};
+}
+
1;
diff --git a/lib/VNDB/Func.pm b/lib/VNDB/Func.pm
index 435f0fec..64b56625 100644
--- a/lib/VNDB/Func.pm
+++ b/lib/VNDB/Func.pm
@@ -7,7 +7,7 @@ use TUWF ':html';
use Exporter 'import';
use POSIX 'strftime', 'ceil', 'floor';
use VNDBUtil;
-our @EXPORT = (@VNDBUtil::EXPORT, qw| clearfloat cssicon tagscore mt minage fil_parse fil_serialize |);
+our @EXPORT = (@VNDBUtil::EXPORT, qw| clearfloat cssicon tagscore mt minage fil_parse fil_serialize parenttags childtags charspoil |);
# three ways to represent the same information
@@ -105,5 +105,80 @@ sub fil_serialize {
} grep defined($fil->{$_}), keys %$fil;
}
+
+# generates a parent tags/traits listing
+sub parenttags {
+ my($t, $index, $type) = @_;
+ p;
+ my @p = _parenttags(@{$t->{parents}});
+ for my $p (@p ? @p : []) {
+ a href => "/$type", $index; #mt '_tagp_indexlink';
+ for (reverse @$p) {
+ txt ' > ';
+ a href => "/$type$_->{id}", $_->{name};
+ }
+ txt " > $t->{name}";
+ br;
+ }
+ end 'p';
+}
+
+# arg: tag/trait hashref
+# returns: [ [ tag1, tag2, tag3 ], [ tag1, tag2, tag5 ] ]
+sub _parenttags {
+ my @r;
+ for my $t (@_) {
+ for (@{$t->{'sub'}}) {
+ push @r, [ $t, @$_ ] for _parenttags($_);
+ }
+ push @r, [$t] if !@{$t->{'sub'}};
+ }
+ return @r;
+}
+
+
+# a child tags/traits box
+sub childtags {
+ my($self, $title, $type, $t) = @_;
+
+ div class => 'mainbox';
+ h1 $title;
+ ul class => 'tagtree';
+ for my $p (sort { @{$b->{'sub'}} <=> @{$a->{'sub'}} } @{$t->{childs}}) {
+ li;
+ a href => "/$type$p->{id}", $p->{name};
+ b class => 'grayedout', " ($p->{c_items})" if $p->{c_items};
+ end, next if !@{$p->{'sub'}};
+ ul;
+ for (0..$#{$p->{'sub'}}) {
+ last if $_ >= 5 && @{$p->{'sub'}} > 6;
+ li;
+ txt '> ';
+ a href => "/$type$p->{sub}[$_]{id}", $p->{'sub'}[$_]{name};
+ b class => 'grayedout', " ($p->{sub}[$_]{c_items})" if $p->{'sub'}[$_]{c_items};
+ end;
+ }
+ if(@{$p->{'sub'}} > 6) {
+ li;
+ txt '> ';
+ a href => "/$type$p->{id}", style => 'font-style: italic', mt $type eq 'g' ? '_tagp_moretags' : '_traitp_more', @{$p->{'sub'}}-5;
+ end;
+ }
+ end;
+ end 'li';
+ }
+ end 'ul';
+ clearfloat;
+ br;
+ end 'div';
+}
+
+
+# generates the class elements for character spoiler hiding
+sub charspoil {
+ return "charspoil charspoil_$_[0]".($_[0] ? ' hidden' : '');
+}
+
+
1;
diff --git a/lib/VNDB/Handler/Chars.pm b/lib/VNDB/Handler/Chars.pm
new file mode 100644
index 00000000..ca9cf105
--- /dev/null
+++ b/lib/VNDB/Handler/Chars.pm
@@ -0,0 +1,514 @@
+
+package VNDB::Handler::Chars;
+
+use strict;
+use warnings;
+use TUWF ':html', 'uri_escape';
+use Exporter 'import';
+use VNDB::Func;
+
+our @EXPORT = ('charTable');
+
+TUWF::register(
+ qr{c([1-9]\d*)(?:\.([1-9]\d*))?} => \&page,
+ qr{c(?:([1-9]\d*)(?:\.([1-9]\d*))?/(edit|copy)|/new)}
+ => \&edit,
+ qr{c/([a-z0]|all)} => \&list,
+);
+
+
+sub page {
+ my($self, $id, $rev) = @_;
+
+ my $r = $self->dbCharGet(
+ id => $id,
+ what => 'extended traits vns'.($rev ? ' changes' : ''),
+ $rev ? ( rev => $rev ) : ()
+ )->[0];
+ return $self->resNotFound if !$r->{id};
+
+ $self->htmlHeader(title => $r->{name});
+ $self->htmlMainTabs(c => $r);
+ return if $self->htmlHiddenMessage('c', $r);
+
+ if($rev) {
+ my $prev = $rev && $rev > 1 && $self->dbCharGet(id => $id, rev => $rev-1, what => 'changes extended traits vns')->[0];
+ $self->htmlRevision('c', $prev, $r,
+ [ name => diff => 1 ],
+ [ original => diff => 1 ],
+ [ alias => diff => qr/[ ,\n\.]/ ],
+ [ desc => diff => qr/[ ,\n\.]/ ],
+ [ gender => serialize => sub { mt "_gender_$_[0]" } ],
+ [ b_month => serialize => sub { $_[0]||mt '_revision_empty' } ],
+ [ b_day => serialize => sub { $_[0]||mt '_revision_empty' } ],
+ [ s_bust => serialize => sub { $_[0]||mt '_revision_empty' } ],
+ [ s_waist => serialize => sub { $_[0]||mt '_revision_empty' } ],
+ [ s_hip => serialize => sub { $_[0]||mt '_revision_empty' } ],
+ [ height => serialize => sub { $_[0]||mt '_revision_empty' } ],
+ [ weight => serialize => sub { $_[0]||mt '_revision_empty' } ],
+ [ bloodt => serialize => sub { mt "_bloodt_$_[0]" } ],
+ [ main => htmlize => sub { $_[0] ? sprintf '<a href="/c%d">c%d</a>', $_[0], $_[0] : mt '_revision_empty' } ],
+ [ main_spoil=> serialize => sub { mt "_spoil_$_[0]" } ],
+ [ image => htmlize => sub {
+ return $_[0] > 0 ? sprintf '<img src="%s/ch/%02d/%d.jpg" />', $self->{url_static}, $_[0]%100, $_[0]
+ : mt $_[0] < 0 ? '_chdiff_image_proc' : '_chdiff_image_none';
+ }],
+ [ traits => join => '<br />', split => sub {
+ map sprintf('%s<a href="/i%d">%s</a> (%s)', $_->{group}?qq|<b class="grayedout">$_->{groupname} / </b> |:'',
+ $_->{tid}, $_->{name}, mt("_spoil_$_->{spoil}")), @{$_[0]}
+ }],
+ [ vns => join => '<br />', split => sub {
+ map sprintf('<a href="/v%d">v%d</a> %s %s (%s)', $_->{vid}, $_->{vid},
+ $_->{rid}?sprintf('[<a href="/r%d">r%d</a>]', $_->{rid}, $_->{rid}):'',
+ mt("_charrole_$_->{role}"), mt("_spoil_$_->{spoil}")), @{$_[0]};
+ }],
+ );
+ }
+
+ div class => 'mainbox';
+ $self->htmlItemMessage('c', $r);
+ p id => 'charspoil_sel';
+ a href => '#', class => 'sel', mt '_vnpage_tags_spoil0'; # _vnpage!?
+ a href => '#', mt '_vnpage_tags_spoil1';
+ a href => '#', mt '_vnpage_tags_spoil2';
+ end;
+ h1 $r->{name};
+ h2 class => 'alttitle', $r->{original} if $r->{original};
+ $self->charTable($r);
+ end;
+
+ # TODO: ordering of these instances?
+ my $inst = [];
+ if(!$r->{main}) {
+ $inst = $self->dbCharGet(instance => $r->{id}, what => 'extended traits vns');
+ } else {
+ $inst = $self->dbCharGet(instance => $r->{main}, notid => $r->{id}, what => 'extended traits vns');
+ push @$inst, $self->dbCharGet(id => $r->{main}, what => 'extended traits vns')->[0];
+ }
+ if(@$inst) {
+ div class => 'mainbox';
+ h1 mt '_charp_instances';
+ $self->charTable($_, 1, $_ != $inst->[0], 0, !$r->{main} ? $_->{main_spoil} : $_->{main_spoil} > $r->{main_spoil} ? $_->{main_spoil} : $r->{main_spoil}) for @$inst;
+ end;
+ }
+
+ $self->htmlFooter;
+}
+
+
+# Also used from Handler::VNPage
+sub charTable {
+ my($self, $r, $link, $sep, $vn, $spoil) = @_;
+ $spoil ||= 0;
+
+ div class => 'chardetails '.charspoil($spoil).($sep ? ' charsep' : '');
+
+ # image
+ div class => 'charimg';
+ if(!$r->{image}) {
+ p mt '_charp_noimg';
+ } elsif($r->{image} < 0) {
+ p mt '_charp_imgproc';
+ } else {
+ img src => sprintf('%s/ch/%02d/%d.jpg', $self->{url_static}, $r->{image}%100, $r->{image}),
+ alt => $r->{name} if $r->{image};
+ }
+ end 'div';
+
+ # info table
+ table;
+ Tr;
+ td colspan => 2;
+ if($link) {
+ a href => "/c$r->{id}", style => 'margin-right: 10px; font-weight: bold', $r->{name};
+ } else {
+ b style => 'margin-right: 10px', $r->{name};
+ }
+ b class => 'grayedout', style => 'margin-right: 10px', $r->{original} if $r->{original};
+ cssicon "gen $r->{gender}", mt "_gender_$r->{gender}" if $r->{gender} ne 'unknown';
+ span mt "_bloodt_$r->{bloodt}" if $r->{bloodt} ne 'unknown';
+ end;
+ end;
+ my $i = 0;
+ if($r->{alias}) {
+ $r->{alias} =~ s/\n/, /g;
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td class => 'key', mt '_charp_alias';
+ td $r->{alias};
+ end;
+ }
+ if($r->{height} || $r->{s_bust} || $r->{s_waist} || $r->{s_hip}) {
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td class => 'key', mt '_charp_meas';
+ td join ', ',
+ $r->{height} ? mt('_charp_meas_h', $r->{height}) : (),
+ $r->{weight} ? mt('_charp_meas_w', $r->{weight}) : (),
+ $r->{s_bust} || $r->{s_waist} || $r->{s_hip} ? mt('_charp_meas_bwh', $r->{s_bust}||'??', $r->{s_waist}||'??', $r->{s_hip}||'??') : ();
+ end;
+ }
+ if($r->{b_month} && $r->{b_day}) {
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td class => 'key', mt '_charp_bday';
+ td mt '_charp_bday_fmt', $r->{b_day}, mt "_month_$r->{b_month}";
+ end;
+ }
+
+ # traits
+ # TODO: handle 'sexual' traits
+ my %groups;
+ my @groups;
+ for (@{$r->{traits}}) {
+ my $g = $_->{group}||$_->{tid};
+ push @groups, $g if !$groups{$g};
+ push @{$groups{ $g }}, $_
+ }
+ for my $g (@groups) {
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td class => 'key'; a href => '/i'.($groups{$g}[0]{group}||$groups{$g}[0]{tid}), $groups{$g}[0]{groupname} || $groups{$g}[0]{name}; end;
+ td;
+ for (@{$groups{$g}}) {
+ span class => charspoil $_->{spoil};
+ txt ', ' if $_->{tid} != $groups{$g}[0]{tid};
+ a href => "/i$_->{tid}", $_->{name};
+ end;
+ }
+ end;
+ end;
+ }
+
+ # vns
+ if(@{$r->{vns}} && (!$vn || $vn && (@{$r->{vns}} > 1 || $r->{vns}[0]{rid}))) {
+ my %vns;
+ push @{$vns{$_->{vid}}}, $_ for(sort { !defined($a->{rid})?1:!defined($b->{rid})?-1:$a->{rtitle} cmp $b->{rtitle} } @{$r->{vns}});
+ Tr ++$i % 2 ? (class => 'odd') : ();
+ td class => 'key', mt $vn ? '_charp_releases' : '_charp_vns';
+ td;
+ my $first = 0;
+ for my $g (sort { $vns{$a}[0]{vntitle} cmp $vns{$b}[0]{vntitle} } keys %vns) {
+ br if $first++;
+ my @r = @{$vns{$g}};
+ # special case: all releases, no exceptions
+ if(!$vn && @r == 1 && !$r[0]{rid}) {
+ span class => charspoil $r[0]{spoil};
+ txt mt("_charrole_$r[0]{role}").' - ';
+ a href => "/v$r[0]{vid}/chars", $r[0]{vntitle};
+ end;
+ next;
+ }
+ # otherwise, print VN title and list releases separately
+ my $minspoil = 5;
+ $minspoil = $minspoil > $_->{spoil} ? $_->{spoil} : $minspoil for (@r);
+ span class => charspoil $minspoil;
+ a href => "/v$r[0]{vid}/chars", $r[0]{vntitle} if !$vn;
+ for(@r) {
+ span class => charspoil $_->{spoil};
+ br if !$vn || $_ != $r[0];
+ b class => 'grayedout', '> ';
+ txt mt("_charrole_$_->{role}").' - ';
+ if($_->{rid}) {
+ b class => 'grayedout', "r$_->{rid}:";
+ a href => "/r$_->{rid}", $_->{rtitle};
+ } else {
+ txt mt '_charp_vns_other';
+ }
+ end;
+ }
+ end;
+ }
+ end;
+ end;
+ }
+
+ # description
+ if($r->{desc}) {
+ Tr;
+ td class => 'chardesc', colspan => 2;
+ h2 mt '_charp_description';
+ p;
+ lit bb2html $r->{desc};
+ end;
+ end;
+ end;
+ }
+
+ end 'table';
+ end;
+ clearfloat;
+}
+
+
+
+sub edit {
+ my($self, $id, $rev, $copy) = @_;
+
+ my $copy = $rev && $rev eq 'copy' || $copy && $copy eq 'copy';
+ $rev = undef if defined $rev && $rev !~ /^\d+$/;
+
+ my $r = $id && $self->dbCharGet(id => $id, what => 'changes extended vns traits', $rev ? (rev => $rev) : ())->[0];
+ return $self->resNotFound if $id && !$r->{id};
+ $rev = undef if !$r || $r->{cid} == $r->{latest};
+
+ return $self->htmlDenied if !$self->authCan('charedit')
+ || $id && ($r->{locked} && !$self->authCan('lock') || $r->{hidden} && !$self->authCan('del'));
+
+ my %b4 = !$id ? () : (
+ (map +($_ => $r->{$_}), qw|name original alias desc image ihid ilock s_bust s_waist s_hip height weight bloodt gender main_spoil|),
+ main => $r->{main}||0,
+ bday => $r->{b_month} ? sprintf('%02d-%02d', $r->{b_month}, $r->{b_day}) : '',
+ traits => join(' ', map sprintf('%d-%d', $_->{tid}, $_->{spoil}), sort { $a->{tid} <=> $b->{tid} } @{$r->{traits}}),
+ vns => join(' ', map sprintf('%d-%d-%d-%s', $_->{vid}, $_->{rid}||0, $_->{spoil}, $_->{role}),
+ sort { $a->{vid} <=> $b->{vid} || ($a->{rid}||0) <=> ($b->{rid}||0) } @{$r->{vns}}),
+ );
+ my $frm;
+
+ if($self->reqMethod eq 'POST') {
+ return if !$self->authCheckCode;
+ $frm = $self->formValidate(
+ { post => 'name', maxlength => 200 },
+ { post => 'original', required => 0, maxlength => 200, default => '' },
+ { post => 'alias', required => 0, maxlength => 500, default => '' },
+ { post => 'desc', required => 0, maxlength => 5000, default => '' },
+ { post => 'gender', required => 0, default => 'unknown', enum => $self->{genders} },
+ { post => 'image', required => 0, default => 0, template => 'int' },
+ { post => 'bday', required => 0, default => '', regex => [ qr/^\d{2}-\d{2}$/, mt('_chare_form_bday_err') ] },
+ { post => 's_bust', required => 0, default => 0, template => 'int' },
+ { post => 's_waist', required => 0, default => 0, template => 'int' },
+ { post => 's_hip', required => 0, default => 0, template => 'int' },
+ { post => 'height', required => 0, default => 0, template => 'int' },
+ { post => 'weight', required => 0, default => 0, template => 'int' },
+ { post => 'bloodt', required => 0, default => 'unknown', enum => $self->{blood_types} },
+ { post => 'main', required => 0, default => 0, template => 'int' },
+ { post => 'main_spoil', required => 0, default => 0, enum => [ 0..2 ] },
+ { post => 'traits', required => 0, default => '', regex => [ qr/^(?:[1-9]\d*-[0-2])(?: +[1-9]\d*-[0-2])*$/, 'Incorrect trait format.' ] },
+ { post => 'vns', required => 0, default => '', regex => [ qr/^(?:[1-9]\d*-\d+-[0-2]-[a-z]+)(?: +[1-9]\d*-\d+-[0-2]-[a-z]+)*$/, 'Incorrect VN format.' ] },
+ { post => 'editsum', required => 0, maxlength => 5000 },
+ { post => 'ihid', required => 0 },
+ { post => 'ilock', required => 0 },
+ );
+ push @{$frm->{_err}}, 'badeditsum' if !$frm->{editsum} || lc($frm->{editsum}) eq lc($frm->{desc});
+
+ # handle image upload
+ $frm->{image} = _uploadimage($self, $r, $frm);
+
+ # validate main character
+ if(!$frm->{_err} && $frm->{main}) {
+ my $m = $self->dbCharGet(id => $frm->{main}, what => 'extended')->[0];
+ push @{$frm->{_err}}, 'mainchar' if !$m || $m->{id} == $r->{id} || $m->{main}
+ || $self->dbCharGet(instance => $r->{id})->[0];
+ }
+
+ my(@traits, @vns);
+ if(!$frm->{_err}) {
+ # parse and normalize
+ @traits = sort { $a->[0] <=> $b->[0] } map /^(\d+)-(\d+)$/&&[$1,$2], split / /, $frm->{traits};
+ @vns = sort { $a->[0] <=> $b->[0] || $a->[1] <=> $b->[1] } map [split /-/], split / /, $frm->{vns};
+ $frm->{traits} = join(' ', map sprintf('%d-%d', @$_), @traits);
+ $frm->{vns} = join(' ', map sprintf('%d-%d-%d-%s', @$_), @vns);
+ $frm->{ihid} = $frm->{ihid} ?1:0;
+ $frm->{ilock} = $frm->{ilock}?1:0;
+ $frm->{main_spoil} = 0 if !$frm->{main};
+
+ # check for changes
+ my $same = $id && !grep $frm->{$_} ne $b4{$_}, keys %b4;
+ return $self->resRedirect("/c$id", 'post') if !$copy && $same;
+ $frm->{_err} = ['nochanges'] if $copy && $same;
+ }
+
+ if(!$frm->{_err}) {
+ # modify for dbCharRevisionInsert
+ ($frm->{b_month}, $frm->{b_day}) = delete($frm->{bday}) =~ /^(\d{2})-(\d{2})$/ ? ($1, $2) : (0, 0);
+ $frm->{main} ||= undef;
+ $frm->{traits} = \@traits;
+ $_->[1]||=undef for (@vns);
+ $frm->{vns} = \@vns;
+
+ my $nrev = $self->dbItemEdit(c => !$copy && $id ? $r->{cid} : undef, %$frm);
+
+ # TEMPORARY SOLUTION! I'll investigate more efficient solutions and incremental updates whenever I have more data
+ $self->dbExec('SELECT traits_chars_calc()');
+
+ return $self->resRedirect("/c$nrev->{iid}.$nrev->{rev}", 'post');
+ }
+ }
+
+ $frm->{$_} //= $b4{$_} for keys %b4;
+ $frm->{editsum} //= sprintf 'Reverted to revision c%d.%d', $id, $rev if !$copy && $rev;
+ $frm->{editsum} = sprintf 'New character based on c%d.%d', $id, $r->{rev} if $copy;
+
+ my $title = mt $r ? ($copy ? '_chare_title_copy' : '_chare_title_edit', $r->{name}) : '_chare_title_add';
+ $self->htmlHeader(title => $title, noindex => 1);
+ $self->htmlMainTabs('c', $r, $copy ? 'copy' : 'edit') if $r;
+ $self->htmlEditMessage('c', $r, $title, $copy);
+ $self->htmlForm({ frm => $frm, action => $r ? "/c$id/".($copy ? 'copy' : 'edit') : '/c/new', editsum => 1, upload => 1 },
+ chare_geninfo => [ mt('_chare_form_generalinfo'),
+ [ input => name => mt('_chare_form_name'), short => 'name' ],
+ [ input => name => mt('_chare_form_original'), short => 'original' ],
+ [ static => content => mt('_chare_form_original_note') ],
+ [ text => name => mt('_chare_form_alias'), short => 'alias', rows => 3 ],
+ [ static => content => mt('_chare_form_alias_note') ],
+ [ text => name => mt('_chare_form_desc').'<br /><b class="standout">'.mt('_inenglish').'</b>', short => 'desc', rows => 6 ],
+ [ select => name => mt('_chare_form_gender'),short => 'gender', options => [
+ map [ $_, mt("_gender_$_") ], @{$self->{genders}} ] ],
+ [ input => name => mt('_chare_form_bday'), short => 'bday', width => 100, post => ' '.mt('_chare_form_bday_fmt') ],
+ [ input => name => mt('_chare_form_bust'), short => 's_bust', width => 50, post => ' cm' ],
+ [ input => name => mt('_chare_form_waist'), short => 's_waist',width => 50, post => ' cm' ],
+ [ input => name => mt('_chare_form_hip'), short => 's_hip', width => 50, post => ' cm' ],
+ [ input => name => mt('_chare_form_height'),short => 'height', width => 50, post => ' cm' ],
+ [ input => name => mt('_chare_form_weight'),short => 'weight', width => 50, post => ' kg' ],
+ [ select => name => mt('_chare_form_bloodt'),short => 'bloodt', options => [
+ map [ $_, mt("_bloodt_$_") ], @{$self->{blood_types}} ] ],
+ [ static => content => '<br />' ],
+ [ input => name => mt('_chare_form_main'), short => 'main', width => 50, post => ' '.mt('_chare_form_main_note') ],
+ [ select => name => mt('_chare_form_main_spoil'), short => 'main_spoil', options => [
+ map [$_, mt("_spoil_$_")], 0..2 ] ],
+ ],
+
+ chare_img => [ mt('_chare_image'), [ static => nolabel => 1, content => sub {
+ div class => 'img';
+ p mt '_chare_image_none' if !$frm->{image};
+ p mt '_chare_image_processing' if $frm->{image} && $frm->{image} < 0;
+ img src => sprintf("%s/ch/%02d/%d.jpg", $self->{url_static}, $frm->{image}%100, $frm->{image}) if $frm->{image} && $frm->{image} > 0;
+ end;
+
+ div;
+ h2 mt '_chare_image_id';
+ input type => 'text', class => 'text', name => 'image', id => 'image', value => $frm->{image};
+ p mt '_chare_image_id_msg';
+ br; br;
+
+ h2 mt '_chare_image_upload';
+ input type => 'file', class => 'text', name => 'img', id => 'img';
+ p mt('_chare_image_upload_msg');
+ end;
+ }]],
+
+ chare_traits => [ mt('_chare_traits'),
+ [ hidden => short => 'traits' ],
+ [ static => nolabel => 1, content => sub {
+ h2 mt '_chare_traits_sel';
+ table; tbody id => 'traits_tbl';
+ Tr id => 'traits_loading'; td colspan => '3', mt('_js_loading'); end;
+ end; end;
+ h2 mt '_chare_traits_add';
+ table; Tr;
+ td class => 'tc_name'; input id => 'trait_input', type => 'text', class => 'text'; end;
+ td colspan => 2, '';
+ end; end 'table';
+ }],
+ ],
+
+ chare_vns => [ mt('_chare_vns'),
+ [ hidden => short => 'vns' ],
+ [ static => nolabel => 1, content => sub {
+ h2 mt '_chare_vns_sel';
+ table; tbody id => 'vns_tbl';
+ Tr id => 'vns_loading'; td colspan => '4', mt('_js_loading'); end;
+ end; end;
+ h2 mt '_chare_vns_add';
+ table; Tr;
+ td class => 'tc_vnadd'; input id => 'vns_input', type => 'text', class => 'text'; end;
+ td colspan => 3, '';
+ end; end;
+ }],
+ ]);
+ $self->htmlFooter;
+}
+
+
+sub _uploadimage {
+ my($self, $c, $frm) = @_;
+ return $c ? $frm->{image} : 0 if $frm->{_err} || !$self->reqPost('img');
+
+ # perform some elementary checks
+ my $imgdata = $self->reqUploadRaw('img');
+ $frm->{_err} = [ 'noimage' ] if $imgdata !~ /^(\xff\xd8|\x89\x50)/; # JPG or PNG headers
+ $frm->{_err} = [ 'toolarge' ] if length($imgdata) > 1024*1024;
+ return undef if $frm->{_err};
+
+ # get image ID and save it, to be processed by Multi
+ my $imgid = $self->dbCharImageId;
+ my $fn = sprintf '%s/static/ch/%02d/%d.jpg', $VNDB::ROOT, $imgid%100, $imgid;
+ $self->reqSaveUpload('img', $fn);
+ chmod 0666, $fn;
+
+ return -1*$imgid;
+}
+
+
+sub list {
+ my($self, $fch) = @_;
+
+ my $f = $self->formValidate(
+ { get => 'p', required => 0, default => 1, template => 'int' },
+ { get => 'q', required => 0, default => '' },
+ );
+ return $self->resNotFound if $f->{_err};
+
+ my($list, $np) = $self->dbCharGet(
+ $fch ne 'all' ? ( char => $fch ) : (),
+ $f->{q} ? ( search => $f->{q} ) : (),
+ results => 50,
+ page => $f->{p},
+ what => 'vns',
+ );
+
+ $self->htmlHeader(title => mt '_charb_title');
+
+ my $quri = uri_escape($f->{q});
+ div class => 'mainbox';
+ h1 mt '_charb_title';
+ form action => '/c/all', 'accept-charset' => 'UTF-8', method => 'get';
+ $self->htmlSearchBox('c', $f->{q});
+ end;
+ p class => 'browseopts';
+ for ('all', 'a'..'z', 0) {
+ a href => "/c/$_?q=$quri", $_ eq $fch ? (class => 'optselected') : (), $_ eq 'all' ? mt('_char_all') : $_ ? uc $_ : '#';
+ }
+ end;
+ end;
+
+ if(!@$list) {
+ div class => 'mainbox';
+ h1 mt '_charb_noresults';
+ p mt '_charb_noresults_msg';
+ end;
+ }
+
+ my $uri = "/c/$fch?q=$quri";
+ @$list && $self->htmlBrowse(
+ class => 'charb',
+ items => $list,
+ options => $f,
+ nextpage => $np,
+ pageurl => $uri,
+ sorturl => $uri,
+ header => [ [ '' ], [ '' ] ],
+ row => sub {
+ my($s, $n, $l) = @_;
+ Tr $n % 2 ? (class => 'odd') : ();
+ td class => 'tc1';
+ cssicon "gen $l->{gender}", mt "_gender_$l->{gender}" if $l->{gender} ne 'unknown';
+ end;
+ td class => 'tc2';
+ a href => "/c$l->{id}", title => $l->{original}||$l->{name}, shorten $l->{name}, 50;
+ b class => 'grayedout';
+ my $i = 1;
+ my %vns;
+ for (@{$l->{vns}}) {
+ next if $_->{spoil} || $vns{$_->{vid}}++;
+ last if $i++ > 4;
+ txt ', ' if $i > 2;
+ a href => "/v$_->{vid}/chars", title => $_->{vntitle}, shorten $_->{vntitle}, 30;
+ }
+ end;
+ end;
+ end;
+ }
+ );
+
+ $self->htmlFooter;
+}
+
+
+1;
+
diff --git a/lib/VNDB/Handler/Misc.pm b/lib/VNDB/Handler/Misc.pm
index 062e3ae4..8f5b4949 100644
--- a/lib/VNDB/Handler/Misc.pm
+++ b/lib/VNDB/Handler/Misc.pm
@@ -11,7 +11,7 @@ use POSIX 'strftime';
TUWF::register(
qr{}, \&homepage,
- qr{(?:([upvr])([1-9]\d*)/)?hist}, \&history,
+ qr{(?:([upvrc])([1-9]\d*)/)?hist}, \&history,
qr{d([1-9]\d*)}, \&docpage,
qr{setlang}, \&setlang,
qr{nospam}, \&nospam,
@@ -196,7 +196,7 @@ sub history {
{ get => 'p', required => 0, default => 1, template => 'int' },
{ get => 'm', required => 0, default => !$type, enum => [ 0, 1 ] },
{ get => 'h', required => 0, default => 0, enum => [ -1..1 ] },
- { get => 't', required => 0, default => '', enum => [ 'v', 'r', 'p' ] },
+ { get => 't', required => 0, default => '', enum => [qw|v r p c|] },
{ get => 'e', required => 0, default => 0, enum => [ -1..1 ] },
{ get => 'r', required => 0, default => 0, enum => [ 0, 1 ] },
);
@@ -206,6 +206,7 @@ sub history {
my $obj = $type eq 'u' ? $self->dbUserGet(uid => $id, what => 'hide_list')->[0] :
$type eq 'p' ? $self->dbProducerGet(id => $id)->[0] :
$type eq 'r' ? $self->dbReleaseGet(id => $id)->[0] :
+ $type eq 'c' ? $self->dbCharGet(id => $id)->[0] :
$type eq 'v' ? $self->dbVNGet(id => $id)->[0] : undef;
my $title = mt $type ? ('_hist_title_item', $obj->{title} || $obj->{name} || $obj->{username}) : '_hist_title';
return $self->resNotFound if $type && !$obj->{id};
@@ -260,6 +261,7 @@ sub history {
a $f->{t} eq 'v' ? (class => 'optselected') : (), href => $u->(t => 'v'), mt '_hist_filter_onlyvn';
a $f->{t} eq 'r' ? (class => 'optselected') : (), href => $u->(t => 'r'), mt '_hist_filter_onlyreleases';
a $f->{t} eq 'p' ? (class => 'optselected') : (), href => $u->(t => 'p'), mt '_hist_filter_onlyproducers';
+ a $f->{t} eq 'c' ? (class => 'optselected') : (), href => $u->(t => 'c'), mt '_hist_filter_onlychars';
end;
p class => 'browseopts';
a !$f->{e} ? (class => 'optselected') : (), href => $u->(e => 0), mt '_hist_filter_allactions';
diff --git a/lib/VNDB/Handler/Releases.pm b/lib/VNDB/Handler/Releases.pm
index a3ecbd42..159b7c3f 100644
--- a/lib/VNDB/Handler/Releases.pm
+++ b/lib/VNDB/Handler/Releases.pm
@@ -3,7 +3,7 @@ package VNDB::Handler::Releases;
use strict;
use warnings;
-use TUWF ':html', 'uri_escape';
+use TUWF ':html', ':xml', 'uri_escape';
use VNDB::Func;
@@ -13,6 +13,7 @@ TUWF::register(
qr{r} => \&browse,
qr{r(?:([1-9]\d*)(?:\.([1-9]\d*))?/(edit|copy))}
=> \&edit,
+ qr{xml/releases.xml} => \&relxml,
);
@@ -590,5 +591,36 @@ sub _fil_compat {
}
+sub relxml {
+ my $self = shift;
+
+ my $f = $self->formValidate(
+ { get => 'v', required => 1, multi => 1, mincount => 1, template => 'int' }
+ );
+ return $self->resNotFound if $f->{_err};
+
+ my $list = $self->dbReleaseGet(vid => $f->{v}, results => 100, what => 'vn');
+ my %vns = map +($_,0), @{$f->{v}};
+ for my $r (@$list) {
+ for my $v (@{$r->{vn}}) {
+ next if !exists $vns{$v->{vid}};
+ $vns{$v->{vid}} = [ $v ] if !$vns{$v->{vid}};
+ push @{$vns{$v->{vid}}}, $r;
+ }
+ }
+ $self->resHeader('Content-type' => 'text/xml; charset=UTF-8');
+ xml;
+ tag 'vns';
+ for (sort { $a->[0]{title} cmp $b->[0]{title} } values %vns) {
+ my $v = shift @$_;
+ tag 'vn', id => $v->{vid}, title => $v->{title};
+ tag 'release', id => $_->{id}, lang => join(',', @{$_->{languages}}), $_->{title}
+ for (@$_);
+ end;
+ }
+ end;
+}
+
+
1;
diff --git a/lib/VNDB/Handler/Tags.pm b/lib/VNDB/Handler/Tags.pm
index a5519cd8..3d698fc3 100644
--- a/lib/VNDB/Handler/Tags.pm
+++ b/lib/VNDB/Handler/Tags.pm
@@ -33,12 +33,12 @@ sub tagpage {
{ get => 's', required => 0, default => 'tagscore', enum => [ qw|title rel pop tagscore rating| ] },
{ get => 'o', required => 0, default => 'd', enum => [ 'a','d' ] },
{ get => 'p', required => 0, default => 1, template => 'int' },
- { get => 'm', required => 0, default => -1, enum => [qw|0 1 2|] },
+ { get => 'm', required => 0, default => undef, enum => [qw|0 1 2|] },
{ get => 'fil', required => 0 },
);
return $self->resNotFound if $f->{_err};
my $tagspoil = $self->reqCookie('tagspoil')||'';
- $f->{m} = $tagspoil =~ /^[0-2]$/ ? $tagspoil : 0 if $f->{m} == -1;
+ $f->{m} //= $tagspoil =~ /^[0-2]$/ ? $tagspoil : 0;
$f->{fil} //= $self->authPref('filter_vn');
my($list, $np) = $t->{meta} || $t->{state} != 2 ? ([],0) : $self->filFetchDB(vn => $f->{fil}, undef, {
@@ -78,18 +78,7 @@ sub tagpage {
a class => 'addnew', href => "/g$tag/add", mt '_tagp_addchild' if $self->authCan('tag') && $t->{state} != 1;
h1 $title;
- p;
- my @p = _parenttags(@{$t->{parents}});
- for my $p (@p ? @p : []) {
- a href => '/g', mt '_tagp_indexlink';
- for (reverse @$p) {
- txt ' > ';
- a href => "/g$_->{id}", $_->{name};
- }
- txt " > $t->{name}";
- br;
- }
- end 'p';
+ parenttags($t, mt('_tagp_indexlink'), 'g');
if($t->{description}) {
p class => 'description';
@@ -110,7 +99,7 @@ sub tagpage {
}
end 'div';
- _childtags($self, $t) if @{$t->{childs}};
+ childtags($self, mt('_tagp_childs'), 'g', $t) if @{$t->{childs}};
if(!$t->{meta} && $t->{state} == 2) {
form action => "/g$t->{id}", 'accept-charset' => 'UTF-8', method => 'get';
@@ -142,57 +131,6 @@ sub tagpage {
}
-# arg: tag hashref
-# returns: [ [ tag1, tag2, tag3 ], [ tag1, tag2, tag5 ] ]
-sub _parenttags {
- my @r;
- for my $t (@_) {
- for (@{$t->{'sub'}}) {
- push @r, [ $t, @$_ ] for _parenttags($_);
- }
- push @r, [$t] if !@{$t->{'sub'}};
- }
- return @r;
-}
-
-
-# used for on both /g and /g+
-sub _childtags {
- my($self, $t, $index) = @_;
-
- div class => 'mainbox';
- h1 mt $index ? '_tagp_tree' : '_tagp_childs';
- ul class => 'tagtree';
- for my $p (sort { @{$b->{'sub'}} <=> @{$a->{'sub'}} } @{$t->{childs}}) {
- li;
- a href => "/g$p->{id}", $p->{name};
- b class => 'grayedout', " ($p->{c_vns})" if $p->{c_vns};
- end, next if !@{$p->{'sub'}};
- ul;
- for (0..$#{$p->{'sub'}}) {
- last if $_ >= 5 && @{$p->{'sub'}} > 6;
- li;
- txt '> ';
- a href => "/g$p->{sub}[$_]{id}", $p->{'sub'}[$_]{name};
- b class => 'grayedout', " ($p->{sub}[$_]{c_vns})" if $p->{'sub'}[$_]{c_vns};
- end;
- }
- if(@{$p->{'sub'}} > 6) {
- li;
- txt '> ';
- a href => "/g$p->{id}", style => 'font-style: italic', mt '_tagp_moretags', @{$p->{'sub'}}-5;
- end;
- }
- end;
- end 'li';
- }
- end 'ul';
- clearfloat;
- br;
- end 'div';
-}
-
-
sub tagedit {
my($self, $tag, $act) = @_;
@@ -333,7 +271,7 @@ sub _set_childs_cat {
}
};
- my $childs = $self->dbTagTree($tag, 25);
+ my $childs = $self->dbTTTree(tag => $tag, 25);
$e->($childs);
}
@@ -510,7 +448,7 @@ sub taglinks {
a href => $url->(t=>$l->{tag}), class => 'setfil', '> ' if !$f->{t};
a href => "/g$l->{tag}", $l->{name};
end;
- td class => 'tc5', !defined $l->{spoiler} ? ' ' : mt "_taglink_spoil$l->{spoiler}";
+ td class => 'tc5', !defined $l->{spoiler} ? ' ' : mt "_spoil_$l->{spoiler}";
td class => 'tc6';
a href => $url->(v=>$l->{vid}), class => 'setfil', '> ' if !$f->{v};
a href => "/v$l->{vid}", shorten $l->{title}, 50;
@@ -687,8 +625,8 @@ sub tagindex {
end;
end;
- my $t = $self->dbTagTree(0, 2);
- _childtags($self, {childs => $t}, 1);
+ my $t = $self->dbTTTree(tag => 0, 2);
+ childtags($self, mt('_tagidx_tree'), 'g', {childs => $t});
table class => 'mainbox threelayout';
Tr;
@@ -712,13 +650,13 @@ sub tagindex {
# Popular
td;
a class => 'addnew', href => "/g/links", mt '_tagidx_rawtags';
- $r = $self->dbTagGet(sort => 'vns', reverse => 1, meta => 0, results => 10);
+ $r = $self->dbTagGet(sort => 'items', reverse => 1, meta => 0, results => 10);
h1 mt '_tagidx_popular';
ul;
for (@$r) {
li;
a href => "/g$_->{id}", $_->{name};
- txt " ($_->{c_vns})";
+ txt " ($_->{c_items})";
end;
}
end;
@@ -765,14 +703,14 @@ sub fulltree {
li;
txt '> ';
a href => "/g$_->{id}", $_->{name};
- b class => 'grayedout', " ($_->{c_vns})" if $_->{c_vns};
+ b class => 'grayedout', " ($_->{c_items})" if $_->{c_items};
end;
$e->($_->{sub}) if $_->{sub};
}
end;
};
- my $tags = $self->dbTagTree(0, 25);
+ my $tags = $self->dbTTTree(tag => 0, 25);
$self->htmlHeader(title => '[DEBUG] Tag tree', noindex => 1);
div class => 'mainbox';
h1 '[DEBUG] Tag tree';
diff --git a/lib/VNDB/Handler/Traits.pm b/lib/VNDB/Handler/Traits.pm
new file mode 100644
index 00000000..887890b6
--- /dev/null
+++ b/lib/VNDB/Handler/Traits.pm
@@ -0,0 +1,435 @@
+
+package VNDB::Handler::Traits;
+
+use strict;
+use warnings;
+use TUWF ':html', ':xml', 'html_escape';
+use VNDB::Func;
+
+
+TUWF::register(
+ qr{i([1-9]\d*)}, \&traitpage,
+ qr{i([1-9]\d*)/(edit)}, \&traitedit,
+ qr{i([1-9]\d*)/(add)}, \&traitedit,
+ qr{i/new}, \&traitedit,
+ qr{i/list}, \&traitlist,
+ qr{i}, \&traitindex,
+ qr{xml/traits\.xml}, \&traitxml,
+);
+
+
+sub traitpage {
+ my($self, $trait) = @_;
+
+ my $t = $self->dbTraitGet(id => $trait, what => 'parents(0) childs(2)')->[0];
+ return $self->resNotFound if !$t;
+
+ my $f = $self->formValidate(
+ { get => 'p', required => 0, default => 1, template => 'int' },
+ { get => 'm', required => 0, default => undef, enum => [qw|0 1 2|] },
+ );
+ return $self->resNotFound if $f->{_err};
+ my $tagspoil = $self->reqCookie('tagspoil')||'';
+ $f->{m} //= $tagspoil =~ /^[0-2]$/ ? $tagspoil : 0;
+
+ my $title = mt '_traitp_title', $t->{meta}?0:1, $t->{name};
+ $self->htmlHeader(title => $title, noindex => $t->{state} != 2);
+ $self->htmlMainTabs('i', $t);
+
+ if($t->{state} != 2) {
+ div class => 'mainbox';
+ h1 $title;
+ if($t->{state} == 1) {
+ div class => 'warning';
+ h2 mt '_traitp_del_title';
+ p;
+ lit mt '_traitp_del_msg';
+ end;
+ end;
+ } else {
+ div class => 'notice';
+ h2 mt '_traitp_pending_title';
+ p mt '_traitp_pending_msg';
+ end;
+ }
+ end 'div';
+ }
+
+ div class => 'mainbox';
+ a class => 'addnew', href => "/i$trait/add", mt '_traitp_addchild' if $self->authCan('charedit') && $t->{state} != 1;
+ h1 $title;
+
+ parenttags($t, mt('_traitp_indexlink'), 'i');
+
+ if($t->{description}) {
+ p class => 'description';
+ lit bb2html $t->{description};
+ end;
+ }
+ if($t->{sexual}) {
+ p class => 'center';
+ b mt '_traitp_sexual';
+ end;
+ }
+ if($t->{alias}) {
+ p class => 'center';
+ b mt('_traitp_aliases');
+ br;
+ lit html_escape($t->{alias});
+ end;
+ }
+ end 'div';
+
+ childtags($self, mt('_traitp_childs'), 'i', $t) if @{$t->{childs}};
+
+ if(!$t->{meta} && $t->{state} == 2) {
+ my($chars, $np) = $self->dbCharGet(
+ trait_inc => $trait,
+ traitspoil => $f->{m},
+ results => 50, page => $f->{p},
+ );
+
+ div class => 'mainbox';
+ h1 mt '_traitp_charlist';
+
+ p class => 'browseopts';
+ # Q: tagp!? A: lazyness >_>
+ a href => "/i$trait?m=0", $f->{m} == 0 ? (class => 'optselected') : (), onclick => "setCookie('tagspoil', 0);return true;", mt '_tagp_spoil0';
+ a href => "/i$trait?m=1", $f->{m} == 1 ? (class => 'optselected') : (), onclick => "setCookie('tagspoil', 1);return true;", mt '_tagp_spoil1';
+ a href => "/i$trait?m=2", $f->{m} == 2 ? (class => 'optselected') : (), onclick => "setCookie('tagspoil', 2);return true;", mt '_tagp_spoil2';
+ end;
+
+ if(!@$chars) {
+ p; br; br; txt mt '_traitp_nochars'; end;
+ }
+ # not really cached at the moment
+ # p; br; txt mt '_traitp_cached'; end;
+ end 'div';
+
+ # TODO: proper table with info and such
+ $self->htmlBrowse(
+ class => 'traitchars',
+ options => $f,
+ nextpage => $np,
+ items => $chars,
+ pageurl => "/i$trait?m=$f->{m}",
+ sorturl => "/i$trait?m=$f->{m}",
+ header => [
+ [ 'Name' ],
+ ],
+ row => sub {
+ my($s, $n, $l) = @_;
+ Tr $n%2?(class => 'odd') : ();
+ td class => 'tc1'; a href => "/c$l->{id}", $l->{name}; end;
+ end;
+ },
+ ) if @$chars;
+ }
+
+ $self->htmlFooter;
+}
+
+
+sub traitedit {
+ my($self, $trait, $act) = @_;
+
+ my($frm, $par);
+ if($act && $act eq 'add') {
+ $par = $self->dbTraitGet(id => $trait)->[0];
+ return $self->resNotFound if !$par;
+ $frm->{parents} = $par->{id};
+ $trait = undef;
+ }
+
+ return $self->htmlDenied if !$self->authCan('charedit') || $trait && !$self->authCan('tagmod');
+
+ my $t = $trait && $self->dbTraitGet(id => $trait, what => 'parents(1) addedby')->[0];
+ return $self->resNotFound if $trait && !$t;
+
+ if($self->reqMethod eq 'POST') {
+ return if !$self->authCheckCode;
+ $frm = $self->formValidate(
+ { post => 'name', required => 1, maxlength => 250, regex => [ qr/^[^,]+$/, 'A comma is not allowed in trait names' ] },
+ { post => 'state', required => 0, default => 0, enum => [ 0..2 ] },
+ { post => 'meta', required => 0, default => 0 },
+ { post => 'sexual', required => 0, default => 0 },
+ { post => 'alias', required => 0, maxlength => 1024, default => '', regex => [ qr/^[^,]+$/s, 'No comma allowed in aliases' ] },
+ { post => 'description', required => 0, maxlength => 10240, default => '' },
+ { post => 'parents', required => !$self->authCan('tagmod'), default => '', regex => [ qr/^(?:$|(?:[1-9]\d*)(?: +[1-9]\d*)*)$/, 'Parent traits must be a space-separated list of trait IDs' ] },
+ { post => 'order', required => 0, default => 0, template => 'int', min => 0 },
+ );
+ my @parents = split /[\t ]+/, $frm->{parents};
+ my $group = undef;
+ if(!$frm->{_err}) {
+ for(@parents) {
+ my $c = $self->dbTraitGet(id => $_);
+ push @{$frm->{_err}}, [ 'parents', 'func', [ 0, mt '_tagedit_err_notfound', $_ ]] if !@$c;
+ $group //= $c->[0]{group}||$c->[0]{id} if @$c;
+ }
+ }
+
+ if(!$frm->{_err}) {
+ $frm->{state} = $frm->{meta} = 0 if !$self->authCan('tagmod');
+ my %opts = (
+ name => $frm->{name},
+ state => $frm->{state},
+ description => $frm->{description},
+ meta => $frm->{meta}?1:0,
+ sexual => $frm->{sexual}?1:0,
+ alias => $frm->{alias},
+ order => $frm->{order},
+ parents => \@parents,
+ group => $group,
+ );
+ if(!$trait) {
+ $trait = $self->dbTraitAdd(%opts);
+ } else {
+ $self->dbTraitEdit($trait, %opts, upddate => $frm->{state} == 2 && $t->{state} != 2) if $trait;
+ _set_childs_group($self, $trait, $group||$trait) if ($group||0) != ($t->{group}||0);
+
+ # TEMPORARY SOLUTION! I'll investigate more efficient solutions and incremental updates whenever I have more data
+ $self->dbExec('SELECT traits_chars_calc()');
+ }
+ $self->resRedirect("/i$trait", 'post');
+ return;
+ }
+ }
+
+ if($t) {
+ $frm->{$_} ||= $t->{$_} for (qw|name meta sexual description state alias order|);
+ $frm->{parents} ||= join ' ', map $_->{id}, @{$t->{parents}};
+ }
+
+ my $title = $par ? mt('_traite_title_add', $par->{name}) : $t ? mt('_traite_title_edit', $t->{name}) : mt '_traite_title_new';
+ $self->htmlHeader(title => $title, noindex => 1);
+ $self->htmlMainTabs('i', $par || $t, 'edit') if $t || $par;
+
+ if(!$self->authCan('tagmod')) {
+ div class => 'mainbox';
+ h1 mt '_traite_req_title';
+ div class => 'notice';
+ h2 mt '_traite_req_subtitle';
+ p;
+ lit mt '_traite_req_msg';
+ end;
+ end;
+ end;
+ }
+
+ $self->htmlForm({ frm => $frm, action => $par ? "/i$par->{id}/add" : $t ? "/i$trait/edit" : '/i/new' }, 'traitedit' => [ $title,
+ [ input => short => 'name', name => mt '_traite_frm_name' ],
+ $self->authCan('tagmod') ? (
+ $t ?
+ [ static => label => mt('_traite_frm_by'), content => $self->{l10n}->userstr($t->{addedby}, $t->{username}) ] : (),
+ [ select => short => 'state', name => mt('_traite_frm_state'), options => [
+ map [$_, mt '_traite_frm_state'.$_], 0..2 ] ],
+ [ checkbox => short => 'meta', name => mt '_traite_frm_meta' ]
+ ) : (),
+ [ checkbox => short => 'sexual', name => mt '_traite_frm_sexual' ],
+ [ textarea => short => 'alias', name => mt('_traite_frm_alias'), cols => 30, rows => 4 ],
+ [ textarea => short => 'description', name => mt '_traite_frm_desc' ],
+ [ input => short => 'parents', name => mt '_traite_frm_parents' ],
+ [ static => content => mt '_traite_frm_parents_msg' ],
+ [ input => short => 'order', name => mt('_traite_frm_gorder'), width => 50, post => ' '.mt('_traite_frm_gorder_msg') ],
+ ]);
+
+ $self->htmlFooter;
+}
+
+# recursively edit all child traits and set the group field
+sub _set_childs_group {
+ my($self, $trait, $group) = @_;
+ my %done;
+
+ my $e;
+ $e = sub {
+ my $l = shift;
+ for (@$l) {
+ $self->dbTraitEdit($_->{id}, group => $group) if !$done{$_->{id}}++;
+ $e->($_->{sub}) if $_->{sub};
+ }
+ };
+ $e->($self->dbTTTree(trait => $trait, 25));
+}
+
+
+sub traitlist {
+ my $self = shift;
+
+ my $f = $self->formValidate(
+ { get => 's', required => 0, default => 'name', enum => ['added', 'name'] },
+ { get => 'o', required => 0, default => 'a', enum => ['a', 'd'] },
+ { get => 'p', required => 0, default => 1, template => 'int' },
+ { get => 't', required => 0, default => -1, enum => [ -1..2 ] },
+ { get => 'q', required => 0, default => '' },
+ );
+ return $self->resNotFound if $f->{_err};
+
+ my($t, $np) = $self->dbTraitGet(
+ sort => $f->{s}, reverse => $f->{o} eq 'd',
+ page => $f->{p},
+ results => 50,
+ state => $f->{t},
+ search => $f->{q}
+ );
+
+ $self->htmlHeader(title => mt '_traitb_title');
+ div class => 'mainbox';
+ h1 mt '_traitb_title';
+ form action => '/i/list', 'accept-charset' => 'UTF-8', method => 'get';
+ input type => 'hidden', name => 't', value => $f->{t};
+ $self->htmlSearchBox('i', $f->{q});
+ end;
+ p class => 'browseopts';
+ a href => "/i/list?q=$f->{q};t=-1", $f->{t} == -1 ? (class => 'optselected') : (), mt '_traitb_state-1';
+ a href => "/i/list?q=$f->{q};t=0", $f->{t} == 0 ? (class => 'optselected') : (), mt '_traitb_state0';
+ a href => "/i/list?q=$f->{q};t=1", $f->{t} == 1 ? (class => 'optselected') : (), mt '_traitb_state1';
+ a href => "/i/list?q=$f->{q};t=2", $f->{t} == 2 ? (class => 'optselected') : (), mt '_traitb_state2';
+ end;
+ if(!@$t) {
+ p mt '_traitb_noresults';
+ }
+ end 'div';
+ if(@$t) {
+ $self->htmlBrowse(
+ class => 'taglist',
+ options => $f,
+ nextpage => $np,
+ items => $t,
+ pageurl => "/i/list?t=$f->{t};q=$f->{q};s=$f->{s};o=$f->{o}",
+ sorturl => "/i/list?t=$f->{t};q=$f->{q}",
+ header => [
+ [ mt('_traitb_col_added'), 'added' ],
+ [ mt('_traitb_col_name'), 'name' ],
+ ],
+ row => sub {
+ my($s, $n, $l) = @_;
+ Tr $n % 2 ? (class => 'odd') : ();
+ td class => 'tc1', $self->{l10n}->age($l->{added});
+ td class => 'tc3';
+ if($l->{group}) {
+ b class => 'grayedout', $l->{groupname}.' / ';
+ }
+ a href => "/i$l->{id}", $l->{name};
+ if($f->{t} == -1) {
+ b class => 'grayedout', ' '.mt '_traitb_note_awaiting' if $l->{state} == 0;
+ b class => 'grayedout', ' '.mt '_traitb_note_del' if $l->{state} == 1;
+ }
+ end;
+ end 'tr';
+ }
+ );
+ }
+ $self->htmlFooter;
+}
+
+
+sub traitindex {
+ my $self = shift;
+
+ $self->htmlHeader(title => mt '_traiti_title');
+ div class => 'mainbox';
+ a class => 'addnew', href => "/i/new", mt '_traiti_create' if $self->authCan('charedit');
+ h1 mt '_traiti_search';
+ form action => '/i/list', 'accept-charset' => 'UTF-8', method => 'get';
+ $self->htmlSearchBox('i', '');
+ end;
+ end;
+
+ my $t = $self->dbTTTree(trait => 0, 2);
+ childtags($self, mt('_traiti_tree'), 'i', {childs => $t});
+
+ table class => 'mainbox threelayout';
+ Tr;
+
+ # Recently added
+ td;
+ a class => 'right', href => '/i/list', mt '_traiti_browseall';
+ my $r = $self->dbTraitGet(sort => 'added', reverse => 1, results => 10);
+ h1 mt '_traiti_recent';
+ ul;
+ for (@$r) {
+ li;
+ txt $self->{l10n}->age($_->{added});
+ txt ' ';
+ b class => 'grayedout', $_->{groupname}.' / ' if $_->{group};
+ a href => "/i$_->{id}", $_->{name};
+ end;
+ }
+ end;
+ end;
+
+ # Popular
+ td;
+ h1 mt '_traiti_popular';
+ ul;
+ $r = $self->dbTraitGet(sort => 'items', reverse => 1, results => 10);
+ for (@$r) {
+ li;
+ b class => 'grayedout', $_->{groupname}.' / ' if $_->{group};
+ a href => "/i$_->{id}", $_->{name};
+ txt " ($_->{c_items})";
+ end;
+ }
+ end;
+ end;
+
+ # Moderation queue
+ td;
+ h1 mt '_traiti_queue';
+ $r = $self->dbTraitGet(state => 0, sort => 'added', reverse => 1, results => 10);
+ ul;
+ li mt '_traiti_queue_empty' if !@$r;
+ for (@$r) {
+ li;
+ txt $self->{l10n}->age($_->{added});
+ txt ' ';
+ b class => 'grayedout', $_->{groupname}.' / ' if $_->{group};
+ a href => "/i$_->{id}", $_->{name};
+ end;
+ }
+ li;
+ br;
+ a href => '/i/list?t=0;o=d;s=added', mt '_traiti_queue_link';
+ txt ' - ';
+ a href => '/i/list?t=1;o=d;s=added', mt '_traiti_denied';
+ end;
+ end;
+ end;
+
+ end 'tr';
+ end 'table';
+ $self->htmlFooter;
+}
+
+
+sub traitxml {
+ my $self = shift;
+
+ my $f = $self->formValidate(
+ { get => 'q', required => 0, maxlength => 500 },
+ { get => 'id', required => 0, multi => 1, template => 'int' },
+ { get => 'r', required => 0, default => 15, template => 'int', min => 1, max => 100 },
+ );
+ return $self->resNotFound if $f->{_err} || (!$f->{q} && !$f->{id} && !$f->{id}[0]);
+
+ my($list, $np) = $self->dbTraitGet(
+ !$f->{q} ? () : $f->{q} =~ /^i([1-9]\d*)/ ? (id => $1) : (search => $f->{q}),
+ $f->{id} && $f->{id}[0] ? (id => $f->{id}) : (),
+ results => $f->{r},
+ page => 1,
+ sort => 'group'
+ );
+
+ $self->resHeader('Content-type' => 'text/xml; charset=UTF-8');
+ xml;
+ tag 'traits', more => $np ? 'yes' : 'no';
+ for(@$list) {
+ tag 'item', id => $_->{id}, meta => $_->{meta} ? 'yes' : 'no', group => $_->{group}||'', groupname => $_->{groupname}||'', state => $_->{state}, $_->{name};
+ }
+ end;
+}
+
+
+1;
+
diff --git a/lib/VNDB/Handler/VNPage.pm b/lib/VNDB/Handler/VNPage.pm
index aa3c0538..763fc82d 100644
--- a/lib/VNDB/Handler/VNPage.pm
+++ b/lib/VNDB/Handler/VNPage.pm
@@ -10,6 +10,7 @@ use VNDB::Func;
TUWF::register(
qr{v/rand} => \&rand,
qr{v([1-9]\d*)/rg} => \&rg,
+ qr{v([1-9]\d*)/(chars)} => \&page,
qr{v([1-9]\d*)(?:\.([1-9]\d*))?} => \&page,
);
@@ -45,6 +46,9 @@ sub rg {
sub page {
my($self, $vid, $rev) = @_;
+ my $char = $rev && $rev eq 'chars';
+ $rev = undef if $char;
+
my $v = $self->dbVNGet(
id => $vid,
what => 'extended anime relations screenshots rating ranking'.($rev ? ' changes' : ''),
@@ -177,9 +181,21 @@ sub page {
}
end 'div'; # /mainbox
- _releases($self, $v, $r);
- _stats($self, $v);
- _screenshots($self, $v, $r) if @{$v->{screenshots}};
+ my $haschar = $self->dbVNHasChar($v->{id});
+ if($haschar) {
+ ul class => 'maintabs notfirst';
+ li class => 'left '.(!$char ? ' tabselected' : ''); a href => "/v$v->{id}#main", name => 'main', mt '_vnpage_tab_main'; end;
+ li class => 'left '.( $char ? ' tabselected' : ''); a href => "/v$v->{id}/chars#chars", name => 'chars', mt '_vnpage_tab_chars'; end;
+ end;
+ }
+
+ if(!$char) {
+ _releases($self, $v, $r);
+ _stats($self, $v);
+ _screenshots($self, $v, $r) if @{$v->{screenshots}};
+ } else {
+ _chars($self, $haschar, $v);
+ }
$self->htmlFooter;
}
@@ -513,5 +529,38 @@ sub _stats {
}
+sub _chars {
+ my($self, $has, $v) = @_;
+ my $l = $has && $self->dbCharGet(vid => $v->{id}, what => "extended vns($v->{id}) traits");
+ return if !$has;
+ # TODO: spoiler handling + hide unimportant roles by default
+ my %done;
+ my %rol;
+ for my $r (@{$self->{char_roles}}) {
+ $rol{$r} = [ grep grep($_->{role} eq $r, @{$_->{vns}}) && !$done{$_->{id}}++, @$l ];
+ }
+ my $first = 0;
+ for my $r (@{$self->{char_roles}}) {
+ next if !@{$rol{$r}};
+ div class => 'mainbox';
+ if(!$first++) {
+ p id => 'charspoil_sel';
+ a href => '#', class => 'sel', mt '_vnpage_tags_spoil0'; # _vnpage!?
+ a href => '#', mt '_vnpage_tags_spoil1';
+ a href => '#', mt '_vnpage_tags_spoil2';
+ end;
+ }
+ h1 mt "_charrole_$r";
+ for my $c (@{$rol{$r}}) {
+ my $minspoil = 5;
+ $minspoil = $_->{vid} == $v->{id} && $_->{spoil} < $minspoil ? $_->{spoil} : $minspoil
+ for(@{$c->{vns}});
+ $self->charTable($c, 1, $c != $rol{$r}[0], 1, $minspoil);
+ }
+ end;
+ }
+}
+
+
1;
diff --git a/lib/VNDB/Util/CommonHTML.pm b/lib/VNDB/Util/CommonHTML.pm
index 1872c083..41370b4b 100644
--- a/lib/VNDB/Util/CommonHTML.pm
+++ b/lib/VNDB/Util/CommonHTML.pm
@@ -17,7 +17,7 @@ our @EXPORT = qw|
# generates the "main tabs". These are the commonly used tabs for
# 'objects', i.e. VN/producer/release entries and users
-# Arguments: u/v/r/p/g, object, currently selected item (empty=main)
+# Arguments: u/v/r/p/g/i/c, object, currently selected item (empty=main)
sub htmlMainTabs {
my($self, $type, $obj, $sel) = @_;
$sel ||= '';
@@ -26,7 +26,7 @@ sub htmlMainTabs {
return if $type eq 'g' && !$self->authCan('tagmod');
ul class => 'maintabs';
- if($type =~ /[uvrp]/) {
+ if($type =~ /[uvrpc]/) {
li $sel eq 'hist' ? (class => 'tabselected') : ();
a href => "/$id/hist", mt '_mtabs_hist';
end;
@@ -65,7 +65,7 @@ sub htmlMainTabs {
end;
}
- if($type eq 'r' && $self->authCan('edit')) {
+ if($type =~ /[rc]/ && $self->authCan('edit')) {
li $sel eq 'copy' ? (class => 'tabselected') : ();
a href => "/$id/copy", mt '_mtabs_copy';
end;
@@ -73,7 +73,8 @@ sub htmlMainTabs {
if( $type eq 'u' && ($self->authInfo->{id} && $obj->{id} == $self->authInfo->{id} || $self->authCan('usermod'))
|| $type =~ /[vrp]/ && $self->authCan('edit') && (!$obj->{locked} || $self->authCan('lock')) && (!$obj->{hidden} || $self->authCan('del'))
- || $type eq 'g' && $self->authCan('tagmod')
+ || $type eq 'c' && $self->authCan('charedit') && (!$obj->{locked} || $self->authCan('lock')) && (!$obj->{hidden} || $self->authCan('del'))
+ || $type =~ /[gi]/ && $self->authCan('tagmod')
) {
li $sel eq 'edit' ? (class => 'tabselected') : ();
a href => "/$id/edit", mt '_mtabs_edit';
@@ -120,15 +121,16 @@ sub htmlDenied {
# Generates message saying that the current item has been deleted,
-# Arguments: [pvr], obj
+# Arguments: [pvrc], obj
# Returns 1 if the use doesn't have access to the page, 0 otherwise
sub htmlHiddenMessage {
my($self, $type, $obj) = @_;
return 0 if !$obj->{hidden};
- my $board = $type eq 'r' ? 'v'.$obj->{vn}[0]{vid} : $type.$obj->{id};
+ my $board = $type eq 'c' ? 'db' : $type eq 'r' ? 'v'.$obj->{vn}[0]{vid} : $type.$obj->{id};
# fetch edit summary (not present in $obj because the changes aren't fetched)
my $editsum = $type eq 'v' ? $self->dbVNGet(id => $obj->{id}, what => 'changes')->[0]{comments}
: $type eq 'r' ? $self->dbReleaseGet(id => $obj->{id}, what => 'changes')->[0]{comments}
+ : $type eq 'c' ? $self->dbCharGet(id => $obj->{id}, what => 'changes')->[0]{comments}
: $self->dbProducerGet(id => $obj->{id}, what => 'changes')->[0]{comments};
div class => 'mainbox';
h1 $obj->{title}||$obj->{name};
@@ -147,7 +149,7 @@ sub htmlHiddenMessage {
# Shows a revision, including diff if there is a previous revision.
-# Arguments: v|p|r, old revision, new revision, @fields
+# Arguments: v|p|r|c, old revision, new revision, @fields
# Where @fields is a list of fields as arrayrefs with:
# [ shortname, displayname, %options ],
# Where %options:
@@ -266,8 +268,8 @@ sub revdiff {
# Arguments: v/r/p, obj
sub htmlEditMessage {
my($self, $type, $obj, $title, $copy) = @_;
- my $num = {v => 0, r => 1, p => 2}->{$type};
- my $guidelines = {v => 2, r => 3, p => 4}->{$type};
+ my $num = {v => 0, r => 1, p => 2, c => 3}->{$type};
+ my $guidelines = {v => 2, r => 3, p => 4, c => 12}->{$type};
div class => 'mainbox';
h1 $title;
@@ -275,7 +277,7 @@ sub htmlEditMessage {
div class => 'warning';
h2 mt '_editmsg_copy_title';
p;
- lit mt '_editmsg_copy_msg', sprintf '<a href="/%s%d">%s</a>', $type, $obj->{id}, xml_escape $obj->{title};
+ lit mt '_editmsg_copy_msg', sprintf '<a href="/%s%d">%s</a>', $type, $obj->{id}, xml_escape $obj->{title}||$obj->{name};
end;
end;
}
@@ -284,7 +286,7 @@ sub htmlEditMessage {
ul;
li; lit mt '_editmsg_msg_guidelines', "/d$guidelines"; end;
if($obj) {
- li; lit mt '_editmsg_msg_discuss', $type eq 'r' ? "/t/v$obj->{vn}[0]{vid}" : "/t/$type$obj->{id}"; end;
+ li; lit mt '_editmsg_msg_discuss', $type eq 'c' ? '/t/db' : $type eq 'r' ? "/t/v$obj->{vn}[0]{vid}" : "/t/$type$obj->{id}"; end;
li; lit mt '_editmsg_msg_history', "/$type$obj->{id}/hist"; end;
} elsif($type ne 'r') {
li; lit mt '_editmsg_msg_search', "/$type/all", $num; end;
@@ -303,9 +305,10 @@ sub htmlEditMessage {
# Generates a small message when the user can't edit the item,
# or the item is locked.
-# Arguments: v/r/p, obj
+# Arguments: v/r/p/c, obj
sub htmlItemMessage {
my($self, $type, $obj) = @_;
+ # $type isn't being used at all... oh well.
if($obj->{locked}) {
p class => 'locked', mt '_itemmsg_locked';
@@ -404,7 +407,9 @@ sub htmlSearchBox {
a href => '/v/all', $sel eq 'v' ? (class => 'sel') : (), mt '_searchbox_vn';
a href => '/r', $sel eq 'r' ? (class => 'sel') : (), mt '_searchbox_releases';
a href => '/p/all', $sel eq 'p' ? (class => 'sel') : (), mt '_searchbox_producers';
+ a href => '/c/all', $sel eq 'c' ? (class => 'sel') : (), mt '_searchbox_chars';
a href => '/g', $sel eq 'g' ? (class => 'sel') : (), mt '_searchbox_tags';
+ a href => '/i', $sel eq 'i' ? (class => 'sel') : (), mt '_searchbox_traits';
a href => '/u/all', $sel eq 'u' ? (class => 'sel') : (), mt '_searchbox_users';
end;
input type => 'text', name => 'q', id => 'q', class => 'text', value => $v;
diff --git a/lib/VNDB/Util/LayoutHTML.pm b/lib/VNDB/Util/LayoutHTML.pm
index 7c03b6a3..22052a75 100644
--- a/lib/VNDB/Util/LayoutHTML.pm
+++ b/lib/VNDB/Util/LayoutHTML.pm
@@ -63,9 +63,11 @@ sub _menu {
div;
a href => '/', mt '_menu_home'; br;
a href => '/v/all', mt '_menu_vn'; br;
+ b class => 'grayedout', '> '; a href => '/g', mt '_menu_tags'; br;
a href => '/r', mt '_menu_releases'; br;
a href => '/p/all', mt '_menu_producers'; br;
- a href => '/g', mt '_menu_tags'; br;
+ a href => '/c/all', mt '_menu_characters'; br;
+ b class => 'grayedout', '> '; a href => '/i', mt '_menu_traits'; br;
a href => '/u/all', mt '_menu_users'; br;
a href => '/hist', mt '_menu_recent_changes'; br;
a href => '/t', mt '_menu_discussion_board'; br;
@@ -100,6 +102,7 @@ sub _menu {
br;
a href => '/v/new', mt '_menu_addvn'; br;
a href => '/p/new', mt '_menu_addproducer'; br;
+ a href => '/c/new', mt '_menu_addcharacter'; br;
br;
a href => "$uid/logout", mt '_menu_logout';
end;
diff --git a/lib/VNDBUtil.pm b/lib/VNDBUtil.pm
index 6285e77a..a0469a1c 100644
--- a/lib/VNDBUtil.pm
+++ b/lib/VNDBUtil.pm
@@ -55,8 +55,8 @@ sub bb2html {
};
while($raw =~ m{(
- ([tdvpr][1-9][0-9]*\.[1-9][0-9]*) | # 2. exid
- ([tdvprug][1-9][0-9]*) | # 3. id
+ ([tdvpric][1-9][0-9]*\.[1-9][0-9]*) | # 2. exid
+ ([tdvprugc][1-9][0-9]*) | # 3. id
(\[[^\s\]]+\]) | # 4. tag
((?:https?|ftp)://[^><"\n\s\]\[]+[\d\w=/-]) # 5. url
)}xg) {
diff --git a/static/f/icons.png b/static/f/icons.png
index e8d9f1f3..f48f4c7f 100644
--- a/static/f/icons.png
+++ b/static/f/icons.png
Binary files differ
diff --git a/util/dbgraph.pl b/util/dbgraph.pl
index f7cb9923..90b48ef7 100755
--- a/util/dbgraph.pl
+++ b/util/dbgraph.pl
@@ -26,6 +26,8 @@ my %subgraphs = (
'Users' => [qw| CCFFFF users votes rlists wlists vnlists sessions notifications users_prefs |],
'Discussion board' => [qw| FFDCDC threads threads_boards threads_posts |],
'Tags' => [qw| FFC8C8 tags tags_aliases tags_parents tags_vn |],
+ 'Traits' => [qw| D1C2FF traits traits_parents traits_chars |],
+ 'Characters' => [qw| F0C2FF chars chars_rev chars_traits chars_vns |],
'Misc' => [qw| F5F5F5 changes anime screenshots stats_cache quotes relgraphs |],
);
diff --git a/util/jsgen.pl b/util/jsgen.pl
index 112bfb7e..f046fe1a 100755
--- a/util/jsgen.pl
+++ b/util/jsgen.pl
@@ -105,6 +105,7 @@ sub jsgen {
$common .= sprintf "age_ratings = [ %s ];\n", join ',', @{$S{age_ratings}};
$common .= sprintf "languages = [ %s ];\n", join ', ', map qq{"$_"}, @{$S{languages}};
$common .= sprintf "platforms = [ %s ];\n", join ', ', map qq{"$_"}, @{$S{platforms}};
+ $common .= sprintf "char_roles = [ %s ];\n", join ', ', map qq{"$_"}, @{$S{char_roles}};
$common .= sprintf "media = [ %s ];\n", join ', ', map qq{"$_"}, sort keys %{$S{media}};
$common .= sprintf "release_types = [ %s ];\n", join ', ', map qq{"$_"}, @{$S{release_types}};
$common .= sprintf "animated = [ %s ];\n", join ', ', @{$S{animated}};
diff --git a/util/sql/all.sql b/util/sql/all.sql
index 0a6d1037..d05297da 100644
--- a/util/sql/all.sql
+++ b/util/sql/all.sql
@@ -4,8 +4,11 @@
-- data types
CREATE TYPE anime_type AS ENUM ('tv', 'ova', 'mov', 'oth', 'web', 'spe', 'mv');
-CREATE TYPE dbentry_type AS ENUM ('v', 'r', 'p');
+CREATE TYPE blood_type AS ENUM ('unknown', 'a', 'b', 'ab', 'o');
+CREATE TYPE char_role AS ENUM ('main', 'primary', 'side', 'appears');
+CREATE TYPE dbentry_type AS ENUM ('v', 'r', 'p', 'c');
CREATE TYPE edit_rettype AS (iid integer, cid integer, rev integer);
+CREATE TYPE gender AS ENUM ('unknown', 'm', 'f', 'b');
CREATE TYPE language AS ENUM ('cs', 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja', 'ko', 'nl', 'no', 'pl', 'pt-pt', 'pt-br', 'ru', 'sk', 'sv', 'tr', 'vi', 'zh');
CREATE TYPE medium AS ENUM ('cd', 'dvd', 'gdr', 'blr', 'flp', 'mrt', 'mem', 'umd', 'nod', 'in', 'otc');
CREATE TYPE notification_ntype AS ENUM ('pm', 'dbdel', 'listdel', 'dbedit', 'announce');
@@ -32,6 +35,7 @@ CREATE TYPE vn_relation AS ENUM ('seq', 'preq', 'set', 'alt', 'char', 'sid
CREATE TRIGGER hidlock_update BEFORE UPDATE ON vn FOR EACH ROW WHEN (OLD.latest IS DISTINCT FROM NEW.latest) EXECUTE PROCEDURE update_hidlock();
CREATE TRIGGER hidlock_update BEFORE UPDATE ON producers FOR EACH ROW WHEN (OLD.latest IS DISTINCT FROM NEW.latest) EXECUTE PROCEDURE update_hidlock();
CREATE TRIGGER hidlock_update BEFORE UPDATE ON releases FOR EACH ROW WHEN (OLD.latest IS DISTINCT FROM NEW.latest) EXECUTE PROCEDURE update_hidlock();
+CREATE TRIGGER hidlock_update BEFORE UPDATE ON chars FOR EACH ROW WHEN (OLD.latest IS DISTINCT FROM NEW.latest) EXECUTE PROCEDURE update_hidlock();
CREATE TRIGGER users_changes_update AFTER INSERT OR DELETE ON changes FOR EACH ROW EXECUTE PROCEDURE update_users_cache();
CREATE TRIGGER users_votes_update AFTER INSERT OR DELETE ON votes FOR EACH ROW EXECUTE PROCEDURE update_users_cache();
@@ -55,6 +59,7 @@ CREATE TRIGGER vn_anime_aid_edit BEFORE UPDATE ON vn_anime
CREATE TRIGGER anime_fetch_notify AFTER INSERT OR UPDATE ON anime FOR EACH ROW WHEN (NEW.lastfetch IS NULL) EXECUTE PROCEDURE anime_fetch_notify();
CREATE TRIGGER vn_rev_image_notify AFTER INSERT OR UPDATE ON vn_rev FOR EACH ROW WHEN (NEW.image < 0) EXECUTE PROCEDURE vn_rev_image_notify();
+CREATE TRIGGER chars_rev_image_notify AFTER INSERT OR UPDATE ON chars_rev FOR EACH ROW WHEN (NEW.image < 0) EXECUTE PROCEDURE chars_rev_image_notify();
CREATE TRIGGER screenshot_process_notify AFTER INSERT OR UPDATE ON screenshots FOR EACH ROW WHEN (NEW.processed = FALSE) EXECUTE PROCEDURE screenshot_process_notify();
@@ -73,6 +78,7 @@ CREATE TRIGGER producer_relgraph_notify AFTER UPDATE ON producers FOR EACH ROW
CREATE TRIGGER insert_notify AFTER INSERT ON changes FOR EACH STATEMENT EXECUTE PROCEDURE insert_notify();
CREATE TRIGGER insert_notify AFTER INSERT ON threads_posts FOR EACH STATEMENT EXECUTE PROCEDURE insert_notify();
CREATE TRIGGER insert_notify AFTER INSERT ON tags FOR EACH STATEMENT EXECUTE PROCEDURE insert_notify();
+CREATE TRIGGER insert_notify AFTER INSERT ON traits FOR EACH STATEMENT EXECUTE PROCEDURE insert_notify();
CREATE TRIGGER release_vncache_update AFTER UPDATE ON releases FOR EACH ROW
WHEN (OLD.latest IS DISTINCT FROM NEW.latest OR OLD.hidden IS DISTINCT FROM NEW.hidden)
@@ -104,6 +110,7 @@ CREATE CONSTRAINT TRIGGER update_vnlist_rlist AFTER INSERT ON rlists DEFERRABLE
-- Sequences used for ID generation of items not in the DB
CREATE SEQUENCE covers_seq;
+CREATE SEQUENCE charimg_seq;
-- Rows that are assumed to be available
diff --git a/util/sql/func.sql b/util/sql/func.sql
index d7b5ab2f..75e5ecad 100644
--- a/util/sql/func.sql
+++ b/util/sql/func.sql
@@ -127,13 +127,47 @@ BEGIN
-- recreate index
CREATE INDEX tags_vn_inherit_tag_vid ON tags_vn_inherit (tag, vid);
-- and update the VN count in the tags table
- UPDATE tags SET c_vns = (SELECT COUNT(*) FROM tags_vn_inherit WHERE tag = id);
+ UPDATE tags SET c_items = (SELECT COUNT(*) FROM tags_vn_inherit WHERE tag = id);
RETURN;
END;
$$ LANGUAGE plpgsql;
+-- recalculate traits_chars
+CREATE OR REPLACE FUNCTION traits_chars_calc() RETURNS void AS $$
+BEGIN
+ TRUNCATE traits_chars;
+ INSERT INTO traits_chars (tid, cid, spoil)
+ -- all char<->trait links of the latest revisions, including chars inherited from child traits
+ -- (also includes meta traits, because they could have a normal trait as parent)
+ WITH RECURSIVE traits_chars_all(lvl, tid, cid, spoiler, meta) AS (
+ SELECT 15, tid, c.id, spoil, false
+ FROM chars_traits ct
+ JOIN chars c ON c.latest = ct.cid
+ WHERE NOT c.hidden
+ UNION ALL
+ SELECT lvl-1, tp.parent, tc.cid, tc.spoiler, t.meta
+ FROM traits_chars_all tc
+ JOIN traits_parents tp ON tp.trait = tc.tid
+ JOIN traits t ON t.id = tp.parent
+ WHERE t.state = 2
+ AND tc.lvl > 0
+ )
+ -- now grouped by (tid, cid) and with meta traits filtered out
+ SELECT tid, cid, (CASE WHEN AVG(spoiler) > 1.3 THEN 2 WHEN AVG(spoiler) > 0.7 THEN 1 ELSE 0 END)::smallint AS spoiler
+ FROM traits_chars_all
+ WHERE NOT meta
+ GROUP BY tid, cid;
+ -- and update the VN count in the tags table
+ UPDATE traits SET c_items = (SELECT COUNT(*) FROM traits_chars WHERE tid = id);
+ RETURN;
+END;
+$$ LANGUAGE plpgsql;
+
+
+
+
----------------------------------------------------------
@@ -164,6 +198,7 @@ BEGIN
INSERT INTO edit_revision (type, iid, ihid, ilock) VALUES (t,
( SELECT vid FROM vn_rev WHERE id = i
UNION SELECT rid FROM releases_rev WHERE id = i
+ UNION SELECT cid FROM chars_rev WHERE id = i
UNION SELECT pid FROM producers_rev WHERE id = i),
COALESCE((SELECT ihid FROM changes WHERE id = i), FALSE),
COALESCE((SELECT ilock FROM changes WHERE id = i), FALSE)
@@ -189,6 +224,7 @@ BEGIN
JOIN ( SELECT id FROM vn_rev WHERE t = 'v' AND vid = i
UNION SELECT id FROM releases_rev WHERE t = 'r' AND rid = i
UNION SELECT id FROM producers_rev WHERE t = 'p' AND pid = i
+ UNION SELECT id FROM chars_rev WHERE t = 'c' AND cid = i
) x(id) ON x.id = c.id
ORDER BY c.id DESC
LIMIT 1;
@@ -204,6 +240,7 @@ BEGIN
WHEN 'v' THEN INSERT INTO vn (latest) VALUES (0) RETURNING id INTO r.iid;
WHEN 'r' THEN INSERT INTO releases (latest) VALUES (0) RETURNING id INTO r.iid;
WHEN 'p' THEN INSERT INTO producers (latest) VALUES (0) RETURNING id INTO r.iid;
+ WHEN 'c' THEN INSERT INTO chars (latest) VALUES (0) RETURNING id INTO r.iid;
END CASE;
ELSE
r.iid := i;
@@ -370,6 +407,52 @@ $$ LANGUAGE plpgsql;
+CREATE OR REPLACE FUNCTION edit_char_init(hid integer) RETURNS void AS $$
+BEGIN
+ BEGIN
+ CREATE TEMPORARY TABLE edit_char (LIKE chars_rev INCLUDING DEFAULTS INCLUDING CONSTRAINTS);
+ ALTER TABLE edit_char DROP COLUMN id;
+ ALTER TABLE edit_char DROP COLUMN cid;
+ CREATE TEMPORARY TABLE edit_char_traits (LIKE chars_traits INCLUDING DEFAULTS INCLUDING CONSTRAINTS);
+ ALTER TABLE edit_char_traits DROP COLUMN cid;
+ CREATE TEMPORARY TABLE edit_char_vns (LIKE chars_vns INCLUDING DEFAULTS INCLUDING CONSTRAINTS);
+ ALTER TABLE edit_char_vns DROP COLUMN cid;
+ EXCEPTION WHEN duplicate_table THEN
+ TRUNCATE edit_char, edit_char_traits, edit_char_vns;
+ END;
+ PERFORM edit_revtable('c', hid);
+ -- new char
+ IF hid IS NULL THEN
+ INSERT INTO edit_char DEFAULT VALUES;
+ -- load revision
+ ELSE
+ INSERT INTO edit_char SELECT name, original, alias, image, "desc", gender, s_bust, s_waist, s_hip, b_month, b_day, height, weight, bloodt, main, main_spoil FROM chars_rev WHERE id = hid;
+ INSERT INTO edit_char_traits SELECT tid, spoil FROM chars_traits WHERE cid = hid;
+ INSERT INTO edit_char_vns SELECT vid, rid, spoil, role FROM chars_vns WHERE cid = hid;
+ END IF;
+END;
+$$ LANGUAGE plpgsql;
+
+
+
+CREATE OR REPLACE FUNCTION edit_char_commit() RETURNS edit_rettype AS $$
+DECLARE
+ r edit_rettype;
+BEGIN
+ IF (SELECT COUNT(*) FROM edit_char) <> 1 THEN
+ RAISE 'edit_char must have exactly one row!';
+ END IF;
+ SELECT INTO r * FROM edit_commit();
+ INSERT INTO chars_rev SELECT r.cid, r.iid, name, original, alias, image, "desc", gender, s_bust, s_waist, s_hip, b_month, b_day, height, weight, bloodt, main, main_spoil FROM edit_char;
+ INSERT INTO chars_traits SELECT r.cid, tid, spoil FROM edit_char_traits;
+ INSERT INTO chars_vns SELECT r.cid, vid, rid, spoil, role FROM edit_char_vns;
+ UPDATE chars SET latest = r.cid WHERE id = r.iid;
+ RETURN r;
+END;
+$$ LANGUAGE plpgsql;
+
+
+
----------------------------------------------------------
@@ -517,6 +600,13 @@ $$ LANGUAGE plpgsql;
+-- Send a notify when a new character image is uploaded
+CREATE OR REPLACE FUNCTION chars_rev_image_notify() RETURNS trigger AS $$
+ BEGIN NOTIFY charimage; RETURN NULL; END;
+$$ LANGUAGE plpgsql;
+
+
+
-- Send a notify when a screenshot needs to be processed
CREATE OR REPLACE FUNCTION screenshot_process_notify() RETURNS trigger AS $$
BEGIN NOTIFY screenshot; RETURN NULL; END;
@@ -590,7 +680,7 @@ $$ LANGUAGE plpgsql;
--- NOTIFY on insert into changes/posts/tags
+-- NOTIFY on insert into changes/posts/tags/trait
CREATE OR REPLACE FUNCTION insert_notify() RETURNS trigger AS $$
BEGIN
IF TG_TABLE_NAME = 'changes' THEN
@@ -599,6 +689,8 @@ BEGIN
NOTIFY newpost;
ELSIF TG_TABLE_NAME = 'tags' THEN
NOTIFY newtag;
+ ELSIF TG_TABLE_NAME = 'traits' THEN
+ NOTIFY newtrait;
END IF;
RETURN NULL;
END;
@@ -618,7 +710,7 @@ $$ LANGUAGE plpgsql;
--- update (vn|release|producer).(hidden|locked) on a new revision
+-- update (vn|release|producer|char).(hidden|locked) on a new revision
-- NOTE: this is a /before/ trigger, it modifies NEW
CREATE OR REPLACE FUNCTION update_hidlock() RETURNS trigger AS $$
DECLARE
@@ -817,3 +909,4 @@ BEGIN
END;
$$ LANGUAGE plpgsql;
+
diff --git a/util/sql/schema.sql b/util/sql/schema.sql
index dd2b2ffd..39e9351a 100644
--- a/util/sql/schema.sql
+++ b/util/sql/schema.sql
@@ -25,6 +25,53 @@ CREATE TABLE changes (
ilock boolean NOT NULL DEFAULT FALSE
);
+-- chars
+CREATE TABLE chars (
+ id SERIAL PRIMARY KEY,
+ latest integer NOT NULL DEFAULT 0,
+ locked boolean NOT NULL DEFAULT FALSE,
+ hidden boolean NOT NULL DEFAULT FALSE
+);
+
+-- chars_rev
+CREATE TABLE chars_rev (
+ id integer NOT NULL PRIMARY KEY,
+ cid integer NOT NULL,
+ name varchar(250) NOT NULL DEFAULT '',
+ original varchar(250) NOT NULL DEFAULT '',
+ alias varchar(500) NOT NULL DEFAULT '',
+ image integer NOT NULL DEFAULT 0,
+ "desc" text NOT NULL DEFAULT '',
+ gender gender NOT NULL DEFAULT 'unknown',
+ s_bust smallint NOT NULL DEFAULT 0,
+ s_waist smallint NOT NULL DEFAULT 0,
+ s_hip smallint NOT NULL DEFAULT 0,
+ b_month smallint NOT NULL DEFAULT 0,
+ b_day smallint NOT NULL DEFAULT 0,
+ height smallint NOT NULL DEFAULT 0,
+ weight smallint NOT NULL DEFAULT 0,
+ bloodt blood_type NOT NULL DEFAULT 'unknown',
+ main integer,
+ main_spoil smallint NOT NULL DEFAULT 0
+);
+
+-- chars_traits
+CREATE TABLE chars_traits (
+ cid integer NOT NULL,
+ tid integer NOT NULL,
+ spoil smallint NOT NULL DEFAULT 0,
+ PRIMARY KEY(cid, tid)
+);
+
+-- chars_vns
+CREATE TABLE chars_vns (
+ cid integer NOT NULL,
+ vid integer NOT NULL,
+ rid integer NULL,
+ spoil smallint NOT NULL DEFAULT 0,
+ role char_role NOT NULL DEFAULT 'main'
+);
+
-- notifications
CREATE TABLE notifications (
id serial PRIMARY KEY NOT NULL,
@@ -192,7 +239,7 @@ CREATE TABLE tags (
meta boolean NOT NULL DEFAULT FALSE,
added timestamptz NOT NULL DEFAULT NOW(),
state smallint NOT NULL DEFAULT 0,
- c_vns integer NOT NULL DEFAULT 0,
+ c_items integer NOT NULL DEFAULT 0,
addedby integer NOT NULL DEFAULT 0,
cat tag_category NOT NULL DEFAULT 'cont'
);
@@ -200,7 +247,7 @@ CREATE TABLE tags (
-- tags_aliases
CREATE TABLE tags_aliases (
alias varchar(250) NOT NULL PRIMARY KEY,
- tag integer NOT NULL,
+ tag integer NOT NULL
);
-- tags_parents
@@ -260,6 +307,37 @@ CREATE TABLE threads_boards (
PRIMARY KEY(tid, type, iid)
);
+-- traits
+CREATE TABLE traits (
+ id SERIAL PRIMARY KEY,
+ name varchar(250) NOT NULL,
+ alias varchar(500) NOT NULL DEFAULT '',
+ description text NOT NULL DEFAULT '',
+ meta boolean NOT NULL DEFAULT false,
+ added timestamptz NOT NULL DEFAULT NOW(),
+ state smallint NOT NULL DEFAULT 0,
+ addedby integer NOT NULL DEFAULT 0,
+ "group" integer,
+ "order" smallint NOT NULL DEFAULT 0,
+ sexual boolean NOT NULL DEFAULT false,
+ c_items integer NOT NULL DEFAULT 0
+);
+
+-- traits_chars
+CREATE TABLE traits_chars (
+ cid integer NOT NULL,
+ tid integer NOT NULL,
+ spoil smallint NOT NULL DEFAULT 0,
+ PRIMARY KEY(cid, tid)
+);
+
+-- traits_parents
+CREATE TABLE traits_parents (
+ trait integer NOT NULL,
+ parent integer NOT NULL,
+ PRIMARY KEY(trait, parent)
+);
+
-- users
CREATE TABLE users (
id SERIAL NOT NULL PRIMARY KEY,
@@ -375,6 +453,15 @@ CREATE TABLE wlists (
ALTER TABLE changes ADD FOREIGN KEY (requester) REFERENCES users (id) ON DELETE SET DEFAULT;
+ALTER TABLE chars ADD FOREIGN KEY (latest) REFERENCES chars_rev (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE chars_rev ADD FOREIGN KEY (id) REFERENCES changes (id);
+ALTER TABLE chars_rev ADD FOREIGN KEY (cid) REFERENCES chars (id);
+ALTER TABLE chars_rev ADD FOREIGN KEY (main) REFERENCES chars (id);
+ALTER TABLE chars_traits ADD FOREIGN KEY (cid) REFERENCES chars_rev (id);
+ALTER TABLE chars_traits ADD FOREIGN KEY (tid) REFERENCES traits (id);
+ALTER TABLE chars_vns ADD FOREIGN KEY (cid) REFERENCES chars_rev (id);
+ALTER TABLE chars_vns ADD FOREIGN KEY (vid) REFERENCES vn (id);
+ALTER TABLE chars_vns ADD FOREIGN KEY (rid) REFERENCES releases (id);
ALTER TABLE notifications ADD FOREIGN KEY (uid) REFERENCES users (id) ON DELETE CASCADE;
ALTER TABLE notifications ADD FOREIGN KEY (c_byuser) REFERENCES users (id) ON DELETE SET DEFAULT;
ALTER TABLE producers ADD FOREIGN KEY (latest) REFERENCES producers_rev (id) DEFERRABLE INITIALLY DEFERRED;
@@ -408,6 +495,12 @@ ALTER TABLE threads ADD FOREIGN KEY (id, count) REFERENCES threads_p
ALTER TABLE threads_posts ADD FOREIGN KEY (tid) REFERENCES threads (id);
ALTER TABLE threads_posts ADD FOREIGN KEY (uid) REFERENCES users (id) ON DELETE SET DEFAULT;
ALTER TABLE threads_boards ADD FOREIGN KEY (tid) REFERENCES threads (id);
+ALTER TABLE traits ADD FOREIGN KEY (addedby) REFERENCES users (id);
+ALTER TABLE traits ADD FOREIGN KEY ("group") REFERENCES traits (id);
+ALTER TABLE traits_parents ADD FOREIGN KEY (trait) REFERENCES traits (id);
+ALTER TABLE traits_parents ADD FOREIGN KEY (parent) REFERENCES traits (id);
+ALTER TABLE traits_chars ADD FOREIGN KEY (cid) REFERENCES chars (id);
+ALTER TABLE traits_chars ADD FOREIGN KEY (tid) REFERENCES traits (id);
ALTER TABLE users_prefs ADD FOREIGN KEY (uid) REFERENCES users (id) ON DELETE CASCADE;
ALTER TABLE vn ADD FOREIGN KEY (latest) REFERENCES vn_rev (id) DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE vn ADD FOREIGN KEY (rgraph) REFERENCES relgraphs (id);
@@ -428,7 +521,7 @@ ALTER TABLE wlists ADD FOREIGN KEY (uid) REFERENCES users
ALTER TABLE wlists ADD FOREIGN KEY (vid) REFERENCES vn (id);
-
CREATE INDEX releases_vn_vid ON releases_vn (vid);
CREATE INDEX tags_vn_date ON tags_vn (date);
+CREATE UNIQUE INDEX chars_vns_pkey ON chars_vns (cid, vid, COALESCE(rid, 0));
diff --git a/util/updates/update_2.19.sql b/util/updates/update_2.19.sql
new file mode 100644
index 00000000..1d980f3d
--- /dev/null
+++ b/util/updates/update_2.19.sql
@@ -0,0 +1,206 @@
+
+-- character database -> traits
+
+CREATE TABLE traits (
+ id SERIAL PRIMARY KEY,
+ name varchar(250) NOT NULL,
+ alias varchar(500) NOT NULL DEFAULT '',
+ description text NOT NULL DEFAULT '',
+ meta boolean NOT NULL DEFAULT false,
+ added timestamptz NOT NULL DEFAULT NOW(),
+ state smallint NOT NULL DEFAULT 0,
+ addedby integer NOT NULL DEFAULT 0 REFERENCES users (id),
+ "group" integer,
+ "order" smallint NOT NULL DEFAULT 0,
+ sexual boolean NOT NULL DEFAULT false,
+ c_items integer NOT NULL DEFAULT 0
+);
+ALTER TABLE traits ADD FOREIGN KEY ("group") REFERENCES traits (id);
+
+CREATE TABLE traits_parents (
+ trait integer NOT NULL REFERENCES traits (id),
+ parent integer NOT NULL REFERENCES traits (id),
+ PRIMARY KEY(trait, parent)
+);
+
+CREATE TRIGGER insert_notify AFTER INSERT ON traits FOR EACH STATEMENT EXECUTE PROCEDURE insert_notify();
+
+ALTER TABLE tags RENAME c_vns TO c_items;
+
+
+-- character database -> chars
+
+CREATE TYPE char_role AS ENUM ('main', 'primary', 'side', 'appears');
+CREATE TYPE blood_type AS ENUM ('unknown', 'a', 'b', 'ab', 'o');
+CREATE TYPE gender AS ENUM ('unknown', 'm', 'f', 'b');
+
+CREATE TABLE chars (
+ id SERIAL PRIMARY KEY,
+ latest integer NOT NULL DEFAULT 0,
+ locked boolean NOT NULL DEFAULT FALSE,
+ hidden boolean NOT NULL DEFAULT FALSE
+);
+
+CREATE TABLE chars_rev (
+ id integer NOT NULL PRIMARY KEY REFERENCES changes (id),
+ cid integer NOT NULL REFERENCES chars (id),
+ name varchar(250) NOT NULL DEFAULT '',
+ original varchar(250) NOT NULL DEFAULT '',
+ alias varchar(500) NOT NULL DEFAULT '',
+ image integer NOT NULL DEFAULT 0,
+ "desc" text NOT NULL DEFAULT '',
+ gender gender NOT NULL DEFAULT 'unknown',
+ s_bust smallint NOT NULL DEFAULT 0,
+ s_waist smallint NOT NULL DEFAULT 0,
+ s_hip smallint NOT NULL DEFAULT 0,
+ b_month smallint NOT NULL DEFAULT 0,
+ b_day smallint NOT NULL DEFAULT 0,
+ height smallint NOT NULL DEFAULT 0,
+ weight smallint NOT NULL DEFAULT 0,
+ bloodt blood_type NOT NULL DEFAULT 'unknown',
+ main integer REFERENCES chars (id),
+ main_spoil smallint NOT NULL DEFAULT 0
+);
+ALTER TABLE chars ADD FOREIGN KEY (latest) REFERENCES chars_rev (id) DEFERRABLE INITIALLY DEFERRED;
+
+CREATE TABLE chars_traits (
+ cid integer NOT NULL REFERENCES chars_rev (id),
+ tid integer NOT NULL REFERENCES traits (id),
+ spoil smallint NOT NULL DEFAULT 0,
+ PRIMARY KEY(cid, tid)
+);
+
+CREATE TABLE chars_vns (
+ cid integer NOT NULL REFERENCES chars_rev (id),
+ vid integer NOT NULL REFERENCES vn (id),
+ rid integer NULL REFERENCES releases (id),
+ spoil smallint NOT NULL DEFAULT 0,
+ role char_role NOT NULL DEFAULT 'main'
+);
+-- primary key won't work when one column allows NULL
+CREATE UNIQUE INDEX chars_vns_pkey ON chars_vns (cid, vid, COALESCE(rid, 0));
+
+-- cache table
+CREATE TABLE traits_chars (
+ cid integer NOT NULL REFERENCES chars (id),
+ tid integer NOT NULL REFERENCES traits (id),
+ spoil smallint NOT NULL DEFAULT 0,
+ PRIMARY KEY(cid, tid)
+);
+
+CREATE SEQUENCE charimg_seq;
+
+
+
+-- allow characters to be versioned using the changes table
+
+CREATE TYPE dbentry_type_tmp AS ENUM ('v', 'r', 'p', 'c');
+ALTER TABLE changes ALTER COLUMN "type" TYPE dbentry_type_tmp USING "type"::text::dbentry_type_tmp;
+DROP FUNCTION edit_revtable(dbentry_type, integer);
+DROP TYPE dbentry_type;
+ALTER TYPE dbentry_type_tmp RENAME TO dbentry_type;
+
+
+-- load the updated functions
+
+\i util/sql/func.sql
+
+
+CREATE TRIGGER hidlock_update BEFORE UPDATE ON chars FOR EACH ROW WHEN (OLD.latest IS DISTINCT FROM NEW.latest) EXECUTE PROCEDURE update_hidlock();
+CREATE TRIGGER chars_rev_image_notify AFTER INSERT OR UPDATE ON chars_rev FOR EACH ROW WHEN (NEW.image < 0) EXECUTE PROCEDURE chars_rev_image_notify();
+
+
+/* Debugging data *-/
+
+
+-- some traits, based on Echo's draft
+INSERT INTO traits (name, meta, state, addedby, "group") VALUES
+ ('Hair', true, 2, 2, NULL),
+ ('Hair Color', true, 2, 2, 1),
+ ('Auburn', false, 2, 2, 1),
+ ('Black', false, 2, 2, 1),
+ ('Blond', false, 2, 2, 1), -- 5
+ ('Brown', false, 2, 2, 1),
+ ('Hairstyle', true, 2, 2, 1),
+ ('Bun', false, 2, 2, 1),
+ ('Odango', false, 2, 2, 1),
+ ('Ponytail', false, 2, 2, 1), -- 10
+ ('Twin Tails', false, 2, 2, 1),
+ ('Short', false, 2, 2, 1),
+ ('Straight', false, 2, 2, 1),
+ ('Eyes', true, 2, 2, NULL),
+ ('Color', true, 2, 2, 14), -- 15
+ ('Amber', false, 2, 2, 14),
+ ('Black', false, 2, 2, 14),
+ ('Red', false, 2, 2, 14),
+ ('Body', true, 2, 2, NULL),
+ ('Apparent age', true, 2, 2, 19), --20
+ ('Child', false, 2, 2, 19),
+ ('Teen', false, 2, 2, 19),
+ ('Young-Adult', false, 2, 2, 19),
+ ('Adult', false, 2, 2, 19),
+ ('Old', false, 2, 2, 19), -- 25
+ ('Body Type', true, 2, 2, 19),
+ ('Slim', false, 2, 2, 19),
+ ('Muscular', false, 2, 2, 19),
+ ('Overweight', false, 2, 2, 19),
+ ('Huge', false, 2, 2, 19); -- 30
+INSERT INTO traits_parents (trait, parent) VALUES
+ (2, 1),
+ (3, 2),
+ (4, 2),
+ (5, 2),
+ (6, 2),
+ (7, 1),
+ (8, 7),
+ (9, 8),
+ (9, 11),
+ (10, 7),
+ (11, 10),
+ (12, 7),
+ (13, 7),
+ (15, 14),
+ (16, 15),
+ (17, 15),
+ (18, 15),
+ (20, 19),
+ (21, 20),
+ (22, 20),
+ (23, 20),
+ (24, 20),
+ (25, 20),
+ (26, 19),
+ (27, 26),
+ (28, 26),
+ (29, 26),
+ (30, 26);
+
+
+-- phorni!
+SELECT edit_char_init(null);
+UPDATE edit_revision SET comments = 'New test entry', requester = 2, ip = '0.0.0.0';
+UPDATE edit_char SET name = 'Phorni', original = 'フォーニ', "desc" = 'Sprite of Music', height = 14, gender = 'f';
+INSERT INTO edit_char_vns VALUES (38, null, 0, 'primary'), (97, null, 2, 'appears');
+SELECT edit_char_commit();
+
+-- saya (incorrect test data)
+SELECT edit_char_init(null);
+UPDATE edit_revision SET comments = '2nd test entry', requester = 2, ip = '0.0.0.0';
+UPDATE edit_char SET name = 'Saya', original = '沙耶', "desc" = 'There is more than meets the eye!', alias = 'Cute monster', height = 140, weight = 52, s_bust = 41, s_waist = 38, s_hip = 40, b_month = 3, b_day = 15, bloodt = 'a', gender = 'f', main = 1;
+INSERT INTO edit_char_traits VALUES (4, 0), (12, 2), (22, 0), (27, 0), (18, 1);
+INSERT INTO edit_char_vns VALUES (97, null, 0, 'primary');
+SELECT edit_char_commit();
+
+-- lafiel (not even a VN character...)
+SELECT edit_char_init(null);
+UPDATE edit_revision SET comments = '3rd test entry', requester = 2, ip = '0.0.0.0';
+UPDATE edit_char SET name = 'Abriel Nei Debrusc Borl Paryun Lafiel', original = 'アブリアル・ネイ=ドゥブレスク・パリューニュ・ベール・パリュン・ラフィール',
+ alias = E'Ablïarsec néïc Dubreuscr Bœrh Parhynr Lamhirh\nLafiel', gender = 'f', height = 163, weight = 53, main = 1, "desc" = 'Not scary at all!';
+INSERT INTO edit_char_traits VALUES (13, 0), (17, 0), (22, 0);
+INSERT INTO edit_char_vns VALUES (97, null, 0, 'primary'), (17, 2479, 1, 'side'), (17, 626, 2, 'primary'), (17, null, 0, 'appears');
+SELECT edit_char_commit();
+
+SELECT traits_chars_calc();
+
+-- */
+