App structure: a swipe-in menu shell; pages/, link/, actions/; Settings
/ crosscheck (push) Successful in 1s
CI / Analyze & Test (push) Successful in 13s
CI / Debug APK (push) Successful in 27s

The shell (shell.dart): a drawer from the left edge or each page's menu button.
Dash, Signals, Captures; Tools (Camera, Link test, Orin power options), the
rarely used ones, after pirate-station-aide; Settings (gateway address, dark/bright,
build-time theme and landing). Landing view at build time: YUKART_HOME=dash|signals,
signals for now. Dash, Signals and Captures stay alive while hidden; Captures has its
own navigator, so a run survives switching views. Back goes inside Captures, then to
the landing view, then out. The mock banner sits over every page.

main.dart is startup only; the old home screen is pages/signals.dart; dialogs and
sheets moved to actions/; the protocol client, USB binding and Reading to link/.
lib/README.md catalogues it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
This commit is contained in:
2026-10-14 14:08:56 +09:00
parent 3aeb09d61f
commit 4cf7a786d6
22 changed files with 941 additions and 579 deletions
+71
View File
@@ -0,0 +1,71 @@
// The link to the Orin at a glance: the state chip, and the details behind it.
import 'package:flutter/material.dart';
import '../link/boundary.dart';
import '../link/net.dart';
import '../theme/palette.dart';
/// CONNECTED / CONNECTING / DOWN in good / caution / alarm.
class LinkChip extends StatelessWidget {
const LinkChip({super.key, required this.boundary});
final Boundary boundary;
@override
Widget build(BuildContext context) {
final p = AppTheme.of(context);
final (color, label) = switch (boundary.state) {
LinkState.up => (p.good, 'CONNECTED'),
LinkState.connecting => (p.caution, 'CONNECTING'),
LinkState.down => (p.alarm, 'DOWN'),
};
return Chip(
label: Text(
label,
style: TextStyle(color: color, fontWeight: FontWeight.w600),
),
backgroundColor: color.withValues(alpha: 0.3),
side: BorderSide.none,
visualDensity: VisualDensity.compact,
);
}
}
/// Address, gateway, last-message age, clock offset, USB link. Fixed-width numbers,
/// padded with figure spaces (U+2007, digit-wide): nothing reflows as a value gains a digit.
class LinkDetails extends StatelessWidget {
const LinkDetails({super.key, required this.boundary, required this.usb});
final Boundary boundary;
final UsbLink usb;
@override
Widget build(BuildContext context) {
final b = boundary;
String pad(String v, int n) => v.padLeft(n, ' ');
final age = b.lastMessage == null
? pad('—', 4)
: pad(
'${DateTime.now().difference(b.lastMessage!).inMilliseconds.clamp(0, 9999)}',
4,
);
final offset = b.clockOffset == null
? pad('—', 6)
: pad((b.clockOffset!.inMilliseconds / 1000).toStringAsFixed(1), 6);
const fixed = TextStyle(fontFeatures: [FontFeature.tabularFigures()]);
return Wrap(
spacing: 16,
runSpacing: 4,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text(b.url, style: const TextStyle(fontFamily: 'monospace')),
Text(b.detail),
Text('last msg $age ms', style: fixed),
Text('Orin−tablet clock $offset s', style: fixed),
if (usb.supported)
Text(
'USB link ${usb.up ? 'up' : 'absent'}${usb.bound ? ', bound' : ''}',
),
],
);
}
}