Repository navigation
Expand file tree
/
Copy pathindex.html
More file actions
58 lines (58 loc) · 17.3 KB
/
Copy pathindex.html
File metadata and controls
58 lines (58 loc) · 17.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<!doctype html>
<html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>JV-DOS | Disk Workshop</title>
<style>
:root{color-scheme:dark;font-family:Inter,"Segoe UI",system-ui,sans-serif;background:#101215;color:#e7e9ed;font-size:14px;--line:#30343a;--muted:#939ba7;--accent:#bbef71}*{box-sizing:border-box}body{max-width:1440px;margin:auto;padding:36px 40px 24px}header{display:flex;justify-content:space-between;align-items:center;gap:24px;padding-bottom:28px;border-bottom:1px solid var(--line)}.tag{color:var(--accent);font:11px ui-monospace,monospace;letter-spacing:2px}h1{font-size:36px;font-weight:600;letter-spacing:-1.5px;margin:10px 0}p{color:var(--muted);line-height:1.7;margin:8px 0}.meta{font:11px ui-monospace,monospace;color:var(--muted);line-height:1.8}.badge{border:1px solid var(--line);border-radius:24px;padding:8px 13px;white-space:nowrap}.badge:before{content:"";display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin-right:8px}.toolbar{display:flex;gap:8px;flex-wrap:wrap;margin:20px 0}button,.button{display:inline-flex;align-items:center;justify-content:center;background:#24282e;color:#e7e9ed;border:1px solid #393e47;border-radius:7px;padding:10px 14px;cursor:pointer;font:500 12px "Segoe UI",sans-serif;transition:background .15s,border-color .15s}.button{margin:0}button:hover:not(:disabled),.button:hover{background:#30363e;border-color:#68717d}button.primary,.primary{background:var(--accent);color:#19210f;border-color:var(--accent);font-weight:650}button.primary:hover:not(:disabled),.primary:hover{background:#d0ff91;border-color:#d0ff91}button:disabled{opacity:.35;cursor:default}button:focus-visible,.button:focus-within,input:focus-visible,select:focus-visible,textarea:focus-visible,summary:focus-visible,tr:focus-visible{outline:2px solid var(--accent);outline-offset:3px}.hidden{display:none!important}.status{min-height:43px;padding:12px 15px;color:#b8c7a7;background:#191e19;border:1px solid #303d2c;border-radius:7px;margin:0 0 24px;font-size:12px}.status:before{content:"\25cf";font-size:8px;margin-right:9px;color:var(--accent)}.status.error{color:#ffb1b1;background:#2b1c20;border-color:#67333d}.status.error:before{color:#ff9292}main{display:grid;grid-template-columns:minmax(330px,.85fr) minmax(0,1.5fr);gap:20px;align-items:start}.panel{min-width:0;background:#181b20;border:1px solid var(--line);border-radius:10px;padding:24px}.section-kicker{font:10px ui-monospace,monospace;letter-spacing:1.8px;color:var(--muted);margin-bottom:9px}h2{font-size:17px;font-weight:550;margin:0 0 20px;letter-spacing:-.3px}h2 .meta{display:block;margin-top:7px;font-weight:400;letter-spacing:0}.volume-controls{display:flex;align-items:end;gap:12px;padding-bottom:18px;border-bottom:1px solid var(--line);margin-bottom:12px}.volume-controls label{margin:0;flex:1}.volume-controls input{max-width:100%}#folder{padding:8px 0;color:var(--accent);overflow-wrap:anywhere}label{display:block;color:#aeb5bf;font-size:11px;margin-top:16px}input[type=text],input[type=number],select,textarea{width:100%;background:#111418;color:#e7e9ed;border:1px solid #363c45;border-radius:6px;padding:11px 12px;margin-top:7px;font:13px ui-monospace,monospace}textarea{min-height:330px;resize:vertical;line-height:1.8;tab-size:4}textarea:disabled{color:#69717e}input::placeholder,textarea::placeholder{color:#5f6977}.row{display:flex;gap:16px}.row>*{flex:1;min-width:0}.row label{margin-top:0}table{width:100%;border-collapse:collapse;table-layout:fixed}th{text-transform:uppercase;letter-spacing:1px;color:#7e8998;font-size:9px;font-weight:500}td,th{text-align:left;padding:13px 8px;border-bottom:1px solid #2a2f36}td{font:12px ui-monospace,monospace;overflow-wrap:anywhere}th:first-child{width:53%}th:last-child,td:last-child{text-align:right}td:nth-child(2){color:#a0b28d;font-size:10px}tr.selected{background:#293723;box-shadow:inset 2px 0 var(--accent)}tr[data-slot]{cursor:pointer}tr[data-slot]:hover{background:#242b30}#empty{padding:32px 16px;text-align:center;border:1px dashed #363d46;border-radius:8px;margin:16px 0}#empty p:first-child{color:#dce2eb;font-size:14px}.hint{font-size:11px;line-height:1.8}canvas{display:block;margin:20px auto;max-width:100%;background:#000;image-rendering:pixelated;border:1px solid var(--line);border-radius:4px}.counter{display:flex;gap:12px;flex-wrap:wrap;justify-content:space-between;font:10px ui-monospace,monospace;margin-top:10px;color:var(--muted)}#command{color:var(--accent)}#delete{margin-left:auto;color:#ec9da2;border-color:#5a353d;background:#291e23}details{margin-top:22px;border-top:1px solid var(--line);padding-top:18px}summary{cursor:pointer;color:#bcc3ce;font-size:12px}details button{margin-top:12px}.foot{display:grid;grid-template-columns:1fr 1fr;gap:32px;margin-top:28px;padding-top:18px;border-top:1px solid var(--line)}.foot b{color:#c6cdd7;font-weight:500}#diskName{color:#e0e5ec;overflow-wrap:anywhere}#directory{margin-top:12px}.panel>.toolbar{margin-bottom:12px}@media(min-width:1100px){.panel:last-child{position:sticky;top:20px}}@media(max-width:800px){body{padding:24px 18px}main{grid-template-columns:1fr}header{align-items:start}.badge{font-size:9px}h1{font-size:30px}.foot{grid-template-columns:1fr;gap:8px}.panel{padding:20px}}@media(max-width:480px){header{flex-direction:column;gap:12px}.row{flex-direction:column}.row label+label{margin-top:0}.toolbar>button,.toolbar>.button{flex:1}#delete{margin-left:0}.badge{align-self:start}}
</style>
<header><div><div class="tag">J V - D O S / UTILITIES</div><h1>Disk Workshop<span style="color:var(--accent)">.</span></h1><p>A workspace for your virtual disk.<br>Create programs, manage files, and bring images and music to DOS.</p></div><div class="meta badge">LOCAL FILES · WORKS OFFLINE</div></header>
<div class="toolbar"><label class="button primary" role="button" tabindex="0" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();document.getElementById('diskInput').click();}" style="margin:0">Open storage.img<input id="diskInput" type="file" accept=".img" hidden></label><button id="export" disabled>Download updated disk</button><button id="samples" disabled>Add sample files</button><button id="desktop" disabled>Add JCTRL desktop</button></div>
<div id="status" class="status" role="status" aria-live="polite">Choose storage.img from your JavaVCPU project to begin.</div>
<main><section class="panel"><div class="section-kicker">01 / EXPLORER</div><h2 id="diskName">No disk open</h2><div class="volume-controls"><span class="meta">JVFS 1 / one volume per disk</span><button id="up" disabled>↑ Parent folder</button></div><p id="folder" class="meta"></p><h2>Disk directory <span id="usage" class="meta"></span></h2><div id="empty"><p>Your disk, ready to explore.</p><p class="hint">Open a disk image above to browse its files.<br>Your system files are preserved as you work.</p></div><table id="directory" class="hidden"><thead><tr><th>Name</th><th>Type</th><th>Bytes</th></tr></thead><tbody id="files"></tbody></table><div class="toolbar"><label class="button" id="importLabel">Import files<input id="import" type="file" multiple hidden></label><button id="new" disabled>New text file</button><button id="mkdir" disabled>New folder</button></div><p class="hint">Import text, JVS code, COM bytes, PNG/JPEG images, or MIDI clips. Files share the full disk; directories grow as needed. Images are resized to at most 240 × 240 pixels; MIDI becomes an eight-voice note stream.</p></section>
<section class="panel"><div class="section-kicker">02 / EDITOR</div><h2 id="editorTitle">Create something for DOS</h2><div class="row"><label>Name<input id="name" type="text" maxlength="12" placeholder="DEMO.JVS" disabled></label><label>Format<select id="type" disabled><option value="1">Text / JVS source</option><option value="0">Binary / COM</option><option value="2">Palette image</option><option value="3">MIDI note stream</option></select></label></div><label id="contentLabel">Contents<textarea id="content" disabled spellcheck="false" placeholder="PRINT HELLO FROM DOS BEEP END"></textarea></label><div class="counter"><span id="size">0 / 61200 bytes</span><span id="command">RUN DEMO.JVS</span></div><canvas id="preview" width="240" height="240" class="hidden"></canvas><div class="toolbar"><button id="save" disabled class="primary">Apply to disk</button><button id="fileExport" disabled>Download file</button><button id="delete" disabled>Delete file</button></div><details><summary>Create a short tune</summary><p class="hint">One MIDI note number and duration per line. Durations are 33 ms frames. Up to 7,649 notes.</p><textarea id="notes" style="min-height:100px">60 6
64 6
67 12</textarea><button id="makeTune" disabled>Create THEME.JMT</button></details></section></main>
<div class="foot"><p class="hint">Stop the VM before replacing its disk. Download the updated image, rename it to <b>storage.img</b> in your JavaVCPU folder, then start the VM. In DOS use <b>VIEW FILE.TXT</b>, <b>IMAGE FILE.IMG</b>, <b>PLAY FILE.JMT</b>, or <b>RUN FILE.JVS</b>. Use uppercase JVS keywords; see the <a href="../../docs/wiki/JVS-programming.md">JVS guide</a> for syntax. Nothing is uploaded to a server.</p><p class="hint">MIDI: Standard MIDI format 0/1, PPQN timing, eight output voices and events limited by available disk space. When notes overlap beyond eight voices, new notes replace the oldest sounding notes. Instruments use synthesized triangle waves; percussion uses noise. Oversized files are rejected rather than silently truncated.</p></div>
<script src="disk.js"></script><script src="desktop.js"></script>
<script>
'use strict';
const $=id=>document.getElementById(id),D=DosDisk;let disk=null,selected=null;
const status=(message,error=false)=>{$('status').textContent=message;$('status').classList.toggle('error',error);};
const guard=fn=>async(...args)=>{try{await fn(...args);}catch(error){status(error.message,true);}};
function download(name,bytes){const url=URL.createObjectURL(new Blob([bytes],{type:'application/octet-stream'}));const a=document.createElement('a');a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);}
function data(){const value=$('content').value;if(Number($('type').value)===1)return D.textBytes(value);const tokens=value.trim()?value.trim().split(/\s+/):[];if(tokens.some(v=>!/^[0-9a-f]{2}$/i.test(v)))throw new Error('Binary contents use two-digit hex bytes separated by spaces.');return Uint8Array.from(tokens,v=>parseInt(v,16));}
function count(){try{$('size').textContent=`${data().length} / ${D.MAX} bytes`;const command=Number($('type').value)===2?'IMAGE':Number($('type').value)===3?'PLAY':/\.JVS$/i.test($('name').value)?'RUN':'VIEW';$('command').textContent=command+' '+$('name').value.toUpperCase();}catch(error){$('size').textContent=error.message;}}
$('up').onclick=()=>{if(disk.cwd!==D.ROOT){disk.cwd=disk.parent();choose(null);}};
function refresh(){$('up').disabled=!disk||disk.cwd===D.ROOT;$('folder').textContent=disk?'Folder: '+disk.path():'';const files=disk?.list()||[];$('files').replaceChildren();$('usage').textContent=disk?`${files.length} files | ${disk.capacity().used}/${disk.capacity().total} clusters | ${disk.capacity().freeBytes.toLocaleString()} bytes free`:'';$('empty').classList.toggle('hidden',files.length>0);$('directory').classList.toggle('hidden',!files.length);
for(const file of files){const tr=document.createElement('tr');tr.dataset.slot=file.slot;tr.tabIndex=0;tr.setAttribute('aria-label',file.name);tr.setAttribute('aria-selected',String(file.slot===selected));tr.onkeydown=event=>{if(event.key==='Enter'||event.key===' '){event.preventDefault();tr.click();}};tr.classList.toggle('selected',file.slot===selected);for(const value of [file.name,['BIN','TXT','IMG','MIDI','DIR'][file.type]||'BIN',file.data.length]){const td=document.createElement('td');td.textContent=value;tr.append(td);}tr.onclick=()=>{if(file.type===4){disk.cwd=file.first;choose(null);}else choose(file);};$('files').append(tr);}
for(const id of ['export','samples','desktop','new','mkdir','makeTune'])$(id).disabled=!disk;}
function choose(file){$('editorTitle').textContent=file?file.name:(disk?'New file':'Create something for DOS');selected=file?.slot??null;for(const id of ['name','type','content','save'])$(id).disabled=!disk;for(const id of ['delete','fileExport'])$(id).disabled=!file;
$('name').value=file?.name||'NEW.TXT';$('type').value=String(file?.type??1);$('content').value=file?(file.type===1?D.textOf(file.data):Array.from(file.data,b=>b.toString(16).padStart(2,'0')).join(' ')):'';refresh();count();preview();}
function preview(){$('preview').classList.add('hidden');if(Number($('type').value)!==2)return;try{const image=D.decodeImage(data()),canvas=$('preview');canvas.classList.remove('hidden');const ctx=canvas.getContext('2d');ctx.fillStyle='#000';ctx.fillRect(0,0,240,240);const scale=Math.floor(240/Math.max(image.width,image.height));for(let y=0;y<image.height;y++)for(let x=0;x<image.width;x++){const c=image.pixels[y*image.width+x];ctx.fillStyle=`rgb(${((c>>5)&3)*85},${Math.floor(((c>>2)&7)*255/7)},${(c&3)*85})`;ctx.fillRect(x*scale,y*scale,scale,scale);}}catch(error){status(error.message,true);}}
$('diskInput').onchange=guard(async event=>{const file=event.target.files[0];if(!file)return;disk=new D.Disk(new Uint8Array(await file.arrayBuffer()));$('diskName').textContent=file.name;selected=null;refresh();choose(disk.list().find(f=>f.type!==4));status('Disk opened. Changes stay in this browser until you download the updated disk.');});
$('export').onclick=()=>download('storage-updated.img',disk.bytes);
$('new').onclick=()=>choose(null);
$('mkdir').onclick=guard(()=>{const name=prompt('Folder name','DOCS');if(!name)return;disk.mkdir(name);choose(null);status('Folder created in the working copy. Download the disk to keep it.');});
$('content').oninput=()=>{count();};$('name').oninput=count;$('type').onchange=()=>{count();preview();};
$('save').onclick=guard(()=>{const name=$('name').value.toUpperCase();const clone=disk.clone();if(selected!==null){const old=disk.list().find(f=>f.slot===selected);if(old&&old.name!==name){if(disk.list().some(f=>f.name===name))throw new Error('That filename already exists. Choose another name.');clone.remove(selected);}}clone.put(name,data(),Number($('type').value));disk=clone;choose(disk.list().find(f=>f.name===name));status(`${name} applied. Download the updated disk to use it in DOS.`);});
$('delete').onclick=guard(()=>{const file=disk.list().find(f=>f.slot===selected);if(!file)return;disk.remove(selected);choose(null);status(`${file.name} deleted from the working copy. The original disk is unchanged.`);});
$('fileExport').onclick=()=>{const file=disk.list().find(f=>f.slot===selected);if(file)download(file.name,file.data);};
$('makeTune').onclick=guard(()=>{disk.put('THEME.JMT',D.notes($('notes').value),3);choose(disk.list().find(f=>f.name==='THEME.JMT'));status('Tune created. PLAY THEME.JMT in DOS.');});
async function convert(file){const ext=file.name.split('.').pop().toLowerCase();if(['png','jpg','jpeg','gif','webp','bmp'].includes(ext)){
const bitmap=await createImageBitmap(file);let width=Math.min(240,bitmap.width),height=Math.max(1,Math.round(bitmap.height*width/bitmap.width));if(height>240){height=240;width=Math.max(1,Math.round(bitmap.width*height/bitmap.height));}
const canvas=document.createElement('canvas');canvas.width=width;canvas.height=height;const context=canvas.getContext('2d');context.drawImage(bitmap,0,0,width,height);bitmap.close();return {name:D.nameOf(file.name,'IMG'),data:D.image(width,height,context.getImageData(0,0,width,height).data),type:2};}
const bytes=new Uint8Array(await file.arrayBuffer());if(ext==='mid'||ext==='midi')return {name:D.nameOf(file.name,'JMT'),data:D.midi(bytes),type:3};
if(['txt','jvs','bat','asm','md'].includes(ext))return {name:D.nameOf(file.name,ext),data:D.textBytes(new TextDecoder('utf-8',{fatal:true}).decode(bytes)),type:1};
return {name:D.nameOf(file.name,ext),data:bytes,type:ext==='jmt'?3:ext==='img'&&bytes[0]===74&&bytes[1]===73?2:0};}
$('import').onchange=guard(async event=>{
try{
if(!disk)throw new Error('Open storage.img first.');
const clone=disk.clone(),locations=[];let last;
for(const file of event.target.files){last=await convert(file);const location=clone.importFile(last.name,last.data,last.type);locations.push(last.name);}
disk=clone;refresh();if(last)choose(disk.list().find(f=>f.name===last.name));
status('Imported '+locations.join('; ')+'. Download the updated disk to use these files.');
}finally{event.target.value='';}
});
$('desktop').onclick=guard(()=>{const clone=disk.clone();if(clone.list().some(f=>f.name==='JCTRL.COM'))throw new Error('JCTRL.COM already exists in this folder. Rename or remove it before adding the desktop.');clone.put('JCTRL.COM',Uint8Array.from(atob(DosDesktop),c=>c.charCodeAt(0)),0);disk=clone;choose(disk.list().find(f=>f.name==='JCTRL.COM'));status('JCTRL added. Download the updated disk, then RUN JCTRL.COM in DOS.');});
$('samples').onclick=guard(()=>{const clone=disk.clone();clone.put('WELCOME.TXT',D.textBytes('Welcome to JV-DOS 2.0!\nUse IMAGE SMILE.IMG and PLAY THEME.JMT.\nHELP lists everyday commands.\nSee docs/wiki/JVS-programming.md for JVS syntax.'),1);
const pixels=Uint8Array.from([74,73,5,5,0,124,124,124,0,124,0,124,0,124,124,124,124,124,124,124,0,124,0,124,0,124,0,124,0]);clone.put('SMILE.IMG',pixels,2);clone.put('THEME.JMT',D.notes('60 6\n64 6\n67 12'),3);disk=clone;refresh();choose(disk.list().find(f=>f.name==='WELCOME.TXT'));status('Samples added: WELCOME.TXT, SMILE.IMG, THEME.JMT.');});
</script></html>