turn empty tab screen into home screen
This commit is contained in:
@@ -286,6 +286,20 @@ class TabRepository extends _$TabRepository {
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> resumeLatestTab() async {
|
||||
final latestTab = await ref
|
||||
.read(tabDatabaseProvider)
|
||||
.tabDao
|
||||
.getTabsFifo(limit: 1)
|
||||
.getSingleOrNull();
|
||||
|
||||
if (!ref.mounted || latestTab == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return selectTab(latestTab.id);
|
||||
}
|
||||
|
||||
Future<bool> selectPreviousTab(
|
||||
String tabId, {
|
||||
String? containerId,
|
||||
|
||||
+101
-44
@@ -1,28 +1,40 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
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/quotes/data/database/definitions.drift.dart';
|
||||
import 'package:weblibre/features/quotes/domain/providers.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
|
||||
class TabsEmptyPlaceholder extends ConsumerWidget {
|
||||
const TabsEmptyPlaceholder();
|
||||
class BrowserHome extends ConsumerWidget {
|
||||
const BrowserHome({super.key});
|
||||
|
||||
static const _brandPurple = Color(0xFF9C83F8);
|
||||
static const _brandYellow = Color(0xFFFBDC6B);
|
||||
static const _brandGrey = Color(0xFFA7A7A7);
|
||||
|
||||
static const _auraPurple = Color(0xFF2C2543);
|
||||
static const _auraGold = Color(0xFF3C3827);
|
||||
static const _auraShadow = Color(0xFF222224);
|
||||
static const _auraShadowHighlight = Color(0xFF2D2D31);
|
||||
static const _auraTint = Color(0xFF000000);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final settings = ref.watch(generalSettingsWithDefaultsProvider);
|
||||
final quoteAsync = ref.watch(randomQuoteProvider);
|
||||
final hasTabs = ref.watch(
|
||||
tabListProvider.select((tabs) => tabs.value.isNotEmpty),
|
||||
);
|
||||
final viewportToolbarInsets = ref.watch(
|
||||
browserViewportToolbarInsetsControllerProvider,
|
||||
);
|
||||
@@ -38,6 +50,14 @@ class TabsEmptyPlaceholder extends ConsumerWidget {
|
||||
).push(context);
|
||||
}
|
||||
|
||||
Future<void> viewTabs() {
|
||||
return const TabViewRoute().push(context);
|
||||
}
|
||||
|
||||
Future<void> resumeLatestTab() async {
|
||||
await ref.read(tabRepositoryProvider.notifier).resumeLatestTab();
|
||||
}
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
@@ -45,12 +65,15 @@ class TabsEmptyPlaceholder extends ConsumerWidget {
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Color.alphaBlend(
|
||||
_brandPurple.withValues(alpha: 0.06),
|
||||
_auraPurple.withValues(alpha: 0.38),
|
||||
colorScheme.surfaceContainerLowest,
|
||||
),
|
||||
colorScheme.surface,
|
||||
Color.alphaBlend(
|
||||
_brandYellow.withValues(alpha: 0.05),
|
||||
_auraShadow.withValues(alpha: 0.72),
|
||||
colorScheme.surface,
|
||||
),
|
||||
Color.alphaBlend(
|
||||
_auraGold.withValues(alpha: 0.34),
|
||||
colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
],
|
||||
@@ -59,25 +82,33 @@ class TabsEmptyPlaceholder extends ConsumerWidget {
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Align(
|
||||
alignment: const Alignment(-0.9, -0.95),
|
||||
const Positioned(
|
||||
top: -70,
|
||||
left: -120,
|
||||
child: _BackdropOrb(width: 400, height: 400, color: _auraPurple),
|
||||
),
|
||||
const Positioned(
|
||||
top: 220,
|
||||
right: -150,
|
||||
child: _BackdropOrb(width: 340, height: 340, color: _auraGold),
|
||||
),
|
||||
const Positioned(
|
||||
bottom: 18,
|
||||
left: -8,
|
||||
child: _BackdropOrb(
|
||||
size: 220,
|
||||
color: _brandPurple.withValues(alpha: 0.14),
|
||||
width: 320,
|
||||
height: 320,
|
||||
color: _auraShadowHighlight,
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: const Alignment(1.0, -0.35),
|
||||
child: _BackdropOrb(
|
||||
size: 180,
|
||||
color: _brandYellow.withValues(alpha: 0.14),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: const Alignment(-0.75, 0.9),
|
||||
child: _BackdropOrb(
|
||||
size: 160,
|
||||
color: _brandGrey.withValues(alpha: 0.12),
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 72, sigmaY: 72),
|
||||
child: ColoredBox(color: _auraTint.withValues(alpha: 0.12)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
LayoutBuilder(
|
||||
@@ -148,23 +179,25 @@ class TabsEmptyPlaceholder extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'WebLibre is ready',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
if (!hasTabs) ...[
|
||||
Text(
|
||||
'WebLibre is ready',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'A browser that respects you. Open a tab and experience the web, libre.',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
height: 1.45,
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'A browser that respects you. Open a tab and experience the web, libre.',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
height: 1.45,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
const SizedBox(height: 28),
|
||||
],
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
@@ -238,10 +271,29 @@ class TabsEmptyPlaceholder extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: openNewTab,
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: const Text('Open new tab'),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
if (hasTabs)
|
||||
OutlinedButton.icon(
|
||||
onPressed: viewTabs,
|
||||
icon: const Icon(Icons.tab_rounded),
|
||||
label: const Text('View tabs'),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: openNewTab,
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: const Text('New tab'),
|
||||
),
|
||||
if (hasTabs)
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: resumeLatestTab,
|
||||
icon: const Icon(Icons.history_rounded),
|
||||
label: const Text('Resume last tab'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -310,17 +362,22 @@ class _QuoteBlock extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _BackdropOrb extends StatelessWidget {
|
||||
final double size;
|
||||
final double width;
|
||||
final double height;
|
||||
final Color color;
|
||||
|
||||
const _BackdropOrb({required this.size, required this.color});
|
||||
const _BackdropOrb({
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
||||
),
|
||||
);
|
||||
+54
-2
@@ -33,6 +33,7 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/provid
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/toolbar_button.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
||||
|
||||
class CompactAppBarTitle extends ConsumerWidget {
|
||||
@@ -41,13 +42,21 @@ class CompactAppBarTitle extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabState = ref.watch(selectedTabStateProvider);
|
||||
final selectedTabType = ref.watch(selectedTabTypeProvider);
|
||||
final settings = ref.watch(generalSettingsWithDefaultsProvider);
|
||||
final isTabTuneledAsync = ref.watch(isTabTunneledProvider(tabState?.id));
|
||||
final showSiteSettingsBadge = ref.watch(
|
||||
showSiteSettingsBadgeProvider.select((value) => value.value == true),
|
||||
);
|
||||
|
||||
if (tabState == null) {
|
||||
return const SizedBox.shrink();
|
||||
return _EmptyAppBarAddressField(
|
||||
onTap: () async {
|
||||
await SearchRoute(
|
||||
tabType: selectedTabType ?? settings.effectiveDefaultCreateTabType,
|
||||
).push(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return CompactAppBarTitleView(
|
||||
@@ -178,13 +187,21 @@ class AppBarTitle extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabState = ref.watch(selectedTabStateProvider);
|
||||
final selectedTabType = ref.watch(selectedTabTypeProvider);
|
||||
final settings = ref.watch(generalSettingsWithDefaultsProvider);
|
||||
final isTabTuneledAsync = ref.watch(isTabTunneledProvider(tabState?.id));
|
||||
final showSiteSettingsBadge = ref.watch(
|
||||
showSiteSettingsBadgeProvider.select((value) => value.value == true),
|
||||
);
|
||||
|
||||
if (tabState == null) {
|
||||
return const SizedBox.shrink();
|
||||
return _EmptyAppBarAddressField(
|
||||
onTap: () async {
|
||||
await SearchRoute(
|
||||
tabType: selectedTabType ?? settings.effectiveDefaultCreateTabType,
|
||||
).push(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return AppBarTitleView(
|
||||
@@ -360,6 +377,41 @@ class _SecurityStatusIcon extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyAppBarAddressField extends StatelessWidget {
|
||||
const _EmptyAppBarAddressField({required this.onTap});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Search or enter URL',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _searchTextForTab(TabState tabState) {
|
||||
final searchText = tabState.url.scheme == 'about'
|
||||
? ''
|
||||
|
||||
+1
-3
@@ -114,7 +114,6 @@ class BrowserBottomAppBar extends StatelessWidget {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Material(
|
||||
elevation: 0,
|
||||
shadowColor: Colors.transparent,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
color: colorScheme.surfaceContainer,
|
||||
@@ -211,8 +210,7 @@ class BrowserTabBar extends HookConsumerWidget {
|
||||
|
||||
final dragStartPosition = useRef(Offset.zero);
|
||||
|
||||
final showTabTitle =
|
||||
selectedTabId != null && displayedSheet is! ViewTabsSheet;
|
||||
final showTabTitle = displayedSheet is! ViewTabsSheet;
|
||||
|
||||
final backgroundColor =
|
||||
(settings.showContainerUi &&
|
||||
|
||||
+2
-2
@@ -45,7 +45,7 @@ import 'package:weblibre/features/geckoview/features/browser/domain/providers/li
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_data.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/services/engine_settings_replication.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/services/proxy_settings_replication.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tabs_empty.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_home.dart';
|
||||
import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart';
|
||||
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart';
|
||||
import 'package:weblibre/features/geckoview/features/pwa/domain/providers.dart';
|
||||
@@ -291,7 +291,7 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!hasTab) const Positioned.fill(child: TabsEmptyPlaceholder()),
|
||||
if (!hasTab) const Positioned.fill(child: BrowserHome()),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
+32
-3
@@ -30,6 +30,9 @@ import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mozilla.components.browser.storage.sync.GlobalPlacesDependencyProvider
|
||||
import mozilla.components.browser.session.storage.RecoverableBrowserState
|
||||
import mozilla.components.browser.state.action.RestoreCompleteAction
|
||||
import mozilla.components.browser.state.action.TabListAction
|
||||
import mozilla.components.browser.state.action.CustomTabListAction
|
||||
import mozilla.components.browser.state.selector.findCustomTab
|
||||
import mozilla.components.concept.engine.selection.SelectionActionDelegate
|
||||
@@ -100,9 +103,31 @@ object GlobalComponents {
|
||||
}
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
private fun restoreBrowserState(newComponents: Components) =
|
||||
private fun restoreBrowserState(
|
||||
newComponents: Components,
|
||||
restoreTabsWithoutResumingSelection: Boolean,
|
||||
) =
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
newComponents.useCases.tabsUseCases.restore(newComponents.core.sessionStorage)
|
||||
if (restoreTabsWithoutResumingSelection) {
|
||||
val restoredState = newComponents.core.sessionStorage.restore { true }
|
||||
|
||||
if (restoredState != null) {
|
||||
newComponents.useCases.tabsUseCases.restore(
|
||||
state = RecoverableBrowserState(
|
||||
tabs = restoredState.tabs,
|
||||
// Intentionally do not resume the previously selected tab during the
|
||||
// first full setup. We restore the tab list, but let startup open in
|
||||
// its neutral/default state instead of jumping back into prior content.
|
||||
selectedTabId = null,
|
||||
),
|
||||
restoreLocation = TabListAction.RestoreAction.RestoreLocation.BEGINNING,
|
||||
)
|
||||
}
|
||||
|
||||
newComponents.core.store.dispatch(RestoreCompleteAction)
|
||||
} else {
|
||||
newComponents.useCases.tabsUseCases.restore(newComponents.core.sessionStorage)
|
||||
}
|
||||
|
||||
newComponents.core.sessionStorage.autoSave(newComponents.core.store)
|
||||
.periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS)
|
||||
@@ -198,7 +223,11 @@ object GlobalComponents {
|
||||
}
|
||||
}
|
||||
|
||||
val restoreJob = restoreBrowserState(newComponents)
|
||||
val isFirstFullSetup = previousMode != ComponentsMode.FULL
|
||||
val restoreJob = restoreBrowserState(
|
||||
newComponents,
|
||||
restoreTabsWithoutResumingSelection = isFirstFullSetup,
|
||||
)
|
||||
if (previousCustomTabs.isNotEmpty()) {
|
||||
restoreJob.invokeOnCompletion {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
|
||||
Reference in New Issue
Block a user