// ==UserScript==
// @name Tumblr-image-sorter-get
// @description Format file name & save path for current image by its tags
// @version 1.3.1.0
// @author Seedmanc
// @namespace https://github.com/Seedmanc/Tumblr-image-sorter
// @include http*://*.amazonaws.com/data.tumblr.com/*
// @include http*://*.media.tumblr.com/*
//these sites were used by animage.tumblr.com to host original images
// @include http://scenario.myweb.hinet.net/*
// @include http*://mywareroom.files.wordpress.com/*
// @include http://e.blog.xuite.net/*
// @include http://voice.x.fc2.com/*
// @grant none
// @require https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js
// @require https://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js
// @require https://greasyfork.org/scripts/11847-swfstore/code/SwfStore.js?version=77621
// @require https://greasyfork.org/scripts/11848-downloadify-clip/code/Downloadify%20+%20Clip.js?version=68937
// @run-at document-start
// @noframes
// ==/UserScript==
// ==Settings=====================================================
var root= 'E:\\#-A\\!Seiyuu\\'; //Main collection folder
//Make sure to use double backslashes instead of single ones everywhere
var ms= '!'; //Metasymbol, denotes folders for categories instead of names, must be their first character
var folders= { //Folder and names matching database
" !!group " : " !!group ", // used both for tag translation and providing the list of existing folders
" !!solo " : " !!solo ", // trailing whitespaces are voluntary in both keys and values,
" !!unsorted" : " !!unsorted ", // first three key names are not to be changed, but folder names can be anything
" 原由実 " : " !iM@S\\Hara Yumi", // subfolders for categories instead of names must have the metasymbol as first symbol
" 今井麻美 " : " !iM@S\\Imai Asami ",
" 沼倉愛美 " : " !iM@S\\Numakura Manami",
" けいおん! " : " !K-On ", //Category folders can have their own tag, which, if present, will affect the folder choice
" 日笠陽子 " : " !K-On\\Hikasa Yoko ", // for solo and group images
" 寿美菜子 " : " !K-On\\Kotobuki Minako",
" 竹達彩奈 " : " !K-On\\Taketatsu Ayana",
" 豊崎愛生 " : " !K-On\\Toyosaki Aki ",
" クリスマス " : " !Kurisumasu ",
" Lisp " : " !Lisp ", //Roman tags can be used as well
" 阿澄佳奈 " : " !Lisp\\Asumi Kana ",
" 酒井香奈子 " : " !Lovedoll\\Sakai Kanako",
" らき☆すた " : " !Lucky Star ",
" 遠藤綾 " : " !Lucky Star\\Endo Aya ",
" 福原香織 " : " !Lucky Star\\Fukuhara Kaori",
" 長谷川静香 " : " !Lucky Star\\Hasegawa Shizuka",
" 加藤英美里 " : " !Lucky Star\\Kato Emiri ",
" 今野宏美 " : " !Lucky Star\\Konno Hiromi ",
" 井上麻里奈 " : " !Minami-ke\\Inoue Marina ",
" 佐藤利奈 " : " !Minami-ke\\Sato Rina ",
" Petit Milady ": " !Petit Milady ",
" 悠木碧 " : " !Petit Milady\\Yuuki Aoi ",
" ロウきゅーぶ! " : " !Ro-Kyu-Bu ",
" Kalafina " : " !Singer\\Kalafina ",
" LiSA " : " !Singer\\LiSA ",
" May'n " : " !Singer\\May'n ",
" 茅原実里 " : " !SOS-dan\\Chihara Minori",
" 後藤邑子 " : " !SOS-dan\\Goto Yuko ",
" 平野綾 " : " !SOS-dan\\Hirano Aya ",
" スフィア " : " !Sphere ",
" やまとなでしこ " : " !Yamato Nadeshiko ",
" 堀江由衣 " : " !Yamato Nadeshiko\\Horie Yui",
" 田村ゆかり " : " !Yamato Nadeshiko\\Tamura Yukari",
" 雨宮天 " : " Amamiya Sora ",
" 千葉紗子 " : " Chiba Saeko ",
" 渕上舞 " : " Fuchigami Mai ",
" 藤田咲 " : " Fujita Saki ",
" 後藤沙緒里 " : " Goto Saori ",
" 花澤香菜 " : " Hanazawa Kana ",
" 早見沙織 " : " Hayami Saori ",
" 井口裕香 " : " Iguchi Yuka ",
" 井上喜久子 " : " Inoue Kikuko ",
" 伊藤かな恵 " : " Ito Kanae ",
" 伊藤静 " : " Ito Shizuka ",
" 門脇舞以 " : " Kadowaki Mai ",
" 金元寿子 " : " Kanemoto Hisako ",
" 茅野愛衣 " : " Kayano Ai ",
" 喜多村英梨 " : " Kitamura Eri ",
" 小林ゆう " : " Kobayashi Yuu ",
" 小清水亜美 " : " Koshimizu Ami ",
" 釘宮理恵 " : " Kugimiya Rie ",
" 宮崎羽衣 " : " Miyazaki Ui ",
" 水樹奈々 " : " Mizuki Nana ",
" 桃井はるこ " : " Momoi Haruko ",
" 中原麻衣 " : " Nakahara Mai ",
" 中島愛 " : " Nakajima Megumi ",
" 名塚佳織 " : " Nazuka Kaori ",
" 野川さくら " : " Nogawa Sakura ",
" 野中藍 " : " Nonaka Ai ",
" 能登麻美子 " : " Noto Mamiko ",
" 折笠富美子 " : " Orikasa Fumiko ",
" 朴璐美 " : " Paku Romi ",
" 榊原ゆい " : " Sakakibara Yui ",
" 坂本真綾 " : " Sakamoto Maaya ",
" 佐倉綾音 " : " Sakura Ayane ",
" 沢城みゆき " : " Sawashiro Miyuki ",
" 椎名へきる " : " Shiina Hekiru ",
" 清水愛 " : " Shimizu Ai ",
" 下田麻美 " : " Shimoda Asami ",
" 新谷良子 " : " Shintani Ryoko ",
" 白石涼子 " : " Shiraishi Ryoko ",
" 田中理恵 " : " Tanaka Rie ",
" 丹下桜 " : " Tange Sakura ",
" 東山奈央 " : " Toyama Nao ",
" 植田佳奈 " : " Ueda Kana ",
" 上坂すみれ " : " Uesaka Sumire ",
" ゆかな " : " Yukana "
};
var ignore= "歌手, seiyuu, 声優"; //These tags will not count towards any category and won't be included into filename
var allowUnicode= false; //Whether to allow unicode characters in manual translation input, not tested
var useFolderNames= true; //In addition to tags listed in keys of the folders object, recognize also folder names themselves
// this way you won't have to provide both roman and kanji spellings for names as separate tags
var debug= false; //Initial debug state, affects creation of flashDBs. Value saved in the DB overrides it after DB init.
var storeUrl= '//dl.dropboxusercontent.com/u/74005421/js%20requisites/storage.swf';
//Flash databases are bound to the URL, must be same as in the other script
// ==/Settings=========================================================
tagsDB=null; //Makes sure databases are accessible from console for debugging
names=null ;
meta=null ;
var title;
var filename;
var folder = '';
var DBrec=''; //Raw DB record, stringified object with fields for saved flag and tag list
var N=M=T=false; //Flags indicating readiness of plugins loaded simultaneously
var exclrgxp=/%|\/|:|\||>|<|\?|"|\*/g; //Pattern of characters not to be used in filepaths
var downloadifySwf= '//dl.dropboxusercontent.com/u/74005421/js%20requisites/downloadify.swf';
//Flash button URL
var style={ //In an object so you can fold it in any decent editor. If only you had that in chrome.
s:" \
div#output { \
position: absolute; \
left: 0; top: 0; \
width: 100px; height: 30px; \
} \
div#down { \
left: 1px; \
position: fixed; \
z-index: 98; \
} \
table#port { \
top: 30px; \
left: 1px; \
position: fixed; \
background-color: \
rgba(192,192,192,0.85); \
border-bottom: 1px solid black; \
z-index: 97; \
width: 100px; \
border-collapse: collapse; \
} \
table#translations { \
position: absolute; \
background-color: \
rgba(255,255,255,0.8); \
top: 48px; \
overflow: scroll; \
font-size: 90%; \
margin-left: -1px; \
width: 103px; \
table-layout: fixed; \
} \
td.settings { \
border-left: 1px solid black; \
border-right: 1px solid black; \
} \
a.settings { \
text-decoration: none; \
} \
table, tr { \
text-align: center; \
} \
td#ex { \
padding: 0; \
} \
input.txt { \
width: 95%; \
} \
td.cell, td.radio{ \
border: 1px solid black; \
overflow: hidden; \
} \
table.cell { \
background-color: \
rgba(255,255,255,0.75); \
width: 100%; \
border-collapse: collapse; \
} \
a { \
font-family: Arial; \
font-size: small; \
} \
th { \
border: 0; \
color:black; \
} \
input#submit { \
width: 98%; \
height: 29px; \
} \
"}; //This certainly needs optimisation
var out=$('<div id="output"><div id="down"></div></div>'); //Main layer that holds the GUI
var tb =$('<table id="translations">'); //Table for entering manual translation of unknown tags
var tagcell='<table class="cell"><tr> \
<td class="radio"><input type="radio" class="category" value="name"/></td> \
<td class="radio"><input type="radio" class="category" value="meta"/></td> \
</tr><tr> \
<td colspan="2"><a href="#" title="Click to ignore this tag for now" class="ignr">';
//Each cell has the following in it:
// two radiobuttons to choose a category for the tag - name or meta
// the tag itself, either in roman or in kanji
// the tag is also a link, clicking which removes the tag from results until refresh
// if the tag is in kanji, cell has a text field to input translation manually
// if there are also roman tags, they are used as options for quick input into the text field
// if the tag is in roman and consists of two words, cell has a button enabled to swap their order
// otherwise the button is disabled
var tfoot=$('<tfoot><tr><td> \
<input type="submit" id="submit" value="submit"> \
</td></tr></tfoot>'); //At the bottom of the table there is the "submit" button that applies changes
var thead=$('<thead><tr><td > \
<table class="cell" style="font-width:95%; font-size:small;"> \
<tr class="cell"><th class="cell">name</th><th class="cell">meta</th></tr> \
</table> \
</td></tr></thead>');
tb.append(thead).append(tfoot).hide();
port=document.createElement('table'); //Subtable for settings and im/export of tag databases
row= port.insertRow(0);
cell=row.insertCell(0);
cell.setAttribute('class','settings');
cell.innerHTML=' <a href="##" onclick=toggleSettings() class="settings">- settings -</a> ';
row0=port.insertRow(1);
row0.insertCell(0).innerHTML='<input type="checkbox" id="debug"/> debug';
row1=port.insertRow(2);
row1.insertCell(0).innerHTML=' <a href="###" onclick=ex() id="aex" class="exim">export db</a>';
row2=port.insertRow(3);
row2.insertCell(0).id='ex';
row3=port.insertRow(4);
row3.insertCell(0).innerHTML=' <a href="####" onclick=im() id="aim" class="exim">import db</a> ';
row4=port.insertRow(5);
row4.insertCell(0).id='im';
port.id='port';
window.onerror = function(msg, url, line, col, error) { //General error handler
var extra = !col ? '' : '\ncolumn: ' + col;
extra += !error ? '' : '\nerror: ' + error; //Shows '✗' for errors in title and also alerts a message if in debug mode
if (msg.search('this.swf')!=-1)
return true; //Except for irrelevant errors
document.title+='✗';
if (debug)
alert("Error: " + msg + "\nurl: " + url + "\nline: " + line + extra);
var suppressErrorAlert = true;
return suppressErrorAlert;
};
var xhr = new XMLHttpRequest(); //Redownloads opened image as blob
xhr.responseType="blob"; // so that it would be possible to get it via downloadify button
xhr.onreadystatechange = function() { // supposedly the image is being taken from cache so it shouldn't cause any slowdown
if (this.readyState == 4 && this.status == 200) {
var blob=this.response;
var reader = new window.FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function() {
base64data = reader.result;
base64data=base64data.replace(/data\:image\/\w+\;base64\,/,"");
dl(base64data); //Call the button creation function
}
} else if ((this.status!=200)&&(this.status!=0)) {
if (this.status==404) {
document.title='Error '+this.status;
throw new Error('404');
};
throw new Error('Error getting image: '+this.status);
};
};
function expandFolders(){ //Complement DB with tags produced from folders names
var t,rx,x;
for (var key in folders) {
if (folders.hasOwnProperty(key)&&(['!group','!solo','!unsorted'].indexOf(key)==-1)) {
t=folders[key];
rx=new RegExp('/^'+String.fromCharCode(92)+ms+'/', '');
x=getFileName(t).toLowerCase().replace(rx,'');
folders[x]=t;
};
};
};
rootrgxp=/^([a-z]:){1}(\\[^<>:"/\\|?*]+)+\\$/gi;
try {
if (!(rootrgxp.test(root)))
throw new Error('Illegal characters in root folder path: "'+root+'"');
ms=ms[0]; //It's a symbol, not a string, after all
if ((exclrgxp.test(ms))||(/\\|\s/.test(ms)))
throw new Error ('Illegal character as metasymbol: "'+ms+'"');
} catch (err) {
if (!debug)
alert(err.name+': '+err.message);
throw err;
};
function checkMatch(obj,fix){ //Remove trailing whitespace in object keys and values & check correctness of user input
fix=fix||false;
try { //make sure that folder names have no illegal characters
for (var key in obj) { //Convert keys to lower case for better matching
if (obj.hasOwnProperty(key)) {
t=obj[key].trim().replace(/^\\|\\$/g, '').trim();
delete obj[key];
k=key.trim().toLowerCase();
obj[k]=t;
if (exclrgxp.test(obj[k])) //Can't continue until the problem is fixed
if (!fix)
throw new Error('Illegal characters in folder name entry: "'+obj[k]+'" for name "'+k+'"')
else
obj[k]=t.replace(exclrgxp, '-');
};
};
} catch (err) {
if (!debug)
alert(err.name+': '+err.message); //Gotta always notify the user
throw err;
}; //TODO: even more checks here
};
function toggleSettings(){ //Show drop-down menu with settings
$('table#port td').not('.settings').toggle();
$('table#translations').css('top',($('table#port').height()+30)+'px');
sign=$('a.settings').eq(0);
if (sign.text().search(/\+/,'-')!=-1) {
sign.text(sign.text().replace(/\+/gi,'-'));
$('td.settings').css('border-bottom','');
}
else {
sign.text(sign.text().replace(/\-/gi,'+'));
$('td.settings').css('border-bottom','1px solid black');
}
};
function debugSwitch(checkbox){ //Toggling debug mode requires page reload
debug = checkbox.checked;
tagsDB.set(':debug:',debug );
location.reload();
};
onDOMcontentLoaded();
function onDOMcontentLoaded(){ //Load plugins and databases
checkMatch(folders); //Run checks on user-input content and format it
if (useFolderNames)
expandFolders();
ignore=$.map(ignore.split(','), function(v,i){
return v.trim().toLowerCase();
});
href=document.location.href;
if (href.indexOf('tumblr')==-1) //If not on tumblr
if (!(/(jpe?g|bmp|png|gif)/gi).test(href.split('.').pop())) // check if this is actually an image link
return;
$('img').wrap("<center></center>");
$('body').append(out);
names = new SwfStore({ //Auxiliary database for names that don't have folders
namespace: "names",
swf_url: storeUrl,
onready: function(){
document.title+=(debug)?' NM ':'';
N=true;
mutex();
},
onerror: function() {
document.title+=' ✗ names failed to load';}
});
meta = new SwfStore({ //Auxiliary DB for meta tags such as franchise name or costume/accessories
namespace: "meta",
swf_url: storeUrl,
onready: function(){
M=true;
mutex();
},
onerror: function() {
document.title+=' ✗ meta failed to load';}
});
tagsDB = new SwfStore({ //Loading main tag database, holds pairs "filename {s:is_saved?1:0,t:'tag1,tag2,...,tagN'}"
namespace: "animage",
swf_url: storeUrl,
onready: function(){
document.title+=(debug)?' T ':'';
debug =(tagsDB.get(':debug:')=='true'); //Override initial debug state with the one stored in DB
tagsDB.config.debug=debug;
getTags();
},
debug: debug,
onerror: function() {
document.title='tagsdb error';
throw new Error('tagsDB failed to load');
}
}); //TODO: delay aux DBs loading until & if they're actually needed?
};
function getTags(retry){ //Manages tags acquisition for current image file name from db
DBrec=JSON.parse(tagsDB.get(getFileName(document.location.href))); // first attempt at getting taglist for current filename is done upon the beginning of image load
if ((DBrec!=null) || (debug)) { // if tags are found report readiness
T=true; // or if we're in debug mode, proceed anyway
mutex();
} else
if ((retry) || (document.readyState=='complete')) //Otherwise if we ran out of attempts or it's too late
return // stop execution
else {
retry=true; // but if not schedule the second attempt at retrieving tags to image load end
window.addEventListener('load',function(){ getTags(true);},false);
};
}; //TODO: make getTags actually return the value to main() to get rid of the global var
function mutex(){ //Check readiness of plugins and databases when they're loading simultaneously
if (N && M && T) { // when everything is loaded, proceed further
N=M=T=false;
main();
};
};
function main(){ //Launch tag processing and handle afterwork
$("<style>"+style.s+"</style>" ).appendTo( "head" ); //assign functions to events and whatnot
$('div#output').append(port);
toggleSettings();
$('input#debug').prop('checked',debug);
$('a#aim')[0].onclick=im;
$('a#aex')[0].onclick=ex;
$('a.settings')[0].onclick=toggleSettings;
$('input#debug')[0].onclick=function(){debugSwitch(this);};
if (debug)
$("div[id^='SwfStore_animage_']").css('top','0').css('left','101px').css("position",'absolute').css('opacity','0.7');
//TODO: make the code above run regardless of found DB record
$('div#output').append(tb);
analyzeTags();
$('input#submit')[0].onclick=submit;
$('input.txt').on('change',selected);
xhr.open("get", document.location.href, true); //Reget the image to attach it to downloadify button
xhr.send();
$(window).load(function(){document.title=title;});
};
function isANSI(s) { //Some tags might be already in roman and do not require translation
is=true;
s=s.split('');
$.each(s,function(i,v){
is=is&&(/[\u0000-\u00ff]/.test(v));});
return is;
};
function analyzeTags() { //This is where the tag matching magic occurs
filename=getFileName(document.location.href, true);
if (!DBrec) return; // if there are any tags, that is
folder='';
if (debug)
document.title=JSON.stringify(DBrec,null,' ')+' ' //Show raw DB record
else
document.title='';
tags=DBrec.t.split(',');
fldrs=[];
nms=[];
mt=[];
ansi={}
rest=[];
tags=$.map(tags,function(v,i){ //Some formatting is applied to the taglist before processing
v=v.replace(/’/g,"\'").replace(/"/g,"''");
v=v.replace(/\\/g, '-');
v=v.replace(/(ou$)|(ou )/gim,'o ').trim(); //Eliminate variations in writing 'ō' as o/ou at the end of the name in favor of 'o'
// I dunno if it should be done in the middle of the name as well
sp=v.split(' ');
if (sp.length>1)
$.each(tags, function(ii,vv){
if (ii==i) return true;
if (sp.join('')==vv)
return v=false; //Some bloggers put kanji tags both with and without spaces, remove duplicates with spaces
}
);
if (!v)
return null;
if ((ignore.indexOf(v)!=-1)||(ignore.indexOf(v.split(' ').reverse().join(' '))!=-1))
return null //Remove ignored tags so that they don't affect the tag amount
else return v;
});
//1st sorting stage, no prior knowledge about found categories
$.each(tags, function(i,v){ //Divide tags for the image into 5 categories
if (folders.hasOwnProperty(v)) // the "has folder" category
fldrs.push(folders[v])
else if (names.get(v)) // the "no folder name tag" category
nms.push(names.get(v))
else if (meta.get(v)) // the "no folder meta tag" category,
mt.push(meta.get(v)) // which doesn't count towards final folder decision, but simply adds to filename
else if (isANSI(v)) {
if (tags.length==1) //If the tag is already in roman and has no folder it might be either name or meta
nms.push(v) //if it's the only tag it is most likely the name
else { // otherwise put it into the "ansi" category that does not require translation
splt=v.split(' ');
if (splt.length==2) { //Some bloggers put tags for both name reading orders (name<->surname),
rvrs=splt.reverse().join(' ');
if (names.get(rvrs)) { // thus creating duplicating tags
nms.push(names.get(rvrs)) // try to find database entry for reversed order first,
return true;
}
else if (ansi.hasOwnProperty(rvrs)) // then check for duplicates
return true;
}
ansi[v]=true;
};
}
else
rest.push(v); // finally the "untranslated" category
});
//2nd sorting stage, now we know how many tags of each category there are
//It's time to filter the "ansi" category further
$.each(fldrs.concat(nms.concat(mt)), function(i,v){ //Some bloggers put both kanji and translated names into tags
rx=new RegExp('/^'+String.fromCharCode(92)+ms+'/', '');
x=getFileName(v).toLowerCase().replace(rx,'');
y=x.split(' ').reverse().join(' '); // check if we already have a name translated to avoid duplicates
delete ansi[x]; //I have to again check for both orders even though I deleted one of them before,
delete ansi[y]; // but at the time of deletion there was no way to know yet which one would match the kanji tag
}); //This also gets rid of reverse duplicates between recognized tags and ansi
fldrs=mkUniq(fldrs);
nms=$(nms).not(fldrs).get(); //subtract fldrs from nms if they happen to have repeating elements
fldrs2=[];
fldrs=$.grep(fldrs,function(v,i){ //A trick to process folders for meta tags, having subfolders for names inside
fmeta=getFileName(v);
if ((fmeta.indexOf(ms)==0)) { // such folders must have the metasymbol as the first character
fldrs2.push(fmeta);
if (fldrs.concat(nms).length==1) //In the rare case when there are no name tags at all we put the image to meta folder
folder+=v+'\\' // no need to put meta tag into filename this way, since the image will be in the same folder
else
mt.push(fmeta.replace(ms,'')); //usually it needs to be done though
return false; //exclude processed meta tags from folder category
}
else
return true; //return all the non-meta folder tags
}
);
if (fldrs2.length==1) { //Make sure only one folder meta tag exists
folders['!!solo']=fldrs2[0]; //replace solo folder with metatag folder, so the image can go there if needed,
folders['!!group']=fldrs2[0]; // same for group folder (see 3rd sorting stage)
};
fldrs2=$.map(fldrs,function(vl,ix){
return getFileName(vl); //Extract names from folder paths
});
mt=mt.concat(Object.keys(ansi)); //Roman tags have to go somewhere until assigned a category manually
filename=(mkUniq(fldrs2.concat(nms)).concat(['']).concat(mkUniq(mt)).join(',').replace(/\s/g,'_').replace(/\,/g,' ')+' '+filename).trim();
//Format the filename in a booru-compatible way, replacing spaces with underscores,
// first come the names alphabetically sorted, then the meta sorted separately
// and lastly the original filename;
// any existing commas will be replaced with spaces as well
//this way the images are ready to be uploaded to boorus using the mass booru uploader script
unsorted=(rest.length>0)||(Object.keys(ansi).length>0); //Unsorted flag is set if there are tags outside of 3 main categories
//Final, 3rd sorting stage, assign a folder to the image based on found tags and categories
nms=mkUniq(nms);
if (unsorted) { //If there are any untranslated tags, make a table with text fields to provide manual translation
var fn=rest.reduce(function (fn, v){
return fn+' '+'['+v.replace(/\s/g,'_')+']'; // such tags are enclosed in [ ] in filename for better searchability on disk
},'');
buildTable(ansi, rest);
folder=folders["!!unsorted"]+'\\'; //Mark image as going to "unsorted" folder if it still has untranslated tags
filename=fn+' '+filename;
document.title+='? '; //no match ;_;
} else //TODO: option to disable unsorted category if translations are not required by user
if ((fldrs.length==1)&&(nms.length==0)){ //Otherwise if there's only one tag and it's a folder tag, assign the image right there
folder=fldrs[0]+'\\';
filename=filename.split(' ');
filename.shift(); //Remove the folder name from file name since the image goes into that folder anyway
filename=filename.join(' ').trim();
document.title+='✓ '; //100% match, yay
} else
if ((fldrs.length==0)&&(nms.length==1)){ //If there's only one name tag without a folder for it, goes into default "solo" folder
folder=folders['!!solo']+'\\'; // unless we had a !meta folder tag earlier, then the solo folder
// would have been replaced with the appropriate !meta folder
} else
if (nms.length+fldrs.length>1) //Otherwise if there are several name tags, folder or not, move to the default "group" folder
folder=folders['!!group']+'\\'; // same as the above applies for meta
filename=filename.replace(exclrgxp, '-').trim(); //Make sure there are no forbidden characters in the resulting name
document.title+=' \\'+folder+filename;
folder=(root+folder).replace(/\\\\/g,'\\'); //If no name or folder tags were found, folder will be set to root directory
if (DBrec.s=='1') document.title='♥ '+document.title; //Indicate if the image has been marked as saved before
title=document.title;
};
function buildTable(ansi, rest) { //Create table of untranslated tags for manual translation input
tb.show();
options='';
tbd=tb[0].appendChild(document.createElement('tbody'));
$.each(ansi, function(i,v){ //First process the unassigned roman tags
row1=tbd.insertRow(0);
cell1=row1.insertCell(0);
cell1.id=i;
swp='<input type="button" value="swap" id="swap" />'
cell1.innerHTML=tagcell+i+'</a><br>'+swp+'</td></tr></table>';
if (i.split(' ').length!=2) //For roman tags consisting of 2 words enable button for swapping their order
$(cell1).find('input#swap').attr('disabled','disabled'); // script can't know which name/surname order is correct so the choice is left to user
$(cell1).attr('class','cell ansi');
$(cell1).find('input[type="radio"]').attr('name',i);
options='<option value="'+i+'"></option>'+options; //Populate the drop-down selection lists with these tags
$(cell1).find('input#swap').on('click',function(){swap(this);});
}); // so they can be used for translating kanji tags if possible
$.each(rest, function(i,v){ //Now come the untranslated kanji tags
row1=tbd.insertRow(0);
cell1=row1.insertCell(0);
cell1.id=v;
cell1.innerHTML=tagcell+v+'</a><br><input list="translation" size=10 class="txt"/>\
<datalist id="translation">'+options+'</datalist></td></tr></table>';
$(cell1).attr('class','cell kanji');
$(cell1).find('input[type="radio"]').attr('name',v); //In case the blogger provided both roman tag and kanji tag for names,
}); // the user can simply select one of roman tags for every kanji tag as translation
// to avoid typing them in manually. Ain't that cool?
$.each($('a.ignr'),function(i,v){v.onclick=function(){ignor3(this);};});
};
function ignor3(anc){ //Remove clicked tag from results for current session (until page reload)
ignore.push(anc.textContent); // this way you don't have to fill in the "ignore" list,
// while still being able to control which tags will be counted
tdc=$(anc).parent().parent().parent().parent().parent().parent(); //a long way up from tag link to tag cell table
tdc.attr('hidden','hidden');
tdc.attr('ignore','ignore');
$.each($('datalist').find('option'), function(i,v){ //Hide these tags from the drop-down lists of translations too
if (v.value==anc.textContent)
v.parentNode.removeChild(v);
}
);
};
function swap(txt){ //Swap roman tags consisting of 2 words
data=$('datalist'); // these are most likely the names so they can have different writing orders
set=[];
theTag=$(txt).prev().prev()[0];
$.each(data.find('option'), function(i,v){
if (v.value==theTag.textContent)
set.push(v); //Collect all options from drop-down lists containing the tag to be swapped
}
);
swapped=theTag.textContent.split(' ').reverse().join(' ');
theTag.textContent=swapped;
tdc=$(txt).parent().parent().parent().parent().parent(); //Change ids of tag cells as well
tdc.prop('swap',!tdc.prop('swap')); //mark node as swapped
$.each(set,function(i,v){
v.value=swapped; //apply changes to the quick selection lists too
}
);
};
function selected(e){ //Hide the corresponding roman tag from results when it has been selected
$(e.target).css('background-color','');
ansi=$('td.ansi'); // as a translation for kanji tag
kanji=$('td.kanji').find('input.txt'); //that's not a filename, fyi
knj={};
$.each(kanji,function(i,v){
knj[v.value]=true;
$.each(ansi,function(ix,vl){ //Have to show a previously hidden tag if another was selected
if (vl.textContent.trim()==v.value.trim())
$(vl).parent().attr('hidden','hidden');
});
});
$.each(ansi,function(ix,vl){
if ((!knj.hasOwnProperty(vl.textContent.trim()))&&(!$(vl).parent().attr('ignore')))
$(vl).parent().removeAttr('hidden');
});
var test={tag:e.target.value};
checkMatch(test, true);
if (test.tag!=e.target.value) {
$(e.target).css('background-color','#ffff00');
e.target.value=test.tag;
}
}
function mkUniq(arr){ //Sorts an array and ensures uniqueness of its elements
to={};
$.each(arr, function(i,v){
to[v.toLowerCase()]=true;});
arr2=Object.keys(to);
return arr2.sort(); //I thought key names are already sorted in an object but for some reason they're not
};
function getFileName(fullName, full){ //Source URL processing for filename
full=full || false;
fullName=fullName.replace(/(#|\?).*$/gim,''); //first remove url parameters
if (fullName.indexOf('xuite')!=-1) { //This blog names their images as "(digit).jpg" causing filename collisions
i=fullName.lastIndexOf('/');
fullName=fullName.substr(0,i)+'-'+fullName.substr(i+1); // add parent catalog name to the filename to ensure uniqueness
}
else if ((fullName.indexOf('amazonaws')!=-1)&&(!full)) //Older tumblr images are weirdly linked via some encrypted redirect to amazon services,
fullName=fullName.substring(0,fullName.lastIndexOf('_')-2); // where links only have a part of the filename without a few last symbols and extension,
// have to match it here as well, but we need full filename for downloadify, thus the param
if ((fullName.indexOf('tumblr_')!=-1)&&!full)
fullName=fullName.replace(/(tumblr_)|(_\d{2}\d{0,2})(?=\.)/gim,'');
fullName=fullName.replace(/\\/g,'/'); //Function is used both for URLs and folder paths which have opposite slashes
return fullName.split('/').pop();
};
function dl(base64data){ //Make downloadify button with base64 encoded image file as parameter
// which will both cause save file dialog with custom filename and copy save path to clipboard
Downloadify.create( 'down' ,{
filename: function(){ return filename;}, //is this called "stateless"?
data: base64data,
dataType: 'base64',
downloadImage: '//dl.dropboxusercontent.com/u/74005421/js%20requisites/downloadify.png',
onError: function(){ throw new Error('Downloadify error');},
onComplete: onCmplt,
swf: downloadifySwf,
width: 100,
height: 30,
transparent: true,
append: true,
textcopy: function(){ if (DBrec) {return folder+filename;} else return '';}
}); //If no database record is found, don't change the clipboard
};
function onCmplt(){ //Mark image as saved in the tag database
if (DBrec) { // it is used to mark saved images on tumblr pages
DBrec.s='1';
tagsDB.set(getFileName(document.location.href), JSON.stringify(DBrec));
document.title='♥ '+document.title; //Actually I wanted to put a diskette symbol there,
}; // but because chromse sucks it does not support extended unicode in title
}
function submit(){ //Collects entered translations for missing tags
tgs=$('td.cell'); //saves them to databases and relaunches tag analysis with new data
$('input.category').parent().parent().css("background-color","");
missing=false;
$.each(tgs,function(i,v){
if ($(v).parent().attr('ignore')) {
ignore.push(v.id); //Mark hidden tags as ignored
return true;
};
if ($(v).parent().attr('hidden'))
return true;
tg=$(v).find('input.txt');
if (tg.length)
tg=tg[0].value.trim(); //found translation tag
else {
tg=v.textContent.trim(); //found roman tag
if ($(v).prop('swap')) {
t=DBrec.t.replace(tg.split(' ').reverse().join(' '),tg);
DBrec.t=t; //Apply swap changes to the current taglist
};
} //TODO: add checks for existing entries in another DB?
cat=$(v).find('input.category');
if (tg.length){
if (!isANSI(tg)&&!allowUnicode) {
$(v).find('input.txt').css("background-color","#ffb080");
missing=true; //Indicate unicode characters in user input
}
else if (cat[0].checked) //name category was selected for this tag
names.set(v.textContent.trim().toLowerCase(),tg)
else if (cat[1].checked) //meta category was selected
meta.set(v.textContent.trim().toLowerCase(), tg)
else { //no category was selected, indicate missing input
$(cat[0].parentNode.parentNode ).css("background-color","#ff8080");
missing=true;
}
}
else {
$(v).find('input.txt').css("background-color","#ff8080");
missing=true; //no translation was provided, indicate missing input
return true;
}
}
);
tbd=$('#translations > tbody')[0];
if (!missing){
tbd.parentNode.removeChild(tbd);
tb.hide();
analyzeTags();
};
};
function ex(){ //Export auxiliary tag databases as a text file
Downloadify.create('ex' ,{
filename: 'names&meta tags DB.txt',
data: function(){
xport={names:names.getAll(), meta:meta.getAll()};
return JSON.stringify(xport, null, '\t');
},
dataType:'string',
downloadImage: '//dl.dropboxusercontent.com/u/74005421/js%20requisites/downloadify2.png',
onError: function(){ throw new Error('Downloadify2 error');},
swf: downloadifySwf,
width: 100,
height: 30,
transparent: true,
append: false,
textcopy: ''
});
$('a.exim')[0].removeAttribute('onclick');
$('a#aex')[0].textContent='';
};
function im(){ //Import auxiliary tag databases as text file
$('#im').append('<input type="file" id="files" style="width:97px;" accept="text/plain"/>');
$('input#files')[0].onchange=handleFileSelect;
$('a.exim')[1].removeAttribute('onclick');
$('a#aim')[0].textContent='';
};
function handleFileSelect(evt) { //Fill in databases with data from imported file
var file = evt.target.files[0];
$('input#files')[0].value='';
if (file.type!='text/plain') {
alert('Wrong filetype: must be text');
return false;
};
var reader = new FileReader();
reader.onloadend = function(e) {
clear=confirm('Would you like to clear existing databases before importing?');
try {
o=JSON.parse(e.target.result);
} catch(err){
alert('Error: '+err.message);
return false;
};
if (o.meta) {
checkMatch(o.meta);
if (clear)
meta.clearAll();
$.each(o.meta, function(i,v){
meta.set(i,v);});
}
else
alert('No meta DB found');
if (o.names) {
checkMatch(o.names);
if (clear)
names.clearAll();
$.each(o.names, function(i,v){
names.set(i,v);});
}
else
alert('No names DB found');
};
reader.readAsText(file);
};
//TODO: add save button activation via keyboard
//TODO: improve the button: open assigned folder directly, use modern dialog
//TODO: ^ try to set last used directory in flash save dialog so as to avoid clipboard usage
//TODO: add fallback to the tumblr hosted image if link url fails (requires storing post id and blog name)
//TODO: add checks for common mistakes in unicode names like 実/美 & 奈/菜
//TODO: option to disable unsorted category if translations are not required by user