diff --git a/design/first-run/build-first-run.py b/design/first-run/build-first-run.py new file mode 100644 index 0000000..d569df4 --- /dev/null +++ b/design/first-run/build-first-run.py @@ -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') diff --git a/design/first-run/first-run-1-welcome.png b/design/first-run/first-run-1-welcome.png new file mode 100644 index 0000000..9bb47a5 Binary files /dev/null and b/design/first-run/first-run-1-welcome.png differ diff --git a/design/first-run/first-run-2-server.png b/design/first-run/first-run-2-server.png new file mode 100644 index 0000000..99b6623 Binary files /dev/null and b/design/first-run/first-run-2-server.png differ diff --git a/design/first-run/first-run-3-sign-in.png b/design/first-run/first-run-3-sign-in.png new file mode 100644 index 0000000..f55be71 Binary files /dev/null and b/design/first-run/first-run-3-sign-in.png differ diff --git a/design/first-run/first-run-4-channels.png b/design/first-run/first-run-4-channels.png new file mode 100644 index 0000000..7c32d34 Binary files /dev/null and b/design/first-run/first-run-4-channels.png differ diff --git a/design/first-run/first-run-5-who-watches.png b/design/first-run/first-run-5-who-watches.png new file mode 100644 index 0000000..71fa314 Binary files /dev/null and b/design/first-run/first-run-5-who-watches.png differ diff --git a/design/first-run/first-run-6-all-set.png b/design/first-run/first-run-6-all-set.png new file mode 100644 index 0000000..e848966 Binary files /dev/null and b/design/first-run/first-run-6-all-set.png differ