Skip to content

Instantly share code, notes, and snippets.

@ariankordi
Created March 10, 2026 19:31
Show Gist options
  • Select an option

  • Save ariankordi/a8e24dcc17fd4fd78c32771725f0efe4 to your computer and use it in GitHub Desktop.

Select an option

Save ariankordi/a8e24dcc17fd4fd78c32771725f0efe4 to your computer and use it in GitHub Desktop.
MiiImportController wip version and esbuild bundled 03/10/2026
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// ../../../build/qr-scanner/qr-scanner.legacy.min.js
var require_qr_scanner_legacy_min = __commonJS({
"../../../build/qr-scanner/qr-scanner.legacy.min.js"(exports, module) {
"use strict";
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, global.QrScanner = factory());
})(exports, function() {
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P ? value : new P(function(resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function(resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
class QrScanner2 {
static hasCamera() {
return __awaiter(
this,
void 0,
void 0,
function* () {
try {
return !!(yield QrScanner2.listCameras(false)).length;
} catch (e) {
return false;
}
}
);
}
static setBarcodeDetectorDisabled() {
this._disableBarcodeDetector = true;
}
static listCameras(requestLabels = false) {
return __awaiter(this, void 0, void 0, function* () {
if (!navigator.mediaDevices) return [];
let enumerateCameras = () => __awaiter(this, void 0, void 0, function* () {
return (yield navigator.mediaDevices.enumerateDevices()).filter((device) => device.kind === "videoinput");
});
let openedStream;
try {
if (requestLabels && (yield enumerateCameras()).every((camera) => !camera.label)) openedStream = yield navigator.mediaDevices.getUserMedia({ audio: false, video: true });
} catch (e) {
}
try {
return (yield enumerateCameras()).map((camera, i) => ({ id: camera.deviceId, label: camera.label || (i === 0 ? "Default Camera" : `Camera ${i + 1}`) }));
} finally {
if (openedStream) {
console.warn("Call listCameras after successfully starting a QR scanner to avoid creating a temporary video stream");
QrScanner2._stopVideoStream(openedStream);
}
}
});
}
constructor(video, onDecode, options) {
this._legacyCanvasSize = QrScanner2.DEFAULT_CANVAS_SIZE;
this._preferredCamera = "environment";
this._maxScansPerSecond = 25;
this._lastScanTimestamp = -1;
this._active = false;
this._paused = false;
this._flashOn = false;
this._destroyed = false;
this.$video = video;
this.$canvas = document.createElement("canvas");
this._onDecode = onDecode;
options = typeof options === "object" ? options : {};
this._onDecodeError = options.onDecodeError || this._onDecodeError;
this._calculateScanRegion = options.calculateScanRegion || this._calculateScanRegion;
this._preferredCamera = options.preferredCamera || this._preferredCamera;
this._maxScansPerSecond = options.maxScansPerSecond || this._maxScansPerSecond;
this._onPlay = this._onPlay.bind(this);
this._onLoadedMetaData = this._onLoadedMetaData.bind(this);
this._onVisibilityChange = this._onVisibilityChange.bind(this);
this._updateOverlay = this._updateOverlay.bind(this);
video.disablePictureInPicture = true;
video.playsInline = true;
video.muted = true;
let shouldHideVideo = false;
if (video.hidden) {
video.hidden = false;
shouldHideVideo = true;
}
if (!document.body.contains(video)) {
document.body.append(video);
shouldHideVideo = true;
}
let videoContainer = video.parentElement;
if (options.highlightScanRegion || options.highlightCodeOutline) {
let gotExternalOverlay = !!options.overlay;
this.$overlay = options.overlay || document.createElement("div");
let overlayStyle = this.$overlay.style;
overlayStyle.position = "absolute";
overlayStyle.display = "none";
overlayStyle.pointerEvents = "none";
this.$overlay.classList.add("scan-region-highlight");
if (!gotExternalOverlay && options.highlightScanRegion) {
this.$overlay.innerHTML = '<svg class="scan-region-highlight-svg" viewBox="0 0 238 238" preserveAspectRatio="none" style="position:absolute;width:100%;height:100%;left:0;top:0;fill:none;stroke:#e9b213;stroke-width:4;stroke-linecap:round;stroke-linejoin:round"><path d="M31 2H10a8 8 0 0 0-8 8v21M207 2h21a8 8 0 0 1 8 8v21m0 176v21a8 8 0 0 1-8 8h-21m-176 0H10a8 8 0 0 1-8-8v-21"/></svg>';
try {
this.$overlay.firstElementChild.animate({ transform: ["scale(.98)", "scale(1.01)"] }, { duration: 400, iterations: Infinity, direction: "alternate", easing: "ease-in-out" });
} catch (e) {
}
videoContainer.insertBefore(
this.$overlay,
this.$video.nextSibling
);
}
if (options.highlightCodeOutline) {
this.$overlay.insertAdjacentHTML("beforeend", '<svg class="code-outline-highlight" preserveAspectRatio="none" style="display:none;width:100%;height:100%;fill:none;stroke:#e9b213;stroke-width:5;stroke-dasharray:25;stroke-linecap:round;stroke-linejoin:round"><polygon/></svg>');
this.$codeOutlineHighlight = this.$overlay.lastElementChild;
}
}
this._scanRegion = this._calculateScanRegion(video);
requestAnimationFrame(() => {
let videoStyle = globalThis.getComputedStyle(video);
if (videoStyle.display === "none") {
video.style.setProperty("display", "block", "important");
shouldHideVideo = true;
}
if (videoStyle.visibility !== "visible") {
video.style.setProperty("visibility", "visible", "important");
shouldHideVideo = true;
}
if (shouldHideVideo) {
console.warn("QrScanner has overwritten the video hiding style to avoid Safari stopping the playback.");
video.style.opacity = "0";
video.style.width = "0";
video.style.height = "0";
if (this.$overlay && this.$overlay.parentElement) this.$overlay.remove();
delete this.$overlay;
delete this.$codeOutlineHighlight;
}
if (this.$overlay) this._updateOverlay();
});
video.addEventListener("play", this._onPlay);
video.addEventListener("loadedmetadata", this._onLoadedMetaData);
document.addEventListener("visibilitychange", this._onVisibilityChange);
window.addEventListener("resize", this._updateOverlay);
this._qrEnginePromise = QrScanner2.createQrEngine();
}
hasFlash() {
return __awaiter(this, void 0, void 0, function* () {
let stream;
try {
if (this.$video.srcObject) {
if (!(this.$video.srcObject instanceof MediaStream)) return false;
stream = this.$video.srcObject;
} else stream = (yield this._getCameraStream()).stream;
return "torch" in stream.getVideoTracks()[0].getSettings();
} catch (e) {
return false;
} finally {
if (stream && stream !== this.$video.srcObject) {
console.warn("Call hasFlash after successfully starting the scanner to avoid creating a temporary video stream");
QrScanner2._stopVideoStream(stream);
}
}
});
}
isFlashOn() {
return this._flashOn;
}
toggleFlash() {
return __awaiter(this, void 0, void 0, function* () {
yield this._flashOn ? this.turnFlashOff() : this.turnFlashOn();
});
}
turnFlashOn() {
return __awaiter(
this,
void 0,
void 0,
function* () {
if (this._flashOn || this._destroyed) return;
this._flashOn = true;
if (!this._active || this._paused) return;
try {
if (!(yield this.hasFlash())) throw new Error("No flash available");
yield this.$video.srcObject.getVideoTracks()[0].applyConstraints({ advanced: [{ torch: true }] });
} catch (e) {
this._flashOn = false;
throw e;
}
}
);
}
turnFlashOff() {
return __awaiter(this, void 0, void 0, function* () {
if (!this._flashOn) return;
this._flashOn = false;
yield this._restartVideoStream();
});
}
destroy() {
this.$video.removeEventListener(
"loadedmetadata",
this._onLoadedMetaData
);
this.$video.removeEventListener("play", this._onPlay);
document.removeEventListener("visibilitychange", this._onVisibilityChange);
window.removeEventListener("resize", this._updateOverlay);
this._destroyed = true;
this._flashOn = false;
this.stop();
QrScanner2._postWorkerMessage(this._qrEnginePromise, "close");
}
start() {
return __awaiter(this, void 0, void 0, function* () {
if (this._destroyed) throw new Error("The QR scanner can not be started as it had been destroyed.");
if (this._active && !this._paused) return;
if (globalThis.location.protocol !== "https:") console.warn("The camera stream is only accessible if the page is transferred via https.");
this._active = true;
if (document.hidden) return;
this._paused = false;
if (this.$video.srcObject) {
yield this.$video.play();
return;
}
try {
let { stream, facingMode } = yield this._getCameraStream();
if (!this._active || this._paused) {
QrScanner2._stopVideoStream(stream);
return;
}
this._setVideoMirror(facingMode);
this.$video.srcObject = stream;
yield this.$video.play();
if (this._flashOn) {
this._flashOn = false;
this.turnFlashOn().catch(() => {
});
}
} catch (e) {
if (this._paused) return;
this._active = false;
throw e;
}
});
}
stop() {
this.pause();
this._active = false;
}
pause(stopStreamImmediately = false) {
return __awaiter(this, void 0, void 0, function* () {
this._paused = true;
if (!this._active) return true;
this.$video.pause();
if (this.$overlay) this.$overlay.style.display = "none";
let stopStream = () => {
if (this.$video.srcObject instanceof MediaStream) {
QrScanner2._stopVideoStream(this.$video.srcObject);
this.$video.srcObject = null;
}
};
if (stopStreamImmediately) {
stopStream();
return true;
}
yield new Promise((resolve) => setTimeout(resolve, 300));
if (!this._paused) return false;
stopStream();
return true;
});
}
setCamera(facingModeOrDeviceId) {
return __awaiter(this, void 0, void 0, function* () {
if (facingModeOrDeviceId === this._preferredCamera) return;
this._preferredCamera = facingModeOrDeviceId;
yield this._restartVideoStream();
});
}
static scanImage(imageOrFileOrBlobOrUrl, options) {
return __awaiter(this, void 0, void 0, function* () {
let scanRegion = options.scanRegion || void 0;
let qrEngine;
let canvas = options.canvas || document.createElement("canvas");
let disallowCanvasResizing = false;
let alsoTryWithoutScanRegion = false;
options = typeof options === "object" ? options : {};
qrEngine = options.qrEngine || qrEngine;
disallowCanvasResizing = options.disallowCanvasResizing || false;
alsoTryWithoutScanRegion = options.alsoTryWithoutScanRegion || false;
let gotExternalEngine = !!qrEngine;
try {
let image;
[qrEngine, image] = yield Promise.all([qrEngine || QrScanner2.createQrEngine(), QrScanner2._loadImage(imageOrFileOrBlobOrUrl)]);
let canvasContext = QrScanner2._drawToCanvas(
image,
canvas,
scanRegion,
disallowCanvasResizing
);
let detailedScanResult;
if (qrEngine instanceof Worker) {
let qrEngineWorker = qrEngine;
if (!gotExternalEngine) QrScanner2._postWorkerMessageSync(qrEngineWorker, "inversionMode", "both");
detailedScanResult = yield new Promise((resolve, reject) => {
let timeout;
let onError;
let onMessage;
let expectedResponseId = -1;
onMessage = (event) => {
let data = event.data;
if (data["id"] !== expectedResponseId) return;
qrEngineWorker.removeEventListener("message", onMessage);
qrEngineWorker.removeEventListener(
"error",
onError
);
clearTimeout(timeout);
if (data["data"] === null) reject(QrScanner2.NO_QR_CODE_FOUND);
else resolve({ data: data["data"], binaryData: data["binaryData"], cornerPoints: QrScanner2._convertPoints(data["cornerPoints"], scanRegion) });
};
onError = (error) => {
qrEngineWorker.removeEventListener("message", onMessage);
qrEngineWorker.removeEventListener("error", onError);
clearTimeout(timeout);
let errorMessage = error ? error.message || error : "Unknown Error";
reject("Scanner error: " + errorMessage);
};
qrEngineWorker.addEventListener(
"message",
onMessage
);
qrEngineWorker.addEventListener("error", onError);
timeout = setTimeout(() => onError("timeout"), 1e4);
let imageData = canvasContext.getImageData(0, 0, canvas.width, canvas.height);
expectedResponseId = QrScanner2._postWorkerMessageSync(qrEngineWorker, "decode", imageData, [imageData.data.buffer]);
});
} else detailedScanResult = yield Promise.race([new Promise((resolve, reject) => globalThis.setTimeout(() => reject("Scanner error: timeout"), 1e4)), (() => __awaiter(this, void 0, void 0, function* () {
try {
let [scanResult] = yield qrEngine.detect(canvas);
if (!scanResult) throw QrScanner2.NO_QR_CODE_FOUND;
return { data: scanResult.rawValue, cornerPoints: QrScanner2._convertPoints(scanResult.cornerPoints, scanRegion) };
} catch (e) {
let errorMessage = e.message || e;
if (/not implemented|service unavailable/.test(errorMessage)) {
QrScanner2._disableBarcodeDetector = true;
return QrScanner2.scanImage(imageOrFileOrBlobOrUrl, { scanRegion, canvas, disallowCanvasResizing, alsoTryWithoutScanRegion });
}
throw `Scanner error: ${errorMessage}`;
}
}))()]);
return detailedScanResult;
} catch (e) {
if (!scanRegion || !alsoTryWithoutScanRegion) throw e;
let detailedScanResult = yield QrScanner2.scanImage(imageOrFileOrBlobOrUrl, { qrEngine, canvas, disallowCanvasResizing });
return detailedScanResult;
} finally {
if (!gotExternalEngine) QrScanner2._postWorkerMessage(qrEngine, "close");
}
});
}
setGrayscaleWeights(red, green, blue, useIntegerApproximation = true) {
QrScanner2._postWorkerMessage(this._qrEnginePromise, "grayscaleWeights", { red, green, blue, useIntegerApproximation });
}
setInversionMode(inversionMode) {
QrScanner2._postWorkerMessage(
this._qrEnginePromise,
"inversionMode",
inversionMode
);
}
static createQrEngine() {
return __awaiter(this, void 0, void 0, function* () {
let createWorker2 = () => Promise.resolve().then(function() {
return qrScannerWorker_min;
}).then((module2) => module2.createWorker());
let useBarcodeDetector = !QrScanner2._disableBarcodeDetector && "BarcodeDetector" in globalThis && BarcodeDetector.getSupportedFormats && (yield BarcodeDetector.getSupportedFormats()).indexOf("qr_code") !== -1;
if (!useBarcodeDetector) return createWorker2();
let userAgentData = navigator.userAgentData;
let isChromiumOnMacWithArmVentura = userAgentData && userAgentData.brands.some(({ brand, version }) => /Chromium/i.test(brand) && Number.parseInt(version) < 113) && /mac ?OS/i.test(userAgentData.platform) && (yield userAgentData.getHighEntropyValues(["architecture", "platformVersion"]).then(({ architecture, platformVersion }) => /arm/i.test(architecture || "arm") && Number.parseInt(platformVersion || "13") >= 13).catch(() => true));
if (isChromiumOnMacWithArmVentura) return createWorker2();
return new BarcodeDetector({ formats: ["qr_code"] });
});
}
_onPlay() {
this._scanRegion = this._calculateScanRegion(this.$video);
this._updateOverlay();
if (this.$overlay) this.$overlay.style.display = "";
this._scanFrame();
}
_onLoadedMetaData() {
this._scanRegion = this._calculateScanRegion(this.$video);
this._updateOverlay();
}
_onVisibilityChange() {
if (document.hidden) this.pause();
else if (this._active) this.start();
}
_calculateScanRegion(video) {
let smallestDimension = Math.min(video.videoWidth, video.videoHeight);
let scanRegionSize = Math.round(2 / 3 * smallestDimension);
return { x: Math.round((video.videoWidth - scanRegionSize) / 2), y: Math.round((video.videoHeight - scanRegionSize) / 2), width: scanRegionSize, height: scanRegionSize, downScaledWidth: this._legacyCanvasSize, downScaledHeight: this._legacyCanvasSize };
}
_updateOverlay() {
requestAnimationFrame(() => {
if (!this.$overlay) return;
let video = this.$video;
let videoWidth = video.videoWidth;
let videoHeight = video.videoHeight;
let elementWidth = video.offsetWidth;
let elementHeight = video.offsetHeight;
let elementX = video.offsetLeft;
let elementY = video.offsetTop;
let videoStyle = globalThis.getComputedStyle(video);
let videoObjectFit = videoStyle.objectFit;
let videoAspectRatio = videoWidth / videoHeight;
let elementAspectRatio = elementWidth / elementHeight;
let videoScaledWidth;
let videoScaledHeight;
switch (videoObjectFit) {
case "none":
videoScaledWidth = videoWidth;
videoScaledHeight = videoHeight;
break;
case "fill":
videoScaledWidth = elementWidth;
videoScaledHeight = elementHeight;
break;
default:
if (videoObjectFit === "cover" ? videoAspectRatio > elementAspectRatio : videoAspectRatio < elementAspectRatio) {
videoScaledHeight = elementHeight;
videoScaledWidth = videoScaledHeight * videoAspectRatio;
} else {
videoScaledWidth = elementWidth;
videoScaledHeight = videoScaledWidth / videoAspectRatio;
}
if (videoObjectFit === "scale-down") {
videoScaledWidth = Math.min(videoScaledWidth, videoWidth);
videoScaledHeight = Math.min(videoScaledHeight, videoHeight);
}
}
let [videoX, videoY] = videoStyle.objectPosition.split(" ").map((length, i) => {
const lengthValue = Number.parseFloat(length);
return length.endsWith("%") ? (i ? elementHeight - videoScaledHeight : elementWidth - videoScaledWidth) * lengthValue / 100 : lengthValue;
});
let regionWidth = this._scanRegion.width || videoWidth;
let regionHeight = this._scanRegion.height || videoHeight;
let regionX = this._scanRegion.x || 0;
let regionY = this._scanRegion.y || 0;
let overlayStyle = this.$overlay.style;
overlayStyle.width = `${regionWidth / videoWidth * videoScaledWidth}px`;
overlayStyle.height = `${regionHeight / videoHeight * videoScaledHeight}px`;
overlayStyle.top = `${elementY + videoY + regionY / videoHeight * videoScaledHeight}px`;
let isVideoMirrored = /scaleX\(-1\)/.test(video.style.transform);
overlayStyle.left = `${elementX + (isVideoMirrored ? elementWidth - videoX - videoScaledWidth : videoX) + (isVideoMirrored ? videoWidth - regionX - regionWidth : regionX) / videoWidth * videoScaledWidth}px`;
overlayStyle.transform = video.style.transform;
});
}
static _convertPoints(points, scanRegion) {
if (!scanRegion) return points;
let offsetX = scanRegion.x || 0;
let offsetY = scanRegion.y || 0;
let scaleFactorX = scanRegion.width && scanRegion.downScaledWidth ? scanRegion.width / scanRegion.downScaledWidth : 1;
let scaleFactorY = scanRegion.height && scanRegion.downScaledHeight ? scanRegion.height / scanRegion.downScaledHeight : 1;
for (let point of points) {
point.x = point.x * scaleFactorX + offsetX;
point.y = point.y * scaleFactorY + offsetY;
}
return points;
}
_scanFrame() {
if (!this._active || this.$video.paused || this.$video.ended) return;
let requestFrame = "requestVideoFrameCallback" in this.$video ? this.$video.requestVideoFrameCallback.bind(this.$video) : requestAnimationFrame;
requestFrame(() => __awaiter(this, void 0, void 0, function* () {
if (this.$video.readyState <= 1) {
this._scanFrame();
return;
}
let timeSinceLastScan = Date.now() - this._lastScanTimestamp;
let minimumTimeBetweenScans = 1e3 / this._maxScansPerSecond;
if (timeSinceLastScan < minimumTimeBetweenScans) yield new Promise((resolve) => setTimeout(resolve, minimumTimeBetweenScans - timeSinceLastScan));
this._lastScanTimestamp = Date.now();
let result;
try {
result = yield QrScanner2.scanImage(this.$video, { scanRegion: this._scanRegion, qrEngine: this._qrEnginePromise, canvas: this.$canvas });
} catch (error) {
if (!this._active) return;
this._onDecodeError(error);
}
if (QrScanner2._disableBarcodeDetector && !((yield this._qrEnginePromise) instanceof Worker)) this._qrEnginePromise = QrScanner2.createQrEngine();
if (result) {
if (this._onDecode) this._onDecode(result);
if (this.$codeOutlineHighlight) {
clearTimeout(this._codeOutlineHighlightRemovalTimeout);
this._codeOutlineHighlightRemovalTimeout = void 0;
this.$codeOutlineHighlight.setAttribute("viewBox", `${this._scanRegion.x || 0} ${this._scanRegion.y || 0} ${this._scanRegion.width || this.$video.videoWidth} ${this._scanRegion.height || this.$video.videoHeight}`);
let polygon = this.$codeOutlineHighlight.firstElementChild;
console.assert(!!polygon);
polygon.setAttribute("points", result.cornerPoints.map(({ x, y }) => `${x},${y}`).join(" "));
this.$codeOutlineHighlight.style.display = "";
}
} else if (!this._codeOutlineHighlightRemovalTimeout) this._codeOutlineHighlightRemovalTimeout = setTimeout(() => {
if (this.$codeOutlineHighlight) this.$codeOutlineHighlight.style.display = "none";
}, 100);
this._scanFrame();
}));
}
_onDecodeError(error) {
if (error === QrScanner2.NO_QR_CODE_FOUND) return;
console.log(error);
}
_getCameraStream() {
return __awaiter(
this,
void 0,
void 0,
function* () {
if (!navigator.mediaDevices) throw new Error("Camera not found.");
let preferenceType = /^(environment|user)$/.test(this._preferredCamera) ? "facingMode" : "deviceId";
let constraintsWithoutCamera = [{ width: { min: 1024 } }, { width: { min: 768 } }, {}];
let constraintsWithCamera = constraintsWithoutCamera.map((constraint) => Object.assign({}, constraint, { [preferenceType]: { exact: this._preferredCamera } }));
for (let constraints of [...constraintsWithCamera, ...constraintsWithoutCamera]) try {
let stream = yield navigator.mediaDevices.getUserMedia({
video: constraints,
audio: false
});
let facingMode = QrScanner2._getFacingMode(stream) || (constraints.facingMode ? this._preferredCamera : this._preferredCamera === "environment" ? "user" : "environment");
return { stream, facingMode };
} catch (e) {
}
throw new Error("Camera not found.");
}
);
}
_restartVideoStream() {
return __awaiter(this, void 0, void 0, function* () {
let wasPaused = this._paused;
let paused = yield this.pause(true);
if (!paused || wasPaused || !this._active) return;
yield this.start();
});
}
static _stopVideoStream(stream) {
for (let track of stream.getTracks()) {
track.stop();
stream.removeTrack(track);
}
}
_setVideoMirror(facingMode) {
let scaleFactor = facingMode === "user" ? -1 : 1;
this.$video.style.transform = "scaleX(" + scaleFactor + ")";
}
static _getFacingMode(videoStream) {
let videoTrack = videoStream.getVideoTracks()[0];
if (!videoTrack) return null;
return /rear|back|environment/i.test(videoTrack.label) ? "environment" : /front|user|face/i.test(videoTrack.label) ? "user" : null;
}
static _drawToCanvas(image, canvas, scanRegion, disallowCanvasResizing = false) {
let scanRegionX = scanRegion && scanRegion.x ? scanRegion.x : 0;
let scanRegionY = scanRegion && scanRegion.y ? scanRegion.y : 0;
let scanRegionWidth = scanRegion && scanRegion.width ? scanRegion.width : image.videoWidth || image.width;
let scanRegionHeight = scanRegion && scanRegion.height ? scanRegion.height : image.videoHeight || image.height;
if (!disallowCanvasResizing) {
let canvasWidth = scanRegion && scanRegion.downScaledWidth ? scanRegion.downScaledWidth : scanRegionWidth;
let canvasHeight = scanRegion && scanRegion.downScaledHeight ? scanRegion.downScaledHeight : scanRegionHeight;
if (canvas.width !== canvasWidth) canvas.width = canvasWidth;
if (canvas.height !== canvasHeight) canvas.height = canvasHeight;
}
let context = canvas.getContext("2d", { alpha: false, willReadFrequently: true });
if (!context) throw new Error("Could not get canvas rendering context.");
context.imageSmoothingEnabled = false;
context.drawImage(image, scanRegionX, scanRegionY, scanRegionWidth, scanRegionHeight, 0, 0, canvas.width, canvas.height);
return context;
}
static _loadImage(resource) {
return __awaiter(this, void 0, void 0, function* () {
if (resource instanceof Image) {
yield QrScanner2._awaitImageLoad(resource);
return resource;
} else if (resource instanceof HTMLVideoElement || resource instanceof HTMLCanvasElement || resource instanceof SVGImageElement || "OffscreenCanvas" in globalThis && resource instanceof OffscreenCanvas || "ImageBitmap" in globalThis && resource instanceof ImageBitmap) return resource;
else if (resource instanceof File || resource instanceof Blob || resource instanceof URL || typeof resource === "string") {
let image = new Image();
image.src = resource instanceof File || resource instanceof Blob ? URL.createObjectURL(resource) : resource.toString();
try {
yield QrScanner2._awaitImageLoad(image);
return image;
} finally {
if (resource instanceof File || resource instanceof Blob) URL.revokeObjectURL(image.src);
}
} else throw new TypeError("Unsupported image type.");
});
}
static _awaitImageLoad(image) {
return __awaiter(this, void 0, void 0, function* () {
if (image.complete && image.naturalWidth !== 0) return;
yield new Promise((resolve, reject) => {
let listener = (event) => {
image.removeEventListener("load", listener);
image.removeEventListener("error", listener);
if (event instanceof ErrorEvent) reject("Image load error");
else resolve();
};
image.addEventListener("load", listener);
image.addEventListener("error", listener);
});
});
}
static _postWorkerMessage(qrEngineOrQrEnginePromise, type, data, transfer) {
return __awaiter(this, void 0, void 0, function* () {
return QrScanner2._postWorkerMessageSync(yield qrEngineOrQrEnginePromise, type, data, transfer);
});
}
static _postWorkerMessageSync(qrEngine, type, data, transfer) {
if (!(qrEngine instanceof Worker)) return -1;
let id = QrScanner2._workerMessageId++;
qrEngine.postMessage({ id, type, data }, { transfer });
return id;
}
}
QrScanner2.DEFAULT_CANVAS_SIZE = 400;
QrScanner2.NO_QR_CODE_FOUND = "No QR code found";
QrScanner2._disableBarcodeDetector = false;
QrScanner2._workerMessageId = 0;
let createWorker = () => new Worker(URL.createObjectURL(new Blob([`var w=class a{static ja(b,c){return new a(new Uint8ClampedArray(b*c),b)}constructor(b,c){this.width=c;this.height=b.length/c;this.data=b}get(b,c){return 0>b||b>=this.width||0>c||c>=this.height?!1:!!this.data[c*this.width+b]}set(b,c,d){this.data[c*this.width+b]=d?1:0}Z(b,c,d,e){for(let f=c;f<c+e;f++)for(let g=b;g<b+d;g++)this.set(g,f,!0)}},A=class{constructor(a,b,c){this.width=a;a*=b;if(c&&c.length!==a)throw Error("Wrong buffer size");this.data=c||new Uint8ClampedArray(a)}get(a,b){return this.data[b*
this.width+a]}set(a,b,c){this.data[b*this.width+a]=c}};function B(a){return 8*(a.H.length-a.byteOffset)-a.W}function C(a,b){if(1>b||32<b||b>B(a))throw Error("Cannot read "+b.toString()+" bits");var c=0;if(0<a.W){c=8-a.W;var d=b<c?b:c;c-=d;c=(a.H[a.byteOffset]&255>>8-d<<c)>>c;b-=d;a.W+=d;8===a.W&&(a.W=0,a.byteOffset++)}if(0<b){for(;8<=b;)c=c<<8|a.H[a.byteOffset]&255,a.byteOffset++,b-=8;0<b&&(d=8-b,c=c<<b|(a.H[a.byteOffset]&255>>d<<d)>>d,a.W+=b)}return c}
var aa=class{constructor(a){this.W=this.byteOffset=0;this.H=a}},D="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:".split("");function ba(a,b){let c=[],d="";b=C(a,[8,16,16][b]);for(let e=0;e<b;e++){let f=C(a,8);c.push(f)}try{d+=decodeURIComponent(c.map(e=>"%"+("0"+e.toString(16)).substr(-2)).join(""))}catch(e){}return{H:c,text:d}}
function ca(a,b){a=new aa(a);let c=9>=b?0:26>=b?1:2;for(b={text:"",H:[],U:[],version:b};4<=B(a);){var d=C(a,4);if(0===d)return b;if(7===d)0===C(a,1)?b.U.push({type:"eci",ma:C(a,7)}):0===C(a,1)?b.U.push({type:"eci",ma:C(a,14)}):0===C(a,1)?b.U.push({type:"eci",ma:C(a,21)}):b.U.push({type:"eci",ma:-1});else if(1===d){var e=a,f=[];d="";for(var g=C(e,[10,12,14][c]);3<=g;){var h=C(e,10);if(1E3<=h)throw Error("Invalid numeric value above 999");var k=Math.floor(h/100),l=Math.floor(h/10)%10;h%=10;f.push(48+
k,48+l,48+h);d+=k.toString()+l.toString()+h.toString();g-=3}if(2===g){g=C(e,7);if(100<=g)throw Error("Invalid numeric value above 99");e=Math.floor(g/10);g%=10;f.push(48+e,48+g);d+=e.toString()+g.toString()}else if(1===g){e=C(e,4);if(10<=e)throw Error("Invalid numeric value above 9");f.push(48+e);d+=e.toString()}b.text+=d;b.H.push(...f);b.U.push({type:"numeric",text:d})}else if(2===d){e=a;f=[];d="";for(g=C(e,[9,11,13][c]);2<=g;)l=C(e,11),k=Math.floor(l/45),l%=45,f.push(D[k].charCodeAt(0),D[l].charCodeAt(0)),
d+=D[k]+D[l],g-=2;1===g&&(e=C(e,6),f.push(D[e].charCodeAt(0)),d+=D[e]);b.text+=d;b.H.push(...f);b.U.push({type:"alphanumeric",text:d})}else if(4===d)d=ba(a,c),b.text+=d.text,b.H.push(...d.H),b.U.push({type:"byte",H:d.H,text:d.text});else if(8===d){f=a;d=[];e=C(f,[8,10,12][c]);for(g=0;g<e;g++)k=C(f,13),k=Math.floor(k/192)<<8|k%192,k=7936>k?k+33088:k+49472,d.push(k>>8,k&255);f=(new TextDecoder("shift-jis")).decode(Uint8Array.from(d));b.text+=f;b.H.push(...d);b.U.push({type:"kanji",H:d,text:f})}else 3===
d&&b.U.push({type:"structuredappend",Ja:C(a,4),Na:C(a,4),Ka:C(a,8)})}if(0===B(a)||0===C(a,B(a)))return b}
var F=class a{constructor(b,c){if(0===c.length)throw Error("No coefficients.");this.V=b;let d=c.length;if(1<d&&0===c[0]){let e=1;for(;e<d&&0===c[e];)e++;if(e===d)this.C=b.$.C;else for(this.C=new Uint8ClampedArray(d-e),b=0;b<this.C.length;b++)this.C[b]=c[e+b]}else this.C=c}T(){return this.C.length-1}ba(){return 0===this.C[0]}fa(b){return this.C[this.C.length-1-b]}la(b){if(this.ba())return b;if(b.ba())return this;let c=this.C;b=b.C;c.length>b.length&&([c,b]=[b,c]);let d=new Uint8ClampedArray(b.length),
e=b.length-c.length;for(var f=0;f<e;f++)d[f]=b[f];for(f=e;f<b.length;f++)d[f]=c[f-e]^b[f];return new a(this.V,d)}multiply(b){if(0===b)return this.V.$;if(1===b)return this;let c=this.C.length,d=new Uint8ClampedArray(c);for(let e=0;e<c;e++)d[e]=this.V.multiply(this.C[e],b);return new a(this.V,d)}Ba(b){if(this.ba()||b.ba())return this.V.$;let c=this.C,d=c.length;b=b.C;let e=b.length,f=new Uint8ClampedArray(d+e-1);for(let g=0;g<d;g++){let h=c[g];for(let k=0;k<e;k++)f[g+k]=E(f[g+k],this.V.multiply(h,b[k]))}return new a(this.V,
f)}Aa(b,c){if(0>b)throw Error("Invalid degree less than 0");if(0===c)return this.V.$;let d=this.C.length;b=new Uint8ClampedArray(d+b);for(let e=0;e<d;e++)b[e]=this.V.multiply(this.C[e],c);return new a(this.V,b)}na(b){let c=0;if(0===b)return this.fa(0);let d=this.C.length;if(1===b)return this.C.forEach(e=>{c^=e}),c;c=this.C[0];for(let e=1;e<d;e++)c=E(this.V.multiply(b,c),this.C[e]);return c}};function E(a,b){return a^b}
function G(a,b,c){if(0>b)throw Error("Invalid monomial degree less than 0");if(0===c)return a.$;b=new Uint8ClampedArray(b+1);b[0]=c;return new F(a,b)}
var da=class{constructor(){this.Ea=285;this.size=256;this.sa=0;this.ea=Array(this.size);this.ga=Array(this.size);var a=1;for(let b=0;b<this.size;b++)this.ea[b]=a,a*=2,a>=this.size&&(a=(a^this.Ea)&this.size-1);for(a=0;a<this.size-1;a++)this.ga[this.ea[a]]=a;this.$=new F(this,Uint8ClampedArray.from([0]));this.Ca=new F(this,Uint8ClampedArray.from([1]))}multiply(a,b){return 0===a||0===b?0:this.ea[(this.ga[a]+this.ga[b])%(this.size-1)]}inverse(a){if(0===a)throw Error("Can't invert 0");return this.ea[this.size-
this.ga[a]-1]}log(a){if(0===a)throw Error("Can't take log(0)");return this.ga[a]}exp(a){return this.ea[a]}};
function ea(a,b,c,d){b.T()<c.T()&&([b,c]=[c,b]);let e=a.$;for(var f=a.Ca;c.T()>=d/2;){var g=b;let h=e;b=c;e=f;if(b.ba())return null;c=g;f=a.$;for(g=a.inverse(b.fa(b.T()));c.T()>=b.T()&&!c.ba();){let k=c.T()-b.T(),l=a.multiply(c.fa(c.T()),g);f=f.la(G(a,k,l));c=c.la(b.Aa(k,l))}f=f.Ba(e).la(h);if(c.T()>=b.T())return null}d=f.fa(0);if(0===d)return null;a=a.inverse(d);return[f.multiply(a),c.multiply(a)]}
function fa(a,b){let c=new Uint8ClampedArray(a.length);c.set(a);a=new da;var d=new F(a,c),e=new Uint8ClampedArray(b),f=!1;for(var g=0;g<b;g++){var h=d.na(a.exp(g+a.sa));e[e.length-1-g]=h;0!==h&&(f=!0)}if(!f)return c;d=new F(a,e);d=ea(a,G(a,b,1),d,b);if(null===d)return null;b=d[0];g=b.T();if(1===g)b=[b.fa(1)];else{e=Array(g);f=0;for(h=1;h<a.size&&f<g;h++)0===b.na(h)&&(e[f]=a.inverse(h),f++);b=f!==g?null:e}if(null==b)return null;e=d[1];f=b.length;d=Array(f);for(g=0;g<f;g++){h=a.inverse(b[g]);let k=
1;for(let l=0;l<f;l++)g!==l&&(k=a.multiply(k,E(1,a.multiply(b[l],h))));d[g]=a.multiply(e.na(h),a.inverse(k));0!==a.sa&&(d[g]=a.multiply(d[g],h))}for(e=0;e<b.length;e++){f=c.length-1-a.log(b[e]);if(0>f)return null;c[f]^=d[e]}return c}
var H=[{l:null,m:1,o:[],s:[{j:7,i:[{h:1,g:19}]},{j:10,i:[{h:1,g:16}]},{j:13,i:[{h:1,g:13}]},{j:17,i:[{h:1,g:9}]}]},{l:null,m:2,o:[6,18],s:[{j:10,i:[{h:1,g:34}]},{j:16,i:[{h:1,g:28}]},{j:22,i:[{h:1,g:22}]},{j:28,i:[{h:1,g:16}]}]},{l:null,m:3,o:[6,22],s:[{j:15,i:[{h:1,g:55}]},{j:26,i:[{h:1,g:44}]},{j:18,i:[{h:2,g:17}]},{j:22,i:[{h:2,g:13}]}]},{l:null,m:4,o:[6,26],s:[{j:20,i:[{h:1,g:80}]},{j:18,i:[{h:2,g:32}]},{j:26,i:[{h:2,g:24}]},{j:16,i:[{h:4,g:9}]}]},{l:null,m:5,o:[6,30],s:[{j:26,i:[{h:1,g:108}]},
{j:24,i:[{h:2,g:43}]},{j:18,i:[{h:2,g:15},{h:2,g:16}]},{j:22,i:[{h:2,g:11},{h:2,g:12}]}]},{l:null,m:6,o:[6,34],s:[{j:18,i:[{h:2,g:68}]},{j:16,i:[{h:4,g:27}]},{j:24,i:[{h:4,g:19}]},{j:28,i:[{h:4,g:15}]}]},{l:31892,m:7,o:[6,22,38],s:[{j:20,i:[{h:2,g:78}]},{j:18,i:[{h:4,g:31}]},{j:18,i:[{h:2,g:14},{h:4,g:15}]},{j:26,i:[{h:4,g:13},{h:1,g:14}]}]},{l:34236,m:8,o:[6,24,42],s:[{j:24,i:[{h:2,g:97}]},{j:22,i:[{h:2,g:38},{h:2,g:39}]},{j:22,i:[{h:4,g:18},{h:2,g:19}]},{j:26,i:[{h:4,g:14},{h:2,g:15}]}]},{l:39577,
m:9,o:[6,26,46],s:[{j:30,i:[{h:2,g:116}]},{j:22,i:[{h:3,g:36},{h:2,g:37}]},{j:20,i:[{h:4,g:16},{h:4,g:17}]},{j:24,i:[{h:4,g:12},{h:4,g:13}]}]},{l:42195,m:10,o:[6,28,50],s:[{j:18,i:[{h:2,g:68},{h:2,g:69}]},{j:26,i:[{h:4,g:43},{h:1,g:44}]},{j:24,i:[{h:6,g:19},{h:2,g:20}]},{j:28,i:[{h:6,g:15},{h:2,g:16}]}]},{l:48118,m:11,o:[6,30,54],s:[{j:20,i:[{h:4,g:81}]},{j:30,i:[{h:1,g:50},{h:4,g:51}]},{j:28,i:[{h:4,g:22},{h:4,g:23}]},{j:24,i:[{h:3,g:12},{h:8,g:13}]}]},{l:51042,m:12,o:[6,32,58],s:[{j:24,i:[{h:2,
g:92},{h:2,g:93}]},{j:22,i:[{h:6,g:36},{h:2,g:37}]},{j:26,i:[{h:4,g:20},{h:6,g:21}]},{j:28,i:[{h:7,g:14},{h:4,g:15}]}]},{l:55367,m:13,o:[6,34,62],s:[{j:26,i:[{h:4,g:107}]},{j:22,i:[{h:8,g:37},{h:1,g:38}]},{j:24,i:[{h:8,g:20},{h:4,g:21}]},{j:22,i:[{h:12,g:11},{h:4,g:12}]}]},{l:58893,m:14,o:[6,26,46,66],s:[{j:30,i:[{h:3,g:115},{h:1,g:116}]},{j:24,i:[{h:4,g:40},{h:5,g:41}]},{j:20,i:[{h:11,g:16},{h:5,g:17}]},{j:24,i:[{h:11,g:12},{h:5,g:13}]}]},{l:63784,m:15,o:[6,26,48,70],s:[{j:22,i:[{h:5,g:87},{h:1,
g:88}]},{j:24,i:[{h:5,g:41},{h:5,g:42}]},{j:30,i:[{h:5,g:24},{h:7,g:25}]},{j:24,i:[{h:11,g:12},{h:7,g:13}]}]},{l:68472,m:16,o:[6,26,50,74],s:[{j:24,i:[{h:5,g:98},{h:1,g:99}]},{j:28,i:[{h:7,g:45},{h:3,g:46}]},{j:24,i:[{h:15,g:19},{h:2,g:20}]},{j:30,i:[{h:3,g:15},{h:13,g:16}]}]},{l:70749,m:17,o:[6,30,54,78],s:[{j:28,i:[{h:1,g:107},{h:5,g:108}]},{j:28,i:[{h:10,g:46},{h:1,g:47}]},{j:28,i:[{h:1,g:22},{h:15,g:23}]},{j:28,i:[{h:2,g:14},{h:17,g:15}]}]},{l:76311,m:18,o:[6,30,56,82],s:[{j:30,i:[{h:5,g:120},
{h:1,g:121}]},{j:26,i:[{h:9,g:43},{h:4,g:44}]},{j:28,i:[{h:17,g:22},{h:1,g:23}]},{j:28,i:[{h:2,g:14},{h:19,g:15}]}]},{l:79154,m:19,o:[6,30,58,86],s:[{j:28,i:[{h:3,g:113},{h:4,g:114}]},{j:26,i:[{h:3,g:44},{h:11,g:45}]},{j:26,i:[{h:17,g:21},{h:4,g:22}]},{j:26,i:[{h:9,g:13},{h:16,g:14}]}]},{l:84390,m:20,o:[6,34,62,90],s:[{j:28,i:[{h:3,g:107},{h:5,g:108}]},{j:26,i:[{h:3,g:41},{h:13,g:42}]},{j:30,i:[{h:15,g:24},{h:5,g:25}]},{j:28,i:[{h:15,g:15},{h:10,g:16}]}]},{l:87683,m:21,o:[6,28,50,72,94],s:[{j:28,
i:[{h:4,g:116},{h:4,g:117}]},{j:26,i:[{h:17,g:42}]},{j:28,i:[{h:17,g:22},{h:6,g:23}]},{j:30,i:[{h:19,g:16},{h:6,g:17}]}]},{l:92361,m:22,o:[6,26,50,74,98],s:[{j:28,i:[{h:2,g:111},{h:7,g:112}]},{j:28,i:[{h:17,g:46}]},{j:30,i:[{h:7,g:24},{h:16,g:25}]},{j:24,i:[{h:34,g:13}]}]},{l:96236,m:23,o:[6,30,54,74,102],s:[{j:30,i:[{h:4,g:121},{h:5,g:122}]},{j:28,i:[{h:4,g:47},{h:14,g:48}]},{j:30,i:[{h:11,g:24},{h:14,g:25}]},{j:30,i:[{h:16,g:15},{h:14,g:16}]}]},{l:102084,m:24,o:[6,28,54,80,106],s:[{j:30,i:[{h:6,
g:117},{h:4,g:118}]},{j:28,i:[{h:6,g:45},{h:14,g:46}]},{j:30,i:[{h:11,g:24},{h:16,g:25}]},{j:30,i:[{h:30,g:16},{h:2,g:17}]}]},{l:102881,m:25,o:[6,32,58,84,110],s:[{j:26,i:[{h:8,g:106},{h:4,g:107}]},{j:28,i:[{h:8,g:47},{h:13,g:48}]},{j:30,i:[{h:7,g:24},{h:22,g:25}]},{j:30,i:[{h:22,g:15},{h:13,g:16}]}]},{l:110507,m:26,o:[6,30,58,86,114],s:[{j:28,i:[{h:10,g:114},{h:2,g:115}]},{j:28,i:[{h:19,g:46},{h:4,g:47}]},{j:28,i:[{h:28,g:22},{h:6,g:23}]},{j:30,i:[{h:33,g:16},{h:4,g:17}]}]},{l:110734,m:27,o:[6,34,
62,90,118],s:[{j:30,i:[{h:8,g:122},{h:4,g:123}]},{j:28,i:[{h:22,g:45},{h:3,g:46}]},{j:30,i:[{h:8,g:23},{h:26,g:24}]},{j:30,i:[{h:12,g:15},{h:28,g:16}]}]},{l:117786,m:28,o:[6,26,50,74,98,122],s:[{j:30,i:[{h:3,g:117},{h:10,g:118}]},{j:28,i:[{h:3,g:45},{h:23,g:46}]},{j:30,i:[{h:4,g:24},{h:31,g:25}]},{j:30,i:[{h:11,g:15},{h:31,g:16}]}]},{l:119615,m:29,o:[6,30,54,78,102,126],s:[{j:30,i:[{h:7,g:116},{h:7,g:117}]},{j:28,i:[{h:21,g:45},{h:7,g:46}]},{j:30,i:[{h:1,g:23},{h:37,g:24}]},{j:30,i:[{h:19,g:15},{h:26,
g:16}]}]},{l:126325,m:30,o:[6,26,52,78,104,130],s:[{j:30,i:[{h:5,g:115},{h:10,g:116}]},{j:28,i:[{h:19,g:47},{h:10,g:48}]},{j:30,i:[{h:15,g:24},{h:25,g:25}]},{j:30,i:[{h:23,g:15},{h:25,g:16}]}]},{l:127568,m:31,o:[6,30,56,82,108,134],s:[{j:30,i:[{h:13,g:115},{h:3,g:116}]},{j:28,i:[{h:2,g:46},{h:29,g:47}]},{j:30,i:[{h:42,g:24},{h:1,g:25}]},{j:30,i:[{h:23,g:15},{h:28,g:16}]}]},{l:133589,m:32,o:[6,34,60,86,112,138],s:[{j:30,i:[{h:17,g:115}]},{j:28,i:[{h:10,g:46},{h:23,g:47}]},{j:30,i:[{h:10,g:24},{h:35,
g:25}]},{j:30,i:[{h:19,g:15},{h:35,g:16}]}]},{l:136944,m:33,o:[6,30,58,86,114,142],s:[{j:30,i:[{h:17,g:115},{h:1,g:116}]},{j:28,i:[{h:14,g:46},{h:21,g:47}]},{j:30,i:[{h:29,g:24},{h:19,g:25}]},{j:30,i:[{h:11,g:15},{h:46,g:16}]}]},{l:141498,m:34,o:[6,34,62,90,118,146],s:[{j:30,i:[{h:13,g:115},{h:6,g:116}]},{j:28,i:[{h:14,g:46},{h:23,g:47}]},{j:30,i:[{h:44,g:24},{h:7,g:25}]},{j:30,i:[{h:59,g:16},{h:1,g:17}]}]},{l:145311,m:35,o:[6,30,54,78,102,126,150],s:[{j:30,i:[{h:12,g:121},{h:7,g:122}]},{j:28,i:[{h:12,
g:47},{h:26,g:48}]},{j:30,i:[{h:39,g:24},{h:14,g:25}]},{j:30,i:[{h:22,g:15},{h:41,g:16}]}]},{l:150283,m:36,o:[6,24,50,76,102,128,154],s:[{j:30,i:[{h:6,g:121},{h:14,g:122}]},{j:28,i:[{h:6,g:47},{h:34,g:48}]},{j:30,i:[{h:46,g:24},{h:10,g:25}]},{j:30,i:[{h:2,g:15},{h:64,g:16}]}]},{l:152622,m:37,o:[6,28,54,80,106,132,158],s:[{j:30,i:[{h:17,g:122},{h:4,g:123}]},{j:28,i:[{h:29,g:46},{h:14,g:47}]},{j:30,i:[{h:49,g:24},{h:10,g:25}]},{j:30,i:[{h:24,g:15},{h:46,g:16}]}]},{l:158308,m:38,o:[6,32,58,84,110,136,
162],s:[{j:30,i:[{h:4,g:122},{h:18,g:123}]},{j:28,i:[{h:13,g:46},{h:32,g:47}]},{j:30,i:[{h:48,g:24},{h:14,g:25}]},{j:30,i:[{h:42,g:15},{h:32,g:16}]}]},{l:161089,m:39,o:[6,26,54,82,110,138,166],s:[{j:30,i:[{h:20,g:117},{h:4,g:118}]},{j:28,i:[{h:40,g:47},{h:7,g:48}]},{j:30,i:[{h:43,g:24},{h:22,g:25}]},{j:30,i:[{h:10,g:15},{h:67,g:16}]}]},{l:167017,m:40,o:[6,30,58,86,114,142,170],s:[{j:30,i:[{h:19,g:118},{h:6,g:119}]},{j:28,i:[{h:18,g:47},{h:31,g:48}]},{j:30,i:[{h:34,g:24},{h:34,g:25}]},{j:30,i:[{h:20,
g:15},{h:61,g:16}]}]}];function I(a,b){a^=b;for(b=0;a;)b++,a&=a-1;return b}function J(a,b){return b<<1|a}
var ha=[{u:21522,B:{A:1,v:0}},{u:20773,B:{A:1,v:1}},{u:24188,B:{A:1,v:2}},{u:23371,B:{A:1,v:3}},{u:17913,B:{A:1,v:4}},{u:16590,B:{A:1,v:5}},{u:20375,B:{A:1,v:6}},{u:19104,B:{A:1,v:7}},{u:30660,B:{A:0,v:0}},{u:29427,B:{A:0,v:1}},{u:32170,B:{A:0,v:2}},{u:30877,B:{A:0,v:3}},{u:26159,B:{A:0,v:4}},{u:25368,B:{A:0,v:5}},{u:27713,B:{A:0,v:6}},{u:26998,B:{A:0,v:7}},{u:5769,B:{A:3,v:0}},{u:5054,B:{A:3,v:1}},{u:7399,B:{A:3,v:2}},{u:6608,B:{A:3,v:3}},{u:1890,B:{A:3,v:4}},{u:597,B:{A:3,v:5}},{u:3340,B:{A:3,v:6}},
{u:2107,B:{A:3,v:7}},{u:13663,B:{A:2,v:0}},{u:12392,B:{A:2,v:1}},{u:16177,B:{A:2,v:2}},{u:14854,B:{A:2,v:3}},{u:9396,B:{A:2,v:4}},{u:8579,B:{A:2,v:5}},{u:11994,B:{A:2,v:6}},{u:11245,B:{A:2,v:7}}],ia=[a=>0===(a.y+a.x)%2,a=>0===a.y%2,a=>0===a.x%3,a=>0===(a.y+a.x)%3,a=>0===(Math.floor(a.y/2)+Math.floor(a.x/3))%2,a=>0===a.x*a.y%2+a.x*a.y%3,a=>0===(a.y*a.x%2+a.y*a.x%3)%2,a=>0===((a.y+a.x)%2+a.y*a.x%3)%2];
function ja(a,b,c){c=ia[c.v];let d=a.height;var e=17+4*b.m;let f=w.ja(e,e);f.Z(0,0,9,9);f.Z(e-8,0,8,9);f.Z(0,e-8,9,8);for(var g of b.o)for(var h of b.o)6===g&&6===h||6===g&&h===e-7||g===e-7&&6===h||f.Z(g-2,h-2,5,5);f.Z(6,9,1,e-17);f.Z(9,6,e-17,1);6<b.m&&(f.Z(e-11,0,3,6),f.Z(0,e-11,6,3));b=[];h=g=0;e=!0;for(let k=d-1;0<k;k-=2){6===k&&k--;for(let l=0;l<d;l++){let m=e?d-1-l:l;for(let n=0;2>n;n++){let q=k-n;if(!f.get(q,m)){h++;let t=a.get(q,m);c({y:m,x:q})&&(t=!t);g=g<<1|t;8===h&&(b.push(g),g=h=0)}}}e=
!e}return b}function ka(a){var b=a.height,c=Math.floor((b-17)/4);if(6>=c)return H[c-1];c=0;for(var d=5;0<=d;d--)for(var e=b-9;e>=b-11;e--)c=J(a.get(e,d),c);d=0;for(e=5;0<=e;e--)for(let g=b-9;g>=b-11;g--)d=J(a.get(e,g),d);a=Infinity;let f;for(let g of H){if(g.l===c||g.l===d)return g;b=I(c,g.l);b<a&&(f=g,a=b);b=I(d,g.l);b<a&&(f=g,a=b)}if(3>=a)return f}
function la(a){let b=0;for(var c=0;8>=c;c++)6!==c&&(b=J(a.get(c,8),b));for(c=7;0<=c;c--)6!==c&&(b=J(a.get(8,c),b));var d=a.height;c=0;for(var e=d-1;e>=d-7;e--)c=J(a.get(8,e),c);for(e=d-8;e<d;e++)c=J(a.get(e,8),c);a=Infinity;d=null;for(let {u:f,B:g}of ha){if(f===b||f===c)return g;e=I(b,f);e<a&&(d=g,a=e);b!==c&&(e=I(c,f),e<a&&(d=g,a=e))}return 3>=a?d:null}
function ma(a,b,c){let d=b.s[c],e=[],f=0;d.i.forEach(h=>{for(let k=0;k<h.h;k++)e.push({pa:h.g,da:[]}),f+=h.g+d.j});if(a.length<f)return null;a=a.slice(0,f);b=d.i[0].g;for(c=0;c<b;c++)for(var g of e)g.da.push(a.shift());if(1<d.i.length)for(g=d.i[0].h,b=d.i[1].h,c=0;c<b;c++)e[g+c].da.push(a.shift());for(;0<a.length;)for(let h of e)h.da.push(a.shift());return e}
function K(a){let b=ka(a);if(!b)return null;var c=la(a);if(!c)return null;a=ja(a,b,c);var d=ma(a,b,c.A);if(!d)return null;c=d.reduce((e,f)=>e+f.pa,0);c=new Uint8ClampedArray(c);a=0;for(let e of d){d=fa(e.da,e.da.length-e.pa);if(!d)return null;for(let f=0;f<e.pa;f++)c[a++]=d[f]}try{return ca(c,b.m)}catch(e){return null}}
function L(a,b,c,d){var e=a.x-b.x+c.x-d.x;let f=a.y-b.y+c.y-d.y;if(0===e&&0===f)return{J:b.x-a.x,K:b.y-a.y,L:0,M:c.x-b.x,N:c.y-b.y,O:0,P:a.x,R:a.y,S:1};let g=b.x-c.x;var h=d.x-c.x;let k=b.y-c.y,l=d.y-c.y;c=g*l-h*k;h=(e*l-h*f)/c;e=(g*f-e*k)/c;return{J:b.x-a.x+h*b.x,K:b.y-a.y+h*b.y,L:h,M:d.x-a.x+e*d.x,N:d.y-a.y+e*d.y,O:e,P:a.x,R:a.y,S:1}}
function na(a,b,c){a=L({x:3.5,y:3.5},a,b,c);return{J:a.N*a.S-a.O*a.R,K:a.L*a.R-a.K*a.S,L:a.K*a.O-a.L*a.N,M:a.O*a.P-a.M*a.S,N:a.J*a.S-a.L*a.P,O:a.L*a.M-a.J*a.O,P:a.M*a.R-a.N*a.P,R:a.K*a.P-a.J*a.R,S:a.J*a.N-a.K*a.M}}function oa(a,b){return{J:a.J*b.J+a.M*b.K+a.P*b.L,K:a.K*b.J+a.N*b.K+a.R*b.L,L:a.L*b.J+a.O*b.K+a.S*b.L,M:a.J*b.M+a.M*b.N+a.P*b.O,N:a.K*b.M+a.N*b.N+a.R*b.O,O:a.L*b.M+a.O*b.N+a.S*b.O,P:a.J*b.P+a.M*b.R+a.P*b.S,R:a.K*b.P+a.N*b.R+a.R*b.S,S:a.L*b.P+a.O*b.R+a.S*b.S}}
function pa(a,b){let c=oa(L(b.ha,b.ia,b.X,b.ca),na({x:b.D-3.5,y:3.5},{x:b.D-6.5,y:b.D-6.5},{x:3.5,y:b.D-3.5})),d=w.ja(b.D,b.D),e=(f,g)=>{const h=c.L*f+c.O*g+c.S;return{x:(c.J*f+c.M*g+c.P)/h,y:(c.K*f+c.N*g+c.R)/h}};for(let f=0;f<b.D;f++)for(let g=0;g<b.D;g++){let h=e(g+.5,f+.5);d.set(g,f,a.get(Math.floor(h.x),Math.floor(h.y)))}return{oa:d,ka:e}}var M=(a,b)=>Math.sqrt(Math.pow(b.x-a.x,2)+Math.pow(b.y-a.y,2));function N(a){return a.reduce((b,c)=>b+c)}
function qa(a,b,c){let d=M(a,b),e=M(b,c),f=M(a,c),g,h,k;e>=d&&e>=f?[g,h,k]=[b,a,c]:f>=e&&f>=d?[g,h,k]=[a,b,c]:[g,h,k]=[a,c,b];0>(k.x-h.x)*(g.y-h.y)-(k.y-h.y)*(g.x-h.x)&&([g,k]=[k,g]);return{ca:g,ha:h,ia:k}}function ra(a,b,c,d){d=(N(O(a,c,d,5))/7+N(O(a,b,d,5))/7+N(O(c,a,d,5))/7+N(O(b,a,d,5))/7)/4;if(1>d)throw Error("Invalid module size");a=Math.floor((Math.round(M(a,b)/d)+Math.round(M(a,c)/d))/2)+7;switch(a%4){case 0:a++;break;case 2:a--}return{D:a,za:d}}
function P(a,b,c,d){let e=[{x:Math.floor(a.x),y:Math.floor(a.y)}];var f=Math.abs(b.y-a.y)>Math.abs(b.x-a.x);if(f){var g=Math.floor(a.y);var h=Math.floor(a.x);a=Math.floor(b.y);b=Math.floor(b.x)}else g=Math.floor(a.x),h=Math.floor(a.y),a=Math.floor(b.x),b=Math.floor(b.y);let k=Math.abs(a-g),l=Math.abs(b-h),m=Math.floor(-k/2),n=g<a?1:-1,q=h<b?1:-1,t=!0;for(let x=g,r=h;x!==a+n;x+=n){g=f?r:x;h=f?x:r;if(c.get(g,h)!==t&&(t=!t,e.push({x:g,y:h}),e.length===d+1))break;m+=l;if(0<m){if(r===b)break;r+=q;m-=k}}c=
[];for(f=0;f<d;f++)e[f]&&e[f+1]?c.push(M(e[f],e[f+1])):c.push(0);return c}function O(a,b,c,d){let e=b.y-a.y,f=b.x-a.x;b=P(a,b,c,Math.ceil(d/2));a=P(a,{x:a.x-f,y:a.y-e},c,Math.ceil(d/2));c=b.shift()+a.shift()-1;return a.concat(c).concat(...b)}function Q(a,b){let c=N(a)/N(b),d=0;b.forEach((e,f)=>{d+=Math.pow(a[f]-e*c,2)});return{Y:c,error:d}}
function R(a,b,c){try{let d=O(a,{x:-1,y:a.y},c,b.length),e=O(a,{x:a.x,y:-1},c,b.length),f=O(a,{x:Math.max(0,a.x-a.y)-1,y:Math.max(0,a.y-a.x)-1},c,b.length),g=O(a,{x:Math.min(c.width,a.x+a.y)+1,y:Math.min(c.height,a.y+a.x)+1},c,b.length),h=Q(d,b),k=Q(e,b),l=Q(f,b),m=Q(g,b),n=(h.Y+k.Y+l.Y+m.Y)/4;return Math.sqrt(h.error*h.error+k.error*k.error+l.error*l.error+m.error*m.error)+Math.pow(Math.pow(h.Y-n,2)+Math.pow(k.Y-n,2)+Math.pow(l.Y-n,2)+(m.Y-n),2)/n}catch(d){return Infinity}}
function S(a,b){for(var c=Math.round(b.x);a.get(c,Math.round(b.y));)c--;for(var d=Math.round(b.x);a.get(d,Math.round(b.y));)d++;c=(c+d)/2;for(d=Math.round(b.y);a.get(Math.round(c),d);)d--;for(b=Math.round(b.y);a.get(Math.round(c),b);)b++;return{x:c,y:(d+b)/2}}
function sa(a){var b=[],c=[];let d=[];var e=[];for(let r=0;r<=a.height;r++){var f=0,g=!1;let p=[0,0,0,0,0];for(let u=-1;u<=a.width;u++){var h=a.get(u,r);if(h===g)f++;else{p=[p[1],p[2],p[3],p[4],f];f=1;g=h;var k=N(p)/7;k=Math.abs(p[0]-k)<k&&Math.abs(p[1]-k)<k&&Math.abs(p[2]-3*k)<3*k&&Math.abs(p[3]-k)<k&&Math.abs(p[4]-k)<k&&!h;var l=N(p.slice(-3))/3;h=Math.abs(p[2]-l)<l&&Math.abs(p[3]-l)<l&&Math.abs(p[4]-l)<l&&h;if(k){let z=u-p[3]-p[4],y=z-p[2];k={G:y,F:z,y:r};l=c.filter(v=>y>=v.bottom.G&&y<=v.bottom.F||
z>=v.bottom.G&&y<=v.bottom.F||y<=v.bottom.G&&z>=v.bottom.F&&1.5>p[2]/(v.bottom.F-v.bottom.G)&&.5<p[2]/(v.bottom.F-v.bottom.G));0<l.length?l[0].bottom=k:c.push({top:k,bottom:k})}if(h){let z=u-p[4],y=z-p[3];h={G:y,y:r,F:z};k=e.filter(v=>y>=v.bottom.G&&y<=v.bottom.F||z>=v.bottom.G&&y<=v.bottom.F||y<=v.bottom.G&&z>=v.bottom.F&&1.5>p[2]/(v.bottom.F-v.bottom.G)&&.5<p[2]/(v.bottom.F-v.bottom.G));0<k.length?k[0].bottom=h:e.push({top:h,bottom:h})}}}b.push(...c.filter(u=>u.bottom.y!==r&&2<=u.bottom.y-u.top.y));
c=c.filter(u=>u.bottom.y===r);d.push(...e.filter(u=>u.bottom.y!==r));e=e.filter(u=>u.bottom.y===r)}b.push(...c.filter(r=>2<=r.bottom.y-r.top.y));d.push(...e);c=[];for(var m of b)2>m.bottom.y-m.top.y||(b=(m.top.G+m.top.F+m.bottom.G+m.bottom.F)/4,e=(m.top.y+m.bottom.y+1)/2,a.get(Math.round(b),Math.round(e))&&(f=[m.top.F-m.top.G,m.bottom.F-m.bottom.G,m.bottom.y-m.top.y+1],f=N(f)/f.length,g=R({x:Math.round(b),y:Math.round(e)},[1,1,3,1,1],a),c.push({I:g,x:b,y:e,size:f})));if(3>c.length)return null;c.sort((r,
p)=>r.I-p.I);m=[];for(b=0;b<Math.min(c.length,5);++b){e=c[b];f=[];for(var n of c)n!==e&&f.push({x:n.x,y:n.y,size:n.size,I:n.I+Math.pow(n.size-e.size,2)/e.size});f.sort((r,p)=>r.I-p.I);m.push({Da:[e,f[0],f[1]],I:e.I+f[0].I+f[1].I})}m.sort((r,p)=>r.I-p.I);let {ia:q,ha:t,ca:x}=qa(...m[0].Da);m=T(a,d,q,t,x);n=[];m&&n.push({X:{x:m.X.x,y:m.X.y},ca:{x:x.x,y:x.y},D:m.D,ha:{x:t.x,y:t.y},ia:{x:q.x,y:q.y}});m=S(a,q);b=S(a,t);c=S(a,x);(a=T(a,d,m,b,c))&&n.push({X:{x:a.X.x,y:a.X.y},ca:{x:c.x,y:c.y},ha:{x:b.x,y:b.y},
ia:{x:m.x,y:m.y},D:a.D});return 0===n.length?null:n}
function T(a,b,c,d,e){let f;try{({D:f,za:g}=ra(d,c,e,a))}catch(l){return null}var g=(M(d,e)+M(d,c))/2/g;let h=1-3/g,k={x:d.x+h*(c.x-d.x+e.x-d.x),y:d.y+h*(c.y-d.y+e.y-d.y)};b=b.map(l=>{const m=(l.top.G+l.top.F+l.bottom.G+l.bottom.F)/4;l=(l.top.y+l.bottom.y+1)/2;if(a.get(Math.floor(m),Math.floor(l))){var n=R({x:Math.floor(m),y:Math.floor(l)},[1,1,1],a)+M({x:m,y:l},k);return{x:m,y:l,I:n}}}).filter(l=>!!l).sort((l,m)=>l.I-m.I);return{X:15<=g&&b.length?b[0]:k,D:f}}
function U(a){var b=sa(a);if(!b)return null;for(let e of b){b=pa(a,e);var c=b.oa;if(null==c)c=null;else{var d=K(c);if(d)c=d;else{for(d=0;d<c.width;d++)for(let f=d+1;f<c.height;f++)c.get(d,f)!==c.get(f,d)&&(c.set(d,f,!c.get(d,f)),c.set(f,d,!c.get(f,d)));c=K(c)}}if(c)return{ua:c.H,data:c.text,U:c.U,version:c.version,location:{Ga:b.ka(e.D,0),Fa:b.ka(0,0),wa:b.ka(e.D,e.D),va:b.ka(0,e.D),Ma:e.ia,La:e.ha,Ha:e.ca,Ia:e.X},oa:b.oa}}return null}
var ta={aa:"attemptBoth",ta:{red:.2126,green:.7152,blue:.0722,qa:!1},xa:!0};function V(a,b){Object.keys(b).forEach(c=>{a[c]=b[c]})}
function W(a,b,c,d={}){let e=Object.create(null);V(e,ta);V(e,d);d="onlyInvert"===e.aa||"invertFirst"===e.aa;var f="attemptBoth"===e.aa||d,g=e.ta,h=e.xa,k=b*c;if(a.length!==4*k)throw Error("Malformed data passed to binarizer.");var l=0;if(h){var m=new Uint8ClampedArray(a.buffer,l,k);l+=k}m=new A(b,c,m);if(g.qa)for(var n=0;n<c;n++)for(var q=0;q<b;q++){var t=4*(n*b+q);m.set(q,n,g.red*a[t]+g.green*a[t+1]+g.blue*a[t+2]+128>>8)}else for(n=0;n<c;n++)for(q=0;q<b;q++)t=4*(n*b+q),m.set(q,n,g.red*a[t]+g.green*
a[t+1]+g.blue*a[t+2]);g=Math.ceil(b/8);n=Math.ceil(c/8);q=g*n;if(h){var x=new Uint8ClampedArray(a.buffer,l,q);l+=q}x=new A(g,n,x);for(q=0;q<n;q++)for(t=0;t<g;t++){var r=Infinity,p=0;for(var u=0;8>u;u++)for(let v=0;8>v;v++){let Z=m.get(8*t+v,8*q+u);r=Math.min(r,Z);p=Math.max(p,Z)}u=(r+p)/2;u=Math.min(255,1.11*u);24>=p-r&&(u=r/2,0<q&&0<t&&(p=(x.get(t,q-1)+2*x.get(t-1,q)+x.get(t-1,q-1))/4,r<p&&(u=p)));x.set(t,q,u)}h?(q=new Uint8ClampedArray(a.buffer,l,k),l+=k,q=new w(q,b)):q=w.ja(b,c);t=null;f&&(h?(a=
new Uint8ClampedArray(a.buffer,l,k),t=new w(a,b)):t=w.ja(b,c));for(b=0;b<n;b++)for(a=0;a<g;a++){c=g-3;c=2>a?2:a>c?c:a;h=n-3;h=2>b?2:b>h?h:b;k=0;for(l=-2;2>=l;l++)for(r=-2;2>=r;r++)k+=x.get(c+l,h+r);c=k/25;for(h=0;8>h;h++)for(k=0;8>k;k++)l=8*a+h,r=8*b+k,p=m.get(l,r),q.set(l,r,p<=c),f&&t.set(l,r,!(p<=c))}let {ra:z,ya:y}=f?{ra:q,ya:t}:{ra:q};(f=U(d?y:z))||"attemptBoth"!==e.aa&&"invertFirst"!==e.aa||(f=U(d?z:y));return f}W.default=W;let X="dontInvert",Y={red:77,green:150,blue:29,qa:!0};
self.onmessage=a=>{let b=a.data.id;var c=a.data.data;switch(a.data.type){case "decode":a=W(c.data,c.width,c.height,{aa:X,ta:Y});c={};c.id=b;c.type="qrResult";a?(c.data=a.data,c.binaryData=Uint8Array.from(a.ua),c.cornerPoints=[a.location.Fa,a.location.Ga,a.location.wa,a.location.va]):c.data=null;self.postMessage(c);break;case "grayscaleWeights":Y.red=c.red;Y.green=c.green;Y.blue=c.blue;Y.qa=c.useIntegerApproximation;break;case "inversionMode":switch(c){case "original":X="dontInvert";break;case "invert":X=
"onlyInvert";break;case "both":X="attemptBoth";break;default:throw Error("Invalid inversion mode");}break;case "close":self.close()}}
`]), { type: "application/javascript" }));
var qrScannerWorker_min = Object.freeze({ __proto__: null, createWorker });
return QrScanner2;
});
}
});
// MiiImportController.js
var import_qr_scanner = __toESM(require_qr_scanner_legacy_min());
// ../MiiDecoderCli.js
var MiiDecoder = class _MiiDecoder {
/**
* @private
*/
constructor() {
}
/**
* Size of Wii format Mii data (RFLCharData).
* @public
* @readonly
* @type {number}
*/
static SIZE_WII_DATA = 74;
/**
* Base size of 3DS/Wii U format Mii data.
* Usually 96 bytes long (FFLStoreData, Ver3StoreData).
* @public
* @readonly
* @type {number}
*/
static SIZE3DS_WIIU_DATA = 72;
/**
* Size of Switch "nn::mii::CharInfo" format.
* @public
* @readonly
* @type {number}
*/
static SIZE_NX_CHAR_INFO = 88;
/**
* @public
* @readonly
* @type {number}
*/
static SIZE_STUDIO_RAW_DATA = 46;
/**
* @public
* @readonly
* @type {number}
*/
static SIZE_STUDIO_URL_DATA = 47;
/**
* @public
* @readonly
* @type {number}
*/
static SIZE_NX_STORE_DATA = 68;
/**
* @public
* @readonly
* @type {number}
*/
static SIZE_NX_CORE_DATA = 48;
/**
* Base URL of /miis/image.png API that returns a Mii image.
* This can also be replaced by mii-unsecure.ariankordi.net.
* @public
* @readonly
* @type {string}
*/
static URL_BASE_IMAGE_PNG = "https://truenas.local:8443/miis/image.png?shaderType=miitomo&data=";
/**
* Gets the Mii image URL using the Mii data converted in this instance.
*
* <p>Decodes the input Mii data, where the type is automatically detected.
* Returns null if the format is not supported.
* Formats (3DS/Wii U):
* <ul>
* <li>96 byte FFLStoreData/nn::mii::Ver3StoreData (has CRC)</li>
* <li>92 byte FFLiMiiDataOfficial (in database, or kazuki-4ys ".3dsmii")</li>
* <li>72 byte FFLiMiiDataCore (no creator name or CRC)
* Formats (Wii):</li>
* <li>76 byte RFLStoreData (has CRC)</li>
* <li>74 byte RFLCharData (no CRC)
* Formats (Switch, Studio):</li>
* <li>88 byte nn::mii::CharInfo</li>
* <li>47 byte obfuscated studio URL data</li>
* <li>46 byte un-obfuscated raw "Studio Code"</li>
* </ul>
* @public
* @param {Uint8Array} dst
* @param {number} dataSize Size of the input data used to detect the type.
* @param {Uint8Array} src Input Mii data to be converted.
*/
static fromAnyMiiData(dst, dataSize, src) {
switch (dataSize) {
case 96:
case 92:
case 72:
_MiiDecoder.from3dsWiiuData(dst, src);
return true;
case 76:
case 74:
_MiiDecoder.fromWiiData(dst, src);
return true;
case 46:
dst.set(src.subarray(0, 46));
return true;
case 47:
_MiiDecoder._deobfuscateStudioUrl(dst, src);
return true;
case 88:
_MiiDecoder.fromNxCharInfo(dst, src);
return true;
case 68:
case 48:
_MiiDecoder.fromNxCoreData(dst, src);
return true;
default:
return false;
}
}
/**
* @public
* @param {Uint8Array} dst
* @param {MiiExtraInfo} dstExtra
* @param {number} dataSize
* @param {Uint8Array} src
*/
static fromAnyMiiDataExtra(dst, dstExtra, dataSize, src) {
switch (dataSize) {
case 96:
case 92:
case 72:
_MiiDecoder.from3dsWiiuData(dst, src);
_MiiDecoder.setExtraFromStudioData(dstExtra, dst);
_MiiDecoder.setExtraFrom3dsWiiuData(dstExtra, src);
return true;
case 76:
case 74:
_MiiDecoder.fromWiiData(dst, src);
_MiiDecoder.setExtraFromStudioData(dstExtra, dst);
_MiiDecoder.setExtraFromWiiData(dstExtra, src);
return true;
case 46:
dst.set(src.subarray(0, 46));
_MiiDecoder.setExtraFromStudioData(dstExtra, dst);
return true;
case 47:
_MiiDecoder._deobfuscateStudioUrl(dst, src);
_MiiDecoder.setExtraFromStudioData(dstExtra, dst);
return true;
case 88:
_MiiDecoder.fromNxCharInfo(dst, src);
_MiiDecoder.setExtraFromStudioData(dstExtra, dst);
_MiiDecoder.setExtraFromNxCharInfo(dstExtra, src);
return true;
case 68:
case 48:
_MiiDecoder.fromNxCoreData(dst, src);
_MiiDecoder.setExtraFromStudioData(dstExtra, dst);
_MiiDecoder.setExtraFromNxCoreData(dstExtra, src);
return true;
default:
return false;
}
}
/**
* Verifies if the converted data in this instance is considered valid.
* Derived from nn::mii::detail::CharInfoRaw::IsValid,
* but does not return the specific reason.
* @public
* @param {Uint8Array} buf
*/
static isValid(buf) {
return buf[0] < 100 && buf[1] < 6 && 128 > buf[2] && buf[3] < 7 && buf[4] < 100 && buf[5] < 8 && buf[6] < 8 && buf[7] < 60 && buf[8] < 13 && buf[9] < 19 && buf[10] < 7 && buf[11] < 100 && buf[12] < 12 && buf[13] < 9 && buf[14] < 24 && buf[15] < 13 && buf[16] >= 3 && buf[16] <= 18 && buf[17] < 10 && buf[18] < 12 && buf[19] < 12 && buf[20] < 12 && buf[21] < 12 && buf[22] < 2 && buf[23] < 100 && buf[24] < 8 && buf[25] < 20 && buf[26] < 21 && buf[27] < 100 && buf[28] < 2 && buf[29] < 132 && 128 > buf[30] && buf[31] < 9 && buf[32] < 2 && buf[33] < 17 && buf[34] < 31 && buf[35] < 7 && buf[36] < 100 && buf[37] < 9 && buf[38] < 36 && buf[39] < 19 && buf[40] < 9 && buf[41] < 6 && buf[42] < 17 && buf[43] < 9 && buf[44] < 18 && buf[45] < 19;
}
/**
* Decodes the input 3DS/Wii U Mii data (FFLStoreData/nn::mii::Ver3StoreData).
* @public
* @param {Uint8Array} dst
* @param {Uint8Array} src
*/
static from3dsWiiuData(dst, src) {
dst[0] = src[66] >> 3 & 7;
dst[1] = src[66] & 7;
dst[2] = src[47];
dst[3] = src[53] >> 5;
dst[4] = (src[53] & 1) << 2 | src[52] >> 6;
dst[5] = src[54] & 31;
dst[6] = src[53] >> 1 & 15;
dst[7] = src[52] & 63;
dst[8] = (src[55] & 1) << 3 | src[54] >> 5;
dst[9] = src[55] >> 1 & 31;
dst[10] = src[57] >> 4 & 7;
dst[11] = src[56] >> 5;
dst[12] = src[58] & 31;
dst[13] = src[57] & 15;
dst[14] = src[56] & 31;
dst[15] = (src[59] & 1) << 3 | src[58] >> 5;
dst[16] = src[59] >> 1 & 31;
dst[17] = src[48] >> 5;
dst[18] = src[49] >> 4;
dst[19] = src[48] >> 1 & 15;
dst[20] = src[49] & 15;
dst[21] = src[25] >> 2 & 15;
dst[22] = src[24] & 1;
dst[23] = src[68] >> 4 & 7;
dst[24] = (src[69] & 7) * 2 | src[68] >> 7;
dst[25] = src[68] & 15;
dst[26] = src[69] >> 3;
dst[27] = src[51] & 7;
dst[28] = src[51] >> 3 & 1;
dst[29] = src[50];
dst[30] = src[46];
dst[31] = src[70] >> 1 & 15;
dst[32] = src[70] & 1;
dst[33] = (src[71] & 3) << 3 | src[70] >> 5;
dst[34] = src[71] >> 2 & 31;
dst[35] = src[63] >> 5;
dst[36] = (src[63] & 1) << 2 | src[62] >> 6;
dst[37] = src[63] >> 1 & 15;
dst[38] = src[62] & 63;
dst[39] = src[64] & 31;
dst[40] = (src[67] & 3) << 2 | src[66] >> 6;
dst[41] = src[64] >> 5;
dst[42] = src[67] >> 2 & 31;
dst[43] = (src[61] & 1) << 3 | src[60] >> 5;
dst[44] = src[60] & 31;
dst[45] = src[61] >> 1 & 31;
_MiiDecoder._convertFieldsVer3ToNx(dst);
return dst;
}
/**
* Decodes the input Wii Mii data (RFLCharData, RFLStoreData).
* @public
* @param {Uint8Array} dst
* @param {Uint8Array} src
*/
static fromWiiData(dst, src) {
dst[0] = src[50] >> 1 & 7;
dst[1] = src[50] >> 4 & 3;
dst[2] = src[23];
dst[3] = 3;
dst[4] = src[42] >> 5;
dst[5] = src[41] >> 5 | (src[40] & 3) << 3;
dst[6] = src[42] >> 1 & 15;
dst[7] = src[40] >> 2;
dst[8] = src[43] >> 5 | (src[42] & 1) << 3;
dst[9] = src[41] & 31;
dst[10] = 3;
dst[11] = src[38] >> 5;
dst[12] = src[37] >> 6 | (src[36] & 7) << 2;
dst[13] = src[38] >> 1 & 15;
dst[14] = src[36] >> 3;
dst[15] = src[39] & 15;
dst[16] = src[39] >> 4 | (src[38] & 1) << 4;
dst[17] = src[32] >> 2 & 7;
let faceTex = src[33] >> 6 | (src[32] & 3) << 2;
dst[18] = _MiiDecoder._FROM_WII_DATA_FACE_TEX_TABLE[faceTex * 2 + 1];
dst[19] = src[32] >> 5;
dst[20] = _MiiDecoder._FROM_WII_DATA_FACE_TEX_TABLE[faceTex * 2];
dst[21] = src[1] >> 1 & 15;
dst[22] = src[0] >> 6 & 1;
dst[23] = src[48] >> 1 & 7;
dst[24] = src[49] >> 5 | (src[48] & 1) << 3;
dst[25] = src[48] >> 4;
dst[26] = src[49] & 31;
dst[27] = src[35] >> 6 | (src[34] & 1) << 2;
dst[28] = src[35] >> 5 & 1;
dst[29] = src[34] >> 1;
dst[30] = src[22];
dst[31] = src[52] >> 3 & 15;
dst[32] = src[52] >> 7;
dst[33] = src[53] >> 1 & 31;
dst[34] = src[53] >> 6 | (src[52] & 7) << 2;
dst[35] = 3;
dst[36] = src[46] >> 1 & 3;
dst[37] = src[47] >> 5 | (src[46] & 1) << 3;
dst[38] = src[46] >> 3;
dst[39] = src[47] & 31;
dst[40] = src[51] >> 5 | (src[50] & 1) << 3;
dst[41] = src[50] >> 6;
dst[42] = src[51] & 31;
dst[43] = src[44] & 15;
dst[44] = src[44] >> 4;
dst[45] = src[45] >> 3;
_MiiDecoder._convertFieldsVer3ToNx(dst);
return dst;
}
/**
* Decodes the input Switch nn::mii::CharInfo format.
* @public
* @param {Uint8Array} dst
* @param {Uint8Array} src
*/
static fromNxCharInfo(dst, src) {
dst[0] = src[74];
dst[1] = src[75];
dst[2] = src[42];
dst[3] = src[55];
dst[4] = src[53];
dst[5] = src[56];
dst[6] = src[54];
dst[7] = src[52];
dst[8] = src[57];
dst[9] = src[58];
dst[10] = src[62];
dst[11] = src[60];
dst[12] = src[63];
dst[13] = src[61];
dst[14] = src[59];
dst[15] = src[64];
dst[16] = src[65];
dst[17] = src[46];
dst[18] = src[48];
dst[19] = src[45];
dst[20] = src[47];
dst[21] = src[39];
dst[22] = src[40];
dst[23] = src[80];
dst[24] = src[81];
dst[25] = src[79];
dst[26] = src[82];
dst[27] = src[50];
dst[28] = src[51];
dst[29] = src[49];
dst[30] = src[41];
dst[31] = src[84];
dst[32] = src[83];
dst[33] = src[85];
dst[34] = src[86];
dst[35] = src[72];
dst[36] = src[70];
dst[37] = src[71];
dst[38] = src[69];
dst[39] = src[73];
dst[40] = src[77];
dst[41] = src[76];
dst[42] = src[78];
dst[43] = src[67];
dst[44] = src[66];
dst[45] = src[68];
return dst;
}
/**
* Decodes the input Switch nn::mii::CoreData (database) format.
* @public
* @param {Uint8Array} dst
* @param {Uint8Array} src
*/
static fromNxCoreData(dst, src) {
dst[0] = src[7] & 127;
dst[1] = src[13] >> 5;
dst[2] = src[2] & 127;
dst[3] = src[17] >> 5;
dst[4] = src[4] & 127;
dst[5] = src[16] >> 5;
dst[6] = src[18] >> 5;
dst[7] = src[9] & 63;
dst[8] = src[23] >> 4;
dst[9] = src[11] & 31;
dst[10] = src[15] >> 5;
dst[11] = src[5] & 127;
dst[12] = src[24] >> 4;
dst[13] = src[24] & 15;
dst[14] = src[12] & 31;
dst[15] = src[25] & 15;
dst[16] = (src[25] >> 4) + 3;
dst[17] = src[22] & 15;
dst[18] = src[23] & 15;
dst[19] = src[21] >> 4;
dst[20] = src[22] >> 4;
dst[21] = src[21] & 15;
dst[22] = src[4] >> 7;
dst[23] = src[8] & 127;
dst[24] = src[11] >> 5;
dst[25] = src[20] & 31;
dst[26] = src[17] & 31;
dst[27] = src[3] & 127;
dst[28] = src[2] >> 7;
dst[29] = src[0];
dst[30] = src[1] & 127;
dst[31] = src[27] >> 4;
dst[32] = src[1] >> 7;
dst[33] = src[18] & 31;
dst[34] = src[19] & 31;
dst[35] = src[14] >> 5;
dst[36] = src[6] & 127;
dst[37] = src[26] >> 4;
dst[38] = src[10] & 63;
dst[39] = src[15] & 31;
dst[40] = src[27] & 15;
dst[41] = src[12] >> 5;
dst[42] = src[16] & 31;
dst[43] = src[26] & 15;
dst[44] = src[13] & 31;
dst[45] = src[14] & 31;
return dst;
}
/**
* Common method to convert colors and other fields from
* 3DS/Wii U and Wii data to Switch/Studio equivalents.
* @param {Uint8Array} buf
*/
static _convertFieldsVer3ToNx(buf) {
if (buf[27] == 0)
buf[27] = 8;
if (buf[0] == 0)
buf[0] = 8;
if (buf[11] == 0)
buf[11] = 8;
buf[36] += 19;
buf[4] += 8;
if (buf[23] == 0)
buf[23] = 8;
else if (buf[23] < 6)
buf[23] += 13;
if (127 < buf[2])
buf[2] = 127;
if (127 < buf[30])
buf[30] = 127;
}
/**
* Obfuscates Studio data to be used in the URL.
* @public
* @param {Uint8Array} dst
* @param {Uint8Array} src
* @param {number} [seed=0] The random value to use for the obfuscation. Best left as 0.
*/
static obfuscateStudioUrl(dst, src, seed = 0) {
dst[0] = seed;
for (let i = 0; i < 46; i++) {
let val = src[i] ^ dst[i];
dst[i + 1] = (7 + val) % 256;
}
}
/**
* Deobfuscates Studio URL data to raw decodable data.
* @param {Uint8Array} dst
* @param {Uint8Array} src
*/
static _deobfuscateStudioUrl(dst, src) {
for (let i = 0; i < 46; i++) {
let val = (src[i + 1] - 7) % 256;
dst[i] = val ^ src[i];
}
}
/**
* @public
* @param {MiiExtraInfo} dst
* @param {Uint8Array} src
*/
static setExtraFromStudioData(dst, src) {
dst.isNx = true;
dst.height = src[30];
dst.build = src[2];
dst.skinColor = src[17];
dst.gender = src[22];
dst.favoriteColor = src[21];
dst.fontRegion = 0;
dst.birthMonth = 0;
dst.birthDay = 0;
dst.favorite = false;
dst.copyable = false;
dst.ngWord = false;
dst.localonly = false;
dst.regionMove = 0;
dst.nicknameSize = dst.creatorNameSize = 0;
}
/**
* @public
* @param {MiiExtraInfo} dst
* @param {Uint8Array} src
*/
static setExtraFrom3dsWiiuData(dst, src) {
dst.isNx = false;
dst.fontRegion = src[1] >> 4 & 3;
dst.birthMonth = src[24] >> 1 & 15;
dst.birthDay = (src[25] & 3) << 3 | src[24] >> 5;
dst.favorite = (src[25] >> 6 & 1) == 1;
dst.copyable = (src[1] & 1) == 1;
dst.ngWord = (src[1] >> 1 & 1) == 1;
dst.localonly = (src[48] & 1) == 1;
dst.regionMove = src[1] >> 2 & 3;
dst.authorId.set(src.subarray(4, 12));
dst.createId.set(src.subarray(12, 22));
const name = new Uint16Array(10);
for (let i = 0; i < 10; i++)
name[i] = src[26 + i * 2] | src[26 + i * 2 + 1] << 8;
const creator = new Uint16Array(10);
for (let i = 0; i < 10; i++)
creator[i] = src[72 + i * 2] | src[72 + i * 2 + 1] << 8;
dst.nicknameSize = _MiiDecoder._char16ToUtf8(dst.nicknameData, name, 10);
dst.creatorNameSize = _MiiDecoder._char16ToUtf8(dst.creatorNameData, creator, 10);
}
/**
* @public
* @param {MiiExtraInfo} dst
* @param {Uint8Array} src
*/
static setExtraFromWiiData(dst, src) {
dst.isNx = false;
dst.fontRegion = 0;
dst.birthMonth = src[0] >> 2 & 15;
dst.birthDay = src[1] >> 5 | (src[0] & 3) << 3;
dst.favorite = (src[1] & 1) == 1;
dst.copyable = false;
dst.ngWord = false;
dst.localonly = (src[33] >> 2 & 1) == 1;
dst.regionMove = 0;
dst.createId.set(src.subarray(24, 32));
const name = new Uint16Array(10);
for (let i = 0; i < 10; i++)
name[i] = src[2 + i * 2] << 8 | src[2 + i * 2 + 1];
const creator = new Uint16Array(10);
for (let i = 0; i < 10; i++)
creator[i] = src[54 + i * 2] << 8 | src[54 + i * 2 + 1];
dst.nicknameSize = _MiiDecoder._char16ToUtf8(dst.nicknameData, name, 10);
dst.creatorNameSize = _MiiDecoder._char16ToUtf8(dst.creatorNameData, creator, 10);
}
/**
* @public
* @param {MiiExtraInfo} dst
* @param {Uint8Array} src
*/
static setExtraFromNxCharInfo(dst, src) {
dst.isNx = true;
dst.fontRegion = src[38];
dst.createId.set(src.subarray(0, 16));
const name = new Uint16Array(10);
for (let i = 0; i < 10; i++)
name[i] = src[16 + i * 2] | src[16 + i * 2 + 1] << 8;
dst.nicknameSize = _MiiDecoder._char16ToUtf8(dst.nicknameData, name, 10);
}
/**
* @public
* @param {MiiExtraInfo} dst
* @param {Uint8Array} src
*/
static setExtraFromNxCoreData(dst, src) {
dst.isNx = true;
dst.fontRegion = src[10] >> 6;
const name = new Uint16Array(10);
for (let i = 0; i < 10; i++)
name[i] = src[28 + i * 2] | src[28 + i * 2 + 1] << 8;
dst.nicknameSize = _MiiDecoder._char16ToUtf8(dst.nicknameData, name, 10);
}
/**
* Writes UTF-8 text from a buffer of 16-bit wide characters
* that are within the Unicode BMP, also known as UCS-2 encoded text.
* This does not support decoding UTF-16 surrogate pairs for
* characters beyond the Basic Multilingual Plane (emoji, etc.)
* @param {Uint8Array} dst Destination UTF-8 bytes to write to. Size MUST be 3 * characterCount.
* @param {Uint16Array} src Source array of 16-bit code points.
* @param {number} characterCount Amount of characters from the original string to process.
*/
static _char16ToUtf8(dst, src, characterCount) {
let iDst = 0;
for (let i = 0; i < characterCount && src[i] != 0; i++) {
let chr = src[i];
if (chr <= 127)
dst[iDst++] = chr;
else if (chr <= 2047) {
dst[iDst + 0] = 192 | chr >> 6;
dst[iDst + 1] = 128 | chr & 63;
iDst += 2;
} else {
if (chr >= 55296 && chr <= 57343)
chr = 65533;
dst[iDst + 0] = 224 | chr >> 12;
dst[iDst + 1] = 128 | chr >> 6 & 63;
dst[iDst + 2] = 128 | chr & 63;
iDst += 3;
}
}
return iDst;
}
/**
* @param {Uint16Array} dst
* @param {Uint8Array} src
* @param {number} srcSize
*/
static _utf8ToChar16(dst, src, srcSize) {
let iDst = 0;
for (let i = 0; i < srcSize && src[i] != 0; iDst++) {
let chr = 0;
if ((src[i] & 224) == 224 && (src[i] & 16) == 0) {
chr = (src[i + 0] & 15) << 12 | (src[i + 1] & 63) << 6 | (src[i + 2] & 63) << 0;
i += 3;
} else if ((src[i] & 192) == 192 && (src[i] & 32) == 0) {
chr = (src[i + 0] & 31) << 6 | (src[i + 1] & 63) << 0;
i += 2;
} else
chr = src[i++] & 127;
dst[iDst] = chr;
}
return iDst;
}
/**
* @readonly
* @type {Uint8Array}
*/
static _FROM_WII_DATA_FACE_TEX_TABLE = new Uint8Array([
0,
0,
0,
1,
0,
6,
0,
9,
5,
0,
2,
0,
3,
0,
7,
0,
8,
0,
0,
10,
9,
0,
11,
0
]);
};
var MiiExtraInfo = class {
/**
* @readonly
* @type {number}
*/
static _NAME_LENGTH = 10;
/**
* True if the info is from NX Mii data.
* @public
* @type {boolean}
*/
isNx;
/**
* Mii's nickname as UTF-8.
* @public
* @readonly
* @type {Uint8Array}
*/
nicknameData = new Uint8Array(30);
/**
* Mii's creator name as UTF-8.
* Only present if IsNx = false.
* @public
* @readonly
* @type {Uint8Array}
*/
creatorNameData = new Uint8Array(30);
/**
* Size of buffer for nickname in bytes.
* @public
* @type {number}
*/
nicknameSize;
/**
* Size of buffer for creator name in bytes.
* @public
* @type {number}
*/
creatorNameSize;
/**
* @public
* @type {number}
*/
height;
/**
* @public
* @type {number}
*/
build;
/**
* @public
* @type {number}
*/
skinColor;
/**
* @public
* @type {number}
*/
gender;
/**
* @public
* @type {number}
*/
favoriteColor;
/**
* @public
* @type {number}
*/
fontRegion;
/**
* Only present if IsNx = false.
* @public
* @type {number}
*/
birthMonth;
/**
* Only present if IsNx = false.
* @public
* @type {number}
*/
birthDay;
/**
* Only present if IsNx = false.
* @public
* @type {boolean}
*/
favorite;
/**
* Only present if IsNx = false.
* @public
* @type {boolean}
*/
copyable;
/**
* Only present if IsNx = false.
* @public
* @type {boolean}
*/
ngWord;
/**
* Only present if IsNx = false.
* @public
* @type {boolean}
*/
localonly;
/**
* Only present if IsNx = false.
* @public
* @type {number}
*/
regionMove;
/**
* Mii unique ID.
* When IsNx = true, this is a 16-byte UUIDv4,
* otherwise 10 bytes (3DS/Wii U) or 8 bytes (Wii).
* @public
* @readonly
* @type {Uint8Array}
*/
createId = new Uint8Array(16);
/**
* Mii console ID only for 3DS/Wii U data.
* Only present if IsNx = false.
* This is equal to the Transferable ID (not permanent).
* @public
* @readonly
* @type {Uint8Array}
*/
authorId = new Uint8Array(8);
/**
* @public
*/
getNickname() {
return new TextDecoder().decode(this.nicknameData.subarray(0, this.nicknameSize));
}
/**
* @public
*/
getCreatorName() {
return new TextDecoder().decode(this.creatorNameData.subarray(0, this.creatorNameSize));
}
};
// ../../WrappedStoreData/WrappedStoreDataTest.js
var Aes128 = class _Aes128 {
/**
* @readonly
* @type {Int32Array}
*/
_encKey = new Int32Array(44);
/**
* @readonly
* @type {Int32Array}
*/
_encTable0 = new Int32Array(256);
/**
* @readonly
* @type {Int32Array}
*/
_encTable1 = new Int32Array(256);
/**
* @readonly
* @type {Int32Array}
*/
_encTable2 = new Int32Array(256);
/**
* @readonly
* @type {Int32Array}
*/
_encTable3 = new Int32Array(256);
/**
* @readonly
* @type {Int32Array}
*/
_sbox = new Int32Array(256);
/**
* Initializes AES-128 with the given 16-byte key.
* Must be called before <code>EncryptBlock</code>.
* Runs table pre-computation and key schedule.
* Ported from sjcl.cipher.aes constructor and _precompute:
* https://github.com/bitwiseshiftleft/sjcl/blob/85caa53c/core/aes.js_L25
* @public
* @param {Readonly<Uint8Array>} key The 16-byte AES key.
* @param {number} keyOffset Offset into <code>key</code>.
*/
initialize(key, keyOffset) {
this._precompute();
this._scheduleKey(key, keyOffset);
}
/**
* Shifts a byte value (0-255) into the top byte of a 32-bit signed int: result = b &lt;&lt; 24.
* Avoids signed-integer overflow (which is undefined behaviour in C and not guaranteed
* to wrap in Fusion/Python) by splitting around the sign bit:
* (b &amp; 0x7F) &lt;&lt; 24 is at most 0x7F000000 = 2130706432, which fits in int.
* If bit 7 of b is set we subtract 2147483648 (INT_MIN) to set the sign bit.
* @param {number} b
*/
static _byteToTop(b) {
let hi = (b & 127) << 24;
if ((b & 128) != 0)
hi = Number(hi - 2147483648);
return hi;
}
/**
* Rotates a 32-bit integer left by 8 bits (= right by 24): [b3,b2,b1,b0] -&gt; [b2,b1,b0,b3].
* Equivalent to <code>x &lt;&lt; 24 ^ x &gt;&gt; 8 &amp; 0xFFFFFF</code>, but calls ByteToTop to avoid overflow.
* @param {number} x
*/
static _rot8Left(x) {
return _Aes128._byteToTop(x & 255) ^ x >> 8 & 16777215;
}
/**
* Loads a big-endian 32-bit word from <code>buf[o..o+4)</code>.
* @param {Readonly<Uint8Array>} buf
* @param {number} o
*/
static _loadWord(buf, o) {
return _Aes128._byteToTop(buf[o] & 255) | (buf[o + 1] & 255) << 16 | (buf[o + 2] & 255) << 8 | buf[o + 3] & 255;
}
/**
* Stores a big-endian 32-bit word into <code>buf[o..o+4)</code>.
* @param {Uint8Array} buf
* @param {number} o
* @param {number} w
*/
static _storeWord(buf, o, w) {
buf[o] = w >> 24 & 255;
buf[o + 1] = w >> 16 & 255;
buf[o + 2] = w >> 8 & 255;
buf[o + 3] = w & 255;
}
/**
* Pre-computes the AES S-box and MixColumns tables.
*
* <p>Ported from sjcl.cipher.aes._precompute:
* https://github.com/bitwiseshiftleft/sjcl/blob/85caa53c/core/aes.js_L114
* <p>The loop mirrors sjcl's <code>for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1)</code>.
* It processes all 256 GF(2^8) elements and stops when sbox[x] is already populated.
*/
_precompute() {
this._sbox.fill(0);
const d = new Int32Array(256);
const th = new Int32Array(256);
for (let i = 0; i < 256; i++) {
d[i] = i << 1 ^ (i >> 7) * 283;
th[d[i] ^ i] = i;
}
let x = 0;
let xInv = 0;
for (; ; ) {
if (this._sbox[x] != 0)
break;
let s = xInv ^ xInv << 1 ^ xInv << 2 ^ xInv << 3 ^ xInv << 4;
s = s >> 8 ^ s & 255 ^ 99;
this._sbox[x] = s;
let x2 = d[x];
let x4 = d[x2];
let x8 = d[x4];
let tEnc = d[s] * 257 ^ s * 16843008;
tEnc = _Aes128._rot8Left(tEnc);
this._encTable0[x] = tEnc;
tEnc = _Aes128._rot8Left(tEnc);
this._encTable1[x] = tEnc;
tEnc = _Aes128._rot8Left(tEnc);
this._encTable2[x] = tEnc;
tEnc = _Aes128._rot8Left(tEnc);
this._encTable3[x] = tEnc;
let x2or1 = x2;
if (x2or1 == 0)
x2or1 = 1;
x ^= x2or1;
let nextXInv = th[xInv];
if (nextXInv == 0)
nextXInv = 1;
xInv = nextXInv;
}
}
/**
* Expands the 16-byte AES-128 key into the 44-word encryption key schedule.
* Ported from the key expansion loop in sjcl.cipher.aes constructor:
* https://github.com/bitwiseshiftleft/sjcl/blob/85caa53c/core/aes.js_L42
* @param {Readonly<Uint8Array>} key
* @param {number} keyOffset
*/
_scheduleKey(key, keyOffset) {
for (let i = 0; i < 4; i++) {
this._encKey[i] = _Aes128._loadWord(key, keyOffset + i * 4);
}
let rcon = 1;
for (let i = 4; i < 44; i++) {
let tmp = this._encKey[i - 1];
if (i % 4 == 0) {
tmp = _Aes128._byteToTop(this._sbox[tmp >> 16 & 255]) ^ this._sbox[tmp >> 8 & 255] << 16 ^ this._sbox[tmp & 255] << 8 ^ this._sbox[tmp >> 24 & 255] ^ _Aes128._byteToTop(rcon);
rcon = rcon << 1 ^ (rcon >> 7) * 283;
}
this._encKey[i] = this._encKey[i - 4] ^ tmp;
}
}
/**
* Encrypts one 16-byte block in-place (big-endian word order).
* Ported from sjcl.cipher.aes._crypt (encrypt direction, dir=0):
* https://github.com/bitwiseshiftleft/sjcl/blob/85caa53c/core/aes.js_L156
* @public
* @param {Uint8Array} block 16-byte block buffer. Encrypted in-place.
* @param {number} offset Offset into <code>block</code>.
*/
encryptBlock(block, offset) {
let a = _Aes128._loadWord(block, offset) ^ this._encKey[0];
let b = _Aes128._loadWord(block, offset + 4) ^ this._encKey[1];
let c = _Aes128._loadWord(block, offset + 8) ^ this._encKey[2];
let d = _Aes128._loadWord(block, offset + 12) ^ this._encKey[3];
let a2;
let b2;
let c2;
let kIndex = 4;
for (let i = 0; i < 9; i++) {
a2 = this._encTable0[a >> 24 & 255] ^ this._encTable1[b >> 16 & 255] ^ this._encTable2[c >> 8 & 255] ^ this._encTable3[d & 255] ^ this._encKey[kIndex];
b2 = this._encTable0[b >> 24 & 255] ^ this._encTable1[c >> 16 & 255] ^ this._encTable2[d >> 8 & 255] ^ this._encTable3[a & 255] ^ this._encKey[kIndex + 1];
c2 = this._encTable0[c >> 24 & 255] ^ this._encTable1[d >> 16 & 255] ^ this._encTable2[a >> 8 & 255] ^ this._encTable3[b & 255] ^ this._encKey[kIndex + 2];
d = this._encTable0[d >> 24 & 255] ^ this._encTable1[a >> 16 & 255] ^ this._encTable2[b >> 8 & 255] ^ this._encTable3[c & 255] ^ this._encKey[kIndex + 3];
kIndex += 4;
a = a2;
b = b2;
c = c2;
}
let out0 = _Aes128._byteToTop(this._sbox[a >> 24 & 255]) ^ this._sbox[b >> 16 & 255] << 16 ^ this._sbox[c >> 8 & 255] << 8 ^ this._sbox[d & 255] ^ this._encKey[kIndex];
let out1 = _Aes128._byteToTop(this._sbox[b >> 24 & 255]) ^ this._sbox[c >> 16 & 255] << 16 ^ this._sbox[d >> 8 & 255] << 8 ^ this._sbox[a & 255] ^ this._encKey[kIndex + 1];
let out2 = _Aes128._byteToTop(this._sbox[c >> 24 & 255]) ^ this._sbox[d >> 16 & 255] << 16 ^ this._sbox[a >> 8 & 255] << 8 ^ this._sbox[b & 255] ^ this._encKey[kIndex + 2];
let out3 = _Aes128._byteToTop(this._sbox[d >> 24 & 255]) ^ this._sbox[a >> 16 & 255] << 16 ^ this._sbox[b >> 8 & 255] << 8 ^ this._sbox[c & 255] ^ this._encKey[kIndex + 3];
_Aes128._storeWord(block, offset, out0);
_Aes128._storeWord(block, offset + 4, out1);
_Aes128._storeWord(block, offset + 8, out2);
_Aes128._storeWord(block, offset + 12, out3);
}
};
var AesCcmCtr = class {
/**
* @private
*/
constructor() {
}
/**
* Runs AES-CCM CTR mode on <code>data</code>, XORing each 16-byte block with
* AES(key, counter), where the counter is initialized from the nonce
* and incremented for each block.
*
* <p>This is used for both encryption and decryption (CTR mode is symmetric).
* Tag verification is intentionally skipped — see the 3DS AES CCM errata:
* https://www.3dbrew.org/wiki/AES_Registers_CCM_mode_pitfall
* <p>Ported from sjcl.mode.ccm._ctrMode:
* https://github.com/bitwiseshiftleft/sjcl/blob/85caa53c/core/ccm.js_L194
* <p>Counter block layout (16 bytes):
* [0] = L - 1 (flags byte; L=3 for 3DS so this is 0x02)
* [1..12) = nonce (11 bytes, nonce is clamped to 8*(15-L) = 96 bits)
* [12..16) = 0x00000000 (counter, starts at 0 for tag, 1 for data)
* <p>The data counter starts at 1 (counter 0 is reserved for the tag).
* @public
* @param {Aes128} aes Aes128 instance initialized with the key.
* @param {Uint8Array} data Data buffer to XOR in-place (plaintext for encrypt, ciphertext for decrypt).
* @param {number} dataOffset Offset into <code>data</code>.
* @param {number} dataLength Number of bytes to process.
* @param {Readonly<Uint8Array>} nonce Nonce buffer. Must be at least <code>nonceLength</code> bytes at <code>nonceOffset</code>.
* @param {number} nonceOffset Offset into <code>nonce</code>.
* @param {number} nonceLength Length of the nonce in bytes. For 3DS CCM this is 12, but only
* 11 bytes are used after clamping to 8*(15-L) bits with L=3.
*/
static ctrXor(aes, data, dataOffset, dataLength, nonce, nonceOffset, nonceLength) {
const ctr = new Uint8Array(16);
ctr[0] = 2;
let nonceBytes = nonceLength;
if (nonceBytes > 11)
nonceBytes = 11;
ctr.set(nonce.subarray(nonceOffset, nonceOffset + nonceBytes), 1);
ctr[15] = 1;
const block = new Uint8Array(16);
let processed = 0;
for (; ; ) {
if (processed >= dataLength)
break;
block.set(ctr);
aes.encryptBlock(block, 0);
let blockLen = dataLength - processed;
if (blockLen > 16)
blockLen = 16;
for (let j = 0; j < blockLen; j++) {
data[dataOffset + processed + j] ^= block[j];
}
processed += blockLen;
ctr[15]++;
if (ctr[15] == 0) {
ctr[14]++;
if (ctr[14] == 0) {
ctr[13]++;
if (ctr[13] == 0) {
ctr[12]++;
}
}
}
}
}
/**
* Computes the AES-CCM CBC-MAC authentication tag over <code>data</code>.
*
* <p>Ported from sjcl.mode.ccm._computeTag and _macAdditionalData (no adata):
* https://github.com/bitwiseshiftleft/sjcl/blob/85caa53c/core/ccm.js_L156
* <p>The tag is then encrypted with CTR counter 0 (before the data blocks).
* The result is written into <code>tagOut</code>.
* @public
* @param {Aes128} aes Aes128 instance initialized with the key.
* @param {Readonly<Uint8Array>} data Plaintext data to MAC.
* @param {number} dataOffset Offset into <code>data</code>.
* @param {number} dataLength Length of data.
* @param {Readonly<Uint8Array>} nonce Nonce buffer.
* @param {number} nonceOffset Offset into <code>nonce</code>.
* @param {number} nonceLength Length of nonce in bytes.
* @param {number} tagLength Desired tag length in bytes (must be even, 4..16).
* @param {Uint8Array} tagOut Output buffer for the encrypted tag. Must be at least <code>tagLength</code> bytes.
* @param {number} tagOutOffset Offset into <code>tagOut</code>.
*/
static computeTag(aes, data, dataOffset, dataLength, nonce, nonceOffset, nonceLength, tagLength, tagOut, tagOutOffset) {
let l = 3;
let flags = (tagLength / 2 | 0) - 1 << 3 | l - 1;
const mac = new Uint8Array(16);
mac[0] = flags;
let nonceBytes = nonceLength;
if (nonceBytes > 11)
nonceBytes = 11;
mac.set(nonce.subarray(nonceOffset, nonceOffset + nonceBytes), 1);
mac[13] = dataLength >> 16 & 255;
mac[14] = dataLength >> 8 & 255;
mac[15] = dataLength & 255;
aes.encryptBlock(mac, 0);
const block = new Uint8Array(16);
let processed = 0;
for (; ; ) {
if (processed >= dataLength)
break;
let blockLen = dataLength - processed;
if (blockLen > 16)
blockLen = 16;
block.fill(0);
block.set(data.subarray(dataOffset + processed, dataOffset + processed + blockLen));
for (let j = 0; j < 16; j++) {
mac[j] ^= block[j];
}
aes.encryptBlock(mac, 0);
processed += blockLen;
}
const ctr0 = new Uint8Array(16);
ctr0[0] = 2;
let nb = nonceLength;
if (nb > 11)
nb = 11;
ctr0.set(nonce.subarray(nonceOffset, nonceOffset + nb), 1);
aes.encryptBlock(ctr0, 0);
for (let i = 0; i < tagLength; i++) {
tagOut[tagOutOffset + i] = mac[i] ^ ctr0[i];
}
}
};
var AesCcmCipher = class {
};
var NativeAesCcmCipher = class extends AesCcmCipher {
/**
* @readonly
* @type {Aes128}
*/
_aes = new Aes128();
/**
* Initializes the cipher with the given 16-byte key.
* @public
* @param {Readonly<Uint8Array>} key
* @param {number} keyOffset
*/
initialize(key, keyOffset) {
this._aes.initialize(key, keyOffset);
}
/**
* @public
* @param {Uint8Array} dst
* @param {number} dstOffset
* @param {Readonly<Uint8Array>} content
* @param {number} contentOffset
* @param {number} contentLength
* @param {Readonly<Uint8Array>} nonce
* @param {number} nonceOffset
* @param {number} nonceLength
* @param {number} tagLength
*/
encrypt(dst, dstOffset, content, contentOffset, contentLength, nonce, nonceOffset, nonceLength, tagLength) {
dst.set(content.subarray(contentOffset, contentOffset + contentLength), dstOffset);
AesCcmCtr.computeTag(this._aes, dst, dstOffset, contentLength, nonce, nonceOffset, nonceLength, tagLength, dst, dstOffset + contentLength);
AesCcmCtr.ctrXor(this._aes, dst, dstOffset, contentLength, nonce, nonceOffset, nonceLength);
}
/**
* @public
* @param {Uint8Array} dst
* @param {number} dstOffset
* @param {Readonly<Uint8Array>} ciphertext
* @param {number} ciphertextOffset
* @param {number} ciphertextLength
* @param {Readonly<Uint8Array>} nonce
* @param {number} nonceOffset
* @param {number} nonceLength
* @param {number} tagLength
*/
decryptSkipTag(dst, dstOffset, ciphertext, ciphertextOffset, ciphertextLength, nonce, nonceOffset, nonceLength, tagLength) {
dst.set(ciphertext.subarray(ciphertextOffset, ciphertextOffset + ciphertextLength), dstOffset);
AesCcmCtr.ctrXor(this._aes, dst, dstOffset, ciphertextLength, nonce, nonceOffset, nonceLength);
}
};
var WrappedStoreData = class {
/**
* @private
*/
constructor() {
}
/**
* Size of encrypted Mii data found in QR codes (CFLiWrappedMiiData, FFLiWrappedStoreData).
* @public
* @readonly
* @type {number}
*/
static LENGTH = 112;
/**
* Size of 3DS/Wii U format Mii data (FFLStoreData, CFLiMiiDataPacket, nn::mii::Ver3StoreData).
* @public
* @readonly
* @type {number}
*/
static STORE_DATA_LENGTH = 96;
/**
* Size of the AES-CCM nonce (IV) within wrapped data.
* @readonly
* @type {number}
*/
static _NONCE_LENGTH = 12;
/**
* Size of the AES-CCM authentication tag (MAC) within wrapped data.
* @readonly
* @type {number}
*/
static _TAG_LENGTH = 16;
/**
* Offset within StoreData of the ID field used to form the nonce (CreateID).
* @readonly
* @type {number}
*/
static _ID_OFFSET = 12;
/**
* Number of bytes of the ID copied into the nonce.
* The CreateID is 10 bytes, but it is truncated to be a multiple of 4
* and no larger than the nonce, so it is 8 bytes.
* @readonly
* @type {number}
*/
static _ID_LENGTH = 8;
/**
* Gets 96-byte 3DS/Wii U format Mii data from wrapped QR code data.
*
* <p>Decrypts AES-CCM encrypted data (CFLiWrappedMiiData) using the provided cipher.
* <p>The default AES-CCM tag verification fails due to the following errata:
* https://www.3dbrew.org/wiki/AES_Registers_CCM_mode_pitfall
* Therefore tag verification is skipped. You MUST verify the CRC-16 of
* the output to ensure the data is valid.
* <p>Layout of <code>encryptedData</code> (112 bytes total):
* <ul>
* <li>[0..8) ID / nonce prefix (8 bytes, copied verbatim into output at [12..20))</li>
* <li>[8..96) ciphertext (88 bytes = StoreDataLength - IdLength)</li>
* <li>[96..112) authentication tag (16 bytes, not verified)</li>
* </ul>
* @public
* @param {Uint8Array} dst Destination for decrypted StoreData. Must be <code>StoreDataLength</code> (96) bytes.
* @param {number} dstOffset Offset into <code>dst</code>.
* @param {Readonly<Uint8Array>} encryptedData Encrypted wrapped Mii QR code data (CFLiWrappedMiiData). Must be at least 112 bytes.
* @param {number} encryptedOffset Offset into <code>encryptedData</code>.
* @param {AesCcmCipher} cipher AES-CCM cipher implementation to use for decryption.
*/
static decrypt(dst, dstOffset, encryptedData, encryptedOffset, cipher) {
const nonce = new Uint8Array(12);
nonce.set(encryptedData.subarray(encryptedOffset, encryptedOffset + 8));
let ciphertextLength = 88;
const decrypted = new Uint8Array(88);
cipher.decryptSkipTag(decrypted, 0, encryptedData, encryptedOffset + 8, ciphertextLength, nonce, 0, 12, 16);
dst.set(decrypted.subarray(0, 12), dstOffset);
dst.set(nonce.subarray(0, 8), dstOffset + 12);
dst.set(decrypted.subarray(12, 12 + ciphertextLength - 12), dstOffset + 12 + 8);
}
/**
* Encrypts 3DS/Wii U Mii StoreData into the wrapped format (CFLiWrappedMiiData)
* for use in a Mii QR code.
*
* <p>Layout of output <code>dst</code> (112 bytes total):
* <ul>
* <li>[0..8) ID / nonce prefix (from storeData[12..20))</li>
* <li>[8..96) ciphertext (88 bytes)</li>
* <li>[96..112) authentication tag (16 bytes)</li>
* </ul>
* @public
* @param {Uint8Array} dst Destination for encrypted wrapped data. Must be 112 bytes.
* @param {number} dstOffset Offset into <code>dst</code>.
* @param {Readonly<Uint8Array>} storeData Input 3DS/Wii U StoreData. Must be <code>StoreDataLength</code> (96) bytes starting at <code>storeDataOffset</code>.
* @param {number} storeDataOffset Offset into <code>storeData</code>.
* @param {AesCcmCipher} cipher AES-CCM cipher implementation to use for encryption.
*/
static encrypt(dst, dstOffset, storeData, storeDataOffset, cipher) {
let idEndOffset = 20;
const nonce = new Uint8Array(12);
nonce.set(storeData.subarray(storeDataOffset + 12, storeDataOffset + 12 + 8));
const content = new Uint8Array(96);
content.set(storeData.subarray(storeDataOffset, storeDataOffset + 12));
content.set(storeData.subarray(storeDataOffset + idEndOffset, storeDataOffset + idEndOffset + 96 - idEndOffset), 12);
const encryptedBytes = new Uint8Array(112);
cipher.encrypt(encryptedBytes, 0, content, 0, 96, nonce, 0, 12, 16);
let correctEncryptedContentLength = 88;
dst.set(nonce.subarray(0, 8), dstOffset);
dst.set(encryptedBytes.subarray(0, correctEncryptedContentLength), dstOffset + 8);
dst.set(encryptedBytes.subarray(96, 112), dstOffset + 96);
}
};
var WrappedStoreDataTest = class _WrappedStoreDataTest {
/**
* @readonly
* @type {Readonly<Uint8Array>}
*/
static _WRAP_KEY_DEVELOPMENT = new Uint8Array([18, 223, 146, 182, 255, 212, 56, 171, 41, 28, 79, 212, 215, 206, 37, 109]);
/**
* @readonly
* @type {Readonly<Uint8Array>}
*/
static _WRAP_TEST_DATA = new Uint8Array([
3,
0,
0,
48,
223,
154,
52,
2,
131,
165,
234,
189,
144,
241,
7,
220,
120,
162,
160,
53,
216,
164,
0,
0,
1,
0,
81,
48,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
64,
64,
0,
0,
12,
1,
4,
104,
67,
24,
32,
52,
70,
20,
129,
18,
23,
104,
13,
0,
0,
41,
0,
82,
72,
80,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
65,
200
]);
/**
* @readonly
* @type {Readonly<Uint8Array>}
*/
static _WRAP_EXPECTED = new Uint8Array([
144,
241,
7,
220,
120,
162,
160,
53,
255,
197,
89,
28,
47,
51,
192,
18,
5,
139,
52,
86,
184,
185,
164,
113,
113,
109,
56,
161,
6,
122,
20,
145,
35,
23,
132,
182,
82,
5,
169,
255,
169,
40,
21,
59,
107,
168,
156,
139,
163,
255,
179,
59,
117,
11,
201,
3,
234,
37,
99,
164,
228,
14,
87,
168,
161,
221,
210,
52,
194,
214,
103,
27,
133,
26,
208,
25,
47,
196,
121,
213,
187,
121,
250,
69,
226,
12,
1,
234,
154,
68,
54,
41,
243,
203,
24,
163,
248,
17,
248,
142,
190,
95,
25,
38,
162,
103,
177,
151,
240,
122,
13,
167
]);
/**
* @param {Readonly<Uint8Array>} a
* @param {Readonly<Uint8Array>} b
* @param {number} size
*/
static _compareByteArraysForTest(a, b, size) {
for (let i = 0; i < size; i++) {
if (a[i] != b[i]) {
console.log("MISMATCH! Array A:");
let hexA = "";
let hexB = "";
for (let i2 = 0; i2 < size; i2++) {
hexA += `${a[i2].toString(16).toUpperCase().padStart(2, "0")} `;
hexB += `${b[i2].toString(16).toUpperCase().padStart(2, "0")} `;
}
console.log(hexA);
console.log(`vs. Array B (mismatching at ${i}):`);
console.log(hexB);
throw new Error(`Bytes are mismatching at position ${i}.`);
}
}
}
/**
* @public
* @param {AesCcmCipher} cipher
*/
static testEncrypt(cipher) {
const wrapped = new Uint8Array(112);
WrappedStoreData.encrypt(wrapped, 0, _WrappedStoreDataTest._WRAP_TEST_DATA, 0, cipher);
_WrappedStoreDataTest._compareByteArraysForTest(wrapped, _WrappedStoreDataTest._WRAP_EXPECTED, 112);
}
/**
* @public
* @param {AesCcmCipher} cipher
*/
static testDecrypt(cipher) {
const unwrapped = new Uint8Array(96);
WrappedStoreData.decrypt(unwrapped, 0, _WrappedStoreDataTest._WRAP_EXPECTED, 0, cipher);
_WrappedStoreDataTest._compareByteArraysForTest(unwrapped, _WrappedStoreDataTest._WRAP_TEST_DATA, 96);
}
/**
* @public
*/
static main() {
const cipher = new NativeAesCcmCipher();
cipher.initialize(_WrappedStoreDataTest._WRAP_KEY_DEVELOPMENT, 0);
_WrappedStoreDataTest.testEncrypt(cipher);
_WrappedStoreDataTest.testDecrypt(cipher);
console.log("Encrypt & decrypt tests passed.");
return 0;
}
};
// MiiImportController.js
var base64ToBytes = (base64) => Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
var hexToBytes = (hex) => Uint8Array.from(
{ length: hex.length >>> 1 },
(_, i) => Number.parseInt(hex.slice(i << 1, (i << 1) + 2), 16)
);
var bytesToHex = (bytes) => Array.prototype.map.call(
bytes,
(x) => x.toString(16).padStart(2, "0")
).join("");
var base64ExToBytes = (base64) => {
base64 = base64.replace(/-/g, "+").replace(/_/g, "/");
while (base64.length % 4 !== 0) {
base64 += "=";
}
return base64ToBytes(base64);
};
var parseHexOrBase64ExToBytes = (text) => {
text = text.replace(/\s+/g, "");
return /^[0-9a-fA-F]+$/.test(text) ? hexToBytes(text) : base64ExToBytes(text);
};
var MiiErrorReason = {
/** Data size does not match any known Mii format. */
UnsupportedSize: 0,
/** Converted studio data failed MiiDecoder.isValid(). */
ValidationFailed: 1,
/** QR code was found but contained no binary payload. */
QrNoBinary: 2,
/** QR code scanner failed while scanning. */
QrScanError: 3,
/** No QR code detected in uploaded image. */
QrNotFound: 4,
/** File could not be read from disk. */
FileReadError: 5
};
var ErrorMessageTable = /* @__PURE__ */ new Map([
[MiiErrorReason.UnsupportedSize, "Data size does not match any known Mii format."],
[MiiErrorReason.ValidationFailed, "Decoded data failed validation check."],
[MiiErrorReason.QrNoBinary, "QR code found but contains no binary data."],
[MiiErrorReason.QrScanError, "Unexpected QR code scanner error. (Camera not present? Permissions not granted?)"],
[MiiErrorReason.QrNotFound, "No QR code detected in uploaded image."],
[MiiErrorReason.FileReadError, "Failed to read the file."]
]);
var ImportSourceType = {
Text: 0,
File: 1,
Qr: 2,
Custom: 3
};
var ImportParsedEvent = class {
/**
* @param {symbol} sourceId - Which source produced this data.
* @param {Uint8Array} rawData - Original input bytes before conversion.
* @param {Uint8Array} studioData - 46-byte converted Studio data.
* @param {string} studioHexUrl - Obfuscated hex string for the Studio image URL.
* @param {MiiExtraInfo} extraInfo - Extra metadata extracted from the source format.
*/
constructor(sourceId, rawData, studioData, studioHexUrl, extraInfo) {
this.sourceId = sourceId;
this.rawData = rawData;
this.studioData = studioData;
this.studioHexUrl = studioHexUrl;
this.extraInfo = extraInfo;
}
/** @returns {string} Full URL to the rendered Mii image. */
getImageUrl() {
return MiiDecoder.URL_BASE_IMAGE_PNG + this.studioHexUrl;
}
};
var ImportErrorEvent = class {
/**
* @param {symbol} sourceId - Which source produced the error.
* @param {MiiErrorReason} reason - Error reason enum value.
* @param {Error} [cause] - Optional underlying error for debugging.
*/
constructor(sourceId, reason, cause) {
this.sourceId = sourceId;
this.reason = reason;
this.cause = cause || null;
}
/**
* Get a default English friendly message for this error.
* Consumers who need localization should use the reason enum
* to look up their own strings instead.
* @returns {string}
*/
getFriendlyMessage() {
return ErrorMessageTable.get(this.reason) || "Unknown error.";
}
};
var ImportSourceChangedEvent = class {
/**
* @param {symbol} activeSourceId - Newly active source.
* @param {symbol | null} previousSourceId - Previously active source, or null.
*/
constructor(activeSourceId, previousSourceId) {
this.activeSourceId = activeSourceId;
this.previousSourceId = previousSourceId;
}
};
var ImportCamerasLoadedEvent = class {
/** @param {Array<{id: string, label: string}>} cameras */
constructor(cameras) {
this.cameras = cameras;
}
};
function buildStudioHexUrl(studioData) {
const obfuscated = new Uint8Array(MiiDecoder.SIZE_STUDIO_URL_DATA);
MiiDecoder.obfuscateStudioUrl(obfuscated, studioData, 0);
return bytesToHex(obfuscated);
}
function decryptIfWrappedStoreData(data) {
if (data.length < WrappedStoreData.LENGTH) {
return data;
}
const cipher = new NativeAesCcmCipher();
cipher.initialize(/* @__PURE__ */ new Uint8Array([
89,
252,
129,
126,
100,
70,
234,
97,
144,
52,
123,
32,
233,
189,
206,
82
]), 0);
const unwrapped = new Uint8Array(WrappedStoreData.STORE_DATA_LENGTH);
WrappedStoreData.decrypt(unwrapped, 0, data, 0, cipher);
return unwrapped;
}
var ListenerRecord = class {
/**
* @param {EventTarget} target
* @param {string} event
* @param {EventListener} handler
* @param {AddEventListenerOptions} [options]
*/
constructor(target, event, handler, options) {
this.target = target;
this.event = event;
this.handler = handler;
this.options = options;
}
/** Remove this listener from its target. */
remove() {
this.target.removeEventListener(this.event, this.handler, this.options);
}
};
var SourceRecord = class {
/**
* @param {symbol} id
* @param {ImportSourceType} type
*/
constructor(id, type) {
this.id = id;
this.type = type;
this.lastRawData = null;
this.lastStudioData = null;
this.lastExtraInfo = null;
this.listeners = [];
this.extra = null;
this._disposeExtra = null;
}
/** Dispose of this source: remove listeners and run type-specific cleanup. */
dispose() {
for (const record of this.listeners) {
record.remove();
}
this.listeners.length = 0;
if (this._disposeExtra) {
this._disposeExtra();
this._disposeExtra = null;
}
}
};
function trackListener(source, target, event, handler, options) {
target.addEventListener(event, handler, options);
source.listeners.push(new ListenerRecord(target, event, handler, options));
}
var MiiImportController = class {
/**
* @type {Map<symbol, SourceRecord>}
* @private
*/
_sources = /* @__PURE__ */ new Map();
/**
* @type {symbol | null}
* @private
*/
_activeSourceId = null;
/**
* @type {Map<string, Set<Function>>}
* @private
*/
_eventHandlers = /* @__PURE__ */ new Map();
/**
* @type {boolean}
* @private
*/
_disposed = false;
// // -------------------------------------------------------
// // Event emitter
// // -------------------------------------------------------
/**
* a
* @typedef {'parsed' | 'error' | 'sourceChanged' | 'scanning' |
* 'scannerStopped' | 'camerasLoaded'} ImportEventType
*/
/**
* s
* @typedef {ImportParsedHandler | ImportErrorHandler | ImportSourceChangedHandler |
* ImportScanningHandler | ImportScannerStoppedHandler} ImportEventHandler
*/
/**
* Register an event handler.
* Supported events:
* - `'parsed'`: {@link ImportParsedHandler}
* - `'error'`: {@link ImportErrorHandler}
* - `'sourceChanged'`: {@link ImportSourceChangedHandler}
* - `'scanning'`: {@link ImportScanningHandler}
* - `'scannerStopped'`: {@link ImportScannerStoppedHandler}
* - `'camerasLoaded'`: {@link ImportCamerasLoadedHandler}
* @param {ImportEventType} event
* @param {ImportEventHandler} handler
* @returns {this} For chaining.
*/
on(event, handler) {
let set = this._eventHandlers.get(event);
if (!set) {
set = /* @__PURE__ */ new Set();
this._eventHandlers.set(event, set);
}
set.add(handler);
return this;
}
/**
* Remove an event handler.
* @param {ImportEventType} event
* @param {ImportEventHandler} handler
* @returns {this}
*/
off(event, handler) {
const handlers = this._eventHandlers.get(event);
if (handlers) {
handlers.delete(handler);
}
return this;
}
/**
* Emit an event to all registered handlers.
* @param {string} event
* @param {Object} [data]
* @private
*/
_emit(event, data) {
const handlers = this._eventHandlers.get(event);
if (!handlers) {
return;
}
for (const handler of handlers) {
try {
handler(data);
} catch (err) {
console.error(`[MiiImportController] Error in '${event}' handler:`, err);
}
}
}
// // -------------------------------------------------------
// // Active source management
// // -------------------------------------------------------
/** @returns {symbol | null} Currently active source ID. */
get activeSourceId() {
return this._activeSourceId;
}
/**
* Switch the active source.
* @param {symbol} id - Source ID returned from an add method.
* @throws {Error} If the source ID is not registered.
*/
setActiveSource(id) {
if (!this._sources.has(id)) {
throw new Error("Unknown source ID");
}
this._switchActive(id);
}
/**
* Get the source record for an ID.
* @param {symbol} id
* @returns {SourceRecord | undefined}
*/
getSource(id) {
return this._sources.get(id);
}
/**
* Internal active-source switching with event emission.
* @param {symbol} newId
* @private
*/
_switchActive(newId) {
const previousId = this._activeSourceId;
if (previousId === newId) {
return;
}
this._activeSourceId = newId;
this._emit("sourceChanged", new ImportSourceChangedEvent(newId, previousId));
}
// // -------------------------------------------------------
// // Source registration: text input
// // -------------------------------------------------------
/**
* Register a text input that accepts hex, base64, or base64url Mii data.
* Automatically parses on input with configurable debounce.
* @param {HTMLInputElement} element - The text input element.
* @param {Object} [opts]
* @param {number} [opts.debounceMs] - Debounce delay in milliseconds. Default 500.
* @returns {symbol} Source ID for this input.
*/
addTextInput(element, opts) {
const debounceMs = opts && opts.debounceMs ? opts.debounceMs : 500;
const id = Symbol("import-source-text");
const source = new SourceRecord(id, ImportSourceType.Text);
source.extra = { debounceTimer: 0 };
source._disposeExtra = () => {
clearTimeout(source.extra.debounceTimer);
};
const onInput = () => {
clearTimeout(source.extra.debounceTimer);
source.extra.debounceTimer = setTimeout(() => {
const value = element.value.trim();
if (value.length === 0) {
return;
}
this._switchActive(id);
const data = parseHexOrBase64ExToBytes(value);
this.submitData(id, data);
}, debounceMs);
};
trackListener(source, element, "input", onInput);
this._sources.set(id, source);
return id;
}
// // -------------------------------------------------------
// // Source registration: file input
// // -------------------------------------------------------
/**
* Register a file input that reads Mii data files.
* Supports any binary file whose size matches a known Mii format.
* @param {HTMLInputElement} element - File input element (type="file").
* @returns {symbol} Source ID for this input.
*/
addFileInput(element) {
const id = Symbol("import-source-file");
const source = new SourceRecord(id, ImportSourceType.File);
const onChange = async () => {
if (!element.files || !element.files.length) {
return;
}
const file = element.files[0];
this._switchActive(id);
const reader = new FileReader();
reader.onload = () => {
const data = new Uint8Array(
/** @type {ArrayBuffer} */
reader.result
);
this.submitData(id, data);
};
reader.onerror = () => {
const e = reader.error;
this._emit("error", new ImportErrorEvent(
id,
MiiErrorReason.FileReadError,
e instanceof Error ? e : new Error(String(e))
));
};
reader.readAsArrayBuffer(file);
};
trackListener(source, element, "change", onChange);
this._sources.set(id, source);
return id;
}
// // -------------------------------------------------------
// // Source registration: QR scanner
// // -------------------------------------------------------
/**
* Register a QR code scanner source.
* The controller creates the QrScanner instance from the imported module.
* @param {Object} opts
* @param {HTMLVideoElement} opts.videoElement - Video element for camera feed.
* @param {HTMLElement} opts.startButton - Element that starts scanning on click.
* @param {HTMLElement} [opts.stopButton] - Element that stops scanning on click.
* @param {HTMLElement} opts.containerElement - Toggled visible/hidden by the controller.
* @param {HTMLInputElement} [opts.fileInput] - Optional file input for QR image scanning.
* @param {HTMLSelectElement} [opts.cameraSelect] - Select element the controller populates with available cameras.
* @param {Object} [opts.scannerOptions] - Extra options forwarded to QrScanner constructor.
* @returns {symbol} Source ID for this scanner.
*/
addQrScanner(opts) {
const {
videoElement,
startButton,
stopButton,
containerElement,
fileInput,
cameraSelect,
scannerOptions
} = opts;
import_qr_scanner.default.setBarcodeDetectorDisabled !== void 0 && import_qr_scanner.default.setBarcodeDetectorDisabled();
const id = Symbol("import-source-qr");
const source = new SourceRecord(id, ImportSourceType.Qr);
source.extra = { scanner: null, scanning: false };
const onDecode = (result) => {
const bytes = result.binaryData;
if (!bytes || bytes.length <= 0) {
this._emit("error", new ImportErrorEvent(id, MiiErrorReason.QrNoBinary));
return;
}
this._switchActive(id);
this.submitData(id, new Uint8Array(bytes));
stopScanning();
};
const scanner = new import_qr_scanner.default(videoElement, onDecode, {
// @ts-ignore -- This property may have been removed.
returnDetailedScanResult: true,
highlightScanRegion: true,
highlightCodeOutline: true,
...scannerOptions,
onDecodeError: function(err) {
if (typeof err === "string" && err.indexOf("No QR code found") !== -1) {
return;
}
scannerOptions?.onDecodeError?.(err);
}
});
source.extra.scanner = scanner;
const startScanning = () => {
containerElement.style.display = "";
source.extra.scanning = true;
this._emit("scanning");
scanner.start().then(() => {
import_qr_scanner.default.listCameras(true).then((cameras) => {
if (cameraSelect) {
const existing = cameraSelect.getElementsByClassName("device-camera");
for (const el of existing) {
el.remove();
}
for (const camera of cameras) {
const option = document.createElement("option");
option.value = camera.id;
option.text = camera.label;
option.className = "device-camera";
cameraSelect.add(option);
}
}
this._emit("camerasLoaded", new ImportCamerasLoadedEvent(cameras));
});
}).catch((error) => {
this._emit("error", new ImportErrorEvent(
id,
MiiErrorReason.QrScanError,
error instanceof Error ? error : new Error(String(error))
));
});
};
const stopScanning = () => {
scanner.stop();
containerElement.style.display = "none";
source.extra.scanning = false;
this._emit("scannerStopped");
};
if (cameraSelect) {
trackListener(source, cameraSelect, "change", () => {
scanner.setCamera(cameraSelect.value);
});
}
trackListener(source, startButton, "click", startScanning);
if (stopButton) {
trackListener(source, stopButton, "click", stopScanning);
}
if (fileInput) {
const onFileChange = () => {
if (!fileInput.files || !fileInput.files.length) {
return;
}
const file = fileInput.files[0];
this._switchActive(id);
import_qr_scanner.default.scanImage(
file,
// @ts-ignore -- This property may have been removed.
{ returnDetailedScanResult: true }
).then(onDecode).catch(() => {
this._emit("error", new ImportErrorEvent(id, MiiErrorReason.QrNotFound));
});
};
trackListener(source, fileInput, "change", onFileChange);
}
containerElement.style.display = "none";
source._disposeExtra = () => {
try {
scanner.stop();
scanner.destroy();
} catch (e) {
}
};
this._sources.set(id, source);
return id;
}
// // -------------------------------------------------------
// // Source registration: custom source
// // -------------------------------------------------------
/**
* Register a custom source that participates in active-source switching.
* The consumer is responsible for obtaining data and calling
* {@link submitData} when ready.
* @returns {symbol} Source ID.
*/
addCustomSource() {
const id = Symbol("import-source-custom");
const source = new SourceRecord(id, ImportSourceType.Custom);
this._sources.set(id, source);
return id;
}
// // -------------------------------------------------------
// // Universal data submission
// // -------------------------------------------------------
/**
* Submit raw Mii data from any source for decoding.
* Built-in sources call this internally; custom sources call it directly.
* On success, fires a 'parsed' event. On failure, fires an 'error' event.
* @param {symbol} sourceId - Source ID that produced this data.
* @param {Uint8Array} data - Raw Mii data bytes.
* @throws {Error} Throws if the source symbol ID does not exist.
*/
submitData(sourceId, data) {
if (this._disposed) {
return;
}
const source = this._sources.get(sourceId);
if (!source) {
throw new Error("Unknown source ID");
}
data = decryptIfWrappedStoreData(data);
const studioData = new Uint8Array(MiiDecoder.SIZE_STUDIO_RAW_DATA);
const extraInfo = new MiiExtraInfo();
const decoded = MiiDecoder.fromAnyMiiDataExtra(
studioData,
extraInfo,
data.length,
data
);
if (!decoded) {
source.lastRawData = null;
source.lastStudioData = null;
source.lastExtraInfo = null;
this._emit("error", new ImportErrorEvent(
sourceId,
MiiErrorReason.UnsupportedSize
));
return;
}
if (!MiiDecoder.isValid(studioData)) {
source.lastRawData = data;
source.lastStudioData = null;
source.lastExtraInfo = null;
this._emit("error", new ImportErrorEvent(
sourceId,
MiiErrorReason.ValidationFailed
));
return;
}
const studioHexUrl = buildStudioHexUrl(studioData);
source.lastRawData = data;
source.lastStudioData = studioData;
source.lastExtraInfo = extraInfo;
this._emit("parsed", new ImportParsedEvent(
sourceId,
data,
studioData,
studioHexUrl,
extraInfo
));
}
// // -------------------------------------------------------
// // Cleanup
// // -------------------------------------------------------
/**
* Dispose of the controller.
* Delegates cleanup to each source record's own dispose method.
* The controller is unusable after this call.
*/
dispose() {
this._disposed = true;
for (const source of this._sources.values()) {
source.dispose();
}
this._sources.clear();
this._eventHandlers.clear();
this._activeSourceId = null;
}
};
export {
ImportCamerasLoadedEvent,
ImportErrorEvent,
ImportParsedEvent,
ImportSourceChangedEvent,
ImportSourceType,
MiiErrorReason,
MiiImportController
};
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MiiImportController — Test</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, sans-serif;
background: #1a1a2e;
color: #eee;
padding: 1.5rem;
max-width: 640px;
margin: 0 auto;
}
h1 { font-size: 1.1rem; margin-bottom: 1rem; }
/* Source rows — the entire row is clickable. */
.source-row {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
margin-bottom: 0.35rem;
background: #16213e;
border: 2px solid #333;
border-radius: 6px;
cursor: pointer;
opacity: 0.5;
outline: none;
}
.source-row:hover, .source-row:focus-visible { opacity: 0.75; }
.source-row.active { opacity: 1; }
/* Border state colors — only visible when active. */
.source-row.active.state-loaded { border-color: #53d769; }
.source-row.active.state-error { border-color: #ff6b6b; }
.source-row.active.state-idle { border-color: #666; }
.source-row label {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
color: #888;
min-width: 2.5rem;
cursor: pointer;
}
.source-row .source-input { flex: 1; min-width: 0; }
.source-row input[type="text"],
.source-row input[type="file"] {
width: 100%;
padding: 0.3rem 0.5rem;
background: #0f3460;
border: 1px solid #333;
border-radius: 3px;
color: #eee;
font-size: 0.85rem;
font-family: monospace;
}
.source-row input:disabled { opacity: 0.3; cursor: default; }
.source-row input[type="radio"] { accent-color: #53d769; }
.loaded-tag {
display: none;
font-size: 0.7rem;
white-space: nowrap;
}
.state-loaded .loaded-tag { color: #53d769; }
.state-error .loaded-tag { color: #ff6b6b; }
/* QR buttons and select. */
.qr-controls { display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap; }
#qr-start-btn, #qr-stop-btn {
padding: 0.3rem 0.7rem;
border: 1px solid #333;
border-radius: 3px;
cursor: pointer;
font-size: 0.85rem;
}
#qr-start-btn { background: #0f3460; color: #eee; }
#qr-stop-btn { background: #e94560; color: #fff; border: none; }
#cam-list {
padding: 0.2rem;
background: #0f3460;
color: #eee;
border: 1px solid #333;
border-radius: 3px;
font-size: 0.85rem;
max-width: 120px;
}
.qr-upload-label { font-size: 0.8rem; color: #888; }
#qr-file-input { font-size: 0.8rem; max-width: 160px; }
/* QR container — no CSS display rule; JS controls visibility. */
#qr-container {
margin: 0.5rem 0;
padding: 0.75rem;
background: #16213e;
border-radius: 6px;
text-align: center;
}
#qr-container .qr-title {
font-size: 0.8rem;
color: #888;
margin-bottom: 0.4rem;
}
#qr-container video {
width: 100%;
max-width: 320px;
border-radius: 3px;
display: block;
margin: 0 auto;
}
/* Status. */
#status-bar { margin-top: 0.5rem; font-size: 0.8rem; min-height: 1.2em; }
.status-error { color: #ff6b6b; }
.status-ok { color: #53d769; }
.status-info { color: #5bc0de; }
/* Result panel. */
#result-panel {
margin-top: 1rem;
padding: 0.75rem;
background: #16213e;
border-radius: 6px;
}
.result-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; }
#result-image { width: 96px; height: 96px; border-radius: 6px; background: #fff; flex-shrink: 0; }
.result-name { font-size: 1.2rem; font-weight: 700; }
.result-source { font-size: 0.7rem; color: #888; }
.result-props {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 0.2rem 0.75rem;
font-size: 0.8rem;
}
.prop-row { display: flex; gap: 0.2rem; }
.prop-row dt { color: #888; }
.prop-row dd { font-weight: 600; }
.hex-preview {
margin-top: 0.5rem;
padding: 0.4rem;
background: #0f3460;
border-radius: 3px;
font-family: monospace;
font-size: 0.7rem;
word-break: break-all;
color: #888;
max-height: 3rem;
overflow-y: auto;
}
</style>
</head>
<body>
<h1>MiiImportController Test</h1>
<!-- Source: Text -->
<div class="source-row state-idle" id="row-text" tabindex="0">
<input type="radio" name="active-source" id="radio-text" tabindex="-1">
<label for="radio-text">Text</label>
<div class="source-input">
<input type="text" id="text-input"
placeholder="Hex or Base64 Mii data"
value="AwBgMIJUICvpzY4vnWYVrXy7ikd01AAAWR1KAGEAcwBtAGkAbgBlAAAAAAAAABw3EhB7ASFuQxwNZMcYAAgegg0AMEGzW4JtAAAAAAAAAAAAAAAAAAAAAAAAAAAAAML0">
</div>
<span class="loaded-tag" id="loaded-text"></span>
</div>
<!-- Source: File -->
<div class="source-row state-idle" id="row-file" tabindex="0">
<input type="radio" name="active-source" id="radio-file" tabindex="-1">
<label for="radio-file">File</label>
<div class="source-input">
<input type="file" id="file-input" accept=".cfsd,.ffsd,.3dsmii,.rcd,.miigx,.mii,.mae,.rsd,.charinfo,.nfcd,.nfsd,.mnms,*/*">
</div>
<span class="loaded-tag" id="loaded-file"></span>
</div>
<!-- Source: QR -->
<div class="source-row state-idle" id="row-qr" tabindex="0">
<input type="radio" name="active-source" id="radio-qr" tabindex="-1">
<label for="radio-qr">QR</label>
<div class="source-input">
<div class="qr-controls">
<button id="qr-start-btn" type="button">Scan</button>
<button id="qr-stop-btn" type="button">Stop</button>
<select id="cam-list">
<option value="environment" selected>Back Camera</option>
<option value="user">Front Camera</option>
</select>
<span class="qr-upload-label">or upload QR image:</span>
<input type="file" id="qr-file-input" accept="image/*">
</div>
</div>
<span class="loaded-tag" id="loaded-qr"></span>
</div>
<!-- QR camera container. -->
<div id="qr-container">
<div class="qr-title">QR Code Scanner</div>
<video id="qr-video" muted playsinline></video>
</div>
<div id="status-bar"></div>
<!-- Result panel. -->
<div id="result-panel" style="display: none;">
<div class="result-header">
<img id="result-image" src="" alt="">
<div>
<div class="result-name" id="result-name"></div>
<div class="result-source" id="result-source"></div>
</div>
</div>
<div class="result-props" id="result-props"></div>
<div class="hex-preview" id="hex-preview"></div>
</div>
<!--
<script type="importmap">
{
"imports": {
"qr-scanner": "https://esm.sh/@getify-as-is/qr-scanner@0.20251225.0"
}
}
</script>
-->
<script type="importmap">{"imports": {
"./MiiImportController.js": "./MiiImportController.esm.js"
}}</script>
<script type="module" src="MiiImportTest.js"></script>
</body>
</html>
// @ts-check
import {
MiiImportController,
ImportParsedEvent,
ImportErrorEvent,
ImportSourceChangedEvent
} from './MiiImportController.js';
// ---- Helpers ----
const elementByIdOrThrow = (/** @type {string} */ id) => {
const el = document.getElementById(id);
if (!el) {
throw new Error('Element not found: ' + id);
}
return el;
};
// ---- DOM references ----
const textInput = /** @type {HTMLInputElement} */ (elementByIdOrThrow('text-input'));
const fileInput = /** @type {HTMLInputElement} */ (elementByIdOrThrow('file-input'));
const qrStartBtn = elementByIdOrThrow('qr-start-btn');
const qrStopBtn = elementByIdOrThrow('qr-stop-btn');
const qrVideo = /** @type {HTMLVideoElement} */ (elementByIdOrThrow('qr-video'));
const qrContainer = elementByIdOrThrow('qr-container');
const qrFileInput = /** @type {HTMLInputElement} */ (elementByIdOrThrow('qr-file-input'));
const camList = /** @type {HTMLSelectElement} */ (elementByIdOrThrow('cam-list'));
const statusBar = elementByIdOrThrow('status-bar');
const resultPanel = elementByIdOrThrow('result-panel');
const resultImage = /** @type {HTMLImageElement} */ (elementByIdOrThrow('result-image'));
const resultName = elementByIdOrThrow('result-name');
const resultSource = elementByIdOrThrow('result-source');
const resultProps = elementByIdOrThrow('result-props');
const hexPreview = elementByIdOrThrow('hex-preview');
const radioText = /** @type {HTMLInputElement} */ (elementByIdOrThrow('radio-text'));
const radioFile = /** @type {HTMLInputElement} */ (elementByIdOrThrow('radio-file'));
const radioQr = /** @type {HTMLInputElement} */ (elementByIdOrThrow('radio-qr'));
const rowText = elementByIdOrThrow('row-text');
const rowFile = elementByIdOrThrow('row-file');
const rowQr = elementByIdOrThrow('row-qr');
const loadedText = elementByIdOrThrow('loaded-text');
const loadedFile = elementByIdOrThrow('loaded-file');
const loadedQr = elementByIdOrThrow('loaded-qr');
// ---- Controller setup ----
const controller = new MiiImportController();
const textSourceId = controller.addTextInput(textInput, { debounceMs: 400 });
const fileSourceId = controller.addFileInput(fileInput);
const qrSourceId = controller.addQrScanner({
videoElement: qrVideo,
startButton: qrStartBtn,
stopButton: qrStopBtn,
containerElement: qrContainer,
fileInput: qrFileInput,
cameraSelect: camList
});
// ---- Per-source UI config ----
/**
* @typedef {Object} SourceUiEntry
* @property {HTMLElement} row
* @property {HTMLInputElement} radio
* @property {HTMLInputElement | null} input - The main input to disable/enable.
* @property {HTMLElement} loadedIndicator
* @property {string} label - Human-readable source name.
*/
/** @type {Map<symbol, SourceUiEntry>} */
const sourceUiMap = new Map([
[textSourceId, { row: rowText, radio: radioText, input: textInput, loadedIndicator: loadedText, label: 'Text Input' }],
[fileSourceId, { row: rowFile, radio: radioFile, input: fileInput, loadedIndicator: loadedFile, label: 'File Input' }],
[qrSourceId, { row: rowQr, radio: radioQr, input: null, loadedIndicator: loadedQr, label: 'QR Scanner' }]
]);
/** @type {Map<symbol, ImportParsedEvent>} Per-source last successful parse. */
const sourceResults = new Map();
/**
* Per-source state for border coloring.
* @type {Map<symbol, 'idle' | 'loaded' | 'error'>}
*/
const sourceState = new Map();
for (const id of sourceUiMap.keys()) {
sourceState.set(id, 'idle');
}
// ---- Row click activates source ----
// Clicking anywhere on the row (not just the radio) switches source.
for (const [id, ui] of sourceUiMap) {
// Click anywhere on the row to activate this source.
ui.row.addEventListener('click', (e) => {
const target = /** @type {HTMLElement} */ (e.target);
// Don't hijack clicks on functional inputs (text box, file picker, buttons).
if (target === ui.input || target.tagName === 'BUTTON') {
return;
}
controller.setActiveSource(id);
});
// Keyboard activation: Enter or Space on focused row.
ui.row.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
controller.setActiveSource(id);
}
});
}
// Radio change handlers (for native radio keyboard nav).
radioText.addEventListener('change', () => controller.setActiveSource(textSourceId));
radioFile.addEventListener('change', () => controller.setActiveSource(fileSourceId));
radioQr.addEventListener('change', () => controller.setActiveSource(qrSourceId));
// ---- Source changed: update row states ----
controller.on('sourceChanged', (/** @type {ImportSourceChangedEvent} */ evt) => {
for (const [id, ui] of sourceUiMap) {
const isActive = id === evt.activeSourceId;
ui.row.classList.toggle('active', isActive);
ui.radio.checked = isActive;
// Enable/disable the main input.
if (ui.input) {
ui.input.disabled = !isActive;
}
// Update border color class based on state.
updateRowBorder(id);
}
// Switch displayed result to the newly active source if it has one.
const storedResult = sourceResults.get(evt.activeSourceId);
if (storedResult) {
displayResult(storedResult);
} else {
// No result for this source — clear the display.
resultPanel.style.display = 'none';
// if (sourceState.get(evt.activeSourceId) === 'idle') {
// clearStatus();
// }
}
// Always clear the status, good or bad, after switching.
clearStatus();
});
// ---- Parsed: store and show result ----
controller.on('parsed', (/** @type {ImportParsedEvent} */ evt) => {
// Store per-source.
sourceResults.set(evt.sourceId, evt);
sourceState.set(evt.sourceId, 'loaded');
// Update loaded indicator on the source row.
const ui = sourceUiMap.get(evt.sourceId);
if (ui) {
const name = evt.extraInfo.getNickname();
ui.loadedIndicator.textContent = '\u2714 ' + (name || evt.rawData.length + ' bytes');
ui.loadedIndicator.style.display = '';
updateRowBorder(evt.sourceId);
}
// Display result if this is the active source.
if (evt.sourceId === controller.activeSourceId) {
setStatus('ok', 'Decoded ' + evt.rawData.length + ' bytes.');
displayResult(evt);
}
});
/** Render a parsed result into the result panel. */
function displayResult(/** @type {ImportParsedEvent} */ evt) {
resultImage.src = evt.getImageUrl() + '&type=face&width=128&bgColor=FFFFFFFF';
resultImage.alt = 'Mii face render';
const nickname = evt.extraInfo.getNickname();
resultName.textContent = nickname || '(no name)';
// Identify the source for display.
const sourceUi = sourceUiMap.get(evt.sourceId);
const sourceName = (sourceUi ? sourceUi.label : '?');
resultSource.textContent = 'Source: ' + sourceName +
' \u00B7 ' + evt.rawData.length + ' bytes';
// Build property rows.
const extra = evt.extraInfo;
/** @type {Array<Array<string>>} */
const props = [];
const creatorName = extra.getCreatorName();
if (nickname) {
props.push(['Nickname', nickname]);
}
if (creatorName) {
props.push(['Creator', creatorName]);
}
if (extra.height !== undefined) {
props.push(['Height', String(extra.height)]);
}
if (extra.build !== undefined) {
props.push(['Build', String(extra.build)]);
}
if (extra.skinColor !== undefined) {
props.push(['Skin Color', String(extra.skinColor)]);
}
if (extra.gender !== undefined) {
props.push(['Gender', extra.gender === 0 ? 'Male' : 'Female']);
}
if (extra.favoriteColor !== undefined) {
props.push(['Favorite Color', String(extra.favoriteColor)]);
}
if (extra.isNx !== undefined) {
props.push(['Is NX', String(extra.isNx)]);
}
if (extra.birthMonth) {
props.push(['Birth Month', String(extra.birthMonth)]);
}
if (extra.birthDay) {
props.push(['Birth Day', String(extra.birthDay)]);
}
if (extra.favorite !== undefined) {
props.push(['Favorite', String(extra.favorite)]);
}
if (extra.copyable !== undefined) {
props.push(['Copyable', String(extra.copyable)]);
}
resultProps.innerHTML = props.map(([key, val]) =>
`<div class="prop-row"><dt>${key}:</dt><dd>${val}</dd></div>`
).join('');
// Hex preview of raw data.
const rawHex = Array.from(evt.rawData)
.map(b => b.toString(16).padStart(2, '0'))
.join(' ');
hexPreview.textContent = rawHex;
resultPanel.style.display = '';
}
// ---- Error: update state and status ----
controller.on('error', (/** @type {ImportErrorEvent} */ evt) => {
sourceState.set(evt.sourceId, 'error');
const ui = sourceUiMap.get(evt.sourceId);
if (ui) {
ui.loadedIndicator.textContent = '\u2718';
ui.loadedIndicator.style.display = '';
updateRowBorder(evt.sourceId);
}
if (evt.sourceId === controller.activeSourceId) {
// Map reason to a display string. This is the test harness —
// a real consumer would use their own localized elements.
setStatus('error', evt.getFriendlyMessage());
}
});
// ---- Scanning state ----
controller.on('scanning', () => {
setStatus('info', 'QR scanner active — point camera at a Mii QR code.');
// Show stop, hide start, show camera list.
qrStopBtn.style.display = '';
qrStartBtn.style.display = 'none';
camList.style.display = '';
});
controller.on('scannerStopped', () => {
// Restore start button, hide camera selection and camera list.
qrStopBtn.style.display = 'none';
qrStartBtn.style.display = '';
camList.style.display = 'none';
// Clear the scanning status if it was still showing.
if (statusBar.classList.contains('status-info')) {
clearStatus();
}
});
// ---- Utilities ----
/**
* Update the border color class on a source row.
* @param {symbol} id
*/
function updateRowBorder(id) {
const ui = sourceUiMap.get(id);
if (!ui) {
return;
}
const state = sourceState.get(id);
ui.row.classList.remove('state-loaded', 'state-error', 'state-idle');
if (state === 'loaded') {
ui.row.classList.add('state-loaded');
} else if (state === 'error') {
ui.row.classList.add('state-error');
} else {
ui.row.classList.add('state-idle');
}
}
/**
* Set the status bar text and style class.
* @param {'ok' | 'error' | 'info'} type
* @param {string} message
*/
function setStatus(type, message) {
statusBar.className = 'status-' + type;
statusBar.textContent = message;
}
/** Clear the status bar. */
function clearStatus() {
statusBar.className = '';
statusBar.textContent = '';
}
// ---- Initial state ----
// Hide camera list and stop button initially.
camList.style.display = 'none';
qrStopBtn.style.display = 'none';
// If the text input has a default value, fire input to kick off the debounced parse.
if (textInput.value.trim().length > 0) {
textInput.dispatchEvent(new Event('input'));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment