|
4 | 4 | <meta charset="UTF-8"> |
5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no"> |
6 | 6 | <title>Flow</title> |
| 7 | + |
| 8 | + <script> |
| 9 | + const savedTheme = localStorage.getItem('theme') || 'dark'; |
| 10 | + document.documentElement.setAttribute('data-theme', savedTheme); |
| 11 | + </script> |
| 12 | + |
7 | 13 | <style> |
8 | 14 | @import url('https://fonts.googleapis.com/css2?family=Fredoka:wght@500;600;700&display=swap'); |
9 | 15 |
|
|
20 | 26 | margin: 0; padding: 0; box-sizing: border-box; |
21 | 27 | -webkit-font-smoothing: antialiased; font-family: 'Fredoka', sans-serif; |
22 | 28 | -webkit-tap-highlight-color: transparent; |
23 | | - user-select: none; |
24 | | - outline: none; |
| 29 | + user-select: none; outline: none; touch-action: manipulation; |
25 | 30 | } |
26 | 31 |
|
27 | | - ::-webkit-scrollbar { display: none; } |
28 | | - html { scrollbar-width: none; -ms-overflow-style: none; } |
29 | | - |
30 | 32 | body { |
31 | 33 | background: var(--bg); color: var(--text); |
32 | 34 | height: 100dvh; display: flex; flex-direction: column; overflow: hidden; |
33 | | - transition: background 0.4s ease; |
| 35 | + transition: background 0.4s ease, color 0.4s ease; |
34 | 36 | padding: env(safe-area-inset-top) 10px env(safe-area-inset-bottom); |
35 | 37 | } |
36 | 38 |
|
| 39 | + /* UI ELEMENTS */ |
37 | 40 | .top-nav { height: 65px; display: flex; justify-content: space-between; align-items: center; z-index: 1000; flex-shrink: 0; padding: 0 10px; } |
38 | 41 | .icon-btn { width: 46px; height: 46px; border-radius: 50%; border: none; background: var(--card); color: var(--text); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: 0.2s; } |
39 | 42 | .icon-btn:active { transform: scale(0.9); } |
40 | 43 |
|
| 44 | + /* CLOCK AREA */ |
41 | 45 | .stage { flex: 1; display: flex; align-items: center; justify-content: center; overflow: hidden; width: 100%; cursor: pointer; } |
42 | 46 | .time-row { display: flex; align-items: baseline; justify-content: center; gap: 0.6vw; width: fit-content; max-width: 100%; pointer-events: none; } |
43 | 47 | .digit-box { height: var(--digit-size); width: calc(var(--digit-size) * 0.62); overflow: hidden; display: flex; justify-content: center; align-items: center; } |
44 | | - .digit { font-size: var(--digit-size); font-weight: 700; line-height: 1; } |
| 48 | + .digit { font-size: var(--digit-size); font-weight: 700; line-height: 1; transition: transform 0.4s var(--ease), opacity 0.2s; } |
45 | 49 | .separator { font-size: calc(var(--digit-size) * 0.7); color: var(--sep-color); font-weight: 700; line-height: 1; } |
46 | 50 |
|
47 | 51 | .sec-group { display: none; align-items: baseline; opacity: 0.5; } |
48 | 52 | .sec-group .digit-box { height: calc(var(--digit-size) * 0.6); width: calc(var(--digit-size) * 0.38); } |
49 | 53 | .sec-group .digit { font-size: calc(var(--digit-size) * 0.6); } |
50 | 54 |
|
51 | | - body.has-seconds { --digit-size: clamp(50px, 13vw, 25vh); } |
52 | | - body.has-ampm { --digit-size: clamp(50px, 14vw, 28vh); } |
53 | | - body.has-seconds.has-ampm { --digit-size: clamp(45px, 11vw, 22vh); } |
54 | | - |
55 | | - #ampm { font-size: clamp(0.9rem, 3.5vw, 1.5rem); font-weight: 700; color: var(--sep-color); margin-left: 8px; } |
56 | | - |
| 55 | + /* LANDSCAPE MODE (AUTO FULLSCREEN VIEW) */ |
57 | 56 | @media (orientation: landscape) and (max-height: 500px) { |
58 | 57 | body.has-seconds { --digit-size: 52vh; } |
59 | 58 | body:not(.has-seconds) { --digit-size: 75vh; } |
60 | 59 | .top-nav, .dock { display: none !important; } |
61 | 60 | body { padding: 0; } |
62 | 61 | } |
63 | 62 |
|
| 63 | + body.has-seconds { --digit-size: clamp(50px, 13vw, 25vh); } |
| 64 | + body.has-ampm { --digit-size: clamp(50px, 14vw, 28vh); } |
| 65 | + #ampm { font-size: clamp(0.9rem, 3.5vw, 1.5rem); font-weight: 700; color: var(--sep-color); margin-left: 8px; } |
| 66 | + |
| 67 | + /* DOCK */ |
64 | 68 | .dock { width: 100%; max-width: 450px; margin: 0 auto; display: flex; flex-direction: column; gap: 12px; flex-shrink: 0; padding-bottom: 20px; } |
65 | 69 | .segmented { background: var(--card); padding: 5px; border-radius: 100px; display: flex; } |
66 | 70 | .seg-btn { flex: 1; border: none; background: transparent; color: var(--text); padding: 12px; border-radius: 100px; font-weight: 700; opacity: 0.3; font-size: 0.85rem; cursor: pointer; transition: 0.2s; } |
|
69 | 73 | .btn { background: var(--card); color: var(--text); border: none; height: 64px; border-radius: 20px; display: flex; align-items: center; justify-content: center; flex: 1; cursor: pointer; transition: 0.2s; } |
70 | 74 | .btn-main { flex: 2; background: var(--text); color: var(--bg); } |
71 | 75 |
|
| 76 | + /* OVERLAYS */ |
72 | 77 | .overlay { position: fixed; inset: 0; background: var(--bg); display: none; z-index: 3000; flex-direction: column; animation: fadeIn 0.3s ease; } |
73 | 78 | .overlay-close { position: absolute; top: 20px; left: 20px; z-index: 3001; } |
74 | 79 | .overlay-content { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 30px; } |
75 | | - |
76 | | - .edit-label { font-size: 0.8rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; opacity: 0.4; margin-bottom: 20px; } |
77 | 80 | .counter-group { background: var(--card); padding: 25px 35px; border-radius: 35px; display: flex; align-items: center; gap: 15px; } |
78 | | - .timer-input { background: transparent; border: none; color: var(--text); font-size: 4rem; font-weight: 700; width: 2.2ch; text-align: center; outline: none; caret-color: #34c759; user-select: text; } |
79 | | - .timer-input::-webkit-inner-spin-button, .timer-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } |
80 | | - |
81 | | - .step-btn { |
82 | | - width: 52px; height: 52px; border: none; background: var(--bg); color: var(--text); |
83 | | - border-radius: 18px; font-size: 1.4rem; cursor: pointer; display: flex; |
84 | | - align-items: center; justify-content: center; |
85 | | - } |
86 | | - .step-btn:active { background: var(--text); color: var(--bg); } |
87 | | - |
| 81 | + .timer-input { background: transparent; border: none; color: var(--text); font-size: 4rem; font-weight: 700; width: 2.2ch; text-align: center; } |
| 82 | + .step-btn { width: 52px; height: 52px; border: none; background: var(--bg); color: var(--text); border-radius: 18px; font-size: 1.4rem; cursor: pointer; touch-action: none; } |
88 | 83 | .confirm-btn { width: 100%; max-width: 300px; padding: 18px; border-radius: 100px; border: none; background: var(--text); color: var(--bg); font-weight: 700; margin-top: 40px; cursor: pointer; } |
89 | 84 |
|
| 85 | + /* SETTINGS */ |
90 | 86 | .setting-item { width: 100%; max-width: 360px; display: flex; justify-content: space-between; align-items: center; background: var(--card); padding: 20px 24px; border-radius: 24px; margin-bottom: 10px; } |
91 | 87 | .toggle { width: 48px; height: 26px; position: relative; } |
92 | 88 | .toggle input { opacity: 0; width: 0; } |
|
95 | 91 | input:checked + .slider { background: #34c759; } |
96 | 92 | input:checked + .slider:before { transform: translateX(22px); } |
97 | 93 |
|
98 | | - #toast { position: fixed; top: 30px; left: 50%; transform: translate(-50%, -20px); background: var(--text); color: var(--bg); padding: 12px 24px; border-radius: 100px; font-weight: 700; opacity: 0; transition: 0.4s var(--ease); z-index: 5000; font-size: 0.85rem; } |
| 94 | + #toast { position: fixed; top: 30px; left: 50%; transform: translate(-50%, -20px); background: var(--text); color: var(--bg); padding: 12px 24px; border-radius: 100px; font-weight: 700; opacity: 0; transition: 0.4s var(--ease); z-index: 5000; font-size: 0.85rem; pointer-events: none; } |
99 | 95 | #toast.show { opacity: 1; transform: translate(-50%, 0); } |
100 | 96 |
|
101 | 97 | @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } |
102 | 98 | svg { fill: none; stroke: currentColor; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; } |
103 | 99 | </style> |
104 | 100 | </head> |
105 | | -<body onclick="handleGlobalClick(event)" oncontextmenu="return false;"> |
| 101 | +<body onclick="handleGlobalClick(event)"> |
106 | 102 |
|
107 | 103 | <div id="toast">THEME UPDATED</div> |
108 | 104 |
|
|
143 | 139 | <div id="edit-panel" class="overlay"> |
144 | 140 | <button class="icon-btn overlay-close" onclick="closeModal('edit-panel')"><svg width="20" height="20" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></svg></button> |
145 | 141 | <div class="overlay-content"> |
146 | | - <div class="edit-label">Set Timer</div> |
147 | 142 | <div class="counter-group"> |
148 | 143 | <div style="display:flex; flex-direction:column; align-items:center; gap:14px;"> |
149 | | - <button class="step-btn" |
150 | | - onmousedown="hold(event, 'h-in', 1)" |
151 | | - onmouseup="stopHold()" |
152 | | - onmouseleave="stopHold()" |
153 | | - ontouchstart="hold(event, 'h-in', 1)" |
154 | | - ontouchend="stopHold()">+</button> |
| 144 | + <button class="step-btn" onpointerdown="hold(event, 'h-in', 1)" onpointerup="stopHold()" onpointerleave="stopHold()">+</button> |
155 | 145 | <input type="number" id="h-in" class="timer-input" value="0"> |
156 | | - <button class="step-btn" |
157 | | - onmousedown="hold(event, 'h-in', -1)" |
158 | | - onmouseup="stopHold()" |
159 | | - onmouseleave="stopHold()" |
160 | | - ontouchstart="hold(event, 'h-in', -1)" |
161 | | - ontouchend="stopHold()">−</button> |
| 146 | + <button class="step-btn" onpointerdown="hold(event, 'h-in', -1)" onpointerup="stopHold()" onpointerleave="stopHold()">−</button> |
162 | 147 | </div> |
163 | | - <div class="separator" style="margin-top:-5px; opacity:0.5;">:</div> |
| 148 | + <div class="separator" style="opacity:0.5;">:</div> |
164 | 149 | <div style="display:flex; flex-direction:column; align-items:center; gap:14px;"> |
165 | | - <button class="step-btn" |
166 | | - onmousedown="hold(event, 'm-in', 1)" |
167 | | - onmouseup="stopHold()" |
168 | | - onmouseleave="stopHold()" |
169 | | - ontouchstart="hold(event, 'm-in', 1)" |
170 | | - ontouchend="stopHold()">+</button> |
| 150 | + <button class="step-btn" onpointerdown="hold(event, 'm-in', 1)" onpointerup="stopHold()" onpointerleave="stopHold()">+</button> |
171 | 151 | <input type="number" id="m-in" class="timer-input" value="25"> |
172 | | - <button class="step-btn" |
173 | | - onmousedown="hold(event, 'm-in', -1)" |
174 | | - onmouseup="stopHold()" |
175 | | - onmouseleave="stopHold()" |
176 | | - ontouchstart="hold(event, 'm-in', -1)" |
177 | | - ontouchend="stopHold()">−</button> |
| 152 | + <button class="step-btn" onpointerdown="hold(event, 'm-in', -1)" onpointerup="stopHold()" onpointerleave="stopHold()">−</button> |
178 | 153 | </div> |
179 | 154 | </div> |
180 | 155 | <button class="confirm-btn" onclick="saveEdit()">Apply Changes</button> |
|
195 | 170 | const chime = new Audio('https://actions.google.com/sounds/v1/alarms/beep_short.ogg'); |
196 | 171 |
|
197 | 172 | window.onload = () => { |
198 | | - app.s_sec = localStorage.getItem('s_sec') === 'true'; |
| 173 | + app.s_sec = localStorage.getItem('s_sec') !== 'false'; |
199 | 174 | app.s_ampm = localStorage.getItem('s_ampm') === 'true'; |
200 | 175 | document.getElementById('tog-sec').checked = app.s_sec; |
201 | 176 | document.getElementById('tog-ampm').checked = app.s_ampm; |
|
204 | 179 | }; |
205 | 180 |
|
206 | 181 | function handleGlobalClick(e) { |
207 | | - if (window.innerHeight < 500 && e.target.id === 'main-stage') { |
208 | | - toggleTimer(); |
209 | | - } else if (!document.fullscreenElement && !['INPUT', 'BUTTON', 'A'].includes(e.target.tagName)) { |
210 | | - document.documentElement.requestFullscreen().catch(() => {}); |
| 182 | + |
| 183 | + if (e.target.closest('.stage')) { |
| 184 | + |
| 185 | + if (window.innerHeight < 500) { |
| 186 | + toggleTimer(); |
| 187 | + } |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + function toggleFS() { |
| 192 | + if (!document.fullscreenElement) { |
| 193 | + document.documentElement.requestFullscreen().catch(()=>{}); |
| 194 | + } else { |
| 195 | + document.exitFullscreen().catch(()=>{}); |
211 | 196 | } |
212 | 197 | } |
213 | 198 |
|
|
220 | 205 | h = h % 12 || 12; |
221 | 206 | document.getElementById('c-sec').style.display = app.s_sec ? 'flex' : 'none'; |
222 | 207 | document.getElementById('p-sec').style.display = 'none'; |
223 | | - document.title = "Flow Clock"; |
224 | 208 | } else { |
225 | 209 | if (app.active && app.left > 0) { |
226 | 210 | app.left--; |
227 | | - if (app.left === 0) { chime.play(); app.active = false; updateIcon(); } |
| 211 | + if (app.left === 0) { |
| 212 | + app.active = false; updateIcon(); chime.play().catch(()=>{}); |
| 213 | + if (app.mode === 'focus') { showToast("FOCUS DONE!"); setTimeout(()=>setMode('break'), 1500); } |
| 214 | + else { showToast("BREAK DONE!"); setTimeout(()=>setMode('focus'), 1500); } |
| 215 | + } |
228 | 216 | } |
229 | 217 | if (app.left >= 3600) { h = Math.floor(app.left/3600); m = Math.floor((app.left%3600)/60); isPomoHour=true; } |
230 | 218 | else { h = Math.floor(app.left/60); m = app.left%60; } |
231 | 219 | s = app.left % 60; |
232 | 220 | document.getElementById('ampm').style.display = 'none'; |
233 | 221 | document.getElementById('c-sec').style.display = 'none'; |
234 | 222 | document.getElementById('p-sec').style.display = isPomoHour ? 'flex' : 'none'; |
235 | | - document.title = `[${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}] Flow`; |
236 | 223 | } |
237 | 224 | updateScaling(); |
238 | 225 | render(h, m, s); |
|
241 | 228 | function updateScaling() { |
242 | 229 | const isClock = app.mode === 'clock'; |
243 | 230 | const showSec = (isClock && app.s_sec) || (!isClock && app.left >= 3600); |
244 | | - const showAM = isClock && app.s_ampm; |
245 | 231 | document.body.classList.toggle('has-seconds', showSec); |
246 | | - document.body.classList.toggle('has-ampm', showAM); |
| 232 | + document.body.classList.toggle('has-ampm', isClock && app.s_ampm); |
247 | 233 | } |
248 | 234 |
|
249 | 235 | function render(v1, v2, v3) { |
|
281 | 267 | function closeModal(id) { document.getElementById(id).style.display = 'none'; } |
282 | 268 |
|
283 | 269 | function saveEdit() { |
284 | | - const h = Math.min(99, parseInt(document.getElementById('h-in').value) || 0); |
285 | | - const m = Math.min(59, parseInt(document.getElementById('m-in').value) || 0); |
286 | | - const total = (h * 3600) + (m * 60); |
287 | | - if (app.mode === 'focus') app.f = total; else app.b = total; |
| 270 | + app.f = (parseInt(document.getElementById('h-in').value) || 0) * 3600 + (parseInt(document.getElementById('m-in').value) || 0) * 60; |
288 | 271 | closeModal('edit-panel'); resetTimer(); |
289 | 272 | } |
290 | 273 |
|
291 | | - function step(id, d) { let i = document.getElementById(id); i.value = Math.max(0, parseInt(i.value)+d); } |
292 | | - |
293 | | - function hold(e, id, d) { |
294 | | - if (e.cancelable) e.preventDefault(); // Stop mobile "double-tap" count |
| 274 | + function hold(e, id, d) { |
295 | 275 | clearInterval(app.h_int); |
296 | | - step(id, d); |
297 | | - app.h_int = setInterval(() => step(id, d), 150); |
| 276 | + const step = () => { let i = document.getElementById(id); i.value = Math.max(0, parseInt(i.value)+d); }; |
| 277 | + step(); app.h_int = setInterval(step, 150); |
298 | 278 | } |
299 | | - |
300 | 279 | function stopHold() { clearInterval(app.h_int); } |
301 | 280 |
|
302 | 281 | function syncPrefs() { |
|
311 | 290 | const cur = document.documentElement.getAttribute('data-theme'); |
312 | 291 | const next = themes[(themes.indexOf(cur) + 1) % themes.length]; |
313 | 292 | document.documentElement.setAttribute('data-theme', next); |
| 293 | + localStorage.setItem('theme', next); |
314 | 294 | showToast(`${next.toUpperCase()} THEME`); |
315 | 295 | } |
316 | 296 |
|
317 | 297 | function showToast(m) { |
318 | 298 | const t = document.getElementById('toast'); t.textContent = m; |
319 | | - t.classList.add('show'); setTimeout(()=>t.classList.remove('show'), 1800); |
320 | | - } |
321 | | - |
322 | | - function toggleFS() { |
323 | | - if (!document.fullscreenElement) document.documentElement.requestFullscreen(); |
324 | | - else document.exitFullscreen(); |
| 299 | + t.classList.add('show'); setTimeout(()=>t.classList.remove('show'), 2000); |
325 | 300 | } |
326 | 301 | </script> |
327 | 302 | </body> |
|
0 commit comments