diff --git a/README.md b/README.md
index b3b14dc..2371714 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
-
+
@@ -20,6 +20,9 @@
## ✨ Основные возможности
+- **⚡ Мгновенные подсказки тегов (0 мс) и постоянный файловый кэш**: Локальный JSON-кэш на телефоне и ПК обеспечивает мгновенный отклик при наборе, а Danbooru, Gelbooru, Moebooru и e621 динамически дополняют базу тегов.
+- **🎨 Цветные бейджи категорий и подсветка совпадений**: Выделение введенных букв жирным цветом акцента и четкая цветовая палитра категорий (художник — красный, персонаж — зеленый, копирайт — фиолетовый, вид — голубой).
+- **📚 Расширенная история поиска и гибкие настройки**: До 2000 записей истории и до 20000 закэшированных тегов с ручным управлением и кнопками очистки в Настройках.
- **🎯 Общие модификаторы в мультипоиске**: При вводе `cat and dog anthro` модификатор `anthro` автоматически применится к обоим тегам (`cat anthro` + `dog anthro`).
- **🔀 Раздельный мульти-поиск по тегам через `and`**: Оператор `and` опрашивает серверную базу по каждому тегу независимо и плавно чередует посты обоих тегов в общей ленте.
- **🌐 Pawchive в главной ленте**: Глобальная лента недавних постов с Pawchive подключена прямо в общий Feed наравне с Rule34, Gelbooru, e621 и Realbooru.
@@ -66,9 +69,9 @@
🔗 **[Gitea Releases (RarDog/Lunaris)](https://gitea.rardogsynapse.online/RarDog/Lunaris/releases)**
-- **Android**: `Lunaris-v2.0.8.apk`
-- **Linux (x64)**: `Lunaris-v2.0.8-linux-x64.tar.gz`
-- **Windows**: `LunarisSetup-v2.0.8.exe` / `LunarisPortable-v2.0.8.zip`
+- **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`
---
diff --git a/lib/app/app_version.dart b/lib/app/app_version.dart
index 93e2ecc..85e5d90 100644
--- a/lib/app/app_version.dart
+++ b/lib/app/app_version.dart
@@ -1,3 +1,3 @@
-const appDisplayVersion = '2.0.8';
-const appBuildNumber = 30;
+const appDisplayVersion = '2.0.9';
+const appBuildNumber = 31;
const appReleaseChannel = 'Stable';
diff --git a/lib/app/changelog.dart b/lib/app/changelog.dart
index 74ed28b..9917a12 100644
--- a/lib/app/changelog.dart
+++ b/lib/app/changelog.dart
@@ -1,4 +1,18 @@
const lunarisChangelog = [
+ LunarisChange(
+ version: '2.0.9',
+ title: 'Instant Tag Suggestions, Persistent Disk Cache, Expanded History & Match Highlighting',
+ bullets: [
+ 'Instant 0ms tag suggestions: cached tags and search history are served immediately without network delay.',
+ 'Persistent disk cache: tag suggestions are preserved in lunaris_tag_cache.json across app restarts on Android and PC.',
+ 'Debounce reduced from 530ms to 100ms+160ms for ultra-responsive typing.',
+ 'Match highlighting: matched characters are bolded and highlighted with the primary theme color in the dropdown.',
+ 'Vibrant category badges: distinct colors for Artist (red), Character (green), Copyright (purple), Species (blue), and Meta (grey).',
+ 'Danbooru tag autocomplete support connected (/tags/autocomplete.json).',
+ 'Configurable search history limit (up to 2000 items) and tag cache limit (up to 20000 tags) in Settings.',
+ 'Dedicated Clear Tag Cache actions in Settings and Cache Manager.',
+ ],
+ ),
LunarisChange(
version: '2.0.8',
title: 'Shared Context Modifiers for Multi-Tag Search & Clean Tag Filtering',
diff --git a/lib/backend/backend.dart b/lib/backend/backend.dart
index 409dacb..35b2821 100644
--- a/lib/backend/backend.dart
+++ b/lib/backend/backend.dart
@@ -28,6 +28,7 @@ export 'services/favorite_service.dart';
export 'services/feed_service.dart';
export 'services/provider_check_service.dart';
export 'services/search_service.dart';
+export 'services/tag_cache_service.dart';
export 'services/settings_service.dart';
export 'services/update_service.dart';
export 'services/viewed_history_service.dart';
diff --git a/lib/backend/di/backend_providers.dart b/lib/backend/di/backend_providers.dart
index 2575b12..d9f2a99 100644
--- a/lib/backend/di/backend_providers.dart
+++ b/lib/backend/di/backend_providers.dart
@@ -1,3 +1,5 @@
+import 'dart:async';
+
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -28,6 +30,7 @@ import '../services/favorite_service.dart';
import '../services/feed_service.dart';
import '../services/provider_check_service.dart';
import '../services/search_service.dart';
+import '../services/tag_cache_service.dart';
import '../services/settings_service.dart';
import '../services/update_service.dart';
import '../services/viewed_history_service.dart';
@@ -125,10 +128,17 @@ final viewedKeysProvider = FutureProvider>((ref) async {
return result is Success> ? result.data : {};
});
+final tagCacheServiceProvider = Provider((ref) {
+ final service = TagCacheService();
+ unawaited(service.init());
+ return service;
+});
+
final searchServiceProvider = Provider((ref) {
return SearchService(
ref.watch(searchRepositoryProvider),
ref.watch(providerManagerProvider),
+ ref.watch(tagCacheServiceProvider),
);
});
diff --git a/lib/backend/models/tag_suggestion.dart b/lib/backend/models/tag_suggestion.dart
index a95c795..2b3c4bb 100644
--- a/lib/backend/models/tag_suggestion.dart
+++ b/lib/backend/models/tag_suggestion.dart
@@ -22,6 +22,30 @@ class TagSuggestion {
final String providerId;
String get categoryLabel => category.name;
+
+ Map toJson() => {
+ 'name': name,
+ 'category': category.name,
+ 'postCount': postCount,
+ 'providerId': providerId,
+ };
+
+ factory TagSuggestion.fromJson(Map json) => TagSuggestion(
+ name: (json['name'] ?? '').toString(),
+ category: tagCategoryFromString((json['category'] ?? '').toString()),
+ postCount: (json['postCount'] as num?)?.toInt() ?? 0,
+ providerId: (json['providerId'] ?? '').toString(),
+ );
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is TagSuggestion &&
+ runtimeType == other.runtimeType &&
+ name.toLowerCase() == other.name.toLowerCase();
+
+ @override
+ int get hashCode => name.toLowerCase().hashCode;
}
TagCategory tagCategoryFromString(String? value) {
diff --git a/lib/backend/providers/danbooru_provider.dart b/lib/backend/providers/danbooru_provider.dart
index f6fb544..e1ed400 100644
--- a/lib/backend/providers/danbooru_provider.dart
+++ b/lib/backend/providers/danbooru_provider.dart
@@ -5,11 +5,16 @@ import '../mappers/danbooru_mapper.dart';
import '../models/post.dart';
import '../models/post_comment.dart';
import '../models/provider_health.dart';
+import '../models/tag_suggestion.dart';
import '../models/top_period_filter.dart';
import 'content_provider.dart';
class DanbooruProvider
- implements ContentProvider, CommentProvider, PostPageProvider {
+ implements
+ ContentProvider,
+ CommentProvider,
+ PostPageProvider,
+ TagSuggestionProvider {
DanbooruProvider({
required this.id,
required this.name,
@@ -171,4 +176,49 @@ class DanbooruProvider
'${value.month.toString().padLeft(2, '0')}-'
'${value.day.toString().padLeft(2, '0')}';
}
+
+ @override
+ Future> suggestTags(
+ String query, {
+ int limit = 20,
+ }) async {
+ final cleanQuery = query
+ .replaceAll(RegExp(r'^[\(\)]+|[\(\)]+$'), '')
+ .trim()
+ .toLowerCase();
+ if (cleanQuery.isEmpty) return const [];
+ try {
+ final response = await _dio.get(
+ '/tags/autocomplete.json',
+ queryParameters: {
+ 'search[name_matches]': cleanQuery,
+ 'limit': limit.clamp(1, 50),
+ ..._queryParameters,
+ },
+ );
+ final items = response.data is List ? response.data as List : const [];
+ return items
+ .whereType