Repository navigation
feat(remote): add remote control settings - #322
Merged
Merged
Conversation
# Conflicts: # Cargo.lock
… Linux The platform layer around mvp_core::remote's Target. One tokio task owns the Target and carries out its actions: it listens on TCP (port 47130 when it's free), advertises _cinette._tcp through the discovery module, runs each connection's reader and writer, and calls tick at the core's next deadline. Time comes from a clock that counts sleep (CLOCK_BOOTTIME on Linux, CLOCK_MONOTONIC on macOS), so a session whose 30 s grace ran out while the computer slept ends instead of resuming without asking. The device key is a 0600 file in the app data folder, replaced in one step. Catalog queries are answered from the cache, with every item built by CatalogItem::from_channel. Commands for the player go to the webview as remote://command. The player carries them out through its playback destination, so a remote load is the target's own play and goes in its history, and it sends its state back through remote_set_state while a controller is connected. A command the player doesn't finish in 10 s fails.
Starts the target through the mock app on a loopback port, with an advertiser that only records, and checks the settings file, the consent, sessions and command events, state and catalog answers, a new identity and quitting against a phone on a real socket. The loopback controller moves into a test module both test files share.
Browses _cinette._tcp and keeps a list of the other devices, leaving out this one and records that don't read as Cinette protocol 1. The device being controlled stays listed as unavailable when discovery loses it. A link task per target owns the core's Controller: it connects to each address in turn, runs the handshake, reports the code and the target's answer, and passes commands and catalog queries through, with a ping after 10 s of quiet. A connection that drops without the target closing it reconnects with backoff and the session id, so the session resumes without a dialog, until the target's 30 s grace runs out, counted with time asleep. Each link has a generation, so events from a link that was left or replaced never reach the webview. The webview gets remote://devices, remote://controller and remote://state, and the remote_devices, remote_connect, remote_leave, remote_command and remote_catalog commands. On the frontend, a CinetteDestination plays the other device's items by id, and the remote-control store holds the devices and the link.
…l bars - The device picker lists this device, other Cinette devices (TVs first), and room for Cast and AirPlay, each group under its heading. A row that can't be chosen stays listed, greyed out, with the reason. A route this device has to convert asks before it starts. The Devices button in the sidebar opens it. - The consent dialog on the target names the device and its type, shows the code in large digits and counts down. Closing it denies. - While this device asks another, a dialog shows the same code and Cancel. - While it controls another, a bar on every page says which device plays and what, with play, pause and stop, and Return to independent playback. A link that ends by itself says why once. - While other devices control this one, a bar names them, each with Disconnect. - The channels page reads the other device's catalog while it's the one that plays: groups, pages of items, series episodes and search.
A Remote control card in Settings, on macOS and Linux: - Discoverable on this network, on by default, with text saying that turning it off stops new connections and leaves connected devices connected until they're disconnected. - Remote control: Always allow, Only when allowed (the default) and Never allow, each with a line saying what it means. Never allow ends every session at once. - The device name, with the device type as the placeholder, saved when the field loses focus or on Enter. - The devices controlling this one, each with Disconnect. - Reset device identity, after a confirm step.
The consent dialog's countdown moves into a component of its own, keyed by request, so each request gets a clock that starts when it arrives. The remote catalog keeps one view state (loading, failed or ready) in place of three flags. Class names in constants go through cn(), which the mutation run leaves out. New tests cover the picker's icons and spinner, opening it again, the remote catalog's failures, search delay, going back from a series found by search, and the rest.
Commands keep their own tokens and a closed connection only drops its own; a command fails at its deadline, not at the next look at the clock; a player may take its time within the limit; an episode keeps its show, season and number; a key file the app may not read is an error and is left alone. Removing a leftover temporary key file has a skip comment: another error leaves it in place and create_new then fails on it.
The saved settings and the typed name share one state, so nothing is shown before they load. New tests cover a late first load, an empty default name, the On and Off buttons, and that only Enter saves.
The On state now leaves the variant out, so the button's default applies, in place of naming it; cva gave an empty name the default too, which no test could tell apart.
…rors iOS registers the remote_* commands so the frontend's calls are the same everywhere, but only their error path builds there. The settings store names build on macOS and Linux only, and the commands module allows the types it doesn't read on other platforms.
…ller opens it An Xtream series asked for by a controller is now loaded from the provider, as the series list does when it opens one, with the cached episodes when that fails or finds none. The target keeps the episodes of the last four series it answered, so a load of one of them comes with the episode: the player's channel list has no episodes to find it in.
The App and the player now have tests that fail without the remote target hooks. useRemoteSessions takes anything but a list as none, and its tests cover a late first load and a player that changes. Two dependency lists that never change get disable comments.
The link's handshake and ping times are now part of its config, so tests can shorten them. New tests cover a silent device, a slow consent answer, pings only after a quiet spell, the growing wait between attempts, and a device that leaves discovery for a moment. A request that comes while the link waits to reconnect is dropped, which answers it the same way the removed refuse did. Events from a replaced link are tested to stay away from the webview.
…link's codes A state from the other device that came before the load's answer is no longer overwritten with an opening state, which also drops the old item's tracks now. The link's own error codes start with link_.
# Conflicts: # apps/player/src-tauri/src/commands/tests/mod.rs # apps/player/src/components/player/VideoPlayer.test.tsx # apps/player/src/components/player/VideoPlayer.tsx # apps/player/src/lib/tauri.ts
# Conflicts: # apps/player/src/lib/tauri.ts
# Conflicts: # apps/player/src/hooks/useRemoteTarget.ts
…rches from paging clean_name also drops format characters, such as bidi overrides and zero-width spaces, and line and paragraph separators. A search page cut to fit a frame has no next, since a search has no cursor to go on from.
…at Linux can't tell apart
remoteSetState calls go out one after another, so a playing state still on its way can't land after the idle one a closing player sends. New bucket tests catch its refill mutants. The mutants no test can tell apart are skipped: the window calls the mock can't see, adding nothing earned, and the load gap's < against <=. The local network test passes over an interface with no subnet.
5 of 8 tasks
…wrong key A decoy that closes, says nothing or breaks the handshake before the key is checked no longer ends a connect or a resume: the next address is tried, and with none left the link ends with the most telling reason. A connect numbered at or below a link already left is refused, so a leave that overtook it can't leave a link no one tracks.
5 of 8 tasks
…dent Allow works only a second after a request shows or the window gains focus, and ignores a click that began before it showed. Names are set off from the text around them. The controllers bar lists a device that's reconnecting, the picker looks before it says nothing was found, names the same show the start of their ids, and rows keep still under the pointer. A failed cancel says so, a link that comes back keeps the pages loaded, a search says it's one, a request raised before the page listened shows, and the connect dialog and catalog stay off phones.
5 of 8 tasks
# Conflicts: # CHANGELOG.md
… device runs Each save builds on the device's last answer when its turn comes, so a change that failed isn't sent again with the next one, and after a failure the card shows what the device runs. A change shows at once, Always allow asks first, and every choice says what it means before it's made. Session names are set apart and wrap, two with one name show the start of their ids, a reply no longer clears a name being typed, a load error says remote control didn't start, a reset says it worked, and fitName moves to src/lib.
…f a held picker order The remote bar's notice shows outside the link's branch, since leaving clears the link here before the backend answers, and it says what to do rather than to try again. The picker lets go of the order it held when it closes or opens. Quotes sit outside a name's bdi, and the confirm step's heading sets the name apart too.
A save on its way says what the device runs, so a reset that answers after it no longer puts the older settings back. Tests also cover a save queued behind one that failed, and the card on the settings page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Remote control settings on macOS and Linux (spec 4 A and B). This is the last step of remote control on the desktop. With it, two Cinette instances on one network can find each other, ask, allow and control.
This builds on #321, which is merged in, and merges last of the four, right after #318, #319 and #321.
devis merged in, and the changelog line sits under## [Unreleased],### Added, after dev's two, above### Changed.A Remote control card in Settings:
update_settings, and feat(remote): run this device as a remote-control target on macOS and Linux #318's tests check it.The card shows nothing where remote control doesn't run (iOS and Android for now), and says so when it didn't start. Settings are kept under
remoteinsettings.json.CHANGELOG.mdgets one entry under Unreleased, Added, for remote control as a whole.Since the first version
A fresh-eyes review found these, all fixed:
After the #322 review
settings.jsoncan't be written). The error says the choice lasts only until Cinette quits. If the device can't be asked either, the card goes back to the last answer. A tested case: a change that failed and that the device dropped isn't sent with the next save.<bdi>, wrap long names, keep Disconnect on the card, and show the start of the device id when two names match, in the row and in Disconnect's label.showisn't a missing dependency;fitNamemoved tosrc/lib/remote-devices.tswith a test.set_settingsputs the store's old value back when the save fails (in feat(remote): run this device as a remote-control target on macOS and Linux #318).Follow-ups, not fixed here:
setNameandsaveupdate the form from the last render rather than with a functional update.How it was tested
On Linux, with no display:
Vitest: 31 tests for the card cover:
Settings' own tests check the card is on the page.
StrykerJS: every mutant on this PR's changed lines is caught. One
// Stryker disable next-line ArrayDeclarationsits on the first load's[]: a list that never changes runs the effect once either way.Still needs hardware: the user's Mac and Linux PC on one Wi-Fi, going through the whole flow:
After the review: Vitest passes (3,067 tests),
tsc, lint and Prettier are clean,pnpm build:desktopbuilds with the React Compiler, Clippy with-D warningsand the workspace's Rust tests pass. StrykerJS on this PR's changed lines, as CI runs it, left 10 survivors in the review fixes; each got a test or a code change, and a rerun of the card catches all 171 mutants. The one Stryker comment is the load effect's empty list (ArrayDeclaration), which never changes.Checklist
devpnpm lint,pnpm testandcargo clippy --workspace --all-targets -- -D warningspass (on this branch, which has the whole stack: 3,067 Vitest tests, 500 app and 788 core Rust tests)Unreleasedsection ofCHANGELOG.md