Salah 0.3: larger AMOLED UI with swipe-scrollable prayers
This commit is contained in:
@@ -13,21 +13,24 @@ Morocco (Ministry of Habous), matching AlAdhan `method=21`:
|
|||||||
- Fajr 19°, Isha 17° (twilight angles)
|
- Fajr 19°, Isha 17° (twilight angles)
|
||||||
- Standard Asr (shadow factor 1× — Maliki default in Morocco)
|
- Standard Asr (shadow factor 1× — Maliki default in Morocco)
|
||||||
- Dhuhr = solar noon + 5 min; Maghrib = sunset + 5 min (AlAdhan's baked offsets)
|
- Dhuhr = solar noon + 5 min; Maghrib = sunset + 5 min (AlAdhan's baked offsets)
|
||||||
- Location hardcoded: Casablanca 33.5731, −7.5898 · TZ = UTC+1 year-round
|
- Location hardcoded: Casablanca 33.5731, −7.5898 · fixed TZ = UTC+1.
|
||||||
(Morocco dropped DST in 2018)
|
Ramadan clock changes are not handled by this version.
|
||||||
|
|
||||||
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
|
## Screen (v0.3)
|
||||||
|
|
||||||
- Header: CASABLANCA + current clock
|
- Pure #000000 AMOLED background, including the list and prayer rows
|
||||||
- Purple banner: next prayer name + countdown (`1h 55m`, `+1d …` wraps past midnight)
|
- Larger 42px clock, 28px prayer names and 36px times
|
||||||
- Six chord-tapered rows: widths [380, 412, 424, 406, 368, 304] px computed from
|
- Native vertically swipe-scrollable list: 80px rows in a 320×240px viewport
|
||||||
the circle chord at each row's position (fixed-width rows clipped off the disc
|
- Top/bottom spacers let the first and last prayers scroll to the center
|
||||||
in the first layout — asserted numerically in tests now)
|
- Fixed next-prayer countdown; lavender outline/text highlight the next prayer
|
||||||
- Passed prayers dimmed (opacity 0.45); next row purple-highlighted
|
- Normal system back button exits; no crown support assumed
|
||||||
- 15s refresh
|
- 15s refresh; no changes to prayer calculations
|
||||||
|
|
||||||
|
Build and automated tests pass; v0.3 scrolling/rendering needs an on-watch check.
|
||||||
|
`preview-0.3.png` is a static layout mock with illustrative times, not a watch screenshot.
|
||||||
|
|
||||||
## Project layout
|
## Project layout
|
||||||
|
|
||||||
@@ -52,6 +55,7 @@ npm ci
|
|||||||
npm run release
|
npm run release
|
||||||
node tests/prayer.cjs
|
node tests/prayer.cjs
|
||||||
node tests/page.cjs
|
node tests/page.cjs
|
||||||
|
node tests/amoled.cjs
|
||||||
```
|
```
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
+3
-3
@@ -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.2",
|
"versionName": "0.3",
|
||||||
"versionCode": 2,
|
"versionCode": 3,
|
||||||
"minAPILevel": 1,
|
"minAPILevel": 1,
|
||||||
"deviceTypeList": [
|
"deviceTypeList": [
|
||||||
"watch"
|
"watch"
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"display": {
|
"display": {
|
||||||
"backgroundColor": "#0a0d18"
|
"backgroundColor": "#000000"
|
||||||
},
|
},
|
||||||
"features": [
|
"features": [
|
||||||
{
|
{
|
||||||
|
|||||||
+25
-86
@@ -1,19 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<!-- header: city + clock -->
|
|
||||||
<text class="city">CASABLANCA</text>
|
<text class="city">CASABLANCA</text>
|
||||||
<text class="clock">{{ clock }}</text>
|
<text class="clock">{{ clock }}</text>
|
||||||
<!-- next-prayer banner -->
|
|
||||||
<div class="banner">
|
<div class="banner">
|
||||||
<text class="bannerLabel">{{ nextName }} in</text>
|
<text class="bannerLabel">{{ nextName }} in</text>
|
||||||
<text class="bannerTime">{{ countdown }}</text>
|
<text class="bannerTime">{{ countdown }}</text>
|
||||||
</div>
|
</div>
|
||||||
<!-- prayer rows: width tapers with the chord via per-row bound style (sundial markerStyle pattern) -->
|
<list class="prayers" id="prayers">
|
||||||
<div class="row {{ row.key === nextKey ? 'rowNext' : '' }} {{ row.passed ? 'rowPassed' : '' }}"
|
<list-item type="spacer" class="spacer"><div></div></list-item>
|
||||||
style="{{ row.rowStyle }}" for="{{ row in rows }}" tid="key">
|
<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 class="rowName {{ row.key === nextKey ? 'nameNext' : '' }}">{{ row.label }}</text>
|
||||||
<text class="rowTime {{ row.key === nextKey ? 'timeNext' : '' }}">{{ row.time }}</text>
|
<text class="rowTime {{ row.key === nextKey ? 'timeNext' : '' }}">{{ row.time }}</text>
|
||||||
</div>
|
</list-item>
|
||||||
|
<list-item type="spacer" class="spacer"><div></div></list-item>
|
||||||
|
</list>
|
||||||
|
<text class="hint">SWIPE TO SCROLL</text>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
@@ -22,7 +23,7 @@ import prayer from '../../common/prayer.js'
|
|||||||
// Casablanca
|
// Casablanca
|
||||||
const LAT = 33.5731
|
const LAT = 33.5731
|
||||||
const LON = -7.5898
|
const LON = -7.5898
|
||||||
const TZ = 1 // Africa/Casablanca is UTC+1 year-round (no DST since 2018)
|
const TZ = 1 // Fixed UTC+1; Ramadan clock changes are not handled.
|
||||||
|
|
||||||
const LABELS = {
|
const LABELS = {
|
||||||
fajr: 'FAJR',
|
fajr: 'FAJR',
|
||||||
@@ -33,8 +34,8 @@ const LABELS = {
|
|||||||
isha: 'ISHA'
|
isha: 'ISHA'
|
||||||
}
|
}
|
||||||
|
|
||||||
// chord-tapered row widths (v0.2 round-screen layout): safe = chord - 2*10px inset
|
// Uniform rows fit the entire scroll viewport.
|
||||||
const ROW_WIDTHS = [380, 412, 424, 406, 368, 304]
|
const ROW_WIDTHS = [320, 320, 320, 320, 320, 320]
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
private: {
|
private: {
|
||||||
@@ -80,81 +81,19 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.page {
|
.page { width: 466px; height: 466px; background-color: #000000; }
|
||||||
width: 466px;
|
.city { position: absolute; top: 26px; left: 103px; width: 260px; height: 26px; color: #aaa5b5; font-size: 20px; text-align: center; }
|
||||||
height: 466px;
|
.clock { position: absolute; top: 52px; left: 103px; width: 260px; height: 48px; color: #ffffff; font-size: 42px; text-align: center; }
|
||||||
background-color: #0a0d18;
|
.banner { position: absolute; top: 104px; left: 53px; width: 360px; height: 36px; background-color: #000000; flex-direction: row; justify-content: center; align-items: center; }
|
||||||
flex-direction: column;
|
.bannerLabel { color: #c5a3ff; font-size: 24px; margin-right: 8px; }
|
||||||
align-items: center;
|
.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; }
|
||||||
.city {
|
.spacer { width: 320px; height: 80px; background-color: #000000; }
|
||||||
height: 20px;
|
.row { width: 320px; height: 80px; background-color: #000000; flex-direction: row; justify-content: space-between; align-items: center; padding-left: 10px; padding-right: 10px; }
|
||||||
color: #8f95c7;
|
.rowNext { border-width: 1px; border-color: #a779ef; border-radius: 18px; }
|
||||||
font-size: 15px;
|
.rowName { color: #d0cbd6; font-size: 28px; }
|
||||||
letter-spacing: 2px;
|
.nameNext { color: #c5a3ff; font-weight: bold; }
|
||||||
text-align: center;
|
.rowTime { color: #ffffff; font-size: 36px; }
|
||||||
margin-top: 8px;
|
.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; }
|
||||||
.clock {
|
|
||||||
height: 40px;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 34px;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
.banner {
|
|
||||||
width: 300px;
|
|
||||||
height: 56px;
|
|
||||||
border-radius: 28px;
|
|
||||||
background-color: #6d4fd4;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
.bannerLabel {
|
|
||||||
color: #d9d2ff;
|
|
||||||
font-size: 20px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
.bannerTime {
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.row {
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 20px;
|
|
||||||
background-color: #161a2e;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 4px;
|
|
||||||
padding-left: 20px;
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
|
||||||
.rowNext {
|
|
||||||
background-color: #2b2460;
|
|
||||||
border-width: 1px;
|
|
||||||
border-color: #6d4fd4;
|
|
||||||
}
|
|
||||||
.rowPassed {
|
|
||||||
opacity: 0.45;
|
|
||||||
}
|
|
||||||
.rowName {
|
|
||||||
color: #a7acce;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
.nameNext {
|
|
||||||
color: #b9a8ff;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.rowTime {
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
.timeNext {
|
|
||||||
color: #ffffff;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
const assert = require('node:assert/strict')
|
||||||
|
const fs = require('node:fs')
|
||||||
|
const src = fs.readFileSync('src/pages/index/index.ux', 'utf8')
|
||||||
|
const manifest = JSON.parse(fs.readFileSync('src/manifest.json'))
|
||||||
|
function css(name, prop) {
|
||||||
|
const block = src.match(new RegExp('\\.' + name + '\\s*\\{([^}]+)'))[1]
|
||||||
|
return block.match(new RegExp(prop + ':\\s*([^;]+)'))[1].trim()
|
||||||
|
}
|
||||||
|
assert.equal(css('page', 'background-color'), '#000000', 'page must be AMOLED black')
|
||||||
|
assert.equal(manifest.display.backgroundColor, '#000000')
|
||||||
|
assert.match(src, /<list\s[^>]*class="prayers"/, 'native swipe-scrollable list')
|
||||||
|
assert.match(src, /<list-item[^>]*type="prayer"[^>]*for="\{\{ row in rows \}\}"/)
|
||||||
|
assert.ok(parseFloat(css('rowName', 'font-size')) >= 28)
|
||||||
|
assert.ok(parseFloat(css('rowTime', 'font-size')) >= 36)
|
||||||
|
const top = parseFloat(css('prayers', 'top'))
|
||||||
|
const height = parseFloat(css('prayers', 'height'))
|
||||||
|
const width = parseFloat(css('prayers', 'width'))
|
||||||
|
const dy = Math.max(Math.abs(top - 233), Math.abs(top + height - 233))
|
||||||
|
assert.ok(width <= 2 * Math.sqrt(233 ** 2 - dy ** 2) - 20, 'whole scroll viewport fits circle')
|
||||||
|
const rowH = parseFloat(css('row', 'height'))
|
||||||
|
const pad = parseFloat(css('spacer', 'height'))
|
||||||
|
assert.equal(pad, (height - rowH) / 2, 'first/last rows can reach viewport center')
|
||||||
|
assert.ok(6 * rowH + 2 * pad > height, 'content actually scrolls')
|
||||||
|
assert.ok(!src.includes('onBackPress'), 'system back still exits')
|
||||||
|
console.log('AMOLED, large text, scroll wiring and circular viewport checks passed')
|
||||||
+4
-12
@@ -148,18 +148,10 @@ test('update() rows: six entries, key+label+time wired', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test('row widths taper with the chord and fit the 466 circle at every row position', () => {
|
test('all rows fit the scroll viewport', () => {
|
||||||
// rows: 6 x 40px + 5 x 4px margins, block starts after header+banner (top=130)
|
const widths = sandbox.ROW_WIDTHS
|
||||||
const ROW_WIDTHS = spec.ROW_WIDTHS || sandbox.ROW_WIDTHS
|
assert.equal(widths.length, 6)
|
||||||
assert.equal(ROW_WIDTHS.length, 6)
|
widths.forEach(width => assert.equal(width, 320))
|
||||||
const H = 40, MARGIN = 4, BLOCK_TOP = 130, R = 233, INSET = 10
|
|
||||||
for (let n = 0; n < 6; n++) {
|
|
||||||
const top = BLOCK_TOP + n * (H + MARGIN)
|
|
||||||
const dy = Math.max(Math.abs(233 - top), Math.abs(233 - (top + H)))
|
|
||||||
const chord = 2 * Math.sqrt(Math.max(0, R * R - dy * dy))
|
|
||||||
const safe = chord - 2 * INSET
|
|
||||||
assert.ok(ROW_WIDTHS[n] <= safe, `row ${n} width ${ROW_WIDTHS[n]} exceeds safe chord ${safe.toFixed(0)} at top ${top}`)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('bound rowStyle width equals ROW_WIDTHS[i]', () => {
|
test('bound rowStyle width equals ROW_WIDTHS[i]', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user