teardown scene window when unused
This commit is contained in:
@@ -94,6 +94,10 @@ impl Network {
|
|||||||
self.keypair.public_key()
|
self.keypair.public_key()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn online_friend_ids(&self) -> Result<Vec<String>, String> {
|
||||||
|
self.friend_presence.snapshot()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn send_live_data(&self, data: LiveData) {
|
pub fn send_live_data(&self, data: LiveData) {
|
||||||
let active_remotes = match self.friend_presence.active_remotes() {
|
let active_remotes = match self.friend_presence.active_remotes() {
|
||||||
Ok(remotes) if remotes.is_empty() => return,
|
Ok(remotes) if remotes.is_empty() => return,
|
||||||
@@ -502,7 +506,7 @@ pub fn list_statuses(
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub fn list_friend_statuses(network: State<'_, Network>) -> Result<Vec<String>, String> {
|
pub fn list_friend_statuses(network: State<'_, Network>) -> Result<Vec<String>, String> {
|
||||||
network.friend_presence.snapshot()
|
network.online_friend_ids()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
+53
-16
@@ -1,5 +1,7 @@
|
|||||||
|
mod visibility;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::{HashMap, HashSet},
|
||||||
sync::RwLock,
|
sync::RwLock,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
@@ -91,6 +93,7 @@ pub fn init(handle: &AppHandle) -> Result<(), String> {
|
|||||||
let mut ticker = tokio::time::interval(TICK_INTERVAL);
|
let mut ticker = tokio::time::interval(TICK_INTERVAL);
|
||||||
ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||||
let mut previous_tick = Instant::now();
|
let mut previous_tick = Instant::now();
|
||||||
|
let mut visibility = visibility::Visibility::default();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
ticker.tick().await;
|
ticker.tick().await;
|
||||||
@@ -98,40 +101,74 @@ pub fn init(handle: &AppHandle) -> Result<(), String> {
|
|||||||
let elapsed = now.duration_since(previous_tick);
|
let elapsed = now.duration_since(previous_tick);
|
||||||
previous_tick = now;
|
previous_tick = now;
|
||||||
|
|
||||||
let cursor_positions = match handle.state::<CursorState>().snapshot() {
|
// UI state is registered only after the splash/onboarding boundary.
|
||||||
|
if !crate::ui::scene::is_initialized(&handle) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let online = match handle
|
||||||
|
.state::<crate::network::Network>()
|
||||||
|
.online_friend_ids()
|
||||||
|
{
|
||||||
|
Ok(online) => online.into_iter().collect::<HashSet<_>>(),
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("failed to read scene friend presence: {error}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let configuration = match handle.state::<SceneConfigurationState>().snapshot() {
|
||||||
|
Ok(configuration) => configuration,
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("failed to read scene configuration: {error}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let has_online_friends = !online.is_empty();
|
||||||
|
visibility.update(
|
||||||
|
configuration.hide_local_puppet_when_alone,
|
||||||
|
has_online_friends,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
let local_id = handle
|
||||||
|
.state::<crate::keypair::AppKeypair>()
|
||||||
|
.public_key()
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
let mut cursor_positions = match handle.state::<CursorState>().snapshot() {
|
||||||
Ok(positions) => positions,
|
Ok(positions) => positions,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("failed to read cursor positions for puppet motion: {error}");
|
eprintln!("failed to read cursor positions for puppet motion: {error}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let movement_mode = match handle
|
cursor_positions
|
||||||
.state::<SceneConfigurationState>()
|
.retain(|id, _| online.contains(id) || (id == &local_id && visibility.show_local));
|
||||||
.puppet_movement_mode()
|
|
||||||
{
|
|
||||||
Ok(mode) => mode,
|
|
||||||
Err(error) => {
|
|
||||||
eprintln!("failed to read Puppet movement mode: {error}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let puppets = match handle.state::<PuppetStateStore>().update(
|
let puppets = match handle.state::<PuppetStateStore>().update(
|
||||||
&cursor_positions,
|
&cursor_positions,
|
||||||
viewport,
|
viewport,
|
||||||
elapsed,
|
elapsed,
|
||||||
movement_mode,
|
configuration.puppet_movement_mode,
|
||||||
) {
|
) {
|
||||||
Ok(Some(puppets)) => puppets,
|
Ok(puppets) => puppets,
|
||||||
Ok(None) => continue,
|
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("failed to update puppet motion: {error}");
|
eprintln!("failed to update puppet motion: {error}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(error) = emit_changed(&handle, puppets) {
|
if let Some(puppets) = puppets
|
||||||
|
&& let Err(error) = emit_changed(&handle, puppets)
|
||||||
|
{
|
||||||
eprintln!("failed to emit puppet states: {error}");
|
eprintln!("failed to emit puppet states: {error}");
|
||||||
}
|
}
|
||||||
|
// Reconcile even when motion is unchanged (including the idle deadline).
|
||||||
|
if let Err(error) = crate::ui::scene::reconcile_window(
|
||||||
|
&handle,
|
||||||
|
visibility.window_open(has_online_friends),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!("failed to reconcile scene window: {error}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
const HIDE_DELAY: Duration = Duration::from_secs(5);
|
||||||
|
const SHOW_DELAY: Duration = Duration::from_secs(3);
|
||||||
|
|
||||||
|
/// Backend-owned visibility survives destruction of the scene webview.
|
||||||
|
pub(super) struct Visibility {
|
||||||
|
pub show_local: bool,
|
||||||
|
pending: Option<(bool, Instant)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Visibility {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
show_local: true,
|
||||||
|
pending: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Visibility {
|
||||||
|
pub fn window_open(&self, has_friends: bool) -> bool {
|
||||||
|
self.show_local || has_friends
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, hide_when_alone: bool, has_friends: bool, now: Instant) {
|
||||||
|
if !hide_when_alone {
|
||||||
|
self.show_local = true;
|
||||||
|
self.pending = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.show_local == has_friends {
|
||||||
|
self.pending = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (target, deadline) = *self.pending.get_or_insert_with(|| {
|
||||||
|
(
|
||||||
|
has_friends,
|
||||||
|
now + if has_friends { SHOW_DELAY } else { HIDE_DELAY },
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if now >= deadline {
|
||||||
|
self.show_local = target;
|
||||||
|
self.pending = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn local_puppet_hides_after_five_seconds_and_return_delays_only_local_puppet() {
|
||||||
|
let now = Instant::now();
|
||||||
|
let mut visibility = Visibility::default();
|
||||||
|
visibility.update(true, false, now);
|
||||||
|
visibility.update(true, false, now + HIDE_DELAY - Duration::from_millis(1));
|
||||||
|
assert!(visibility.show_local);
|
||||||
|
visibility.update(true, false, now + HIDE_DELAY);
|
||||||
|
assert!(!visibility.show_local);
|
||||||
|
assert!(!visibility.window_open(false));
|
||||||
|
let returned = now + HIDE_DELAY;
|
||||||
|
visibility.update(true, true, returned);
|
||||||
|
assert!(!visibility.show_local);
|
||||||
|
assert!(visibility.window_open(true));
|
||||||
|
visibility.update(true, true, returned + SHOW_DELAY);
|
||||||
|
assert!(visibility.show_local);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reconnect_cancels_hide_and_disconnect_cancels_show() {
|
||||||
|
let now = Instant::now();
|
||||||
|
let mut visibility = Visibility::default();
|
||||||
|
visibility.update(true, false, now);
|
||||||
|
visibility.update(true, true, now + Duration::from_secs(4));
|
||||||
|
visibility.update(true, false, now + Duration::from_secs(5));
|
||||||
|
assert!(visibility.show_local);
|
||||||
|
visibility.update(true, false, now + Duration::from_secs(10));
|
||||||
|
assert!(!visibility.show_local);
|
||||||
|
visibility.update(true, true, now + Duration::from_secs(11));
|
||||||
|
visibility.update(true, false, now + Duration::from_secs(12));
|
||||||
|
visibility.update(true, true, now + Duration::from_secs(14));
|
||||||
|
assert!(!visibility.show_local);
|
||||||
|
visibility.update(true, true, now + Duration::from_secs(17));
|
||||||
|
assert!(visibility.show_local);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disabling_setting_restores_local_and_cancels_pending_timer() {
|
||||||
|
let now = Instant::now();
|
||||||
|
let mut visibility = Visibility::default();
|
||||||
|
visibility.update(true, false, now);
|
||||||
|
visibility.update(true, false, now + HIDE_DELAY);
|
||||||
|
visibility.update(false, false, now + HIDE_DELAY);
|
||||||
|
assert!(visibility.show_local);
|
||||||
|
visibility.update(true, false, now + HIDE_DELAY);
|
||||||
|
visibility.update(false, false, now + HIDE_DELAY + Duration::from_secs(1));
|
||||||
|
let restarted = now + HIDE_DELAY + Duration::from_secs(2);
|
||||||
|
visibility.update(true, false, restarted);
|
||||||
|
visibility.update(
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
restarted + HIDE_DELAY - Duration::from_millis(1),
|
||||||
|
);
|
||||||
|
assert!(visibility.show_local);
|
||||||
|
visibility.update(true, false, restarted + HIDE_DELAY);
|
||||||
|
assert!(!visibility.show_local);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,10 +41,10 @@ impl SceneConfigurationState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn puppet_movement_mode(&self) -> Result<PuppetMovementMode, String> {
|
pub(crate) fn snapshot(&self) -> Result<SceneConfiguration, String> {
|
||||||
self.0
|
self.0
|
||||||
.read()
|
.read()
|
||||||
.map(|configuration| configuration.puppet_movement_mode)
|
.map(|configuration| configuration.clone())
|
||||||
.map_err(|error| error.to_string())
|
.map_err(|error| error.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ pub fn apply_macos_decoration_window_policy(app_handle: &AppHandle, window_label
|
|||||||
pub const WINDOW_LABEL: &str = "scene";
|
pub const WINDOW_LABEL: &str = "scene";
|
||||||
|
|
||||||
fn track_scene_hitboxes(app_handle: AppHandle, window: tauri::WebviewWindow) {
|
fn track_scene_hitboxes(app_handle: AppHandle, window: tauri::WebviewWindow) {
|
||||||
tauri::async_runtime::spawn(async move {
|
let tracked_window = window.clone();
|
||||||
|
let task = tauri::async_runtime::spawn(async move {
|
||||||
let mut ignores_cursor = true;
|
let mut ignores_cursor = true;
|
||||||
let mut interval = tokio::time::interval(crate::cursor::SYSTEM_CURSOR_POLL_INTERVAL);
|
let mut interval = tokio::time::interval(crate::cursor::SYSTEM_CURSOR_POLL_INTERVAL);
|
||||||
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||||
@@ -101,14 +102,14 @@ fn track_scene_hitboxes(app_handle: AppHandle, window: tauri::WebviewWindow) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let window_position = match window.outer_position() {
|
let window_position = match tracked_window.outer_position() {
|
||||||
Ok(position) => position,
|
Ok(position) => position,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("Failed to read scene position for hit-testing: {error}");
|
eprintln!("Failed to read scene position for hit-testing: {error}");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let scale_factor = match window.scale_factor() {
|
let scale_factor = match tracked_window.scale_factor() {
|
||||||
Ok(scale_factor) => scale_factor,
|
Ok(scale_factor) => scale_factor,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("Failed to read scene scale factor for hit-testing: {error}");
|
eprintln!("Failed to read scene scale factor for hit-testing: {error}");
|
||||||
@@ -126,7 +127,7 @@ fn track_scene_hitboxes(app_handle: AppHandle, window: tauri::WebviewWindow) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if should_ignore != ignores_cursor {
|
if should_ignore != ignores_cursor {
|
||||||
match window.set_ignore_cursor_events(should_ignore) {
|
match tracked_window.set_ignore_cursor_events(should_ignore) {
|
||||||
Ok(()) => ignores_cursor = should_ignore,
|
Ok(()) => ignores_cursor = should_ignore,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("Failed to update scene cursor event policy: {error}")
|
eprintln!("Failed to update scene cursor event policy: {error}")
|
||||||
@@ -135,6 +136,11 @@ fn track_scene_hitboxes(app_handle: AppHandle, window: tauri::WebviewWindow) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
window.on_window_event(move |event| {
|
||||||
|
if matches!(event, tauri::WindowEvent::Destroyed) {
|
||||||
|
task.abort();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn overlay_fullscreen(
|
pub fn overlay_fullscreen(
|
||||||
@@ -156,9 +162,8 @@ pub fn overlay_fullscreen(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_window(app_handle: &AppHandle) -> Result<tauri::WebviewWindow, String> {
|
fn open_window(app_handle: &AppHandle) -> Result<tauri::WebviewWindow, String> {
|
||||||
if let Some(window) = app_handle.get_webview_window(WINDOW_LABEL) {
|
if let Some(window) = app_handle.get_webview_window(WINDOW_LABEL) {
|
||||||
window.show().map_err(|e| e.to_string())?;
|
|
||||||
return Ok(window);
|
return Ok(window);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -181,26 +186,77 @@ pub fn open_window(app_handle: &AppHandle) -> Result<tauri::WebviewWindow, Strin
|
|||||||
.skip_taskbar(true)
|
.skip_taskbar(true)
|
||||||
.shadow(false)
|
.shadow(false)
|
||||||
.accept_first_mouse(true)
|
.accept_first_mouse(true)
|
||||||
.visible(true)
|
.visible(false)
|
||||||
.focused(false);
|
.focused(false);
|
||||||
|
|
||||||
let window = builder.build().map_err(|e: tauri::Error| e.to_string())?;
|
let window = builder.build().map_err(|e: tauri::Error| e.to_string())?;
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
let configure = || -> Result<(), tauri::Error> {
|
||||||
window.open_devtools();
|
overlay_fullscreen(app_handle, &window)?;
|
||||||
|
window.set_ignore_cursor_events(true)?;
|
||||||
|
window.show()?;
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
if let Err(error) = configure() {
|
||||||
|
let _ = window.destroy();
|
||||||
|
return Err(error.to_string());
|
||||||
|
}
|
||||||
|
apply_macos_decoration_window_policy(app_handle, WINDOW_LABEL.to_string());
|
||||||
|
track_scene_hitboxes(app_handle.clone(), window.clone());
|
||||||
|
|
||||||
Ok(window)
|
Ok(window)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serializes native create/destroy requests, including destruction acknowledgement.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct SceneWindow(tokio::sync::Mutex<()>);
|
||||||
|
|
||||||
|
pub(crate) fn is_initialized(handle: &AppHandle) -> bool {
|
||||||
|
handle.try_state::<SceneWindow>().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a fully configured scene, or destroys its webview and stops hit-testing.
|
||||||
|
pub(crate) async fn reconcile_window(handle: &AppHandle, open: bool) -> Result<(), String> {
|
||||||
|
let state = handle
|
||||||
|
.try_state::<SceneWindow>()
|
||||||
|
.ok_or("scene UI has not started")?;
|
||||||
|
let _guard = state.0.lock().await;
|
||||||
|
if open {
|
||||||
|
if handle.get_webview_window(WINDOW_LABEL).is_none() {
|
||||||
|
handle
|
||||||
|
.state::<SceneHitboxes>()
|
||||||
|
.0
|
||||||
|
.write()
|
||||||
|
.map_err(|error| error.to_string())?
|
||||||
|
.clear();
|
||||||
|
open_window(&handle)?;
|
||||||
|
}
|
||||||
|
} else if let Some(window) = handle.get_webview_window(WINDOW_LABEL) {
|
||||||
|
let (sender, receiver) = tokio::sync::oneshot::channel();
|
||||||
|
let sender = std::sync::Mutex::new(Some(sender));
|
||||||
|
window.on_window_event(move |event| {
|
||||||
|
if matches!(event, tauri::WindowEvent::Destroyed)
|
||||||
|
&& let Some(sender) = sender.lock().unwrap().take()
|
||||||
|
{
|
||||||
|
let _ = sender.send(());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.destroy().map_err(|error| error.to_string())?;
|
||||||
|
receiver.await.map_err(|error| error.to_string())?;
|
||||||
|
handle
|
||||||
|
.state::<SceneHitboxes>()
|
||||||
|
.0
|
||||||
|
.write()
|
||||||
|
.map_err(|error| error.to_string())?
|
||||||
|
.clear();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init(app_handle: &AppHandle) {
|
pub fn init(app_handle: &AppHandle) {
|
||||||
app_handle.manage(SceneHitboxes::default());
|
app_handle.manage(SceneHitboxes::default());
|
||||||
let window = open_window(app_handle).expect("Scene window should be opened successfully");
|
// The existing puppet tick opens the scene only after publishing its snapshot.
|
||||||
overlay_fullscreen(app_handle, &window).expect("Scene window should be fullscreened");
|
app_handle.manage(SceneWindow::default());
|
||||||
apply_macos_decoration_window_policy(app_handle, WINDOW_LABEL.to_string());
|
|
||||||
window
|
|
||||||
.set_ignore_cursor_events(true)
|
|
||||||
.expect("Scene window needs to ignore cursor events");
|
|
||||||
track_scene_hitboxes(app_handle.clone(), window);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ export const puppetStates = writable<PuppetState[]>([]);
|
|||||||
export const puppetStatesListenerError = writable("");
|
export const puppetStatesListenerError = writable("");
|
||||||
|
|
||||||
export async function initPuppetStatesListener() {
|
export async function initPuppetStatesListener() {
|
||||||
|
let receivedEvent = false;
|
||||||
const unlisten = await events.puppetStatesChanged.listen((event) => {
|
const unlisten = await events.puppetStatesChanged.listen((event) => {
|
||||||
|
receivedEvent = true;
|
||||||
puppetStates.set(event.payload.puppets);
|
puppetStates.set(event.payload.puppets);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
puppetStates.set(await commands.listPuppetStates());
|
const snapshot = await commands.listPuppetStates();
|
||||||
|
// A newly created scene must not overwrite a newer event with its startup snapshot.
|
||||||
|
if (!receivedEvent) puppetStates.set(snapshot);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
puppetStatesListenerError.set(String(error));
|
puppetStatesListenerError.set(String(error));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import Popover from "$lib/components/popover.svelte";
|
import Popover from "$lib/components/popover.svelte";
|
||||||
import { friendName, friends } from "$lib/listeners/friends";
|
import { friendName, friends } from "$lib/listeners/friends";
|
||||||
import { onlineFriendIds } from "$lib/listeners/friend-statuses";
|
|
||||||
import { incomingInteraction } from "$lib/listeners/interactions";
|
import { incomingInteraction } from "$lib/listeners/interactions";
|
||||||
import { liveMetadata } from "$lib/listeners/live-metadata";
|
import { liveMetadata } from "$lib/listeners/live-metadata";
|
||||||
import { profile } from "$lib/listeners/profile";
|
import { profile } from "$lib/listeners/profile";
|
||||||
@@ -17,28 +16,16 @@
|
|||||||
import SceneUserPopoverContent from "./popovers/user-interaction.svelte";
|
import SceneUserPopoverContent from "./popovers/user-interaction.svelte";
|
||||||
import { messages } from "$lib/i18n";
|
import { messages } from "$lib/i18n";
|
||||||
|
|
||||||
const LOCAL_PUPPET_HIDE_DELAY_MS = 5_000;
|
|
||||||
const LOCAL_PUPPET_SHOW_DELAY_MS = 3_000;
|
|
||||||
|
|
||||||
let selectedUserId = $state<string | null>(null);
|
let selectedUserId = $state<string | null>(null);
|
||||||
let lockedPopoverUserId = $state<string | null>(null);
|
let lockedPopoverUserId = $state<string | null>(null);
|
||||||
let viewedImage = $state<{ source: string; senderName: string } | null>(null);
|
let viewedImage = $state<{ source: string; senderName: string } | null>(null);
|
||||||
let puppetBounds = $state<PuppetScreenBounds[]>([]);
|
let puppetBounds = $state<PuppetScreenBounds[]>([]);
|
||||||
let showLocalPuppet = $state(true);
|
|
||||||
let puppetBoundsById = $derived(
|
let puppetBoundsById = $derived(
|
||||||
new Map(puppetBounds.map((bounds) => [bounds.id, bounds])),
|
new Map(puppetBounds.map((bounds) => [bounds.id, bounds])),
|
||||||
);
|
);
|
||||||
let visiblePuppets = $derived(
|
|
||||||
$puppetStates.filter(
|
|
||||||
({ id }) =>
|
|
||||||
$onlineFriendIds.has(id) ||
|
|
||||||
(id === $liveMetadata.localId &&
|
|
||||||
(!$sceneConfiguration.hideLocalPuppetWhenAlone || showLocalPuppet)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
let skinHashes = $derived(
|
let skinHashes = $derived(
|
||||||
new Map(
|
new Map(
|
||||||
visiblePuppets.map(({ id }) => [
|
$puppetStates.map(({ id }) => [
|
||||||
id,
|
id,
|
||||||
id === $profile?.id
|
id === $profile?.id
|
||||||
? $profile.skinHash
|
? $profile.skinHash
|
||||||
@@ -51,23 +38,6 @@
|
|||||||
void getCurrentWindow().setTitle($messages.scene_window_title());
|
void getCurrentWindow().setTitle($messages.scene_window_title());
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (!$sceneConfiguration.hideLocalPuppetWhenAlone) {
|
|
||||||
showLocalPuppet = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextShowLocalPuppet = $onlineFriendIds.size > 0;
|
|
||||||
if (showLocalPuppet === nextShowLocalPuppet) return;
|
|
||||||
const timer = window.setTimeout(
|
|
||||||
() => (showLocalPuppet = nextShowLocalPuppet),
|
|
||||||
nextShowLocalPuppet
|
|
||||||
? LOCAL_PUPPET_SHOW_DELAY_MS
|
|
||||||
: LOCAL_PUPPET_HIDE_DELAY_MS,
|
|
||||||
);
|
|
||||||
return () => window.clearTimeout(timer);
|
|
||||||
});
|
|
||||||
|
|
||||||
onMount(startHitboxSync);
|
onMount(startHitboxSync);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -84,7 +54,7 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (
|
if (
|
||||||
selectedUserId &&
|
selectedUserId &&
|
||||||
!visiblePuppets.some((puppet) => puppet.id === selectedUserId)
|
!$puppetStates.some((puppet) => puppet.id === selectedUserId)
|
||||||
) {
|
) {
|
||||||
dismissPopover();
|
dismissPopover();
|
||||||
}
|
}
|
||||||
@@ -133,14 +103,14 @@
|
|||||||
|
|
||||||
<div class="relative size-full">
|
<div class="relative size-full">
|
||||||
<Renderer
|
<Renderer
|
||||||
puppets={visiblePuppets}
|
puppets={$puppetStates}
|
||||||
selectedPuppetId={selectedUserId}
|
selectedPuppetId={selectedUserId}
|
||||||
onBoundsChange={(bounds) => (puppetBounds = bounds)}
|
onBoundsChange={(bounds) => (puppetBounds = bounds)}
|
||||||
{skinHashes}
|
{skinHashes}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div role="banner" class="pointer-events-none fixed inset-0 z-10">
|
<div role="banner" class="pointer-events-none fixed inset-0 z-10">
|
||||||
{#each visiblePuppets as puppet, index (puppet.id)}
|
{#each $puppetStates as puppet, index (puppet.id)}
|
||||||
{@const userId = puppet.id}
|
{@const userId = puppet.id}
|
||||||
{@const bounds = puppetBoundsById.get(userId)}
|
{@const bounds = puppetBoundsById.get(userId)}
|
||||||
{@const foregroundApp = $liveMetadata.foregroundApps.get(userId)}
|
{@const foregroundApp = $liveMetadata.foregroundApps.get(userId)}
|
||||||
|
|||||||
Reference in New Issue
Block a user