Fix feed tag search and prepare 2.0.1+22
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "ff37bef603469fb030f2b72995ab929ccfc227f0"
|
||||
revision: "559ffa3f75e7402d65a8def9c28389a9b2e6fe42"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
@@ -13,17 +13,11 @@ project_type: app
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
- platform: android
|
||||
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||
- platform: linux
|
||||
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
- platform: windows
|
||||
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
|
||||
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
|
||||
|
||||
# User provided section
|
||||
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||
android.builtInKotlin=false
|
||||
# This newDsl flag was added automatically by Flutter migrator
|
||||
android.newDsl=false
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
const appDisplayVersion = '2.0.1';
|
||||
const appBuildNumber = 21;
|
||||
const appBuildNumber = 22;
|
||||
const appReleaseChannel = 'Stable';
|
||||
|
||||
@@ -32,7 +32,7 @@ class _FeedScreenState extends ConsumerState<FeedScreen> {
|
||||
final _scrollController = ScrollController();
|
||||
final Set<String> _selectedKeys = {};
|
||||
bool _selectionMode = false;
|
||||
bool _usedInitialQuery = false;
|
||||
String? _appliedInitialQuery;
|
||||
Timer? _scrollSaveDebounce;
|
||||
|
||||
@override
|
||||
@@ -70,10 +70,10 @@ class _FeedScreenState extends ConsumerState<FeedScreen> {
|
||||
if (nextQuery != null &&
|
||||
nextQuery.isNotEmpty &&
|
||||
nextQuery != oldWidget.initialQuery) {
|
||||
_usedInitialQuery = true;
|
||||
_appliedInitialQuery = nextQuery;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
ref.read(feedControllerProvider.notifier).search(nextQuery);
|
||||
_applySearchQuery(nextQuery);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -93,10 +93,16 @@ class _FeedScreenState extends ConsumerState<FeedScreen> {
|
||||
final favoriteKeys = ref.watch(favoriteKeysProvider).value ?? <String>{};
|
||||
final viewedKeys = ref.watch(viewedKeysProvider).value ?? <String>{};
|
||||
|
||||
if (!_usedInitialQuery && (widget.initialQuery?.isNotEmpty ?? false)) {
|
||||
_usedInitialQuery = true;
|
||||
final initialQuery = widget.initialQuery?.trim();
|
||||
final currentTags = feed.value?.selectedTags.join(' ');
|
||||
if (initialQuery != null &&
|
||||
initialQuery.isNotEmpty &&
|
||||
_appliedInitialQuery != initialQuery &&
|
||||
currentTags != initialQuery) {
|
||||
_appliedInitialQuery = initialQuery;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(feedControllerProvider.notifier).search(widget.initialQuery!);
|
||||
if (!mounted) return;
|
||||
_applySearchQuery(initialQuery);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -159,17 +165,14 @@ class _FeedScreenState extends ConsumerState<FeedScreen> {
|
||||
onSearchChanged: (query) => ref
|
||||
.read(feedControllerProvider.notifier)
|
||||
.updateTagSuggestions(query),
|
||||
onSuggestionTap: (query) =>
|
||||
ref.read(feedControllerProvider.notifier).search(query),
|
||||
onSuggestionTap: _submitSearch,
|
||||
onTopPeriodChanged: (period) => ref
|
||||
.read(feedControllerProvider.notifier)
|
||||
.setTopPeriod(period),
|
||||
onSearch: (query) =>
|
||||
ref.read(feedControllerProvider.notifier).search(query),
|
||||
onSearch: _submitSearch,
|
||||
onRefresh: () =>
|
||||
ref.read(feedControllerProvider.notifier).refresh(),
|
||||
onClearFilters: () =>
|
||||
ref.read(feedControllerProvider.notifier).clearFilters(),
|
||||
onClearFilters: _clearFilters,
|
||||
selectionMode: _selectionMode,
|
||||
onToggleSelectionMode: () {
|
||||
setState(() {
|
||||
@@ -279,6 +282,35 @@ class _FeedScreenState extends ConsumerState<FeedScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _submitSearch(String query) {
|
||||
final trimmed = query.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
_clearFilters();
|
||||
return;
|
||||
}
|
||||
final location = '/?q=${Uri.encodeQueryComponent(trimmed)}';
|
||||
if (widget.initialQuery?.trim() == trimmed) {
|
||||
_applySearchQuery(trimmed);
|
||||
return;
|
||||
}
|
||||
context.go(location);
|
||||
}
|
||||
|
||||
Future<void> _applySearchQuery(String query) async {
|
||||
await ref.read(feedControllerProvider.notifier).search(query);
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.jumpTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _clearFilters() async {
|
||||
_appliedInitialQuery = null;
|
||||
await ref.read(feedControllerProvider.notifier).clearFilters();
|
||||
if (mounted && widget.initialQuery != null) {
|
||||
context.go('/');
|
||||
}
|
||||
}
|
||||
|
||||
void _toggleSelected(Post post) {
|
||||
setState(() {
|
||||
_selectionMode = true;
|
||||
|
||||
@@ -3,33 +3,57 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../backend/backend.dart';
|
||||
import '../../../core/utils/result.dart';
|
||||
|
||||
// Меняем FutureProvider на StreamProvider
|
||||
final postDetailsControllerProvider =
|
||||
FutureProvider.family<Post?, PostDetailsArgs>((ref, args) async {
|
||||
StreamProvider.family<Post?, PostDetailsArgs>((ref, args) async* {
|
||||
// Правильно собираем зависимости на самом верхнем уровне (фиксим антипаттерн)
|
||||
final providerManager = ref.watch(providerManagerProvider);
|
||||
final cacheService = ref.watch(cacheServiceProvider);
|
||||
final postRepository = ref.watch(postRepositoryProvider);
|
||||
|
||||
// Вспомогательная функция для загрузки тегов
|
||||
Future<Post?> enrich(Post? post) async {
|
||||
if (post == null) return null;
|
||||
final shouldEnrich = post.tagGroups.isEmpty ||
|
||||
(post.tagGroups.length == 1 && post.tagGroups.containsKey('general'));
|
||||
if (!shouldEnrich) return post;
|
||||
final enriched =
|
||||
await ref.watch(providerManagerProvider).enrichPostTags(post);
|
||||
|
||||
final enriched = await providerManager.enrichPostTags(post);
|
||||
if (enriched is Success<Post>) {
|
||||
await ref.watch(cacheServiceProvider).cachePosts([enriched.data]);
|
||||
await cacheService.cachePosts([enriched.data]);
|
||||
return enriched.data;
|
||||
}
|
||||
return post;
|
||||
}
|
||||
|
||||
if (args.initialPost != null) return enrich(args.initialPost);
|
||||
final cached = await ref
|
||||
.watch(postRepositoryProvider)
|
||||
.getCachedPost(args.postId, args.providerId);
|
||||
// СЦЕНАРИЙ 1: Пост уже передан из ленты (Самый частый случай)
|
||||
if (args.initialPost != null) {
|
||||
yield args
|
||||
.initialPost; // МГНОВЕННО отдаем пост в UI, экран сразу рендерится!
|
||||
final enriched = await enrich(args.initialPost);
|
||||
yield enriched; // Чуть позже отдаем версию с загруженными тегами
|
||||
return;
|
||||
}
|
||||
|
||||
// СЦЕНАРИЙ 2: Прямой переход (например, по ссылке), проверяем локальный кэш
|
||||
final cached =
|
||||
await postRepository.getCachedPost(args.postId, args.providerId);
|
||||
if (cached is Success<Post?> && cached.data != null) {
|
||||
return enrich(cached.data);
|
||||
yield cached.data; // Нашли в кэше — сразу показываем контент
|
||||
final enriched = await enrich(cached.data);
|
||||
yield enriched; // Догружаем теги на фоне
|
||||
return;
|
||||
}
|
||||
|
||||
// СЦЕНАРИЙ 3: Полный фоллбэк, если вообще ничего нет — идем в сеть за самим постом
|
||||
final remote = await providerManager.getPost(args.providerId, args.postId);
|
||||
if (remote is Success<Post?> && remote.data != null) {
|
||||
yield remote.data;
|
||||
final enriched = await enrich(remote.data);
|
||||
yield enriched;
|
||||
} else {
|
||||
yield null;
|
||||
}
|
||||
final remote = await ref
|
||||
.watch(providerManagerProvider)
|
||||
.getPost(args.providerId, args.postId);
|
||||
return remote is Success<Post?> ? enrich(remote.data) : null;
|
||||
});
|
||||
|
||||
class PostDetailsArgs {
|
||||
|
||||
@@ -178,8 +178,7 @@ class PostDetailsScreen extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton.filledTonal(
|
||||
tooltip:
|
||||
strings.ru ? 'Предыдущий' : 'Previous',
|
||||
tooltip: strings.ru ? 'Предыдущий' : 'Previous',
|
||||
onPressed: previous == null
|
||||
? null
|
||||
: () => _openPost(context, previous),
|
||||
@@ -205,7 +204,7 @@ class PostDetailsScreen extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
tooltip: strings.ru ? 'Следующий' : 'Next',
|
||||
tooltip: strings.ru ? 'Следующий' : 'Next',
|
||||
onPressed: next == null
|
||||
? null
|
||||
: () => _openPost(context, next),
|
||||
@@ -402,9 +401,7 @@ class PostDetailsScreen extends ConsumerWidget {
|
||||
final strings = ref.read(appStringsProvider);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(strings.ru
|
||||
? 'Скачивание началось'
|
||||
: 'Download started'),
|
||||
content: Text(strings.ru ? 'Скачивание началось' : 'Download started'),
|
||||
),
|
||||
);
|
||||
Future<void>.delayed(const Duration(seconds: 2), () {
|
||||
@@ -518,7 +515,7 @@ class PostDetailsScreen extends ConsumerWidget {
|
||||
collection: strings.collection,
|
||||
similar: strings.similar,
|
||||
openOriginal: strings.open,
|
||||
copyLink: strings.ru ? 'Копировать ссылку' : 'Copy link',
|
||||
copyLink: strings.ru ? 'Копировать ссылку' : 'Copy link',
|
||||
download: strings.download,
|
||||
deleteLocalFile: strings.deleteLocalFile,
|
||||
hidePost: strings.hidePost,
|
||||
@@ -602,12 +599,10 @@ class _MobilePostPager extends StatefulWidget {
|
||||
|
||||
class _MobilePostPagerState extends State<_MobilePostPager> {
|
||||
late final PageController _controller;
|
||||
late int _currentIndex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentIndex = widget.initialIndex;
|
||||
_controller = PageController(initialPage: widget.initialIndex);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_prefetchAround(widget.initialIndex);
|
||||
@@ -617,10 +612,10 @@ class _MobilePostPagerState extends State<_MobilePostPager> {
|
||||
@override
|
||||
void didUpdateWidget(covariant _MobilePostPager oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.initialIndex != _currentIndex &&
|
||||
// Исправленная проверка: сравниваем старый и новый initialIndex, а не текущий свайп
|
||||
if (oldWidget.initialIndex != widget.initialIndex &&
|
||||
widget.initialIndex >= 0 &&
|
||||
widget.initialIndex < widget.posts.length) {
|
||||
_currentIndex = widget.initialIndex;
|
||||
if (_controller.hasClients) {
|
||||
_controller.jumpToPage(widget.initialIndex);
|
||||
}
|
||||
@@ -642,7 +637,6 @@ class _MobilePostPagerState extends State<_MobilePostPager> {
|
||||
controller: _controller,
|
||||
itemCount: widget.posts.length,
|
||||
onPageChanged: (index) {
|
||||
_currentIndex = index;
|
||||
_prefetchAround(index);
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
|
||||
+22
-14
@@ -165,10 +165,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: dad6bf6b9f4f378b0a69edbf42584d336efd1a9ce15deb1ba591cbb1b5ff440f
|
||||
sha256: "67cf6d84013f9c601e42a6f8a6b74c4c0d9dc1a1619d775f2b28b732d3551b85"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.2.0"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -289,6 +289,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
ffi_leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi_leak_tracker
|
||||
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -675,10 +683,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.18.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -731,18 +739,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20"
|
||||
sha256: "4bf625947f6c7713ee242296a682e23e44823c09cf9d79e4f1238923c92db852"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.1"
|
||||
version: "10.1.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||
sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
version: "4.1.0"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1056,10 +1064,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
version: "0.7.11"
|
||||
time:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1200,10 +1208,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_plus
|
||||
sha256: ddf3db70eaa10c37558ff817519b85d527dbd21034fd5d8e1c2e85f31588f1c1
|
||||
sha256: "824c5bba0f800e86d32e57d3d1843c531f090005cc89d9a837933e6601093d53"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
version: "1.6.1"
|
||||
wakelock_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1256,10 +1264,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
|
||||
sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.15.0"
|
||||
version: "6.3.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
+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.1+21
|
||||
version: 2.0.1+22
|
||||
|
||||
environment:
|
||||
sdk: ">=3.3.0 <4.0.0"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:ffi' show Abi;
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -15,6 +16,7 @@ import 'package:gel_rule_app/core/cache/cache_service.dart';
|
||||
import 'package:gel_rule_app/core/database/app_database.dart';
|
||||
import 'package:gel_rule_app/core/database/database_service.dart';
|
||||
import 'package:gel_rule_app/core/utils/result.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
void main() {
|
||||
late Directory directory;
|
||||
@@ -24,6 +26,14 @@ void main() {
|
||||
late File copiedIsarDll;
|
||||
|
||||
setUpAll(() async {
|
||||
if (Platform.isLinux) {
|
||||
await Isar.initializeIsarCore(
|
||||
libraries: {
|
||||
Abi.linuxX64:
|
||||
'${Directory.current.path}/third_party/isar_flutter_libs/linux/libisar.so',
|
||||
},
|
||||
);
|
||||
}
|
||||
copiedIsarDll =
|
||||
File('${Directory.current.path}${Platform.pathSeparator}isar.dll');
|
||||
if (Platform.isWindows && !copiedIsarDll.existsSync()) {
|
||||
|
||||
@@ -57,6 +57,48 @@ void main() {
|
||||
expect(changed, 'video');
|
||||
});
|
||||
|
||||
testWidgets('clear button removes all committed tags', (tester) async {
|
||||
String? changed;
|
||||
await tester.pumpWidget(_Harness(
|
||||
child: TagInputSearchBar(
|
||||
initialValue: 'touhou video',
|
||||
onSubmitted: (_) {},
|
||||
onChanged: (value) {
|
||||
changed = value;
|
||||
},
|
||||
),
|
||||
));
|
||||
|
||||
await tester.tap(find.byTooltip('Clear'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('touhou'), findsNothing);
|
||||
expect(find.text('video'), findsNothing);
|
||||
expect(changed, '');
|
||||
});
|
||||
|
||||
testWidgets('external initial value replaces committed tags', (tester) async {
|
||||
await tester.pumpWidget(_Harness(
|
||||
child: TagInputSearchBar(
|
||||
initialValue: 'old_tag',
|
||||
onSubmitted: (_) {},
|
||||
),
|
||||
));
|
||||
|
||||
expect(find.text('old_tag'), findsOneWidget);
|
||||
|
||||
await tester.pumpWidget(_Harness(
|
||||
child: TagInputSearchBar(
|
||||
initialValue: 'new_tag',
|
||||
onSubmitted: (_) {},
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('old_tag'), findsNothing);
|
||||
expect(find.text('new_tag'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('suggestion appends active tag as a chip', (tester) async {
|
||||
String? applied;
|
||||
await tester.pumpWidget(_Harness(
|
||||
|
||||
Reference in New Issue
Block a user