improve weblibre search

This commit is contained in:
Fabian Freund
2026-06-01 07:17:37 +02:00
parent 88f128621f
commit 453e1a6c30
23 changed files with 1095 additions and 796 deletions
@@ -4,6 +4,8 @@ import 'package:weblibre/features/web_search/domain/controllers/search_controlle
import 'package:weblibre/features/web_search/domain/entities/captured_page_state.dart';
import 'package:weblibre/features/web_search/domain/entities/fetch_method.dart';
import 'package:weblibre/features/web_search/domain/services/capture_artifact_downloader.dart';
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
import 'package:weblibre/presentation/widgets/url_icon.dart';
Future<void> showFetchMethodSheet(
BuildContext context, {
@@ -55,13 +57,12 @@ class _FetchMethodSheet extends ConsumerWidget {
children: [
Text('Fetch Page Data', style: textTheme.titleLarge),
const SizedBox(height: 4),
Text(
url.toString(),
UriBreadcrumb(
uri: url,
icon: UrlIcon([url], iconSize: 14, cacheOnly: true),
style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
@@ -99,35 +100,63 @@ class _MethodTile extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final textTheme = theme.textTheme;
final busy = state.isMethodBusy(url, choice);
final ready = state.isMethodReady(url, choice);
final captured = state.capturedPage(url, choice);
final failed = captured?.status == CapturedPageStatus.downloadFailed;
// The trafilatura "preview" goes through fetchPage/fetchErrorByUrl rather
// than the capture pipeline, so its failures live in a different place
// than a capture's downloadFailed status. Surface either as the tile's
// error here (the card no longer shows per-method error chips).
final String? errorMessage;
if (busy || ready) {
errorMessage = null;
} else if (choice == FetchMethodChoice.trafilatura) {
errorMessage = state.fetchError(url);
} else if (captured?.status == CapturedPageStatus.downloadFailed) {
errorMessage = captured?.errorMessage ?? 'Download failed — tap to retry';
} else {
errorMessage = null;
}
final failed = errorMessage != null;
// Starting (or retrying) anything needs an open session; already-ready
// artifacts can still be opened after the session closes.
final enabled = !busy && (ready || state.hasOpenSession);
// A disabled ListTile only dims its title automatically; the leading icon
// and subtitle carry explicit colors, so dim them too (matching the title's
// disabled color) to keep the whole tile reading as deactivated.
final leadingColor = enabled
? colorScheme.onSurfaceVariant
: theme.disabledColor;
final subtitleColor = !enabled
? theme.disabledColor
: (failed ? colorScheme.error : colorScheme.onSurfaceVariant);
return ListTile(
enabled: enabled,
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
leading: Icon(choice.icon, color: colorScheme.onSurfaceVariant),
leading: Icon(choice.icon, color: leadingColor),
title: Text(choice.title),
subtitle: Text(
failed
? (captured?.errorMessage ?? 'Download failed — tap to retry')
: choice.subtitle,
style: textTheme.bodySmall?.copyWith(
color: failed ? colorScheme.error : colorScheme.onSurfaceVariant,
),
failed ? errorMessage : choice.subtitle,
style: textTheme.bodySmall?.copyWith(color: subtitleColor),
),
trailing: _StatusIndicator(
busy: busy,
ready: ready,
failed: failed,
idleColor: leadingColor,
colorScheme: colorScheme,
),
onTap: busy
? null
: () => _handleTap(context, ref, ready, failed, captured),
onTap: enabled
? () => _handleTap(context, ref, ready, failed, captured)
: null,
);
}
@@ -148,17 +177,25 @@ class _MethodTile extends ConsumerWidget {
return;
}
Navigator.of(context).pop();
// Keep the sheet open while a fetch/download runs so its per-method tile
// shows the live spinner; the user opens the result from here once ready.
if (failed && captured != null) {
await ref
.read(metaSearchControllerProvider.notifier)
.retryCaptureDownload(url, choice);
// Trafilatura (re)fetch — the same call covers a first fetch and a retry
// after a previous fetch error.
if (choice == FetchMethodChoice.trafilatura) {
await ref.read(metaSearchControllerProvider.notifier).fetchPage(url);
return;
}
if (choice == FetchMethodChoice.trafilatura) {
await ref.read(metaSearchControllerProvider.notifier).fetchPage(url);
// A failed *download* (we still hold the captureId/downloadToken) only
// needs the artifact re-fetched — no new upstream render. A server-side
// capture failure has no token, so it falls through to a fresh capture.
if (failed &&
captured?.captureId != null &&
captured?.downloadToken != null) {
await ref
.read(metaSearchControllerProvider.notifier)
.retryCaptureDownload(url, choice);
return;
}
@@ -178,12 +215,14 @@ class _StatusIndicator extends StatelessWidget {
final bool busy;
final bool ready;
final bool failed;
final Color idleColor;
final ColorScheme colorScheme;
const _StatusIndicator({
required this.busy,
required this.ready,
required this.failed,
required this.idleColor,
required this.colorScheme,
});
@@ -202,6 +241,7 @@ class _StatusIndicator extends StatelessWidget {
if (failed) {
return Icon(Icons.refresh, color: colorScheme.error);
}
return Icon(Icons.download_rounded, color: colorScheme.onSurfaceVariant);
// Idle: mirror the leading icon so a disabled tile dims uniformly.
return Icon(Icons.download_rounded, color: idleColor);
}
}