level_selection.dart (5479B)
1 import 'dart:io'; 2 3 import 'package:flutter/material.dart'; 4 import 'package:permission_handler/permission_handler.dart'; 5 import 'package:shared_preferences/shared_preferences.dart'; 6 import 'package:file_picker/file_picker.dart'; 7 import 'package:sense_the_rhythm/utils/esense_input.dart'; 8 import 'package:sense_the_rhythm/utils/simfile.dart'; 9 import 'package:sense_the_rhythm/widgets/connection_status_button.dart'; 10 import 'package:sense_the_rhythm/widgets/level_list_entry.dart'; 11 12 class LevelSelection extends StatefulWidget { 13 const LevelSelection({super.key}); 14 15 @override 16 State<LevelSelection> createState() => _LevelSelectionState(); 17 } 18 19 class _LevelSelectionState extends State<LevelSelection> { 20 String? _stepmaniaCoursesPath; 21 List<Simfile> _stepmaniaCoursesFolders = []; 22 List<Simfile> _stepmaniaCoursesFoldersFiltered = []; 23 24 @override 25 void initState() { 26 super.initState(); 27 _loadFolderPath(); 28 } 29 30 /// gets folder path from persistent storage and updates state with loaded simfiles 31 Future<void> _loadFolderPath() async { 32 SharedPreferences prefs = await SharedPreferences.getInstance(); 33 final String? stepmaniaCoursesPathSetting = 34 prefs.getString('stepmania_courses'); 35 36 if (stepmaniaCoursesPathSetting == null) return; 37 List<Simfile> stepmaniaCoursesFoldersFuture = 38 await _listFilesAndFolders(stepmaniaCoursesPathSetting); 39 40 setState(() { 41 _stepmaniaCoursesPath = stepmaniaCoursesPathSetting; 42 _stepmaniaCoursesFolders = stepmaniaCoursesFoldersFuture; 43 _stepmaniaCoursesFoldersFiltered = stepmaniaCoursesFoldersFuture; 44 }); 45 } 46 47 /// open folder selection dialog and save selected folder in persistent storage 48 Future<void> _selectFolder() async { 49 await Permission.manageExternalStorage.request(); 50 String? selectedFolder = await FilePicker.platform.getDirectoryPath(); 51 52 if (selectedFolder != null) { 53 // Save the selected folder path 54 SharedPreferences prefs = await SharedPreferences.getInstance(); 55 await prefs.setString('stepmania_courses', selectedFolder); 56 57 _loadFolderPath(); 58 } 59 } 60 61 /// load all simfiles from a [directoryPath] 62 Future<List<Simfile>> _listFilesAndFolders(String directoryPath) async { 63 final directory = Directory(directoryPath); 64 try { 65 // List all files and folders in the directory 66 List<Simfile> simfiles = directory 67 .listSync(recursive: true) 68 .where((entity) => entity.path.endsWith('.sm')) 69 .map((entity) => Simfile(entity.path)) 70 .toList(); 71 72 List<bool> successfullLoads = 73 await Future.wait(simfiles.map((simfile) => simfile.load())); 74 List<Simfile> simfilesFiltered = []; 75 for (int i = 0; i < simfiles.length; i++) { 76 if (successfullLoads[i]) { 77 simfilesFiltered.add(simfiles[i]); 78 } 79 } 80 81 simfilesFiltered 82 .sort((a, b) => a.tags['TITLE']!.compareTo(b.tags['TITLE']!)); 83 84 return simfilesFiltered; 85 } catch (e) { 86 print("Error reading directory: $e"); 87 return []; 88 } 89 } 90 91 /// filter stepmaniaCoursesFolders based on [input] 92 void _filterLevels(String input) { 93 setState(() { 94 _stepmaniaCoursesFoldersFiltered = _stepmaniaCoursesFolders 95 .where((simfile) => simfile.tags["TITLE"]! 96 .toLowerCase() 97 .contains(input.toLowerCase())) 98 .toList(); 99 }); 100 } 101 102 @override 103 Widget build(BuildContext context) { 104 return Scaffold( 105 appBar: AppBar( 106 title: const Text('Sense the Rhythm'), 107 actions: [ 108 Padding( 109 padding: const EdgeInsets.symmetric(horizontal: 8.0), 110 child: ValueListenableBuilder( 111 valueListenable: ESenseInput.instance.deviceStatus, 112 builder: 113 (BuildContext context, String deviceStatus, Widget? child) { 114 return ConnectionStatusButton(deviceStatus); 115 }, 116 ), 117 ) 118 ], 119 ), 120 body: Builder(builder: (context) { 121 if (_stepmaniaCoursesPath == null) { 122 return Text('Add a Directory with Stepmania Songs on \'+\''); 123 } else if (_stepmaniaCoursesFolders.isEmpty) { 124 return Text( 125 'Folder empty. Add Stepmania Songs to Folder or select a different folder on \'+\''); 126 } else { 127 return Column( 128 children: [ 129 Padding( 130 padding: 131 const EdgeInsets.symmetric(horizontal: 16.0, vertical: 0.0), 132 child: TextField( 133 onChanged: _filterLevels, 134 decoration: InputDecoration( 135 // icon: Icon(Icons.search), 136 hintText: 'Search'), 137 ), 138 ), 139 Expanded( 140 child: ListView.separated( 141 itemCount: _stepmaniaCoursesFoldersFiltered.length, 142 separatorBuilder: (BuildContext context, int index) => 143 const Divider(), 144 itemBuilder: (context, index) { 145 Simfile simfile = _stepmaniaCoursesFoldersFiltered[index]; 146 return LevelListEntry(simfile: simfile); 147 }, 148 ), 149 ), 150 ], 151 ); 152 } 153 }), 154 floatingActionButton: FloatingActionButton( 155 onPressed: () { 156 _selectFolder(); 157 }, 158 child: Icon(Icons.add)), 159 ); 160 } 161 }