initial commit
Some checks failed
xiao_pet_tracker / semantic-pull-request (push) Has been cancelled
xiao_pet_tracker / build (push) Has been cancelled
xiao_pet_tracker / spell-check (push) Has been cancelled

This commit is contained in:
2025-02-23 20:50:34 +01:00
commit 2fe59fee0d
360 changed files with 14384 additions and 0 deletions

20
lib/app/app.dart Normal file
View File

@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:xiao_pet_tracker/app/view/app.dart';
import 'package:xiao_pet_tracker/xiao_connector/cubit/xiao_connector_cubit.dart';
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (BuildContext context) => XiaoConnectorCubit()..init(),
),
],
child: const AppView(),
);
}
}

106
lib/app/view/app.dart Normal file
View File

@@ -0,0 +1,106 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:forui/forui.dart';
import 'package:xiao_pet_tracker/app_router/app_router.dart';
import 'package:xiao_pet_tracker/bootstrap.dart';
import 'package:xiao_pet_tracker/l10n/l10n.dart';
class AppView extends StatefulWidget {
const AppView({super.key});
@override
State<AppView> createState() => _AppState();
}
class _AppState extends State<AppView> {
BluetoothAdapterState _adapterState = BluetoothAdapterState.unknown;
late StreamSubscription<BluetoothAdapterState> _adapterStateStateSubscription;
@override
void initState() {
super.initState();
_adapterStateStateSubscription =
FlutterBluePlus.adapterState.listen((state) {
_adapterState = state;
if (mounted) {
setState(() {});
}
});
}
@override
void dispose() {
_adapterStateStateSubscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
// Widget screen = _adapterState == BluetoothAdapterState.on
// ? const HomePage()
// : BluetoothOffScreen(
// adapterState: _adapterState,
// );
// only allow portrait orientations
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
return MaterialApp.router(
title: 'Xiao Pet Tracker',
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
// localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
routerConfig: getIt<AppRouter>().config(),
theme: ThemeData(),
darkTheme: ThemeData.dark(),
builder: (context, child) => FTheme(
data: MediaQuery.of(context).platformBrightness == Brightness.light
? FThemes.zinc.light
: FThemes.zinc.dark,
child: child!,
),
);
}
}
// This observer listens for Bluetooth Off and dismisses the DeviceScreen
class BluetoothAdapterStateObserver extends NavigatorObserver {
StreamSubscription<BluetoothAdapterState>? _adapterStateSubscription;
@override
void didPush(Route route, Route? previousRoute) {
super.didPush(route, previousRoute);
if (route.settings.name == '/DeviceScreen') {
// Start listening to Bluetooth state changes when a new route is pushed
_adapterStateSubscription ??= FlutterBluePlus.adapterState.listen(
(state) {
if (state != BluetoothAdapterState.on) {
// Pop the current route if Bluetooth is off
navigator?.pop();
}
},
);
}
}
@override
void didPop(Route route, Route? previousRoute) {
super.didPop(route, previousRoute);
// Cancel the subscription when the route is popped
_adapterStateSubscription?.cancel();
_adapterStateSubscription = null;
}
}

View File

@@ -0,0 +1,89 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
import 'package:xiao_pet_tracker/app_router/app_router.gr.dart';
@AutoRouterConfig()
class AppRouter extends RootStackRouter {
@override
RouteType get defaultRouteType => const RouteType.material();
@override
List<AutoRoute> get routes => [
AutoRoute(
page: MainRoute.page,
path: '/',
children: [
AutoRoute(
page: XiaoConnectorRoute.page,
),
AutoRoute(
page: RecordingsRoute.page,
children: [],
),
],
),
AutoRoute(
page: RecordingsDetailsRoute.page,
),
];
}
@RoutePage()
class MainPage extends StatelessWidget {
const MainPage({super.key});
@override
Widget build(BuildContext context) {
return AutoTabsScaffold(
// transitionBuilder: (context, child, animation) => AnimatedBuilder(
// animation: animation,
// child: child,
// // builder: (BuildContext context, Widget? child) {
// // const begin = Offset(0.0, 0.1);
// // const end = Offset.zero;
// // final tween = Tween(begin: begin, end: end);
// // final offsetAnimation = animation.drive(tween);
// // return SlideTransition(
// // position: offsetAnimation,
// // child: Transform.scale(
// // alignment: Alignment.bottomCenter,
// // scale: animation.value,
// // child: ClipPath(
// // clipper: CircularRevealClipper(
// // fraction: animation.value,
// // centerAlignment: Alignment.bottomCenter,
// // // centerOffset: centerOffset,
// // // minRadius: minRadius,
// // // maxRadius: maxRadius,
// // ),
// // child: child,
// // ),
// // ),
// // );
// // },
// ),
animationDuration: const Duration(milliseconds: 650),
routes: const [
XiaoConnectorRoute(),
RecordingsRoute(),
],
bottomNavigationBuilder: (context, tabsRouter) {
return FBottomNavigationBar(
index: tabsRouter.activeIndex,
onChange: tabsRouter.setActiveIndex,
children: [
FBottomNavigationBarItem(
label: const Text('Xiao Connector'),
icon: FIcon(FAssets.icons.microchip),
),
FBottomNavigationBarItem(
label: const Text('Recordings'),
icon: FIcon(FAssets.icons.disc3),
),
],
);
},
);
}
}

View File

@@ -0,0 +1,149 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// AutoRouterGenerator
// **************************************************************************
// ignore_for_file: type=lint
// coverage:ignore-file
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:auto_route/auto_route.dart' as _i6;
import 'package:flutter/material.dart' as _i7;
import 'package:xiao_pet_tracker/app_router/app_router.dart' as _i1;
import 'package:xiao_pet_tracker/recordings/view/recordings_details.dart'
as _i2;
import 'package:xiao_pet_tracker/recordings/view/recordings_page.dart' as _i3;
import 'package:xiao_pet_tracker/xiao_connector/view/xiao_connector_page.dart'
as _i4;
import 'package:xiao_pet_tracker/xiao_connector/view/xiao_data_page.dart'
as _i5;
/// generated route for
/// [_i1.MainPage]
class MainRoute extends _i6.PageRouteInfo<void> {
const MainRoute({List<_i6.PageRouteInfo>? children})
: super(
MainRoute.name,
initialChildren: children,
);
static const String name = 'MainRoute';
static _i6.PageInfo page = _i6.PageInfo(
name,
builder: (data) {
return const _i1.MainPage();
},
);
}
/// generated route for
/// [_i2.RecordingsDetailsPage]
class RecordingsDetailsRoute
extends _i6.PageRouteInfo<RecordingsDetailsRouteArgs> {
RecordingsDetailsRoute({
required String uuid,
required String type,
_i7.Key? key,
List<_i6.PageRouteInfo>? children,
}) : super(
RecordingsDetailsRoute.name,
args: RecordingsDetailsRouteArgs(
uuid: uuid,
type: type,
key: key,
),
initialChildren: children,
);
static const String name = 'RecordingsDetailsRoute';
static _i6.PageInfo page = _i6.PageInfo(
name,
builder: (data) {
final args = data.argsAs<RecordingsDetailsRouteArgs>();
return _i2.RecordingsDetailsPage(
uuid: args.uuid,
type: args.type,
key: args.key,
);
},
);
}
class RecordingsDetailsRouteArgs {
const RecordingsDetailsRouteArgs({
required this.uuid,
required this.type,
this.key,
});
final String uuid;
final String type;
final _i7.Key? key;
@override
String toString() {
return 'RecordingsDetailsRouteArgs{uuid: $uuid, type: $type, key: $key}';
}
}
/// generated route for
/// [_i3.RecordingsPage]
class RecordingsRoute extends _i6.PageRouteInfo<void> {
const RecordingsRoute({List<_i6.PageRouteInfo>? children})
: super(
RecordingsRoute.name,
initialChildren: children,
);
static const String name = 'RecordingsRoute';
static _i6.PageInfo page = _i6.PageInfo(
name,
builder: (data) {
return const _i3.RecordingsPage();
},
);
}
/// generated route for
/// [_i4.XiaoConnectorPage]
class XiaoConnectorRoute extends _i6.PageRouteInfo<void> {
const XiaoConnectorRoute({List<_i6.PageRouteInfo>? children})
: super(
XiaoConnectorRoute.name,
initialChildren: children,
);
static const String name = 'XiaoConnectorRoute';
static _i6.PageInfo page = _i6.PageInfo(
name,
builder: (data) {
return const _i4.XiaoConnectorPage();
},
);
}
/// generated route for
/// [_i5.XiaoDataPage]
class XiaoDataRoute extends _i6.PageRouteInfo<void> {
const XiaoDataRoute({List<_i6.PageRouteInfo>? children})
: super(
XiaoDataRoute.name,
initialChildren: children,
);
static const String name = 'XiaoDataRoute';
static _i6.PageInfo page = _i6.PageInfo(
name,
builder: (data) {
return const _i5.XiaoDataPage();
},
);
}

44
lib/bootstrap.dart Normal file
View File

@@ -0,0 +1,44 @@
import 'dart:async';
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:flutter/widgets.dart';
import 'package:get_it/get_it.dart';
import 'package:xiao_pet_tracker/app_router/app_router.dart';
import 'package:xiao_pet_tracker/objectbox.dart';
final getIt = GetIt.instance;
class AppBlocObserver extends BlocObserver {
const AppBlocObserver();
@override
void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) {
super.onChange(bloc, change);
log('onChange(${bloc.runtimeType}, $change)');
}
@override
void onError(BlocBase<dynamic> bloc, Object error, StackTrace stackTrace) {
log('onError(${bloc.runtimeType}, $error, $stackTrace)');
super.onError(bloc, error, stackTrace);
}
}
Future<void> bootstrap(FutureOr<Widget> Function() builder) async {
FlutterError.onError = (details) {
log(details.exceptionAsString(), stackTrace: details.stack);
};
Bloc.observer = const AppBlocObserver();
// Add cross-flavor configuration here
WidgetsFlutterBinding.ensureInitialized();
final AppRouter appRouter = getIt.registerSingleton(AppRouter());
final ObjectBox objectBox = getIt.registerSingleton(await ObjectBox.create());
runApp(await builder());
}

7
lib/l10n/arb/app_en.arb Normal file
View File

@@ -0,0 +1,7 @@
{
"@@locale": "en",
"counterAppBarTitle": "Counter",
"@counterAppBarTitle": {
"description": "Text shown in the AppBar of the Counter Page"
}
}

7
lib/l10n/arb/app_es.arb Normal file
View File

@@ -0,0 +1,7 @@
{
"@@locale": "es",
"counterAppBarTitle": "Contador",
"@counterAppBarTitle": {
"description": "Texto mostrado en la AppBar de la página del contador"
}
}

8
lib/l10n/l10n.dart Normal file
View File

@@ -0,0 +1,8 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
export 'package:flutter_gen/gen_l10n/app_localizations.dart';
extension AppLocalizationsX on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this);
}

View File

@@ -0,0 +1,8 @@
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
import 'package:xiao_pet_tracker/app/app.dart';
import 'package:xiao_pet_tracker/bootstrap.dart';
void main() {
FlutterBluePlus.setLogLevel(LogLevel.verbose);
bootstrap(() => const App());
}

6
lib/main_production.dart Normal file
View File

@@ -0,0 +1,6 @@
import 'package:xiao_pet_tracker/app/app.dart';
import 'package:xiao_pet_tracker/bootstrap.dart';
void main() {
bootstrap(() => const App());
}

6
lib/main_staging.dart Normal file
View File

@@ -0,0 +1,6 @@
import 'package:xiao_pet_tracker/app/app.dart';
import 'package:xiao_pet_tracker/bootstrap.dart';
void main() {
bootstrap(() => const App());
}

129
lib/objectbox-model.json Normal file
View File

@@ -0,0 +1,129 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:8883777039160106085",
"lastPropertyId": "28:5600095333294587898",
"name": "CapturePoint",
"properties": [
{
"id": "1:7031141970833098443",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:9089711773119462722",
"name": "type",
"type": 9
},
{
"id": "12:8018734828546727200",
"name": "uuid",
"type": 9
},
{
"id": "21:5571567596004635004",
"name": "rotationX",
"type": 6
},
{
"id": "22:128950109858162458",
"name": "rotationY",
"type": 6
},
{
"id": "23:572745053585623035",
"name": "rotationZ",
"type": 6
},
{
"id": "24:174662964807733060",
"name": "accelerationX",
"type": 6
},
{
"id": "25:2607558191101966289",
"name": "accelerationY",
"type": 6
},
{
"id": "26:8078744267935466967",
"name": "accelerationZ",
"type": 6
},
{
"id": "27:2235290443011784871",
"name": "millisecondsSinceEpochReceived",
"type": 6
},
{
"id": "28:5600095333294587898",
"name": "millisecondsSinceEpochSend",
"type": 6
}
],
"relations": []
},
{
"id": "2:1642885530071590702",
"lastPropertyId": "4:5477306218303066701",
"name": "CaptureBox",
"properties": [
{
"id": "1:6772033063452157797",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:6118609604439630810",
"name": "type",
"type": 9
},
{
"id": "3:4134744801341910087",
"name": "uuid",
"type": 9
},
{
"id": "4:5477306218303066701",
"name": "startTime",
"type": 10
}
],
"relations": []
}
],
"lastEntityId": "2:1642885530071590702",
"lastIndexId": "0:0",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [
5928562934131159642,
5591187076567204697,
595530508372810754,
376663189826498742,
2249450669900993772,
3338488084769147616,
6118432389289072923,
652072338599943564,
4974899424217069697,
6174239378189558256,
890878501153569237,
1635504337236403097,
3739937748546759727,
1440167199258268261,
90858017595682556,
8864904344725714676,
6079624305294726236
],
"retiredRelationUids": [],
"version": 1
}

18
lib/objectbox.dart Normal file
View File

@@ -0,0 +1,18 @@
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'objectbox.g.dart';
class ObjectBox {
late final Store store;
/// Initialization
ObjectBox._create(this.store);
/// Create an instance of ObjectBox to use throughout the app.
static Future<ObjectBox> create() async {
final docsDir = await getApplicationDocumentsDirectory();
final store =
await openStore(directory: p.join(docsDir.path, 'obx-pet-tracker-2'));
return ObjectBox._create(store);
}
}

352
lib/objectbox.g.dart Normal file
View File

@@ -0,0 +1,352 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// This code was generated by ObjectBox. To update it run the generator again
// with `dart run build_runner build`.
// See also https://docs.objectbox.io/getting-started#generate-objectbox-code
// ignore_for_file: camel_case_types, depend_on_referenced_packages
// coverage:ignore-file
import 'dart:typed_data';
import 'package:flat_buffers/flat_buffers.dart' as fb;
import 'package:objectbox/internal.dart'
as obx_int; // generated code can access "internal" functionality
import 'package:objectbox/objectbox.dart' as obx;
import 'package:objectbox_flutter_libs/objectbox_flutter_libs.dart';
import 'xiao_connector/models/capture_box.dart';
import 'xiao_connector/models/capture_point.dart';
export 'package:objectbox/objectbox.dart'; // so that callers only have to import this file
final _entities = <obx_int.ModelEntity>[
obx_int.ModelEntity(
id: const obx_int.IdUid(1, 8883777039160106085),
name: 'CapturePoint',
lastPropertyId: const obx_int.IdUid(28, 5600095333294587898),
flags: 0,
properties: <obx_int.ModelProperty>[
obx_int.ModelProperty(
id: const obx_int.IdUid(1, 7031141970833098443),
name: 'id',
type: 6,
flags: 1),
obx_int.ModelProperty(
id: const obx_int.IdUid(2, 9089711773119462722),
name: 'type',
type: 9,
flags: 0),
obx_int.ModelProperty(
id: const obx_int.IdUid(12, 8018734828546727200),
name: 'uuid',
type: 9,
flags: 0),
obx_int.ModelProperty(
id: const obx_int.IdUid(21, 5571567596004635004),
name: 'rotationX',
type: 6,
flags: 0),
obx_int.ModelProperty(
id: const obx_int.IdUid(22, 128950109858162458),
name: 'rotationY',
type: 6,
flags: 0),
obx_int.ModelProperty(
id: const obx_int.IdUid(23, 572745053585623035),
name: 'rotationZ',
type: 6,
flags: 0),
obx_int.ModelProperty(
id: const obx_int.IdUid(24, 174662964807733060),
name: 'accelerationX',
type: 6,
flags: 0),
obx_int.ModelProperty(
id: const obx_int.IdUid(25, 2607558191101966289),
name: 'accelerationY',
type: 6,
flags: 0),
obx_int.ModelProperty(
id: const obx_int.IdUid(26, 8078744267935466967),
name: 'accelerationZ',
type: 6,
flags: 0),
obx_int.ModelProperty(
id: const obx_int.IdUid(27, 2235290443011784871),
name: 'millisecondsSinceEpochReceived',
type: 6,
flags: 0),
obx_int.ModelProperty(
id: const obx_int.IdUid(28, 5600095333294587898),
name: 'millisecondsSinceEpochSend',
type: 6,
flags: 0)
],
relations: <obx_int.ModelRelation>[],
backlinks: <obx_int.ModelBacklink>[]),
obx_int.ModelEntity(
id: const obx_int.IdUid(2, 1642885530071590702),
name: 'CaptureBox',
lastPropertyId: const obx_int.IdUid(4, 5477306218303066701),
flags: 0,
properties: <obx_int.ModelProperty>[
obx_int.ModelProperty(
id: const obx_int.IdUid(1, 6772033063452157797),
name: 'id',
type: 6,
flags: 1),
obx_int.ModelProperty(
id: const obx_int.IdUid(2, 6118609604439630810),
name: 'type',
type: 9,
flags: 0),
obx_int.ModelProperty(
id: const obx_int.IdUid(3, 4134744801341910087),
name: 'uuid',
type: 9,
flags: 0),
obx_int.ModelProperty(
id: const obx_int.IdUid(4, 5477306218303066701),
name: 'startTime',
type: 10,
flags: 0)
],
relations: <obx_int.ModelRelation>[],
backlinks: <obx_int.ModelBacklink>[])
];
/// Shortcut for [obx.Store.new] that passes [getObjectBoxModel] and for Flutter
/// apps by default a [directory] using `defaultStoreDirectory()` from the
/// ObjectBox Flutter library.
///
/// Note: for desktop apps it is recommended to specify a unique [directory].
///
/// See [obx.Store.new] for an explanation of all parameters.
///
/// For Flutter apps, also calls `loadObjectBoxLibraryAndroidCompat()` from
/// the ObjectBox Flutter library to fix loading the native ObjectBox library
/// on Android 6 and older.
Future<obx.Store> openStore(
{String? directory,
int? maxDBSizeInKB,
int? maxDataSizeInKB,
int? fileMode,
int? maxReaders,
bool queriesCaseSensitiveDefault = true,
String? macosApplicationGroup}) async {
await loadObjectBoxLibraryAndroidCompat();
return obx.Store(getObjectBoxModel(),
directory: directory ?? (await defaultStoreDirectory()).path,
maxDBSizeInKB: maxDBSizeInKB,
maxDataSizeInKB: maxDataSizeInKB,
fileMode: fileMode,
maxReaders: maxReaders,
queriesCaseSensitiveDefault: queriesCaseSensitiveDefault,
macosApplicationGroup: macosApplicationGroup);
}
/// Returns the ObjectBox model definition for this project for use with
/// [obx.Store.new].
obx_int.ModelDefinition getObjectBoxModel() {
final model = obx_int.ModelInfo(
entities: _entities,
lastEntityId: const obx_int.IdUid(2, 1642885530071590702),
lastIndexId: const obx_int.IdUid(0, 0),
lastRelationId: const obx_int.IdUid(0, 0),
lastSequenceId: const obx_int.IdUid(0, 0),
retiredEntityUids: const [],
retiredIndexUids: const [],
retiredPropertyUids: const [
5928562934131159642,
5591187076567204697,
595530508372810754,
376663189826498742,
2249450669900993772,
3338488084769147616,
6118432389289072923,
652072338599943564,
4974899424217069697,
6174239378189558256,
890878501153569237,
1635504337236403097,
3739937748546759727,
1440167199258268261,
90858017595682556,
8864904344725714676,
6079624305294726236
],
retiredRelationUids: const [],
modelVersion: 5,
modelVersionParserMinimum: 5,
version: 1);
final bindings = <Type, obx_int.EntityDefinition>{
CapturePoint: obx_int.EntityDefinition<CapturePoint>(
model: _entities[0],
toOneRelations: (CapturePoint object) => [],
toManyRelations: (CapturePoint object) => {},
getId: (CapturePoint object) => object.id,
setId: (CapturePoint object, int id) {
object.id = id;
},
objectToFB: (CapturePoint object, fb.Builder fbb) {
final typeOffset = fbb.writeString(object.type);
final uuidOffset = fbb.writeString(object.uuid);
fbb.startTable(29);
fbb.addInt64(0, object.id);
fbb.addOffset(1, typeOffset);
fbb.addOffset(11, uuidOffset);
fbb.addInt64(20, object.rotationX);
fbb.addInt64(21, object.rotationY);
fbb.addInt64(22, object.rotationZ);
fbb.addInt64(23, object.accelerationX);
fbb.addInt64(24, object.accelerationY);
fbb.addInt64(25, object.accelerationZ);
fbb.addInt64(26, object.millisecondsSinceEpochReceived);
fbb.addInt64(27, object.millisecondsSinceEpochSend);
fbb.finish(fbb.endTable());
return object.id;
},
objectFromFB: (obx.Store store, ByteData fbData) {
final buffer = fb.BufferContext(fbData);
final rootOffset = buffer.derefObject(0);
final typeParam = const fb.StringReader(asciiOptimization: true)
.vTableGet(buffer, rootOffset, 6, '');
final uuidParam = const fb.StringReader(asciiOptimization: true)
.vTableGet(buffer, rootOffset, 26, '');
final rotationXParam =
const fb.Int64Reader().vTableGet(buffer, rootOffset, 44, 0);
final rotationYParam =
const fb.Int64Reader().vTableGet(buffer, rootOffset, 46, 0);
final rotationZParam =
const fb.Int64Reader().vTableGet(buffer, rootOffset, 48, 0);
final accelerationXParam =
const fb.Int64Reader().vTableGet(buffer, rootOffset, 50, 0);
final accelerationYParam =
const fb.Int64Reader().vTableGet(buffer, rootOffset, 52, 0);
final accelerationZParam =
const fb.Int64Reader().vTableGet(buffer, rootOffset, 54, 0);
final millisecondsSinceEpochReceivedParam =
const fb.Int64Reader().vTableGet(buffer, rootOffset, 56, 0);
final millisecondsSinceEpochSendParam =
const fb.Int64Reader().vTableGet(buffer, rootOffset, 58, 0);
final object = CapturePoint(
type: typeParam,
uuid: uuidParam,
rotationX: rotationXParam,
rotationY: rotationYParam,
rotationZ: rotationZParam,
accelerationX: accelerationXParam,
accelerationY: accelerationYParam,
accelerationZ: accelerationZParam,
millisecondsSinceEpochReceived:
millisecondsSinceEpochReceivedParam,
millisecondsSinceEpochSend: millisecondsSinceEpochSendParam)
..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0);
return object;
}),
CaptureBox: obx_int.EntityDefinition<CaptureBox>(
model: _entities[1],
toOneRelations: (CaptureBox object) => [],
toManyRelations: (CaptureBox object) => {},
getId: (CaptureBox object) => object.id,
setId: (CaptureBox object, int id) {
object.id = id;
},
objectToFB: (CaptureBox object, fb.Builder fbb) {
final typeOffset = fbb.writeString(object.type);
final uuidOffset = fbb.writeString(object.uuid);
fbb.startTable(5);
fbb.addInt64(0, object.id);
fbb.addOffset(1, typeOffset);
fbb.addOffset(2, uuidOffset);
fbb.addInt64(3, object.startTime.millisecondsSinceEpoch);
fbb.finish(fbb.endTable());
return object.id;
},
objectFromFB: (obx.Store store, ByteData fbData) {
final buffer = fb.BufferContext(fbData);
final rootOffset = buffer.derefObject(0);
final typeParam = const fb.StringReader(asciiOptimization: true)
.vTableGet(buffer, rootOffset, 6, '');
final uuidParam = const fb.StringReader(asciiOptimization: true)
.vTableGet(buffer, rootOffset, 8, '');
final startTimeParam = DateTime.fromMillisecondsSinceEpoch(
const fb.Int64Reader().vTableGet(buffer, rootOffset, 10, 0));
final object = CaptureBox(
type: typeParam, uuid: uuidParam, startTime: startTimeParam)
..id = const fb.Int64Reader().vTableGet(buffer, rootOffset, 4, 0);
return object;
})
};
return obx_int.ModelDefinition(model, bindings);
}
/// [CapturePoint] entity fields to define ObjectBox queries.
class CapturePoint_ {
/// See [CapturePoint.id].
static final id =
obx.QueryIntegerProperty<CapturePoint>(_entities[0].properties[0]);
/// See [CapturePoint.type].
static final type =
obx.QueryStringProperty<CapturePoint>(_entities[0].properties[1]);
/// See [CapturePoint.uuid].
static final uuid =
obx.QueryStringProperty<CapturePoint>(_entities[0].properties[2]);
/// See [CapturePoint.rotationX].
static final rotationX =
obx.QueryIntegerProperty<CapturePoint>(_entities[0].properties[3]);
/// See [CapturePoint.rotationY].
static final rotationY =
obx.QueryIntegerProperty<CapturePoint>(_entities[0].properties[4]);
/// See [CapturePoint.rotationZ].
static final rotationZ =
obx.QueryIntegerProperty<CapturePoint>(_entities[0].properties[5]);
/// See [CapturePoint.accelerationX].
static final accelerationX =
obx.QueryIntegerProperty<CapturePoint>(_entities[0].properties[6]);
/// See [CapturePoint.accelerationY].
static final accelerationY =
obx.QueryIntegerProperty<CapturePoint>(_entities[0].properties[7]);
/// See [CapturePoint.accelerationZ].
static final accelerationZ =
obx.QueryIntegerProperty<CapturePoint>(_entities[0].properties[8]);
/// See [CapturePoint.millisecondsSinceEpochReceived].
static final millisecondsSinceEpochReceived =
obx.QueryIntegerProperty<CapturePoint>(_entities[0].properties[9]);
/// See [CapturePoint.millisecondsSinceEpochSend].
static final millisecondsSinceEpochSend =
obx.QueryIntegerProperty<CapturePoint>(_entities[0].properties[10]);
}
/// [CaptureBox] entity fields to define ObjectBox queries.
class CaptureBox_ {
/// See [CaptureBox.id].
static final id =
obx.QueryIntegerProperty<CaptureBox>(_entities[1].properties[0]);
/// See [CaptureBox.type].
static final type =
obx.QueryStringProperty<CaptureBox>(_entities[1].properties[1]);
/// See [CaptureBox.uuid].
static final uuid =
obx.QueryStringProperty<CaptureBox>(_entities[1].properties[2]);
/// See [CaptureBox.startTime].
static final startTime =
obx.QueryDateProperty<CaptureBox>(_entities[1].properties[3]);
}

View File

@@ -0,0 +1 @@
export 'view/view.dart';

View File

@@ -0,0 +1,228 @@
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:csv/csv.dart';
import 'package:ditredi/ditredi.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:vector_math/vector_math_64.dart';
import 'package:xiao_pet_tracker/xiao_connector/cubit/xiao_connector_cubit.dart';
import 'package:xiao_pet_tracker/xiao_connector/models/capture_point.dart';
@RoutePage()
class RecordingsDetailsPage extends StatefulWidget {
const RecordingsDetailsPage({
required this.uuid,
required this.type,
super.key,
});
final String uuid;
final String type;
@override
State<RecordingsDetailsPage> createState() => _RecordingsDetailsPageState();
}
class _RecordingsDetailsPageState extends State<RecordingsDetailsPage> {
final _controllerRotation = DiTreDiController();
final _controllerAcceleration = DiTreDiController();
final f = DateFormat('hh:mm:ss.SSS');
List<CapturePoint> _capturePoints = [];
bool isStoringCSV = false;
@override
void initState() {
_capturePoints =
context.read<XiaoConnectorCubit>().getCapturePointsOfUuid(widget.uuid);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Details: ${widget.type}'),
),
body: SingleChildScrollView(
child: Column(
children: [
const SizedBox(
height: 16,
),
Text(
'Start Time: ${f.format(DateTime.fromMillisecondsSinceEpoch(_capturePoints.first.millisecondsSinceEpochSend))}',
),
Text(
'End Time: ${f.format(DateTime.fromMillisecondsSinceEpoch(_capturePoints.last.millisecondsSinceEpochSend))}',
),
const SizedBox(
height: 8,
),
Text('Amount of captured points: ${_capturePoints.length}'),
const SizedBox(
height: 16,
),
if (isStoringCSV)
const CircularProgressIndicator()
else
ElevatedButton(
onPressed: () async {
setState(() {
isStoringCSV = true;
});
// await Permission.storage.request();
final downloadsDir = (Platform.isIOS)
? await getApplicationDocumentsDirectory()
: await getDownloadsDirectory();
final f = File(
'${downloadsDir!.path}/${widget.type}_${widget.uuid}.csv',
);
final rows = <List<dynamic>>[];
rows.add([
'sendTimeStamp',
'receivedTimeStamp',
'accelerationX',
'accelerationY',
'accelerationZ',
'rotationX',
'rotationY',
'rotationZ',
]);
for (var i = 0; i < _capturePoints.length; i++) {
rows.add([
_capturePoints[i].millisecondsSinceEpochSend,
_capturePoints[i].millisecondsSinceEpochReceived,
_capturePoints[i].accelerationX,
_capturePoints[i].accelerationY,
_capturePoints[i].accelerationZ,
_capturePoints[i].rotationX,
_capturePoints[i].rotationY,
_capturePoints[i].rotationZ,
]);
}
final csv = const ListToCsvConverter().convert(rows);
await f.writeAsString(csv);
setState(
() {
isStoringCSV = false;
},
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Succesfully exported the CSV!'),
),
);
}
},
child: const Text('Export to CSV'),
),
const SizedBox(
height: 16,
),
const Text('Raw Rotation Data'),
Padding(
padding: const EdgeInsets.all(8),
child: Container(
height: 400,
// width: 400,
// color: Color.fromARGB(255, 0, 0, 0),
decoration: BoxDecoration(
color: const Color.fromARGB(255, 0, 0, 0),
border:
Border.all(color: const Color.fromARGB(255, 0, 0, 0))),
child: DiTreDiDraggable(
controller: _controllerRotation,
child: DiTreDi(
figures: [
Cube3D(
0,
Vector3(0, 0, 0),
color: const Color.fromARGB(0, 128, 0, 0),
),
..._capturePoints.map(
(p) => Point3D(
Vector3(
p.rotationX.toDouble(),
p.rotationY.toDouble(),
p.rotationZ.toDouble(),
),
width: 2,
color: const Color.fromARGB(255, 255, 255, 255),
),
)
],
controller: _controllerRotation,
),
),
),
),
const Text('Raw Acceleration Data'),
Padding(
padding: const EdgeInsets.all(8),
child: Container(
height: 400,
// width: 400,
// color: Color.fromARGB(255, 0, 0, 0),
decoration: BoxDecoration(
color: const Color.fromARGB(255, 0, 0, 0),
border:
Border.all(color: const Color.fromARGB(255, 0, 0, 0))),
child: DiTreDiDraggable(
controller: _controllerAcceleration,
child: DiTreDi(
figures: [
Cube3D(
0,
Vector3(0, 0, 0),
color: const Color.fromARGB(0, 128, 0, 0),
),
..._capturePoints.map(
(p) => Point3D(
Vector3(
p.accelerationX.toDouble(),
p.accelerationY.toDouble(),
p.accelerationZ.toDouble(),
),
width: 2,
color: const Color.fromARGB(255, 255, 255, 255),
),
)
],
controller: _controllerAcceleration,
),
),
),
),
const SizedBox(
height: 100,
),
// ListView.builder(
// shrinkWrap: true,
// physics: const NeverScrollableScrollPhysics(),
// itemCount: _capturePoints.length,
// itemBuilder: (context, i) {
// return ListTile(
// title: Text('data'),
// );
// },
// ),
],
),
),
);
}
}

View File

@@ -0,0 +1,103 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:forui/forui.dart';
import 'package:intl/intl.dart';
import 'package:xiao_pet_tracker/app_router/app_router.gr.dart';
import 'package:xiao_pet_tracker/xiao_connector/cubit/xiao_connector_cubit.dart';
import 'package:xiao_pet_tracker/xiao_connector/models/capture_box.dart';
@RoutePage()
class RecordingsPage extends StatelessWidget {
const RecordingsPage({super.key});
@override
Widget build(BuildContext context) {
return const RecordingsView();
}
}
class RecordingsView extends StatefulWidget {
const RecordingsView({super.key});
@override
State<RecordingsView> createState() => _SettingsViewState();
}
class _SettingsViewState extends State<RecordingsView> {
List<CaptureBox> _captureBoxes = [];
final f = DateFormat('hh:mm - dd.MM.yyyy ');
@override
void initState() {
_captureBoxes = context.read<XiaoConnectorCubit>().getAllCaptureBoxes();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Recordings'),
),
body: RefreshIndicator(
onRefresh: _pullRefresh,
child: ListView.builder(
itemCount: _captureBoxes.length,
itemBuilder: (context, i) {
return ListTile(
leading: FIcon(FAssets.icons.pawPrint),
title: Text('Collection Type: ${_captureBoxes[i].type}'),
subtitle: Text(f.format(_captureBoxes[i].startTime)),
onTap: () {
AutoRouter.of(context).push(
RecordingsDetailsRoute(
type: _captureBoxes[i].type,
uuid: _captureBoxes[i].uuid,
),
);
},
onLongPress: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Delete this recording?'),
content: const Text(
"Are you sure you want to delete this recording with all of it's data? This cannot be undone.",
textAlign: TextAlign.center,
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
context
.read<XiaoConnectorCubit>()
.deleteRecording(_captureBoxes[i].uuid);
_pullRefresh();
Navigator.of(context).pop();
},
child: const Text('Delete'),
),
],
);
},
);
},
);
},
),
),
);
}
Future<void> _pullRefresh() async {
_captureBoxes = context.read<XiaoConnectorCubit>().getAllCaptureBoxes();
setState(() {});
}
}

View File

@@ -0,0 +1 @@
export 'recordings_page.dart';

View File

@@ -0,0 +1,2 @@
const uuidLSM6DS3TRService = '4c534d36-4453-3354-5253-657276696365';
const uuidAccelerationData = '61636365-6c65-7261-7469-6f6e44617461';

View File

@@ -0,0 +1,280 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
import 'package:uuid/uuid.dart';
import 'package:xiao_pet_tracker/bootstrap.dart';
import 'package:xiao_pet_tracker/objectbox.dart';
import 'package:xiao_pet_tracker/objectbox.g.dart';
import 'package:xiao_pet_tracker/xiao_connector/models/capture_box.dart';
import 'package:xiao_pet_tracker/xiao_connector/models/capture_point.dart';
import 'package:xiao_pet_tracker/xiao_connector/utils/utils.dart';
part 'xiao_connector_state.dart';
class XiaoConnectorCubit extends Cubit<XiaoConnectorState> {
XiaoConnectorCubit() : super(const XiaoConnectorState());
late BluetoothDevice device;
List<BluetoothService> _services = [];
late BluetoothCharacteristic _senseService;
late StreamSubscription<List<int>> dataCapturingSubscription;
final ObjectBox _objectBox = getIt<ObjectBox>();
late final Box<CapturePoint> _capturePointsBox;
late final Box<CaptureBox> _captureBoxes;
bool gotRotation = false;
bool gotAcceleration = false;
bool gotTimeStamp = false;
int _rotX = 0;
int _rotY = 0;
int _rotZ = 0;
int _accX = 0;
int _accY = 0;
int _accZ = 0;
int _millisecondsSinceEpochSend = 0;
String _captureType = '';
String _uuid = '';
bool isRecording = false;
Future<void> init() async {
_capturePointsBox = _objectBox.store.box<CapturePoint>();
_captureBoxes = _objectBox.store.box<CaptureBox>();
}
Future<void> connect() async {
emit(state.copyWith(status: XiaoConnectorStatus.loading));
try {
// scan for devices for 15 seconds
await FlutterBluePlus.startScan(timeout: const Duration(seconds: 15));
} catch (e) {
emit(state.copyWith(status: XiaoConnectorStatus.failure));
throw Exception('Start Scan Error: $e');
}
FlutterBluePlus.scanResults.listen(
(results) async {
for (final r in results) {
if (kDebugMode) {
print(r);
}
// set the xiao sense name
if (r.device.advName == 'Go Bluetooth 2') {
await FlutterBluePlus.stopScan();
device = r.device;
await device.connect();
try {
_services = await device.discoverServices();
} catch (e) {
emit(state.copyWith(status: XiaoConnectorStatus.failure));
throw Exception('Discover Services Error: $e');
}
device.connectionState
.listen((BluetoothConnectionState blueState) async {
if (blueState == BluetoothConnectionState.disconnected) {
debugPrint('DISCONNECTED!');
emit(state.copyWith(status: XiaoConnectorStatus.initial));
}
});
// device.cancelWhenDisconnected(deviceConnectionStream);
emit(state.copyWith(status: XiaoConnectorStatus.connected));
}
}
},
onDone: () {
debugPrint('DONE');
emit(state.copyWith(status: XiaoConnectorStatus.initial));
},
);
}
Future<void> setCapturingOn() async {
final senseService = _services
.where(
(s) => s.serviceUuid == Guid('4c534d36-4453-3354-5253-657276696365'),
)
.first
.characteristics
.where(
(c) =>
c.characteristicUuid ==
Guid('63617074-7572-696E-6753-657276696365'),
)
.first;
await senseService.write([1]);
}
Uint8List int64BigEndianBytes(int value) =>
Uint8List(8)..buffer.asByteData().setInt64(0, value);
Future<void> setTime(int millisSinceEpoch) async {
final senseService = _services
.where(
(s) => s.serviceUuid == Guid('4c534d36-4453-3354-5253-657276696365'),
)
.first
.characteristics
.where(
(c) =>
c.characteristicUuid ==
Guid('756E6978-5469-6D65-5374-616D70527374'),
)
.first;
final List<int> bytes = int64BigEndianBytes(millisSinceEpoch);
await senseService.write(bytes);
}
Future<void> startCapturing() async {
_senseService = _services
.where(
(s) => s.serviceUuid == Guid('4c534d36-4453-3354-5253-657276696365'),
)
.first
.characteristics
.where(
(c) =>
c.characteristicUuid ==
Guid('61636365-6c65-7261-7469-6f6e44617461'),
)
.first;
dataCapturingSubscription = _senseService.onValueReceived.listen((value) {
// rotation
if (value.last == 1) {
_rotX = fromBytesToInt32(value[0], value[1], value[2], value[3]);
_rotY = fromBytesToInt32(value[4], value[5], value[6], value[7]);
_rotZ = fromBytesToInt32(value[8], value[9], value[10], value[11]);
gotRotation = true;
}
// acceleration
if (value.last == 2) {
_accX = fromBytesToInt32(value[0], value[1], value[2], value[3]);
_accY = fromBytesToInt32(value[4], value[5], value[6], value[7]);
_accZ = fromBytesToInt32(value[8], value[9], value[10], value[11]);
gotAcceleration = true;
}
if (value.last == 3) {
final timeStamp = fromBytesToInt64(
value[0],
value[1],
value[2],
value[3],
value[4],
value[5],
value[6],
value[7],
);
_millisecondsSinceEpochSend = timeStamp;
gotTimeStamp = true;
}
if (gotAcceleration && gotRotation && gotTimeStamp) {
final capturePoint = CapturePoint(
type: _captureType,
uuid: _uuid,
rotationX: _rotX,
rotationY: _rotY,
rotationZ: _rotZ,
accelerationX: _accX,
accelerationY: _accY,
accelerationZ: _accZ,
millisecondsSinceEpochReceived:
DateTime.now().toUtc().millisecondsSinceEpoch,
millisecondsSinceEpochSend: _millisecondsSinceEpochSend,
);
emit(state.copyWith(lastCapturedPoint: capturePoint));
if (isRecording) {
_writeToObjectBox(capturePoint: capturePoint);
}
gotAcceleration = false;
gotRotation = false;
gotTimeStamp = false;
}
});
await setCapturingOn();
await setTime(DateTime.now().millisecondsSinceEpoch);
device.cancelWhenDisconnected(dataCapturingSubscription);
await _senseService.setNotifyValue(true);
emit(state.copyWith(status: XiaoConnectorStatus.capturing));
}
Future<void> stopCapturing() async {
await dataCapturingSubscription.cancel();
await _senseService.setNotifyValue(false);
emit(state.copyWith(status: XiaoConnectorStatus.connected));
}
Future<void> disconnectDevice() async {
await device.disconnect();
_services = [];
emit(state.copyWith(status: XiaoConnectorStatus.initial));
}
void toggleRecording({required String captureType}) {
isRecording = !isRecording;
if (isRecording) {
_captureType = captureType;
_uuid = const Uuid().v4();
_captureBoxes.put(
CaptureBox(
type: _captureType,
uuid: _uuid,
startTime: DateTime.now(),
),
);
}
}
void _writeToObjectBox({
required CapturePoint capturePoint,
}) {
_capturePointsBox.put(capturePoint);
}
List<CapturePoint> getCapturePointsOfUuid(String uuid) {
final query =
_capturePointsBox.query(CapturePoint_.uuid.equals(uuid)).build();
final points = query.find();
return points;
}
void deleteRecording(String uuid) {
_capturePointsBox.query(CapturePoint_.uuid.equals(uuid)).build().remove();
_captureBoxes.query(CaptureBox_.uuid.equals(uuid)).build().remove();
}
List<CaptureBox> getAllCaptureBoxes() {
final query = (_captureBoxes.query()
..order(CaptureBox_.startTime, flags: Order.descending))
.build();
final boxes = query.find();
return boxes;
}
List<CapturePoint> getAllCapturePoints() => _capturePointsBox.getAll();
}

View File

@@ -0,0 +1,40 @@
part of 'xiao_connector_cubit.dart';
enum XiaoConnectorStatus {
initial,
loading,
connected,
capturing,
failure,
}
extension XiaoConnectorStatusX on XiaoConnectorStatus {
bool get isInitial => this == XiaoConnectorStatus.initial;
bool get isLoading => this == XiaoConnectorStatus.loading;
bool get isConnected => this == XiaoConnectorStatus.connected;
bool get isCapturing => this == XiaoConnectorStatus.capturing;
bool get isFailure => this == XiaoConnectorStatus.failure;
}
final class XiaoConnectorState extends Equatable {
const XiaoConnectorState({
this.status = XiaoConnectorStatus.initial,
this.lastCapturedPoint,
});
final XiaoConnectorStatus status;
final CapturePoint? lastCapturedPoint;
XiaoConnectorState copyWith({
XiaoConnectorStatus? status,
CapturePoint? lastCapturedPoint,
}) {
return XiaoConnectorState(
status: status ?? this.status,
lastCapturedPoint: lastCapturedPoint ?? this.lastCapturedPoint,
);
}
@override
List<Object?> get props => [status, lastCapturedPoint];
}

View File

@@ -0,0 +1,18 @@
import 'package:objectbox/objectbox.dart';
@Entity()
class CaptureBox {
CaptureBox({
required this.type,
required this.uuid,
required this.startTime,
});
@Id()
int id = 0;
String type;
String uuid;
@Property(type: PropertyType.date)
DateTime startTime;
}

View File

@@ -0,0 +1,32 @@
import 'package:objectbox/objectbox.dart';
@Entity()
class CapturePoint {
CapturePoint({
required this.type,
required this.uuid,
required this.rotationX,
required this.rotationY,
required this.rotationZ,
required this.accelerationX,
required this.accelerationY,
required this.accelerationZ,
required this.millisecondsSinceEpochReceived,
required this.millisecondsSinceEpochSend,
});
@Id()
int id = 0;
String type;
String uuid;
int rotationX;
int rotationY;
int rotationZ;
int accelerationX;
int accelerationY;
int accelerationZ;
int millisecondsSinceEpochReceived;
int millisecondsSinceEpochSend;
}

View File

@@ -0,0 +1,32 @@
import 'dart:typed_data';
int fromBytesToInt32(int b3, int b2, int b1, int b0) {
final int8List = Int8List(4)
..[3] = b3
..[2] = b2
..[1] = b1
..[0] = b0;
return int8List.buffer.asByteData().getInt32(0);
}
int fromBytesToInt64(
int b7,
int b6,
int b5,
int b4,
int b3,
int b2,
int b1,
int b0,
) {
final int8List = Int8List(8)
..[7] = b7
..[6] = b6
..[5] = b5
..[4] = b4
..[3] = b3
..[2] = b2
..[1] = b1
..[0] = b0;
return int8List.buffer.asByteData().getInt64(0);
}

View File

@@ -0,0 +1,193 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:xiao_pet_tracker/xiao_connector/cubit/xiao_connector_cubit.dart';
class CaptureView extends StatefulWidget {
const CaptureView({super.key});
@override
State<CaptureView> createState() => _CaptureViewState();
}
class _CaptureViewState extends State<CaptureView> {
late TextEditingController _controller;
bool _isTextFieldFocused = false;
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// final count = context.select((XiaoConnectorCubit cubit) => cubit.state);
final lastCapturePoint = context
.select((XiaoConnectorCubit cubit) => cubit.state.lastCapturedPoint);
return Scaffold(
appBar: AppBar(
title: const Text('Live Feed'),
),
body: SingleChildScrollView(
child: Center(
child: Column(
children: [
const SizedBox(
height: 8,
),
ElevatedButton(
onPressed: () {
context.read<XiaoConnectorCubit>().stopCapturing();
},
child: const Text('Close Live Feed'),
),
const SizedBox(
height: 12,
),
const Text(
'Last Captured Point',
style: TextStyle(fontWeight: FontWeight.bold),
),
const Divider(
indent: 80,
endIndent: 80,
),
const Text(
'Received Time',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(
'${DateTime.fromMillisecondsSinceEpoch(
lastCapturePoint?.millisecondsSinceEpochReceived ?? 0,
)}',
),
const Text(
'Send Time',
style: TextStyle(fontWeight: FontWeight.bold),
),
const Text('(time the controller send the data)'),
Text(
'${DateTime.fromMillisecondsSinceEpoch(
lastCapturePoint?.millisecondsSinceEpochSend ?? 0,
)}',
),
const SizedBox(
height: 12,
),
ElevatedButton.icon(
onPressed: () {
// context.read<XiaoConnectorCubit>().startCapturing(
// captureType: 'test',
// );
final timeToSend = DateTime.now().millisecondsSinceEpoch;
context.read<XiaoConnectorCubit>().setTime(timeToSend);
},
icon: const Icon(Icons.watch_later_outlined),
label: const Text('Sync Clocks'),
),
const SizedBox(
height: 12,
),
const Divider(
indent: 80,
endIndent: 80,
),
Text('Acceleration X: ${lastCapturePoint?.accelerationX}'),
Text('Acceleration Y: ${lastCapturePoint?.accelerationY}'),
Text('Acceleration Z: ${lastCapturePoint?.accelerationZ}'),
const Divider(
indent: 80,
endIndent: 80,
),
Text('Rotation X: ${lastCapturePoint?.rotationX}'),
Text('Rotation Y: ${lastCapturePoint?.rotationY}'),
Text('Rotation Z: ${lastCapturePoint?.rotationZ}'),
const SizedBox(
height: 16,
),
Padding(
padding: const EdgeInsets.only(left: 32, right: 32, top: 16),
child: FocusScope(
child: Focus(
onFocusChange: (focus) {
_isTextFieldFocused = focus;
setState(() {});
},
child: TextField(
controller: _controller,
decoration: const InputDecoration(
label: Text('Capture Name'),
),
),
),
),
),
const SizedBox(
height: 16,
)
],
),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
floatingActionButton: _isTextFieldFocused
? null
: FloatingActionButton.large(
child: context.read<XiaoConnectorCubit>().isRecording
? const Stack(
children: [
Align(
child: Icon(Icons.pause),
),
Align(
child: SizedBox.expand(
child: Padding(
padding: EdgeInsets.all(12),
child: CircularProgressIndicator(),
),
),
),
],
)
: const Icon(Icons.play_arrow),
onPressed: () {
if (_controller.value.text == '') {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text(
'Please enter Capture Name',
),
content: const Text(
'You need to enter a capture name before you can start capturing data.',
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text(
'Ok',
),
),
],
);
},
);
} else {
context.read<XiaoConnectorCubit>().toggleRecording(
captureType: _controller.value.text,
);
}
},
),
);
}
}

View File

@@ -0,0 +1,51 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:xiao_pet_tracker/xiao_connector/cubit/xiao_connector_cubit.dart';
class ConnectedView extends StatelessWidget {
const ConnectedView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Xiao Connector'),
),
body: Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'''
You are connected to the Xiao Sense.
Now you can open the live feed.''',
textAlign: TextAlign.center,
),
const SizedBox(
height: 48,
),
ElevatedButton(
onPressed: () {
context.read<XiaoConnectorCubit>().startCapturing();
},
child: const Text('Open Live Feed'),
),
const SizedBox(
height: 48,
),
ElevatedButton(
onPressed: () {
context.read<XiaoConnectorCubit>().disconnectDevice();
},
child: const Text('Disconnect'),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class FailureView extends StatelessWidget {
const FailureView({super.key});
@override
Widget build(BuildContext context) {
return const Text('Oops. Something bad happened! :(');
}
}

View File

@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:xiao_pet_tracker/xiao_connector/cubit/xiao_connector_cubit.dart';
class InitialView extends StatelessWidget {
const InitialView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Xiao Connector'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'''
You are currently not connected to the Xiao Sense.
Click on `Connect` to try to connect to the device.''',
textAlign: TextAlign.center,
),
const SizedBox(
height: 8,
),
ElevatedButton(
onPressed: () {
context.read<XiaoConnectorCubit>().connect();
},
child: const Text('Connect'),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
class LoadingView extends StatelessWidget {
const LoadingView({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
}
}

View File

@@ -0,0 +1 @@
export 'xiao_connector_page.dart';

View File

@@ -0,0 +1,40 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:xiao_pet_tracker/xiao_connector/cubit/xiao_connector_cubit.dart';
import 'package:xiao_pet_tracker/xiao_connector/view/capture_view.dart';
import 'package:xiao_pet_tracker/xiao_connector/view/connected_view.dart';
import 'package:xiao_pet_tracker/xiao_connector/view/failure_view.dart';
import 'package:xiao_pet_tracker/xiao_connector/view/initial_view.dart';
import 'package:xiao_pet_tracker/xiao_connector/view/loading_view.dart';
@RoutePage()
class XiaoConnectorPage extends StatelessWidget {
const XiaoConnectorPage({super.key});
@override
Widget build(BuildContext context) {
return const XiaoConnectorView();
}
}
class XiaoConnectorView extends StatelessWidget {
const XiaoConnectorView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocBuilder<XiaoConnectorCubit, XiaoConnectorState>(
builder: (context, state) {
return switch (state.status) {
XiaoConnectorStatus.initial => const InitialView(),
XiaoConnectorStatus.loading => const LoadingView(),
XiaoConnectorStatus.connected => const ConnectedView(),
XiaoConnectorStatus.capturing => const CaptureView(),
XiaoConnectorStatus.failure => const FailureView(),
};
},
),
);
}
}

View File

@@ -0,0 +1,22 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@RoutePage()
class XiaoDataPage extends StatelessWidget {
const XiaoDataPage({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
class XiaoDataView extends StatelessWidget {
const XiaoDataView({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View File

@@ -0,0 +1 @@
export 'view/view.dart';