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);
}
function getMobileUrl(service) {
if (service.id === 'drive') {
return 'nextcloud://openFiles';
function getMobilePlatform() {
const userAgent = navigator.userAgent || '';
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) {
@@ -92,28 +168,13 @@ function createServiceCard(service) {
<span class="card-domain">${service.domain}</span>
`;
const mobileUrl = getMobileUrl(service);
if (mobileUrl) {
const mobileLaunch = getMobileLaunch(service);
if (mobileLaunch) {
card.addEventListener('click', (e) => {
if (!isMobileDevice()) return;
e.preventDefault();
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);
openMobileLaunch(mobileLaunch.url, mobileLaunch.fallbackUrl);
});
}

View File

@@ -119,6 +119,6 @@
</svg>
</div>
<script src="/app.js?v=30" defer></script>
<script src="/app.js?v=31" defer></script>
</body>
</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 = [
'/',
'/index.html',
'/offline.html',
'/style.css?v=27',
'/app.js?v=30',
'/app.js?v=31',
'/services.json?v=1',
'/manifest.webmanifest?v=4',
'/favicon.ico?v=4',