diff --git a/lib/reader/screens.dart b/lib/reader/screens.dart index d3e2ac1b4efcf8654456a851716ccc22b0555245..70da9adadc454a4a8ca8577b0eb5a3f0ffd5c7d4 100644 --- a/lib/reader/screens.dart +++ b/lib/reader/screens.dart @@ -5,7 +5,7 @@ import 'package:provider/provider.dart'; import 'package:flows/flows.dart' as flows; import 'package:flutter_svg/flutter_svg.dart'; import 'package:fk/utils/deep_link_handler.dart'; -import '../l10n/app_localizations.dart'; +import 'package:fk/l10n/app_localizations.dart'; import '../diagnostics.dart'; diff --git a/lib/services/location_service.dart b/lib/services/location_service.dart index c3360d279fc6619fd83395a7471ec3cd29397b45..4f1116a8539388951d6c08fa2c7ba6d0b04c7074 100644 --- a/lib/services/location_service.dart +++ b/lib/services/location_service.dart @@ -1,5 +1,5 @@ import 'package:geolocator/geolocator.dart'; -import '../l10n/app_localizations.dart'; +import 'package:fk/l10n/app_localizations.dart'; import 'package:flutter/material.dart'; import 'dart:io' show Platform; diff --git a/lib/settings/pdf_viewer.dart b/lib/settings/pdf_viewer.dart index 26567723d21be25bb4e38a0ae769a1c11351cb23..84791d294f2b8d46f3ae0ec9e281abfc4770930d 100644 --- a/lib/settings/pdf_viewer.dart +++ b/lib/settings/pdf_viewer.dart @@ -22,6 +22,8 @@ class PdfViewerPageState extends State { final searchController = TextEditingController(); bool isSearchVisible = false; + int _currentSearchIndex = 0; + bool _isNavigating = false; @override void initState() { @@ -92,7 +94,6 @@ class PdfViewerPageState extends State { @override void dispose() { _linkSubscription?.cancel(); - textSearcher.removeListener(_onSearchUpdate); textSearcher.dispose(); searchController.dispose(); super.dispose(); @@ -101,14 +102,81 @@ class PdfViewerPageState extends State { void _onSearchChanged(String query) { if (query.isEmpty) { textSearcher.resetTextSearch(); + _currentSearchIndex = 0; } else { textSearcher.startTextSearch(query, caseInsensitive: true); + _currentSearchIndex = 0; } - setState(() {}); + + _isNavigating = false; } - void _onSearchUpdate() { - setState(() {}); + Future _navigateToSearchMatch(bool isForward) async { + if (_isNavigating || !textSearcher.hasMatches) return; + + setState(() => _isNavigating = true); + + try { + final totalMatches = textSearcher.matches.length; + + if (isForward) { + if (_currentSearchIndex >= totalMatches - 1) { + // Going beyond the last result, loop to first + _currentSearchIndex = 0; + final currentQuery = searchController.text; + if (currentQuery.isNotEmpty) { + textSearcher.startTextSearch(currentQuery, caseInsensitive: true); + await Future.delayed(const Duration(milliseconds: 100)); + } + } else { + await textSearcher.goToNextMatch(); + _currentSearchIndex++; + } + } else { + if (_currentSearchIndex == 0) { + // At first result, wrap to last + _currentSearchIndex = totalMatches - 1; + for (int i = 0; i < totalMatches - 1; i++) { + await textSearcher.goToNextMatch(); + } + } else { + await textSearcher.goToPrevMatch(); + _currentSearchIndex--; + } + } + + // Ensure the index loops around properly + _currentSearchIndex = _currentSearchIndex % totalMatches; + } catch (e) { + // Navigation error + } finally { + if (mounted) { + setState(() => _isNavigating = false); + } + } + } + + void _navigateToNextResult() { + if (!textSearcher.hasMatches) return; + + final totalMatches = textSearcher.matches.length; + + if (_currentSearchIndex < totalMatches - 1) { + textSearcher.goToNextMatch(); + _currentSearchIndex++; + } else { + // At or beyond last result, loop to first + _currentSearchIndex = 0; + final currentQuery = searchController.text; + if (currentQuery.isNotEmpty) { + textSearcher.startTextSearch(currentQuery, caseInsensitive: true); + } + } + + // Ensure the index loops around properly + _currentSearchIndex = _currentSearchIndex % totalMatches; + + if (mounted) setState(() {}); } void _toggleSearch() { @@ -117,6 +185,8 @@ class PdfViewerPageState extends State { if (!isSearchVisible) { searchController.clear(); textSearcher.resetTextSearch(); + _currentSearchIndex = 0; + _isNavigating = false; } }); } @@ -125,7 +195,6 @@ class PdfViewerPageState extends State { Widget build(BuildContext context) { final localizations = AppLocalizations.of(context)!; final hasResults = textSearcher.hasMatches; - final currentIndex = textSearcher.currentIndex ?? 0; final totalMatches = textSearcher.matches.length; return Scaffold( @@ -142,6 +211,7 @@ class PdfViewerPageState extends State { children: [ if (isSearchVisible) Container( + margin: const EdgeInsets.only(top: 1.0), padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), decoration: BoxDecoration( @@ -172,53 +242,87 @@ class PdfViewerPageState extends State { color: Theme.of(context).primaryColor), ), contentPadding: - const EdgeInsets.symmetric(horizontal: 10.0), + const EdgeInsets.fromLTRB(10, 14, 10, 10), filled: true, fillColor: Colors.white, isDense: true, suffixIcon: searchController.text.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear, size: 18), - onPressed: () { - searchController.clear(); - _onSearchChanged(''); - }, + ? Transform.translate( + offset: const Offset(0, -2), + child: IconButton( + icon: const Icon(Icons.clear, size: 18), + onPressed: () { + searchController.clear(); + _onSearchChanged(''); + }, + ), ) : null, ), onChanged: _onSearchChanged, + onSubmitted: (query) { + if (query.isNotEmpty && textSearcher.hasMatches) { + _navigateToNextResult(); + } + }, ), ), ), - if (searchController.text.isNotEmpty) - Row( - children: [ + Row( + children: [ + if (searchController.text.isNotEmpty) ...[ IconButton( - icon: const Icon(Icons.keyboard_arrow_up, size: 20), + icon: _isNavigating + ? const SizedBox( + width: 20, + height: 20, + child: + CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.keyboard_arrow_up, size: 20), tooltip: localizations.search, - onPressed: hasResults - ? () => textSearcher.goToPrevMatch() + onPressed: (hasResults && !_isNavigating) + ? () { + _navigateToSearchMatch(false); + } : null, ), IconButton( - icon: const Icon(Icons.keyboard_arrow_down, size: 20), + icon: _isNavigating + ? const SizedBox( + width: 20, + height: 20, + child: + CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.keyboard_arrow_down, size: 20), tooltip: localizations.search, - onPressed: hasResults - ? () => textSearcher.goToNextMatch() + onPressed: (hasResults && !_isNavigating) + ? () { + _navigateToSearchMatch(true); + } : null, ), - if (hasResults) + if (searchController.text.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), child: Text( - '$currentIndex/$totalMatches', + hasResults + ? '${_currentSearchIndex + 1}/$totalMatches' + : '', style: const TextStyle( fontSize: 12, color: Colors.black54), ), ), + ] else ...[ + // Placeholder to maintain consistent spacing when no search text + const SizedBox( + width: + 120), // Width of navigation buttons + counter text ], - ), + ], + ), ], ), ), diff --git a/lib/splash_screen.dart b/lib/splash_screen.dart index b0c4a9c8090297dec051d4623a375b1bafe3dee9..ac16f43e4619008363b0806aec27aa026284a6f6 100644 --- a/lib/splash_screen.dart +++ b/lib/splash_screen.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'l10n/app_localizations.dart'; +import 'package:fk/l10n/app_localizations.dart'; class FullScreenLogo extends StatelessWidget { const FullScreenLogo({super.key});