3 Commits
Author SHA1 Message Date
nmemmertandClaude Sonnet 5.5 b0d60d0a5b Fix chat sliding when the iPhone keyboard opens
build-container / build (push) Successful in 4m20s
Size the shell to the visible area while a field is focused, tuck the bottom bar away, and refit the chat as the keyboard comes and goes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 08:11:53 -04:00
nmemmertandClaude Sonnet 5.5 71e759e477 Transcribe with faster-whisper-base by default
Much faster than -small on a CPU-only host; TRANSCRIBE_MODEL still overrides it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 07:47:55 -04:00
Claude 24dd05db5d Merge video-restart
build-container / build (push) Successful in 4m39s
2026-10-27 02:16:30 +00:00
5 changed files with 32 additions and 3 deletions

No files matched your search

+1
View File
@@ -23,6 +23,7 @@ services:
MS_CLIENT_SECRET: ${MS_CLIENT_SECRET:-} MS_CLIENT_SECRET: ${MS_CLIENT_SECRET:-}
LYRICS_API_URL: http://127.0.0.1:9999 # the bundled lyrics service (Lyrica, below, shares this container's network); remove this line and the service to skip it LYRICS_API_URL: http://127.0.0.1:9999 # the bundled lyrics service (Lyrica, below, shares this container's network); remove this line and the service to skip it
TRANSCRIBE_API_URL: http://127.0.0.1:8000 # the bundled speech-to-text service (whisper, below): makes a transcript for a podcast or audiobook on request. Remove this line and the service to skip it TRANSCRIBE_API_URL: http://127.0.0.1:8000 # the bundled speech-to-text service (whisper, below): makes a transcript for a podcast or audiobook on request. Remove this line and the service to skip it
TRANSCRIBE_MODEL: ${TRANSCRIBE_MODEL:-Systran/faster-whisper-base} # smaller and about 3x faster than -small on a CPU-only host; set Systran/faster-whisper-small for better accuracy
SIGNAL_API_URL: http://127.0.0.1:8085 # the bundled Signal bridge (below, shares this container's network); remove this line and the service to skip Signal SIGNAL_API_URL: http://127.0.0.1:8085 # the bundled Signal bridge (below, shares this container's network); remove this line and the service to skip Signal
OPENAUDIBLE_URL: ${OPENAUDIBLE_URL:-} # audiobooks straight from OpenAudible (e.g. http://172.16.16.106:8000); the username and password in Settings -> Audiobooks sign-in are OpenAudible's. Leave empty to go through the audiobook player instead OPENAUDIBLE_URL: ${OPENAUDIBLE_URL:-} # audiobooks straight from OpenAudible (e.g. http://172.16.16.106:8000); the username and password in Settings -> Audiobooks sign-in are OpenAudible's. Leave empty to go through the audiobook player instead
PODCAST_URL: ${PODCAST_URL:-https://podcast.necloud.us/} PODCAST_URL: ${PODCAST_URL:-https://podcast.necloud.us/}
+3 -2
View File
@@ -345,10 +345,11 @@ async function chAnnounceCard(el) {
function chFit() { function chFit() {
const m = chMounted() && ch.root.querySelector('.sgm'); if (!m || m.offsetParent === null) return; const m = chMounted() && ch.root.querySelector('.sgm'); if (!m || m.offsetParent === null) return;
if (window.innerWidth >= 1024) { m.style.height = ''; return; } if (window.innerWidth >= 1024) { m.style.height = ''; return; }
let bottom = window.visualViewport ? window.visualViewport.height : window.innerHeight; let bottom = window.visualViewport ? window.visualViewport.offsetTop + window.visualViewport.height : window.innerHeight; // (page coordinates: the visible area can be panned down while the keyboard is up)
const top0 = m.getBoundingClientRect().top; // (look up from the screen's edge for the first spot that is page content, not the bottom bar or mini player) const top0 = m.getBoundingClientRect().top; // (look up from the screen's edge for the first spot that is page content, not the bottom bar or mini player)
for (let y = Math.floor(bottom) - 2; y > top0 + 130; y -= 4) { const hit = document.elementsFromPoint(innerWidth / 2, y).find(e => !e.closest('.sgm')); const covers = hit && !hit.closest('#view'); if (!covers) { bottom = y + 4; break; } } for (let y = Math.floor(bottom) - 2; y > top0 + 130; y -= 4) { const hit = document.elementsFromPoint(innerWidth / 2, y).find(e => !e.closest('.sgm')); const covers = hit && !hit.closest('#view'); if (!covers) { bottom = y + 4; break; } }
const top = m.getBoundingClientRect().top; m.style.height = Math.max(260, Math.floor(bottom - top - 10)) + 'px'; const top = m.getBoundingClientRect().top; m.style.height = Math.max(document.body.classList.contains('kb') ? 150 : 260, Math.floor(bottom - top - 10)) + 'px';
} }
window.addEventListener('kbfit', () => { chFit(); if (!chMounted() || !chq('.sgm')) return; const v = document.getElementById('view'); if (v) v.scrollTop = 0; const l = chq('#chmsgs'); if (l && document.activeElement && document.activeElement.id === 'chtext') l.scrollTop = l.scrollHeight; }); // (the keyboard came or went: refit, and keep the newest message in sight)
window.addEventListener('resize', chFit); if (window.visualViewport) window.visualViewport.addEventListener('resize', chFit); window.addEventListener('resize', chFit); if (window.visualViewport) window.visualViewport.addEventListener('resize', chFit);
setInterval(chFit, 700); setInterval(chFit, 700);
+19
View File
@@ -68,3 +68,22 @@
else if (!e.shiftKey && (document.activeElement === last || !s.contains(document.activeElement))) { e.preventDefault(); first.focus(); } else if (!e.shiftKey && (document.activeElement === last || !s.contains(document.activeElement))) { e.preventDefault(); first.focus(); }
}); });
})(); })();
// ---- iPhone keyboard: iOS shrinks only the visual viewport when the keyboard opens and pans the whole page to reveal the field, so the app used to slide up with it.
// Pin the shell to what is really visible while typing and tuck the bottom bar away; chat and mail size themselves again on 'kbfit'.
(function () {
const vv = window.visualViewport; if (!vv || !matchMedia('(pointer: coarse)').matches) return;
const root = document.documentElement; let raf = 0;
const typing = () => { const a = document.activeElement; return !!a && (a.tagName === 'TEXTAREA' || a.isContentEditable || (a.tagName === 'INPUT' && !/^(checkbox|radio|button|submit|reset|range|file|color|image)$/.test(a.type))); };
function apply() {
raf = 0;
const kb = typing() && window.innerHeight - vv.height > 120 && window.innerWidth < 768;
document.body.classList.toggle('kb', kb);
if (kb) { root.style.setProperty('--vvh', vv.height + 'px'); root.style.setProperty('--vvt', vv.offsetTop + 'px'); if (window.scrollY) window.scrollTo(0, 0); }
else { root.style.removeProperty('--vvh'); root.style.removeProperty('--vvt'); }
window.dispatchEvent(new Event('kbfit'));
}
const soon = () => { if (!raf) raf = requestAnimationFrame(apply); };
vv.addEventListener('resize', soon); vv.addEventListener('scroll', soon);
document.addEventListener('focusin', soon); document.addEventListener('focusout', () => setTimeout(soon, 80));
})();
+8
View File
@@ -264,6 +264,14 @@ body { -webkit-touch-callout: none; }
button, a, .row { -webkit-user-select: none; user-select: none; } button, a, .row { -webkit-user-select: none; user-select: none; }
input, textarea, [contenteditable], .sheet p, .card p, #view .sub { -webkit-user-select: text; user-select: text; } input, textarea, [contenteditable], .sheet p, .card p, #view .sub { -webkit-user-select: text; user-select: text; }
/* iPhone keyboard (shell.js adds body.kb while a field is focused and the keyboard is up): the shell is exactly the visible area, with no bottom bar in the way */
@media (max-width: 767px) {
body.kb { overflow: hidden; }
body.kb #shell { height: var(--vvh, 100dvh); transform: translateY(var(--vvt, 0px)); }
body.kb #nav, body.kb #player, body.kb #upd, body.kb #toast { display: none; }
body.kb .sgm { min-height: 0; }
}
/* "A new version is available" notice */ /* "A new version is available" notice */
#upd { position: fixed; left: 50%; transform: translateX(-50%); bottom: calc(var(--nav-h) + var(--sab) + 70px); z-index: 30; display: flex; align-items: center; gap: 12px; #upd { position: fixed; left: 50%; transform: translateX(-50%); bottom: calc(var(--nav-h) + var(--sab) + 70px); z-index: 30; display: flex; align-items: center; gap: 12px;
background: var(--text); color: var(--bg); padding: 8px 8px 8px 16px; border-radius: 14px; box-shadow: 0 6px 24px rgba(0,0,0,.35); max-width: 92vw; } background: var(--text); color: var(--bg); padding: 8px 8px 8px 16px; border-radius: 14px; box-shadow: 0 6px 24px rgba(0,0,0,.35); max-width: 92vw; }
+1 -1
View File
@@ -1,5 +1,5 @@
// App-shell cache only; Seafile/API traffic always goes to the network. // App-shell cache only; Seafile/API traffic always goes to the network.
const CACHE = 'hub-v88'; const CACHE = 'hub-v89';
const SHARE = 'hub-share'; // files shared into the app from other apps, held until the person picks a folder const SHARE = 'hub-share'; // files shared into the app from other apps, held until the person picks a folder
const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png']; const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png'];