diff options
Diffstat (limited to 'lib/widgets')
-rw-r--r-- | lib/widgets/arrow.dart | 18 | ||||
-rw-r--r-- | lib/widgets/arrows.dart | 23 | ||||
-rw-r--r-- | lib/widgets/connection_status_button.dart | 40 | ||||
-rw-r--r-- | lib/widgets/esense_connect_dialog.dart | 64 | ||||
-rw-r--r-- | lib/widgets/esense_not_connected_dialog.dart | 34 | ||||
-rw-r--r-- | lib/widgets/level_info_chip.dart | 37 | ||||
-rw-r--r-- | lib/widgets/level_list_entry.dart | 95 |
7 files changed, 311 insertions, 0 deletions
diff --git a/lib/widgets/arrow.dart b/lib/widgets/arrow.dart new file mode 100644 index 0000000..1ad0ec4 --- /dev/null +++ b/lib/widgets/arrow.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:sense_the_rhythm/models/arrow_direction.dart'; + +class Arrow extends StatelessWidget { + final double position; + final ArrowDirection direction; + + const Arrow({super.key, required this.position, required this.direction}); + + @override + Widget build(BuildContext context) { + return Positioned( + left: MediaQuery.of(context).size.width / 2 - 50, // Center the arrow + bottom: position + 50, + child: Icon(size: 100, color: Colors.redAccent.shade400, direction.icon), + ); + } +} diff --git a/lib/widgets/arrows.dart b/lib/widgets/arrows.dart new file mode 100644 index 0000000..162f0f3 --- /dev/null +++ b/lib/widgets/arrows.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:sense_the_rhythm/models/note.dart'; +import 'package:sense_the_rhythm/widgets/arrow.dart'; + +class Arrows extends StatelessWidget { + final List<Note> notes; + + const Arrows({super.key, required this.notes}); + + @override + Widget build(BuildContext context) { + return Stack( + children: notes.map((note) { + double position = + note.position * 10000; // * 20 * MediaQuery.of(context).size.height; + + return Arrow( + position: position, + direction: note.direction, + ); + }).toList()); + } +} diff --git a/lib/widgets/connection_status_button.dart b/lib/widgets/connection_status_button.dart new file mode 100644 index 0000000..e0c4cb4 --- /dev/null +++ b/lib/widgets/connection_status_button.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:sense_the_rhythm/utils/esense_input.dart'; +import 'package:sense_the_rhythm/widgets/esense_connect_dialog.dart'; + +class ConnectionStatusButton extends StatelessWidget { + final String deviceStatus; + const ConnectionStatusButton( + this.deviceStatus, { + super.key, + }); + + @override + Widget build(BuildContext context) { + return FilledButton.icon( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + ESenseInput.instance.connected ? Colors.green : Colors.grey), + padding: + WidgetStateProperty.all(EdgeInsets.symmetric(horizontal: 8.0))), + onPressed: () => showDialog( + context: context, + builder: (BuildContext context) { + return ESenseConnectDialog( + deviceStatus: ESenseInput.instance.deviceStatus, + connect: (String name) { + ESenseInput.instance.connectToESense(name); + }, + disconnect: () { + ESenseInput.instance.eSenseManager.disconnect(); + }, + ); + }, + ), + label: Text(deviceStatus), + iconAlignment: IconAlignment.end, + icon: Icon(ESenseInput.instance.connected + ? Icons.bluetooth_connected + : Icons.bluetooth)); + } +} diff --git a/lib/widgets/esense_connect_dialog.dart b/lib/widgets/esense_connect_dialog.dart new file mode 100644 index 0000000..2853ffe --- /dev/null +++ b/lib/widgets/esense_connect_dialog.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:sense_the_rhythm/utils/esense_input.dart'; + +class ESenseConnectDialog extends StatefulWidget { + final void Function(String) connect; + final VoidCallback disconnect; + final ValueNotifier<String> deviceStatus; + + const ESenseConnectDialog( + {super.key, + required this.deviceStatus, + required this.connect, + required this.disconnect}); + + @override + State<ESenseConnectDialog> createState() => _ESenseConnectDialogState(); +} + +class _ESenseConnectDialogState extends State<ESenseConnectDialog> { + String _eSenseDeviceName = ''; + + @override + Widget build(BuildContext context) { + // rerender whenever the deviceStatus changes + return ValueListenableBuilder( + valueListenable: widget.deviceStatus, + builder: (BuildContext context, String deviceStatus, Widget? child) { + return AlertDialog( + title: const Text('Connect to ESense'), + content: Column(mainAxisSize: MainAxisSize.min, children: [ + TextField( + onChanged: (input) { + setState(() { + _eSenseDeviceName = input; + }); + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + hintText: 'eSense-xxxx', + labelText: 'Device name', + ), + ), + // Text(eSenseDeviceName), + Text(deviceStatus) + ]), + actions: <Widget>[ + TextButton( + onPressed: () => Navigator.pop(context, 'Cancel'), + child: const Text('Close'), + ), + ESenseInput.instance.connected + ? TextButton( + onPressed: () => widget.disconnect(), + child: const Text('Disconnect'), + ) + : TextButton( + onPressed: () => widget.connect(_eSenseDeviceName), + child: const Text('Connect'), + ), + ], + ); + }); + } +} diff --git a/lib/widgets/esense_not_connected_dialog.dart b/lib/widgets/esense_not_connected_dialog.dart new file mode 100644 index 0000000..32d1d6b --- /dev/null +++ b/lib/widgets/esense_not_connected_dialog.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +class ESenseNotConnectedDialog extends StatelessWidget { + const ESenseNotConnectedDialog( + {super.key, required this.onCancel, required this.onContinue}); + + final VoidCallback onCancel; + final VoidCallback onContinue; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text("ESense not connected"), + content: const Text( + "You will only be able to play with the arrow keys of an external keyboard. "), + actions: <Widget>[ + TextButton( + onPressed: () { + Navigator.pop(context, 'Cancel'); + onCancel(); + }, + child: const Text('Connect to ESense'), + ), + TextButton( + onPressed: () { + Navigator.pop(context, 'Cancel'); + onContinue(); + }, + child: const Text('Continue anyway'), + ), + ], + ); + } +} diff --git a/lib/widgets/level_info_chip.dart b/lib/widgets/level_info_chip.dart new file mode 100644 index 0000000..8e4146c --- /dev/null +++ b/lib/widgets/level_info_chip.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +class LevelInfoChip extends StatelessWidget { + final String label; + final IconData icon; + + const LevelInfoChip({super.key, required this.label, required this.icon}); + + @override + Widget build(BuildContext context) { + return OutlinedButton( + style: ButtonStyle( + shape: WidgetStateProperty.all(RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)))), + minimumSize: WidgetStateProperty.all(Size(10, 10)), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + padding: WidgetStateProperty.all( + EdgeInsets.symmetric(vertical: 4.0, horizontal: 5.0)) + ), + onPressed: () {}, + child: Row(children: [ + Icon( + icon, + size: 16, + ), + SizedBox(width: 4), + Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: + FontWeight.w200), // Adjust font size for smaller appearance + ), + ]), + ); + } +} diff --git a/lib/widgets/level_list_entry.dart b/lib/widgets/level_list_entry.dart new file mode 100644 index 0000000..ad7766d --- /dev/null +++ b/lib/widgets/level_list_entry.dart @@ -0,0 +1,95 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:sense_the_rhythm/utils/esense_input.dart'; +import 'package:sense_the_rhythm/utils/simfile.dart'; +import 'package:sense_the_rhythm/screens/level.dart'; +import 'package:sense_the_rhythm/widgets/esense_connect_dialog.dart'; +import 'package:sense_the_rhythm/widgets/esense_not_connected_dialog.dart'; +import 'package:sense_the_rhythm/widgets/level_info_chip.dart'; + +class LevelListEntry extends StatelessWidget { + const LevelListEntry({ + super.key, + required this.simfile, + }); + + final Simfile simfile; + + /// navigates to level screen + void _navigateToLevel(BuildContext context) { + Navigator.push(context, + MaterialPageRoute(builder: (BuildContext context) => Level(simfile))); + } + + /// opens ESenseConnectDialog + void _openESenseConnectDialog(context) { + showDialog( + context: context, + builder: (BuildContext context) { + return ESenseConnectDialog( + deviceStatus: ESenseInput.instance.deviceStatus, + connect: (String name) { + ESenseInput.instance.connectToESense(name); + }, + disconnect: () { + ESenseInput.instance.eSenseManager.disconnect(); + }, + ); + }, + ); + } + + /// when clocked on the level, warn if not connected to ESense + void _tapHandler(BuildContext context) { + if (ESenseInput.instance.connected) { + _navigateToLevel(context); + } else { + showDialog( + context: context, + builder: (BuildContext context) { + return ESenseNotConnectedDialog( + onCancel: () { + _openESenseConnectDialog(context); + }, + onContinue: () { + _navigateToLevel(context); + }, + ); + }, + ); + } + } + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Image.file(File(simfile.bannerPath!)), + trailing: Icon(Icons.play_arrow), + title: Text( + simfile.tags["TITLE"]!, + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Row( + spacing: 2, + children: [ + LevelInfoChip( + label: + '${simfile.duration!.inMinutes}:${simfile.duration!.inSeconds.remainder(60).toString().padLeft(2, "0")}', + icon: Icons.timer_outlined, + ), + LevelInfoChip( + label: '${simfile.bpms.entries.first.value.toInt()} BPM', + icon: Icons.graphic_eq, + ), + ], + ), + ), + onTap: () { + _tapHandler(context); + }, + ); + } +} |