/* ===== FOLLOG(相互フォロー管理ツール)JavaScript(WordPressのJS欄に貼り付け) ===== */ (function(){ 'use strict'; function init(){ /* ================================================== ★ロゴ設定欄★ 各SNSの公式ロゴ画像(WordPressメディアにアップしたもの)のURLを入れると、 文字バッジの代わりにロゴが表示されます。空欄なら色付きの文字バッジになります。 ================================================== */ var SNS_ICONS = { x: '', note: '', instagram: '', threads: '' }; var root = document.getElementById('mft'); if(!root) return; var STORAGE_KEY = 'mft_mutual_follow_v2'; var OLD_KEY = 'mft_mutual_follow_v1'; var UNF_KEEP_DAYS = 3; var MEMO_MAX = 30; var PAGE = 100; var $ = function(id){ return document.getElementById(id); }; function each(sel, fn){ Array.prototype.forEach.call(root.querySelectorAll(sel), fn); } function onEnter(el, fn){ el.addEventListener('keydown', function(e){ if(e.key==='Enter' && !e.isComposing) fn(); }); } /* ---------- SNS定義 ---------- */ var SNS = { x: { name:'X', host:'x.com', ph:'https://x.com/xxxx' }, note: { name:'note', host:'note.com', ph:'https://note.com/xxxx' }, instagram: { name:'Instagram', host:'instagram.com', ph:'https://www.instagram.com/xxxx' }, threads: { name:'Threads', host:'threads.com', ph:'https://www.threads.com/@xxxx' }, other: { name:'その他', host:'', ph:'https://〜(相手のプロフィールURL)' } }; var SNS_ORDER = ['x','note','instagram','threads','other']; var HOST_TO_SNS = { 'x.com':'x', 'note.com':'note', 'instagram.com':'instagram', 'threads.com':'threads' }; var HOST_NAME = { 'youtube.com':'YouTube', 'tiktok.com':'TikTok', 'facebook.com':'Facebook', 'stand.fm':'stand.fm', 'bsky.app':'Bluesky' }; function esc(s){ return String(s).replace(/[&<>"']/g, function(c){ return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); } function snsBadge(key, lg){ var k = SNS[key] ? key : 'other', s = SNS[k], icon = SNS_ICONS[k]; if(icon) return '' + esc(s.name) + ''; return '' + esc(k==='other' ? '他' : s.name) + ''; } function hostBadge(host){ var k = HOST_TO_SNS[host]; if(k) return snsBadge(k); return '' + esc(HOST_NAME[host] || host) + ''; } /* ---------- 日付(ローカル日付で計算) ---------- */ function pad(n){ return (n<10?'0':'')+n; } function today(){ var d=new Date(); return d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate()); } function toUTC(s){ var p=String(s).split('-'); return Date.UTC(+p[0], +p[1]-1, +p[2]); } function daysSince(s){ return Math.round((toUTC(today()) - toUTC(s)) / 86400000); } function fmt(s){ var p=String(s).split('-'); return (+p[1])+'/'+(+p[2]); } function isDate(s){ return /^\d{4}-\d{2}-\d{2}$/.test(s||''); } /* ---------- URLの正規化 ---------- */ var HOST_ALIAS = { 'twitter.com':'x.com', 'mobile.twitter.com':'x.com', 'threads.net':'threads.com' }; var normCache = {}; function dec(s){ try{ return decodeURIComponent(s); }catch(e){ return s; } } function normalize(input){ var s = String(input||'').trim(); if(!s) return null; if(normCache[s]) return normCache[s]; var raw = s; if(!/^https?:\/\//i.test(s)) s = 'https://' + s.replace(/^\/+/, ''); var u; try{ u = new URL(s); }catch(e){ return null; } var host = u.hostname.toLowerCase().replace(/^(www|m)\./, ''); if(host.indexOf('.')<0) return null; host = HOST_ALIAS[host] || host; var path = u.pathname.replace(/\/+$/, ''); var segs = path.split('/').filter(Boolean); var first = segs[0] ? dec(segs[0]) : ''; var label; if(!first) label = host; else if(host==='bsky.app' && segs[0]==='profile' && segs[1]) label = '@'+dec(segs[1]); else label = first.charAt(0)==='@' ? first : (/^(x\.com|instagram\.com|threads\.com|tiktok\.com)$/.test(host) ? '@'+first : first); var r = { url:'https://'+host+path, key:(host+path).toLowerCase(), host:host, label:label }; normCache[raw] = r; normCache[r.url] = r; return r; } /* ---------- データ構造 ---------- 保存形式(容量を抑えるため短いキー): { v:3, c:今のリスト番号, b:最終バックアップ日, L:[ { n:リスト名, p:SNS, d:目安日数, t:[ {u:URL, d:フォロー日, s:'w'|'m'|'u', m:相互日, x:解除日, n:メモ, k:1(キープ)} ] } ] } */ var seq = 0; function nid(){ return 'i' + (++seq); } function newList(name, sns, days){ return { id:nid(), n:name, p:(SNS[sns]?sns:'other'), d:days||7, t:[] }; } function emptyState(){ return { c:0, b:null, L:[] }; } function cleanItem(it){ if(!it) return null; var n = normalize(it.u || it.url); if(!n) return null; var s = it.s || ({waiting:'w', mutual:'m', unfollowed:'u'})[it.status] || 'w'; if(s!=='m' && s!=='u') s = 'w'; var o = { id:nid(), u:n.url, d: isDate(it.d||it.followedAt) ? (it.d||it.followedAt) : today(), s:s }; var m = it.m || it.mutualAt; if(s==='m' && isDate(m)) o.m = m; var x = it.x || it.unfollowedAt; if(s==='u') o.x = isDate(x) ? x : today(); var memo = typeof it.n==='string' ? it.n : (typeof it.memo==='string' ? it.memo : ''); memo = memo.trim().slice(0, MEMO_MAX); if(memo) o.n = memo; if(it.k) o.k = 1; return o; } // v3・v2・v1(旧試作版)どの形式も受け付ける function sanitize(obj){ if(!obj || typeof obj!=='object') return null; var s = emptyState(); if(Array.isArray(obj.L)){ obj.L.forEach(function(l){ if(!l || !Array.isArray(l.t)) return; var d = parseInt(l.d,10); var list = newList(String(l.n||'リスト').slice(0,20), l.p, (d>=1&&d<=365)?d:7); l.t.forEach(function(it){ var c = cleanItem(it); if(c) list.t.push(c); }); s.L.push(list); }); // v2で自動作成された空の「メインリスト」は捨てて、はじめての画面から始める if(!(obj.v>=3)){ s.L = s.L.filter(function(l){ return !(l.n==='メインリスト' && !l.t.length); }); } var c = parseInt(obj.c,10); s.c = (c>=0 && c=1&&d1<=365)?d1:7); obj.items.forEach(function(it){ var c = cleanItem(it); if(c) list1.t.push(c); }); if(list1.t.length) s.L.push(list1); s.b = isDate(obj.lastBackup) ? obj.lastBackup : null; }else return null; return s; } function serialize(st){ return { v:3, c:st.c, b:st.b, L: st.L.map(function(l){ return { n:l.n, p:l.p, d:l.d, t: l.t.map(function(it){ var o = { u:it.u, d:it.d, s:it.s }; if(it.m) o.m = it.m; if(it.x) o.x = it.x; if(it.n) o.n = it.n; if(it.k) o.k = 1; return o; })}; })}; } /* ---------- 保存 ---------- */ var storageOK = true; function load(){ try{ var raw = localStorage.getItem(STORAGE_KEY); if(raw) return sanitize(JSON.parse(raw)) || emptyState(); var old = localStorage.getItem(OLD_KEY); if(old){ var s = sanitize(JSON.parse(old)); if(s) return s; } }catch(e){ storageOK = false; } return emptyState(); } function save(){ try{ localStorage.setItem(STORAGE_KEY, JSON.stringify(serialize(state))); storageOK = true; } catch(e){ storageOK = false; } } /* ---------- 状態 ---------- */ var state = load(); var tab = 'w', overOnly = false, shown = PAGE; var openId = null, delItemId = null, delTimer = null, openPanel = null; function cur(){ return state.L[state.c]; } function isOver(it, list){ return it.s==='w' && !it.k && daysSince(it.d) >= list.d; } function purge(){ var changed = false; state.L.forEach(function(l){ var n = l.t.length; l.t = l.t.filter(function(it){ return !(it.s==='u' && it.x && daysSince(it.x) >= UNF_KEEP_DAYS); }); if(l.t.length!==n) changed = true; }); if(changed) save(); } function findItem(id){ var t = cur().t; for(var i=0;i' + esc(k==='other' ? 'その他(SNSを混ぜてOK)' : SNS[k].name) + ''; }).join(''); function sync(){ var k = sel.value; var prev = root.querySelector('[data-sns-preview="' + selId + '"]'); if(prev) prev.innerHTML = snsBadge(k, true); var suggest = k==='other' ? '' : SNS[k].name; if(!name.value.trim() || name.value===auto){ name.value = suggest; } auto = suggest; name.placeholder = k==='other' ? '例:YouTube' : '例:' + SNS[k].name + '本垢'; } sel.addEventListener('change', sync); return { reset:function(k){ sel.value = SNS[k] ? k : 'x'; name.value=''; auto=''; sync(); } }; } var startForm = setupSnsSelect('mft-start-sns', 'mft-start-name'); var newForm = setupSnsSelect('mft-new-sns', 'mft-new-name'); function createList(sns, name){ name = String(name||'').trim().slice(0,20); if(!name){ msg('リスト名を入力してください','err'); return false; } state.L.push(newList(name, sns, 7)); state.c = state.L.length - 1; save(); return true; } /* ---------- 追加 ---------- */ function addItem(){ var list = cur(), urlEl = $('mft-url'), memoEl = $('mft-memo'), dateEl = $('mft-date'); var n = normalize(urlEl.value); if(!n){ msg('URLの形式を確認してください(例:' + SNS[list.p].ph + ')', 'err'); urlEl.focus(); return; } // SNS指定のリストには、そのSNSのURLしか登録できない(「その他」は何でもOK) if(list.p!=='other' && SNS[list.p].host!==n.host){ var k = HOST_TO_SNS[n.host], other = k ? SNS[k].name : (HOST_NAME[n.host] || n.host); var actions = []; if(k){ state.L.forEach(function(l, i){ if(l.p===k && actions.length<3) actions.push({ label:'「' + l.n + '」に追加する', fn:function(){ state.c = i; save(); clearMsg(); render(); addItem(); } }); }); if(!actions.length) actions.push({ label:'+ ' + other + '用のリストを作る', fn:function(){ clearMsg(); showPanel('new'); newForm.reset(k); $('mft-new-name').focus(); } }); }else{ state.L.forEach(function(l, i){ if(l.p==='other' && actions.length<3) actions.push({ label:'「' + l.n + '」に追加する', fn:function(){ state.c = i; save(); clearMsg(); render(); addItem(); } }); }); if(!actions.length) actions.push({ label:'+「その他」のリストを作る', fn:function(){ clearMsg(); showPanel('new'); newForm.reset('other'); $('mft-new-name').focus(); } }); } msg('「' + list.n + '」は ' + SNS[list.p].name + ' 専用のリストです。' + other + ' のURLは登録できません', 'err', actions); return; } var dup = list.t.filter(function(it){ return normalize(it.u).key===n.key; })[0]; if(dup){ var where = dup.s==='m' ? '相互' : (dup.s==='u' ? '解除済み' : 'フォロー待ち'); msg('「' + list.n + '」に登録済みです(' + where + '・' + fmt(dup.d) + 'にフォロー)', 'err'); return; } var d = (isDate(dateEl.value) && toUTC(dateEl.value) <= toUTC(today())) ? dateEl.value : today(); var it = { id:nid(), u:n.url, d:d, s:'w' }; var memo = memoEl.value.trim().slice(0, MEMO_MAX); if(memo) it.n = memo; list.t.push(it); save(); urlEl.value=''; memoEl.value=''; dateEl.value=today(); msg('「' + n.label + '」を「' + list.n + '」のフォロー待ちに追加しました', 'ok'); setTab('w'); urlEl.focus(); } /* ---------- 描画 ---------- */ function rowHTML(it, list){ var n = normalize(it.u); var cls = 'mft-item', meta, badge, act; var days = daysSince(it.d); if(it.s==='w'){ var over = isOver(it, list); if(over) cls += ' is-over'; meta = '' + fmt(it.d) + '・' + days + '日' + (it.k ? '' : (over ? ' 超過' : '(あと' + (list.d - days) + '日)')); badge = ''; act = ''; }else if(it.s==='m'){ meta = '' + fmt(it.d) + '→' + (it.m ? fmt(it.m) : ''); badge = ''; act = ''; }else{ cls += ' is-unf'; var remain = Math.max(UNF_KEEP_DAYS - daysSince(it.x), 1); meta = '' + fmt(it.x) + '解除・あと' + remain + '日'; badge = '解除済'; act = ''; } // 「その他」リスト、またはリストのSNSと違うURLのときだけSNSバッジを表示 var showBadge = list.p==='other' || SNS[list.p].host!==n.host; var panel = ''; if(openId===it.id){ var armed = delItemId===it.id; panel = '
' + '' + fmt(it.d) + 'にフォロー' + (it.s==='m' && it.m ? '・' + fmt(it.m) + 'に相互' : '') + (it.s==='u' ? '・' + fmt(it.x) + 'に解除' : '') + '' + '' + '' + '' + '
'; } return '
  • ' + '
    ' + '' + '' + (showBadge ? hostBadge(n.host) : '') + '' + esc(n.label) + '' + (it.n ? '' : '') + '' + '' + meta + '' + badge + act + '' + '
    ' + panel + '
  • '; } function renderListSelect(){ $('mft-list-sel').innerHTML = state.L.map(function(l, i){ var w = 0, o = 0; l.t.forEach(function(it){ if(it.s==='w') w++; if(isOver(it,l)) o++; }); // リスト名にSNS名が入っていなければ[X]のように補足する var tag = (l.p==='other' || l.n.toLowerCase().indexOf(SNS[l.p].name.toLowerCase())>=0) ? '' : '[' + SNS[l.p].name + ']'; return ''; }).join(''); $('mft-cur-badge').innerHTML = snsBadge(cur().p, true); } // リスト管理パネル(開くのは常に1つだけ) function showPanel(name){ openPanel = name || null; each('[data-pnl]', function(p){ p.hidden = p.getAttribute('data-pnl')!==openPanel; }); each('[data-panel]', function(b){ b.classList.toggle('is-active', b.getAttribute('data-panel')===openPanel); }); if(!openPanel || !cur()) return; var list = cur(); if(openPanel==='new'){ newForm.reset(); $('mft-new-name').focus(); } if(openPanel==='rename'){ $('mft-rename').value = list.n; $('mft-rename').focus(); $('mft-rename').select(); } if(openPanel==='del'){ $('mft-del-text').textContent = '「' + list.n + '」(' + list.t.length + '件)を削除します。元に戻せないので、必要なら先にバックアップを保存してください。'; } } function render(){ purge(); var hasList = state.L.length > 0; $('mft-start').hidden = hasList; $('mft-main').hidden = !hasList; if(!hasList) return; if(state.c >= state.L.length) state.c = 0; var list = cur(); renderListSelect(); $('mft-days').value = list.d; $('mft-url').placeholder = SNS[list.p].ph; var counts = { w:0, m:0, u:0, all:list.t.length }, over = 0; list.t.forEach(function(it){ counts[it.s]++; if(isOver(it,list)) over++; }); $('mft-s-wait').textContent = counts.w; $('mft-s-over').textContent = over; $('mft-s-mutual').textContent = counts.m; each('[data-count]', function(el){ el.textContent = '(' + counts[el.getAttribute('data-count')] + ')'; }); $('mft-over-only').classList.toggle('is-on', overOnly); $('mft-over-only').style.display = (tab==='w' || tab==='all') ? '' : 'none'; var q = $('mft-search').value.trim().toLowerCase(); var rows = list.t.filter(function(it){ if(tab!=='all' && it.s!==tab) return false; if(overOnly && (tab==='w'||tab==='all') && !isOver(it,list)) return false; if(q && (it.u + ' ' + (it.n||'')).toLowerCase().indexOf(q) < 0) return false; return true; }); rows.sort(function(a,b){ if(tab==='w'){ // キープは下へ、それ以外は新しい順(最近フォローした相手が上) if(!!a.k !== !!b.k) return a.k ? 1 : -1; return a.d > b.d ? -1 : a.d < b.d ? 1 : 0; } var ka = tab==='m' ? (a.m||a.d) : tab==='u' ? (a.x||'') : a.d; var kb = tab==='m' ? (b.m||b.d) : tab==='u' ? (b.x||'') : b.d; return ka > kb ? -1 : ka < kb ? 1 : 0; }); var ul = $('mft-list'); if(!rows.length){ var empty = (q || overOnly) ? '該当するアカウントはありません' : { w:'フォロー待ちのアカウントはありません', m:'相互フォローのアカウントはまだありません', u:'解除済みのアカウントはありません', all:'まだ登録がありません' }[tab]; if(!list.t.length) empty = 'まだ登録がありません。フォローしたら、相手のURLを上の欄に貼って「追加」してください'; ul.innerHTML = '
  • ' + empty + '
  • '; }else{ var html = rows.slice(0, shown).map(function(it){ return rowHTML(it, list); }).join(''); if(rows.length > shown){ html += '
  • '; } ul.innerHTML = html; } var note = $('mft-backup-note'), total = totalCount(state); if(!storageOK){ note.className = 'mft-note is-warn'; note.textContent = 'ブラウザへの自動保存ができませんでした。閉じる前に必ずバックアップを保存してください'; }else if(total && (!state.b || daysSince(state.b) >= 7)){ note.className = 'mft-note is-warn'; note.textContent = state.b ? '最終バックアップ:' + fmt(state.b) + '(1週間以上前です。そろそろ保存しておきましょう)' : 'まだバックアップがありません。念のため保存しておきましょう'; }else{ note.className = 'mft-note'; note.textContent = (state.b ? '最終バックアップ:' + fmt(state.b) + ' ' : '') + 'すべてのリストをまとめて1ファイルに保存します'; } } function setTab(t){ tab = t; shown = PAGE; openId = null; each('.mft-tab', function(el){ el.classList.toggle('is-active', el.getAttribute('data-tab')===t); }); render(); } /* ---------- はじめての画面 ---------- */ $('mft-start-btn').addEventListener('click', function(){ var name = $('mft-start-name').value; if(createList($('mft-start-sns').value, name)){ setTab('w'); msg('リスト「' + name.trim() + '」を作成しました。フォローした相手のURLを貼って追加しましょう','ok'); $('mft-url').focus(); } }); onEnter($('mft-start-name'), function(){ $('mft-start-btn').click(); }); /* ---------- リスト管理 ---------- */ root.querySelector('.mft-bar-btns').addEventListener('click', function(e){ var b = e.target.closest('[data-panel]'); if(!b) return; var name = b.getAttribute('data-panel'); showPanel(openPanel===name ? null : name); }); each('[data-panel-close]', function(b){ b.addEventListener('click', function(){ showPanel(null); }); }); $('mft-new-btn').addEventListener('click', function(){ var name = $('mft-new-name').value; if(createList($('mft-new-sns').value, name)){ showPanel(null); setTab('w'); msg('リスト「' + name.trim() + '」を作成しました','ok'); } }); onEnter($('mft-new-name'), function(){ $('mft-new-btn').click(); }); $('mft-rename-btn').addEventListener('click', function(){ var name = $('mft-rename').value.trim().slice(0,20); if(!name){ msg('新しい名前を入力してください','err'); return; } cur().n = name; save(); showPanel(null); render(); msg('リスト名を「' + name + '」に変更しました','ok'); }); onEnter($('mft-rename'), function(){ $('mft-rename-btn').click(); }); $('mft-del-btn').addEventListener('click', function(){ var list = cur(); state.L.splice(state.c, 1); state.c = 0; save(); showPanel(null); setTab('w'); if(state.L.length){ msg('リスト「' + list.n + '」を削除しました','ok'); } else { startForm.reset(); msg('リスト「' + list.n + '」を削除しました。新しいリストを作りましょう','ok'); } }); $('mft-list-sel').addEventListener('change', function(){ state.c = parseInt(this.value,10) || 0; save(); showPanel(null); setTab('w'); }); $('mft-days').addEventListener('change', function(){ var d = parseInt(this.value,10); cur().d = (d>=1 && d<=365) ? d : 7; save(); render(); }); /* ---------- 行の操作 ---------- */ function saveMemo(it, li){ var mi = li.querySelector('[data-memo-input]'); var v = mi ? mi.value.trim().slice(0, MEMO_MAX) : ''; if(v) it.n = v; else delete it.n; save(); msg('メモを保存しました', 'ok'); render(); } $('mft-list').addEventListener('click', function(e){ if(e.target.closest('[data-more-list]')){ shown += PAGE; render(); return; } var btn = e.target.closest('[data-act]'); if(!btn) return; var li = btn.closest('[data-id]'); if(!li) return; var it = findItem(li.getAttribute('data-id')); if(!it) return; var act = btn.getAttribute('data-act'); var label = normalize(it.u).label; if(act!=='delete') delItemId = null; switch(act){ case 'mutual': it.s='m'; it.m=today(); delete it.x; msg('「'+label+'」と相互フォローになりました 🎉','ok'); break; case 'unmutual': it.s='w'; delete it.m; msg('「'+label+'」をフォロー待ちに戻しました','ok'); break; case 'unfollow': it.s='u'; it.x=today(); msg('「'+label+'」を解除済みにしました(3日間は取り消せます)','ok'); break; case 'restore': it.s='w'; delete it.x; msg('「'+label+'」の解除を取り消しました','ok'); break; case 'keep': if(it.k){ delete it.k; msg('「'+label+'」のキープを外しました','ok'); } else { it.k = 1; msg('「'+label+'」をキープしました(解除の目安の対象外)','ok'); } break; case 'more': openId = (openId===it.id ? null : it.id); render(); return; case 'memo-save': saveMemo(it, li); return; case 'delete': if(delItemId===it.id){ cur().t = cur().t.filter(function(x){ return x.id!==it.id; }); delItemId = null; openId = null; clearTimeout(delTimer); msg('「'+label+'」を削除しました','ok'); }else{ delItemId = it.id; clearTimeout(delTimer); delTimer = setTimeout(function(){ delItemId=null; render(); }, 3000); render(); return; } break; } save(); render(); }); $('mft-list').addEventListener('keydown', function(e){ if(e.target.hasAttribute && e.target.hasAttribute('data-memo-input') && e.key==='Enter' && !e.isComposing){ var li = e.target.closest('[data-id]'); var it = li && findItem(li.getAttribute('data-id')); if(it) saveMemo(it, li); } }); /* ---------- 追加・タブ・絞り込み ---------- */ root.querySelector('.mft-tabs').addEventListener('click', function(e){ var t = e.target.closest('[data-tab]'); if(t) setTab(t.getAttribute('data-tab')); }); $('mft-add').addEventListener('click', addItem); onEnter($('mft-url'), addItem); onEnter($('mft-memo'), addItem); $('mft-search').addEventListener('input', function(){ shown = PAGE; render(); }); $('mft-over-only').addEventListener('click', function(){ overOnly = !overOnly; shown = PAGE; render(); }); /* ---------- バックアップ(.txt) ---------- */ $('mft-export').addEventListener('click', function(){ state.b = today(); var total = totalCount(state); var body = '# FOLLOG(相互フォロー管理ツール)バックアップ(' + today() + ' 保存・' + state.L.length + 'リスト・' + total + '件)\n' + '# このファイルを「バックアップから復元」で読み込むと元の状態に戻せます。中身は編集しないでください。\n' + JSON.stringify(serialize(state)); var blob = new Blob([body], { type:'text/plain;charset=utf-8' }); var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'follog_' + today().replace(/-/g,'') + '.txt'; document.body.appendChild(a); a.click(); a.remove(); setTimeout(function(){ URL.revokeObjectURL(a.href); }, 1000); save(); render(); msg('バックアップを保存しました(' + state.L.length + 'リスト・' + total + '件)','ok'); }); each('[data-import]', function(b){ b.addEventListener('click', function(){ $('mft-file').click(); }); }); $('mft-file').addEventListener('change', function(){ var file = this.files && this.files[0], input = this; if(!file) return; var reader = new FileReader(); reader.onload = function(){ var text = String(reader.result||''), data = null, i = text.indexOf('{'); try{ data = sanitize(JSON.parse(i>=0 ? text.slice(i) : text)); }catch(e){ data = null; } input.value = ''; if(!data || !data.L.length){ msg('読み込めませんでした。このツールで保存したバックアップファイルを選んでください','err'); return; } var n = totalCount(data), m = totalCount(state); if(state.L.length && !window.confirm('バックアップ(' + data.L.length + 'リスト・' + n + '件)で、今のデータ(' + state.L.length + 'リスト・' + m + '件)を置き換えます。よろしいですか?')) return; state = data; save(); showPanel(null); setTab('w'); msg('バックアップから復元しました(' + data.L.length + 'リスト・' + n + '件)','ok'); }; reader.onerror = function(){ input.value=''; msg('ファイルを読み込めませんでした','err'); }; reader.readAsText(file, 'utf-8'); }); /* ---------- 初期化 ---------- */ $('mft-date').value = today(); $('mft-date').max = today(); startForm.reset(); render(); setInterval(render, 60 * 60 * 1000); // 開きっぱなしでも日付の経過を反映 } if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', init); }else{ init(); } })();