consolidate remote & local source into one
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"description": "Capability for Wyd UI windows",
|
||||
"windows": ["debug", "scene"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default"
|
||||
|
||||
+60
-34
@@ -2,6 +2,7 @@ use device_query::{DeviceEvents, DeviceEventsHandler};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::RwLock;
|
||||
use std::time::Duration;
|
||||
@@ -27,18 +28,23 @@ pub struct CursorPositions {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CursorPositionChanged {
|
||||
pub positions: CursorPositions,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FriendCursorPositionChanged {
|
||||
pub friend_id: String,
|
||||
pub positions: CursorPositions,
|
||||
pub positions: HashMap<String, CursorPositions>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CursorState(RwLock<CursorPositions>);
|
||||
pub struct CursorState(RwLock<HashMap<String, CursorPositions>>);
|
||||
|
||||
impl CursorState {
|
||||
fn update(
|
||||
&self,
|
||||
user_id: String,
|
||||
positions: CursorPositions,
|
||||
) -> Result<HashMap<String, CursorPositions>, String> {
|
||||
let mut positions_by_user = self.0.write().map_err(|error| error.to_string())?;
|
||||
positions_by_user.insert(user_id, positions);
|
||||
Ok(positions_by_user.clone())
|
||||
}
|
||||
}
|
||||
|
||||
// Was private, but for some reason LSP
|
||||
// complains even when there's no external references.
|
||||
@@ -92,37 +98,28 @@ pub fn init(app: &AppHandle) {
|
||||
|
||||
#[inline]
|
||||
fn update_cursor_position(handle: &AppHandle, positions: CursorPositions) {
|
||||
let state = handle.state::<CursorState>();
|
||||
let mut guard = state.0.write().expect("Cursor Position lock failed");
|
||||
|
||||
*guard = positions.clone();
|
||||
drop(guard);
|
||||
|
||||
if let Err(error) = (CursorPositionChanged {
|
||||
positions: positions.clone(),
|
||||
})
|
||||
.emit(handle)
|
||||
{
|
||||
eprintln!("Failed to emit cursor position change: {error}");
|
||||
}
|
||||
let user_id = handle
|
||||
.state::<crate::keypair::AppKeypair>()
|
||||
.public_key()
|
||||
.to_owned();
|
||||
emit_position(handle, user_id, positions.clone());
|
||||
|
||||
handle
|
||||
.state::<crate::network::Network>()
|
||||
.send_live_data(crate::live_data::LiveData::Cursor { positions });
|
||||
}
|
||||
|
||||
pub(crate) fn emit_friend_position(
|
||||
handle: &AppHandle,
|
||||
friend_id: String,
|
||||
positions: CursorPositions,
|
||||
) {
|
||||
if let Err(error) = (FriendCursorPositionChanged {
|
||||
friend_id,
|
||||
positions,
|
||||
})
|
||||
.emit(handle)
|
||||
{
|
||||
eprintln!("Failed to emit friend cursor position change: {error}");
|
||||
pub(crate) fn emit_position(handle: &AppHandle, user_id: String, positions: CursorPositions) {
|
||||
let positions = match handle.state::<CursorState>().update(user_id, positions) {
|
||||
Ok(positions) => positions,
|
||||
Err(error) => {
|
||||
eprintln!("Failed to update cursor positions: {error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(error) = (CursorPositionChanged { positions }).emit(handle) {
|
||||
eprintln!("Failed to emit cursor position change: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +153,35 @@ fn transform_coords(pos: &CursorPosition, to_normalized: bool, w: f64, h: f64) -
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn positions(x: f64) -> CursorPositions {
|
||||
CursorPositions {
|
||||
raw: CursorPosition { x, y: x },
|
||||
mapped: CursorPosition { x, y: x },
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_state_tracks_and_replaces_positions_by_user_id() {
|
||||
let state = CursorState::default();
|
||||
|
||||
let snapshot = state.update("local".to_owned(), positions(1.0)).unwrap();
|
||||
assert_eq!(snapshot.len(), 1);
|
||||
assert_eq!(snapshot["local"].raw.x, 1.0);
|
||||
|
||||
let snapshot = state.update("friend".to_owned(), positions(2.0)).unwrap();
|
||||
assert_eq!(snapshot.len(), 2);
|
||||
assert_eq!(snapshot["friend"].raw.x, 2.0);
|
||||
|
||||
let snapshot = state.update("local".to_owned(), positions(3.0)).unwrap();
|
||||
assert_eq!(snapshot.len(), 2);
|
||||
assert_eq!(snapshot["local"].raw.x, 3.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop cursor tracking and unregister all listeners.
|
||||
#[allow(dead_code)] // TODO: get rid of this when app teardown sequence is introduced.
|
||||
pub async fn stop_cursor_tracking() {
|
||||
|
||||
@@ -9,7 +9,6 @@ mod remotes;
|
||||
mod ufa;
|
||||
mod ui;
|
||||
mod user;
|
||||
mod windowing;
|
||||
|
||||
use tauri::Manager;
|
||||
|
||||
@@ -22,7 +21,6 @@ async fn launch_app(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>>
|
||||
app.manage(cursor::CursorState::default());
|
||||
ufa::init(handle);
|
||||
cursor::init(handle);
|
||||
// windowing::init(handle);
|
||||
ui::init(handle);
|
||||
Ok(())
|
||||
}
|
||||
@@ -76,7 +74,6 @@ fn specta_builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
profile::ProfileChanged,
|
||||
network::NetworkStatusChanged,
|
||||
cursor::CursorPositionChanged,
|
||||
cursor::FriendCursorPositionChanged,
|
||||
ufa::ForegroundAppChanged,
|
||||
ufa::FriendForegroundAppChanged,
|
||||
])
|
||||
|
||||
@@ -327,7 +327,7 @@ async fn connect(
|
||||
ServerMessage::FriendLiveData { friend_id, payload } => {
|
||||
match serde_json::from_str(&payload) {
|
||||
Ok(LiveData::Cursor { positions }) => {
|
||||
crate::cursor::emit_friend_position(handle, friend_id, positions);
|
||||
crate::cursor::emit_position(handle, friend_id, positions);
|
||||
}
|
||||
Ok(LiveData::ForegroundApp { meta }) => {
|
||||
crate::ufa::emit_friend_app(handle, friend_id, meta);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use tauri::{AppHandle, Manager, WebviewUrl};
|
||||
|
||||
const WINDOW_LABEL: &str = "main";
|
||||
const WINDOW_LABEL: &str = "debug";
|
||||
|
||||
pub fn init(handle: &AppHandle) {
|
||||
if let Some(window) = handle.get_webview_window(WINDOW_LABEL) {
|
||||
@@ -14,7 +14,7 @@ pub fn init(handle: &AppHandle) {
|
||||
|
||||
let builder =
|
||||
tauri::WebviewWindowBuilder::new(handle, WINDOW_LABEL, WebviewUrl::App("/".into()))
|
||||
.title("Wyd")
|
||||
.title("Wyd Debug")
|
||||
.inner_size(800.0, 600.0)
|
||||
.min_inner_size(500.0, 400.0)
|
||||
.visible(true);
|
||||
@@ -1,7 +1,11 @@
|
||||
use tauri::AppHandle;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
mod debug;
|
||||
mod scene;
|
||||
|
||||
pub fn init(app_handle: &AppHandle) {
|
||||
#[cfg(debug_assertions)]
|
||||
debug::init(app_handle);
|
||||
scene::init(app_handle);
|
||||
}
|
||||
|
||||
+1
-4
@@ -52,7 +52,6 @@ async listStatuses() : Promise<ConnectionStatus[]> {
|
||||
export const events = __makeEvents__<{
|
||||
cursorPositionChanged: CursorPositionChanged,
|
||||
foregroundAppChanged: ForegroundAppChanged,
|
||||
friendCursorPositionChanged: FriendCursorPositionChanged,
|
||||
friendForegroundAppChanged: FriendForegroundAppChanged,
|
||||
friendsChanged: FriendsChanged,
|
||||
networkStatusChanged: NetworkStatusChanged,
|
||||
@@ -61,7 +60,6 @@ remotesChanged: RemotesChanged
|
||||
}>({
|
||||
cursorPositionChanged: "cursor-position-changed",
|
||||
foregroundAppChanged: "foreground-app-changed",
|
||||
friendCursorPositionChanged: "friend-cursor-position-changed",
|
||||
friendForegroundAppChanged: "friend-foreground-app-changed",
|
||||
friendsChanged: "friends-changed",
|
||||
networkStatusChanged: "network-status-changed",
|
||||
@@ -82,10 +80,9 @@ export type AppMeta = { local: string | null; unlocal: string | null; ico: strin
|
||||
export type ConnectionState = "connecting" | "connected" | "disconnected"
|
||||
export type ConnectionStatus = { remoteId: string; address: string; name: string | null; state: ConnectionState }
|
||||
export type CursorPosition = { x: number; y: number }
|
||||
export type CursorPositionChanged = { positions: CursorPositions }
|
||||
export type CursorPositionChanged = { positions: Partial<{ [key in string]: CursorPositions }> }
|
||||
export type CursorPositions = { raw: CursorPosition; mapped: CursorPosition }
|
||||
export type ForegroundAppChanged = { meta: AppMeta }
|
||||
export type FriendCursorPositionChanged = { friendId: string; positions: CursorPositions }
|
||||
export type FriendForegroundAppChanged = { friendId: string; meta: AppMeta }
|
||||
export type FriendsChanged = { friends: User[] }
|
||||
export type NetworkStatusChanged = { statuses: ConnectionStatus[] }
|
||||
|
||||
@@ -1,469 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import {
|
||||
commands,
|
||||
events,
|
||||
type AppMeta,
|
||||
type CursorPositions,
|
||||
type User,
|
||||
} from "$lib/bindings";
|
||||
|
||||
type FriendLiveData = {
|
||||
cursor: CursorPositions | null;
|
||||
foregroundApp: AppMeta | null;
|
||||
};
|
||||
|
||||
let friends: User[] = [];
|
||||
let liveData = new Map<string, FriendLiveData>();
|
||||
let error = "";
|
||||
|
||||
const emptyLiveData = (): FriendLiveData => ({
|
||||
cursor: null,
|
||||
foregroundApp: null,
|
||||
});
|
||||
|
||||
function applyFriends(next: User[]) {
|
||||
const friendIds = new Set(next.map((friend) => friend.id));
|
||||
friends = next;
|
||||
liveData = new Map(
|
||||
[...liveData].filter(([friendId]) => friendIds.has(friendId)),
|
||||
);
|
||||
}
|
||||
|
||||
function updateCursor(friendId: string, cursor: CursorPositions) {
|
||||
liveData = new Map(liveData).set(friendId, {
|
||||
...(liveData.get(friendId) ?? emptyLiveData()),
|
||||
cursor,
|
||||
});
|
||||
}
|
||||
|
||||
function updateForegroundApp(friendId: string, foregroundApp: AppMeta) {
|
||||
liveData = new Map(liveData).set(friendId, {
|
||||
...(liveData.get(friendId) ?? emptyLiveData()),
|
||||
foregroundApp,
|
||||
});
|
||||
}
|
||||
|
||||
function initials(name: string) {
|
||||
return name.trim().slice(0, 1).toUpperCase() || "?";
|
||||
}
|
||||
|
||||
function compactId(id: string) {
|
||||
return id.length > 16 ? `${id.slice(0, 8)}…${id.slice(-6)}` : id;
|
||||
}
|
||||
|
||||
function cursorStyle(cursor: CursorPositions) {
|
||||
const x = Math.min(1, Math.max(0, cursor.mapped.x)) * 100;
|
||||
const y = Math.min(1, Math.max(0, cursor.mapped.y)) * 100;
|
||||
return `--cursor-x: ${x}%; --cursor-y: ${y}%;`;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let disposed = false;
|
||||
let stopListeners: (() => void)[] = [];
|
||||
|
||||
async function initialize() {
|
||||
const listeners = await Promise.all([
|
||||
events.friendsChanged.listen((event) => {
|
||||
if (!disposed) applyFriends(event.payload.friends);
|
||||
}),
|
||||
events.friendCursorPositionChanged.listen((event) => {
|
||||
if (!disposed) {
|
||||
updateCursor(event.payload.friendId, event.payload.positions);
|
||||
}
|
||||
}),
|
||||
events.friendForegroundAppChanged.listen((event) => {
|
||||
if (!disposed) {
|
||||
updateForegroundApp(event.payload.friendId, event.payload.meta);
|
||||
}
|
||||
}),
|
||||
]);
|
||||
|
||||
if (disposed) {
|
||||
listeners.forEach((stop) => stop());
|
||||
return;
|
||||
}
|
||||
stopListeners = listeners;
|
||||
|
||||
try {
|
||||
const snapshot = await commands.listFriends();
|
||||
if (!disposed) applyFriends(snapshot);
|
||||
} catch (err) {
|
||||
if (!disposed) error = String(err);
|
||||
}
|
||||
}
|
||||
|
||||
initialize();
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
stopListeners.forEach((stop) => stop());
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="signal-console" aria-labelledby="friend-signals-title">
|
||||
<header>
|
||||
<div>
|
||||
<p class="eyebrow">Mutual connections</p>
|
||||
<h1 id="friend-signals-title">Friend signals</h1>
|
||||
</div>
|
||||
<span class="live-indicator"><i></i> Live</span>
|
||||
</header>
|
||||
|
||||
{#if error}
|
||||
<p class="error" role="alert">{error}</p>
|
||||
{:else if friends.length === 0}
|
||||
<div class="empty-state">
|
||||
<span aria-hidden="true">⌁</span>
|
||||
<p>Add a friend to watch their live activity arrive here.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<ul>
|
||||
{#each friends as friend (friend.id)}
|
||||
{@const data = liveData.get(friend.id) ?? emptyLiveData()}
|
||||
<li>
|
||||
<div class="identity">
|
||||
<span class="avatar" aria-hidden="true">{initials(friend.displayName)}</span>
|
||||
<div>
|
||||
<h2>{friend.displayName}</h2>
|
||||
<code title={friend.id}>{compactId(friend.id)}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="signals">
|
||||
<div class="app-signal">
|
||||
{#if data.foregroundApp}
|
||||
{#if data.foregroundApp.ico}
|
||||
<img
|
||||
src={`data:image/png;base64,${data.foregroundApp.ico}`}
|
||||
alt=""
|
||||
width="36"
|
||||
height="36"
|
||||
/>
|
||||
{:else}
|
||||
<span class="app-placeholder" aria-hidden="true">◆</span>
|
||||
{/if}
|
||||
<div>
|
||||
<small>Foreground</small>
|
||||
<strong>
|
||||
{data.foregroundApp.local ??
|
||||
data.foregroundApp.unlocal ??
|
||||
"Unknown app"}
|
||||
</strong>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="app-placeholder muted" aria-hidden="true">◇</span>
|
||||
<div>
|
||||
<small>Foreground</small>
|
||||
<span class="waiting">Waiting for activity</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="cursor-signal">
|
||||
<div class="cursor-label">
|
||||
<small>Cursor</small>
|
||||
{#if data.cursor}
|
||||
<span>
|
||||
{Math.round(data.cursor.mapped.x * 100)} ·
|
||||
{Math.round(data.cursor.mapped.y * 100)}
|
||||
</span>
|
||||
{:else}
|
||||
<span>— · —</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class:active={data.cursor !== null}
|
||||
class="cursor-field"
|
||||
style={data.cursor ? cursorStyle(data.cursor) : undefined}
|
||||
aria-label={data.cursor
|
||||
? `Cursor at ${Math.round(data.cursor.mapped.x * 100)} percent horizontal and ${Math.round(data.cursor.mapped.y * 100)} percent vertical`
|
||||
: "Waiting for cursor position"}
|
||||
>
|
||||
<i></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.signal-console {
|
||||
--ink: #172019;
|
||||
--muted: #69736b;
|
||||
--paper: #f5f3e9;
|
||||
--line: #c8c8b9;
|
||||
--signal: #c9ff3d;
|
||||
--hot: #ff6846;
|
||||
box-sizing: border-box;
|
||||
min-height: 100%;
|
||||
padding: 1.15rem;
|
||||
overflow: hidden;
|
||||
color: var(--ink);
|
||||
font-family: "Avenir Next Condensed", "DIN Alternate", sans-serif;
|
||||
background-color: var(--paper);
|
||||
border: 1px solid var(--ink);
|
||||
box-shadow: 6px 6px 0 var(--ink);
|
||||
}
|
||||
|
||||
header,
|
||||
.identity,
|
||||
.signals,
|
||||
.app-signal,
|
||||
.cursor-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header {
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding-bottom: 0.9rem;
|
||||
border-bottom: 1px solid var(--ink);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(1.5rem, 3vw, 2.1rem);
|
||||
line-height: 0.95;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.eyebrow,
|
||||
small,
|
||||
.live-indicator,
|
||||
code {
|
||||
font-family: Menlo, Monaco, monospace;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin-bottom: 0.25rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.63rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.live-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.35rem 0.55rem;
|
||||
font-size: 0.65rem;
|
||||
text-transform: uppercase;
|
||||
border: 1px solid var(--ink);
|
||||
background: var(--signal);
|
||||
}
|
||||
|
||||
.live-indicator i {
|
||||
width: 0.45rem;
|
||||
height: 0.45rem;
|
||||
background: var(--ink);
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
ul {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
margin: 1rem 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 0.8rem;
|
||||
background: rgba(255, 255, 255, 0.42);
|
||||
border: 1px solid var(--line);
|
||||
animation: enter 280ms ease-out both;
|
||||
}
|
||||
|
||||
.identity {
|
||||
gap: 0.65rem;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: grid;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
place-items: center;
|
||||
flex: 0 0 auto;
|
||||
font-weight: 800;
|
||||
background: var(--hot);
|
||||
border: 1px solid var(--ink);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1rem;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
code {
|
||||
color: var(--muted);
|
||||
font-size: 0.62rem;
|
||||
}
|
||||
|
||||
.signals {
|
||||
align-items: stretch;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.app-signal,
|
||||
.cursor-signal {
|
||||
min-width: 0;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.app-signal {
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.app-signal img,
|
||||
.app-placeholder {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid var(--ink);
|
||||
}
|
||||
|
||||
.app-signal img {
|
||||
object-fit: contain;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.app-placeholder {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: var(--signal);
|
||||
}
|
||||
|
||||
.app-placeholder.muted {
|
||||
color: var(--muted);
|
||||
background: transparent;
|
||||
border-color: var(--line);
|
||||
}
|
||||
|
||||
small {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 0.58rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
strong,
|
||||
.waiting {
|
||||
display: block;
|
||||
max-width: 10rem;
|
||||
overflow: hidden;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.waiting {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.cursor-label {
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.25rem;
|
||||
color: var(--muted);
|
||||
font-family: Menlo, Monaco, monospace;
|
||||
font-size: 0.58rem;
|
||||
}
|
||||
|
||||
.cursor-field {
|
||||
position: relative;
|
||||
height: 2rem;
|
||||
overflow: hidden;
|
||||
background-image: linear-gradient(var(--line) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--line) 1px, transparent 1px);
|
||||
background-size: 25% 50%;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.cursor-field i {
|
||||
position: absolute;
|
||||
top: var(--cursor-y, 50%);
|
||||
left: var(--cursor-x, 50%);
|
||||
width: 0.45rem;
|
||||
height: 0.45rem;
|
||||
opacity: 0;
|
||||
background: var(--hot);
|
||||
border: 1px solid var(--ink);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: top 180ms linear, left 180ms linear;
|
||||
}
|
||||
|
||||
.cursor-field.active i {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.empty-state,
|
||||
.error {
|
||||
margin-top: 1rem;
|
||||
padding: 1.2rem;
|
||||
text-align: center;
|
||||
border: 1px dashed var(--line);
|
||||
}
|
||||
|
||||
.empty-state span {
|
||||
display: block;
|
||||
margin-bottom: 0.35rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.empty-state p,
|
||||
.error {
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #9a2410;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
50% {
|
||||
opacity: 0.35;
|
||||
transform: scale(0.75);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 540px) {
|
||||
.signals {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.live-indicator i,
|
||||
li {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.cursor-field i {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,45 +1,81 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import {
|
||||
commands,
|
||||
events,
|
||||
type AppMeta,
|
||||
type CursorPositions,
|
||||
} from "$lib/bindings";
|
||||
|
||||
let cursor: CursorPositions | null = null;
|
||||
let foregroundApp: AppMeta | null = null;
|
||||
let localId = "";
|
||||
let cursorPositions: Partial<Record<string, CursorPositions>> = {};
|
||||
let foregroundApps = new Map<string, AppMeta>();
|
||||
let pendingLocalForegroundApp: AppMeta | null = null;
|
||||
let error = "";
|
||||
|
||||
function updateForegroundApp(userId: string, meta: AppMeta) {
|
||||
foregroundApps = new Map(foregroundApps).set(userId, meta);
|
||||
}
|
||||
|
||||
function liveUserIds() {
|
||||
return [
|
||||
...new Set([
|
||||
localId,
|
||||
...Object.keys(cursorPositions),
|
||||
...foregroundApps.keys(),
|
||||
]),
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
function compactId(id: string) {
|
||||
return id.length > 16 ? `${id.slice(0, 8)}…${id.slice(-6)}` : id;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let disposed = false;
|
||||
let stopCursor: (() => void) | undefined;
|
||||
let stopForegroundApp: (() => void) | undefined;
|
||||
let stopListeners: (() => void)[] = [];
|
||||
|
||||
async function initialize() {
|
||||
const [cursorUnlisten, foregroundAppUnlisten] = await Promise.all([
|
||||
const listeners = await Promise.all([
|
||||
events.cursorPositionChanged.listen((event) => {
|
||||
cursor = event.payload.positions;
|
||||
if (!disposed) cursorPositions = event.payload.positions;
|
||||
}),
|
||||
events.foregroundAppChanged.listen((event) => {
|
||||
foregroundApp = event.payload.meta;
|
||||
if (disposed) return;
|
||||
if (localId) {
|
||||
updateForegroundApp(localId, event.payload.meta);
|
||||
} else {
|
||||
pendingLocalForegroundApp = event.payload.meta;
|
||||
}
|
||||
}),
|
||||
events.friendForegroundAppChanged.listen((event) => {
|
||||
if (!disposed) {
|
||||
updateForegroundApp(event.payload.friendId, event.payload.meta);
|
||||
}
|
||||
}),
|
||||
]);
|
||||
|
||||
if (disposed) {
|
||||
cursorUnlisten();
|
||||
foregroundAppUnlisten();
|
||||
listeners.forEach((stop) => stop());
|
||||
return;
|
||||
}
|
||||
stopListeners = listeners;
|
||||
|
||||
stopCursor = cursorUnlisten;
|
||||
stopForegroundApp = foregroundAppUnlisten;
|
||||
localId = await commands.getPublicKey();
|
||||
if (disposed) return;
|
||||
if (pendingLocalForegroundApp) {
|
||||
updateForegroundApp(localId, pendingLocalForegroundApp);
|
||||
pendingLocalForegroundApp = null;
|
||||
}
|
||||
}
|
||||
|
||||
initialize();
|
||||
initialize().catch((err) => {
|
||||
if (!disposed) error = String(err);
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
stopCursor?.();
|
||||
stopForegroundApp?.();
|
||||
stopListeners.forEach((stop) => stop());
|
||||
};
|
||||
});
|
||||
</script>
|
||||
@@ -47,27 +83,42 @@
|
||||
<section>
|
||||
<h1>Live metadata</h1>
|
||||
|
||||
<h2>Cursor</h2>
|
||||
{#if cursor}
|
||||
<p>Raw: {cursor.raw.x}, {cursor.raw.y}</p>
|
||||
<p>Mapped: {cursor.mapped.x}, {cursor.mapped.y}</p>
|
||||
{#if error}
|
||||
<p>{error}</p>
|
||||
{:else if liveUserIds().length === 0}
|
||||
<p>Waiting for live metadata...</p>
|
||||
{:else}
|
||||
<p>Waiting for cursor data...</p>
|
||||
{/if}
|
||||
{#each liveUserIds() as userId (userId)}
|
||||
{@const cursor = cursorPositions[userId]}
|
||||
{@const foregroundApp = foregroundApps.get(userId)}
|
||||
<article>
|
||||
<h2>{userId === localId ? "Local user" : "Remote user"}</h2>
|
||||
<p title={userId}>ID: {compactId(userId)}</p>
|
||||
|
||||
<h2>Foreground app</h2>
|
||||
{#if foregroundApp}
|
||||
{#if foregroundApp.ico}
|
||||
<img
|
||||
src={`data:image/png;base64,${foregroundApp.ico}`}
|
||||
alt=""
|
||||
width="64"
|
||||
height="64"
|
||||
/>
|
||||
{/if}
|
||||
<p>Localized name: {foregroundApp.local ?? "Unavailable"}</p>
|
||||
<p>Executable name: {foregroundApp.unlocal ?? "Unavailable"}</p>
|
||||
{:else}
|
||||
<p>Waiting for foreground app data...</p>
|
||||
<h3>Cursor</h3>
|
||||
{#if cursor}
|
||||
<p>Raw: {cursor.raw.x}, {cursor.raw.y}</p>
|
||||
<p>Mapped: {cursor.mapped.x}, {cursor.mapped.y}</p>
|
||||
{:else}
|
||||
<p>Waiting for cursor data...</p>
|
||||
{/if}
|
||||
|
||||
<h3>Foreground app</h3>
|
||||
{#if foregroundApp}
|
||||
{#if foregroundApp.ico}
|
||||
<img
|
||||
src={`data:image/png;base64,${foregroundApp.ico}`}
|
||||
alt=""
|
||||
width="64"
|
||||
height="64"
|
||||
/>
|
||||
{/if}
|
||||
<p>Localized name: {foregroundApp.local ?? "Unavailable"}</p>
|
||||
<p>Executable name: {foregroundApp.unlocal ?? "Unavailable"}</p>
|
||||
{:else}
|
||||
<p>Waiting for foreground app data...</p>
|
||||
{/if}
|
||||
</article>
|
||||
{/each}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script>
|
||||
import FriendsList from "$lib/components/friends-list.svelte";
|
||||
import FriendsLiveData from "$lib/components/friends-live-data.svelte";
|
||||
import ConnectionStatus from "$lib/components/connection-status.svelte";
|
||||
import Profile from "$lib/components/profile.svelte";
|
||||
import RemotesList from "$lib/components/remotes-list.svelte";
|
||||
@@ -11,11 +10,8 @@
|
||||
<div>
|
||||
<Profile />
|
||||
</div>
|
||||
<div class="friends-area">
|
||||
<div>
|
||||
<FriendsList />
|
||||
</div>
|
||||
<FriendsLiveData />
|
||||
<div>
|
||||
<FriendsList />
|
||||
</div>
|
||||
<div>
|
||||
<RemotesList />
|
||||
@@ -27,6 +23,3 @@
|
||||
<LiveMetadata />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user