Salah 0.4: center-magnified prayer times and larger clock
This commit is contained in:
@@ -19,18 +19,22 @@ Morocco (Ministry of Habous), matching AlAdhan `method=21`:
|
|||||||
All computation happens **on the watch** — no GPS, no network. Pure solar math
|
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`.
|
(same Meeus approach as the sundial) lives in `src/common/prayer.js`.
|
||||||
|
|
||||||
## Screen (v0.3)
|
## Screen (v0.4)
|
||||||
|
|
||||||
- Pure #000000 AMOLED background, including the list and prayer rows
|
- Pure #000000 AMOLED background
|
||||||
- Larger 42px clock, 28px prayer names and 36px times
|
- Larger 50px clock; no swipe instruction label
|
||||||
- Native vertically swipe-scrollable list: 80px rows in a 320×240px viewport
|
- Prayer names above their times, in fixed 104px native list rows
|
||||||
- Top/bottom spacers let the first and last prayers scroll to the center
|
- Center magnification: prayer times grow continuously from 30px to 60px as
|
||||||
- Fixed next-prayer countdown; lavender outline/text highlight the next prayer
|
their row approaches the physical screen center; names grow from 20px to 28px
|
||||||
- Normal system back button exits; no crown support assumed
|
- Scroll viewport 320×264 at x73/y101, centered at y233; 80px end spacers
|
||||||
- 15s refresh; no changes to prayer calculations
|
- 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
|
||||||
|
|
||||||
Build and automated tests pass; v0.3 scrolling/rendering needs an on-watch check.
|
Automated math/page/focus/layout tests and release build pass. The live scroll
|
||||||
`preview-0.3.png` is a static layout mock with illustrative times, not a watch screenshot.
|
magnification still needs an on-watch check. `preview-0.4.png` is a static layout
|
||||||
|
mock with illustrative times, not a device screenshot.
|
||||||
|
|
||||||
## Project layout
|
## Project layout
|
||||||
|
|
||||||
@@ -56,6 +60,7 @@ npm run release
|
|||||||
node tests/prayer.cjs
|
node tests/prayer.cjs
|
||||||
node tests/page.cjs
|
node tests/page.cjs
|
||||||
node tests/amoled.cjs
|
node tests/amoled.cjs
|
||||||
|
node tests/focus.cjs
|
||||||
```
|
```
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
+2
-2
@@ -2,8 +2,8 @@
|
|||||||
"package": "com.ilias.salah",
|
"package": "com.ilias.salah",
|
||||||
"name": "SALAH",
|
"name": "SALAH",
|
||||||
"icon": "/common/icon.png",
|
"icon": "/common/icon.png",
|
||||||
"versionName": "0.3",
|
"versionName": "0.4",
|
||||||
"versionCode": 3,
|
"versionCode": 4,
|
||||||
"minAPILevel": 1,
|
"minAPILevel": 1,
|
||||||
"deviceTypeList": [
|
"deviceTypeList": [
|
||||||
"watch"
|
"watch"
|
||||||
|
|||||||
+28
-12
@@ -6,15 +6,14 @@
|
|||||||
<text class="bannerLabel">{{ nextName }} in</text>
|
<text class="bannerLabel">{{ nextName }} in</text>
|
||||||
<text class="bannerTime">{{ countdown }}</text>
|
<text class="bannerTime">{{ countdown }}</text>
|
||||||
</div>
|
</div>
|
||||||
<list class="prayers" id="prayers">
|
<list class="prayers" id="prayers" onscroll="onScroll">
|
||||||
<list-item type="spacer" class="spacer"><div></div></list-item>
|
<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' : '' }}">
|
<list-item type="prayer" for="{{ row in rows }}" tid="key" class="row {{ row.key === nextKey ? 'rowNext' : '' }}">
|
||||||
<text class="rowName {{ row.key === nextKey ? 'nameNext' : '' }}">{{ row.label }}</text>
|
<text style="{{ row.nameStyle }}" class="rowName {{ row.key === nextKey ? 'nameNext' : '' }}">{{ row.label }}</text>
|
||||||
<text class="rowTime {{ row.key === nextKey ? 'timeNext' : '' }}">{{ row.time }}</text>
|
<text style="{{ row.timeStyle }}" class="rowTime {{ row.key === nextKey ? 'timeNext' : '' }}">{{ row.time }}</text>
|
||||||
</list-item>
|
</list-item>
|
||||||
<list-item type="spacer" class="spacer"><div></div></list-item>
|
<list-item type="spacer" class="spacer"><div></div></list-item>
|
||||||
</list>
|
</list>
|
||||||
<text class="hint">SWIPE TO SCROLL</text>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
@@ -43,6 +42,7 @@ export default {
|
|||||||
countdown: '',
|
countdown: '',
|
||||||
nextKey: 'fajr',
|
nextKey: 'fajr',
|
||||||
nextName: 'FAJR',
|
nextName: 'FAJR',
|
||||||
|
scrollY: 0,
|
||||||
rows: []
|
rows: []
|
||||||
},
|
},
|
||||||
onInit() {},
|
onInit() {},
|
||||||
@@ -56,6 +56,22 @@ export default {
|
|||||||
onDestroy() {
|
onDestroy() {
|
||||||
clearInterval(this.timer)
|
clearInterval(this.timer)
|
||||||
},
|
},
|
||||||
|
onScroll(event) {
|
||||||
|
if (!event || typeof event.scrollY !== 'number' || !isFinite(event.scrollY)) return
|
||||||
|
this.scrollY = Math.max(0, Math.min(520, event.scrollY))
|
||||||
|
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)
|
||||||
|
return Object.assign({}, row, {
|
||||||
|
timeStyle: { fontSize: Math.round(30 + 30 * focus) + 'px' },
|
||||||
|
nameStyle: { fontSize: Math.round(20 + 8 * focus) + 'px' }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
update() {
|
update() {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const hh = String(now.getHours()).padStart(2, '0')
|
const hh = String(now.getHours()).padStart(2, '0')
|
||||||
@@ -77,23 +93,23 @@ export default {
|
|||||||
passed: t[key] < nowMin,
|
passed: t[key] < nowMin,
|
||||||
rowStyle: { width: ROW_WIDTHS[i] + 'px' }
|
rowStyle: { width: ROW_WIDTHS[i] + 'px' }
|
||||||
}))
|
}))
|
||||||
|
this.applyFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.page { width: 466px; height: 466px; background-color: #000000; }
|
.page { width: 466px; height: 466px; background-color: #000000; }
|
||||||
.city { position: absolute; top: 26px; left: 103px; width: 260px; height: 26px; color: #aaa5b5; font-size: 20px; text-align: center; }
|
.city { position: absolute; top: 12px; left: 103px; width: 260px; height: 26px; color: #aaa5b5; font-size: 20px; text-align: center; }
|
||||||
.clock { position: absolute; top: 52px; left: 103px; width: 260px; height: 48px; color: #ffffff; font-size: 42px; text-align: center; }
|
.clock { position: absolute; top: 35px; left: 103px; width: 260px; height: 56px; color: #ffffff; font-size: 50px; text-align: center; }
|
||||||
.banner { position: absolute; top: 104px; left: 53px; width: 360px; height: 36px; background-color: #000000; flex-direction: row; justify-content: center; align-items: center; }
|
.banner { position: absolute; top: 383px; left: 53px; width: 360px; height: 36px; background-color: #000000; flex-direction: row; justify-content: center; align-items: center; }
|
||||||
.bannerLabel { color: #c5a3ff; font-size: 24px; margin-right: 8px; }
|
.bannerLabel { color: #c5a3ff; font-size: 24px; margin-right: 8px; }
|
||||||
.bannerTime { color: #ffffff; font-size: 26px; font-weight: bold; }
|
.bannerTime { color: #ffffff; font-size: 26px; font-weight: bold; }
|
||||||
.prayers { position: absolute; top: 146px; left: 73px; width: 320px; height: 240px; flex-direction: column; background-color: #000000; }
|
.prayers { position: absolute; top: 101px; left: 73px; width: 320px; height: 264px; flex-direction: column; background-color: #000000; }
|
||||||
.spacer { width: 320px; height: 80px; background-color: #000000; }
|
.spacer { width: 320px; height: 80px; background-color: #000000; }
|
||||||
.row { width: 320px; height: 80px; background-color: #000000; flex-direction: row; justify-content: space-between; align-items: center; padding-left: 10px; padding-right: 10px; }
|
.row { width: 320px; height: 104px; background-color: #000000; flex-direction: column; justify-content: center; align-items: center; padding-left: 10px; padding-right: 10px; }
|
||||||
.rowNext { border-width: 1px; border-color: #a779ef; border-radius: 18px; }
|
.rowNext { border-width: 1px; border-color: #a779ef; border-radius: 18px; }
|
||||||
.rowName { color: #d0cbd6; font-size: 28px; }
|
.rowName { width: 298px; height: 30px; text-align: center; color: #d0cbd6; font-size: 28px; }
|
||||||
.nameNext { color: #c5a3ff; font-weight: bold; }
|
.nameNext { color: #c5a3ff; font-weight: bold; }
|
||||||
.rowTime { color: #ffffff; font-size: 36px; }
|
.rowTime { width: 298px; height: 70px; text-align: center; color: #ffffff; font-size: 60px; }
|
||||||
.timeNext { color: #c5a3ff; font-weight: bold; }
|
.timeNext { color: #c5a3ff; font-weight: bold; }
|
||||||
.hint { position: absolute; top: 403px; left: 103px; width: 260px; height: 24px; color: #aaa5b5; font-size: 18px; text-align: center; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
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()
|
||||||
|
assert.equal(typeof p.onScroll, 'function', 'scroll handler drives center magnification')
|
||||||
|
const size = i => parseFloat(p.rows[i].timeStyle.fontSize)
|
||||||
|
p.onScroll({scrollY: 0})
|
||||||
|
assert.equal(size(0), 60, 'first centered prayer gets 60px time')
|
||||||
|
assert.ok(size(1) < size(0))
|
||||||
|
p.onScroll({scrollY: 104})
|
||||||
|
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})
|
||||||
|
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.rows.forEach(row => assert.ok(parseFloat(row.timeStyle.fontSize) >= 30 && parseFloat(row.timeStyle.fontSize) <= 60))
|
||||||
|
}
|
||||||
|
const before = JSON.stringify(p.rows)
|
||||||
|
for (const event of [undefined, {}, {scrollY: NaN}, {scrollY: Infinity}]) p.onScroll(event)
|
||||||
|
assert.equal(JSON.stringify(p.rows), before, 'invalid events do not corrupt styles')
|
||||||
|
assert.ok(!src.includes('SWIPE TO SCROLL'))
|
||||||
|
assert.match(src, /onscroll="onScroll"/)
|
||||||
|
assert.match(src, /style="\{\{ row.timeStyle \}\}"/)
|
||||||
|
assert.match(src, /style="\{\{ row.nameStyle \}\}"/)
|
||||||
|
function css(c,k) { return parseFloat(src.match(new RegExp('\\.'+c+'\\s*\\{([^}]+)'))[1].match(new RegExp(k+':\\s*([^;]+)'))[1]) }
|
||||||
|
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')
|
||||||
@@ -94,6 +94,8 @@ function page() {
|
|||||||
p.onHide = spec.onHide
|
p.onHide = spec.onHide
|
||||||
p.onDestroy = spec.onDestroy
|
p.onDestroy = spec.onDestroy
|
||||||
p.update = spec.update
|
p.update = spec.update
|
||||||
|
p.applyFocus = spec.applyFocus
|
||||||
|
p.onScroll = spec.onScroll
|
||||||
p.rows = []
|
p.rows = []
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user