Skip to content

Commit

Permalink
key filter ui and service
Browse files Browse the repository at this point in the history
  • Loading branch information
RedyAu committed Oct 28, 2024
1 parent f13fac0 commit aae7e6d
Show file tree
Hide file tree
Showing 14 changed files with 425 additions and 71 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ app.*.map.json

# Ignore generated files (good practice to keep them out of version control)
*.freezed.dart
*.g.dart
*.g.dart
CONTINUE.txt
29 changes: 24 additions & 5 deletions lib/data/database.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import 'dart:io';

import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
import 'package:path/path.dart' as p;

import 'bank/bank.dart';
import 'song/song.dart';
Expand All @@ -19,6 +23,7 @@ late final Directory dataDir;
include: {
'song/song.drift',
'../services/songs/filter.drift',
'../services/key/select_distinct.drift',
},
)
class LyricDatabase extends _$LyricDatabase {
Expand All @@ -27,10 +32,6 @@ class LyricDatabase extends _$LyricDatabase {
@override
int get schemaVersion => 1;

static QueryExecutor _openConnection() {
return driftDatabase(name: 'lyric');
}

@override
MigrationStrategy get migration {
return MigrationStrategy(onCreate: (Migrator m) async {
Expand All @@ -42,6 +43,24 @@ class LyricDatabase extends _$LyricDatabase {
}
}

// see https://drift.simonbinder.eu/setup/#database-class
// implemented for logStatemets capability
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'lyric.sqlite'));

if (Platform.isAndroid) {
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
}

final cachebase = (await getTemporaryDirectory()).path;
sqlite3.tempDirectory = cachebase;

return NativeDatabase.createInBackground(file, logStatements: true);
});
}

class UriConverter extends TypeConverter<Uri, String> {
const UriConverter();

Expand Down
18 changes: 14 additions & 4 deletions lib/data/song/song.dart
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@ class KeyFieldConverter extends TypeConverter<KeyField?, String> {
}

class KeyField {
final String key;
final String scale;
final String pitch;
final String mode;

KeyField(this.key, this.scale);
KeyField(this.pitch, this.mode);

static KeyField? fromString(String? value) {
if (value == null || value.isEmpty) return null;
Expand All @@ -135,6 +135,16 @@ class KeyField {

@override
String toString() {
return '$key-$scale';
return '$pitch-$mode';
}

@override
bool operator ==(Object other) {
if (other is! KeyField) return false;
if (pitch == other.pitch && mode == other.mode) return true;
return false;
}

@override
int get hashCode => Object.hash(pitch, mode);
}
73 changes: 69 additions & 4 deletions lib/services/key/filter.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,77 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lyric/data/database.dart';
import 'package:lyric/data/song/song.dart';
import 'package:lyric/ui/base/songs/filter/key/state.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'filter.g.dart';

typedef KeyFilterSelectable = ({String label, Function(bool) onSelected, bool selected, bool addingKey});

@Riverpod(keepAlive: true)
Future<List<String>> selectableKeyRootsFor(Ref ref, {String? mode}) {
throw UnimplementedError();
Stream<List<KeyFilterSelectable>> selectablePitches(Ref ref) {
final state = ref.watch(keyFilterStateProvider);
if (state.modes.length == 1 && state.pitches.isEmpty) {
return db.selectDistinctPitches(state.modes.first).watch().map((pitches) {
List<KeyFilterSelectable> selectables = [];
for (var e in pitches) {
final key = KeyField(e, state.modes.first);
if (state.keys.contains(key)) continue;
selectables.add((
label: key.toString(),
onSelected: (v) => ref.read(keyFilterStateProvider.notifier).setKeyTo(key, v),
selected: state.keys.contains(key),
addingKey: true,
));
}
return selectables;
});
} else {
return db.selectDistinctPitches('%').watch().map((pitches) {
return pitches
.map(
(e) => (
label: e,
onSelected: (v) => ref.read(keyFilterStateProvider.notifier).setPitchTo(e, v),
selected: state.pitches.contains(e),
addingKey: false,
),
)
.toList();
});
}
}

@Riverpod(keepAlive: true)
Future<List<String>> selectableKeyModesFor(Ref ref, {String? root}) {
throw UnimplementedError();
Stream<List<KeyFilterSelectable>> selectableModes(Ref ref) {
final state = ref.watch(keyFilterStateProvider);
if (state.pitches.length == 1 && state.modes.isEmpty) {
return db.selectDistinctModes(state.pitches.first).watch().map((modes) {
List<KeyFilterSelectable> selectables = [];
for (var e in modes) {
final key = KeyField(state.pitches.first, e);
if (state.keys.contains(key)) continue;
selectables.add((
label: key.toString(),
onSelected: (v) => ref.read(keyFilterStateProvider.notifier).setKeyTo(key, v),
selected: state.keys.contains(key),
addingKey: true,
));
}
return selectables;
});
} else {
return db.selectDistinctModes('%').watch().map((modes) {
return modes
.map(
(e) => (
label: e,
onSelected: (v) => ref.read(keyFilterStateProvider.notifier).setModeTo(e, v),
selected: state.modes.contains(e),
addingKey: false,
),
)
.toList();
});
}
}
26 changes: 26 additions & 0 deletions lib/services/key/select_distinct.drift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import '../../data/song/song.drift';
import '../../data/song/song.dart';

selectDistinctKeys:
SELECT DISTINCT
key_field
FROM songs;

selectDistinctPitches(:for_mode AS TEXT):
SELECT DISTINCT
substr(
key_field
,1
,instr(key_field, '-') - 1
)
FROM songs
WHERE key_field != '' AND key_field LIKE concat('%-', :for_mode);

selectDistinctModes(:for_pitch AS TEXT):
SELECT DISTINCT
substr(
key_field
,instr(key_field, '-') + 1
)
FROM songs
WHERE key_field != '' AND key_field LIKE concat(:for_pitch, '-%');
18 changes: 16 additions & 2 deletions lib/services/songs/filter.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:drift/drift.dart';
import 'package:drift/extensions/json1.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lyric/ui/base/songs/filter/key/state.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../../data/database.dart';
Expand Down Expand Up @@ -72,6 +73,7 @@ Stream<List<SongResult>> filteredSongs(Ref ref) {
final String searchString = sanitize(ref.watch(searchStringStateProvider));
final List<String> searchFields = ref.watch(searchFieldsStateProvider);
final Map<String, List<String>> filters = ref.watch(filterStateProvider);
final KeyFilters keyFilters = ref.watch(keyFilterStateProvider);

String ftsMatchString = '{${searchFields.join(' ')}} : $searchString';
print(ftsMatchString);
Expand All @@ -84,15 +86,27 @@ Stream<List<SongResult>> filteredSongs(Ref ref) {
entry.value.map((value) => fieldData.like('%$value%')),
);
},
));
).followedBy([
Expression.or([
if (keyFilters.pitches.isNotEmpty || keyFilters.modes.isNotEmpty)
Expression.and([
if (keyFilters.pitches.isNotEmpty)
Expression.or(keyFilters.pitches.map((e) => songsFts.keyField.like('$e-%'))),
if (keyFilters.modes.isNotEmpty)
Expression.or(keyFilters.modes.map((e) => songsFts.keyField.like('%-$e'))),
]),
if (keyFilters.keys.isNotEmpty)
Expression.or(keyFilters.keys.map((e) => songsFts.keyField.equals(e.toString()))),
], ifEmpty: Constant(true))
]));
}

if (searchString.isEmpty) {
return ((db.select(db.songsFts)..where((songsFts) => filterExpression(songsFts))).watch()).map(
(songsFtList) => songsFtList.map((songsFt) => SongResult(songsFt: songsFt)).toList(),
);
} else {
return (db.song_fulltext_search(ftsMatchString, (songsFts) => filterExpression(songsFts)).watch()).map(
return (db.songFulltextSearch(ftsMatchString, (songsFts) => filterExpression(songsFts)).watch()).map(
(matchList) => matchList.map((match) => SongResult(match: match)).toList(),
);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/services/songs/filter.drift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import '../../data/song/song.drift';
import '../../data/song/song.dart';

song_fulltext_search(:match_string AS TEXT):
songFulltextSearch(:match_string AS TEXT):
SELECT
bm25(songs_fts, 0.0, 0.0, 10.0, 0.5, 5.0, 5.0, 2.0, 0.0, 0.0) AS rank
,uuid
Expand Down
3 changes: 2 additions & 1 deletion lib/ui/base/songs/filter/general/state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ const Map<String, Map<String, dynamic>> songFieldsMap = {
'type': 'filterable_multiselect',
'icon': Icons.height,
},
'key': {
'pitch': {
// todo change back to key
'title_hu': 'Hangnem',
'type': 'filterable_key',
'icon': Icons.music_note,
Expand Down
75 changes: 42 additions & 33 deletions lib/ui/base/songs/filter/general/widgets/filters.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ class FiltersColumn extends ConsumerWidget {
fieldType: e.value.type,
fieldPopulatedCount: e.value.count,
),
FieldType.key => KeyFilterCard(),
FieldType.key => KeyFilterCard(
fieldPopulatedCount: e.value.count,
),
_ => LErrorCard(
type: LErrorType.warning,
title: 'Nem támogatott szűrőtípus!',
Expand Down Expand Up @@ -75,11 +77,13 @@ class LFilterChipsState extends ConsumerState<FilterChips> {

@override
Widget build(BuildContext context) {
// todo final
var selectableValues =
ref.watch(selectableValuesForFilterableFieldProvider(widget.field, widget.fieldType));
var filterState = ref.watch(filterStateProvider);
var filterStateNotifier = ref.read(filterStateProvider.notifier);

// todo final
bool active() => filterState.containsKey(widget.field);

return Card(
Expand Down Expand Up @@ -116,36 +120,30 @@ class LFilterChipsState extends ConsumerState<FilterChips> {
subtitle: switch (selectableValues) {
AsyncLoading() => LinearProgressIndicator(),
AsyncError(:final error) => Text('Hiba a szűrőértékek lekérdezése közben: $error'),
AsyncValue(:final value) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: SizedBox(
height: 38,
child: FadingEdgeScrollView.fromScrollView(
child: ListView.builder(
shrinkWrap: true,
controller: _filterChipsRowController,
scrollDirection: Axis.horizontal,
itemCount: value!.length,
itemBuilder: (context, i) {
String item = value[i];
bool selected = filterState[widget.field]?.contains(item) ?? false;
onSelected(bool newValue) {
if (newValue) {
filterStateNotifier.addFilter(widget.field, item);
} else {
filterStateNotifier.removeFilter(widget.field, item);
}
}

return LFilterChip(label: item, onSelected: onSelected, selected: selected);
},
),
),
),
)
],
AsyncValue(:final value) => SizedBox(
height: 38,
child: FadingEdgeScrollView.fromScrollView(
child: ListView.builder(
shrinkWrap: true,
controller: _filterChipsRowController,
scrollDirection: Axis.horizontal,
itemCount: value!.length,
itemBuilder: (context, i) {
// todo move all logic to service (like in filter/key/widget.dart)
String item = value[i];
bool selected = filterState[widget.field]?.contains(item) ?? false;
onSelected(bool newValue) {
if (newValue) {
filterStateNotifier.addFilter(widget.field, item);
} else {
filterStateNotifier.removeFilter(widget.field, item);
}
}

return LFilterChip(label: item, onSelected: onSelected, selected: selected);
},
),
),
)
},
trailing: active()
Expand All @@ -162,12 +160,16 @@ class LFilterChip extends StatelessWidget {
required this.label,
required this.onSelected,
required this.selected,
this.leading,
this.special = false,
super.key,
});

final String label;
final Function(bool) onSelected;
final bool selected;
final bool special;
final Widget? leading;

@override
Widget build(BuildContext context) {
Expand All @@ -176,12 +178,19 @@ class LFilterChip extends StatelessWidget {
child: FilterChip.elevated(
color: WidgetStateProperty.resolveWith((states) {
if (!states.contains(WidgetState.selected)) {
if (special) return Theme.of(context).colorScheme.surfaceContainer;
return Theme.of(context).cardColor;
} else {
return Theme.of(context).focusColor;
return Theme.of(context).colorScheme.surfaceContainerHighest;
}
}),
label: Text(label),
labelPadding: EdgeInsets.only(left: leading != null ? 0 : 5, right: 5),
label: Row(
children: [
if (leading != null) Padding(padding: EdgeInsets.only(right: 5), child: leading!),
Text(label),
],
),
selected: selected,
onSelected: onSelected,
materialTapTargetSize: MaterialTapTargetSize.padded,
Expand Down
Loading

0 comments on commit aae7e6d

Please sign in to comment.