Skip to content

Commit c41613f

Browse files
feat: add PWA support
1 parent 467372e commit c41613f

File tree

8 files changed

+141
-8
lines changed

8 files changed

+141
-8
lines changed

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ Since we all want our [SD cards to live a long and prosper life](http://raspberr
1010

1111
The module also includes a **RESTful API** for controlling all aspects of your mirror from other network-enabled devices and controllers--anything that can open a URL. See the [API README](API/README.md) for more info!
1212

13-
**New:** The module can now display a QR code on your mirror for easy mobile access - simply scan and connect!
13+
**New features:**
14+
15+
- 📱 **PWA Support**: Install the remote control as an app on your smartphone for easy access
16+
- 📷 **QR Code Display**: Show a QR code on your mirror for instant mobile connection - simply scan and connect!
1417

1518
## Screenshots
1619

@@ -100,6 +103,15 @@ You can also add multiple devices in an IP range (e.g. all devices with `192.168
100103
101104
- (5) Access the remote interface on [http://192.168.xxx.xxx:8080/remote.html](http://192.168.xxx.xxx:8080/remote.html) (replace with IP address of your RaspberryPi).
102105
106+
### Install as PWA (Progressive Web App)
107+
108+
You can install the remote control as an app on your smartphone:
109+
110+
1. Open the remote interface in your mobile browser (Chrome/Safari)
111+
2. On **Android** (Chrome): Tap the menu (⋮) → "Install app" or "Add to Home screen"
112+
3. On **iOS** (Safari): Tap Share (□↑) → "Add to Home Screen"
113+
4. The remote control will now work like a native app with offline support!
114+
103115
Note: If your user does not have `sudo` rights, the shutdown does not work (it _should_ work for everyone who did not change anything on this matter).
104116
105117
### Update

img/icon-192.png

12.9 KB
Loading

img/icon-512.png

13.4 KB
Loading

manifest.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "MagicMirror² Remote Control",
3+
"short_name": "MM Remote",
4+
"description": "Remote control for your MagicMirror²",
5+
"start_url": "/remote.html",
6+
"scope": "/",
7+
"display": "fullscreen",
8+
"background_color": "#000000",
9+
"theme_color": "#000000",
10+
"orientation": "portrait",
11+
"icons": [
12+
{
13+
"src": "modules/MMM-Remote-Control/img/icon-192.png",
14+
"sizes": "192x192",
15+
"type": "image/png",
16+
"purpose": "any maskable"
17+
},
18+
{
19+
"src": "modules/MMM-Remote-Control/img/icon-512.png",
20+
"sizes": "512x512",
21+
"type": "image/png",
22+
"purpose": "any maskable"
23+
}
24+
],
25+
"categories": ["utilities", "productivity"],
26+
"prefer_related_applications": false
27+
}

remote.html

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@
55
<meta charset="utf-8" />
66
<meta name="viewport" content="width=device-width, initial-scale=1" />
77
<meta name="google" content="notranslate" />
8+
9+
<!-- PWA Manifest -->
10+
<link rel="manifest" href="modules/MMM-Remote-Control/manifest.json" />
11+
12+
<!-- Theme colors for PWA -->
13+
<meta name="theme-color" content="#000000" />
14+
<meta name="msapplication-navbutton-color" content="#000000" />
15+
<meta name="msapplication-config" content="none" />
16+
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
17+
<meta name="apple-mobile-web-app-capable" content="yes" />
18+
<meta name="apple-mobile-web-app-title" content="MM Remote" />
19+
820
<!-- Modern SVG favicon with dark mode support -->
921
<link
1022
rel="icon"
@@ -17,15 +29,9 @@
1729
<link rel="stylesheet" href="modules/MMM-Remote-Control/remote.css" />
1830
<link
1931
rel="apple-touch-icon"
20-
href="modules/MMM-Remote-Control/img/apple-touch-icon.png"
32+
href="modules/MMM-Remote-Control/img/icon-192.png"
2133
/>
22-
<meta name="apple-mobile-web-app-title" content="MagicRemote" />
23-
<meta name="apple-mobile-web-app-capable" content="yes" />
2434
<meta name="mobile-web-app-capable" content="yes" />
25-
<meta
26-
name="apple-mobile-web-app-status-bar-style"
27-
content="black-translucent"
28-
/>
2935
</head>
3036

3137
<body>

remote.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2299,6 +2299,17 @@ Remote.init = function () {
22992299
}
23002300
});
23012301

2302+
// Register service worker for PWA support
2303+
if ("serviceWorker" in navigator) {
2304+
navigator.serviceWorker.register("modules/MMM-Remote-Control/service-worker.js").
2305+
then((registration) => {
2306+
console.log("Service Worker registered:", registration);
2307+
}).
2308+
catch((error) => {
2309+
console.log("Service Worker registration failed:", error);
2310+
});
2311+
}
2312+
23022313
// loading successful, remove error message
23032314
const loadError = document.querySelector("#load-error");
23042315
if (loadError) {

scripts/update-service-worker.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Updates service-worker.js with current version from package.json
5+
*/
6+
7+
const fs = require("node:fs");
8+
const path = require("node:path");
9+
10+
const packagePath = path.resolve(__dirname, "../package.json");
11+
const serviceWorkerPath = path.resolve(__dirname, "../service-worker.js");
12+
13+
// Read package.json
14+
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
15+
const version = packageJson.version;
16+
17+
// Read service-worker.js
18+
let serviceWorker = fs.readFileSync(serviceWorkerPath, "utf8");
19+
20+
// Update version in CACHE_NAME
21+
const newCacheName = `mmm-remote-control-v${version}`;
22+
serviceWorker = serviceWorker.replace(
23+
/const CACHE_NAME = "mmm-remote-control-v[\d.]+";/,
24+
`const CACHE_NAME = "${newCacheName}";`
25+
);
26+
27+
// Write updated service-worker.js
28+
fs.writeFileSync(serviceWorkerPath, serviceWorker, "utf8");
29+
30+
console.log(`✓ Service Worker cache updated to version ${version}`);

service-worker.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const CACHE_NAME = "mmm-remote-control-v3.3.2";
2+
const urlsToCache = [
3+
"/remote.html",
4+
"/css/main.css",
5+
"/css/roboto.css",
6+
"/css/font-awesome.css",
7+
"/modules/MMM-Remote-Control/remote.css",
8+
"/modules/MMM-Remote-Control/remote.js"
9+
];
10+
11+
// Install service worker and skip waiting
12+
self.addEventListener("install", (event) => {
13+
event.waitUntil((async () => {
14+
const cache = await caches.open(CACHE_NAME);
15+
try {
16+
await cache.addAll(urlsToCache);
17+
} catch (error) {
18+
console.warn("Cache addAll failed, continuing:", error);
19+
}
20+
globalThis.skipWaiting();
21+
})());
22+
});
23+
24+
// Cache and return requests
25+
self.addEventListener("fetch", (event) => {
26+
event.respondWith((async () => {
27+
const response = await caches.match(event.request);
28+
// Cache hit - return response
29+
if (response) {
30+
return response;
31+
}
32+
return fetch(event.request);
33+
})());
34+
});
35+
36+
// Update service worker
37+
self.addEventListener("activate", (event) => {
38+
event.waitUntil((async () => {
39+
const cacheNames = await caches.keys();
40+
await Promise.all(cacheNames.map((cacheName) => {
41+
if (cacheName !== CACHE_NAME) {
42+
return caches.delete(cacheName);
43+
}
44+
}));
45+
return globalThis.clients.claim();
46+
})());
47+
});

0 commit comments

Comments
 (0)