nativemind_freedome 0.1.0
nativemind_freedome: ^0.1.0 copied to clipboard
Fulldome UI kit of the NativeMind framework: dome picker, connection and engine status, player controls, show timeline, playlists and cues, calibration wizard, projector-rig editor, audio-zone mixer, [...]
example/lib/main.dart
// The nativemind_freedome prototype (task 4.SDK.9): every widget over the fake ports, light / dark, five languages,
// a scripted copilot model. For the owner's review and the Figma export.
// written by Claude Code (generated by Claude Code), 2026-10-20; SDD: flows/sdd-nativemind-freedome
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:nativemind_copilot/nativemind_copilot.dart';
import 'package:nativemind_core/nativemind_core.dart';
import 'package:nativemind_freedome/nativemind_freedome.dart';
import 'package:nativemind_freedome/testing.dart';
// written by Claude Code (generated by Claude Code), 2026-10-20; SDD: flows/sdd-nativemind-freedome
void main() {
NmFreedomeL10n.installExtraLanguages();
runApp(const FreedomeKitPrototype());
}
/// A scripted on-device model stand-in: asks for the player state, then pauses / resumes / reads domes.
// written by Claude Code (generated by Claude Code), 2026-10-20; SDD: flows/sdd-nativemind-freedome
class DemoLlm implements NmLlmProvider {
var _n = 0;
// written by Claude Code (generated by Claude Code), 2026-10-20; SDD: flows/sdd-nativemind-freedome
@override
bool get holdsKeys => false;
// written by Claude Code (generated by Claude Code), 2026-10-20; SDD: flows/sdd-nativemind-freedome
@override
Stream<NmLlmChunk> complete(NmLlmRequest r) async* {
await Future<void>.delayed(const Duration(milliseconds: 400));
final last = r.messages.last;
final user = r.messages.lastWhere((m) => m.role == NmLlmRole.user).text.toLowerCase();
if (last.role == NmLlmRole.user && r.tools.isNotEmpty) {
final tool = user.contains('pause') || user.contains('пауз')
? 'pause'
: user.contains('calibr') || user.contains('калибр')
? 'calibration_status'
: 'list_domes';
yield NmLlmToolCallsChunk([NmLlmToolCall(id: 'call${_n++}', name: tool, args: const {})]);
return;
}
final result = last.role == NmLlmRole.tool ? last.text : '';
for (final w in 'Done. ${result.length > 160 ? '${result.substring(0, 160)}…' : result}'.split(' ')) {
await Future<void>.delayed(const Duration(milliseconds: 30));
yield NmLlmTextChunk('$w ');
}
}
}
/// The prototype app.
// written by Claude Code (generated by Claude Code), 2026-10-20; SDD: flows/sdd-nativemind-freedome
class FreedomeKitPrototype extends StatefulWidget {
/// Creates the app.
// written by Claude Code (generated by Claude Code), 2026-10-20; SDD: flows/sdd-nativemind-freedome
const FreedomeKitPrototype({super.key});
// written by Claude Code (generated by Claude Code), 2026-10-20; SDD: flows/sdd-nativemind-freedome
@override
State<FreedomeKitPrototype> createState() => _FreedomeKitPrototypeState();
}
// written by Claude Code (generated by Claude Code), 2026-10-20; SDD: flows/sdd-nativemind-freedome
class _FreedomeKitPrototypeState extends State<FreedomeKitPrototype> {
final _fakes = NmFreedomeFakes();
final _llm = DemoLlm();
Timer? _meters;
var _tick = 0;
var _dark = false;
var _locale = const Locale('en');
var _page = 0;
// written by Claude Code (generated by Claude Code), 2026-10-20; SDD: flows/sdd-nativemind-freedome
@override
void initState() {
super.initState();
_meters = Timer.periodic(const Duration(milliseconds: 50), (_) => _fakes.audioZones.emit(NmFreedomeFakes.demoLevels(_tick++)));
}
// written by Claude Code (generated by Claude Code), 2026-10-20; SDD: flows/sdd-nativemind-freedome
@override
void dispose() {
_meters?.cancel();
super.dispose();
}
// written by Claude Code (generated by Claude Code), 2026-10-20; SDD: flows/sdd-nativemind-freedome
@override
Widget build(BuildContext context) {
final pages = <(String, NmIconData, Widget)>[
(
'Connection',
NmFugue.globeNetwork,
Column(
children: [
const NmDomeConnectionStatus(compact: true),
const SizedBox(height: 12),
NmDomePicker(onScanQr: (_) async => 'freedome://pair?code=DEMO', onOpenSettings: () {}),
const SizedBox(height: 12),
NmDomeConnectionStatus(onRetry: () {}, onSignIn: () {}),
const SizedBox(height: 12),
const NmEngineStatusPanel(),
],
),
),
(
'Player',
NmFugue.mediaPlayer,
const Column(
children: [
NmDomePlayerControls(compact: true),
SizedBox(height: 12),
NmDomePlayerControls(),
SizedBox(height: 12),
NmShowTimeline(),
SizedBox(height: 12),
NmPlaylistView(),
SizedBox(height: 12),
NmCueList(),
],
),
),
(
'Calibration',
NmFugue.target,
Column(
children: [
NmCalibrationWizard(onCapture: (_) async => Uint8List.fromList(List.filled(16, 1))),
const SizedBox(height: 12),
const NmCalibrationWizard(plan: NmDomeCalibrationPlan(kind: NmDomeCalibrationKind.audio)),
const SizedBox(height: 12),
const NmProjectorRigEditor(),
],
),
),
('Audio', NmFugue.speakerVolumeControl, const NmAudioZoneMixer()),
('Venues', NmFugue.map, const NmVenueMap(pins: nmDemoVenues)),
('Copilot', NmFugue.robot, SizedBox(height: 640, child: NmFreedomeCopilot(provider: _llm))),
];
return NmFreedomeScope.ports(
ports: _fakes.ports,
child: MaterialApp(
title: 'Freedome kit',
debugShowCheckedModeBanner: false,
locale: _locale,
supportedLocales: const [Locale('en'), Locale('ru'), Locale('th'), Locale('hi'), Locale('zh')],
localizationsDelegates: NmApp.delegates([NmFreedomeL10n.delegate, NmCopilot.delegate]),
theme: const NmTheme().toThemeData(Brightness.light),
darkTheme: const NmTheme().toThemeData(Brightness.dark),
themeMode: _dark ? ThemeMode.dark : ThemeMode.light,
home: Builder(
builder: (context) => Scaffold(
appBar: AppBar(
title: Text('Freedome kit · ${pages[_page].$1}'),
actions: [
for (final l in const ['en', 'ru', 'th', 'hi', 'zh'])
TextButton(
onPressed: () => setState(() => _locale = Locale(l)),
child: Text(l, style: TextStyle(fontWeight: _locale.languageCode == l ? FontWeight.bold : null)),
),
NmFreedomeIconButton(icon: _dark ? NmFugue.lightBulb : NmFugue.lightBulbOff, label: 'Theme', onPressed: () => setState(() => _dark = !_dark)),
],
),
body: Row(
children: [
NavigationRail(
selectedIndex: _page,
onDestinationSelected: (i) => setState(() => _page = i),
labelType: NavigationRailLabelType.all,
destinations: [for (final p in pages) NavigationRailDestination(icon: NmIcon(p.$2), label: Text(p.$1))],
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 960), child: pages[_page].$3),
),
),
),
],
),
),
),
),
);
}
}