Files
sheetless/lib/sheet.dart
2026-01-24 19:22:03 +01:00

186 lines
5.2 KiB
Dart

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;
DateTime updatedAt;
Sheet({
required this.uuid,
required this.name,
required this.composerUuid,
required this.composerName,
required this.updatedAt,
});
// Factory constructor for creating a Sheet from JSON
factory Sheet.fromJson(Map<String, dynamic> json) {
final composer = json['composer'] as Map<String, dynamic>?;
return Sheet(
uuid: json['uuid'].toString(),
name: json['title'],
composerUuid: json['composer_uuid']?.toString() ?? '',
composerName: composer?['name'] ?? 'Unknown',
updatedAt: DateTime.parse(json['updated_at']),
);
}
}
class SheetsWidget extends StatefulWidget {
final List<Sheet> sheets;
final ValueSetter<Sheet> onSheetOpenRequest;
const SheetsWidget({
super.key,
required this.sheets,
required this.onSheetOpenRequest,
});
@override
State<SheetsWidget> createState() => _SheetsWidgetState();
}
class _SheetsWidgetState extends State<SheetsWidget> {
late List<Sheet> 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<String> 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),
);
},
),
),
),
],
);
}
}