penguin/narou-android

Simple 縦書き reader for narou

commit 90a3efe43e19b40c5dc37b2ad12b4ffbd85dd432

author斟酌 鵬兄 <tgckpg@gmail.com>
date2026-10-14T09:04:04Z
subjectSync SystemBar color
commit 90a3efe43e19b40c5dc37b2ad12b4ffbd85dd432
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2026-10-14T09:04:04Z

    Sync SystemBar color
---
 app/src/main/assets/reader/content.js              | 20 +++++++++-------
 .../net/astropenguin/naroureader/MainActivity.java | 26 ++++++++++----------
 tests/reader.test.cjs                              | 28 ++++++++++++++++++----
 3 files changed, 48 insertions(+), 26 deletions(-)

diff --git a/app/src/main/assets/reader/content.js b/app/src/main/assets/reader/content.js
index e14841a..269a247 100644
--- a/app/src/main/assets/reader/content.js
+++ b/app/src/main/assets/reader/content.js
@@ -13,7 +13,7 @@
   const defaults = {font: 20, spacing: 1.9, paragraph: 0.3, theme: 'paper', auto: true, normalColor: null, highlightColor: null, backgroundColor: null, language: i18n.language};
   const textColors = {paper: ['#2d332f', '#17694f', '#e3e3df'], white: ['#222222', '#17694f', '#e3e3df'], dark: ['#e2e4de', '#79dfb5', '#000000']};
   let settings = {...defaults}, opened = false, lastFocus;
-  let nativeTheme = null;
+  let nativeBackgroundColor = null;
   let controlsVisible = false, savedPosition = 0, restoring = false, saveTimer;
   const positionKey = `narouPosition:${location.pathname}`;
   const notify = data => browser.runtime.sendNativeMessage('narou', data).catch(() => {});
@@ -157,7 +157,7 @@
     input.addEventListener('input', () => { settings[key] = input.value; applyTextColors(); save(); });
   }
   const resetColors = el('button', '@resetColors', {type: 'button', id: 'reading-reset-colors'});
-  resetColors.addEventListener('click', () => { settings.normalColor = settings.highlightColor = null; applyTextColors(); save(); });
+  resetColors.addEventListener('click', () => { settings.normalColor = settings.highlightColor = settings.backgroundColor = null; applyTextColors(); save(); });
   header.append(resetColors);
   const appMenu = el('button', '☰', {type: 'button', 'aria-label': '@appMenu'});
   appMenu.addEventListener('click', () => notify({type: 'menu'}));
@@ -465,7 +465,7 @@
     pageActive = true;
     touching = false;
     resetMotion();
-    if (opened) { notify({type: 'reader', open: true}); notify({type: 'theme', theme: settings.theme}); }
+    if (opened) { notify({type: 'reader', open: true}); notify({type: 'background', color: settings.backgroundColor || textColors[settings.theme][2]}); }
     startMotion();
   });
   // Continuous scrolling must checkpoint periodically, not only after scrolling stops.
@@ -491,10 +491,6 @@
     restoring = true;
     panel.dataset.theme = settings.theme;
     applyTextColors();
-    if (nativeTheme !== settings.theme) {
-      nativeTheme = settings.theme;
-      notify({type: 'theme', theme: settings.theme});
-    }
     article.style.setProperty('--font', `${settings.font}px`);
     article.style.setProperty('--spacing', settings.spacing);
     article.style.setProperty('--paragraph', `${settings.paragraph}em`);
@@ -518,9 +514,15 @@
     const [normal, highlight, background] = textColors[settings.theme];
     article.style.setProperty('--normal-text', settings.normalColor || normal);
     article.style.setProperty('--highlight-text', settings.highlightColor || highlight);
-    panel.style.setProperty('--bg',  settings.backgroundColor || background);
+    const backgroundColor = settings.backgroundColor || background;
+    panel.style.setProperty('--bg', backgroundColor);
     colorInputs.normalColor.value = settings.normalColor || normal;
     colorInputs.highlightColor.value = settings.highlightColor || highlight;
+    colorInputs.backgroundColor.value = backgroundColor;
+    if (nativeBackgroundColor !== backgroundColor) {
+      nativeBackgroundColor = backgroundColor;
+      notify({type: 'background', color: backgroundColor});
+    }
   }
   function open() {
     if (opened) return;
@@ -611,7 +613,7 @@
     settings.paragraph = Number.isFinite(stored.paragraph) ? Math.min(2, Math.max(0, stored.paragraph)) : defaults.paragraph;
     settings.theme = ['paper', 'white', 'dark'].includes(stored.theme) ? stored.theme : defaults.theme;
     settings.auto = typeof stored.auto === 'boolean' ? stored.auto : defaults.auto;
-    for (const key of ['normalColor', 'highlightColor']) settings[key] = /^#[0-9a-f]{6}$/i.test(stored[key]) ? stored[key] : null;
+    for (const key of ['normalColor', 'highlightColor', 'backgroundColor']) settings[key] = /^#[0-9a-f]{6}$/i.test(stored[key]) ? stored[key] : null;
     if (settings.auto && !opened) open(); else apply(savedPosition);
   }).catch(() => { open(); });
   notify({type: 'language', language: i18n.language});
diff --git a/app/src/main/java/net/astropenguin/naroureader/MainActivity.java b/app/src/main/java/net/astropenguin/naroureader/MainActivity.java
index ab9dfcb..2b21851 100644
--- a/app/src/main/java/net/astropenguin/naroureader/MainActivity.java
+++ b/app/src/main/java/net/astropenguin/naroureader/MainActivity.java
@@ -38,7 +38,7 @@ public final class MainActivity extends Activity {
     private GeckoSession session;
     private GeckoView geckoView;
     private FrameLayout root;
-    private boolean darkReaderTheme;
+    private boolean darkReaderBackground;
     private Button menuButton;
     private ProgressBar progress;
     private SharedPreferences prefs;
@@ -91,7 +91,7 @@ public final class MainActivity extends Activity {
             return insets;
         });
         setContentView(root);
-        applyReaderTheme(prefs.getString("readerTheme", "paper"));
+        applyReaderBackground(prefs.getString("readerBackgroundColor", "#e3e3df"));
         if (Build.VERSION.SDK_INT >= 33) getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
             android.window.OnBackInvokedDispatcher.PRIORITY_DEFAULT, this::navigateBack);
         immersive();
@@ -270,7 +270,7 @@ public final class MainActivity extends Activity {
                         menuButton.setContentDescription(tr("native.menu"));
                     }
                     if ("menu".equals(data.optString("type"))) showMenu();
-                    if ("theme".equals(data.optString("type"))) applyReaderTheme(data.optString("theme", "paper"));
+                    if ("background".equals(data.optString("type"))) applyReaderBackground(data.optString("color"));
                     if ("reader".equals(data.optString("type"))) {
                         reading = data.optBoolean("open");
                         if (!reading) tilt.stop();
@@ -399,16 +399,16 @@ public final class MainActivity extends Activity {
         if (session != null) session.close();
         super.onDestroy();
     }
-    /** Match the native cutout/safe-area background to the extension's palette. */
-    private void applyReaderTheme(String theme) {
-        if (!"dark".equals(theme) && !"white".equals(theme)) theme = "paper";
-        darkReaderTheme = "dark".equals(theme);
-        int background = darkReaderTheme ? Color.rgb(24, 32, 30)
-                : "white".equals(theme) ? Color.WHITE : Color.rgb(246, 240, 227);
+    /** Match the native cutout/safe-area background to the reader's actual color. */
+    private void applyReaderBackground(String color) {
+        if (color == null || !color.matches("#[0-9a-fA-F]{6}")) return;
+        int background = Color.parseColor(color);
+        darkReaderBackground = (299 * Color.red(background) + 587 * Color.green(background)
+                + 114 * Color.blue(background)) < 128000;
         root.setBackgroundColor(background);
         getWindow().setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(background));
-        if (!theme.equals(prefs.getString("readerTheme", "paper"))) {
-            prefs.edit().putString("readerTheme", theme).apply();
+        if (!color.equals(prefs.getString("readerBackgroundColor", ""))) {
+            prefs.edit().putString("readerBackgroundColor", color).apply();
         }
         immersive();
     }
@@ -421,7 +421,7 @@ public final class MainActivity extends Activity {
             if (controller != null) {
                 int lightBars = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
                         | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
-                controller.setSystemBarsAppearance(darkReaderTheme ? 0 : lightBars, lightBars);
+                controller.setSystemBarsAppearance(darkReaderBackground ? 0 : lightBars, lightBars);
                 controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
                 controller.hide(WindowInsets.Type.systemBars());
             }
@@ -430,7 +430,7 @@ public final class MainActivity extends Activity {
                 View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
                 View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
                 View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
-                (darkReaderTheme ? 0 : View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR));
+                (darkReaderBackground ? 0 : View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR));
         }
     }
 }
diff --git a/tests/reader.test.cjs b/tests/reader.test.cjs
index cf5c588..0fd9a3d 100644
--- a/tests/reader.test.cjs
+++ b/tests/reader.test.cjs
@@ -30,7 +30,7 @@ const ROOT=path.resolve(__dirname,'..');
  assert.equal(await v.evaluate(e=>e.clientHeight),840);
  assert.equal(await panel.locator('article p').evaluateAll(ps=>ps.filter(p=>!p.textContent.trim()&&!p.querySelector('img')).length),0);
  assert(messages.some(m=>m.type==='reader'&&m.open));
- assert(messages.some(m=>m.type==='theme'&&m.theme==='paper'));
+ assert(messages.some(m=>m.type==='background'&&m.color==='#e3e3df'));
  const width=await v.evaluate(e=>e.scrollWidth);
  const firstParagraph=panel.locator('article p').first();
  // A tap on ruby text focuses its paragraph without opening chrome.
@@ -54,9 +54,9 @@ const ROOT=path.resolve(__dirname,'..');
  await panel.locator('#paragraph').evaluate(e=>{e.value='1.2';e.dispatchEvent(new Event('input'));});
  await page.waitForTimeout(100);assert.equal(data.narouTategaki.paragraph,1.2);
  await panel.locator('select').first().selectOption('white');
- assert(messages.some(m=>m.type==='theme'&&m.theme==='white'));
+ assert(messages.some(m=>m.type==='background'&&m.color==='#e3e3df'));
  await panel.locator('select').first().selectOption('dark');
- assert(messages.some(m=>m.type==='theme'&&m.theme==='dark'));
+ assert(messages.some(m=>m.type==='background'&&m.color==='#000000'));
  await panel.getByRole('button',{name:'App menu'}).click();assert(messages.some(m=>m.type==='menu'));
  await panel.getByRole('button',{name:'Continue reading'}).click();
  await v.focus();await page.keyboard.press('PageDown');await page.keyboard.press('PageDown');
@@ -68,8 +68,28 @@ const ROOT=path.resolve(__dirname,'..');
  assert(Math.abs(before-restored)<.002,`${before} != ${restored}`);
  assert.equal(await panel.getAttribute('data-theme'),'dark');
  assert.equal(await panel.locator('#reading-highlightColor').inputValue(),'#ff7755','custom colors survive reload and theme change');
- assert(messages.filter(m=>m.type==='theme'&&m.theme==='dark').length>=2,'restored theme must reach the native app');
+ assert(messages.filter(m=>m.type==='background'&&m.color==='#000000').length>=2,'restored background must reach the native app');
  await page.touchscreen.tap(5,5);
+ await panel.locator('#reading-backgroundColor').evaluate(e=>{e.value='#123456';e.dispatchEvent(new Event('input'));});
+ assert.equal(data.narouTategaki.backgroundColor,'#123456');
+ assert.equal(await panel.evaluate(e=>getComputedStyle(e).backgroundColor),'rgb(18, 52, 86)');
+ assert(messages.some(m=>m.type==='background'&&m.color==='#123456'));
+ await load();
+ assert.equal(await panel.locator('#reading-backgroundColor').inputValue(),'#123456','background survives reload');
+ const backgroundMessages=messages.filter(m=>m.type==='background').length;
+ await page.evaluate(()=>window.dispatchEvent(new PageTransitionEvent('pageshow',{persisted:true})));
+ await page.waitForTimeout(50);
+ assert.equal(messages.filter(m=>m.type==='background').length,backgroundMessages+1,'cached page resends background');
+ assert.equal(messages.filter(m=>m.type==='background').at(-1).color,'#123456');
+ await page.touchscreen.tap(5,5);
+ await panel.locator('select').first().selectOption('white');
+ assert.equal(await panel.locator('#reading-backgroundColor').inputValue(),'#123456','custom background overrides theme');
+ await panel.locator('#reading-reset-colors').click();
+ assert.equal(data.narouTategaki.backgroundColor,null);
+ assert.equal(await panel.locator('#reading-backgroundColor').inputValue(),'#e3e3df');
+ assert.equal(messages.filter(m=>m.type==='background').at(-1).color,'#e3e3df');
+ assert(!messages.some(m=>m.type==='theme'),'native protocol no longer sends themes');
+ await panel.locator('#reading-highlightColor').evaluate(e=>{e.value='#ff7755';e.dispatchEvent(new Event('input'));});
  // Language switches update existing controls without losing focus, colors or position.
  const language=panel.locator('#reading-language');
  const positionBeforeLanguage=await v.evaluate(e=>e.scrollLeft);