minecraft skins!

This commit is contained in:
2026-08-29 01:41:59 +08:00
parent 9d695e704e
commit b6401a79a3
29 changed files with 1466 additions and 383 deletions
+7 -4
View File
@@ -35,8 +35,8 @@ async deleteRemote(id: string) : Promise<boolean> {
async getProfile() : Promise<User> {
return await TAURI_INVOKE("get_profile");
},
async updateProfile(displayName: string) : Promise<User> {
return await TAURI_INVOKE("update_profile", { displayName });
async updateProfile(displayName: string, skinData: number[] | null) : Promise<User> {
return await TAURI_INVOKE("update_profile", { displayName, skinData });
},
async getPublicKey() : Promise<string> {
return await TAURI_INVOKE("get_public_key");
@@ -50,6 +50,9 @@ async listFriendStatuses() : Promise<string[]> {
async resolveFriendDisplayName(userId: string) : Promise<string | null> {
return await TAURI_INVOKE("resolve_friend_display_name", { userId });
},
async resolveSkin(userId: string, skinHash: string) : Promise<string | null> {
return await TAURI_INVOKE("resolve_skin", { userId, skinHash });
},
async pickAndSendImage(recipientId: string) : Promise<boolean> {
return await TAURI_INVOKE("pick_and_send_image", { recipientId });
},
@@ -125,7 +128,7 @@ export type ForegroundAppChanged = { meta: AppMeta }
* A configured public-key relationship with optional cached remote metadata.
* The display name remains absent until learned from a signed remote profile.
*/
export type Friend = { id: string; displayName: string | null }
export type Friend = { id: string; displayName: string | null; skinHash: string | null }
export type FriendForegroundAppChanged = { friendId: string; meta: AppMeta }
export type FriendInteractionReceived = { interactionId: string; friendId: string; content: InteractionContent }
export type FriendStatusesChanged = { friendIds: string[] }
@@ -143,7 +146,7 @@ export type SceneHitbox = { x: number; y: number; width: number; height: number
* Public user identity exchanged with peers and remote servers. `id` is the
* user's Ed25519 public key and is not persisted as local profile metadata.
*/
export type User = { id: string; displayName: string }
export type User = { id: string; displayName: string; skinHash: string | null }
/** tauri-specta globals **/
@@ -12,6 +12,8 @@
let dirty = $state(false);
let busy = $state(false);
let error = $state("");
let skinFile = $state<File | null>(null);
let skinInput = $state<HTMLInputElement | null>(null);
$effect(() => {
if (!dirty) displayName = $profile?.displayName ?? "";
@@ -21,12 +23,15 @@
onMount(() => register("general", { apply, reset }, { dirty, busy }));
function updateDirty() {
dirty = displayName.trim() !== ($profile?.displayName ?? "");
dirty =
displayName.trim() !== ($profile?.displayName ?? "") || skinFile !== null;
error = "";
}
function reset() {
displayName = $profile?.displayName ?? "";
skinFile = null;
if (skinInput) skinInput.value = "";
dirty = false;
error = "";
}
@@ -42,8 +47,13 @@
busy = true;
error = "";
try {
await commands.updateProfile(nextName);
const skinData = skinFile
? Array.from(new Uint8Array(await skinFile.arrayBuffer()))
: null;
await commands.updateProfile(nextName, skinData);
displayName = nextName;
skinFile = null;
if (skinInput) skinInput.value = "";
dirty = false;
return true;
} catch (cause) {
@@ -78,6 +88,34 @@
This name can be changed at any time.
</p>
<label class="fieldset-label mt-2" for="skin-file">Character skin</label>
<div class="join w-full">
<input
class="input join-item min-w-0 flex-1"
value={skinFile?.name ?? ($profile?.skinHash ? "Custom skin" : "Default skin")}
readonly
aria-label="Selected character skin"
/>
<button
class="btn join-item"
type="button"
disabled={!$profile || busy}
onclick={() => skinInput?.click()}>Choose PNG</button
>
</div>
<input
id="skin-file"
class="hidden"
type="file"
accept="image/png"
bind:this={skinInput}
onchange={(event) => {
skinFile = event.currentTarget.files?.[0] ?? null;
updateDirty();
}}
/>
<p class="fieldset-label">64×64 Minecraft-format PNG, base layer only.</p>
<label class="fieldset-label mt-2" for="public-key"
>Identification Key
<div
+1 -1
View File
@@ -12,7 +12,7 @@
error = "";
try {
await commands.updateProfile(displayName);
await commands.updateProfile(displayName, null);
} catch (err) {
error = String(err);
}
+18
View File
@@ -0,0 +1,18 @@
import { commands } from "$lib/bindings";
export const DEFAULT_SKIN_URL = "/default-skin.png";
export async function resolveSkinSource(
userId: string,
skinHash: string | null,
) {
if (!skinHash) return DEFAULT_SKIN_URL;
try {
const data = await commands.resolveSkin(userId, skinHash);
return data ? `data:image/png;base64,${data}` : DEFAULT_SKIN_URL;
} catch (error) {
console.error(`Failed to resolve custom skin for ${userId}`, error);
return DEFAULT_SKIN_URL;
}
}