import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:sheetless/core/models/change.dart'; import 'package:sheetless/core/models/sheet.dart'; import 'package:sheetless/core/services/storage_service.dart'; import '../../../shared/widgets/edit_sheet_bottom_sheet.dart'; import 'sheet_list_item.dart'; import 'sheet_search_bar.dart'; /// Widget displaying a searchable list of sheets. /// /// Features: /// - Debounced search filtering by name and composer /// - Long-press to edit sheet metadata /// - Cross-platform scroll support (mouse and touch) class SheetsList extends StatefulWidget { final List sheets; final ValueSetter onSheetSelected; const SheetsList({ super.key, required this.sheets, required this.onSheetSelected, }); @override State createState() => _SheetsListState(); } class _SheetsListState extends State { static const _searchDebounceMs = 500; final _storageService = StorageService(); final _searchController = TextEditingController(); Timer? _debounceTimer; late List _filteredSheets; @override void initState() { super.initState(); _filteredSheets = widget.sheets; _searchController.addListener(_onSearchChanged); } @override void dispose() { _searchController.removeListener(_onSearchChanged); _searchController.dispose(); _debounceTimer?.cancel(); super.dispose(); } // --------------------------------------------------------------------------- // Search // --------------------------------------------------------------------------- void _onSearchChanged() { _debounceTimer?.cancel(); _debounceTimer = Timer( const Duration(milliseconds: _searchDebounceMs), _filterSheets, ); } void _filterSheets() { final query = _searchController.text.toLowerCase().trim(); if (query.isEmpty) { setState(() => _filteredSheets = widget.sheets); return; } // Split query into terms for multi-word search final terms = query.split(RegExp(r'\s+')); setState(() { _filteredSheets = widget.sheets.where((sheet) { final name = sheet.name.toLowerCase(); final composer = sheet.composerName.toLowerCase(); // Each term must appear in either name or composer return terms.every( (term) => name.contains(term) || composer.contains(term), ); }).toList(); }); } void _clearSearch() { _searchController.clear(); setState(() => _filteredSheets = widget.sheets); } // --------------------------------------------------------------------------- // Edit Sheet // --------------------------------------------------------------------------- void _openEditSheet(BuildContext context, Sheet sheet) { showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => EditSheetBottomSheet( sheet: sheet, onSave: (newName, newComposer) => _handleSheetEdit(sheet, newName, newComposer), ), ); } void _handleSheetEdit(Sheet sheet, String newName, String newComposer) { // Queue changes for server sync if (newName != sheet.name) { _storageService.writeChange( Change( type: ChangeType.sheetNameChange, sheetUuid: sheet.uuid, value: newName, ), ); } if (newComposer != sheet.composerName) { _storageService.writeChange( Change( type: ChangeType.composerNameChange, sheetUuid: sheet.uuid, value: newComposer, ), ); } // Update local state setState(() { sheet.name = newName; sheet.composerName = newComposer; }); } // --------------------------------------------------------------------------- // Sheet Selection // --------------------------------------------------------------------------- void _handleSheetTap(Sheet sheet) { // Move selected sheet to top of list (most recently accessed) widget.sheets.remove(sheet); widget.sheets.insert(0, sheet); widget.onSheetSelected(sheet); } // --------------------------------------------------------------------------- // UI // --------------------------------------------------------------------------- @override Widget build(BuildContext context) { return Column( children: [ SheetSearchBar(controller: _searchController, onClear: _clearSearch), Expanded(child: _buildList()), ], ); } Widget _buildList() { // Enable both mouse and touch scrolling for web compatibility return ScrollConfiguration( behavior: ScrollConfiguration.of(context).copyWith( dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse}, ), child: ListView.builder( physics: const AlwaysScrollableScrollPhysics(), itemCount: _filteredSheets.length, itemBuilder: (context, index) { final sheet = _filteredSheets[index]; return SheetListItem( sheet: sheet, onTap: () => _handleSheetTap(sheet), onLongPress: () => _openEditSheet(context, sheet), ); }, ), ); } }