Compare commits

...
2 Commits
9 changed files with 912 additions and 311 deletions
+4 -4
View File
@@ -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.11-blueviolet?style=for-the-badge" alt="Version 2.0.11" />
<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`
---
+2 -2
View File
@@ -1,3 +1,3 @@
const appDisplayVersion = '2.0.9';
const appBuildNumber = 31;
const appDisplayVersion = '2.0.11';
const appBuildNumber = 33;
const appReleaseChannel = 'Stable';
+24
View File
@@ -1,4 +1,28 @@
const lunarisChangelog = [
LunarisChange(
version: '2.0.11',
title: 'Mobile Video Player Redesign & Format Recognition Fix',
bullets: [
'Fixed "Failed to recognize file format" error on initial video load by providing synchronous Referer headers and automatic retry.',
'Modern Mobile Video Player: cinematic top and bottom gradient glassmorphism overlays with immersive controls.',
'Double-Tap Seek: double-tap left side to seek -10s, right side to seek +10s with glowing ripple animations.',
'Hold for 2X Speed Boost: press and hold video to temporarily play at 2.0x with glowing badge, release to resume regular speed.',
'Vertical Volume Swipe: swipe up/down on the right half of the screen to adjust player volume with a floating badge.',
'Playback Speed Selector: quick chip button supporting 0.5x, 0.75x, 1.0x, 1.25x, 1.5x, 2.0x speeds.',
'Screen Lock Mode: lock player controls with one tap to prevent accidental touches during video watching.',
'Sleek Neon Seekbar with live network buffer progress indicator.',
],
),
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',
+9 -7
View File
@@ -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(
File diff suppressed because it is too large Load Diff
+40 -12
View File
@@ -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
View File
@@ -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.11+33
environment:
sdk: ">=3.3.0 <4.0.0"
+11
View File
@@ -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 {