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: [
|
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,
|
||||||
|
|
|
@ -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 [];
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
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/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);
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue