Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
Expand All @@ -25,6 +26,7 @@
import androidx.test.platform.app.InstrumentationRegistry;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -568,6 +570,41 @@ public void settingsReportsInstalledAppVersion() throws Exception {
);
}

@Test
public void openingSecondSaveKeepsOriginalPanePainted() throws Exception {
try (NativeDisplayFixture fixture = new NativeDisplayFixture()) {
fixture.setViewport(360, 640, 1, false);
awaitControllerSurface();
importEmeraldSave();
importSave("x/011020252224.sav", "x.sav");
runJavaScript(
"[...document.querySelectorAll('.save-card')]"
+ ".find(card => card.textContent.includes('emerald.sav'))"
+ ".querySelector('.card-main').click()"
);
awaitJavaScript("document.querySelector('#box-0-slot-0')?.textContent.includes('ARON')");
runJavaScript("document.querySelector('.source-chip').click()");
awaitJavaScript("document.querySelector('[role=dialog][aria-label=\"Box Menu\"]')");
runJavaScript("document.querySelector('#box-menu-command-3').click()");
awaitJavaScript(
"document.querySelector('[role=dialog][aria-label=\"Open another collection\"]')"
);
runJavaScript(
"[...document.querySelectorAll('[data-source-picker-control]')]"
+ ".find(control => control.textContent.includes('x.sav')).click()"
);
awaitJavaScript(
"document.querySelectorAll('.box-pane').length === 2"
+ " && !document.querySelector('[data-pane-id=\"pane-active-save\"]')"
+ ".hasAttribute('aria-busy')"
+ " && document.querySelector('[data-pane-id=\"pane-active-save\"]')"
+ ".textContent.includes('ARON')",
ENGINE_TIMEOUT_SECONDS
);
assertPainted("[data-pane-id=\"pane-active-save\"] .location-grid");
}
}

@Test
public void nativeRotationPreservesBoxesCarryMenuAndTakeover() throws Throwable {
assumeTrue("Requires Android 11 display controls", Build.VERSION.SDK_INT >= Build.VERSION_CODES.R);
Expand Down Expand Up @@ -2206,21 +2243,64 @@ private void importEmeraldSave() throws Exception {
awaitJavaScript(
"location.pathname === '/' && document.querySelector('#save-file-input')"
);
String encoded = Base64.encodeToString(readAsset("emerald-011020251345.sav"), Base64.NO_WRAP);
importSave("emerald-011020251345.sav", "emerald.sav");
}

private void importSave(String assetName, String fileName) throws Exception {
String encoded = Base64.encodeToString(readAsset(assetName), Base64.NO_WRAP);
runJavaScript(
"(() => { const bytes = Uint8Array.from(atob('"
+ encoded
+ "'), value => value.charCodeAt(0)); const transfer = new DataTransfer();"
+ " transfer.items.add(new File([bytes], 'emerald.sav'));"
+ " transfer.items.add(new File([bytes], '"
+ fileName
+ "'));"
+ " const input = document.querySelector('#save-file-input'); input.files = transfer.files;"
+ " input.dispatchEvent(new Event('change', { bubbles: true })); return true; })()"
);
awaitJavaScript(
"document.body.textContent.includes('emerald.sav imported and made active.')",
"document.body.textContent.includes('"
+ fileName
+ " imported and made active.')",
ENGINE_TIMEOUT_SECONDS
);
}

private void assertPainted(String selector) throws Exception {
long deadline = SystemClock.uptimeMillis() + TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS);
if (!awaitNextVisualState(deadline)) fail("Timed out waiting for WebView paint");
JSONObject bounds = new JSONObject(
runJavaScript(
"(() => { const rect=document.querySelector('"
+ selector
+ "').getBoundingClientRect();"
+ " return {left:rect.left,top:rect.top,right:rect.right,bottom:rect.bottom}; })()"
)
);
AtomicReference<int[]> origin = new AtomicReference<>();
activityRule
.getScenario()
.onActivity(
activity -> {
int[] value = new int[2];
activity.getBridge().getWebView().getLocationOnScreen(value);
origin.set(value);
}
);
Bitmap screenshot = InstrumentationRegistry.getInstrumentation().getUiAutomation().takeScreenshot();
int[] webViewOrigin = origin.get();
HashSet<Integer> colors = new HashSet<>();
int left = webViewOrigin[0] + bounds.getInt("left") + 4;
int top = webViewOrigin[1] + bounds.getInt("top") + 4;
int right = webViewOrigin[0] + bounds.getInt("right") - 4;
int bottom = webViewOrigin[1] + bounds.getInt("bottom") - 4;
for (int y = top; y < bottom; y += 4) {
for (int x = left; x < right; x += 4) colors.add(screenshot.getPixel(x, y));
}
screenshot.recycle();
if (colors.size() < 24) fail("Save pane was not painted, sampled colors: " + colors.size());
}

private byte[] readAsset(String name) throws Exception {
try (
InputStream input = InstrumentationRegistry
Expand Down
12 changes: 12 additions & 0 deletions src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<script>
try {
const savedTheme = localStorage.getItem('pksx-theme');
document.documentElement.dataset.pksxTheme =
savedTheme ?? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
} catch {
document.documentElement.dataset.pksxTheme = matchMedia('(prefers-color-scheme: dark)')
.matches
? 'dark'
: 'light';
}
</script>

<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />

Expand Down
3 changes: 2 additions & 1 deletion src/lib/components/pksx/DetailRail.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,8 @@
var(--paper);
}

:global(.app-shell.dark) .portrait-card {
:global(.app-shell.dark) .portrait-card,
:global(:root[data-pksx-theme='dark']) .portrait-card {
background:
repeating-radial-gradient(
circle at 30% 35%,
Expand Down
6 changes: 4 additions & 2 deletions src/lib/components/pksx/StorageSlot.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@
);
}

:global(.app-shell.dark) .slot {
:global(.app-shell.dark) .slot,
:global(:root[data-pksx-theme='dark']) .slot {
--slot-fill: color-mix(
in oklch,
var(--paper-hi) 84%,
Expand All @@ -158,7 +159,8 @@
);
}

:global(.app-shell.dark) .slot.dual-type {
:global(.app-shell.dark) .slot.dual-type,
:global(:root[data-pksx-theme='dark']) .slot.dual-type {
background: linear-gradient(
135deg,
var(--slot-fill) 0%,
Expand Down
3 changes: 2 additions & 1 deletion src/lib/components/pksx/ToastRegion.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@
will-change: opacity, transform;
}

:global(.app-shell.dark) .toast {
:global(.app-shell.dark) .toast,
:global(:root[data-pksx-theme='dark']) .toast {
background: color-mix(in srgb, var(--paper-hi), white 4%);
border-color: color-mix(in srgb, white, transparent 88%);
}
Expand Down
14 changes: 13 additions & 1 deletion src/lib/pksx/theme.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
const storageKey = 'pksx-theme';

class ThemeState {
dark = $state(false);

restore() {
this.dark = document.documentElement.dataset.pksxTheme === 'dark';
}

setDark(dark: boolean) {
this.dark = dark;
document.documentElement.dataset.pksxTheme = dark ? 'dark' : 'light';
try {
localStorage.setItem(storageKey, dark ? 'dark' : 'light');
} catch {
// Keep the in-memory preference when storage is unavailable.
}
}

toggle() {
this.dark = !this.dark;
this.setDark(!this.dark);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@
});

onMount(() => {
theme.restore();
window.addEventListener('keydown', handleRootKeydown, true);
const backListener = Capacitor.isNativePlatform()
? CapacitorApp.addListener('backButton', ({ canGoBack }) => handlePlatformBack(canGoBack))
Expand Down
52 changes: 52 additions & 0 deletions src/routes/boxes-two-save.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { expect, test } from '@playwright/test';
import path from 'node:path';

const emeraldFixturePath = path.resolve(
'test-fixtures/save-files/bl1ndbeholder-pokemon-saves/emerald-011020251345.sav'
);
const xFixturePath = path.resolve(
'test-fixtures/save-files/bl1ndbeholder-pokemon-saves/x/011020252224.sav'
);

test('keeps the original save visible after opening a second save on Android landscape', async ({
page
}) => {
await page.setViewportSize({ width: 640, height: 360 });
await page.goto('/');
await expect(page.locator('[data-destination-root="saves"]')).toHaveAttribute(
'data-initial-state',
'ready'
);

const importInput = page.getByLabel('Import Save File');
await importInput.setInputFiles(emeraldFixturePath);
await expect(page.getByText('011020251345.sav imported and made active.')).toBeVisible({
timeout: 30_000
});
await importInput.setInputFiles(xFixturePath);
await expect(page.getByText('011020252224.sav imported and made active.')).toBeVisible({
timeout: 30_000
});

await page.getByRole('button', { name: 'Open emerald-011020251345.sav in Boxes' }).click();
await expect(page.locator('[data-destination-root="boxes"]')).toHaveAttribute(
'data-initial-state',
'ready',
{ timeout: 30_000 }
);
await page.getByRole('button', { name: 'Open Box Menu for emerald-011020251345.sav' }).click();
await page
.getByRole('dialog', { name: 'Box Menu' })
.getByRole('button', { name: 'Open another' })
.click();
await page
.getByRole('dialog', { name: 'Open another collection' })
.getByRole('button', { name: /011020252224\.sav/ })
.click();

const panes = page.locator('.box-pane');
await expect(panes).toHaveCount(2);
await expect(panes.nth(1)).not.toHaveAttribute('aria-busy', 'true', { timeout: 30_000 });
await expect(panes.nth(0).locator('.slot.pokemon').first()).toBeVisible();
await expect(panes.nth(1).getByRole('grid')).toBeVisible();
});
6 changes: 5 additions & 1 deletion src/routes/boxes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5445,7 +5445,11 @@
}

.two-pane .box-pane-strip {
display: contents;
grid-area: 1 / 1 / -1 / -1;
display: grid;
grid-template-columns: subgrid;
grid-template-rows: subgrid;
overflow: visible;
}

.two-pane .box-pane:first-child {
Expand Down
6 changes: 4 additions & 2 deletions src/routes/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@
--pksx-height-band: tall;
}

:root:has(.app-shell.dark) {
:root:has(.app-shell.dark),
:root[data-pksx-theme='dark'] {
--pksx-color-surface-canvas: #0e0f12;
--pksx-color-surface-panel: #181a1e;
--pksx-color-surface-subtle: #08090b;
Expand Down Expand Up @@ -371,7 +372,8 @@ html[data-pksx-route-direction='backward']::view-transition-new(pksx-route) {
}
}

.app-shell.dark {
.app-shell.dark,
:root[data-pksx-theme='dark'] .app-shell {
background:
radial-gradient(circle at 20% 10%, rgba(255, 138, 92, 0.04), transparent 50%),
radial-gradient(circle at 80% 90%, rgba(120, 140, 180, 0.04), transparent 55%), var(--paper);
Expand Down
14 changes: 14 additions & 0 deletions src/routes/settings.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,20 @@ test('Settings owns floor overflow and uses shared theme state', async ({ page }
'aria-pressed',
'true'
);
await page.reload();
await expect(page.locator('.app-shell')).toHaveClass(/dark/);
});

test('Settings uses the system theme until the user chooses an override', async ({ page }) => {
await page.emulateMedia({ colorScheme: 'dark' });
await openSettings(page);
await expect(page.locator('html')).toHaveAttribute('data-pksx-theme', 'dark');
await expect(page.locator('.app-shell')).toHaveClass(/dark/);

await page.getByRole('button', { name: 'Use light theme' }).click();
await page.reload();
await expect(page.locator('html')).toHaveAttribute('data-pksx-theme', 'light');
await expect(page.locator('.app-shell')).not.toHaveClass(/dark/);
});

test('raw viewport height selects the inherited Height Band at 560px', async ({ page }) => {
Expand Down