feat(engine): "Copy share link" Stats button — clipboards the replay URL
Quat: replay sharing as the next punch-list item. End-to-end: 1. Player wins a game on a server-backed sync backend. 2. `sync_plugin::push_replay_on_win` spawns the upload task on `AsyncComputeTaskPool` and stores the handle in the new `PendingReplayUpload` resource. The previous in-flight task (if any) is dropped — the most recent win is the one whose share link the player will care about. 3. `poll_replay_upload_result` harvests the task on the main thread each frame; on success writes `<server>/replays/<id>` to `LastSharedReplayUrl`. `UnsupportedPlatform` (LocalOnlyProvider) is silently absorbed; real network/auth errors warn-log. 4. The Stats overlay's action bar gains a "Copy share link" button. Click writes `LastSharedReplayUrl` to the OS clipboard via `arboard` and surfaces a "Copied: <url>" toast. Trait change: `SyncProvider::push_replay` now returns `Result<String, SyncError>` (the share URL) instead of `Result<(), SyncError>`. The default (`UnsupportedPlatform`) is unchanged for non-server backends; `SolitaireServerClient` parses the response body's `id` field and composes `<base_url>/replays/<id>`. Both call paths (initial + 401 retry) go through the new `share_url_from_response` helper so the parse logic isn't duplicated. New deps: - `arboard` (~10 KB, cross-platform clipboard) added to workspace + `solitaire_engine`. `default-features = false` keeps the X11/Wayland binary-feature deps off the dependency graph; arboard handles the fallback. Approved per the ASK BEFORE rule. Persistence: the URL is in-memory only — the player must share within the session of the win. A future revision can persist it alongside the replay history file if cross-session sharing is needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,13 +56,13 @@ pub trait SyncProvider: Send + Sync {
|
||||
async fn delete_account(&self) -> Result<(), SyncError> {
|
||||
Ok(())
|
||||
}
|
||||
/// Upload a winning replay to the backend so it's available for web
|
||||
/// playback at `<server>/replays/<id>`. Default returns
|
||||
/// `UnsupportedPlatform` so backends without a server (e.g.
|
||||
/// `LocalOnlyProvider`) are silently no-op'd by the engine's
|
||||
/// push-on-win system, matching the same pattern `pull` / `push`
|
||||
/// follow.
|
||||
async fn push_replay(&self, _replay: &crate::replay::Replay) -> Result<(), SyncError> {
|
||||
/// Upload a winning replay to the backend. On success, returns the
|
||||
/// shareable web URL the player can copy to their clipboard
|
||||
/// (`<server>/replays/<id>`). Default returns `UnsupportedPlatform`
|
||||
/// so backends without a server (e.g. `LocalOnlyProvider`) are
|
||||
/// silently no-op'd by the engine's push-on-win system, matching
|
||||
/// the same pattern `pull` / `push` follow.
|
||||
async fn push_replay(&self, _replay: &crate::replay::Replay) -> Result<String, SyncError> {
|
||||
Err(SyncError::UnsupportedPlatform)
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ impl SyncProvider for Box<dyn SyncProvider + Send + Sync> {
|
||||
async fn delete_account(&self) -> Result<(), SyncError> {
|
||||
(**self).delete_account().await
|
||||
}
|
||||
async fn push_replay(&self, replay: &crate::replay::Replay) -> Result<(), SyncError> {
|
||||
async fn push_replay(&self, replay: &crate::replay::Replay) -> Result<String, SyncError> {
|
||||
(**self).push_replay(replay).await
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user