Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c20c2927c |
@@ -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
|
||||
|
||||
+2
-2
@@ -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"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<text class="bannerLabel">{{ nextName }} in</text>
|
||||
<text class="bannerTime">{{ countdown }}</text>
|
||||
</div>
|
||||
<list class="prayers" id="prayers" onscroll="onScroll">
|
||||
<list class="prayers" id="prayers" onscroll="onScroll" onscrollend="onScrollEnd">
|
||||
<list-item type="spacer" class="spacer"><div></div></list-item>
|
||||
<list-item type="prayer" for="{{ row in rows }}" tid="key" class="row {{ row.key === nextKey ? 'rowNext' : '' }}">
|
||||
<text style="{{ row.nameStyle }}" class="rowName {{ row.key === nextKey ? 'nameNext' : '' }}">{{ row.label }}</text>
|
||||
@@ -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')
|
||||
|
||||
+8
-8
@@ -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')
|
||||
|
||||
@@ -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(/<script>([\s\S]*?)<\/script>/)[1].replace(/import\s+\w+\s+from\s+'[^']*';?/, '').replace('export default', 'globalThis.spec =')
|
||||
const ctx = {prayer: require('../src/common/prayer.js'), setInterval: () => 1, clearInterval: () => {}}
|
||||
vm.createContext(ctx); vm.runInContext(body, ctx)
|
||||
const p = Object.assign({}, ctx.spec, JSON.parse(JSON.stringify(ctx.spec.private)))
|
||||
p.update()
|
||||
let rows = p.rows, writes = 0
|
||||
Object.defineProperty(p, 'rows', {get: () => rows, set: value => {writes++; rows = value}})
|
||||
const original = JSON.stringify(rows)
|
||||
for (let i=0; i<500; i++) p.onScroll({scrollY: i, scrollState: i < 250 ? 1 : 2})
|
||||
console.log('Row-array replacements during 500 gesture/inertia events:', writes)
|
||||
assert.equal(writes, 0, 'native scrolling must not rebuild reactive rows')
|
||||
assert.equal(JSON.stringify(rows), original, 'no font changes while scrolling')
|
||||
p.update()
|
||||
assert.equal(writes, 0, 'periodic refresh must defer during scrolling')
|
||||
p.onScroll({scrollY: 520, scrollState: 0})
|
||||
assert.equal(parseFloat(p.rows[5].timeStyle.fontSize), 60, 'last row grows after stop')
|
||||
assert.ok(writes <= 1, 'one focus update at stop')
|
||||
const settled = writes
|
||||
p.onScrollEnd()
|
||||
assert.equal(writes, settled, 'duplicate stop event is a no-op')
|
||||
p.onScroll({scrollY: 208, scrollState: 1})
|
||||
p.onScrollEnd()
|
||||
assert.equal(parseFloat(p.rows[2].timeStyle.fontSize), 60, 'scrollend fallback applies nearest row')
|
||||
assert.equal(parseFloat(p.rows[1].timeStyle.fontSize), 30)
|
||||
const saved = JSON.stringify(p.rows)
|
||||
for (const e of [undefined, {}, {scrollY: NaN}, {scrollY: Infinity}]) p.onScroll(e)
|
||||
assert.equal(JSON.stringify(p.rows), saved)
|
||||
p.onScroll({scrollY: 312, scrollState: 1})
|
||||
p.onHide()
|
||||
p.onShow()
|
||||
assert.equal(p._scrolling, false, 'hiding during a swipe must not freeze refresh after resume')
|
||||
assert.equal(parseFloat(p.rows[3].timeStyle.fontSize), 60)
|
||||
assert.match(src, /onscrollend="onScrollEnd"/)
|
||||
console.log('Scroll workload, deferred refresh, settled focus and duplicate-event checks passed')
|
||||
Reference in New Issue
Block a user