import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/services.dart'; import 'package:sense_the_rhythm/arrows.dart'; import 'package:sense_the_rhythm/simfile.dart'; class Level extends StatefulWidget { const Level({super.key, required this.stepmaniaFolderPath}); final String stepmaniaFolderPath; @override State createState() => _LevelState(); } class InputDirection { bool up = false; bool down = false; bool left = false; bool right = false; } class _LevelState extends State { final player = AudioPlayer(); Simfile? simfile; bool _isPlaying = true; Duration? _duration; Duration? _position; StreamSubscription? _durationSubscription; StreamSubscription? _positionSubscription; final FocusNode _focusNode = FocusNode(); InputDirection inputDirection = InputDirection(); List notes = []; @override void setState(VoidCallback fn) { // Subscriptions only can be closed asynchronously, // therefore events can occur after widget has been disposed. if (mounted) { super.setState(fn); } } @override void initState() { super.initState(); // Use initial values from player player.getDuration().then( (value) => setState(() { _duration = value; }), ); player.getCurrentPosition().then( (value) => setState(() { _position = value; }), ); _durationSubscription = player.onDurationChanged.listen((duration) { setState(() => _duration = duration); }); _positionSubscription = player.onPositionChanged.listen( (p) => setState(() => _position = p), ); player.onDurationChanged.listen((Duration d) { // print('Max duration: $d'); setState(() => _duration = d); }); player.onPositionChanged.listen((Duration p) { // print('Current position: $p'); setState(() => _position = p); for (final note in notes) { note.position = note.time - p.inMilliseconds / 60000.0; if (note.wasHit != null) { continue; } if (note.position.abs() < 0.5 * 1.0 / 60.0) { bool keypressCorrect = false; switch (note.direction) { case ArrowDirection.up: keypressCorrect = inputDirection.up; break; case ArrowDirection.down: keypressCorrect = inputDirection.down; break; case ArrowDirection.right: keypressCorrect = inputDirection.right; break; case ArrowDirection.left: keypressCorrect = inputDirection.left; break; } if (keypressCorrect) { print("you hit!"); note.wasHit = true; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Great!'), duration: Duration(milliseconds: 500), ), ); } } else if (note.position < -0.5 * 1.0 / 60.0) { print("Missed"); note.wasHit = false; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Missed!'), duration: Duration(milliseconds: 500), ), ); } } }); String simfilePath = Directory(widget.stepmaniaFolderPath) .listSync() .firstWhere((entity) => entity.path.endsWith('.sm'), orElse: () => File('')) .path; String audioPath = Directory(widget.stepmaniaFolderPath) .listSync() .firstWhere((entity) => entity.path.endsWith('.ogg'), orElse: () => File('')) .path; simfile = Simfile(simfilePath); simfile!.load(); simfile!.chartSimplest!.beats.forEach((time, noteData) { int arrowIndex = noteData.indexOf('1'); if (arrowIndex < 0 || arrowIndex > 3) { return; } notes.add(Note(time: time, direction: ArrowDirection.values[arrowIndex])); }); print(audioPath); player.play(DeviceFileSource(audioPath)); } @override Widget build(BuildContext context) { return KeyboardListener( focusNode: _focusNode, autofocus: true, onKeyEvent: (event) { bool isDown = false; if (event is KeyDownEvent) { isDown = true; } else if (event is KeyUpEvent) { isDown = false; } else { return; } switch (event.logicalKey) { case LogicalKeyboardKey.arrowUp: inputDirection.up = isDown; break; case LogicalKeyboardKey.arrowDown: inputDirection.down = isDown; break; case LogicalKeyboardKey.arrowLeft: inputDirection.left = isDown; break; case LogicalKeyboardKey.arrowRight: inputDirection.right = isDown; break; } }, child: Scaffold( appBar: AppBar( leading: IconButton( icon: Icon(_isPlaying ? Icons.pause : Icons.play_arrow), onPressed: () { if (_isPlaying) { player.pause(); setState(() { _isPlaying = false; }); } else { player.resume(); setState(() { _isPlaying = true; }); } }, ), title: Text(widget.stepmaniaFolderPath.split('/').last), actions: [ IconButton( icon: Icon(Icons.close), onPressed: () => Navigator.pop(context)) ], bottom: PreferredSize( preferredSize: Size(double.infinity, 1.0), child: LinearProgressIndicator( value: (_duration != null && _position != null && _position!.inMilliseconds > 0 && _position!.inMilliseconds < _duration!.inMilliseconds) ? _position!.inMilliseconds / _duration!.inMilliseconds : 0.0, )), ), body: Stack(children: [ Arrows( notes: notes, position: _position != null ? _position!.inMilliseconds.toDouble() : 0.0), Positioned( top: 50, width: MediaQuery.of(context).size.width, left: 0, child: Text( "Great!", textScaler: TextScaler.linear(4), textAlign: TextAlign.center, ), ), Positioned( left: MediaQuery.of(context).size.width / 2 - 50, bottom: 50, child: Container( width: 100, height: 100, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: Colors.black, width: 10)), ), ), ])), ); } @override void dispose() { _durationSubscription?.cancel(); _positionSubscription?.cancel(); player.dispose(); super.dispose(); } }