mirror of
https://github.com/AndrioCelos/TableturfBattleApp.git
synced 2026-04-24 23:36:53 -05:00
Add import from screenshot and import/export multiple decks at once
This commit is contained in:
parent
7cc4c35b37
commit
e417b148c6
|
|
@ -371,6 +371,7 @@
|
|||
<section id="deckEditordeckListPage">
|
||||
<a id="deckListBackButton" href=".">Back</a>
|
||||
<h3>Deck list</h3>
|
||||
<button id="deckExportAllButton">Export all</button>
|
||||
<div id="deckList" class="deckList">
|
||||
<div id="addDeckControls">
|
||||
<button id="newDeckButton">New deck</button>
|
||||
|
|
@ -395,12 +396,53 @@
|
|||
</section>
|
||||
<dialog id="deckImportDialog">
|
||||
<form id="deckImportForm" method="dialog">
|
||||
<textarea id="deckImportTextBox" rows="24" cols="45"></textarea>
|
||||
<div id="deckImportErrorBox" class="error" hidden></div>
|
||||
<div>
|
||||
<label for="deckImportTextButton">
|
||||
<input type="radio" id="deckImportTextButton" name="deckImportMethod"/>
|
||||
Import from a Tableturf Battle simulator
|
||||
</label>
|
||||
<section id="deckImportTextSection">
|
||||
<textarea id="deckImportTextBox" rows="4" cols="45" autocomplete="off" placeholder="Paste the export string here."></textarea>
|
||||
<button type="submit" id="deckImportOkButton">OK</button>
|
||||
<button type="submit" id="deckImportCancelButton">Cancel</button>
|
||||
</div>
|
||||
</section>
|
||||
<label for="deckImportScreenshotButton">
|
||||
<input type="radio" id="deckImportScreenshotButton" name="deckImportMethod"/>
|
||||
Import from screenshots
|
||||
</label>
|
||||
<section id="deckImportScreenshotSection">
|
||||
<input type="file" id="deckImportFileBox" accept="image/png,image/jpeg,image/webp,image/bmp" multiple autocomplete="off"/>
|
||||
<div>
|
||||
<button type="button" id="deckImportScreenshotInstructionsButton">Show instructions</button>
|
||||
<div id="deckImportScreenshotInstructions" hidden>
|
||||
<ol>
|
||||
<li>In <em>Splatoon 3</em>, open the main menu with X, select the Status tab, the Tableturf Battle section, then Card List.</li>
|
||||
<li>Enter the Edit Deck menu with Y.</li>
|
||||
<li>Move the cursor to the deck you want to import.</li>
|
||||
<li>Take a screenshot using the capture button on your controller.<br/>If you have an external capture device, you can alternatively take a screenshot that way.</li>
|
||||
<li>Repeat steps 3 and 4 for each deck you want to import.</li>
|
||||
</ol>
|
||||
<label for="deckImportScreenshotInstructionsButtonPC">
|
||||
<input type="radio" id="deckImportScreenshotInstructionsButtonPC" name="deckImportScreenshotMethod"/>
|
||||
Download screenshots using a PC
|
||||
</label>
|
||||
<label for="deckImportScreenshotInstructionsButtonMobile">
|
||||
<input type="radio" id="deckImportScreenshotInstructionsButtonMobile" name="deckImportScreenshotMethod"/>
|
||||
Download screenshots using a mobile device
|
||||
</label>
|
||||
<ol id="deckImportScreenshotInstructionsPC" start="6" hidden>
|
||||
<li>Go to System Settings, then select Data Management, then Manage Screenshots and Videos, then Copy to PC via USB Connection.</li>
|
||||
<li>Connect a USB cable to your PC and the USB-C port on the bottom of your Nintendo Switch.</li>
|
||||
<li>The Nintendo Switch Album is now accessible in This PC or Devices in your file manager. Click Browse below and select the screenshots you created.</li>
|
||||
</ol>
|
||||
<ol id="deckImportScreenshotInstructionsMobile" start="6" hidden>
|
||||
<li>In the Album, select one of the screenshots you created.</li>
|
||||
<li>Enter the Editing and Posting menu with A, then select Send to Smart Device.</li>
|
||||
<li>Follow the prompts to download the screenshots on your mobile device.<br/>On Android devices, it may be necessary to turn off mobile data.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div id="deckImportErrorBox" class="error" hidden></div>
|
||||
<button type="submit" id="deckImportCancelButton">Cancel</button>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="deckExportDialog">
|
||||
|
|
|
|||
|
|
@ -19,13 +19,26 @@ const deckExportDialog = document.getElementById('deckExportDialog') as HTMLDial
|
|||
const deckExportCopyButton = document.getElementById('deckExportCopyButton') as HTMLButtonElement;
|
||||
const deckExportTextBox = document.getElementById('deckExportTextBox') as HTMLTextAreaElement;
|
||||
|
||||
const deckExportAllButton = document.getElementById('deckExportAllButton') as HTMLButtonElement;
|
||||
|
||||
const deckImportDialog = document.getElementById('deckImportDialog') as HTMLDialogElement;
|
||||
const deckImportForm = document.getElementById('deckImportForm') as HTMLFormElement;
|
||||
const deckImportTextBox = document.getElementById('deckImportTextBox') as HTMLTextAreaElement;
|
||||
const deckImportTextButton = document.getElementById('deckImportTextButton') as HTMLInputElement;
|
||||
const deckImportTextSection = document.getElementById('deckImportTextSection')!;
|
||||
const deckImportScreenshotButton = document.getElementById('deckImportScreenshotButton') as HTMLInputElement;
|
||||
const deckImportScreenshotSection = document.getElementById('deckImportScreenshotSection')!;
|
||||
const deckImportScreenshotInstructionsButton = document.getElementById('deckImportScreenshotInstructionsButton') as HTMLButtonElement;
|
||||
const deckImportScreenshotInstructionsButtonPC = document.getElementById('deckImportScreenshotInstructionsButtonPC') as HTMLInputElement;
|
||||
const deckImportScreenshotInstructionsButtonMobile = document.getElementById('deckImportScreenshotInstructionsButtonMobile') as HTMLInputElement;
|
||||
const deckImportScreenshotInstructions = document.getElementById('deckImportScreenshotInstructions')!;
|
||||
const deckImportScreenshotInstructionsPC = document.getElementById('deckImportScreenshotInstructionsPC')!;
|
||||
const deckImportScreenshotInstructionsMobile = document.getElementById('deckImportScreenshotInstructionsMobile')!;
|
||||
const deckImportFileBox = document.getElementById('deckImportFileBox') as HTMLInputElement;
|
||||
const deckImportErrorBox = document.getElementById('deckImportErrorBox')!;
|
||||
const deckImportOkButton = document.getElementById('deckImportOkButton') as HTMLButtonElement;
|
||||
|
||||
const deckButtons = new CheckButtonGroup<number>();
|
||||
const deckButtons = new CheckButtonGroup<Deck>();
|
||||
|
||||
function showDeckList() {
|
||||
showPage('deckList');
|
||||
|
|
@ -84,16 +97,39 @@ function saveDecks() {
|
|||
function createDeckButton(index: number, deck: Deck) {
|
||||
const buttonElement = document.createElement('button');
|
||||
buttonElement.type = 'button';
|
||||
buttonElement.dataset.index = index.toString();
|
||||
deckButtons.add(new CheckButton(buttonElement), index);
|
||||
const button = new CheckButton(buttonElement);
|
||||
deckButtons.add(button, deck);
|
||||
buttonElement.addEventListener('click', () => {
|
||||
selectedDeck = decks[deckButtons.value!];
|
||||
selectedDeck = deckButtons.value;
|
||||
selectDeck();
|
||||
});
|
||||
buttonElement.innerText = deck.name;
|
||||
|
||||
deckList.insertBefore(buttonElement, addDeckControls);
|
||||
return buttonElement;
|
||||
return button;
|
||||
}
|
||||
|
||||
function importDecks(decksToImport: (Deck | number[])[]) {
|
||||
let newSelectedDeck: Deck | null = null;
|
||||
for (const el of decksToImport) {
|
||||
let deck;
|
||||
if (el instanceof Array)
|
||||
deck = new Deck(`Imported Deck ${decks.length + 1}`, el, false);
|
||||
else {
|
||||
deck = el;
|
||||
if (!deck.name) deck.name = `Imported Deck ${decks.length + 1}`;
|
||||
}
|
||||
createDeckButton(decks.length, deck);
|
||||
decks.push(deck);
|
||||
newSelectedDeck ??= deck;
|
||||
}
|
||||
if (newSelectedDeck) {
|
||||
selectedDeck = newSelectedDeck;
|
||||
deckButtons.deselect();
|
||||
deckButtons.entries.find(e => e.value == newSelectedDeck)!.button.checked = true;
|
||||
selectDeck();
|
||||
saveDecks();
|
||||
}
|
||||
}
|
||||
|
||||
newDeckButton.addEventListener('click', () => {
|
||||
|
|
@ -104,17 +140,28 @@ newDeckButton.addEventListener('click', () => {
|
|||
});
|
||||
importDeckButton.addEventListener('click', () => {
|
||||
deckImportErrorBox.hidden = true;
|
||||
deckImportTextSection.hidden = true;
|
||||
deckImportScreenshotSection.hidden = true;
|
||||
deckImportScreenshotInstructions.hidden = true;
|
||||
deckImportScreenshotInstructionsPC.hidden = true;
|
||||
deckImportScreenshotInstructionsMobile.hidden = true;
|
||||
deckImportTextButton.checked = false;
|
||||
deckImportScreenshotButton.checked = false;
|
||||
deckImportScreenshotInstructionsButtonPC.checked = false;
|
||||
deckImportScreenshotInstructionsButtonMobile.checked = false;
|
||||
deckImportScreenshotInstructionsButton.innerText = 'Show instructions';
|
||||
deckImportDialog.showModal();
|
||||
});
|
||||
deckImportForm.addEventListener('submit', e => {
|
||||
if (e.submitter == deckImportOkButton) {
|
||||
try {
|
||||
const deck = JSON.parse(deckImportTextBox.value) as Deck;
|
||||
if (typeof(deck) != 'object' || !Array.isArray(deck.cards) || deck.cards.length != 15 || deck.cards.find(i => i < 0 || i > cardDatabase.cards!.length))
|
||||
throw new SyntaxError('Invalid JSON deck');
|
||||
if (!deck.name) deck.name = `Deck ${decks.length + 1}`;
|
||||
createDeckButton(decks.length, deck);
|
||||
decks.push(deck);
|
||||
const data = JSON.parse(deckImportTextBox.value);
|
||||
const decks = (data instanceof Array ? data : [ data ]) as Deck[];
|
||||
for (const deck of decks) {
|
||||
if (typeof(deck) != 'object' || !Array.isArray(deck.cards) || deck.cards.length != 15 || deck.cards.find(i => i < 0 || i > cardDatabase.cards!.length))
|
||||
throw new SyntaxError('Invalid JSON deck');
|
||||
}
|
||||
importDecks(decks);
|
||||
} catch (ex: any) {
|
||||
e.preventDefault();
|
||||
deckImportErrorBox.innerText = ex.message;
|
||||
|
|
@ -167,13 +214,12 @@ function deselectDeck() {
|
|||
|
||||
deckExportButton.addEventListener('click', () => {
|
||||
if (selectedDeck == null) return;
|
||||
const json = JSON.stringify(selectedDeck, [ 'name', 'cards' ], '\t');
|
||||
const json = JSON.stringify(selectedDeck, [ 'name', 'cards' ]);
|
||||
deckExportTextBox.value = json;
|
||||
deckExportCopyButton.innerText = 'Copy';
|
||||
deckExportDialog.showModal();
|
||||
});
|
||||
deckExportCopyButton.addEventListener('click', () => {
|
||||
if (selectedDeck == null) return;
|
||||
navigator.clipboard.writeText(deckExportTextBox.value);
|
||||
deckExportCopyButton.innerText = 'Copied';
|
||||
});
|
||||
|
|
@ -190,31 +236,22 @@ deckRenameButton.addEventListener('click', () => {
|
|||
|
||||
deckCopyButton.addEventListener('click', () => {
|
||||
if (selectedDeck == null) return;
|
||||
selectedDeck = new Deck(selectedDeck.name, Array.from(selectedDeck.cards), false);
|
||||
const button = createDeckButton(decks.length, selectedDeck);
|
||||
decks.push(selectedDeck);
|
||||
(button.getElementsByTagName('input')[0] as HTMLInputElement).checked = true;
|
||||
selectDeck();
|
||||
saveDecks();
|
||||
importDecks([ new Deck(`${selectedDeck.name} - Copy`, Array.from(selectedDeck.cards), false) ]);
|
||||
});
|
||||
|
||||
deckDeleteButton.addEventListener('click', () => {
|
||||
if (selectedDeck == null) return;
|
||||
const index = decks.indexOf(selectedDeck);
|
||||
if (index < 0) return;
|
||||
if (!confirm(`Are you sure you want to delete ${selectedDeck.name}?`)) return;
|
||||
|
||||
let removed = false;
|
||||
for (const el of deckButtons.entries) {
|
||||
if (removed) {
|
||||
el.value--;
|
||||
} else if (el.value == index) {
|
||||
if (el.value == selectedDeck) {
|
||||
deckList.removeChild(el.button.buttonElement);
|
||||
removed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
decks.splice(index, 1);
|
||||
const index = decks.indexOf(selectedDeck);
|
||||
if (index >= 0) decks.splice(index, 1);
|
||||
deckButtons.entries.splice(index, 1);
|
||||
deckButtons.deselect();
|
||||
selectedDeck = null;
|
||||
|
|
@ -222,5 +259,53 @@ deckDeleteButton.addEventListener('click', () => {
|
|||
saveDecks();
|
||||
});
|
||||
|
||||
deckImportTextButton.addEventListener('input', () => {
|
||||
deckImportTextSection.hidden = false;
|
||||
deckImportScreenshotSection.hidden = true;
|
||||
});
|
||||
deckImportScreenshotButton.addEventListener('input', () => {
|
||||
deckImportTextSection.hidden = true;
|
||||
deckImportScreenshotSection.hidden = false;
|
||||
});
|
||||
|
||||
deckImportScreenshotInstructionsButton.addEventListener('click', () => {
|
||||
if (deckImportScreenshotInstructions.hidden) {
|
||||
deckImportScreenshotInstructions.hidden = false;
|
||||
deckImportScreenshotInstructionsButton.innerText = 'Hide instructions';
|
||||
} else {
|
||||
deckImportScreenshotInstructions.hidden = true;
|
||||
deckImportScreenshotInstructionsButton.innerText = 'Show instructions';
|
||||
}
|
||||
});
|
||||
deckImportScreenshotInstructionsButtonPC.addEventListener('input', () => {
|
||||
deckImportScreenshotInstructionsPC.hidden = false;
|
||||
deckImportScreenshotInstructionsMobile.hidden = true;
|
||||
});
|
||||
deckImportScreenshotInstructionsButtonMobile.addEventListener('input', () => {
|
||||
deckImportScreenshotInstructionsPC.hidden = true;
|
||||
deckImportScreenshotInstructionsMobile.hidden = false;
|
||||
});
|
||||
|
||||
deckImportFileBox.addEventListener('change', async () => {
|
||||
if (deckImportFileBox.files && deckImportFileBox.files.length > 0) {
|
||||
try {
|
||||
const bitmaps = await Promise.all(Array.from(deckImportFileBox.files, f => createImageBitmap(f)));
|
||||
importDecks(bitmaps.map(getCardListFromImageBitmap));
|
||||
} catch (ex: any) {
|
||||
deckImportErrorBox.innerText = ex.message;
|
||||
deckImportErrorBox.hidden = false;
|
||||
}
|
||||
deckImportFileBox.value = '';
|
||||
deckImportDialog.close();
|
||||
}
|
||||
});
|
||||
|
||||
deckExportAllButton.addEventListener('click', () => {
|
||||
const json = JSON.stringify(decks.filter(d => !d.isReadOnly), [ 'name', 'cards' ]);
|
||||
deckExportTextBox.value = json;
|
||||
deckExportCopyButton.innerText = 'Copy';
|
||||
deckExportDialog.showModal();
|
||||
});
|
||||
|
||||
if (!canPushState)
|
||||
deckListBackButton.href = '#';
|
||||
|
|
|
|||
45
TableturfBattleClient/src/ScreenshotImporter.ts
Normal file
45
TableturfBattleClient/src/ScreenshotImporter.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
function getCardListFromImageBitmap(bitmap: ImageBitmap) {
|
||||
if (!cardDatabase.cards) throw new Error('Card database is not yet initialised.');
|
||||
|
||||
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.drawImage(bitmap, 0, 0);
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
|
||||
function getR(x: number, y: number) { return imageData.data[(y * imageData.width + x) * 4]; }
|
||||
function getG(x: number, y: number) { return imageData.data[(y * imageData.width + x) * 4 + 1]; }
|
||||
function getB(x: number, y: number) { return imageData.data[(y * imageData.width + x) * 4 + 2]; }
|
||||
function getA(x: number, y: number) { return imageData.data[(y * imageData.width + x) * 4 + 3]; }
|
||||
|
||||
const cards = [ ];
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const cx = i % 3, cy = Math.floor(i / 3);
|
||||
const grid: Space[][] = [ ];
|
||||
for (let x = 0; x < 8; x++) {
|
||||
const col = [ ];
|
||||
for (let y = 0; y < 8; y++) {
|
||||
const px = Math.round(imageData.width * (0.0760416667 + 0.0623372396 * cx + 0.0065476190 * x));
|
||||
const py = Math.round(imageData.height * (0.2342592593 + 0.1521122685 * cy + 0.0116931217 * y));
|
||||
if (getR(px, py) >= 224)
|
||||
col.push(getG(px, py) >= 224 ? Space.Ink1 : Space.SpecialInactive1);
|
||||
else
|
||||
col.push(Space.Empty);
|
||||
}
|
||||
grid.push(col);
|
||||
}
|
||||
|
||||
// Find the card with this pattern.
|
||||
function cardMatches(card: Card) {
|
||||
for (let x = 0; x < 8; x++) {
|
||||
for (let y = 0; y < 8; y++) {
|
||||
if (card.grid[x][y] != grid[x][y]) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const card = cardDatabase.cards.find(cardMatches);
|
||||
if (!card) throw new Error(`Card number ${i + 1} is unknown.`);
|
||||
cards.push(card.number);
|
||||
}
|
||||
return cards;
|
||||
}
|
||||
|
|
@ -1250,6 +1250,15 @@ dialog::backdrop {
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
#deckImportForm, #deckImportTextSection:not([hidden]) {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
#deckImportDialog ol {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
/* Help */
|
||||
|
||||
#helpControls {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user