initial
This commit is contained in:
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$tabRepositoryHash() => r'4dd31901b4d02ca075d3dcec3822f5a2832d164c';
|
||||
String _$tabRepositoryHash() => r'22fdd97ad7f235590926915169c9e7230de94bdb';
|
||||
|
||||
abstract class _$TabRepository extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'browser_viewport_toolbar_insets.g.dart';
|
||||
|
||||
@CopyWith()
|
||||
class BrowserViewportToolbarInsetsState with FastEquatable {
|
||||
final int dynamicToolbarMaxHeightPx;
|
||||
final int verticalClippingPx;
|
||||
|
||||
BrowserViewportToolbarInsetsState({
|
||||
this.dynamicToolbarMaxHeightPx = 0,
|
||||
this.verticalClippingPx = 0,
|
||||
});
|
||||
|
||||
static final zero = BrowserViewportToolbarInsetsState();
|
||||
|
||||
int get effectiveBottomInsetPx =>
|
||||
math.max(0, dynamicToolbarMaxHeightPx + verticalClippingPx);
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [
|
||||
dynamicToolbarMaxHeightPx,
|
||||
verticalClippingPx,
|
||||
];
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class BrowserViewportToolbarInsetsController
|
||||
extends _$BrowserViewportToolbarInsetsController {
|
||||
@override
|
||||
BrowserViewportToolbarInsetsState build() {
|
||||
return BrowserViewportToolbarInsetsState.zero;
|
||||
}
|
||||
|
||||
void setDynamicToolbarMaxHeight(int heightPx) {
|
||||
if (state.dynamicToolbarMaxHeightPx == heightPx) {
|
||||
return;
|
||||
}
|
||||
|
||||
state = state.copyWith(dynamicToolbarMaxHeightPx: heightPx);
|
||||
}
|
||||
|
||||
void setVerticalClipping(int clippingPx) {
|
||||
if (state.verticalClippingPx == clippingPx) {
|
||||
return;
|
||||
}
|
||||
|
||||
state = state.copyWith(verticalClippingPx: clippingPx);
|
||||
}
|
||||
|
||||
void reset() {
|
||||
if (state == BrowserViewportToolbarInsetsState.zero) {
|
||||
return;
|
||||
}
|
||||
|
||||
state = BrowserViewportToolbarInsetsState.zero;
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'browser_viewport_toolbar_insets.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class _$BrowserViewportToolbarInsetsStateCWProxy {
|
||||
BrowserViewportToolbarInsetsState dynamicToolbarMaxHeightPx(
|
||||
int dynamicToolbarMaxHeightPx,
|
||||
);
|
||||
|
||||
BrowserViewportToolbarInsetsState verticalClippingPx(int verticalClippingPx);
|
||||
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `BrowserViewportToolbarInsetsState(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// BrowserViewportToolbarInsetsState(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
BrowserViewportToolbarInsetsState call({
|
||||
int dynamicToolbarMaxHeightPx,
|
||||
int verticalClippingPx,
|
||||
});
|
||||
}
|
||||
|
||||
/// Callable proxy for `copyWith` functionality.
|
||||
/// Use as `instanceOfBrowserViewportToolbarInsetsState.copyWith(...)` or call `instanceOfBrowserViewportToolbarInsetsState.copyWith.fieldName(value)` for a single field.
|
||||
class _$BrowserViewportToolbarInsetsStateCWProxyImpl
|
||||
implements _$BrowserViewportToolbarInsetsStateCWProxy {
|
||||
const _$BrowserViewportToolbarInsetsStateCWProxyImpl(this._value);
|
||||
|
||||
final BrowserViewportToolbarInsetsState _value;
|
||||
|
||||
@override
|
||||
BrowserViewportToolbarInsetsState dynamicToolbarMaxHeightPx(
|
||||
int dynamicToolbarMaxHeightPx,
|
||||
) => call(dynamicToolbarMaxHeightPx: dynamicToolbarMaxHeightPx);
|
||||
|
||||
@override
|
||||
BrowserViewportToolbarInsetsState verticalClippingPx(
|
||||
int verticalClippingPx,
|
||||
) => call(verticalClippingPx: verticalClippingPx);
|
||||
|
||||
@override
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `BrowserViewportToolbarInsetsState(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// BrowserViewportToolbarInsetsState(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
BrowserViewportToolbarInsetsState call({
|
||||
Object? dynamicToolbarMaxHeightPx = const $CopyWithPlaceholder(),
|
||||
Object? verticalClippingPx = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return BrowserViewportToolbarInsetsState(
|
||||
dynamicToolbarMaxHeightPx:
|
||||
dynamicToolbarMaxHeightPx == const $CopyWithPlaceholder() ||
|
||||
dynamicToolbarMaxHeightPx == null
|
||||
? _value.dynamicToolbarMaxHeightPx
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: dynamicToolbarMaxHeightPx as int,
|
||||
verticalClippingPx:
|
||||
verticalClippingPx == const $CopyWithPlaceholder() ||
|
||||
verticalClippingPx == null
|
||||
? _value.verticalClippingPx
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: verticalClippingPx as int,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension $BrowserViewportToolbarInsetsStateCopyWith
|
||||
on BrowserViewportToolbarInsetsState {
|
||||
/// Returns a callable class used to build a new instance with modified fields.
|
||||
/// Example: `instanceOfBrowserViewportToolbarInsetsState.copyWith(...)` or `instanceOfBrowserViewportToolbarInsetsState.copyWith.fieldName(...)`.
|
||||
// ignore: library_private_types_in_public_api
|
||||
_$BrowserViewportToolbarInsetsStateCWProxy get copyWith =>
|
||||
_$BrowserViewportToolbarInsetsStateCWProxyImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(BrowserViewportToolbarInsetsController)
|
||||
final browserViewportToolbarInsetsControllerProvider =
|
||||
BrowserViewportToolbarInsetsControllerProvider._();
|
||||
|
||||
final class BrowserViewportToolbarInsetsControllerProvider
|
||||
extends
|
||||
$NotifierProvider<
|
||||
BrowserViewportToolbarInsetsController,
|
||||
BrowserViewportToolbarInsetsState
|
||||
> {
|
||||
BrowserViewportToolbarInsetsControllerProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'browserViewportToolbarInsetsControllerProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() =>
|
||||
_$browserViewportToolbarInsetsControllerHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
BrowserViewportToolbarInsetsController create() =>
|
||||
BrowserViewportToolbarInsetsController();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(BrowserViewportToolbarInsetsState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<BrowserViewportToolbarInsetsState>(
|
||||
value,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$browserViewportToolbarInsetsControllerHash() =>
|
||||
r'ae6444dc84383bef7d957e3bbd1c48d9ebba15ec';
|
||||
|
||||
abstract class _$BrowserViewportToolbarInsetsController
|
||||
extends $Notifier<BrowserViewportToolbarInsetsState> {
|
||||
BrowserViewportToolbarInsetsState build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref =
|
||||
this.ref
|
||||
as $Ref<
|
||||
BrowserViewportToolbarInsetsState,
|
||||
BrowserViewportToolbarInsetsState
|
||||
>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<
|
||||
BrowserViewportToolbarInsetsState,
|
||||
BrowserViewportToolbarInsetsState
|
||||
>,
|
||||
BrowserViewportToolbarInsetsState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import 'package:weblibre/features/geckoview/features/browser/domain/providers.da
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/keep_tab_dialog.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/providers/browser_viewport_toolbar_insets.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_fab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart';
|
||||
@@ -428,9 +429,26 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
|
||||
final lastToolbarMaxHeightPx = useRef<int?>(null);
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref
|
||||
.read(browserViewportToolbarInsetsControllerProvider.notifier)
|
||||
.reset();
|
||||
});
|
||||
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
useEffect(() {
|
||||
if (lastToolbarMaxHeightPx.value != effectiveToolbarHeightPx) {
|
||||
lastToolbarMaxHeightPx.value = effectiveToolbarHeightPx;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref
|
||||
.read(browserViewportToolbarInsetsControllerProvider.notifier)
|
||||
.setDynamicToolbarMaxHeight(effectiveToolbarHeightPx);
|
||||
});
|
||||
|
||||
unawaited(
|
||||
viewportService.setDynamicToolbarMaxHeight(effectiveToolbarHeightPx),
|
||||
);
|
||||
@@ -465,6 +483,13 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
|
||||
if (targetClippingPx != lastClippingPx.value) {
|
||||
lastClippingPx.value = targetClippingPx;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref
|
||||
.read(browserViewportToolbarInsetsControllerProvider.notifier)
|
||||
.setVerticalClipping(targetClippingPx);
|
||||
});
|
||||
|
||||
unawaited(viewportService.setVerticalClipping(targetClippingPx));
|
||||
}
|
||||
}
|
||||
|
||||
+2
-8
@@ -45,6 +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/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';
|
||||
@@ -108,8 +109,6 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
useOnInitialization(() async {
|
||||
await ref
|
||||
.read(generalSettingsRepositoryProvider.notifier)
|
||||
@@ -292,12 +291,7 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!hasTab)
|
||||
Positioned.fill(
|
||||
child: SizedBox.expand(
|
||||
child: Container(color: colorScheme.surfaceContainerHighest),
|
||||
),
|
||||
),
|
||||
if (!hasTab) const Positioned.fill(child: TabsEmptyPlaceholder()),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
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/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();
|
||||
|
||||
static const _brandPurple = Color(0xFF9C83F8);
|
||||
static const _brandYellow = Color(0xFFFBDC6B);
|
||||
static const _brandGrey = Color(0xFFA7A7A7);
|
||||
|
||||
@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 viewportToolbarInsets = ref.watch(
|
||||
browserViewportToolbarInsetsControllerProvider,
|
||||
);
|
||||
|
||||
final pixelRatio = MediaQuery.devicePixelRatioOf(context);
|
||||
|
||||
final bottomViewportInset =
|
||||
viewportToolbarInsets.effectiveBottomInsetPx / pixelRatio;
|
||||
|
||||
Future<void> openNewTab() {
|
||||
return SearchRoute(
|
||||
tabType: settings.effectiveDefaultCreateTabType,
|
||||
).push(context);
|
||||
}
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Color.alphaBlend(
|
||||
_brandPurple.withValues(alpha: 0.06),
|
||||
colorScheme.surfaceContainerLowest,
|
||||
),
|
||||
colorScheme.surface,
|
||||
Color.alphaBlend(
|
||||
_brandYellow.withValues(alpha: 0.05),
|
||||
colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Align(
|
||||
alignment: const Alignment(-0.9, -0.95),
|
||||
child: _BackdropOrb(
|
||||
size: 220,
|
||||
color: _brandPurple.withValues(alpha: 0.14),
|
||||
),
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
24,
|
||||
32,
|
||||
24,
|
||||
32 + bottomViewportInset,
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: math.max(
|
||||
0,
|
||||
constraints.maxHeight - 64 - bottomViewportInset,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: Column(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
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: 28),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainer.withValues(
|
||||
alpha: 0.9,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
border: Border.all(
|
||||
color: Color.alphaBlend(
|
||||
_brandGrey.withValues(alpha: 0.18),
|
||||
colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: theme
|
||||
.colorScheme
|
||||
.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
MdiIcons.formatQuoteOpen,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'A thought for the road',
|
||||
style: theme.textTheme.titleMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
tooltip: 'Refresh quote',
|
||||
onPressed: () {
|
||||
ref.invalidate(randomQuoteProvider);
|
||||
},
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
switch (quoteAsync) {
|
||||
AsyncData(:final value) => _QuoteBlock(
|
||||
quote: value,
|
||||
),
|
||||
AsyncError() => Text(
|
||||
'Open a new tab and make this space your own.',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
_ => const LinearProgressIndicator(
|
||||
minHeight: 3,
|
||||
),
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: openNewTab,
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: const Text('Open new tab'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuoteBlock extends StatelessWidget {
|
||||
final Quote? quote;
|
||||
|
||||
const _QuoteBlock({required this.quote});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
if (quote == null) {
|
||||
return Text(
|
||||
'Open a new tab and make this space your own.',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
height: 1.5,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'"${quote!.quote}"',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
height: 1.55,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'- ${quote!.author}',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (quote!.source case final String source when source.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
source,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BackdropOrb extends StatelessWidget {
|
||||
final double size;
|
||||
final Color color;
|
||||
|
||||
const _BackdropOrb({required this.size, required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user