Sense_the_Rhythm/lib/level.dart

223 lines
6.1 KiB
Dart

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<Level> 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<Level> {
final player = AudioPlayer();
Simfile? simfile;
bool _isPlaying = true;
Duration? _duration;
Duration? _position;
StreamSubscription? _durationSubscription;
StreamSubscription? _positionSubscription;
List<Note> 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),
);
}
}