はい! こんにちはこんにちは!!
そんなわけでAIRアプリつくってみました!
tPod - tumblrのDashboardに流れる画像を眺めるガジェット
こんなやつです!
tumblrの自分のdashboardの画像が次々に流れてきます!
ちょっとかわいいよ!
tumblrなんて知らないよ…!
なんて人もこの機会に、ちょっと試してみたらどうでしょうか!
なんか気に入った画像や言葉を、みんなでどんどんクリップ(引用)していくだけのサービスで楽しいよ!
http://www.tumblr.com/
ここから、メールアドレス入力すれば、すぐ登録できるよ!
登録したら適当に、ぼくとか以下のような人たちを Follow しておけば、ダッシュボードに画像が流れてくるんじゃないかな!
http://hmcy.tumblr.com/
http://yuiseki.tumblr.com/
http://sui.tumblr.com/
http://acqua.tumblr.com/
http://ot-inc.tumblr.com/
http://onaho.tumblr.com/
http://gokujo.tumblr.com/
tPodのインストール
Install Now を押してね!
tPodのソースコード一式 (v0.84)
http://hamachiya.com/air/tpod/tpod_src084.zip
関連記事
バージョンアップ履歴
- v0.84 ... いつのまにかパースエラーで動かなくなってた(画像が表示されなくなってた)から修正
- v0.83 ... バージョンチェックの後、このページを開くときは標準ブラウザで開くように、tPodを終了させるようにした
- v0.82 ... 窓の位置が外側にハミ出た状態で終了すると、次回からちゃんと起動しなくなってた
- v0.81 ... バージョンチェックのところに余計なコードがあったからけした
- v0.8 ... 独自ドメインを設定してる人がdashboardに現れると止まってた。あと起動時にバージョンチェックするようにしたよ
- v0.7 ... 同じページをぐるぐる表示してた。あとreblogedの部分が表示されていない時があった
- v0.6 ... 起動してすぐに(tPodのロゴが出ている時に)windowのサイズを変えようとするとボタン類が見えなくなったりするバグを修正
- v0.5 ... 最初の公開バージョン
tPodのJavaScript部分のソースコード (800行くらい)
// v0.84 air.trace('start tPod.'); var tPod = { cfg: { versionCheckPage: 'http://hamachiya.com/air/tpod/version', installPage: 'http://d.hatena.ne.jp/Hamachiya2/20090119/tPod', tumblrRoot: 'http://www.tumblr.com', tumblrSave: 'http://www.tumblr.com/share?v=3', tumblrDashboard: 'http://www.tumblr.com/dashboard', checkLoginInterval: 180000, checkLoginInterval_nologin: 12000, changeInterval: 9200, requestInterval: 240000, page: 4, paddingHeight: 129 // メイン表示部以外に必要な縦幅 }, initData: [], initDataList: [], rawPosts: [], posts: [], formKey: null, login: null, elm: {}, settingWindow: null, checkLoginTimer: null, requestTimer: null, changeTimer: null, redrawTimer: null, retryTimer: null, version: new DOMParser().parseFromString(air.NativeApplication.nativeApplication.applicationDescriptor, "text/xml").getElementsByTagName('application')[0].getElementsByTagName("version")[0].firstChild.data }; air.trace(tPod.version); // tpod.iniファイルを読み込むよ readInitData(); // windowの位置と幅を復元するよ(なければデフォルトサイズ) positioning(200, 306); // default size document.observe('dom:loaded', initialize, false); function initialize() { tPod.elm = { loginStatus: $('loginStatus'), viewOuter: $('view'), view: $('main'), postInfo: $('postInfo'), message: $('message'), settingWindow: $('settingWindow'), btMain: $('btMain'), btBack: $('btBack'), btNext: $('btNext') }; // tpod.iniファイルから読み込んだ設定データをtPod.cfgにいれるよ setConfig(); // メイン表示部を正方形にするよ tPod.elm.viewOuter.style.height = tPod.elm.viewOuter.offsetWidth; // tPodのロゴ。じんわりだすよ new Effect.Appear($('tPodImage'), { from: 0.1, to: 1.0, delay: 1.2, // 開始までの秒数 fps: 60, duration: 3.2, beforeStartInternal: function(effect) { }, afterFinishInternal: function(effect) { // 一時停止・再生ボタンのイベント tPod.elm.btMain.addEventListener('click', hPause, false); } }); $('ver').innerHTML = tPod.version; checkVersion(); // ボタンのhover画像 // cssでやると何をどうやっても初回ちらつくからこっちでやるよ tPod.elm.btBack.addEventListener('mouseover', function() { tPod.elm.btBack.src = 'img/tri_l_hi.png'; }, false); tPod.elm.btBack.addEventListener('mouseout', function() { tPod.elm.btBack.src = 'img/tri_l.png'; }, false); tPod.elm.btNext.addEventListener('mouseover', function() { tPod.elm.btNext.src = 'img/tri_r_hi.png'; }, false); tPod.elm.btNext.addEventListener('mouseout', function() { tPod.elm.btNext.src = 'img/tri_r.png'; }, false); // 一度ログインチェックに入るとsetTimeoutで一定間隔ごとにチェックするよ checkLogin(); // dashboardの中身を定期的にtPod.cfg.page分とりにいくよ // とってきたデータ(html)はtPod.postsに格納するよ getDashboard(true); tPod.requestTimer = setInterval(getDashboard, tPod.cfg.requestInterval); // windowをリサイズされてもメイン表示部は正方形になるようにするよ window.addEventListener('resize', hResize, false); // 次々に画像を切り替えるやつだよ tPod.changeTimer = setInterval(changer, tPod.cfg.changeInterval); // 終了時にはwindowの位置とかをtpod.iniに書き出すよ nativeWindow.addEventListener('closing', saveInit, false); // 設定画面ひらくやつ $('openSettings').addEventListener('click', function() { openSettings(); tPod.elm.settingWindow.toggle(); }, false); $('cancel').addEventListener('click', function() { tPod.elm.settingWindow.hide(); }, false); $('save').addEventListener('click', function() { tPod.elm.settingWindow.hide(); saveSettings(); }, false); // デバッグ用のログを表示させるやつだよ $('viewLog').addEventListener('click', function() { if ($('log').style.display == 'none') { nativeWindow.height = Number(nativeWindow.height + 94); $('log').show(); } else { nativeWindow.height = Number(nativeWindow.height - 94); $('log').hide(); } }, false); } // 起動時に呼ばれるよ。 tpod.iniファイルを読み込む function readInitData() { air.trace('read tpod.ini'); // C:\Users\****\AppData\Roaming\Hamachiya2.tPod\Local Store\tpod.ini var file = new air.File('app-storage:/tpod.ini'); // air.trace(file.nativePath); var fs = new air.FileStream(); var data = ''; try { fs.open(file, air.FileMode.READ); var data = fs.readUTFBytes(fs.bytesAvailable); } catch (e) { } fs.close(); // air.trace(data); var dataRows = data.split('\n'); dataRows.map( function(v) { var temp = v.split('\t'); if (temp[1]) { // tPod.initData[temp[0]] = temp[1]; tPod.initData[temp[0]] = temp[1]; tPod.initDataList.push(temp[0]+'\t'+temp[1]); air.trace(temp[0] + '\t' + temp[1]); } } ); } // tpod.iniファイルから読み込んだデータがあればtPod.cfgにいれるよ function setConfig() { if (tPod.initData.changeInterval) { tPod.cfg.changeInterval = tPod.initData.changeInterval; } if (tPod.initData.requestInterval) { tPod.cfg.requestInterval = tPod.initData.requestInterval; } if (tPod.initData.bufferedPages) { tPod.cfg.page = tPod.initData.bufferedPages; } } // 起動時に呼ばれるよ。windowの位置や幅を復元 function positioning(w, h) { var xx, yy, ww, hh; if (tPod.initData.bounds) { // 外側にはみでてた時、マイナスが入ることも… if (tPod.initData.bounds.match(/x=(-{0,1}[\d\.]+)/)) { xx = RegExp.$1; } if (tPod.initData.bounds.match(/y=(-{0,1}[\d\.]+)/)) { yy = RegExp.$1; } if (tPod.initData.bounds.match(/w=([\d\.]+)/)) { ww = RegExp.$1; } if (tPod.initData.bounds.match(/h=([\d\.]+)/)) { hh = RegExp.$1; } } if (xx && yy && ww && hh) { nativeWindow.bounds = new air.Rectangle(xx, yy, ww, hh); } else { nativeWindow.width = w; nativeWindow.height = h; } // 最初はtpod.xmlで非表示(visible:false) にしてあるから、ここで初めて表示 nativeWindow.visible = true; } // settingsの画面のフォームのデフォルト選択状態を変更するよ function openSettings() { function setSelector(elm, cfg, sec) { var n = cfg; n = sec ? n / 1000 : n; for (var i=0; i<elm.length; i++) { if (elm[i].value == n) { elm[i].selected = true; break; } } } setSelector($('change'), tPod.cfg.changeInterval, true); setSelector($('request'), tPod.cfg.requestInterval, true); setSelector($('page'), tPod.cfg.page, false); } // settingsの画面のsave押されたあと function saveSettings() { var fChange = $F($('change')); if (fChange) { tPod.cfg.changeInterval = fChange * 1000; if (tPod.changeTimer) { clearTimeout(tPod.changeTimer); tPod.changeTimer = setInterval(changer, tPod.cfg.changeInterval); } } var fRequest = $F($('request')); if (fRequest) { tPod.cfg.requestInterval = fRequest * 1000; if (tPod.requestTimer) { clearTimeout(tPod.requestTimer); tPod.requestTimer = setInterval(getDashboard, tPod.cfg.requestInterval); } } var fPage = $F($('page')); if (fPage) { tPod.cfg.page = fPage; } saveInit(); } // tpod.iniファイルに、windowの位置とか色々書き出すよ function saveInit() { air.trace('save tpod.ini'); //air.trace(tPod.initDatalist); //air.trace(tPod.initDataList.length); //air.trace(tPod.initDataList[0]); // AIRアプリと同じ位置にiniファイルつくりたいけど… // var file = air.File.applicationDirectory.resolvePath('tpod.ini'); // セキュリティ回避しないと書けない // しかもアンインストール/再インストール時に問題が残る // file = new File(file.nativePath); // しかたないので、ややこしい位置に // C:\Users\****\AppData\Roaming\Hamachiya2.tPod\Local Store\tpod.ini var file = new air.File('app-storage:/tpod.ini'); var fs = new air.FileStream(); fs.open(file, air.FileMode.WRITE); fs.writeUTFBytes('bounds\t' + nativeWindow.bounds + '\n'); fs.writeUTFBytes('changeInterval\t' + tPod.cfg.changeInterval + '\n'); fs.writeUTFBytes('requestInterval\t' + tPod.cfg.requestInterval + '\n'); fs.writeUTFBytes('bufferedPages\t' + tPod.cfg.page + '\n'); fs.close(); } // 一時停止・再生ボタンで呼ばれるところ function hPause() { // load中(getDashboard)ならなにもしない if (tPod.elm.btMain.className == 'load') { return; } if (tPod.changeTimer) { // タイマーがあるってことは、一時停止したらいいの? clearTimeout(tPod.changeTimer); clearTimeout(tPod.requestTimer); tPod.changeTimer = null; message('»Pause', 1.8); tPod.elm.btMain.className = 'play'; } else { // タイマーがないなら、再生すべき? message('»Play', 1.8); changer(); tPod.requestTimer = setInterval(getDashboard, tPod.cfg.requestInterval); tPod.changeTimer = setInterval(changer, tPod.cfg.changeInterval); tPod.elm.btMain.className = 'pause'; } } // もどるボタン function hBack() { air.trace('go back'); // なんか境界ややこしい // air.trace('count:'+tPod.count); // air.trace('length:'+tPod.posts.length); // nullとかだったら0にするよ tPod.count -= 0; // とりあえず2くらい引いてみる。勘で。 var c = tPod.count - 2; // air.trace('a' + c); if (c == -1) { c = tPod.posts.length - 1; // air.trace('b' + c); } else if (c == -2) { c = tPod.posts.length - 2; // air.trace('c' + c); } if (c < 0) { c = 0; } tPod.count = c; // air.trace('changed:'+tPod.count); // air.trace(tPod.posts[c].image.src); // trueにするとエフェクト控えめ changer(true); } // すすむボタン function hNext() { air.trace('go go next'); changer(true); } // windowリサイズ時にviewが正方形になるように調整するやつ function hResize() { // 画像だしたままだと画像の横幅より小さくリサイズした時に比率が崩れちゃう var image; if (image = $('mainImage')) { image.style.display = 'none'; resizeWindow(); adjustImage(image); // ※リサイズ終了時に普通にshowしてもタイミングが合わずうまくいかない tPod.redrawTimer = setTimeout(function() { image.style.display = ''; // show }, 30); } else { resizeWindow(); } } function resizeWindow() { var vw = tPod.elm.viewOuter.offsetWidth; tPod.elm.viewOuter.style.height = vw; // windowの高さが変わってもpanelとかが隠れないように調整するよ var h = Number(vw + tPod.cfg.paddingHeight); if (h != nativeWindow.height) { nativeWindow.height = h; } } // だれそれさんのreblogとかでるとこ function info(str) { tPod.elm.postInfo.innerHTML = str; } // メインの画像表示の上にかぶせて Play とか Pause とかだすよ function message(str, t) { tPod.elm.message.innerHTML = str; tPod.elm.message.style.opacity = '0.85'; tPod.elm.message.show(); // tミリ秒後にじわりときえる if (t) { new Effect.Appear(tPod.elm.message, { from: 0.85, to: 0.1, delay: t, // 開始までの秒数 fps: 60, duration: 0.3, beforeStartInternal: function(effect) { }, afterFinishInternal: function(effect) { tPod.elm.message.innerHTML = ''; tPod.elm.message.hide(); } }); } } // dashboardのデータ取得してtPod.postsにどんどん格納するやつ // なんか配列とか色々ムダが多くてごめん function getDashboard(noMessage) { // ボタンのところ、ぐるぐるさせるよ tPod.elm.btMain.className = 'load'; air.trace('get dashboard ...'); if (tPod.retryTimer) { clearTimeout(tPod.retryTimer); tPod.retryTimer = null; } if (!tPod.login) { tPod.retryTimer = setTimeout(function() { getDashboard(noMessage); },5000); return; } noMessage || message('»Loading', 1); tPod.rawPosts = []; tPod.posts = []; // tPod.images = []; for (var i=0; i<tPod.cfg.page; i++) { (function(page) { air.trace('Request: ' + tPod.cfg.tumblrDashboard + '/' + page); var ajax = new Ajax.Request( tPod.cfg.tumblrDashboard + '/' + page + '?t=' + new Date(), { method: 'get', onComplete: function(xhr) { pageCollector(xhr, page, ajax.url); } } ); })(i+1-0); } } function pageCollector(xhr, page, url) { air.trace('success: ' + url.replace(/t=.+/, '')); /* var srcs = xhr.responseText.match(/src="(http:\/\/data.tumblr.com[^"]+)/g); tPod.images = tPod.images.concat(srcs); tPod.count = 0; */ //var xml = xhr.responseXML; var text = xhr.responseText; // 「&」の出現によるXMLパースエラー回避 // <img src="/images/logo.png?alpha&5" ←なんかこいつのせい? // scriptによるxmlパースエラー回避 text = text.replace(/\n/g, ''); text = text.replace(/<script.+?<\/script>/g, ''); //air.trace(text.match(/&[^#]\w*/g)); // これは不安だね。リンクのurlに「&」がそのままあった時とか… // それでもパースエラーになるかなー。まあいいか text = text.replace(/&[^#]\w*/g, ''); // 取得したhtmlテキストをDOMにしちゃうよ var parser = new DOMParser(); var xml = parser.parseFromString(text, 'text/xml'); //air.trace(xml.childNodes.length); //air.trace(xml.childNodes[0].nodeName); //air.trace(xml.getElementsByTagName('html')[0].innerHTML); // tPod.rawPosts[page-1] = tPod.rawPosts.concat($A(xml.getElementsByClassName('post'))); tPod.rawPosts[page-1] = $A(xml.getElementsByClassName('post')) || 1; // 全部のページが取れたかチェック var checkAll = true; for (var i=0; i<tPod.cfg.page; i++) { if (! tPod.rawPosts[i]) { air.trace('check page'+(i+1-0)+': NG'); checkAll = false; // break; } else { air.trace('check page'+(i+1-0)+': OK'); } } // 全ページ取れていたらなんかするよ if (checkAll) { // rawPostが [page][posts] ってなってるから // rawPosts[posts]になるようにするよ var temp = []; for (var i=0; i<tPod.rawPosts.length; i++) { temp = temp.concat(tPod.rawPosts[i]); } tPod.rawPosts = temp; // rawPosts を tPod.posts に入れなおすよ for (var i=0; i<tPod.rawPosts.length; i++) { if (tPod.rawPosts[i] == 1) { continue; } postCollector(i, tPod.rawPosts[i]); } air.trace('Posts: ' + tPod.rawPosts.length); air.trace('Images: ' + tPod.posts.length); message('»Buffer:' + tPod.posts.length, 1); tPod.count = 0; } } function postCollector(no, rawPost) { var image = rawPost.getElementsByClassName('image')[0]; var postInfo = rawPost.getElementsByClassName('post_info')[0]; var postControls = rawPost.getElementsByClassName('post_controls')[0]; var avatar = rawPost.getElementsByClassName('avatar')[0]; if (avatar.innerHTML) { // ***.tumblr.comの***をリンク文字にするよ if (avatar.href.match(/:\/\/(\w+)\.tumblr/)) { avatar.innerHTML = RegExp.$1; } else if (avatar.title) { // そっかtumblrって独自ドメインも使えるんだね… avatar.innerHTML = avatar.title; } else { avatar.innerHTML = ''; } } air.trace('image:' + (image ? 'OK' : 'NG') + ' info:' + (postInfo ? 'OK' : 'NG') + ' ctrl:' + (postControls ? 'OK' : 'NG') +' avatar:' + avatar); if (image) { air.trace('Check post['+(no+1-0)+']: image'); } else { air.trace('Check post'+(no+1-0)+': other'); } // 画像のpostだけためこむ if (image && postControls) { // post_infoのリンクtargetを変更するよ if (postInfo) { var a = postInfo.getElementsByTagName('a'); for (var x=0; x<a.length; x++) { a[x].target = '_blank'; } } // 同じユーザーが連続してpostしてる時 // postInfoは空になるから、かわりにavatar if (postInfo && postInfo.innerHTML) { postInfo = postInfo.innerHTML; } else if (avatar) { postInfo = '<a href="' + avatar.href + '" target="_blank">' + avatar.innerHTML + '</a>:'; } else { postInfo = ''; } tPod.posts.push({ postInfo: postInfo, image: image, postControls: postControls }); } } // 画像をぴこぴこ切り替えるやつ function changer(noEffect) { tPod.elm.btNext.className = 'busy'; tPod.elm.btNext.removeEventListener('click', hNext, false); tPod.elm.btBack.className = 'busy'; tPod.elm.btBack.removeEventListener('click', hBack, false); air.trace('start image:'+tPod.count + '/' + tPod.posts.length); if (tPod.count >= tPod.posts.length) { tPod.count = 0; } // 表示部が崩れて正方形じゃなくなってたらなおす if (tPod.elm.view.offsetWidth != tPod.elm.view.offsetHeight) { hResize(); } //var src = tPod.images[tPod.count].replace('src="', ''); var src = tPod.posts[tPod.count].image.src; var img = new Image(); img.src = src; img.id = 'mainImage'; img.className = 'mainImage'; // 画像の幅を取得したいからonloadまで待つよ img.addEventListener('load', function() { // imgのwidthとかheight調整するよ adjustImage(img); img.style.display = 'none'; var oldImg = tPod.elm.view.getElementsByTagName('img')[0]; // 何かの間違いでoldImgが無かった時はロゴでも入れとく if (!oldImg) { oldImg = new Image(); img.src = 'img/tpod.png'; tPod.elm.view.appendChild(oldImg); } if (tPod.elm.btMain.className == 'load') { tPod.elm.btMain.className = 'pause'; } // post_info表示するよ var str = tPod.posts[tPod.count].postInfo || ''; info(str + ' [' + (Number(tPod.count + 1)) + '/' + tPod.posts.length + ']'); air.trace('show image:'+tPod.count); // 画像表示(切り替え)するよ new Effect.Appear(oldImg, { from: 0.9, to: 0.1, delay: 0, // 開始までの秒数 fps: 60, duration: noEffect ? 0.1 : 0.3, beforeStartInternal: function(effect) { }, afterFinishInternal: function(effect) { tPod.elm.view.style.background = '#000'; oldImg.removeEventListener('click', reblog, false); // ここで画像が差し替わる tPod.elm.view.replaceChild(img, oldImg); new Effect.Appear(img, { from: 0.1, to: 1.0, delay: 0, // 開始までの秒数 fps: 60, duration: noEffect ? 0.1 : 0.6, beforeStartInternal: function(effect) { }, afterFinishInternal: function(effect) { tPod.elm.btNext.addEventListener('click', hNext, false); tPod.elm.btNext.className = ''; tPod.elm.btBack.addEventListener('click', hBack, false); tPod.elm.btBack.className = ''; img.addEventListener('click', reblog, false); } }); } }); // End of Effect tPod.count++; }, false); // End of img.onload } // 画面の幅にあわせて比率を崩さないように // imgのwidthとかheight調整するよ function adjustImage(img) { // 表示部の幅 var vw = tPod.elm.view.offsetWidth; var vh = tPod.elm.view.offsetHeight; // 画像の幅 var iw = img.naturalWidth; var ih = img.naturalHeight; // 最終的にimgにセットするwidthとheight var w = ''; var h = ''; if ( (vw < iw) && (vh < ih) ) { if (iw < ih) { h = vh; } else { w = vw; } } else { if (vw < iw) { w = vw; } else if (vh < ih) { h = vh; } } air.trace('v:' + vw + 'x' + vh + ' i:' + iw + 'x' + ih + ' r:' + (w ? w : '(null)') + 'x' + (h ? h : '(null)')); if (w) { img.width = w-2; } if (h) { img.height = h-2; } } // リブログ機能! 超かっこいいインターフェースで! って思ったけど // なんかもうめんどくさいからリンクひらくだけでいいや function reblog() { // クリックのタイミングによってはうまくいかないかも? // requestTimerでgetDashboard動いちゃって、tPod.postsが空だとか。 // air.trace(tPod.posts[tPod.count].postControls); var c = tPod.count - 1; if (c < 0) { c = tPod.count.length - 1; } var a = tPod.posts[c].postControls.getElementsByTagName('a'); var href; for (var i=0; i<a.length; i++) { if (a[i].href.match(/reblog/)) { href = a[i].href; break; } } if (href) { open(tPod.cfg.tumblrRoot + href, null, 'menubar=yes,toolbar=yes,location=yes,resizable=yes,scrollbars=yes'); } } // ログインしてるかチェックするやつ // 一応post用のkeyも取得しておくよ function checkLogin() { tPod.elm.loginStatus.className = ''; air.trace('check login ...'); var temp = tPod.elm.loginStatus.innerHTML; tPod.elm.loginStatus.innerHTML = '<span class="check">[Check login]</span>'; new Ajax.Request( tPod.cfg.tumblrSave, { method: 'get', onComplete: function(xhr) { if (xhr.responseText.match(/name="form_key"[^>]+value="([^"]+)"/i)) { tPod.formKey = RegExp.$1; tPod.login = true; //tPod.elm.loginStatus.style.display = 'none'; //air.trace(tPod.formKey); tPod.elm.loginStatus.innerHTML = '[<a href="http://www.tumblr.com/dashboard" target="_blank">»Dashboard</a>]'; tPod.elm.loginStatus.style.display = 'block'; //debug(xhr.responseText); } else { tPod.login = false; tPod.elm.loginStatus.innerHTML = '[Not logined → <a href="http://www.tumblr.com/login" target="_blank">Login</a>]'; tPod.elm.loginStatus.style.display = 'block'; } }, onFailure: function(xhr) { tPod.elm.loginStatus.innerHTML = temp; } } ); if (tPod.checkLoginTimer) { clearTimeout(tPod.checkLoginTimer); } var t = tPod.login ? tPod.cfg.checkLoginInterval : tPod.cfg.checkLoginInterval_nologin; tPod.checkLoginTimer = setTimeout(checkLogin, t); } // バージョンチェック function checkVersion() { air.trace('check version ...'); info('Checking version ...'); new Ajax.Request( tPod.cfg.versionCheckPage, { method: 'get', onComplete: function(xhr) { if (xhr.responseText.match(/^v([0-9a-zA-Z.]+)$/i)) { if (tPod.version < RegExp.$1) { if (confirm('New version v' + RegExp.$1 + ' available. (this version is v' + tPod.version + ')\nDo you want to open download page?')) { // 標準ブラウザでダウンロードページ開くよ air.navigateToURL(new air.URLRequest(tPod.cfg.installPage), "_blank"); // tPod終了するよ saveInit(); nativeWindow.close(); } } } info(''); }, onFailure: function(xhr) { info(''); } } ); } // デバッグログ function debug(str) { if (tPod.cfg.debug) { var d = $('log'); d.value = str + '\n' + d.value; } }