fix: preserve AND tag operator during search and suggestion draft completions (v2.0.10)
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Version-2.0.9-blueviolet?style=for-the-badge" alt="Version 2.0.9" />
|
||||
<img src="https://img.shields.io/badge/Version-2.0.10-blueviolet?style=for-the-badge" alt="Version 2.0.10" />
|
||||
<img src="https://img.shields.io/badge/Flutter-3.47+-02569B?style=for-the-badge&logo=flutter&logoColor=white" alt="Flutter" />
|
||||
<img src="https://img.shields.io/badge/Platform-Android%20%7C%20Linux%20%7C%20Windows-brightgreen?style=for-the-badge" alt="Platforms" />
|
||||
<img src="https://img.shields.io/badge/Gitea-RarDog%2FLunaris-orange?style=for-the-badge&logo=gitea&logoColor=white" alt="Gitea" />
|
||||
@@ -69,9 +69,9 @@
|
||||
|
||||
🔗 **[Gitea Releases (RarDog/Lunaris)](https://gitea.rardogsynapse.online/RarDog/Lunaris/releases)**
|
||||
|
||||
- **Android**: `Lunaris-v2.0.9.apk`
|
||||
- **Linux (x64)**: `Lunaris-v2.0.9-linux-x64.tar.gz`
|
||||
- **Windows**: `LunarisSetup-v2.0.9.exe` / `LunarisPortable-v2.0.9.zip`
|
||||
- **Android**: `Lunaris-v2.0.10.apk`
|
||||
- **Linux (x64)**: `Lunaris-v2.0.10-linux-x64.tar.gz`
|
||||
- **Windows**: `LunarisSetup-v2.0.10.exe` / `LunarisPortable-v2.0.10.zip`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
const appDisplayVersion = '2.0.9';
|
||||
const appBuildNumber = 31;
|
||||
const appDisplayVersion = '2.0.10';
|
||||
const appBuildNumber = 32;
|
||||
const appReleaseChannel = 'Stable';
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
const lunarisChangelog = [
|
||||
LunarisChange(
|
||||
version: '2.0.10',
|
||||
title: 'Fix "and" Tag Operator Retention, Suggestion Preceding Drafts & AND Chip Styling',
|
||||
bullets: [
|
||||
'Fixed "and" operator disappearing when submitting search or applying autocompletions.',
|
||||
'Preserved preceding draft tokens (e.g. typing "cat and d" then picking "dog" now retains "cat", "AND", and "dog").',
|
||||
'Supported multiple "and" separators without accidental deduplication.',
|
||||
'Distinct visual styling for AND separator chips with secondary theme container and alt-route icon.',
|
||||
],
|
||||
),
|
||||
LunarisChange(
|
||||
version: '2.0.9',
|
||||
title: 'Instant Tag Suggestions, Persistent Disk Cache, Expanded History & Match Highlighting',
|
||||
|
||||
@@ -28,13 +28,15 @@ class SearchService {
|
||||
}
|
||||
|
||||
List<String> parseTags(String query) {
|
||||
return query
|
||||
.trim()
|
||||
.split(RegExp(r'\s+'))
|
||||
.map(sanitizeToken)
|
||||
.where((tag) => tag.isNotEmpty && tag != 'and')
|
||||
.toSet()
|
||||
.toList();
|
||||
final seen = <String>{};
|
||||
final result = <String>[];
|
||||
for (final raw in query.trim().split(RegExp(r'\s+'))) {
|
||||
final tag = raw.trim();
|
||||
if (tag.isEmpty) continue;
|
||||
if (tag.toLowerCase() != 'and' && !seen.add(tag.toLowerCase())) continue;
|
||||
result.add(tag);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Result<void>> saveSearch(
|
||||
|
||||
@@ -242,18 +242,34 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
|
||||
|
||||
Widget _buildTagChip(BuildContext context, String tag) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final isAnd = tag.toLowerCase() == 'and';
|
||||
return InputChip(
|
||||
label: Text(tag),
|
||||
avatar: const Icon(Icons.tag_rounded, size: 16),
|
||||
label: Text(
|
||||
isAnd ? 'AND' : tag,
|
||||
style: TextStyle(
|
||||
color: isAnd
|
||||
? scheme.onSecondaryContainer
|
||||
: scheme.onPrimaryContainer,
|
||||
fontWeight: isAnd ? FontWeight.w900 : FontWeight.w700,
|
||||
letterSpacing: isAnd ? 0.8 : 0,
|
||||
),
|
||||
),
|
||||
avatar: Icon(
|
||||
isAnd ? Icons.alt_route_rounded : Icons.tag_rounded,
|
||||
size: 16,
|
||||
color: isAnd ? scheme.secondary : scheme.primary,
|
||||
),
|
||||
deleteIcon: const Icon(Icons.close_rounded, size: 16),
|
||||
onPressed: () => _editTag(tag),
|
||||
onDeleted: () => _removeTag(tag),
|
||||
backgroundColor: scheme.primaryContainer.withValues(alpha: 0.54),
|
||||
labelStyle: TextStyle(
|
||||
color: scheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.w700,
|
||||
backgroundColor: isAnd
|
||||
? scheme.secondaryContainer.withValues(alpha: 0.7)
|
||||
: scheme.primaryContainer.withValues(alpha: 0.54),
|
||||
side: BorderSide(
|
||||
color: isAnd
|
||||
? scheme.secondary.withValues(alpha: 0.4)
|
||||
: scheme.primary.withValues(alpha: 0.18),
|
||||
),
|
||||
side: BorderSide(color: scheme.primary.withValues(alpha: 0.18)),
|
||||
visualDensity: VisualDensity.compact,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
);
|
||||
@@ -306,7 +322,9 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
|
||||
_localDirty = true;
|
||||
setState(() {
|
||||
for (final tag in additions) {
|
||||
if (!_tags.contains(tag)) _tags.add(tag);
|
||||
if (tag.toLowerCase() == 'and' || !_tags.contains(tag)) {
|
||||
_tags.add(tag);
|
||||
}
|
||||
}
|
||||
_controller.clear();
|
||||
});
|
||||
@@ -359,10 +377,19 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
|
||||
_localDirty = true;
|
||||
setState(() {
|
||||
if (draft.isNotEmpty) {
|
||||
final draftTags = _parseTags(draft);
|
||||
_tags.removeWhere(draftTags.contains);
|
||||
final tokens = _parseTags(draft);
|
||||
if (tokens.isNotEmpty) {
|
||||
final prefixTokens = tokens.sublist(0, tokens.length - 1);
|
||||
for (final token in prefixTokens) {
|
||||
if (token.toLowerCase() == 'and' || !_tags.contains(token)) {
|
||||
_tags.add(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!_tags.contains(suggestion)) {
|
||||
_tags.add(suggestion);
|
||||
}
|
||||
if (!_tags.contains(suggestion)) _tags.add(suggestion);
|
||||
_controller.clear();
|
||||
});
|
||||
_notifyChanged();
|
||||
@@ -387,7 +414,8 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
|
||||
final result = <String>[];
|
||||
for (final raw in query.trim().split(RegExp(r'\s+'))) {
|
||||
final tag = raw.trim();
|
||||
if (tag.isEmpty || !seen.add(tag)) continue;
|
||||
if (tag.isEmpty) continue;
|
||||
if (tag.toLowerCase() != 'and' && !seen.add(tag.toLowerCase())) continue;
|
||||
result.add(tag);
|
||||
}
|
||||
return result;
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
name: gel_rule_app
|
||||
description: Local backend/core layer for a Flutter booru browser app.
|
||||
publish_to: "none"
|
||||
version: 2.0.9+31
|
||||
version: 2.0.10+32
|
||||
|
||||
environment:
|
||||
sdk: ">=3.3.0 <4.0.0"
|
||||
|
||||
@@ -43,6 +43,17 @@ void main() {
|
||||
expect(service.parseTags(' cat cute cat '), ['cat', 'cute']);
|
||||
});
|
||||
|
||||
test('parses query preserving and operators and grouping', () {
|
||||
expect(
|
||||
service.parseTags('cat and dog and fox'),
|
||||
['cat', 'and', 'dog', 'and', 'fox'],
|
||||
);
|
||||
expect(
|
||||
service.parseTags('(cat and dog) anthro'),
|
||||
['(cat', 'and', 'dog)', 'anthro'],
|
||||
);
|
||||
});
|
||||
|
||||
test('recent searches are ordered by searchedAt', () async {
|
||||
await service.saveSearch('old', 1);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 1));
|
||||
|
||||
@@ -288,6 +288,57 @@ void main() {
|
||||
expect(size.height, lessThan(96));
|
||||
expect(find.text('touhou'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('preserves AND operator chips when submitted', (tester) async {
|
||||
String submitted = '';
|
||||
await tester.pumpWidget(_Harness(
|
||||
child: TagInputSearchBar(
|
||||
initialValue: 'cat and dog',
|
||||
onSubmitted: (query) => submitted = query,
|
||||
),
|
||||
));
|
||||
|
||||
expect(find.text('cat'), findsOneWidget);
|
||||
expect(find.text('AND'), findsOneWidget);
|
||||
expect(find.text('dog'), findsOneWidget);
|
||||
|
||||
await tester.showKeyboard(find.byType(TextField));
|
||||
await tester.testTextInput.receiveAction(TextInputAction.search);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(submitted, 'cat and dog');
|
||||
});
|
||||
|
||||
testWidgets('applying suggestion preserves preceding tokens and AND operator',
|
||||
(tester) async {
|
||||
String applied = '';
|
||||
await tester.pumpWidget(_Harness(
|
||||
child: TagInputSearchBar(
|
||||
suggestions: const [
|
||||
TagSuggestion(
|
||||
name: 'dog',
|
||||
category: TagCategory.general,
|
||||
postCount: 50,
|
||||
providerId: 'test',
|
||||
),
|
||||
],
|
||||
onSuggestionApplied: (query) => applied = query,
|
||||
onSubmitted: (_) {},
|
||||
),
|
||||
));
|
||||
|
||||
await tester.enterText(find.byType(TextField), 'cat and d');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('dog'), findsOneWidget);
|
||||
await tester.tap(find.text('dog'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(applied, 'cat and dog');
|
||||
expect(find.text('cat'), findsOneWidget);
|
||||
expect(find.text('AND'), findsOneWidget);
|
||||
expect(find.text('dog'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
class _Harness extends StatelessWidget {
|
||||
|
||||
Reference in New Issue
Block a user