您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Format file name & save path for current image by its tags
// ==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