fix: avoid web fallback after app handoff
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 11s

This commit is contained in:
Kroonk
2026-05-21 23:05:09 +02:00
parent 705374cacd
commit fb540813ff
3 changed files with 85 additions and 24 deletions

105
app.js
View File

@@ -60,11 +60,87 @@ function isMobileDevice() {
return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent); return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
} }
function getMobileUrl(service) { function getMobilePlatform() {
if (service.id === 'drive') { const userAgent = navigator.userAgent || '';
return 'nextcloud://openFiles'; if (/Android/i.test(userAgent)) return 'android';
if (/iPhone|iPad|iPod/i.test(userAgent)) return 'ios';
return 'other';
} }
return service.mobileUrl;
function getMobileLaunch(service) {
if (service.id === 'drive') {
const fallbackUrl = service.url;
if (getMobilePlatform() === 'android') {
return {
url: `intent://openFiles#Intent;scheme=nextcloud;package=com.nextcloud.client;S.browser_fallback_url=${encodeURIComponent(fallbackUrl)};end`,
fallbackUrl
};
}
return {
url: 'nextcloud://openFiles',
fallbackUrl
};
}
if (!service.mobileUrl) return null;
return {
url: service.mobileUrl,
fallbackUrl: service.url
};
}
function openMobileLaunch(url, fallbackUrl) {
let didLeavePage = false;
let fallbackTimer = null;
let cleanupTimer = null;
let launcherFrame = null;
const cleanup = () => {
window.removeEventListener('pagehide', markPageLeft);
document.removeEventListener('visibilitychange', handleVisibilityChange);
if (fallbackTimer) window.clearTimeout(fallbackTimer);
if (cleanupTimer) window.clearTimeout(cleanupTimer);
if (launcherFrame) launcherFrame.remove();
};
const markPageLeft = () => {
didLeavePage = true;
cleanup();
};
const handleVisibilityChange = () => {
if (document.visibilityState === 'hidden') {
markPageLeft();
}
};
window.addEventListener('pagehide', markPageLeft, { once: true });
document.addEventListener('visibilitychange', handleVisibilityChange);
fallbackTimer = window.setTimeout(() => {
cleanup();
if (!didLeavePage && document.visibilityState === 'visible') {
window.location.href = fallbackUrl;
}
}, 2200);
if (/^https?:\/\//i.test(url) || /^intent:/i.test(url)) {
window.location.href = url;
return;
}
launcherFrame = document.createElement('iframe');
launcherFrame.hidden = true;
launcherFrame.style.display = 'none';
launcherFrame.setAttribute('aria-hidden', 'true');
document.body.appendChild(launcherFrame);
launcherFrame.src = url;
cleanupTimer = window.setTimeout(() => {
if (launcherFrame && launcherFrame.parentNode) {
launcherFrame.remove();
launcherFrame = null;
}
}, 2600);
} }
function createServiceCard(service) { function createServiceCard(service) {
@@ -92,28 +168,13 @@ function createServiceCard(service) {
<span class="card-domain">${service.domain}</span> <span class="card-domain">${service.domain}</span>
`; `;
const mobileUrl = getMobileUrl(service); const mobileLaunch = getMobileLaunch(service);
if (mobileUrl) { if (mobileLaunch) {
card.addEventListener('click', (e) => { card.addEventListener('click', (e) => {
if (!isMobileDevice()) return; if (!isMobileDevice()) return;
e.preventDefault(); e.preventDefault();
openMobileLaunch(mobileLaunch.url, mobileLaunch.fallbackUrl);
if (/^https?:\/\//i.test(mobileUrl)) {
window.location.href = mobileUrl;
return;
}
const start = Date.now();
// Try to open the native mobile app through its registered URL scheme.
window.location.href = mobileUrl;
// Web fallback if the app is not installed or the scheme is not handled.
setTimeout(() => {
if (Date.now() - start < 1500) {
window.open(service.url, '_blank');
}
}, 800);
}); });
} }

View File

@@ -119,6 +119,6 @@
</svg> </svg>
</div> </div>
<script src="/app.js?v=30" defer></script> <script src="/app.js?v=31" defer></script>
</body> </body>
</html> </html>

4
sw.js
View File

@@ -1,10 +1,10 @@
const CACHE_NAME = 'mischlabs-pwa-v13'; const CACHE_NAME = 'mischlabs-pwa-v14';
const APP_SHELL = [ const APP_SHELL = [
'/', '/',
'/index.html', '/index.html',
'/offline.html', '/offline.html',
'/style.css?v=27', '/style.css?v=27',
'/app.js?v=30', '/app.js?v=31',
'/services.json?v=1', '/services.json?v=1',
'/manifest.webmanifest?v=4', '/manifest.webmanifest?v=4',
'/favicon.ico?v=4', '/favicon.ico?v=4',