Design: first launch for a new user
Paul, 5 November 2026: welcome with Ollie, choose the media server (Emby, Plex, Jellyfin, Silo), sign in while Drift reads the library, pick channels suggested from it, who watches (Grown-ups PIN, Kids profile), all set. For approval only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
"""First launch for a new user, design proposal (Paul, 5 November 2026): welcome with Ollie, choose your media server, sign in,
|
||||
Drift reads the library, pick your channels, who watches (the Grown-ups PIN and a Kids profile), all set. The starry style and
|
||||
drawing come from the profiles design (build-profiles.py) and the cast's model sheet. A design for approval only."""
|
||||
import ast
|
||||
from pathlib import Path
|
||||
HERE = Path(__file__).resolve().parent
|
||||
PROFILES = HERE.parent / 'profiles' / 'build-profiles.py'
|
||||
# Take build-profiles.py's set-up and drawing, without its screens.
|
||||
tree = ast.parse(PROFILES.read_text())
|
||||
keep = []
|
||||
for n in tree.body:
|
||||
if isinstance(n, ast.Expr) and isinstance(n.value, ast.Call) and getattr(n.value.func, 'id', '') != 'exec': continue
|
||||
if isinstance(n, (ast.Import, ast.ImportFrom, ast.FunctionDef, ast.Expr)) or (isinstance(n, ast.Assign) and not any(isinstance(t, ast.Name) and t.id in ('cards', 'kinds', 'kids', 'rows', 'rail', 'overlay', 'x0') for t in n.targets)):
|
||||
keep.append(n)
|
||||
if isinstance(n, ast.Assign) and any(isinstance(t, ast.Name) and t.id == 'GROWNUP' for t in n.targets): break
|
||||
g = {'__file__': str(PROFILES)}
|
||||
exec(compile(ast.Module(keep, []), str(PROFILES), 'exec'), g)
|
||||
globals().update({k: v for k, v in g.items() if k not in ('HERE', '__file__')})
|
||||
import builtins
|
||||
def save(name):
|
||||
g['im'].resize((W, H), Image.LANCZOS).save(HERE / name); print(name)
|
||||
def D(): return g['d']
|
||||
GREEN = (110, 220, 170)
|
||||
|
||||
def steps(at):
|
||||
"""Six dots along the top: where you are in setting up."""
|
||||
names = ['Welcome', 'Server', 'Sign in', 'Channels', 'Who watches', 'All set']
|
||||
x = 960 - (len(names) - 1) * 110
|
||||
for i, n in enumerate(names):
|
||||
cx = x + i * 220
|
||||
col = YELLOW if i == at else (GREEN if i < at else (60, 80, 120))
|
||||
circ((cx, 60), 9 if i == at else 7, col)
|
||||
if i < len(names) - 1: D().line((S(cx + 16), S(60), S(cx + 204), S(60)), fill=(GREEN if i < at else (40, 60, 100)), width=SW(3))
|
||||
text((cx, 80), n, reg(22), WHITE if i == at else MUTED, 'ma')
|
||||
|
||||
def button(x, y, w, label, focus=True):
|
||||
box(x, y, w, 70, fill=YELLOW if focus else PANEL, r=10)
|
||||
text((x + w / 2, y + 16), label, semi(32), NAVY if focus else WHITE, 'ma')
|
||||
|
||||
# 1. Welcome.
|
||||
screen(); steps(0)
|
||||
avatar('ollie', (960, 380), 150)
|
||||
text((960, 580), 'Welcome to Orbit', anton(80), WHITE, 'ma')
|
||||
text((960, 690), 'Proper TV channels, made from your own films and series.', reg(36), MUTED, 'ma')
|
||||
text((960, 740), "It takes about five minutes. I'm Ollie, and I'll show you round.", reg(32), MUTED, 'ma')
|
||||
button(810, 840, 300, "Let's go")
|
||||
keys([('OK', 'Start')])
|
||||
save('first-run-1-welcome.png')
|
||||
|
||||
# 2. Choose your server.
|
||||
screen(); steps(1)
|
||||
text((960, 170), 'Where are your films and series?', anton(60), WHITE, 'ma')
|
||||
text((960, 260), 'Choose the media server they live on. Orbit only reads it, and never changes or copies anything.', reg(30), MUTED, 'ma')
|
||||
servers = [('Emby', 'Sign in with your Emby account', (82, 181, 75)), ('Plex', 'Link with a code at plex.tv/link', (229, 160, 13)),
|
||||
('Jellyfin', 'Sign in with your Jellyfin account', (170, 92, 195)), ('Silo', 'Sign in with your Silo account', (69, 184, 245))]
|
||||
for i, (name, how, col) in enumerate(servers):
|
||||
x = 150 + i * 420; y = 360
|
||||
focus = i == 0
|
||||
box(x, y, 380, 400, fill=NAVY if focus else PANEL, r=16, outline=YELLOW if focus else None, width=4)
|
||||
# A plain server mark in its colour: our own drawing, not the product's logo.
|
||||
for k in range(3): box(x + 130, y + 60 + k * 44, 120, 34, fill=col, r=8)
|
||||
for k in range(3): circ((x + 230, y + 77 + k * 44), 6, NAVY)
|
||||
text((x + 190, y + 230), name, anton(50), WHITE, 'ma')
|
||||
text((x + 190, y + 310), how, reg(24), MUTED, 'ma')
|
||||
text((960, 820), "Not sure? It's whichever app you watch your own films on. You can add another later in Settings.", reg(28), MUTED, 'ma')
|
||||
keys([('◄►', 'Choose'), ('OK', 'Select'), ('Back', 'Welcome')])
|
||||
save('first-run-2-server.png')
|
||||
|
||||
# 3. Sign in, then Drift reads the library.
|
||||
screen(); steps(2)
|
||||
text((480, 170), 'Sign in to Emby', anton(56), WHITE, 'ma')
|
||||
for i, (label, value, focus) in enumerate((('Server address', 'https://media.example.home', False), ('Username', 'paul', False), ('Password', '••••••••••', True))):
|
||||
y = 270 + i * 130
|
||||
text((180, y), label, semi(28), MUTED)
|
||||
box(180, y + 40, 600, 70, fill=NAVY if focus else PANEL, r=10, outline=YELLOW if focus else None)
|
||||
text((206, y + 56), value, reg(32), WHITE)
|
||||
button(180, 690, 600, 'Sign in', focus=False)
|
||||
text((180, 790), 'Your password is never saved: only a sign-in token, encrypted on this box.', reg(24), MUTED)
|
||||
box(1000, 200, 800, 640, fill=DEEP, r=18)
|
||||
drift((1400, 380), 110, (53, 214, 242))
|
||||
text((1400, 540), "Drift's reading your library", semi(40), WHITE, 'ma')
|
||||
box(1120, 610, 560, 18, fill=(30, 50, 90), r=9); box(1120, 610, 380, 18, fill=GREEN, r=9)
|
||||
text((1400, 650), '12,480 films · 3,102 series · 68%', reg(30), MUTED, 'ma')
|
||||
text((1400, 720), 'Next: choosing your channels', reg(26), MUTED, 'ma')
|
||||
keys([('▲▼', 'Move'), ('OK', 'Type'), ('Back', 'Servers')])
|
||||
save('first-run-3-sign-in.png')
|
||||
|
||||
# 4. Pick your channels.
|
||||
screen(); steps(3)
|
||||
text((960, 150), 'Pick your channels', anton(60), WHITE, 'ma')
|
||||
text((960, 240), 'Ticked ones suit your library best. Each runs its own schedule, all day, every day.', reg(30), MUTED, 'ma')
|
||||
chans = [('orbit_one_epg', 'Sitcoms and favourites', 184, True), ('paradox_epg', 'Cult sci-fi and fantasy', 96, True), ('orbit_terra_epg', 'Nature and wildlife', 41, True),
|
||||
('evidence_epg', 'Crime and investigation', 77, True), ('orbit_action_epg', 'Action films', 312, True), ('orbit_horror_epg', 'Horror films', 205, False),
|
||||
('orbit_tots_epg', 'For the youngest', 23, True), ('toonz_epg', 'Cartoons', 64, True), ('orbit_ring_x_epg', 'Wrestling', 18, False),
|
||||
('orbit_romance_epg', 'Romance films', 140, False), ('orbit_gold_epg', 'Classic comedy', 58, True), ('orbit_drive_in_epg', 'Weekend film nights', 0, True)]
|
||||
for i, (logo_name, what, count, on) in enumerate(chans):
|
||||
x = 120 + (i % 4) * 430; y = 310 + (i // 4) * 170
|
||||
focus = i == 5
|
||||
box(x, y, 400, 150, fill=NAVY if focus else PANEL, r=14, outline=YELLOW if focus else None)
|
||||
logo(RES / f'{logo_name}.webp', x + 22, y + 20, 170)
|
||||
text((x + 24, y + 86), what, semi(24), WHITE)
|
||||
text((x + 24, y + 116), f'{count} in your library' if count else 'Picks from your films', reg(20), MUTED)
|
||||
box(x + 330, y + 28, 46, 40, fill=GREEN if on else (40, 60, 100), r=8)
|
||||
if on: D().line([(S(x + 341), S(y + 48)), (S(x + 351), S(y + 59)), (S(x + 367), S(y + 37))], fill=NAVY, width=SW(6))
|
||||
button(760, 840, 400, 'Next: 9 channels')
|
||||
text((960, 930), 'Change them any time in Settings › Channels.', reg(24), MUTED, 'ma')
|
||||
keys([('◄►▲▼', 'Move'), ('OK', 'Tick'), ('Back', 'Sign in')])
|
||||
save('first-run-4-channels.png')
|
||||
|
||||
# 5. Who watches?
|
||||
screen(); steps(4)
|
||||
text((960, 150), 'Who watches?', anton(60), WHITE, 'ma')
|
||||
text((960, 240), 'Set a PIN for Grown-ups, and add a Kids profile if there are little ones about.', reg(30), MUTED, 'ma')
|
||||
box(300, 320, 600, 480, fill=PANEL, r=18)
|
||||
avatar(GROWNUP, (600, 440), 90)
|
||||
text((600, 550), 'Grown-ups', semi(40), WHITE, 'ma')
|
||||
text((600, 605), 'Everything, including After Dark', reg(26), MUTED, 'ma')
|
||||
for k in range(4): box(470 + k * 70, 660, 54, 66, fill=YELLOW if k == 0 else (30, 50, 90), r=8)
|
||||
text((497, 670), '4', semi(40), NAVY, 'ma')
|
||||
box(1020, 320, 600, 480, fill=NAVY, r=18, outline=YELLOW, width=4)
|
||||
avatar('ollie', (1320, 440), 90)
|
||||
text((1320, 550), 'Kids', semi(40), WHITE, 'ma')
|
||||
text((1320, 605), 'Tots, cartoons and the Arcade', reg(26), MUTED, 'ma')
|
||||
box(1170, 660, 300, 66, fill=GREEN, r=10); text((1320, 672), 'Add Kids', semi(32), NAVY, 'ma')
|
||||
text((960, 850), 'You can skip this: Orbit then opens straight to everything, with no PIN.', reg(26), MUTED, 'ma')
|
||||
keys([('◄►', 'Choose'), ('OK', 'Select'), ('Back', 'Channels')])
|
||||
save('first-run-5-who-watches.png')
|
||||
|
||||
# 6. All set.
|
||||
screen(); steps(5)
|
||||
for (fn, cx, cy, r) in ((mascot, 960, 420, 120),):
|
||||
fn((cx, cy), r)
|
||||
blaze((640, 470), 70, (255, 138, 61)); zip_((780, 330), 60, (255, 79, 163)); drift((1140, 330), 64, (53, 214, 242)); luna((1290, 470), 66, (167, 123, 255))
|
||||
bumble((1150, 600), 54, None)
|
||||
text((960, 740), "You're on air", anton(80), WHITE, 'ma')
|
||||
text((960, 850), '9 channels are planning their first week. The Drive-In opens on Friday.', reg(32), MUTED, 'ma')
|
||||
button(780, 920, 360, 'Start watching')
|
||||
keys([('OK', 'Start watching')], y=1020)
|
||||
save('first-run-6-all-set.png')
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 186 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 277 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 175 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
Reference in New Issue
Block a user