import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:sheetless/edit_bottom_sheet.dart'; import 'package:sheetless/storage_helper.dart'; import 'package:sheetless/upload_queue.dart'; class Sheet { final String uuid; String name; String composerUuid; String composerName; Sheet({ required this.uuid, required this.name, required this.composerUuid, required this.composerName, }); // Factory constructor for creating a Sheet from JSON factory Sheet.fromJson(Map json) { return Sheet( uuid: json['uuid'], name: json['sheet_name'], composerUuid: json['composer_uuid'], composerName: json['composer_name'], ); } } class SheetsWidget extends StatefulWidget { final List sheets; final ValueSetter onSheetOpenRequest; const SheetsWidget({ super.key, required this.sheets, required this.onSheetOpenRequest, }); @override State createState() => _SheetsWidgetState(); } class _SheetsWidgetState extends State { late List filteredSheets; final StorageHelper storageHelper = StorageHelper(); final TextEditingController _searchController = TextEditingController(); Timer? _debounce; @override void initState() { super.initState(); filteredSheets = widget.sheets; _searchController.addListener(_onSearchChanged); } @override void dispose() { _searchController.removeListener(_onSearchChanged); _searchController.dispose(); _debounce?.cancel(); super.dispose(); } void _onSearchChanged() { if (_debounce?.isActive ?? false) _debounce!.cancel(); _debounce = Timer(const Duration(milliseconds: 500), () { _filterSheets(); }); } void _filterSheets() { setState(() { String query = _searchController.text.toLowerCase().trim(); List terms = query.split(RegExp(r'\s+')); // Split by whitespace filteredSheets = widget.sheets.where((sheet) { String name = sheet.name.toLowerCase(); String composer = sheet.composerName.toLowerCase(); // Each term must be found in either the name or composer return terms.every( (term) => name.contains(term) || composer.contains(term), ); }).toList(); }); } void _clearSearch() { _searchController.clear(); } void _openEditSheet(BuildContext context, Sheet sheet) { showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => EditItemBottomSheet( sheet: sheet, onSheetEdited: (String newName, String newComposer) { if (newName != sheet.name) { storageHelper.writeChange( Change( type: ChangeType.sheetNameChange, sheetUuid: sheet.uuid, value: newName, ), ); } if (newComposer != sheet.composerName) { storageHelper.writeChange( Change( type: ChangeType.composerNameChange, sheetUuid: sheet.uuid, value: newComposer, ), ); } setState(() { sheet.name = newName; sheet.composerName = newComposer; }); }, ), ); } @override Widget build(BuildContext context) { return Column( children: [ Padding( padding: const EdgeInsets.all(8.0), child: TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Search...', prefixIcon: const Icon(Icons.search), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: _clearSearch, ) : null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8.0), ), ), ), ), Expanded( // Fixes scroll on web child: ScrollConfiguration( behavior: ScrollConfiguration.of(context).copyWith( dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse}, ), child: ListView.builder( physics: const AlwaysScrollableScrollPhysics(), itemCount: filteredSheets.length, itemBuilder: (context, index) { var sheet = filteredSheets[index]; return ListTile( title: Text(sheet.name), subtitle: Text(sheet.composerName), onTap: () => setState(() { widget.onSheetOpenRequest(sheet); widget.sheets.remove(sheet); widget.sheets.insert(0, sheet); }), onLongPress: () => _openEditSheet(context, sheet), ); }, ), ), ), ], ); } }