diff --git a/README.md b/README.md
index c2db146..6003e81 100644
--- a/README.md
+++ b/README.md
@@ -19,22 +19,24 @@ Morocco (Ministry of Habous), matching AlAdhan `method=21`:
All computation happens **on the watch** — no GPS, no network. Pure solar math
(same Meeus approach as the sundial) lives in `src/common/prayer.js`.
-## Screen (v0.4)
+## Screen (v0.5)
-- Pure #000000 AMOLED background
-- Larger 50px clock; no swipe instruction label
+- Pure #000000 AMOLED background, large 50px clock, no swipe hint
- Prayer names above their times, in fixed 104px native list rows
-- Center magnification: prayer times grow continuously from 30px to 60px as
- their row approaches the physical screen center; names grow from 20px to 28px
-- Scroll viewport 320×264 at x73/y101, centered at y233; 80px end spacers
-- Fixed next-prayer countdown below the list; lavender next-prayer highlight
-- Glyph sizes change without changing row heights, avoiding scroll-position drift
-- 15s clock refresh preserves scroll focus; calculation logic unchanged
-- Normal system back exits; crown support not assumed
+- After scrolling stops, the nearest-center prayer gets a 60px time and 28px
+ label; other rows use 30px times and 20px labels
+- No font resizing or row-array replacement during gesture/inertial scrolling
+- Duplicate stop notifications do not repaint unchanged focus; periodic refresh
+ defers while scrolling and resumes normally after hiding/showing the app
+- Scroll viewport 320×264 at x73/y101 (center y233); 80px end spacers
+- Fixed countdown below the list; lavender next-prayer highlight
+- Normal system back exits; calculation logic unchanged
-Automated math/page/focus/layout tests and release build pass. The live scroll
-magnification still needs an on-watch check. `preview-0.4.png` is a static layout
-mock with illustrative times, not a device screenshot.
+User reported watch lag in v0.4. A 500-event VM replay measured 500 row-array
+replacements in v0.4 versus zero during scrolling in v0.5. This verifies reduced
+update work, NOT device frame rate. All automated tests and release build pass;
+on-watch smoothness still needs confirmation. `preview-0.4.png` remains an
+illustrative static layout mock; the settled appearance is unchanged.
## Project layout
@@ -61,6 +63,7 @@ node tests/prayer.cjs
node tests/page.cjs
node tests/amoled.cjs
node tests/focus.cjs
+node tests/scroll-performance.cjs
```
## Install
diff --git a/src/manifest.json b/src/manifest.json
index 1f5409a..b7d592f 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -2,8 +2,8 @@
"package": "com.ilias.salah",
"name": "SALAH",
"icon": "/common/icon.png",
- "versionName": "0.4",
- "versionCode": 4,
+ "versionName": "0.5",
+ "versionCode": 5,
"minAPILevel": 1,
"deviceTypeList": [
"watch"
diff --git a/src/pages/index/index.ux b/src/pages/index/index.ux
index e3e419f..4f08e3e 100644
--- a/src/pages/index/index.ux
+++ b/src/pages/index/index.ux
@@ -6,7 +6,7 @@
{{ nextName }} in
{{ countdown }}
-
+
{{ row.label }}
@@ -42,11 +42,11 @@ export default {
countdown: '',
nextKey: 'fajr',
nextName: 'FAJR',
- scrollY: 0,
rows: []
},
onInit() {},
onShow() {
+ this._scrolling = false
this.update()
this.timer = setInterval(() => this.update(), 15000) // 15s cadence
},
@@ -58,21 +58,33 @@ export default {
},
onScroll(event) {
if (!event || typeof event.scrollY !== 'number' || !isFinite(event.scrollY)) return
- this.scrollY = Math.max(0, Math.min(520, event.scrollY))
+ // Non-template bookkeeping only while the native list is moving.
+ this._scrollY = Math.max(0, Math.min(520, event.scrollY))
+ this._scrolling = event.scrollState !== 0
+ if (event.scrollState === 0) this.onScrollEnd()
+ },
+ onScrollEnd() {
+ this._scrolling = false
this.applyFocus()
},
applyFocus() {
- // Row heights never change: only glyph sizes change, avoiding scroll jumps.
- const offset = this.scrollY || 0
- this.rows = this.rows.map((row, index) => {
- const focus = Math.max(0, 1 - Math.abs(index * 104 - offset) / 104)
+ const focused = Math.round((this._scrollY || 0) / 104)
+ let changed = false
+ const rows = this.rows.map((row, index) => {
+ const timeSize = index === focused ? '60px' : '30px'
+ const nameSize = index === focused ? '28px' : '20px'
+ if (row.timeStyle && row.timeStyle.fontSize === timeSize &&
+ row.nameStyle && row.nameStyle.fontSize === nameSize) return row
+ changed = true
return Object.assign({}, row, {
- timeStyle: { fontSize: Math.round(30 + 30 * focus) + 'px' },
- nameStyle: { fontSize: Math.round(20 + 8 * focus) + 'px' }
+ timeStyle: { fontSize: timeSize },
+ nameStyle: { fontSize: nameSize }
})
})
+ if (changed) this.rows = rows
},
update() {
+ if (this._scrolling) return
const now = new Date()
const hh = String(now.getHours()).padStart(2, '0')
const mm = String(now.getMinutes()).padStart(2, '0')
diff --git a/tests/focus.cjs b/tests/focus.cjs
index f59a911..1dedfa2 100644
--- a/tests/focus.cjs
+++ b/tests/focus.cjs
@@ -9,21 +9,21 @@ const p = Object.assign({}, ctx.spec, JSON.parse(JSON.stringify(ctx.spec.private
p.update()
assert.equal(typeof p.onScroll, 'function', 'scroll handler drives center magnification')
const size = i => parseFloat(p.rows[i].timeStyle.fontSize)
-p.onScroll({scrollY: 0})
+p.onScroll({scrollY: 0, scrollState: 0})
assert.equal(size(0), 60, 'first centered prayer gets 60px time')
assert.ok(size(1) < size(0))
-p.onScroll({scrollY: 104})
+p.onScroll({scrollY: 104, scrollState: 0})
assert.equal(size(1), 60)
assert.equal(size(0), size(2), 'symmetric shrink around center')
-p.onScroll({scrollY: 52})
-assert.equal(size(0), size(1), 'continuous interpolation between rows')
-assert.ok(size(0) > 30 && size(0) < 60)
-p.onScroll({scrollY: 520})
+p.onScroll({scrollY: 52, scrollState: 0})
+assert.equal(size(1), 60, 'nearest row wins after stopping halfway')
+assert.equal(size(0), 30)
+p.onScroll({scrollY: 520, scrollState: 0})
assert.equal(size(5), 60, 'last prayer can be magnified')
p.update()
assert.equal(size(5), 60, 'clock refresh preserves scroll focus')
for (const offset of [-100, 9999]) {
- p.onScroll({scrollY: offset})
+ p.onScroll({scrollY: offset, scrollState: 0})
p.rows.forEach(row => assert.ok(parseFloat(row.timeStyle.fontSize) >= 30 && parseFloat(row.timeStyle.fontSize) <= 60))
}
const before = JSON.stringify(p.rows)
@@ -37,4 +37,4 @@ function css(c,k) { return parseFloat(src.match(new RegExp('\\.'+c+'\\s*\\{([^}]
assert.equal(css('prayers','top') + css('prayers','height') / 2, 233, 'magnification center is physical screen center')
assert.equal(css('row','height'), 104, 'fixed row geometry prevents scroll feedback')
assert.ok(css('clock','font-size') > 42, 'header clock enlarged too')
-console.log('Center magnification, interpolation, end rows, refresh preservation, invalid events and wiring passed')
+console.log('Center magnification, settled focus, end rows, refresh preservation, invalid events and wiring passed')
diff --git a/tests/scroll-performance.cjs b/tests/scroll-performance.cjs
new file mode 100644
index 0000000..9907102
--- /dev/null
+++ b/tests/scroll-performance.cjs
@@ -0,0 +1,38 @@
+const assert = require('node:assert/strict')
+const fs = require('node:fs')
+const vm = require('node:vm')
+const src = fs.readFileSync('src/pages/index/index.ux', 'utf8')
+const body = src.match(/