Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ copy holds that copy's secrets.
Tasks or Microsoft To Do (those two need an app registered for the server). Add at the wall or from a phone; check things off with a moment to undo. A young child's
list has big rows and a picture on each.
- **Photos** from an iCloud shared album, straight from a phone, or from a folder.
- **Screen time.** A child's minutes for the day in a ring, with a timer that stops at zero and chimes; adding or
taking away time takes the household PIN. Off until a grown-up turns it on.
- **Text sized for the room.** Smaller, Standard or Larger, for the rows and the lists; the clock and the layout stay
as they are.
- **It behaves itself.** Brightness follows sunrise and sunset. After a while it drifts to photos and comes back. After a
Expand Down
446 changes: 429 additions & 17 deletions dist/app.js

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ <h1 id="photo-when"></h1>
<div class="photo-card" id="photo-card"><div class="slide-fill" id="slide-fill"></div><img id="slide-a" class="slide" alt=""><img id="slide-b" class="slide" alt=""></div>
</section>

<section class="view screen-view" id="view-screen" hidden aria-label="Screen time">
<header class="ahead-head"><h1>Screen time</h1><p class="view-note" id="screen-note"></p><button id="screen-manage" class="quiet">Manage</button></header>
<div class="screen-card" id="screen-card"></div>
</section>

<section class="view settings-view" id="view-settings" hidden aria-label="Settings">
<header class="ahead-head"><h1>Settings</h1><p class="view-note">Changes take effect right away.</p><button id="manage-button" class="quiet">Manage from a phone</button></header>
<div class="settings-card">
Expand All @@ -84,6 +89,12 @@ <h2 class="eyebrow">When no one is using it</h2>
<h2 class="eyebrow">Photos</h2>
<div class="setting"><p>Change every</p><div class="choices" data-setting="photoEvery"></div></div>
</section>
<section class="setting-group">
<h2 class="eyebrow">Screen time</h2>
<div class="setting"><p>Who has it</p><span class="setting-value" id="screen-who"></span><div class="choices update-now" id="screen-open"></div></div>
<div class="setting" id="screen-chime-row" hidden><p>When time’s up</p><span class="setting-value">A chime sounds</span><div class="choices update-now"><button id="screen-chime">Play the chime</button></div></div>
<p class="setting-note">A daily allowance and a timer, kept on the wall. Turning it on and changing time take the household&nbsp;PIN.</p>
</section>
<section class="setting-group">
<h2 class="eyebrow">Appearance</h2>
<div class="setting"><p>Calendar</p><div class="choices" data-setting="calendarView"></div></div>
Expand Down
72 changes: 72 additions & 0 deletions dist/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,78 @@ button.item:active{opacity:.6}
.dialog.manage .qr-plate:not([hidden])+.pair-code{font-size:3rem;margin:1rem 0 .3rem}
.dialog .wide{margin-top:2.25rem;height:4rem;font-size:1.5rem}

/* ---------- Screen time: a ring for what is left today ----------
One card like the lists and Settings. Left, centred: the person, the ring and the one control a passing child needs.
Right, read up close: the day's sum and its record, or, unlocked behind the PIN, the grown-up's controls. The ring is the
glance layer (Bricolage, the clock's scale); every row and button is Inter. While a timer runs app.js steps the ring
and its seconds once a second, as the clock steps; nothing here animates. */
.screen-card{flex:1;min-height:0;display:flex;background:var(--card);border-radius:1.5rem;box-shadow:0 .1rem .2rem var(--shade);padding:2.5rem 2.75rem}
.screen-card.hug{justify-content:center}
.screen-person{flex:0 0 31rem;display:flex;flex-direction:column;align-items:center;margin-right:3.75rem}
.screen-who{display:flex;align-items:center;font:600 calc(1.9rem*var(--ts))/1.2 var(--text)}
.screen-who .avatar{width:2.8rem;height:2.8rem;font-size:1.5rem;margin-right:.9rem}
.ring{position:relative;flex:none;width:30rem;height:30rem;margin-top:1.75rem}
.ring svg{width:100%;height:100%}
.ring-track{fill:none;stroke:var(--line);stroke-width:14}
.ring-arc{fill:none;stroke:var(--accent);stroke-width:14;stroke-linecap:round}
.ring-text{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center}
.ring-number{font:500 10.5rem/1 var(--display);letter-spacing:-.045em;font-variation-settings:"opsz" 96;font-variant-numeric:tabular-nums;margin-top:-.75rem}
/* A timer's seconds ("0:42", "24:37", "1:04:37") at the size that keeps each inside the ring's inner circle with 1.5rem
clear (measured in Chrome at 1920 × 1080); the weight and optical size stay the clock's. */
.ring-number.n4{font-size:10rem}
.ring-number.n5{font-size:8.2rem}
.ring-number.n7{font-size:6.4rem}
.ring-unit{font:500 calc(1.8rem*var(--ts))/1.2 var(--text);color:var(--ink-2);margin-top:.5rem}
.ring-up{font:600 4.4rem/1.05 var(--display);letter-spacing:-.025em;font-variation-settings:"opsz" 72}
.screen-line{min-height:2.2rem;margin-top:1.4rem;font:500 calc(1.6rem*var(--ts))/1.35 var(--text);color:var(--ink-2);font-variant-numeric:tabular-nums;text-align:center}
.screen-actions{display:flex;flex-direction:column;align-items:center;margin-top:1.2rem}
.screen-go{height:5rem;min-width:20rem;padding:0 2.5rem;border-radius:2.5rem;background:var(--accent);color:var(--on-accent);font:600 1.9rem/1 var(--text);text-align:center}
.screen-go.stop{background:var(--ink);color:var(--surface)}
.screen-go:disabled{background:var(--press);color:var(--ink-2)}
.screen-go:active:not(:disabled){opacity:.85}
.screen-hint{margin-top:.8rem;font-size:calc(1.4rem*var(--ts));color:var(--ink-2)}
.screen-side{flex:1;min-width:0;display:flex;flex-direction:column}
.screen-side .eyebrow{flex:none;margin-bottom:.4rem}
.screen-sum{flex:none;margin:0 0 2.25rem}
.screen-sum div{display:flex;align-items:baseline;padding:.85rem 0;border-bottom:.0625rem solid var(--line)}
.screen-sum dt{flex:1;font-size:calc(1.7rem*var(--ts));font-weight:500}
.screen-sum dd{margin:0;font-size:calc(1.7rem*var(--ts));font-variant-numeric:tabular-nums}
.screen-record{flex:1;min-height:0;overflow-y:auto;scrollbar-width:none}
.screen-record::-webkit-scrollbar{display:none}
.screen-record.has-more{-webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 4.5rem),transparent);mask-image:linear-gradient(to bottom,#000 calc(100% - 4.5rem),transparent)}
.screen-row{display:flex;align-items:center;min-height:calc(3.9rem*var(--ts));padding:.35rem 0;border-bottom:.0625rem solid var(--line);font-size:calc(1.5rem*var(--ts))}
.screen-row:last-child{border-bottom:0}
.screen-row .when{flex:0 0 8.5rem;color:var(--ink-2);font-variant-numeric:tabular-nums}
.screen-row .what{flex:1;min-width:0}
.screen-row .meta{display:block;font-size:calc(1.25rem*var(--ts));color:var(--ink-2)}
.screen-row .amount{flex:none;margin-left:1rem;font-weight:600;font-variant-numeric:tabular-nums}
.screen-row.removed .label,.screen-row.removed .amount{text-decoration:line-through;color:var(--ink-2);font-weight:400}
.screen-row .quiet{flex:none;margin:0 -1.25rem 0 .5rem}
.screen-empty{padding:.9rem 0;font-size:calc(1.5rem*var(--ts));color:var(--ink-2)}
/* Unlocked. The segments are Settings' own, sharing the column's width. */
.screen-manage-panel{flex:none;margin-bottom:1.75rem}
.screen-manage-panel .choices{margin-bottom:.8rem}
.screen-manage-panel .choices button{flex:1;min-width:0;padding:0 .6rem}
.screen-manage-panel .screen-go{display:block;width:100%;margin-top:.2rem}
.screen-daily{display:flex;align-items:center;margin-top:1.2rem}
.screen-daily p{flex:1;font-size:calc(1.6rem*var(--ts));font-weight:500}
.step{flex:none;width:3.5rem;height:3.5rem;border-radius:50%;background:var(--press);text-align:center;font:500 2rem/1 var(--text)}
.step:active:not(:disabled){background:var(--line)}
.step:disabled{opacity:.4}
.screen-daily .setting-value{min-width:7.5rem;text-align:center}
.screen-daily .setting-hint{margin-left:1rem}
.screen-off{display:block;margin:.9rem 0 0 -1.25rem}
.screen-off[aria-pressed=true]{color:var(--bad)}
/* Turning it on: whose it is and how long their days are, near the top so the keyboard never covers the name. */
.screen-setup{width:46rem;align-self:flex-start;margin-top:.5rem}
.screen-setup h2{font:600 3rem/1.1 var(--display);letter-spacing:-.02em;font-variation-settings:"opsz" 40}
.screen-setup-note{margin-top:.9rem;font-size:calc(1.6rem*var(--ts));line-height:1.4;color:var(--ink-2)}
.screen-again{display:flex;flex-wrap:wrap;margin-top:1.5rem}
.screen-again .quiet{display:flex;align-items:center;background:var(--press);color:var(--ink);margin:0 .8rem .8rem 0}
.screen-again .avatar{margin-right:.7rem}
.screen-setup .add-row{margin-top:1.75rem}
.screen-setup .screen-go{display:block;width:100%;margin-top:1.6rem}

/* Photos is a card like the week and the lists: the photo whole, over a blurred copy of itself. */
.photo-card{flex:1;min-height:0;position:relative;overflow:hidden;border-radius:1.5rem;background:#000;box-shadow:0 .1rem .2rem var(--shade);cursor:pointer}
/* By day the fill is the photo's own colours, a little dimmed so the photo stays the brightest thing in the card; at .55 a
Expand Down
4 changes: 2 additions & 2 deletions dist/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
}

/* Light. --selected, --shade, --tint, --star, --rain-ink (the rain chance as text) and --mono-ink (the letter of a
monogram with no colour of its own, on --ink-2) are the wall's; --bad and --good are the phone's (the wall has no
error colour). The phone's card shadow is its own, rgba(16,38,63,.08) in setup.css, not --shade. */
monogram with no colour of its own, on --ink-2) are the wall's; --good is the phone's; --bad is the phone's errors and,
on the wall, only screen time's "Tap again to turn off", always in words. The phone's card shadow is its own, rgba(16,38,63,.08) in setup.css, not --shade. */
:root,.theme-light{
--surface:var(--mist-100);--card:var(--mist-0);--ink:var(--navy-800);--ink-2:var(--navy-600);--line:var(--mist-300);
--accent:var(--blue-600);--on-accent:var(--mist-0);--press:var(--mist-200);
Expand Down
5 changes: 4 additions & 1 deletion households.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// One deployment serves several households. Each is a folder under data/households/<id>/ holding everything that is
// theirs: sources.json (calendars, lists, album, people, place), credentials.json (their Google and Todoist
// authorizations), settings.json, photos/ and cache/. Nothing is shared between folders, so a bug that mixes
// authorizations), settings.json, rewards.json (screen time, once someone turns it on), photos/ and cache/. Nothing is shared between folders, so a bug that mixes
// households has to cross a directory boundary to do it.
const fs = require('node:fs');
const path = require('node:path');
Expand All @@ -17,6 +17,7 @@ const {createMicrosoft} = require('./microsoft');
const {createCalDav} = require('./caldav');
const {createHomeAssistant} = require('./homeassistant');
const {createGoogleTasks} = require('./googletasks');
const {createRewards} = require('./rewards');

const validId = id => /^[a-z0-9][a-z0-9-]{0,39}$/.test(id || '');

Expand Down Expand Up @@ -91,6 +92,8 @@ function createHouseholds({root = path.join(__dirname, 'data/households'), log =
},
calendar: service.calendar, tasks: service.tasks, closeTask: service.closeTask, addTask: service.addTask, lists: local,
settings,
// Screen time (rewards.js), in the household's own time zone. No file until someone turns it on.
rewards: createRewards({file: path.join(dir, 'rewards.json'), zone: () => household.place().timezone}),
// The album's photos and the ones added from a phone, as one set. Added ones come first: whoever just sent
// a picture is standing there waiting to see it.
photoManifest() {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "gingham",
"version": "0.2.1",
"version": "0.3.0",
"private": true,
"license": "AGPL-3.0-only",
"description": "A family wall display for any Android tablet or photo frame: calendar, lists, weather and photos, self-hosted.",
"scripts": {
"start": "node server.js",
"check": "node --check server.js && node --check integrations.js && node --check dist/app.js && node --check api-client.js && node --check album.js && node --check households.js && node --check grants.js && node --check admin.js && node --check zone.js && node --check ics.js && node --check safe-fetch.js && node --check settings.js && node --check vault.js && node --check setup.js && node --check lists.js && node --check mdns.js && node --check folder.js && node --check countdowns.js && node --check microsoft.js && node --check caldav.js && node --check homeassistant.js && node --check googletasks.js && node --check uploads.js && node --check admin-api.js && node --check dist/setup-page.js && node --check dist/lists-page.js && node --check dist/admin-page.js && node --check dist/sw.js && node --check dist/photos-page.js && node --check dist/qr.js && node --check updates.js",
"check": "node --check server.js && node --check integrations.js && node --check dist/app.js && node --check api-client.js && node --check album.js && node --check households.js && node --check grants.js && node --check admin.js && node --check zone.js && node --check ics.js && node --check safe-fetch.js && node --check settings.js && node --check rewards.js && node --check vault.js && node --check setup.js && node --check lists.js && node --check mdns.js && node --check folder.js && node --check countdowns.js && node --check microsoft.js && node --check caldav.js && node --check homeassistant.js && node --check googletasks.js && node --check uploads.js && node --check admin-api.js && node --check dist/setup-page.js && node --check dist/lists-page.js && node --check dist/admin-page.js && node --check dist/sw.js && node --check dist/photos-page.js && node --check dist/qr.js && node --check updates.js",
"test": "node --test test/*.test.js"
}
}
9 changes: 9 additions & 0 deletions releases/v0.3.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
- Screen time: a child's minutes for the day in a ring on the wall, counting down in minutes and seconds.
- A timer anyone can start and stop. It stops by itself when the time runs out, and the frame plays a short chime.
- Adding or taking away time, and changing the daily allowance, take the household PIN.
- Every change goes in the day's record, and starting or stopping the timer can be undone for a few seconds.

Screen time is off until a grown-up turns it on in Settings → Screen time, which needs a household PIN set on the
setup page first. It keeps time only while its timer runs; it never measures a device. On a frame that runs the app,
install this one from Settings → Updates: it updates in place and keeps the frame paired. Calendars, lists, photos and
settings from 0.2.1 carry over as they are.
Loading
Loading