Recently Written · git

subplz-web

git clone https://github.com/equwal/subplz-web

Log | Files | Refs


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>