Imagens de ator: portrait e token
Duas funções completas de montagem com seletor File e a operação multipart real de host.call.
Estes dois exemplos usam host.call("actor.image.upload", payload), uma operação do SDK de bloco. Eles não usam api.actors.update para transmitir arquivos. O retorno é {url} e o servidor valida campanha, edição do ator e imagem.
Prepare o pacote mínimo, um ator fictício e uma imagem original PNG/JPEG/WebP. Registre cada função na superfície actor.sheet: register("actor.sheet", mountPortraitUpload) ou register("actor.sheet", mountTokenUpload). O contexto precisa conter actorId e canEdit; o usuário precisa ser GM ou ter permissão de edição do ator. O controle fica desabilitado quando canEdit não é verdadeiro; o servidor continua sendo a autoridade.
Copie uma função inteira para main.js. Não copie o descritor de arquivo do JSON Schema para o campo file: no navegador ele recebe o File real do seletor. O bridge produz multipart e metadados. Não use base64.
kind: "portrait": Enviar um retrato
Uso: trocar o retrato do ator exibido na ficha.
export function mountPortraitUpload({ root, context, host, signal }) {
const doc = root.ownerDocument;
const label = doc.createElement("label");
label.textContent = "Enviar um retrato";
const input = doc.createElement("input");
input.type = "file";
input.accept = "image/png,image/jpeg,image/webp";
const status = doc.createElement("output");
status.setAttribute("aria-live", "polite");
const preview = doc.createElement("img");
preview.alt = "Imagem enviada";
preview.width = 128;
preview.height = 128;
preview.hidden = true;
label.append(input);
root.append(label, status, preview);
input.disabled = context.canEdit !== true;
input.addEventListener("change", async () => {
const file = input.files?.[0];
if (!file || signal.aborted) return;
input.disabled = true;
status.textContent = "Enviando…";
try {
const result = await host.call("actor.image.upload", {
id: context.actorId,
kind: "portrait",
file,
}, { signal });
if (signal.aborted) return;
preview.src = result.url;
preview.hidden = false;
status.textContent = "Imagem salva.";
} catch (error) {
if (!signal.aborted) status.textContent = "Falha: " + error.code;
} finally {
if (!signal.aborted) input.disabled = context.canEdit !== true;
}
}, { signal });
}Resultado esperado: o preview usa a URL autorizada retornada e o ator conserva a nova imagem portrait após reabrir a ficha.
Valide: envie uma imagem pequena, reabra a ficha e confira a imagem. Repita com jogador autorizado e somente leitura. Feche durante o envio e verifique que a interface encerrada não recebe atualizações.
kind: "token": Enviar a imagem do token
Uso: trocar a imagem de token associada ao ator; isto não altera posição, escala ou tipo de vínculo de tokens já colocados.
export function mountTokenUpload({ root, context, host, signal }) {
const doc = root.ownerDocument;
const label = doc.createElement("label");
label.textContent = "Enviar a imagem do token";
const input = doc.createElement("input");
input.type = "file";
input.accept = "image/png,image/jpeg,image/webp";
const status = doc.createElement("output");
status.setAttribute("aria-live", "polite");
const preview = doc.createElement("img");
preview.alt = "Imagem enviada";
preview.width = 128;
preview.height = 128;
preview.hidden = true;
label.append(input);
root.append(label, status, preview);
input.disabled = context.canEdit !== true;
input.addEventListener("change", async () => {
const file = input.files?.[0];
if (!file || signal.aborted) return;
input.disabled = true;
status.textContent = "Enviando…";
try {
const result = await host.call("actor.image.upload", {
id: context.actorId,
kind: "token",
file,
}, { signal });
if (signal.aborted) return;
preview.src = result.url;
preview.hidden = false;
status.textContent = "Imagem salva.";
} catch (error) {
if (!signal.aborted) status.textContent = "Falha: " + error.code;
} finally {
if (!signal.aborted) input.disabled = context.canEdit !== true;
}
}, { signal });
}Resultado esperado: o preview usa a URL autorizada retornada e o ator conserva a nova imagem token após reabrir a ficha.
Valide: envie uma imagem pequena, reabra a ficha e confira a imagem. Repita com jogador autorizado e somente leitura. Feche durante o envio e verifique que a interface encerrada não recebe atualizações.
Limites, erros e ciclo de vida
O servidor aceita PNG, JPEG ou WebP com até 10.000.000 bytes e 25.000.000 pixels. Ele normaliza a orientação e salva PNG. O atributo accept apenas orienta o seletor; não substitui essa validação.
Ator inacessível pode retornar not_found ou permission_denied; imagem inválida retorna invalid_data; falha de transporte pode retornar unavailable. O fechamento cancela o contexto: ignore atualizações de DOM após signal.aborted. Um envio já confirmado pelo servidor permanece salvo mesmo que a interface feche. O listener usa {signal} e é removido na desmontagem. Use api.actors.get(context.actorId) novamente antes de uma edição posterior que dependa da versão do ator.
Como usar a IA para criar um seletor de imagem
Confira o schema actor.image.upload e browser-bridge.js na release alvo. Adapte mountTokenUpload para exibir progresso acessível usando a opção onProgress de host.call. Preserve File multipart, permissões nativas, signal, limpeza e textContent. Não invente um endpoint ou grave URL do usuário diretamente no ator. Mostre testes de imagem inválida, fechamento durante envio e jogador somente leitura.