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