feat: show duration in level selection + more robust simfile loader

This commit is contained in:
Orangerot 2025-01-14 16:26:57 +01:00
parent 94463a490c
commit 28d0fe7d8c
6 changed files with 114 additions and 43 deletions

View file

@ -244,11 +244,7 @@ class _LevelState extends State<Level> with SingleTickerProviderStateMixin {
)), )),
), ),
body: Stack(children: [ body: Stack(children: [
Arrows( Arrows(notes: notes),
notes: notes,
position: _position != null
? _position!.inMilliseconds.toDouble()
: 0.0),
Positioned( Positioned(
top: 50, top: 50,
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,

View file

@ -62,16 +62,24 @@ class _LevelSelectionState extends State<LevelSelection> {
try { try {
// List all files and folders in the directory // List all files and folders in the directory
List<Simfile> simfiles = directory List<Simfile> simfiles = directory
.listSync() .listSync(recursive: true)
.where((entity) => FileSystemEntity.isDirectorySync(entity.path)) .where((entity) => entity.path.endsWith('.sm'))
.map((entity) { .map((entity) => Simfile(entity.path))
Simfile simfile = Simfile(entity.path); .toList();
simfile.load();
return simfile;
}).toList();
simfiles.sort((a, b) => a.tags['TITLE']!.compareTo(b.tags['TITLE']!));
return simfiles; List<bool> successfullLoads =
await Future.wait(simfiles.map((simfile) => simfile.load()));
List<Simfile> simfilesFiltered = [];
for (int i = 0; i < simfiles.length; i++) {
if (successfullLoads[i]) {
simfilesFiltered.add(simfiles[i]);
}
}
simfilesFiltered
.sort((a, b) => a.tags['TITLE']!.compareTo(b.tags['TITLE']!));
return simfilesFiltered;
} catch (e) { } catch (e) {
print("Error reading directory: $e"); print("Error reading directory: $e");
return []; return [];

View file

@ -1,5 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:audioplayers/audioplayers.dart';
enum Difficulty { Beginner, Easy, Medium, Hard, Challenge, Edit } enum Difficulty { Beginner, Easy, Medium, Hard, Challenge, Edit }
// These are the standard note values: // These are the standard note values:
@ -36,12 +38,13 @@ class Chart {
} }
class Simfile { class Simfile {
String directoryPath; String? directoryPath;
String? simfilePath; String simfilePath;
String? audioPath; String? audioPath;
String? bannerPath; String? bannerPath;
String? lines; String? lines;
Duration? duration;
// tags of simfile // tags of simfile
Map<String, String> tags = {}; Map<String, String> tags = {};
@ -50,7 +53,7 @@ class Simfile {
Map<double, double> bpms = {}; Map<double, double> bpms = {};
double offset = 0; double offset = 0;
Simfile(this.directoryPath); Simfile(this.simfilePath);
void _parseChart({required List<String> keys, required String value}) { void _parseChart({required List<String> keys, required String value}) {
Chart chart = Chart(); Chart chart = Chart();
@ -119,33 +122,41 @@ class Simfile {
_parseChart(keys: keys, value: value); _parseChart(keys: keys, value: value);
} }
void load() { Future<bool> load() async {
simfilePath = Directory(directoryPath) directoryPath = File(simfilePath).parent.path;
.listSync() lines = File(simfilePath).readAsStringSync();
.firstWhere((entity) => entity.path.endsWith('.sm'),
orElse: () => File(''))
.path;
audioPath = Directory(directoryPath)
.listSync()
.firstWhere((entity) => entity.path.endsWith('.ogg'),
orElse: () => File(''))
.path;
bannerPath = Directory(directoryPath)
.listSync()
.firstWhere((file) => file.path.toLowerCase().endsWith('banner.png'),
orElse: () => File(''))
.path;
lines = File(simfilePath!).readAsStringSync();
RegExp commentsRegExp = RegExp(r'//.*$'); RegExp commentsRegExp = RegExp(r'//.*$');
lines = lines?.replaceAll(commentsRegExp, ''); lines = lines?.replaceAll(commentsRegExp, '');
RegExp fieldDataRegExp = RegExp(r'#([^;]+):([^;]*);'); RegExp fieldDataRegExp = RegExp(r'#([^;]+):([^;]*);');
for (final fieldData in fieldDataRegExp.allMatches(lines!)) { for (final fieldData in fieldDataRegExp.allMatches(lines!)) {
_parseTag(fieldData); try {
_parseTag(fieldData);
} catch (err) {
return false;
}
} }
String? musicFileName = tags["MUSIC"];
if (musicFileName == null) return false;
String? bannerFileName = tags["BANNER"];
if (bannerFileName == null) return false;
for (FileSystemEntity entity in Directory(directoryPath!).listSync()) {
if (entity.path.endsWith('.ogg')) {
audioPath = entity.path;
}
if (entity.path.endsWith('anner.png')) {
bannerPath = entity.path;
}
}
AudioPlayer audioplayer = AudioPlayer();
await audioplayer.setSource(DeviceFileSource(audioPath!));
duration = await audioplayer.getDuration();
audioplayer.dispose();
return true;
} }
} }

View file

@ -4,9 +4,8 @@ import 'package:sense_the_rhythm/widgets/arrow.dart';
class Arrows extends StatelessWidget { class Arrows extends StatelessWidget {
final List<Note> notes; final List<Note> notes;
final double position;
const Arrows({super.key, required this.notes, required this.position}); const Arrows({super.key, required this.notes});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View file

@ -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
),
]),
);
}
}

View file

@ -6,6 +6,7 @@ import 'package:sense_the_rhythm/utils/simfile.dart';
import 'package:sense_the_rhythm/screens/level.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_connect_dialog.dart';
import 'package:sense_the_rhythm/widgets/esense_not_connected_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 { class LevelListEntry extends StatelessWidget {
const LevelListEntry({ const LevelListEntry({
@ -62,8 +63,27 @@ class LevelListEntry extends StatelessWidget {
return ListTile( return ListTile(
leading: Image.file(File(simfile.bannerPath!)), leading: Image.file(File(simfile.bannerPath!)),
trailing: Icon(Icons.play_arrow), trailing: Icon(Icons.play_arrow),
title: Text(simfile.tags["TITLE"]!), title: Text(
subtitle: Text('3:45'), 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: () { onTap: () {
tapHandler(context); tapHandler(context);
}, },