make home screen consider containers as well
This commit is contained in:
@@ -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,
|
||||
|
||||
+151
-44
@@ -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;
|
||||
|
||||
|
||||
+3
-5
@@ -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);
|
||||
|
||||
+84
-1
@@ -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._();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user