import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:audioplayers/audioplayers.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(); } enum ArrowDirection { left(Icons.arrow_back), down(Icons.arrow_downward), up(Icons.arrow_upward), right(Icons.arrow_forward); const ArrowDirection(this.icon); final IconData icon; } class Note { final double time; final ArrowDirection direction; const Note({required this.time, required this.direction}); } class _LevelState extends State { final player = AudioPlayer(); Simfile? simfile; bool _isPlaying = true; Duration? _duration; Duration? _position; StreamSubscription? _durationSubscription; StreamSubscription? _positionSubscription; 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); }); 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(); double bpm = simfile!.bpms.entries.first.value; for (final (measureIndex, measure) in simfile!.chartSimplest!.measures!.indexed) { for (final (noteIndex, noteData) in measure.indexed) { int arrowIndex = noteData.indexOf('1'); if (arrowIndex < 0 || arrowIndex > 3) { continue; } notes.add(Note( time: (measureIndex * 4.0 + (noteIndex.toDouble() / measure.length) * 4.0) * 1.0 / bpm + simfile!.offset / 60.0, direction: ArrowDirection.values[arrowIndex])); } } print(audioPath); player.play(DeviceFileSource(audioPath)); } @override Widget build(BuildContext context) { return 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: [ ...notes.map((note) { return Arrow( position: _position != null ? (note.time - _position!.inMilliseconds / 60000.0) * 20 * MediaQuery.of(context).size.height : 0.0, direction: note.direction, ); }), 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, // color: Colors.blue, border: Border.all(color: Colors.black, width: 10)), ), ), ])); } @override void dispose() { _durationSubscription?.cancel(); _positionSubscription?.cancel(); player.dispose(); super.dispose(); } } 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), ); } }