option to hide own puppet when no friends are online

This commit is contained in:
2026-09-05 02:54:45 +08:00
parent 295ba679dd
commit adb4f7fe36
9 changed files with 91 additions and 41 deletions
+1
View File
@@ -158,6 +158,7 @@
"scene_all_puppets": "All puppets", "scene_all_puppets": "All puppets",
"scene_scale": "Scale", "scene_scale": "Scale",
"scene_opacity": "Opacity", "scene_opacity": "Opacity",
"scene_hide_local_puppet_when_alone": "Hide my puppet when no friends are online",
"scene_choose_skin": "Choose a skin", "scene_choose_skin": "Choose a skin",
"scene_loading_preview": "Loading puppet preview", "scene_loading_preview": "Loading puppet preview",
"action_add_friend_title": "Add Friend", "action_add_friend_title": "Add Friend",
+1
View File
@@ -158,6 +158,7 @@
"scene_all_puppets": "所有玩偶", "scene_all_puppets": "所有玩偶",
"scene_scale": "缩放", "scene_scale": "缩放",
"scene_opacity": "不透明度", "scene_opacity": "不透明度",
"scene_hide_local_puppet_when_alone": "没有好友在线时隐藏我的玩偶",
"scene_choose_skin": "选择皮肤", "scene_choose_skin": "选择皮肤",
"scene_loading_preview": "正在加载玩偶预览", "scene_loading_preview": "正在加载玩偶预览",
"action_add_friend_title": "添加好友", "action_add_friend_title": "添加好友",
@@ -0,0 +1,2 @@
ALTER TABLE scene_configuration
ADD COLUMN hide_local_puppet_when_alone BOOLEAN NOT NULL DEFAULT FALSE;
+9 -2
View File
@@ -26,6 +26,7 @@ pub struct SceneConfiguration {
pub puppet_scale: f64, pub puppet_scale: f64,
pub puppet_opacity: f64, pub puppet_opacity: f64,
pub puppet_movement_mode: PuppetMovementMode, pub puppet_movement_mode: PuppetMovementMode,
pub hide_local_puppet_when_alone: bool,
} }
pub struct SceneConfigurationState(RwLock<SceneConfiguration>); pub struct SceneConfigurationState(RwLock<SceneConfiguration>);
@@ -56,7 +57,7 @@ pub struct SceneConfigurationChanged {
async fn get(database: &AppDatabase) -> Result<SceneConfiguration, sqlx::Error> { async fn get(database: &AppDatabase) -> Result<SceneConfiguration, sqlx::Error> {
sqlx::query_as::<_, SceneConfiguration>( sqlx::query_as::<_, SceneConfiguration>(
"SELECT puppet_scale, puppet_opacity, puppet_movement_mode FROM scene_configuration WHERE id = ?1", "SELECT puppet_scale, puppet_opacity, puppet_movement_mode, hide_local_puppet_when_alone FROM scene_configuration WHERE id = ?1",
) )
.bind(SCENE_CONFIGURATION_ID) .bind(SCENE_CONFIGURATION_ID)
.fetch_one(database.pool()) .fetch_one(database.pool())
@@ -83,11 +84,12 @@ async fn update(
} }
sqlx::query( sqlx::query(
"UPDATE scene_configuration SET puppet_scale = ?1, puppet_opacity = ?2, puppet_movement_mode = ?3 WHERE id = ?4", "UPDATE scene_configuration SET puppet_scale = ?1, puppet_opacity = ?2, puppet_movement_mode = ?3, hide_local_puppet_when_alone = ?4 WHERE id = ?5",
) )
.bind(configuration.puppet_scale) .bind(configuration.puppet_scale)
.bind(configuration.puppet_opacity) .bind(configuration.puppet_opacity)
.bind(configuration.puppet_movement_mode) .bind(configuration.puppet_movement_mode)
.bind(configuration.hide_local_puppet_when_alone)
.bind(SCENE_CONFIGURATION_ID) .bind(SCENE_CONFIGURATION_ID)
.execute(database.pool()) .execute(database.pool())
.await .await
@@ -164,6 +166,7 @@ mod tests {
puppet_scale: 1.0, puppet_scale: 1.0,
puppet_opacity: 1.0, puppet_opacity: 1.0,
puppet_movement_mode: PuppetMovementMode::Free, puppet_movement_mode: PuppetMovementMode::Free,
hide_local_puppet_when_alone: false,
} }
); );
} }
@@ -178,6 +181,7 @@ mod tests {
puppet_scale: 1.5, puppet_scale: 1.5,
puppet_opacity: 0.4, puppet_opacity: 0.4,
puppet_movement_mode: PuppetMovementMode::Bottom, puppet_movement_mode: PuppetMovementMode::Bottom,
hide_local_puppet_when_alone: true,
}, },
) )
.await .await
@@ -188,6 +192,7 @@ mod tests {
configuration.puppet_movement_mode, configuration.puppet_movement_mode,
PuppetMovementMode::Bottom PuppetMovementMode::Bottom
); );
assert!(configuration.hide_local_puppet_when_alone);
assert_eq!(get(&database).await.unwrap(), configuration); assert_eq!(get(&database).await.unwrap(), configuration);
let error = update( let error = update(
@@ -196,6 +201,7 @@ mod tests {
puppet_scale: 2.1, puppet_scale: 2.1,
puppet_opacity: 0.4, puppet_opacity: 0.4,
puppet_movement_mode: PuppetMovementMode::Bottom, puppet_movement_mode: PuppetMovementMode::Bottom,
hide_local_puppet_when_alone: true,
}, },
) )
.await .await
@@ -209,6 +215,7 @@ mod tests {
puppet_scale: 1.5, puppet_scale: 1.5,
puppet_opacity: 0.05, puppet_opacity: 0.05,
puppet_movement_mode: PuppetMovementMode::Bottom, puppet_movement_mode: PuppetMovementMode::Bottom,
hide_local_puppet_when_alone: true,
}, },
) )
.await .await
+1 -1
View File
@@ -187,7 +187,7 @@ export type PuppetStatesChanged = { puppets: PuppetState[] }
export type Remote = { id: string; address: string; name: string | null; port: number | null; priority: number } export type Remote = { id: string; address: string; name: string | null; port: number | null; priority: number }
export type RemoteInput = { address: string; name: string | null; port: number | null } export type RemoteInput = { address: string; name: string | null; port: number | null }
export type RemotesChanged = { remotes: Remote[] } export type RemotesChanged = { remotes: Remote[] }
export type SceneConfiguration = { puppetScale: number; puppetOpacity: number; puppetMovementMode: PuppetMovementMode } export type SceneConfiguration = { puppetScale: number; puppetOpacity: number; puppetMovementMode: PuppetMovementMode; hideLocalPuppetWhenAlone: boolean }
export type SceneConfigurationChanged = { configuration: SceneConfiguration } export type SceneConfigurationChanged = { configuration: SceneConfiguration }
export type SceneHitbox = { x: number; y: number; width: number; height: number } export type SceneHitbox = { x: number; y: number; width: number; height: number }
/** /**
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { commands, type PuppetMovementMode } from "$lib/bindings"; import { commands, type SceneConfiguration } from "$lib/bindings";
import { import {
sceneConfiguration, sceneConfiguration,
sceneConfigurationListenerError, sceneConfigurationListenerError,
@@ -13,9 +13,12 @@
let { register }: { register: RegisterPanel } = $props(); let { register }: { register: RegisterPanel } = $props();
let puppetScale = $state(1); let configurationDraft = $state<SceneConfiguration>({
let puppetOpacity = $state(1); puppetScale: 1,
let puppetMovementMode = $state<PuppetMovementMode>("free"); puppetOpacity: 1,
puppetMovementMode: "free",
hideLocalPuppetWhenAlone: false,
});
let skinFile = $state<File | null>(null); let skinFile = $state<File | null>(null);
let skinPreviewUrl = $state<string | null>(null); let skinPreviewUrl = $state<string | null>(null);
let skinInput = $state<HTMLInputElement | null>(null); let skinInput = $state<HTMLInputElement | null>(null);
@@ -26,9 +29,7 @@
$effect(() => { $effect(() => {
if (!dirty) { if (!dirty) {
puppetScale = $sceneConfiguration.puppetScale; configurationDraft = { ...$sceneConfiguration };
puppetOpacity = $sceneConfiguration.puppetOpacity;
puppetMovementMode = $sceneConfiguration.puppetMovementMode;
} }
register("scene", { apply, reset }, { dirty, busy }); register("scene", { apply, reset }, { dirty, busy });
}); });
@@ -40,9 +41,12 @@
function hasConfigurationChanges() { function hasConfigurationChanges() {
return ( return (
puppetScale !== $sceneConfiguration.puppetScale || configurationDraft.puppetScale !== $sceneConfiguration.puppetScale ||
puppetOpacity !== $sceneConfiguration.puppetOpacity || configurationDraft.puppetOpacity !== $sceneConfiguration.puppetOpacity ||
puppetMovementMode !== $sceneConfiguration.puppetMovementMode configurationDraft.puppetMovementMode !==
$sceneConfiguration.puppetMovementMode ||
configurationDraft.hideLocalPuppetWhenAlone !==
$sceneConfiguration.hideLocalPuppetWhenAlone
); );
} }
@@ -73,9 +77,7 @@
} }
function reset() { function reset() {
puppetScale = $sceneConfiguration.puppetScale; configurationDraft = { ...$sceneConfiguration };
puppetOpacity = $sceneConfiguration.puppetOpacity;
puppetMovementMode = $sceneConfiguration.puppetMovementMode;
clearSkinDraft(); clearSkinDraft();
pendingSkinReset = false; pendingSkinReset = false;
dirty = false; dirty = false;
@@ -102,13 +104,9 @@
if (hasConfigurationChanges()) { if (hasConfigurationChanges()) {
const configuration = await commands.updateSceneConfiguration({ const configuration = await commands.updateSceneConfiguration({
puppetScale, ...configurationDraft,
puppetOpacity,
puppetMovementMode,
}); });
puppetScale = configuration.puppetScale; configurationDraft = { ...configuration };
puppetOpacity = configuration.puppetOpacity;
puppetMovementMode = configuration.puppetMovementMode;
} }
dirty = false; dirty = false;
return true; return true;
@@ -149,8 +147,8 @@
<p>{$messages.scene_opacity()}</p> <p>{$messages.scene_opacity()}</p>
</div> </div>
<div class="text-end flex flex-col"> <div class="text-end flex flex-col">
<p>{(puppetScale * 100).toFixed(0)}%</p> <p>{(configurationDraft.puppetScale * 100).toFixed(0)}%</p>
<p>{(puppetOpacity * 100).toFixed(0)}%</p> <p>{(configurationDraft.puppetOpacity * 100).toFixed(0)}%</p>
</div> </div>
</div> </div>
</div> </div>
@@ -159,8 +157,8 @@
userId={$profile.id} userId={$profile.id}
skinHash={pendingSkinReset ? null : $profile.skinHash} skinHash={pendingSkinReset ? null : $profile.skinHash}
skinSource={skinPreviewUrl} skinSource={skinPreviewUrl}
scale={puppetScale} scale={configurationDraft.puppetScale}
opacity={puppetOpacity} opacity={configurationDraft.puppetOpacity}
/> />
</div> </div>
</div> </div>
@@ -189,7 +187,7 @@
min="0.5" min="0.5"
max="2" max="2"
step="0.05" step="0.05"
bind:value={puppetScale} bind:value={configurationDraft.puppetScale}
oninput={updateDirty} oninput={updateDirty}
disabled={busy} disabled={busy}
aria-describedby="puppet-scale-help" aria-describedby="puppet-scale-help"
@@ -207,12 +205,23 @@
min="0.25" min="0.25"
max="1" max="1"
step="0.05" step="0.05"
bind:value={puppetOpacity} bind:value={configurationDraft.puppetOpacity}
oninput={updateDirty} oninput={updateDirty}
disabled={busy} disabled={busy}
aria-describedby="puppet-opacity-help" aria-describedby="puppet-opacity-help"
/> />
</div> </div>
<label class="label cursor-pointer justify-start gap-2">
<input
class="checkbox checkbox-sm checkbox-primary"
type="checkbox"
bind:checked={configurationDraft.hideLocalPuppetWhenAlone}
onchange={updateDirty}
disabled={busy}
/>
<span>{$messages.scene_hide_local_puppet_when_alone()}</span>
</label>
</fieldset> </fieldset>
<div class="grid grid-cols-[minmax(0,1fr)_auto] gap-2"> <div class="grid grid-cols-[minmax(0,1fr)_auto] gap-2">
<button <button
@@ -247,19 +256,21 @@
<legend class="fieldset-legend px-1">{$messages.puppet_movement()}</legend> <legend class="fieldset-legend px-1">{$messages.puppet_movement()}</legend>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<label <label
class:border-primary={puppetMovementMode === "free"} class:border-primary={configurationDraft.puppetMovementMode === "free"}
class="card cursor-pointer overflow-hidden border border-base-300 bg-base-200" class="card cursor-pointer overflow-hidden border border-base-300 bg-base-200"
> >
<img <img
src="/puppet-movement-free.svg" src="/puppet-movement-free.svg"
alt={$messages.puppet_free_alt()} alt={$messages.puppet_free_alt()}
class="w-full object-cover bg-linear-to-b from-base-100 to-base-300" class="w-full object-cover bg-linear-to-b from-base-100 to-base-300"
class:from-primary-content={puppetMovementMode === "free"} class:from-primary-content={configurationDraft.puppetMovementMode ===
"free"}
/> />
<span <span
class="card-body items-center gap-2 p-2 text-center" class="card-body items-center gap-2 p-2 text-center"
class:bg-primary-content={puppetMovementMode === "free"} class:bg-primary-content={configurationDraft.puppetMovementMode ===
class:text-primary={puppetMovementMode === "free"} "free"}
class:text-primary={configurationDraft.puppetMovementMode === "free"}
> >
<span class="text-xs font-medium">{$messages.puppet_free()}</span> <span class="text-xs font-medium">{$messages.puppet_free()}</span>
<input <input
@@ -267,9 +278,9 @@
type="radio" type="radio"
name="puppet-movement-mode" name="puppet-movement-mode"
value="free" value="free"
checked={puppetMovementMode === "free"} checked={configurationDraft.puppetMovementMode === "free"}
onchange={() => { onchange={() => {
puppetMovementMode = "free"; configurationDraft.puppetMovementMode = "free";
updateDirty(); updateDirty();
}} }}
disabled={busy} disabled={busy}
@@ -278,19 +289,22 @@
</label> </label>
<label <label
class:border-primary={puppetMovementMode === "bottom"} class:border-primary={configurationDraft.puppetMovementMode ===
class:text-primary={puppetMovementMode === "bottom"} "bottom"}
class:text-primary={configurationDraft.puppetMovementMode === "bottom"}
class="card cursor-pointer overflow-hidden border border-base-300 bg-base-200" class="card cursor-pointer overflow-hidden border border-base-300 bg-base-200"
> >
<img <img
src="/puppet-movement-bottom.svg" src="/puppet-movement-bottom.svg"
alt={$messages.puppet_bottom_alt()} alt={$messages.puppet_bottom_alt()}
class="w-full object-cover bg-linear-to-b from-base-100 to-base-300" class="w-full object-cover bg-linear-to-b from-base-100 to-base-300"
class:from-primary-content={puppetMovementMode === "bottom"} class:from-primary-content={configurationDraft.puppetMovementMode ===
"bottom"}
/> />
<span <span
class="card-body items-center gap-2 p-2 text-center" class="card-body items-center gap-2 p-2 text-center"
class:bg-primary-content={puppetMovementMode === "bottom"} class:bg-primary-content={configurationDraft.puppetMovementMode ===
"bottom"}
> >
<span class="text-xs font-medium">{$messages.puppet_bottom()}</span> <span class="text-xs font-medium">{$messages.puppet_bottom()}</span>
<input <input
@@ -298,9 +312,9 @@
type="radio" type="radio"
name="puppet-movement-mode" name="puppet-movement-mode"
value="bottom" value="bottom"
checked={puppetMovementMode === "bottom"} checked={configurationDraft.puppetMovementMode === "bottom"}
onchange={() => { onchange={() => {
puppetMovementMode = "bottom"; configurationDraft.puppetMovementMode = "bottom";
updateDirty(); updateDirty();
}} }}
disabled={busy} disabled={busy}
+1
View File
@@ -9,6 +9,7 @@ export const sceneConfiguration = writable<SceneConfiguration>({
puppetScale: 1, puppetScale: 1,
puppetOpacity: 1, puppetOpacity: 1,
puppetMovementMode: "free", puppetMovementMode: "free",
hideLocalPuppetWhenAlone: false,
}); });
export const sceneConfigurationListenerError = writable(""); export const sceneConfigurationListenerError = writable("");
+1
View File
@@ -90,6 +90,7 @@
puppetScale: 1, puppetScale: 1,
puppetOpacity: 1, puppetOpacity: 1,
puppetMovementMode: "free", puppetMovementMode: "free",
hideLocalPuppetWhenAlone: false,
}); });
let remediation = $state(false); let remediation = $state(false);
let stepIndex = $state(0); let stepIndex = $state(0);
+24 -1
View File
@@ -17,17 +17,23 @@
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( let visiblePuppets = $derived(
$puppetStates.filter( $puppetStates.filter(
({ id }) => ({ id }) =>
id === $liveMetadata.localId || $onlineFriendIds.has(id), $onlineFriendIds.has(id) ||
(id === $liveMetadata.localId &&
(!$sceneConfiguration.hideLocalPuppetWhenAlone || showLocalPuppet)),
), ),
); );
let skinHashes = $derived( let skinHashes = $derived(
@@ -45,6 +51,23 @@
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(() => {