|
| 1 | +<!DOCTYPE html> |
| 2 | +<meta lang="en"> |
| 3 | +<meta charset="UTF-8"> |
| 4 | +<title>DWeb Scratchpad</title> |
| 5 | +<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 6 | +<meta name="description" content="Quickly put together a p2p app with live preview"> |
| 7 | +<link rel="stylesheet" href="agregore://theme/style.css"> |
| 8 | +<style> |
| 9 | +textarea { |
| 10 | + min-height: 25vh; |
| 11 | +} |
| 12 | +.closeButton { |
| 13 | + position: absolute; |
| 14 | + top: 2px; |
| 15 | + right: 2px; |
| 16 | +} |
| 17 | +form > *, label > * { |
| 18 | + display: block; |
| 19 | + margin: 0.5em; |
| 20 | +} |
| 21 | +h1 { |
| 22 | + font-size: 1rem; |
| 23 | + display: inline; |
| 24 | +} |
| 25 | +textarea, iframe { |
| 26 | + margin: 0px; |
| 27 | +} |
| 28 | +</style> |
| 29 | +<header> |
| 30 | +<h1>DWeb Scratchpad</h1> |
| 31 | + <button |
| 32 | + title="Save your scratchpad to the dweb" |
| 33 | + onclick="saveDialog.showModal()" |
| 34 | + >💾</button> |
| 35 | + <button |
| 36 | + title="Load and edit a site from a link" |
| 37 | + onclick="loadDialog.showModal()" |
| 38 | + >📂</button> |
| 39 | + <button |
| 40 | + title="Look at your saved sites" |
| 41 | + onclick="window.showAllSaved()" |
| 42 | + >📃</button> |
| 43 | + <button |
| 44 | + title="Help" |
| 45 | + onclick="helpDialog.showModal()" |
| 46 | + >❔</button> |
| 47 | +</header> |
| 48 | +<main id="mainContents"> |
| 49 | + |
| 50 | +<iframe id="previewFrame"></iframe> |
| 51 | +<textarea autofocus id="htmlContent"> |
| 52 | +<marquee>🧑💻HTML content here📃</marquee> |
| 53 | +</textarea> |
| 54 | +<textarea id="cssContent"> |
| 55 | +/*CSS here*/ |
| 56 | +marquee { |
| 57 | + color: var(--ag-theme-primary); |
| 58 | + font-weight: bold; |
| 59 | + font-size: 3em; |
| 60 | +} |
| 61 | +</textarea> |
| 62 | +<textarea id="jsContent"> |
| 63 | +// JavaScript here |
| 64 | +console.log("Hello World!") |
| 65 | +</textarea> |
| 66 | + |
| 67 | +</main> |
| 68 | +<dialog id="saveDialog"> |
| 69 | +<h2>Save to the dweb</h2> |
| 70 | + <form id="saveForm"> |
| 71 | + <label> |
| 72 | + Title |
| 73 | + <input id="appTitle" name="title" value="Scratchpad App"> |
| 74 | + </label> |
| 75 | + <label> |
| 76 | + Filename (.html) |
| 77 | + <input id="appFilename" name="filename" value="example.html"> |
| 78 | + </label> |
| 79 | + <label> |
| 80 | + Description |
| 81 | + <textarea id="appDescription" name="description">My quick scratch</textarea> |
| 82 | + </label> |
| 83 | + <button>Save and View</button> |
| 84 | + </form> |
| 85 | + <button class="closeButton" title="Close dialog" onclick="this.parentElement.close()">❌</button> |
| 86 | +</dialog> |
| 87 | +<dialog id="loadDialog"> |
| 88 | + <form id="loadForm"> |
| 89 | + <label> |
| 90 | + Page URL |
| 91 | + <input name="url" type="url"> |
| 92 | + <button>Load 📂</button> |
| 93 | + </label> |
| 94 | + </form> |
| 95 | + <button class="closeButton" title="Close dialog" onclick="this.parentElement.close()">❌</button> |
| 96 | +</dialog> |
| 97 | +<dialog id="helpDialog"> |
| 98 | + <h2>How does this work?</h2> |
| 99 | + <p> |
| 100 | + This is a scratchpad for making p2p apps. Enter some HTML/CSS/JavaScript and see a preview in real time. You can save the result to your collection or try to load a site somebody else published from their link. |
| 101 | + </p> |
| 102 | + <p> |
| 103 | + If you're new to Agregore, check out our <a href="hyper://agregore.mauve.moe/docs/">docs</a> and if you're new to web programming check out the <a href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Core">Mozilla Developer Network</a> for tutorials and documentation on web development. |
| 104 | + </p> |
| 105 | + <p> |
| 106 | + Once you've set up Ollama you can use the <a href="hyper://agregore.mauve.moe/docs/examples/quickcode.html">Quickcode</a> AI example to help generate code snippets for you. This can help you learn new syntax like "Css for a red box that is half the screen" or "open the camera and render the video to a video tag". |
| 107 | + </p> |
| 108 | + <p> |
| 109 | + If you feel like this page should have more features try opening it within itself and tinkering with the code! |
| 110 | + </p> |
| 111 | + <button class="closeButton" title="Close dialog" onclick="this.parentElement.close()">❌</button> |
| 112 | +</dialog> |
| 113 | +<script type="module"> |
| 114 | +window.showAllSaved = async () => { |
| 115 | + const url = await getDriveURL() |
| 116 | + window.open(url) |
| 117 | +} |
| 118 | + |
| 119 | +const shouldLoad = new URL(location.href).searchParams.get('url') |
| 120 | + |
| 121 | +// Triggered when we generate blobs for the page content |
| 122 | +// FileReader converts them to data URLs |
| 123 | +const previewReader = new FileReader() |
| 124 | +previewReader.onload = (e) => previewFrame.src = e.target.result |
| 125 | + |
| 126 | +mainContents.addEventListener('input', updatePreview) |
| 127 | + |
| 128 | +loadForm.onsubmit = (e) => { |
| 129 | + e.preventDefault(e) |
| 130 | + console.log(loadForm) |
| 131 | + const url = new FormData(loadForm).get('url') |
| 132 | + loadPage(url) |
| 133 | + loadDialog.close() |
| 134 | +} |
| 135 | + |
| 136 | +saveForm.onsubmit = (e) => { |
| 137 | + e.preventDefault() |
| 138 | + const formData = new FormData(saveForm) |
| 139 | + saveDialog.close() |
| 140 | + let filename = formData.get('filename') |
| 141 | + if(!filename.endsWith('.html')) { |
| 142 | + filename+= '.html' |
| 143 | + } |
| 144 | + saveAndPublish(filename) |
| 145 | + .then((url) => window.open(url)) |
| 146 | +} |
| 147 | + |
| 148 | +if(shouldLoad) { |
| 149 | + console.log("Loading", shouldLoad) |
| 150 | + loadPage(shouldLoad) |
| 151 | +} else { |
| 152 | + setPreview(genPage()) |
| 153 | +} |
| 154 | + |
| 155 | +async function getDriveURL() { |
| 156 | + const name = "dweb_scratchpad" |
| 157 | + const response = await fetch(`hyper://localhost/?key=${name}`, { method: 'POST' }); |
| 158 | + if (!response.ok) { |
| 159 | + throw new Error(`Failed to generate Hyperdrive key: ${response.statusText}`); |
| 160 | + } |
| 161 | + return await response.text() |
| 162 | +} |
| 163 | + |
| 164 | +async function saveAndPublish(filename) { |
| 165 | + const driveURL = await getDriveURL() |
| 166 | + const url = new URL(filename, driveURL).href |
| 167 | + const content = genPage() |
| 168 | + const response = await fetch(url, { |
| 169 | + method: 'PUT', |
| 170 | + body: content |
| 171 | + }) |
| 172 | + if(!response.ok) { |
| 173 | + throw new Error(`Failed to upload ${await response.text()}`) |
| 174 | + } |
| 175 | + await response.text() |
| 176 | + return url |
| 177 | +} |
| 178 | + |
| 179 | +function updatePreview() { |
| 180 | + setPreview(genPage()) |
| 181 | +} |
| 182 | + |
| 183 | +async function setPreview(content) { |
| 184 | + const blob = new Blob([content], {type:"text/html"}) |
| 185 | + previewReader.readAsDataURL(blob) |
| 186 | +} |
| 187 | + |
| 188 | +async function loadPage(url) { |
| 189 | + console.log('loading page', url) |
| 190 | + const response = await fetch(url) |
| 191 | + if(!response.ok) throw new Error(`Unable to load page:\n${await response.text()}`) |
| 192 | + const text = await response.text() |
| 193 | + const {title, description, html, js, css} = extractPage(text) |
| 194 | + console.log({title, description, html, js, css}) |
| 195 | + |
| 196 | + const {pathname} = new URL(url) |
| 197 | + const filename = pathname.split('/').at(-1) |
| 198 | + |
| 199 | + appTitle.value = title |
| 200 | + appDescription.value = description |
| 201 | + appFilename.value = filename |
| 202 | + htmlContent.value = html |
| 203 | + cssContent.value = css |
| 204 | + jsContent.value = js |
| 205 | + |
| 206 | + setPreview(genPage()) |
| 207 | +} |
| 208 | + |
| 209 | +function extractPage(content) { |
| 210 | + console.log('Extracting', {content}) |
| 211 | + const parser = new DOMParser() |
| 212 | + const doc = parser.parseFromString(content, 'text/html'); |
| 213 | + const title = doc.title |
| 214 | + const description = doc.head.querySelector('meta[name=description]') |
| 215 | + const css = doc.head.querySelector('style').innerText.trim() |
| 216 | + const script = doc.head.querySelector('script') || doc.body.querySelector('script') |
| 217 | + const js = script.innerText.trim() |
| 218 | + |
| 219 | + for(const script of doc.querySelectorAll('script')) { |
| 220 | + script.parentElement.removeChild(script) |
| 221 | + } |
| 222 | + |
| 223 | + const html = doc.body.innerHTML.trim() |
| 224 | + |
| 225 | + return {title, description, css, html, js} |
| 226 | +} |
| 227 | + |
| 228 | +function genPage({ |
| 229 | + title=appTitle.value, |
| 230 | + description=appDescription.value, |
| 231 | + html=htmlContent.value, |
| 232 | + css=cssContent.value, |
| 233 | + js=jsContent.value |
| 234 | +}={}) { |
| 235 | +return ` |
| 236 | +<!DOCTYPE html> |
| 237 | +<meta lang="en"> |
| 238 | +<meta charset="UTF-8"> |
| 239 | +<title>${title}</title> |
| 240 | +<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 241 | +<meta name="description" content="${description}"> |
| 242 | +<link rel="stylesheet" href="agregore://theme/style.css"> |
| 243 | +<style> |
| 244 | +${css} |
| 245 | +</style> |
| 246 | +${html} |
| 247 | +<script type="module"> |
| 248 | +${js} |
| 249 | +</${"script"}> |
| 250 | +` |
| 251 | +} |
| 252 | +</script> |
0 commit comments