+ {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 {