diff --git a/lib/screens/level.dart b/lib/screens/level.dart index e2b2195..9c5823b 100644 --- a/lib/screens/level.dart +++ b/lib/screens/level.dart @@ -244,11 +244,7 @@ class _LevelState extends State 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, diff --git a/lib/screens/level_selection.dart b/lib/screens/level_selection.dart index 41eb4f7..7241ad7 100644 --- a/lib/screens/level_selection.dart +++ b/lib/screens/level_selection.dart @@ -62,16 +62,24 @@ class _LevelSelectionState extends State { try { // List all files and folders in the directory List 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 successfullLoads = + await Future.wait(simfiles.map((simfile) => simfile.load())); + List 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 []; diff --git a/lib/utils/simfile.dart b/lib/utils/simfile.dart index 0af734f..c528084 100644 --- a/lib/utils/simfile.dart +++ b/lib/utils/simfile.dart @@ -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 tags = {}; @@ -50,7 +53,7 @@ class Simfile { Map bpms = {}; double offset = 0; - Simfile(this.directoryPath); + Simfile(this.simfilePath); void _parseChart({required List 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 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; } } diff --git a/lib/widgets/arrows.dart b/lib/widgets/arrows.dart index cf04e59..162f0f3 100644 --- a/lib/widgets/arrows.dart +++ b/lib/widgets/arrows.dart @@ -4,9 +4,8 @@ import 'package:sense_the_rhythm/widgets/arrow.dart'; class Arrows extends StatelessWidget { final List 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) { diff --git a/lib/widgets/level_info_chip.dart b/lib/widgets/level_info_chip.dart new file mode 100644 index 0000000..8e4146c --- /dev/null +++ b/lib/widgets/level_info_chip.dart @@ -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 + ), + ]), + ); + } +} diff --git a/lib/widgets/level_list_entry.dart b/lib/widgets/level_list_entry.dart index 832186f..abb4784 100644 --- a/lib/widgets/level_list_entry.dart +++ b/lib/widgets/level_list_entry.dart @@ -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); },