frontend/subrep.html (11236 bytes)
1 <!doctype html> 2 <html lang="en"> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1"> 6 <title>Subrep Pro — SubRead</title> 7 <link rel="stylesheet" href="/style.css?v=13"> 8 <style> 9 main.pro { max-width: 720px; padding-top: 24px; padding-bottom: 60px; } 10 main.pro h1 { font-size: 26px; margin: 12px 0 6px; } 11 main.pro li { margin: 6px 0; } 12 main.pro a { color: var(--accent); } 13 .command { display: flex; gap: 8px; align-items: stretch; margin: 10px 0; } 14 .command code { flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap; 15 padding: 9px 12px; border: 1px solid var(--line); border-radius: 9px; 16 background: var(--bg); font-size: 13px; } 17 .waitlist { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; } 18 .waitlist input { flex: 1; min-width: 200px; font: inherit; padding: 9px 12px; 19 border: 1px solid var(--line); border-radius: 9px; background: var(--bg); color: var(--ink); } 20 .note { margin-top: 12px; padding: 10px 14px; border-radius: 9px; 21 background: var(--accent-soft); } 22 .note:empty { display: none; } 23 </style> 24 </head> 25 <body> 26 <main class="pro"> 27 <p><a href="https://honjimaku.com/subrep/">← Subrep</a> · <a href="/">SubRead</a></p> 28 <h1>Subrep Pro</h1> 29 <p class="muted">Subrep makes live captions of the sound on your computer, on your computer. 30 Live captions are free. Pro adds:</p> 31 <ul> 32 <li><strong>Replay clips:</strong> get back the sentence that you just heard, as a clip with 33 subtitles that you can mine with Yomitan.</li> 34 <li><strong>Anki cards</strong> from a clip.</li> 35 <li><strong>The large and medium speech models</strong>, for better text.</li> 36 </ul> 37 <p class="note" id="note" role="status"></p> 38 39 <section class="panel" id="buy"> 40 <h2>Choose a plan</h2> 41 <div class="plans" id="plans"></div> 42 <p class="muted small" id="sign-in-note" hidden>Sign in with your email on 43 <a href="/">the SubRead page</a> first, then come back here.</p> 44 <p class="muted small">Stripe takes the payment. Cancel at any time with "Manage plan": the plan 45 then runs to the end of the period that you paid for. We refund a first payment on request 46 within 14 days. <a href="/terms.html">Terms</a></p> 47 </section> 48 49 <section class="panel" id="pro" hidden> 50 <h2>Your Subrep Pro</h2> 51 <p id="pro-status"></p> 52 <p><a class="download" href="https://honjimaku.com/subrep/SubrepSetup.exe" 53 download="SubrepSetup.exe">Download Subrep for Windows</a> (10 or 11) and install it.</p> 54 <p>Then, on each computer that has Subrep, open the control panel and find 55 <strong>Licence</strong>. Enter this email and this setup key. The computer then renews its 56 licence by itself.</p> 57 <div class="command"> 58 <code id="link-email"></code> 59 </div> 60 <div class="command"> 61 <code id="link-key"></code> 62 <button class="ghost compact" id="copy-key" type="button">Copy</button> 63 </div> 64 <p class="muted small">Or run this command:</p> 65 <div class="command"> 66 <code id="command"></code> 67 <button class="ghost compact" id="copy" type="button">Copy</button> 68 </div> 69 <p class="muted small">Keep the setup key private: it gives Pro to any computer that has it.</p> 70 <div class="actions"> 71 <button class="ghost" id="manage" type="button">Manage plan</button> 72 </div> 73 </section> 74 75 <section class="panel" id="ios"> 76 <h2>Subrep for iPhone</h2> 77 <p>There is no iPhone app yet. Leave your email, and we write to you once, when it ships.</p> 78 <form class="waitlist" id="waitlist"> 79 <input id="waitlist-email" type="email" required autocomplete="email" 80 aria-label="Email" placeholder="you@example.com"> 81 <button class="primary" type="submit">Tell me</button> 82 </form> 83 <p class="muted small">On an iPhone today, open the share link of a caption stream in Safari.</p> 84 </section> 85 86 <section class="panel"> 87 <h2>Subrep for Android</h2> 88 <p><a href="https://github.com/equwal/subrep-android/releases/latest">Subrep for Android</a> is 89 free. It makes the captions on the phone.</p> 90 </section> 91 </main> 92 <footer> 93 <nav class="muted small" aria-label="More sites and apps"> 94 <p>More sites and apps: 95 <a href="/" title="Read along with an audiobook.">SubRead</a> · 96 <a href="https://booksimulator.com/" target="_blank" rel="noopener" title="A reading room for Aozora Bunko and Project Gutenberg books.">Book Simulator</a> · 97 <a href="https://honjimaku.com/" target="_blank" rel="noopener" title="Subtitles for Japanese audiobooks.">honjimaku.com</a> · 98 <a href="https://sbmsync.com/" target="_blank" rel="noopener" title="Your bookmarks, the same on every device.">sbm Sync</a> · 99 <a href="https://dickt.store/" target="_blank" rel="noopener" title="Language-learning tools, flashcards and web toys.">dickt.store</a> · 100 <a href="https://hentaibun.online/" target="_blank" rel="noopener" title="Learn kanbun and kobun.">hentaibun.online</a> · 101 <a href="https://mowimypopol.online/" target="_blank" rel="noopener" title="Archival study materials for Polish.">mowimypopol.online</a> · 102 <a href="https://alljapanesealltheti.me/" target="_blank" rel="noopener" title="All Japanese All The Time.">AJATT mirror</a> · 103 <a href="https://recentlywritten.com/" target="_blank" rel="noopener" title="The blog.">Recently Written</a> 104 </p> 105 <p>Apps: 106 <a href="https://github.com/equwal/subread-android/releases/latest" target="_blank" rel="noopener">SubRead for Android</a> · 107 <a href="https://github.com/equwal/subread-overlay/releases/latest" target="_blank" rel="noopener">SubRead Overlay</a> · 108 <a href="https://github.com/equwal/subread-dictionary/releases/latest" target="_blank" rel="noopener">SubRead Dictionary</a> · 109 <a href="https://github.com/equwal/subread-anki" target="_blank" rel="noopener">SubRead Anki</a> · 110 <a href="https://github.com/equwal/subread-extension/releases/latest" target="_blank" rel="noopener">SubRead for YouTube</a> · 111 <a href="https://github.com/equwal/subread.koplugin" target="_blank" rel="noopener">SubRead for KOReader</a> · 112 <a href="https://github.com/equwal/sbm-android/releases/latest" target="_blank" rel="noopener">sbm for Android</a> · 113 <a href="https://github.com/equwal/sbm-extension/releases/latest" target="_blank" rel="noopener">sbm add-on</a> · 114 <a href="https://github.com/equwal/rebind/releases" target="_blank" rel="noopener">Rebind</a> · 115 <a href="https://github.com/equwal/ink-recents/releases/latest" target="_blank" rel="noopener">Ink Recents</a> · 116 <a href="https://github.com/equwal/ink-dim/releases/latest" target="_blank" rel="noopener">Ink Dim</a> · 117 <a href="https://github.com/equwal/ink-update/releases/latest" target="_blank" rel="noopener">Ink Update</a> · 118 <a href="https://recentlywritten.com/projects.html" target="_blank" rel="noopener">All projects</a> 119 </p> 120 </nav> 121 </footer> 122 <script type="module"> 123 const $ = (id) => document.getElementById(id); 124 const say = (text) => { $('note').textContent = text; }; 125 126 async function post(path, body) { 127 const res = await fetch(path, { 128 method: 'POST', credentials: 'same-origin', 129 headers: { 'Content-Type': 'application/json' }, 130 body: JSON.stringify(body || {}), 131 }); 132 const data = await res.json().catch(() => ({})); 133 if (!res.ok) throw new Error(data.detail || data.error || `Error ${res.status}`); 134 return data; 135 } 136 137 function planCard(plan, state, first) { 138 const card = document.createElement('div'); 139 card.className = 'plan' + (first ? ' featured' : ''); 140 const name = document.createElement('div'); 141 name.className = 'plan-name'; 142 name.textContent = plan.name; 143 const price = document.createElement('div'); 144 price.className = 'plan-price'; 145 price.textContent = plan.price_display + ' '; 146 const per = document.createElement('small'); 147 per.textContent = plan.interval === 'year' ? 'billed yearly' : 'billed monthly'; 148 price.append(per); 149 const blurb = document.createElement('p'); 150 blurb.className = 'plan-blurb'; 151 blurb.textContent = plan.blurb; 152 const buy = document.createElement('button'); 153 buy.className = first ? 'primary' : 'ghost'; 154 buy.type = 'button'; 155 buy.textContent = `Pay ${plan.price_display}`; 156 buy.disabled = !state.available || state.needs_sign_in; 157 buy.addEventListener('click', async () => { 158 buy.disabled = true; 159 try { 160 location.href = (await post('/api/subrep/checkout', { plan_id: plan.id })).url; 161 } catch (err) { 162 say(err.message); 163 buy.disabled = false; 164 } 165 }); 166 card.append(name, price, blurb, buy); 167 return card; 168 } 169 170 async function render() { 171 const state = await (await fetch('/api/subrep', { credentials: 'same-origin' })).json(); 172 $('plans').replaceChildren(...state.plans.map((p, i) => planCard(p, state, i === 0))); 173 $('sign-in-note').hidden = !state.needs_sign_in; 174 $('buy').hidden = state.pro; 175 $('pro').hidden = !state.pro; 176 if (!state.available) say('Subrep Pro is not for sale yet. Check again soon.'); 177 if (state.pro) { 178 const ends = new Date(state.period_end).toLocaleDateString(); 179 const plan = state.plans.find((p) => p.id === state.plan_id); 180 $('pro-status').textContent = `${plan ? plan.name : 'Subrep Pro'}: paid until ${ends}.`; 181 $('link-email').textContent = `Email: ${state.link.email || '(none)'}`; 182 $('link-key').textContent = state.link.refresh_key; 183 $('command').textContent = state.link.command; 184 } 185 return state; 186 } 187 188 $('copy-key').addEventListener('click', async () => { 189 await navigator.clipboard.writeText($('link-key').textContent); 190 $('copy-key').textContent = 'Copied'; 191 }); 192 193 $('copy').addEventListener('click', async () => { 194 await navigator.clipboard.writeText($('command').textContent); 195 $('copy').textContent = 'Copied'; 196 }); 197 198 $('manage').addEventListener('click', async () => { 199 try { 200 location.href = (await post('/api/billing/portal')).url; 201 } catch (err) { 202 say(err.message); 203 } 204 }); 205 206 $('waitlist').addEventListener('submit', async (event) => { 207 event.preventDefault(); 208 try { 209 await post('/api/subrep/waitlist', { email: $('waitlist-email').value, kind: 'ios' }); 210 $('waitlist').replaceChildren('Thank you. We write to you when the iPhone app ships.'); 211 } catch (err) { 212 say(err.message); 213 } 214 }); 215 216 const checkout = new URLSearchParams(location.search).get('checkout'); 217 const state = await render(); 218 if (checkout === 'cancelled') say('The payment was cancelled. No money was taken.'); 219 if (checkout === 'paid' || checkout === 'pending') { 220 // The webhook can land a few seconds after the browser comes back. 221 say(state.pro ? 'Thank you. Subrep Pro is on.' : 'The payment is in progress…'); 222 for (let i = 0; i < 10 && !(await render()).pro; i++) { 223 await new Promise((done) => setTimeout(done, 3000)); 224 } 225 if ((await render()).pro) say('Thank you. Subrep Pro is on.'); 226 } 227 history.replaceState(null, '', location.pathname + location.hash); 228 </script> 229 </body> 230 </html>