original-screenshots.js
'use strict';
(() => {
const W=window.WORKSPACE,ids=['sheet-05','sheet-06','sheet-07','sheet-08','sheet-09','sheet-10','sheet-11','risk-default','product-mix','fx-risk'];
const selected={
'sheet-06':{url:'assets/adopted-future-aum-20261029.webp',position:'50% 48%'},
'product-mix':{url:'assets/adopted-product-mix-20261029.webp',position:'50% 52%'}
};
const revision='photo-fill-20261029',photos=new Map(),sources=new Map(),sizes=new Map(),newerOriginals=new Set();let database;
const photoUrl=id=>newerOriginals.has(id)?photos.get(id):selected[id]?.url||photos.get(id);
const isSelected=id=>Boolean(selected[id]&&!newerOriginals.has(id));
const db=()=>database??=new Promise((resolve,reject)=>{const r=indexedDB.open('samurai-original-screenshots',1);r.onupgradeneeded=()=>r.result.createObjectStore('images',{keyPath:'id'});r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error);});
const ready=db().then(d=>new Promise((resolve,reject)=>{const r=d.transaction('images').objectStore('images').getAll();r.onsuccess=()=>{for(const x of r.result)if(ids.includes(x.id)){photos.set(x.id,URL.createObjectURL(x.blob));sources.set(x.id,x.blob);if(x.imageRevision===revision)newerOriginals.add(x.id);}resolve();};r.onerror=()=>reject(r.error);})).catch(()=>{});
async function store(id,file){
if(!ids.includes(id)||!['image/png','image/jpeg','image/webp'].includes(file.type)||file.size>10*1024*1024)throw Error('10MB以内のPNG・JPEG・WebP画像を選んでください。');
await createImageBitmap(file).then(bitmap=>bitmap.close());
const d=await db();await new Promise((resolve,reject)=>{const tx=d.transaction('images','readwrite');tx.objectStore('images').put({id,blob:file,imageRevision:revision});tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error);tx.onabort=()=>reject(tx.error);});
if(photos.has(id))URL.revokeObjectURL(photos.get(id));photos.set(id,URL.createObjectURL(file));sources.set(id,file);newerOriginals.add(id);sizes.delete(id);apply();W.ctx.notify('元画像を保存し、一覧と詳細へ反映しました。');
}
function metrics(id,img){
if(!img.naturalWidth||!img.naturalHeight||img.src!==new URL(photoUrl(id),location.href).href)return;
sizes.set(id,{width:img.naturalWidth,height:img.naturalHeight});
const size=sizes.get(id);
const label=document.querySelector(`[data-original-status="${id}"]`);if(label)label.textContent=`${isSelected(id)?'採用写真':'元画像を保存済み'} / ${size.width} × ${size.height}px`;
}
function controls(id,host){
if(!host||host.querySelector('[data-original-zoom]'))return;
const button=document.createElement('button');button.type='button';button.className='original-image-zoom';button.dataset.originalZoom=id;button.textContent='画像を拡大';button.setAttribute('aria-label',W.find(id).title+'の元画像を拡大');host.append(button);
}
function apply(){
const parts=location.hash.slice(1).split('/');for(const id of ids){const url=photoUrl(id);if(!url)continue;
const targets=[...document.querySelectorAll(`.subdocument-main[href="#document/${id}"] img,[data-original-preview="${id}"] img`)];
if(parts[1]===id&&parts[0]==='document')targets.push(...document.querySelectorAll('.document-hero img'));
if(parts[1]===id&&parts[0]==='document-looks')targets.push(...document.querySelectorAll('.document-look-preview img'));
for(const img of targets){
img.onload=()=>metrics(id,img);if(img.src!==new URL(url,location.href).href)img.src=url;img.removeAttribute('hidden');img.decoding='async';
Object.assign(img.style,{objectFit:'cover',objectPosition:isSelected(id)?selected[id].position:'center',width:'100%',height:'100%',left:'0',top:'0',imageRendering:'auto'});
img.parentElement.setAttribute('aria-label',W.find(id).title+'の写真');img.parentElement.classList.add('original-photo');img.parentElement.style.filter='none';img.parentElement.style.background='#10211b';
const card=img.closest('.subdocument-card');if(card){card.classList.add('has-original-photo');controls(id,card.querySelector('.subdocument-bottom'));}
const figure=img.closest('.document-hero');if(figure){figure.classList.add('has-original-photo');const caption=figure.querySelector('figcaption');if(caption){caption.textContent=W.find(id).title+' / '+(isSelected(id)?'採用した生成写真':'登録した元画像');controls(id,caption);}}
metrics(id,img);
}
const preview=document.querySelector(`[data-original-slot="${id}"]`);if(preview)controls(id,preview.closest('.screenshot-card'));
const label=document.querySelector(`[data-original-status="${id}"]`);if(label&&!sizes.has(id))label.textContent='元画像を読み込み中';
}
const summary=document.querySelector('[data-original-summary]');if(summary)summary.textContent=`写真設定済み ${ids.filter(photoUrl).length} / 10枚。一覧と詳細に反映しています。`;
if(photos.size){const footer=document.querySelector('.site-footer>p');if(footer)footer.textContent='SAMURAI Workspace · 登録画像・生成素材 · 業務データ未接続';}
if(parts[0]==='document-looks'&&photoUrl(parts[1])){const note=document.querySelector('.look-intro .local-save-note');if(note)note.textContent='設定した写真を固定して、詳細画面の10スタイルを比較しています。';}
}
function zoom(id){
const url=photoUrl(id);if(!url)return;const size=sizes.get(id),blob=sources.get(id),ext=isSelected(id)?'webp':({'image/png':'png','image/jpeg':'jpg','image/webp':'webp'}[blob?.type]||'png');
W.ctx.dialog(W.find(id).title,`<figure class="original-full-view"><img src="${url}" alt="${W.ctx.esc(W.find(id).title)}の写真全体"><figcaption>${size?size.width+' × '+size.height+'px / ':''}写真の全体を表示しています。</figcaption></figure><a class="button" href="${url}" download="${W.ctx.esc(W.find(id).title.replace(/[\\/:*?\"<>|]/g,'_'))}.${ext}">画像を保存</a>`);
}
const crcTable=Array.from({length:256},(_,n)=>{let c=n;for(let k=0;k<8;k++)c=c&1?0xedb88320^(c>>>1):c>>>1;return c>>>0;});
async function exportZip(button){
await ready;if(!sources.size){W.ctx.notify('このブラウザには登録画像がありません。登録したときと同じブラウザで開いてください。');return;}
const label=button.textContent;button.disabled=true;button.textContent='まとめています…';
try{
const entries=[],mapping=[];for(const [n,id]of ids.entries()){const blob=sources.get(id);if(!blob)continue;const ext={'image/png':'png','image/jpeg':'jpg','image/webp':'webp'}[blob.type]||'png',filename=String(n+1).padStart(2,'0')+'_'+W.find(id).title.replace(/[\\/:*?"<>|]/g,'_')+'.'+ext;entries.push({name:filename,data:new Uint8Array(await blob.arrayBuffer())});mapping.push({order:n+1,id,title:W.find(id).title,filename,...sizes.get(id)});}
const encoder=new TextEncoder();entries.push({name:'image-map.json',data:encoder.encode(JSON.stringify({source:'original user screenshots, not enhanced',images:mapping},null,2))});
const local=[],central=[];let offset=0,centralSize=0;
for(const file of entries){const name=encoder.encode(file.name),data=file.data;let crc=0xffffffff;for(const byte of data)crc=crcTable[(crc^byte)&255]^(crc>>>8);crc=(crc^0xffffffff)>>>0;
const lh=new Uint8Array(30),l=new DataView(lh.buffer);l.setUint32(0,0x04034b50,true);l.setUint16(4,20,true);l.setUint16(6,0x800,true);l.setUint16(12,33,true);l.setUint32(14,crc,true);l.setUint32(18,data.length,true);l.setUint32(22,data.length,true);l.setUint16(26,name.length,true);local.push(lh,name,data);
const ch=new Uint8Array(46),v=new DataView(ch.buffer);v.setUint32(0,0x02014b50,true);v.setUint16(4,20,true);v.setUint16(6,20,true);v.setUint16(8,0x800,true);v.setUint16(14,33,true);v.setUint32(16,crc,true);v.setUint32(20,data.length,true);v.setUint32(24,data.length,true);v.setUint16(28,name.length,true);v.setUint32(42,offset,true);central.push(ch,name);centralSize+=46+name.length;offset+=30+name.length+data.length;
}
const end=new Uint8Array(22),v=new DataView(end.buffer);v.setUint32(0,0x06054b50,true);v.setUint16(8,entries.length,true);v.setUint16(10,entries.length,true);v.setUint32(12,centralSize,true);v.setUint32(16,offset,true);
const url=URL.createObjectURL(new Blob([...local,...central,end],{type:'application/zip'})),a=document.createElement('a');a.href=url;a.download='SAMURAI_AUM_original_screenshots.zip';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);W.ctx.notify('元画像と項目の対応をZIPにまとめました。画像は加工していません。');
}finally{button.disabled=false;button.textContent=label;}
}
W.applyOriginalScreenshots=()=>ready.then(apply);
W.originalScreenshotsPage=()=>W.ctx.shell(`<header class="look-intro"><a class="back-link" href="#folder/aum">AUM情報に戻る</a><h1>AUMのスクショをそのまま使う</h1><p>一覧と詳細では写真を枠いっぱいに表示します。「画像を拡大」で写真の全体を確認できます。採用した2枚も反映しています。</p><p class="original-image-summary" data-original-summary>登録状況を確認中</p><div class="original-export"><button class="button primary" data-original-export="true">登録画像をまとめて保存</button><p>AI補正用に画像を渡す場合は、このボタンでZIPを自動作成できます。</p></div><p class="local-save-note">追加・変更は各項目を選んでCtrl+V。画像選択やドラッグでも登録できます。元画像は加工せず、このブラウザ内に保存します。別端末やサーバーとは同期しません。</p></header><section class="screenshot-grid">${ids.map((id,n)=>`<article class="screenshot-card"><h2>${String(n+1).padStart(2,'0')} ${W.ctx.esc(W.find(id).title)}</h2><div class="screenshot-drop" tabindex="0" role="button" data-original-slot="${id}" aria-label="${W.ctx.esc(W.find(id).title)}のスクショを貼り付け"><span data-original-preview="${id}"><img alt="${W.ctx.esc(W.find(id).title)}の登録画像" ${photos.has(id)?`src="${photos.get(id)}"`:''} ${photos.has(id)?'':'hidden'}></span><p>ここを選んで Ctrl+V<br>または画像ファイルをドラッグ</p></div><label class="button" for="original-${id}">画像ファイルを選ぶ</label><input id="original-${id}" type="file" accept="image/png,image/jpeg,image/webp" data-original-file="${id}" class="screenshot-file"><p class="local-save-note" data-original-status="${id}">${photos.has(id)?'元画像を保存済み':'スクショ未登録'}</p></article>`).join('')}</section>`,'workspace-sheet','AUMのスクショを登録');
W.bindOriginalScreenshots=()=>{
const app=document.getElementById('app');const accept=(id,file)=>{if(file)store(id,file).catch(error=>W.ctx.notify(error.message||'画像を保存できませんでした。'));};
app.addEventListener('click',event=>{const target=event.target.closest('[data-original-zoom]');if(target)zoom(target.dataset.originalZoom);const exportButton=event.target.closest('[data-original-export]');if(exportButton)exportZip(exportButton).catch(()=>W.ctx.notify('画像をまとめて保存できませんでした。もう一度お試しください。'));});
app.addEventListener('change',e=>{if(e.target.dataset.originalFile)accept(e.target.dataset.originalFile,e.target.files?.[0]);});
app.addEventListener('paste',e=>{const slot=e.target.closest('[data-original-slot]');if(!slot)return;const file=[...e.clipboardData.files].find(f=>f.type.startsWith('image/'));if(file){e.preventDefault();accept(slot.dataset.originalSlot,file);}});
app.addEventListener('dragover',e=>{if(e.target.closest('[data-original-slot]'))e.preventDefault();});app.addEventListener('drop',e=>{const slot=e.target.closest('[data-original-slot]');if(slot){e.preventDefault();accept(slot.dataset.originalSlot,e.dataTransfer.files[0]);}});
app.addEventListener('keydown',e=>{const slot=e.target.closest('[data-original-slot]');if(slot&&['Enter',' '].includes(e.key)){e.preventDefault();document.getElementById('original-'+slot.dataset.originalSlot).click();}});
let timer;window.addEventListener('resize',()=>{clearTimeout(timer);timer=setTimeout(()=>ready.then(apply),100);});
};
})();