make home screen consider containers as well

This commit is contained in:
Fabian Freund
2026-03-15 05:49:07 +01:00
parent 4354f7ed06
commit 305377c0af
6 changed files with 293 additions and 50 deletions
@@ -300,6 +300,20 @@ class TabRepository extends _$TabRepository {
return selectTab(latestTab.id);
}
Future<bool> resumeLatestContainerTab(String? containerId) async {
final latestTab = await ref
.read(tabDatabaseProvider)
.tabDao
.getContainerTabsFifo(containerId, limit: 1)
.getSingleOrNull();
if (!ref.mounted || latestTab == null) {
return false;
}
return selectTab(latestTab.id);
}
Future<bool> selectPreviousTab(
String tabId, {
String? containerId,
@@ -9,6 +9,9 @@ import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_list.dart';
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/providers/browser_viewport_toolbar_insets.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart';
import 'package:weblibre/features/quotes/data/database/definitions.drift.dart';
import 'package:weblibre/features/quotes/domain/providers.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
@@ -39,6 +42,18 @@ class BrowserHome extends ConsumerWidget {
browserViewportToolbarInsetsControllerProvider,
);
final containerData = ref.watch(
selectedContainerDataProvider.select((value) => value.value),
);
final hasContainerTabs = ref.watch(
selectedContainerTabCountProvider.select(
(data) => switch (data) {
AsyncData(:final value) => value > 0,
_ => false,
},
),
);
final pixelRatio = MediaQuery.devicePixelRatioOf(context);
final bottomViewportInset =
@@ -58,6 +73,13 @@ class BrowserHome extends ConsumerWidget {
await ref.read(tabRepositoryProvider.notifier).resumeLatestTab();
}
Future<void> resumeLatestContainerTab() async {
final containerId = ref.read(selectedContainerProvider);
await ref
.read(tabRepositoryProvider.notifier)
.resumeLatestContainerTab(containerId);
}
return DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
@@ -135,49 +157,10 @@ class BrowserHome extends ConsumerWidget {
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 112,
height: 112,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color.alphaBlend(
_brandPurple.withValues(alpha: 0.18),
colorScheme.surfaceContainerHighest,
),
Color.alphaBlend(
_brandYellow.withValues(alpha: 0.12),
colorScheme.surfaceContainer,
),
],
),
border: Border.all(
color: colorScheme.outlineVariant.withValues(
alpha: 0.45,
),
),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withValues(
alpha: 0.08,
),
blurRadius: 32,
offset: const Offset(0, 18),
),
],
),
child: Center(
child: SvgPicture.asset(
'assets/icon/icon.svg',
width: 72,
height: 72,
),
),
),
if (containerData != null)
_ContainerHeader(container: containerData)
else
_BrandHeader(colorScheme: colorScheme),
const SizedBox(height: 24),
if (!hasTabs) ...[
Text(
@@ -197,6 +180,28 @@ class BrowserHome extends ConsumerWidget {
),
),
const SizedBox(height: 28),
] else if (containerData != null) ...[
Text(
containerData.name?.isNotEmpty == true
? containerData.name!
: 'Container',
textAlign: TextAlign.center,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 10),
Text(
hasContainerTabs
? 'No matching tab selected'
: 'No open tabs in this container',
textAlign: TextAlign.center,
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant,
height: 1.45,
),
),
const SizedBox(height: 28),
],
Container(
width: double.infinity,
@@ -287,7 +292,13 @@ class BrowserHome extends ConsumerWidget {
icon: const Icon(Icons.add_rounded),
label: const Text('New tab'),
),
if (hasTabs)
if (hasContainerTabs)
FilledButton.tonalIcon(
onPressed: resumeLatestContainerTab,
icon: const Icon(Icons.history_rounded),
label: const Text('Resume last tab'),
)
else if (hasTabs && containerData == null)
FilledButton.tonalIcon(
onPressed: resumeLatestTab,
icon: const Icon(Icons.history_rounded),
@@ -309,6 +320,102 @@ class BrowserHome extends ConsumerWidget {
}
}
class _BrandHeader extends StatelessWidget {
final ColorScheme colorScheme;
const _BrandHeader({required this.colorScheme});
@override
Widget build(BuildContext context) {
return Container(
width: 112,
height: 112,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color.alphaBlend(
BrowserHome._brandPurple.withValues(alpha: 0.18),
colorScheme.surfaceContainerHighest,
),
Color.alphaBlend(
BrowserHome._brandYellow.withValues(alpha: 0.12),
colorScheme.surfaceContainer,
),
],
),
border: Border.all(
color: colorScheme.outlineVariant.withValues(alpha: 0.45),
),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withValues(alpha: 0.08),
blurRadius: 32,
offset: const Offset(0, 18),
),
],
),
child: Center(
child: SvgPicture.asset('assets/icon/icon.svg', width: 72, height: 72),
),
);
}
}
class _ContainerHeader extends StatelessWidget {
final ContainerData container;
const _ContainerHeader({required this.container});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final containerColor = container.color;
return Container(
width: 112,
height: 112,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color.alphaBlend(
ContainerColors.forChip(containerColor),
colorScheme.surfaceContainerHighest,
),
Color.alphaBlend(
containerColor.withValues(alpha: 0.12),
colorScheme.surfaceContainer,
),
],
),
border: Border.all(
color: Color.alphaBlend(
containerColor.withValues(alpha: 0.25),
colorScheme.outlineVariant.withValues(alpha: 0.45),
),
),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withValues(alpha: 0.08),
blurRadius: 32,
offset: const Offset(0, 18),
),
],
),
child: Center(
child: SvgPicture.asset('assets/icon/icon.svg', width: 72, height: 72),
),
);
}
}
class _QuoteBlock extends StatelessWidget {
final Quote? quote;
@@ -34,7 +34,6 @@ import 'package:weblibre/features/bangs/domain/providers/bangs.dart';
import 'package:weblibre/features/bangs/domain/services/search_history_cleanup.dart';
import 'package:weblibre/features/geckoview/domain/providers.dart';
import 'package:weblibre/features/geckoview/domain/providers/browser_extension.dart';
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/domain/providers/web_extensions_state.dart';
@@ -50,6 +49,7 @@ import 'package:weblibre/features/geckoview/features/history/domain/repositories
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart';
import 'package:weblibre/features/geckoview/features/pwa/domain/providers.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
import 'package:weblibre/features/share_intent/domain/entities/shared_content.dart';
@@ -150,9 +150,7 @@ class _BrowserViewState extends ConsumerState<BrowserView>
}
});
final hasTab = ref.watch(
selectedTabProvider.select((value) => value != null),
);
final showHome = ref.watch(shouldShowBrowserHomeProvider);
final topRoute = ref.watch(currentTopRouteProvider);
final androidInfoAsync = ref.watch(androidDeviceInfoProvider);
@@ -291,7 +289,7 @@ class _BrowserViewState extends ConsumerState<BrowserView>
},
),
),
if (!hasTab) const Positioned.fill(child: BrowserHome()),
if (showHome) const Positioned.fill(child: BrowserHome()),
],
),
);
@@ -101,6 +101,15 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
..orderBy([(t) => OrderingTerm.desc(t.timestamp)]);
}
Selectable<TabData> getContainerTabsFifo(String? containerId, {int limit = 25}) {
return select(db.tab)
..where((t) => containerId != null
? t.containerId.equals(containerId)
: t.containerId.isNull())
..limit(limit)
..orderBy([(t) => OrderingTerm.desc(t.timestamp)]);
}
SingleOrNullSelectable<String?> getTabContainerId(String tabId) {
final query = selectOnly(db.tab)
..addColumns([db.tab.containerId])
@@ -26,6 +26,7 @@ import 'package:riverpod_annotation/experimental/persist.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/core/logger.dart';
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
@@ -165,6 +166,37 @@ Stream<ContainerData?> selectedContainerData(Ref ref) {
return Stream.value(null);
}
/// Whether the browser home screen should be displayed instead of the
/// active tab's content.
///
/// Returns `true` when any of the following hold:
/// 1. No tab is selected at all (app just started or all tabs closed).
/// 2. The selected tab belongs to a different container than the currently
/// selected container this implies the user manually switched
/// containers after selecting a tab, because tab selection automatically
/// syncs the selected container to match the tab's container.
///
/// Condition (2) also implicitly covers the case where the selected
/// container has zero tabs: if the container has no tabs, the selected tab
/// (if any) necessarily belongs to a different container.
@Riverpod()
bool shouldShowBrowserHome(Ref ref) {
final selectedTab = ref.watch(selectedTabProvider);
// No tab selected → always show home.
if (selectedTab == null) return true;
final selectedContainer = ref.watch(selectedContainerProvider);
final tabContainerId = ref.watch(selectedTabContainerIdProvider);
// Once we know the tab's container, compare with the selected container.
return switch (tabContainerId) {
AsyncData(:final value) => value != selectedContainer,
// While loading, keep the current view to avoid flashing.
_ => false,
};
}
@Riverpod()
Future<int> selectedContainerTabCount(Ref ref) async {
final selectedContainer = ref.watch(selectedContainerProvider);
@@ -41,7 +41,7 @@ final class SelectedContainerProvider
}
}
String _$selectedContainerHash() => r'1e2c67dc70fedb2b910640fbfbe09f2c4806894b';
String _$selectedContainerHash() => r'3d30966f0b8a8ee091afb48fb045be2274f4f417';
abstract class _$SelectedContainer extends $Notifier<String?> {
String? build();
@@ -101,6 +101,89 @@ final class SelectedContainerDataProvider
String _$selectedContainerDataHash() =>
r'1ec86a82e1fc4823a867285f05036c903633a165';
/// Whether the browser home screen should be displayed instead of the
/// active tab's content.
///
/// Returns `true` when any of the following hold:
/// 1. No tab is selected at all (app just started or all tabs closed).
/// 2. The selected tab belongs to a different container than the currently
/// selected container this implies the user manually switched
/// containers after selecting a tab, because tab selection automatically
/// syncs the selected container to match the tab's container.
///
/// Condition (2) also implicitly covers the case where the selected
/// container has zero tabs: if the container has no tabs, the selected tab
/// (if any) necessarily belongs to a different container.
@ProviderFor(shouldShowBrowserHome)
final shouldShowBrowserHomeProvider = ShouldShowBrowserHomeProvider._();
/// Whether the browser home screen should be displayed instead of the
/// active tab's content.
///
/// Returns `true` when any of the following hold:
/// 1. No tab is selected at all (app just started or all tabs closed).
/// 2. The selected tab belongs to a different container than the currently
/// selected container this implies the user manually switched
/// containers after selecting a tab, because tab selection automatically
/// syncs the selected container to match the tab's container.
///
/// Condition (2) also implicitly covers the case where the selected
/// container has zero tabs: if the container has no tabs, the selected tab
/// (if any) necessarily belongs to a different container.
final class ShouldShowBrowserHomeProvider
extends $FunctionalProvider<bool, bool, bool>
with $Provider<bool> {
/// Whether the browser home screen should be displayed instead of the
/// active tab's content.
///
/// Returns `true` when any of the following hold:
/// 1. No tab is selected at all (app just started or all tabs closed).
/// 2. The selected tab belongs to a different container than the currently
/// selected container this implies the user manually switched
/// containers after selecting a tab, because tab selection automatically
/// syncs the selected container to match the tab's container.
///
/// Condition (2) also implicitly covers the case where the selected
/// container has zero tabs: if the container has no tabs, the selected tab
/// (if any) necessarily belongs to a different container.
ShouldShowBrowserHomeProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'shouldShowBrowserHomeProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$shouldShowBrowserHomeHash();
@$internal
@override
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
bool create(Ref ref) {
return shouldShowBrowserHome(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$shouldShowBrowserHomeHash() =>
r'644344c9abe06e0273dad584e75a53dc417781ad';
@ProviderFor(selectedContainerTabCount)
final selectedContainerTabCountProvider = SelectedContainerTabCountProvider._();