mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 13:02:49 +08:00
Review feedback on PR #56: 1. setCustomAvatar(buf) now triggers applyIdleAvatar when the bot is idle (currentSong=null OR avatarEnabled=false). Spec said this; original impl only stored the buffer, so a fresh upload from Settings was invisible until next stop event. Track currentSong in BotProfileManager for the idle check. 2. onConnect drops the !avatarEnabled guard — on a fresh connect there's no song playing yet, so the spec matrix wants the custom avatar shown regardless of sync. Previously bots reconnected with a stale TS3 server-side avatar. 3. CustomAvatarRow: defer the initializing=false flip to nextTick so the load-time data-url assignment's queued watcher sees initializing=true and bails. Removes the redundant PUT-on-mount that echoed the just- loaded bytes back to the server. 4. BotInstance avatar load wrapped in try/catch — a corrupt/locked file no longer crashes startup; we log and continue with no custom avatar. Tests rewritten: 10 cases covering the full behavior matrix (idle vs playing × sync on/off × custom set/null × stop/connect). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
60 lines
1.8 KiB
Vue
60 lines
1.8 KiB
Vue
<template>
|
|
<AvatarUpload v-model="avatarDataUrl" />
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, watch, nextTick } from 'vue';
|
|
import axios from 'axios';
|
|
import AvatarUpload from './AvatarUpload.vue';
|
|
|
|
const props = defineProps<{ botId: string }>();
|
|
const avatarDataUrl = ref<string | null>(null);
|
|
// Stays true until the watcher queued by the load-time assignment has run,
|
|
// so the initial null → loaded-data-url transition does not fire a redundant
|
|
// PUT echoing the just-fetched bytes back to the server.
|
|
let initializing = true;
|
|
|
|
async function loadCurrent() {
|
|
try {
|
|
const res = await axios.get(`/api/bot/${props.botId}/avatar`, { responseType: 'blob' });
|
|
const blob = res.data as Blob;
|
|
avatarDataUrl.value = await blobToDataUrl(blob);
|
|
} catch (err: any) {
|
|
if (err?.response?.status !== 404) {
|
|
console.warn('failed to load avatar', err);
|
|
}
|
|
avatarDataUrl.value = null;
|
|
} finally {
|
|
// Wait for the watcher's flush queue to drain (it'll see initializing=true
|
|
// and bail), then release for real user-driven changes.
|
|
await nextTick();
|
|
initializing = false;
|
|
}
|
|
}
|
|
|
|
function blobToDataUrl(blob: Blob): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => resolve(reader.result as string);
|
|
reader.onerror = () => reject(reader.error);
|
|
reader.readAsDataURL(blob);
|
|
});
|
|
}
|
|
|
|
watch(avatarDataUrl, async (newVal, oldVal) => {
|
|
if (initializing) return;
|
|
if (newVal === oldVal) return;
|
|
try {
|
|
if (newVal && newVal.startsWith('data:')) {
|
|
await axios.put(`/api/bot/${props.botId}/avatar`, { dataUrl: newVal });
|
|
} else if (newVal === null) {
|
|
await axios.delete(`/api/bot/${props.botId}/avatar`);
|
|
}
|
|
} catch (err) {
|
|
console.warn('avatar update failed', err);
|
|
}
|
|
});
|
|
|
|
onMounted(loadCurrent);
|
|
</script>
|