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_scale": "Scale",
"scene_opacity": "Opacity",
"scene_hide_local_puppet_when_alone": "Hide my puppet when no friends are online",
"scene_choose_skin": "Choose a skin",
"scene_loading_preview": "Loading puppet preview",
"action_add_friend_title": "Add Friend",
+1
View File
@@ -158,6 +158,7 @@
"scene_all_puppets": "所有玩偶",
"scene_scale": "缩放",
"scene_opacity": "不透明度",
"scene_hide_local_puppet_when_alone": "没有好友在线时隐藏我的玩偶",
"scene_choose_skin": "选择皮肤",
"scene_loading_preview": "正在加载玩偶预览",
"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_opacity: f64,
pub puppet_movement_mode: PuppetMovementMode,
pub hide_local_puppet_when_alone: bool,
}
pub struct SceneConfigurationState(RwLock<SceneConfiguration>);
@@ -56,7 +57,7 @@ pub struct SceneConfigurationChanged {
async fn get(database: &AppDatabase) -> Result<SceneConfiguration, sqlx::Error> {
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)
.fetch_one(database.pool())
@@ -83,11 +84,12 @@ async fn update(
}
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_opacity)
.bind(configuration.puppet_movement_mode)
.bind(configuration.hide_local_puppet_when_alone)
.bind(SCENE_CONFIGURATION_ID)
.execute(database.pool())
.await
@@ -164,6 +166,7 @@ mod tests {
puppet_scale: 1.0,
puppet_opacity: 1.0,
puppet_movement_mode: PuppetMovementMode::Free,
hide_local_puppet_when_alone: false,
}
);
}
@@ -178,6 +181,7 @@ mod tests {
puppet_scale: 1.5,
puppet_opacity: 0.4,
puppet_movement_mode: PuppetMovementMode::Bottom,
hide_local_puppet_when_alone: true,
},
)
.await
@@ -188,6 +192,7 @@ mod tests {
configuration.puppet_movement_mode,
PuppetMovementMode::Bottom
);
assert!(configuration.hide_local_puppet_when_alone);
assert_eq!(get(&database).await.unwrap(), configuration);
let error = update(
@@ -196,6 +201,7 @@ mod tests {
puppet_scale: 2.1,
puppet_opacity: 0.4,
puppet_movement_mode: PuppetMovementMode::Bottom,
hide_local_puppet_when_alone: true,
},
)
.await
@@ -209,6 +215,7 @@ mod tests {
puppet_scale: 1.5,
puppet_opacity: 0.05,
puppet_movement_mode: PuppetMovementMode::Bottom,
hide_local_puppet_when_alone: true,
},
)
.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 RemoteInput = { address: string; name: string | null; port: number | null }
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 SceneHitbox = { x: number; y: number; width: number; height: number }
/**
@@ -1,5 +1,5 @@
<script lang="ts">
import { commands, type PuppetMovementMode } from "$lib/bindings";
import { commands, type SceneConfiguration } from "$lib/bindings";
import {
sceneConfiguration,
sceneConfigurationListenerError,
@@ -13,9 +13,12 @@
let { register }: { register: RegisterPanel } = $props();
let puppetScale = $state(1);
let puppetOpacity = $state(1);
let puppetMovementMode = $state<PuppetMovementMode>("free");
let configurationDraft = $state<SceneConfiguration>({
puppetScale: 1,
puppetOpacity: 1,
puppetMovementMode: "free",
hideLocalPuppetWhenAlone: false,
});
let skinFile = $state<File | null>(null);
let skinPreviewUrl = $state<string | null>(null);
let skinInput = $state<HTMLInputElement | null>(null);
@@ -26,9 +29,7 @@
$effect(() => {
if (!dirty) {
puppetScale = $sceneConfiguration.puppetScale;
puppetOpacity = $sceneConfiguration.puppetOpacity;
puppetMovementMode = $sceneConfiguration.puppetMovementMode;
configurationDraft = { ...$sceneConfiguration };
}
register("scene", { apply, reset }, { dirty, busy });
});
@@ -40,9 +41,12 @@
function hasConfigurationChanges() {
return (
puppetScale !== $sceneConfiguration.puppetScale ||
puppetOpacity !== $sceneConfiguration.puppetOpacity ||
puppetMovementMode !== $sceneConfiguration.puppetMovementMode
configurationDraft.puppetScale !== $sceneConfiguration.puppetScale ||
configurationDraft.puppetOpacity !== $sceneConfiguration.puppetOpacity ||
configurationDraft.puppetMovementMode !==
$sceneConfiguration.puppetMovementMode ||
configurationDraft.hideLocalPuppetWhenAlone !==
$sceneConfiguration.hideLocalPuppetWhenAlone
);
}
@@ -73,9 +77,7 @@
}
function reset() {
puppetScale = $sceneConfiguration.puppetScale;
puppetOpacity = $sceneConfiguration.puppetOpacity;
puppetMovementMode = $sceneConfiguration.puppetMovementMode;
configurationDraft = { ...$sceneConfiguration };
clearSkinDraft();
pendingSkinReset = false;
dirty = false;
@@ -102,13 +104,9 @@
if (hasConfigurationChanges()) {
const configuration = await commands.updateSceneConfiguration({
puppetScale,
puppetOpacity,
puppetMovementMode,
...configurationDraft,
});
puppetScale = configuration.puppetScale;
puppetOpacity = configuration.puppetOpacity;
puppetMovementMode = configuration.puppetMovementMode;
configurationDraft = { ...configuration };
}
dirty = false;
return true;
@@ -149,8 +147,8 @@
<p>{$messages.scene_opacity()}</p>
</div>
<div class="text-end flex flex-col">
<p>{(puppetScale * 100).toFixed(0)}%</p>
<p>{(puppetOpacity * 100).toFixed(0)}%</p>
<p>{(configurationDraft.puppetScale * 100).toFixed(0)}%</p>
<p>{(configurationDraft.puppetOpacity * 100).toFixed(0)}%</p>
</div>
</div>
</div>
@@ -159,8 +157,8 @@
userId={$profile.id}
skinHash={pendingSkinReset ? null : $profile.skinHash}
skinSource={skinPreviewUrl}
scale={puppetScale}
opacity={puppetOpacity}
scale={configurationDraft.puppetScale}
opacity={configurationDraft.puppetOpacity}
/>
</div>
</div>
@@ -189,7 +187,7 @@
min="0.5"
max="2"
step="0.05"
bind:value={puppetScale}
bind:value={configurationDraft.puppetScale}
oninput={updateDirty}
disabled={busy}
aria-describedby="puppet-scale-help"
@@ -207,12 +205,23 @@
min="0.25"
max="1"
step="0.05"
bind:value={puppetOpacity}
bind:value={configurationDraft.puppetOpacity}
oninput={updateDirty}
disabled={busy}
aria-describedby="puppet-opacity-help"
/>
</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>
<div class="grid grid-cols-[minmax(0,1fr)_auto] gap-2">
<button
@@ -247,19 +256,21 @@
<legend class="fieldset-legend px-1">{$messages.puppet_movement()}</legend>
<div class="grid grid-cols-2 gap-2">
<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"
>
<img
src="/puppet-movement-free.svg"
alt={$messages.puppet_free_alt()}
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
class="card-body items-center gap-2 p-2 text-center"
class:bg-primary-content={puppetMovementMode === "free"}
class:text-primary={puppetMovementMode === "free"}
class:bg-primary-content={configurationDraft.puppetMovementMode ===
"free"}
class:text-primary={configurationDraft.puppetMovementMode === "free"}
>
<span class="text-xs font-medium">{$messages.puppet_free()}</span>
<input
@@ -267,9 +278,9 @@
type="radio"
name="puppet-movement-mode"
value="free"
checked={puppetMovementMode === "free"}
checked={configurationDraft.puppetMovementMode === "free"}
onchange={() => {
puppetMovementMode = "free";
configurationDraft.puppetMovementMode = "free";
updateDirty();
}}
disabled={busy}
@@ -278,19 +289,22 @@
</label>
<label
class:border-primary={puppetMovementMode === "bottom"}
class:text-primary={puppetMovementMode === "bottom"}
class:border-primary={configurationDraft.puppetMovementMode ===
"bottom"}
class:text-primary={configurationDraft.puppetMovementMode === "bottom"}
class="card cursor-pointer overflow-hidden border border-base-300 bg-base-200"
>
<img
src="/puppet-movement-bottom.svg"
alt={$messages.puppet_bottom_alt()}
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
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>
<input
@@ -298,9 +312,9 @@
type="radio"
name="puppet-movement-mode"
value="bottom"
checked={puppetMovementMode === "bottom"}
checked={configurationDraft.puppetMovementMode === "bottom"}
onchange={() => {
puppetMovementMode = "bottom";
configurationDraft.puppetMovementMode = "bottom";
updateDirty();
}}
disabled={busy}
+1
View File
@@ -9,6 +9,7 @@ export const sceneConfiguration = writable<SceneConfiguration>({
puppetScale: 1,
puppetOpacity: 1,
puppetMovementMode: "free",
hideLocalPuppetWhenAlone: false,
});
export const sceneConfigurationListenerError = writable("");
+1
View File
@@ -90,6 +90,7 @@
puppetScale: 1,
puppetOpacity: 1,
puppetMovementMode: "free",
hideLocalPuppetWhenAlone: false,
});
let remediation = $state(false);
let stepIndex = $state(0);
+24 -1
View File
@@ -17,17 +17,23 @@
import SceneUserPopoverContent from "./popovers/user-interaction.svelte";
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 lockedPopoverUserId = $state<string | null>(null);
let viewedImage = $state<{ source: string; senderName: string } | null>(null);
let puppetBounds = $state<PuppetScreenBounds[]>([]);
let showLocalPuppet = $state(true);
let puppetBoundsById = $derived(
new Map(puppetBounds.map((bounds) => [bounds.id, bounds])),
);
let visiblePuppets = $derived(
$puppetStates.filter(
({ id }) =>
id === $liveMetadata.localId || $onlineFriendIds.has(id),
$onlineFriendIds.has(id) ||
(id === $liveMetadata.localId &&
(!$sceneConfiguration.hideLocalPuppetWhenAlone || showLocalPuppet)),
),
);
let skinHashes = $derived(
@@ -45,6 +51,23 @@
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);
$effect(() => {