consolidate remote & local source into one
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../gen/schemas/desktop-schema.json",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "default",
|
"identifier": "default",
|
||||||
"description": "Capability for the main window",
|
"description": "Capability for Wyd UI windows",
|
||||||
"windows": ["main"],
|
"windows": ["debug", "scene"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"opener:default"
|
"opener:default"
|
||||||
|
|||||||
+60
-34
@@ -2,6 +2,7 @@ use device_query::{DeviceEvents, DeviceEventsHandler};
|
|||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use specta::Type;
|
use specta::Type;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -27,18 +28,23 @@ pub struct CursorPositions {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CursorPositionChanged {
|
pub struct CursorPositionChanged {
|
||||||
pub positions: CursorPositions,
|
pub positions: HashMap<String, CursorPositions>,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct FriendCursorPositionChanged {
|
|
||||||
pub friend_id: String,
|
|
||||||
pub positions: CursorPositions,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[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
|
// Was private, but for some reason LSP
|
||||||
// complains even when there's no external references.
|
// complains even when there's no external references.
|
||||||
@@ -92,37 +98,28 @@ pub fn init(app: &AppHandle) {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn update_cursor_position(handle: &AppHandle, positions: CursorPositions) {
|
fn update_cursor_position(handle: &AppHandle, positions: CursorPositions) {
|
||||||
let state = handle.state::<CursorState>();
|
let user_id = handle
|
||||||
let mut guard = state.0.write().expect("Cursor Position lock failed");
|
.state::<crate::keypair::AppKeypair>()
|
||||||
|
.public_key()
|
||||||
*guard = positions.clone();
|
.to_owned();
|
||||||
drop(guard);
|
emit_position(handle, user_id, positions.clone());
|
||||||
|
|
||||||
if let Err(error) = (CursorPositionChanged {
|
|
||||||
positions: positions.clone(),
|
|
||||||
})
|
|
||||||
.emit(handle)
|
|
||||||
{
|
|
||||||
eprintln!("Failed to emit cursor position change: {error}");
|
|
||||||
}
|
|
||||||
|
|
||||||
handle
|
handle
|
||||||
.state::<crate::network::Network>()
|
.state::<crate::network::Network>()
|
||||||
.send_live_data(crate::live_data::LiveData::Cursor { positions });
|
.send_live_data(crate::live_data::LiveData::Cursor { positions });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn emit_friend_position(
|
pub(crate) fn emit_position(handle: &AppHandle, user_id: String, positions: CursorPositions) {
|
||||||
handle: &AppHandle,
|
let positions = match handle.state::<CursorState>().update(user_id, positions) {
|
||||||
friend_id: String,
|
Ok(positions) => positions,
|
||||||
positions: CursorPositions,
|
Err(error) => {
|
||||||
) {
|
eprintln!("Failed to update cursor positions: {error}");
|
||||||
if let Err(error) = (FriendCursorPositionChanged {
|
return;
|
||||||
friend_id,
|
}
|
||||||
positions,
|
};
|
||||||
})
|
|
||||||
.emit(handle)
|
if let Err(error) = (CursorPositionChanged { positions }).emit(handle) {
|
||||||
{
|
eprintln!("Failed to emit cursor position change: {error}");
|
||||||
eprintln!("Failed to emit friend 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.
|
/// Stop cursor tracking and unregister all listeners.
|
||||||
#[allow(dead_code)] // TODO: get rid of this when app teardown sequence is introduced.
|
#[allow(dead_code)] // TODO: get rid of this when app teardown sequence is introduced.
|
||||||
pub async fn stop_cursor_tracking() {
|
pub async fn stop_cursor_tracking() {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ mod remotes;
|
|||||||
mod ufa;
|
mod ufa;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod user;
|
mod user;
|
||||||
mod windowing;
|
|
||||||
|
|
||||||
use tauri::Manager;
|
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());
|
app.manage(cursor::CursorState::default());
|
||||||
ufa::init(handle);
|
ufa::init(handle);
|
||||||
cursor::init(handle);
|
cursor::init(handle);
|
||||||
// windowing::init(handle);
|
|
||||||
ui::init(handle);
|
ui::init(handle);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -76,7 +74,6 @@ fn specta_builder() -> tauri_specta::Builder<tauri::Wry> {
|
|||||||
profile::ProfileChanged,
|
profile::ProfileChanged,
|
||||||
network::NetworkStatusChanged,
|
network::NetworkStatusChanged,
|
||||||
cursor::CursorPositionChanged,
|
cursor::CursorPositionChanged,
|
||||||
cursor::FriendCursorPositionChanged,
|
|
||||||
ufa::ForegroundAppChanged,
|
ufa::ForegroundAppChanged,
|
||||||
ufa::FriendForegroundAppChanged,
|
ufa::FriendForegroundAppChanged,
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ async fn connect(
|
|||||||
ServerMessage::FriendLiveData { friend_id, payload } => {
|
ServerMessage::FriendLiveData { friend_id, payload } => {
|
||||||
match serde_json::from_str(&payload) {
|
match serde_json::from_str(&payload) {
|
||||||
Ok(LiveData::Cursor { positions }) => {
|
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 }) => {
|
Ok(LiveData::ForegroundApp { meta }) => {
|
||||||
crate::ufa::emit_friend_app(handle, friend_id, meta);
|
crate::ufa::emit_friend_app(handle, friend_id, meta);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use tauri::{AppHandle, Manager, WebviewUrl};
|
use tauri::{AppHandle, Manager, WebviewUrl};
|
||||||
|
|
||||||
const WINDOW_LABEL: &str = "main";
|
const WINDOW_LABEL: &str = "debug";
|
||||||
|
|
||||||
pub fn init(handle: &AppHandle) {
|
pub fn init(handle: &AppHandle) {
|
||||||
if let Some(window) = handle.get_webview_window(WINDOW_LABEL) {
|
if let Some(window) = handle.get_webview_window(WINDOW_LABEL) {
|
||||||
@@ -14,7 +14,7 @@ pub fn init(handle: &AppHandle) {
|
|||||||
|
|
||||||
let builder =
|
let builder =
|
||||||
tauri::WebviewWindowBuilder::new(handle, WINDOW_LABEL, WebviewUrl::App("/".into()))
|
tauri::WebviewWindowBuilder::new(handle, WINDOW_LABEL, WebviewUrl::App("/".into()))
|
||||||
.title("Wyd")
|
.title("Wyd Debug")
|
||||||
.inner_size(800.0, 600.0)
|
.inner_size(800.0, 600.0)
|
||||||
.min_inner_size(500.0, 400.0)
|
.min_inner_size(500.0, 400.0)
|
||||||
.visible(true);
|
.visible(true);
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
mod debug;
|
||||||
mod scene;
|
mod scene;
|
||||||
|
|
||||||
pub fn init(app_handle: &AppHandle) {
|
pub fn init(app_handle: &AppHandle) {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
debug::init(app_handle);
|
||||||
scene::init(app_handle);
|
scene::init(app_handle);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-4
@@ -52,7 +52,6 @@ async listStatuses() : Promise<ConnectionStatus[]> {
|
|||||||
export const events = __makeEvents__<{
|
export const events = __makeEvents__<{
|
||||||
cursorPositionChanged: CursorPositionChanged,
|
cursorPositionChanged: CursorPositionChanged,
|
||||||
foregroundAppChanged: ForegroundAppChanged,
|
foregroundAppChanged: ForegroundAppChanged,
|
||||||
friendCursorPositionChanged: FriendCursorPositionChanged,
|
|
||||||
friendForegroundAppChanged: FriendForegroundAppChanged,
|
friendForegroundAppChanged: FriendForegroundAppChanged,
|
||||||
friendsChanged: FriendsChanged,
|
friendsChanged: FriendsChanged,
|
||||||
networkStatusChanged: NetworkStatusChanged,
|
networkStatusChanged: NetworkStatusChanged,
|
||||||
@@ -61,7 +60,6 @@ remotesChanged: RemotesChanged
|
|||||||
}>({
|
}>({
|
||||||
cursorPositionChanged: "cursor-position-changed",
|
cursorPositionChanged: "cursor-position-changed",
|
||||||
foregroundAppChanged: "foreground-app-changed",
|
foregroundAppChanged: "foreground-app-changed",
|
||||||
friendCursorPositionChanged: "friend-cursor-position-changed",
|
|
||||||
friendForegroundAppChanged: "friend-foreground-app-changed",
|
friendForegroundAppChanged: "friend-foreground-app-changed",
|
||||||
friendsChanged: "friends-changed",
|
friendsChanged: "friends-changed",
|
||||||
networkStatusChanged: "network-status-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 ConnectionState = "connecting" | "connected" | "disconnected"
|
||||||
export type ConnectionStatus = { remoteId: string; address: string; name: string | null; state: ConnectionState }
|
export type ConnectionStatus = { remoteId: string; address: string; name: string | null; state: ConnectionState }
|
||||||
export type CursorPosition = { x: number; y: number }
|
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 CursorPositions = { raw: CursorPosition; mapped: CursorPosition }
|
||||||
export type ForegroundAppChanged = { meta: AppMeta }
|
export type ForegroundAppChanged = { meta: AppMeta }
|
||||||
export type FriendCursorPositionChanged = { friendId: string; positions: CursorPositions }
|
|
||||||
export type FriendForegroundAppChanged = { friendId: string; meta: AppMeta }
|
export type FriendForegroundAppChanged = { friendId: string; meta: AppMeta }
|
||||||
export type FriendsChanged = { friends: User[] }
|
export type FriendsChanged = { friends: User[] }
|
||||||
export type NetworkStatusChanged = { statuses: ConnectionStatus[] }
|
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">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import {
|
import {
|
||||||
|
commands,
|
||||||
events,
|
events,
|
||||||
type AppMeta,
|
type AppMeta,
|
||||||
type CursorPositions,
|
type CursorPositions,
|
||||||
} from "$lib/bindings";
|
} from "$lib/bindings";
|
||||||
|
|
||||||
let cursor: CursorPositions | null = null;
|
let localId = "";
|
||||||
let foregroundApp: AppMeta | null = null;
|
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(() => {
|
onMount(() => {
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
let stopCursor: (() => void) | undefined;
|
let stopListeners: (() => void)[] = [];
|
||||||
let stopForegroundApp: (() => void) | undefined;
|
|
||||||
|
|
||||||
async function initialize() {
|
async function initialize() {
|
||||||
const [cursorUnlisten, foregroundAppUnlisten] = await Promise.all([
|
const listeners = await Promise.all([
|
||||||
events.cursorPositionChanged.listen((event) => {
|
events.cursorPositionChanged.listen((event) => {
|
||||||
cursor = event.payload.positions;
|
if (!disposed) cursorPositions = event.payload.positions;
|
||||||
}),
|
}),
|
||||||
events.foregroundAppChanged.listen((event) => {
|
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) {
|
if (disposed) {
|
||||||
cursorUnlisten();
|
listeners.forEach((stop) => stop());
|
||||||
foregroundAppUnlisten();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
stopListeners = listeners;
|
||||||
|
|
||||||
stopCursor = cursorUnlisten;
|
localId = await commands.getPublicKey();
|
||||||
stopForegroundApp = foregroundAppUnlisten;
|
if (disposed) return;
|
||||||
|
if (pendingLocalForegroundApp) {
|
||||||
|
updateForegroundApp(localId, pendingLocalForegroundApp);
|
||||||
|
pendingLocalForegroundApp = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize();
|
initialize().catch((err) => {
|
||||||
|
if (!disposed) error = String(err);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
stopCursor?.();
|
stopListeners.forEach((stop) => stop());
|
||||||
stopForegroundApp?.();
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -47,7 +83,19 @@
|
|||||||
<section>
|
<section>
|
||||||
<h1>Live metadata</h1>
|
<h1>Live metadata</h1>
|
||||||
|
|
||||||
<h2>Cursor</h2>
|
{#if error}
|
||||||
|
<p>{error}</p>
|
||||||
|
{:else if liveUserIds().length === 0}
|
||||||
|
<p>Waiting for live metadata...</p>
|
||||||
|
{:else}
|
||||||
|
{#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>
|
||||||
|
|
||||||
|
<h3>Cursor</h3>
|
||||||
{#if cursor}
|
{#if cursor}
|
||||||
<p>Raw: {cursor.raw.x}, {cursor.raw.y}</p>
|
<p>Raw: {cursor.raw.x}, {cursor.raw.y}</p>
|
||||||
<p>Mapped: {cursor.mapped.x}, {cursor.mapped.y}</p>
|
<p>Mapped: {cursor.mapped.x}, {cursor.mapped.y}</p>
|
||||||
@@ -55,7 +103,7 @@
|
|||||||
<p>Waiting for cursor data...</p>
|
<p>Waiting for cursor data...</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<h2>Foreground app</h2>
|
<h3>Foreground app</h3>
|
||||||
{#if foregroundApp}
|
{#if foregroundApp}
|
||||||
{#if foregroundApp.ico}
|
{#if foregroundApp.ico}
|
||||||
<img
|
<img
|
||||||
@@ -70,4 +118,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<p>Waiting for foreground app data...</p>
|
<p>Waiting for foreground app data...</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
</article>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import FriendsList from "$lib/components/friends-list.svelte";
|
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 ConnectionStatus from "$lib/components/connection-status.svelte";
|
||||||
import Profile from "$lib/components/profile.svelte";
|
import Profile from "$lib/components/profile.svelte";
|
||||||
import RemotesList from "$lib/components/remotes-list.svelte";
|
import RemotesList from "$lib/components/remotes-list.svelte";
|
||||||
@@ -11,12 +10,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<Profile />
|
<Profile />
|
||||||
</div>
|
</div>
|
||||||
<div class="friends-area">
|
|
||||||
<div>
|
<div>
|
||||||
<FriendsList />
|
<FriendsList />
|
||||||
</div>
|
</div>
|
||||||
<FriendsLiveData />
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<RemotesList />
|
<RemotesList />
|
||||||
</div>
|
</div>
|
||||||
@@ -27,6 +23,3 @@
|
|||||||
<LiveMetadata />
|
<LiveMetadata />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user