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(/