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: [
Arrows(
notes: notes,
position: _position != null
? _position!.inMilliseconds.toDouble()
: 0.0),
Arrows(notes: notes),
Positioned(
top: 50,
width: MediaQuery.of(context).size.width,

View file

@ -62,16 +62,24 @@ class _LevelSelectionState extends State<LevelSelection> {
try {
// List all files and folders in the directory
List<Simfile> simfiles = directory
.listSync()
.where((entity) => FileSystemEntity.isDirectorySync(entity.path))
.map((entity) {
Simfile simfile = Simfile(entity.path);
simfile.load();
return simfile;
}).toList();
simfiles.sort((a, b) => a.tags['TITLE']!.compareTo(b.tags['TITLE']!));
.listSync(recursive: true)
.where((entity) => entity.path.endsWith('.sm'))
.map((entity) => Simfile(entity.path))
.toList();
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) {
print("Error reading directory: $e");
return [];

View file

@ -1,5 +1,7 @@
import 'dart:io';
import 'package:audioplayers/audioplayers.dart';
enum Difficulty { Beginner, Easy, Medium, Hard, Challenge, Edit }
// These are the standard note values:
@ -36,12 +38,13 @@ class Chart {
}
class Simfile {
String directoryPath;
String? simfilePath;
String? directoryPath;
String simfilePath;
String? audioPath;
String? bannerPath;
String? lines;
Duration? duration;
// tags of simfile
Map<String, String> tags = {};
@ -50,7 +53,7 @@ class Simfile {
Map<double, double> bpms = {};
double offset = 0;
Simfile(this.directoryPath);
Simfile(this.simfilePath);
void _parseChart({required List<String> keys, required String value}) {
Chart chart = Chart();
@ -119,33 +122,41 @@ class Simfile {
_parseChart(keys: keys, value: value);
}
void load() {
simfilePath = Directory(directoryPath)
.listSync()
.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();
Future<bool> load() async {
directoryPath = File(simfilePath).parent.path;
lines = File(simfilePath).readAsStringSync();
RegExp commentsRegExp = RegExp(r'//.*$');
lines = lines?.replaceAll(commentsRegExp, '');
RegExp fieldDataRegExp = RegExp(r'#([^;]+):([^;]*);');
for (final fieldData in fieldDataRegExp.allMatches(lines!)) {
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 {
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
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/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({
@ -62,8 +63,27 @@ class LevelListEntry extends StatelessWidget {
return ListTile(
leading: Image.file(File(simfile.bannerPath!)),
trailing: Icon(Icons.play_arrow),
title: Text(simfile.tags["TITLE"]!),
subtitle: Text('3:45'),
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);
},