feat: show duration in level selection + more robust simfile loader
This commit is contained in:
parent
94463a490c
commit
28d0fe7d8c
|
@ -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,
|
||||
|
|
|
@ -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 [];
|
||||
|
|
|
@ -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!)) {
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
37
lib/widgets/level_info_chip.dart
Normal file
37
lib/widgets/level_info_chip.dart
Normal 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
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue