Skip to content

Commit 3982813

Browse files
committed
fix(ui) ios 26 quirks
1 parent f7dbaca commit 3982813

File tree

7 files changed

+151
-35
lines changed

7 files changed

+151
-35
lines changed

docs/install-pwa.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ This document provides step-by-step instructions for installing a Progressive We
8080

8181
## 2. Desktop Safari (macOS)
8282

83-
![safari install](https://raw.githubusercontent.com/wiki/xero/text0wnz/img/pwa-ios.png)
83+
![safari install](https://raw.githubusercontent.com/wiki/xero/text0wnz/img/pwa-safari.png)
8484

8585
**Steps:**
8686

@@ -113,16 +113,16 @@ This document provides step-by-step instructions for installing a Progressive We
113113

114114
---
115115

116-
## 4. iPadOS (Safari)
116+
## 4. iPadOS / iOS (Safari)
117117

118118
![ipad install](https://raw.githubusercontent.com/wiki/xero/text0wnz/img/pwa-ios.png)
119119

120120
**Steps:**
121121

122-
1. Open the website in Safari on your iPad.
122+
1. Open the website in Safari on your iPad or iPhone.
123123
2. Tap the "Share" button (square with an arrow pointing up).
124-
3. Tap “Add to Home Screen.”
125-
4. Enter a name for the app and tap "Add."
124+
3. Tap the "More" button (Three dots).
125+
4. Tap “Add to Home Screen.”
126126
5. The web app will appear on your home screen and can be launched like any other app.
127127

128128
**What you get after installing:**

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export default [
3434
Uint16Array: 'readonly',
3535
Uint32Array: 'readonly',
3636
Uint8Array: 'readonly',
37+
URLSearchParams: 'readonly',
3738
WebSocket: 'readonly',
3839
Worker: 'readonly',
3940
alert: 'readonly',

src/css/style.css

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,14 +200,16 @@
200200
@apply w-full flex justify-between h-[var(--header-size)];
201201

202202
&.dynamic {
203+
-webkit-app-region: drag;
203204
app-region: drag;
204-
padding-left: env(titlebar-area-x, 0px);
205-
padding-right: calc(env(titlebar-area-x, 0px) * 1.6);
205+
padding-left: calc(env(titlebar-area-x, 0px) + 2px);
206+
padding-right: calc(env(titlebar-area-x, 0px) + 25px);
206207
}
207208
* {
208209
@apply my-auto;
209210
}
210211
nav {
212+
-webkit-app-region: no-drag;
211213
app-region: no-drag;
212214
div {
213215
@apply flex;
@@ -1177,6 +1179,10 @@
11771179
}
11781180
}
11791181

1182+
.iosWin header.dynamic {
1183+
padding-left: 75px;
1184+
}
1185+
11801186
.includedForWebsocket {
11811187
@apply hidden;
11821188
}

src/js/client/main.js

Lines changed: 89 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ let saveTimeout;
7474
let reload;
7575
let palettePicker;
7676
let pendingFile = null;
77+
let navFullscreen;
7778

7879
const $$$$ = () => {
7980
htmlDoc = $$('html');
@@ -101,6 +102,7 @@ const $$$$ = () => {
101102
metaTheme = $$('meta[name="theme-color"]');
102103
saveTimeout = null;
103104
reload = $('updateReload');
105+
navFullscreen = $('fullscreen');
104106
};
105107

106108
const openHandler = file => {
@@ -164,9 +166,16 @@ const handleLaunchFiles = () => {
164166
const fileHandle = launchParams.files[0];
165167
try {
166168
const file = await fileHandle.getFile();
167-
console.log(`[Launch] File queued: ${file.name} (${file.size} bytes)`);
168-
// Store the file to open after initialization
169-
pendingFile = file;
169+
// If app is already initialized, open immediately
170+
if (
171+
State.textArtCanvas &&
172+
!bodyContainer?.classList.contains('loading')
173+
) {
174+
openHandler(file);
175+
} else {
176+
// Otherwise store for later
177+
pendingFile = file;
178+
}
170179
} catch (error) {
171180
console.error('[App] Error reading launched file:', error);
172181
}
@@ -209,7 +218,23 @@ const save = () => {
209218
}, 300);
210219
};
211220

212-
const isIOS = () => (/iPad|iPhone|iPod/).test(navigator.userAgent);
221+
const isIOS = (/iPad|iPhone|iPod/).test(navigator.userAgent);
222+
223+
const handleIOS = () => {
224+
const isWindowed =
225+
window.navigator.standalone ||
226+
window.matchMedia('(display-mode: standalone)').matches ||
227+
window.matchMedia('(display-mode: window-controls-overlay)').matches;
228+
const isMaximized =
229+
window.innerWidth >= window.screen.availWidth &&
230+
window.innerHeight >= window.screen.availHeight * 0.95;
231+
const isWebkitFullscreen = !!(
232+
document.webkitFullscreenElement || document.webkitCurrentFullScreenElement
233+
);
234+
const needsPadding = isWebkitFullscreen || (isWindowed && !isMaximized);
235+
htmlDoc.classList.toggle('ios', needsPadding);
236+
navFullscreen.classList.toggle('disabled', isWindowed);
237+
};
213238

214239
document.addEventListener('DOMContentLoaded', async () => {
215240
// Initialize service worker
@@ -230,6 +255,39 @@ document.addEventListener('DOMContentLoaded', async () => {
230255
};
231256
};
232257
});
258+
259+
// Listen for shared file messages from service worker
260+
navigator.serviceWorker.addEventListener('message', async event => {
261+
if (event.data.type === 'SHARED_FILE_READY') {
262+
console.log('[Share] File shared:', event.data.filename);
263+
// Wait for app to be ready
264+
const waitForApp = () => {
265+
return new Promise(resolve => {
266+
if (State.textArtCanvas && openHandler) {
267+
resolve();
268+
} else {
269+
setTimeout(() => waitForApp().then(resolve), 100);
270+
}
271+
});
272+
};
273+
await waitForApp();
274+
const sharedFileData = await handleFileShare();
275+
if (sharedFileData) {
276+
pendingFile = sharedFileData.file;
277+
openHandler(pendingFile);
278+
pendingFile = null;
279+
// Clean up the cached file
280+
caches
281+
.open(CACHE_ID)
282+
.then(cache => cache.delete(CACHE_DATA))
283+
.catch(error =>
284+
console.error(
285+
'[Error] Failed to cleanup shared file cache:',
286+
error,
287+
));
288+
}
289+
}
290+
});
233291
}
234292

235293
try {
@@ -312,6 +370,16 @@ const initializeAppComponents = async () => {
312370
if (!pendingFile) {
313371
State.restoreStateFromLocalStorage();
314372
}
373+
// iOS quirks
374+
if (isIOS) {
375+
// Make file picker accept all files on iOS
376+
openFile.setAttribute('accept', '*/*');
377+
// track state to offset traffic lights
378+
handleIOS();
379+
document.addEventListener('webkitfullscreenchange', handleIOS);
380+
window.addEventListener('resize', handleIOS);
381+
}
382+
315383
document.addEventListener('keydown', undoAndRedo);
316384
createResolutionController(
317385
$('resolutionLabel'),
@@ -404,11 +472,6 @@ const initializeAppComponents = async () => {
404472
onFileChange(openFile, openHandler);
405473
createDragDropController(openHandler, $('dragdrop'));
406474

407-
if (isIOS()) {
408-
// Make file picker accept all files on iOS
409-
openFile.setAttribute('accept', '*/*');
410-
}
411-
412475
onClick(navSauce, () => {
413476
State.menus.close();
414477
State.modal.open('sauce');
@@ -500,7 +563,7 @@ const initializeAppComponents = async () => {
500563
onClick($('eraseColumn'), keyboard.eraseColumn);
501564
onClick($('eraseColumnStart'), keyboard.eraseToStartOfColumn);
502565
onClick($('eraseColumnEnd'), keyboard.eraseToEndOfColumn);
503-
onClick($('fullscreen'), toggleFullscreen);
566+
onClick(navFullscreen, toggleFullscreen);
504567

505568
onClick($('defaultColor'), () => {
506569
State.palette.setForegroundColor(7);
@@ -827,25 +890,28 @@ const initializeAppComponents = async () => {
827890
document.addEventListener('onIceColorsChange', save);
828891
document.addEventListener('onOpenedFile', save);
829892

830-
// Handle pending launched file
893+
// Handle pending launchQueue file
831894
if (pendingFile) {
832-
console.log(`[Launch] Opening launched file: ${pendingFile.name}`);
833895
openHandler(pendingFile);
834896
pendingFile = null;
835897
return; // Exit early, don't check for shared files
836898
}
837899

838-
// Check for shared files
839-
const sharedFileData = await handleFileShare();
840-
if (sharedFileData) {
841-
console.log(`[Share] Opening shared file: ${sharedFileData.filename}`);
842-
openHandler(sharedFileData.file);
843-
// Clean up the cached file
844-
caches
845-
.open(CACHE_ID)
846-
.then(cache => cache.delete(CACHE_DATA))
847-
.catch(error =>
848-
console.error('[Error] Failed to cleanup shared file cache:', error));
900+
// Handle share target files
901+
const urlParams = new URLSearchParams(window.location.search);
902+
const source = urlParams.get('source');
903+
if (source === 'share') {
904+
const sharedFileData = await handleFileShare();
905+
if (sharedFileData) {
906+
console.log(`[Share] Opening shared file: ${sharedFileData.filename}`);
907+
openHandler(sharedFileData.file);
908+
// Clean up the cached file
909+
caches
910+
.open(CACHE_ID)
911+
.then(cache => cache.delete(CACHE_DATA))
912+
.catch(error =>
913+
console.error('[Error] Failed to cleanup shared file cache:', error));
914+
}
849915
}
850916
};
851917

src/js/client/ui.js

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,43 @@ const createCanvas = (width, height) => {
1818
};
1919

2020
const toggleFullscreen = () => {
21-
if (document.fullscreenEnabled) {
22-
if (document.fullscreenElement) {
23-
document.exitFullscreen();
21+
// Check if any fullscreen API is available
22+
const isFullscreenEnabled =
23+
document.fullscreenEnabled || document.webkitFullscreenEnabled;
24+
25+
if (!isFullscreenEnabled) {
26+
console.warn('[UI] Fullscreen not supported');
27+
$('fullscreen').classList.add('disabled');
28+
return;
29+
}
30+
31+
try {
32+
const fullscreenElement =
33+
document.fullscreenElement ||
34+
document.webkitFullscreenElement ||
35+
document.webkitCurrentFullScreenElement; // Safari specific
36+
37+
if (fullscreenElement) {
38+
if (document.exitFullscreen) {
39+
document.exitFullscreen();
40+
} else if (document.webkitExitFullscreen) {
41+
document.webkitExitFullscreen();
42+
} else if (document.webkitCancelFullScreen) {
43+
document.webkitCancelFullScreen(); // Older Safari
44+
}
2445
} else {
25-
document.documentElement.requestFullscreen();
46+
const element = document.documentElement;
47+
48+
if (element.requestFullscreen) {
49+
element.requestFullscreen();
50+
} else if (element.webkitRequestFullscreen) {
51+
element.webkitRequestFullscreen();
52+
} else if (element.webkitRequestFullScreen) {
53+
element.webkitRequestFullScreen(); // Older Safari (capital S)
54+
}
2655
}
56+
} catch {
57+
console.error('[UI] Failed toggling fullscreen mode');
2758
}
2859
};
2960

src/service.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,20 @@ async function handleShareTarget(request) {
3232
'X-Filename': file.name || 'untitled.txt',
3333
'X-File-Size': file.size.toString(),
3434
'X-Shared-At': Date.now().toString(),
35+
'X-Source': 'share-target',
3536
},
3637
}),
3738
);
39+
const clients = await self.clients.matchAll({ type: 'window' });
40+
clients.forEach(client => {
41+
client.postMessage({
42+
type: 'SHARED_FILE_READY',
43+
filename: file.name,
44+
size: file.size,
45+
});
46+
});
3847
}
39-
return Response.redirect('/', 303);
48+
return Response.redirect('/?source=share', 303);
4049
} catch (error) {
4150
console.error('[ServiceWorker] Error handling share target:', error);
4251
return Response.redirect('/', 303);

vite.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,9 @@ export default ({ mode }) => {
322322
}
323323
}
324324
],
325+
launch_handler: {
326+
client_mode: 'focus-existing',
327+
},
325328
// fav/app icons
326329
icons: [{
327330
src: `${uiDir}img/web-app-manifest-512x512.png`,

0 commit comments

Comments
 (0)