diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..190aa13 --- /dev/null +++ b/TODO.md @@ -0,0 +1 @@ +Installable PWAs need https... diff --git a/crates/cursor-move-webapp/Dioxus.toml b/crates/cursor-move-webapp/Dioxus.toml index 2e88b30..5bc332c 100644 --- a/crates/cursor-move-webapp/Dioxus.toml +++ b/crates/cursor-move-webapp/Dioxus.toml @@ -1,4 +1,6 @@ [application] +name = "cursor-move-webapp" +default_platform = "web" [web.app] @@ -19,3 +21,7 @@ script = [] # Javascript code file # serve: [dev-server] only script = [] + +[web.watcher] +reload_html = true +watch_path = ["src", "public"] diff --git a/crates/cursor-move-webapp/assets/favicon.ico b/crates/cursor-move-webapp/assets/favicon.ico index eed0c09..2f5df78 100644 Binary files a/crates/cursor-move-webapp/assets/favicon.ico and b/crates/cursor-move-webapp/assets/favicon.ico differ diff --git a/crates/cursor-move-webapp/index.html b/crates/cursor-move-webapp/index.html new file mode 100644 index 0000000..c04c750 --- /dev/null +++ b/crates/cursor-move-webapp/index.html @@ -0,0 +1,22 @@ + + + + {app_title} + + + + + + + + +
+ + diff --git a/crates/cursor-move-webapp/public/assets/logo_192.png b/crates/cursor-move-webapp/public/assets/logo_192.png new file mode 100644 index 0000000..f9c1e34 Binary files /dev/null and b/crates/cursor-move-webapp/public/assets/logo_192.png differ diff --git a/crates/cursor-move-webapp/public/assets/logo_512.png b/crates/cursor-move-webapp/public/assets/logo_512.png new file mode 100644 index 0000000..1a1fac2 Binary files /dev/null and b/crates/cursor-move-webapp/public/assets/logo_512.png differ diff --git a/crates/cursor-move-webapp/public/assets/manifest.json b/crates/cursor-move-webapp/public/assets/manifest.json new file mode 100644 index 0000000..1477271 --- /dev/null +++ b/crates/cursor-move-webapp/public/assets/manifest.json @@ -0,0 +1,34 @@ +{ + "name": "Cursor Mover", + "icons": [ + { + "src": "logo_192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo_512.png", + "type": "image/png", + "sizes": "512x512", + "purpose": "any" + }, + { + "src": "logo_512.png", + "type": "image/png", + "sizes": "any", + "purpose": "any" + } + ], + "start_url": "/", + "id": "/", + "display": "standalone", + "display_override": ["window-control-overlay", "standalone"], + "scope": "/", + "theme_color": "#000000", + "background_color": "#ffffff", + "short_name": "Cursr Mover", + "description": "A web control your cursor and keyboard via a smartphone.", + "dir": "ltr", + "lang": "en", + "orientation": "portrait" +} diff --git a/crates/cursor-move-webapp/public/assets/sw.js b/crates/cursor-move-webapp/public/assets/sw.js new file mode 100644 index 0000000..42e8d73 --- /dev/null +++ b/crates/cursor-move-webapp/public/assets/sw.js @@ -0,0 +1,198 @@ +"use strict"; + +//console.log('WORKER: executing.'); + +/* A version number is useful when updating the worker logic, + allowing you to remove outdated cache entries during the update. +*/ +var version = "v1.0.0::"; + +/* These resources will be downloaded and cached by the service worker + during the installation process. If any resource fails to be downloaded, + then the service worker won't be installed either. +*/ +var offlineFundamentals = [ + // add here the files you want to cache + //"favicon.ico", +]; + +/* The install event fires when the service worker is first installed. + You can use this event to prepare the service worker to be able to serve + files while visitors are offline. +*/ +self.addEventListener("install", function (event) { + //console.log('WORKER: install event in progress.'); + /* Using event.waitUntil(p) blocks the installation process on the provided + promise. If the promise is rejected, the service worker won't be installed. + */ + event.waitUntil( + /* The caches built-in is a promise-based API that helps you cache responses, + as well as finding and deleting them. + */ + caches + /* You can open a cache by name, and this method returns a promise. We use + a versioned cache name here so that we can remove old cache entries in + one fell swoop later, when phasing out an older service worker. + */ + .open(version + "fundamentals") + .then(function (cache) { + /* After the cache is opened, we can fill it with the offline fundamentals. + The method below will add all resources in `offlineFundamentals` to the + cache, after making requests for them. + */ + return cache.addAll(offlineFundamentals); + }) + .then(function () { + //console.log('WORKER: install completed'); + }), + ); +}); + +/* The fetch event fires whenever a page controlled by this service worker requests + a resource. This isn't limited to `fetch` or even XMLHttpRequest. Instead, it + comprehends even the request for the HTML page on first load, as well as JS and + CSS resources, fonts, any images, etc. +*/ +self.addEventListener("fetch", function (event) { + //console.log('WORKER: fetch event in progress.'); + + /* We should only cache GET requests, and deal with the rest of method in the + client-side, by handling failed POST,PUT,PATCH,etc. requests. + */ + if (event.request.method !== "GET") { + /* If we don't block the event as shown below, then the request will go to + the network as usual. + */ + //console.log('WORKER: fetch event ignored.', event.request.method, event.request.url); + return; + } + /* Similar to event.waitUntil in that it blocks the fetch event on a promise. + Fulfillment result will be used as the response, and rejection will end in a + HTTP response indicating failure. + */ + event.respondWith( + caches + /* This method returns a promise that resolves to a cache entry matching + the request. Once the promise is settled, we can then provide a response + to the fetch request. + */ + .match(event.request) + .then(function (cached) { + /* Even if the response is in our cache, we go to the network as well. + This pattern is known for producing "eventually fresh" responses, + where we return cached responses immediately, and meanwhile pull + a network response and store that in the cache. + + Read more: + https://ponyfoo.com/articles/progressive-networking-serviceworker + */ + var networked = fetch(event.request) + // We handle the network request with success and failure scenarios. + .then(fetchedFromNetwork, unableToResolve) + // We should catch errors on the fetchedFromNetwork handler as well. + .catch(unableToResolve); + + /* We return the cached response immediately if there is one, and fall + back to waiting on the network as usual. + */ + //console.log('WORKER: fetch event', cached ? '(cached)' : '(network)', event.request.url); + return cached || networked; + + function fetchedFromNetwork(response) { + /* We copy the response before replying to the network request. + This is the response that will be stored on the ServiceWorker cache. + */ + var cacheCopy = response.clone(); + + //console.log('WORKER: fetch response from network.', event.request.url); + + caches + // We open a cache to store the response for this request. + .open(version + "pages") + .then(function add(cache) { + /* We store the response for this request. It'll later become + available to caches.match(event.request) calls, when looking + for cached responses. + */ + cache.put(event.request, cacheCopy); + }) + .then(function () { + //console.log('WORKER: fetch response stored in cache.', event.request.url); + }); + + // Return the response so that the promise is settled in fulfillment. + return response; + } + + /* When this method is called, it means we were unable to produce a response + from either the cache or the network. This is our opportunity to produce + a meaningful response even when all else fails. It's the last chance, so + you probably want to display a "Service Unavailable" view or a generic + error response. + */ + function unableToResolve() { + /* There's a couple of things we can do here. + - Test the Accept header and then return one of the `offlineFundamentals` + e.g: `return caches.match('/some/cached/image.png')` + - You should also consider the origin. It's easier to decide what + "unavailable" means for requests against your origins than for requests + against a third party, such as an ad provider. + - Generate a Response programmatically, as shown below, and return that. + */ + + //console.log('WORKER: fetch request failed in both cache and network.'); + + /* Here we're creating a response programmatically. The first parameter is the + response body, and the second one defines the options for the response. + */ + return new Response("

Service Unavailable

", { + status: 503, + statusText: "Service Unavailable", + headers: new Headers({ + "Content-Type": "text/html", + }), + }); + } + }), + ); +}); + +/* The activate event fires after a service worker has been successfully installed. + It is most useful when phasing out an older version of a service worker, as at + this point you know that the new worker was installed correctly. In this example, + we delete old caches that don't match the version in the worker we just finished + installing. +*/ +self.addEventListener("activate", function (event) { + /* Just like with the install event, event.waitUntil blocks activate on a promise. + Activation will fail unless the promise is fulfilled. + */ + //console.log('WORKER: activate event in progress.'); + + event.waitUntil( + caches + /* This method returns a promise which will resolve to an array of available + cache keys. + */ + .keys() + .then(function (keys) { + // We return a promise that settles when all outdated caches are deleted. + return Promise.all( + keys + .filter(function (key) { + // Filter by keys that don't start with the latest version prefix. + return !key.startsWith(version); + }) + .map(function (key) { + /* Return a promise that's fulfilled + when each outdated cache is deleted. + */ + return caches.delete(key); + }), + ); + }) + .then(function () { + //console.log('WORKER: activate completed.'); + }), + ); +}); diff --git a/crates/cursor-move-webapp/src/components/controls.rs b/crates/cursor-move-webapp/src/components/controls.rs index 70083af..b16dfac 100644 --- a/crates/cursor-move-webapp/src/components/controls.rs +++ b/crates/cursor-move-webapp/src/components/controls.rs @@ -13,7 +13,7 @@ pub fn Controls() -> Element { #[css_module("/assets/styling/controls.module.css")] struct Styles; - let mut socket = use_websocket(move || mouse_move(WebSocketOptions::new())); + let mut socket = use_websocket(move || remote_control(WebSocketOptions::new())); use_future(move || async move { loop { @@ -34,12 +34,44 @@ pub fn Controls() -> Element { }); }); - rsx! { - div { - class: Styles::controls, - MouseArea { onevent: event_handler } - KeyboardArea { onevent: event_handler } - } + match *socket.status().read() { + dioxus_fullstack::WebsocketState::Connecting => { + rsx! { + div { + "Connecting..." + } + } + }, + dioxus_fullstack::WebsocketState::Open => { + rsx! { + div { + class: Styles::controls, + MouseArea { onevent: event_handler } + KeyboardArea { onevent: event_handler } + } + } + }, + dioxus_fullstack::WebsocketState::Closing => { + rsx! { + div { + "Closing..." + } + } + }, + dioxus_fullstack::WebsocketState::Closed => { + rsx! { + div { + "Closed..." + } + } + }, + dioxus_fullstack::WebsocketState::FailedToConnect => { + rsx! { + div { + "Failed to connect..." + } + } + }, } } @@ -60,8 +92,8 @@ enum ServerEvent { } #[expect(clippy::unused_async)] -#[get("/api/mouse_move_ws", mouse_service: State)] -async fn mouse_move( +#[get("/api/remote_control_wss", mouse_service: State)] +async fn remote_control( options: WebSocketOptions ) -> Result> { Ok(options.on_upgrade(move |mut socket| async move {