From c82bf5b44232680851bc597d3dc4a9dde0089379 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Thu, 1 Jan 2026 09:48:41 +0100 Subject: [PATCH] correctly preview and calculate container colors --- .../browser_modules/bottom_app_bar.dart | 6 +- .../presentation/screens/container_edit.dart | 3 +- .../widgets/color_picker_dialog.dart | 2 + .../presentation/widgets/container_chips.dart | 3 +- .../widgets/container_list_tile.dart | 5 +- .../widgets/material_color_picker.dart | 22 +++-- .../features/tabs/utils/container_colors.dart | 85 +++++++++++++++++++ 7 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 app/lib/features/geckoview/features/tabs/utils/container_colors.dart diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart index f3a0e9e8..ef493e85 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart @@ -49,6 +49,7 @@ import 'package:weblibre/features/geckoview/features/history/domain/repositories import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart'; import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.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/tor/domain/services/tor_proxy.dart'; import 'package:weblibre/features/user/data/models/general_settings.dart'; import 'package:weblibre/features/user/domain/providers.dart'; @@ -264,7 +265,7 @@ class BrowserTabBar extends HookConsumerWidget { titleSpacing: 8.0, backgroundColor: (containerColor != null && displayedSheet is! ViewTabsSheet) - ? containerColor.withValues(alpha: 0.33) + ? ContainerColors.forAppBar(containerColor) : null, title: (selectedTabId != null && displayedSheet is! ViewTabsSheet) @@ -489,7 +490,8 @@ class QuickTabSwitcher extends HookConsumerWidget { ); }, itemAvatar: (item) => UrlIcon([item.url], iconSize: 16), - itemBackgroundColor: (item) => item.color?.withValues(alpha: 0.33), + itemBackgroundColor: (item) => + item.color != null ? ContainerColors.forChip(item.color!) : null, onSelected: (item) async { final animation = chipScrollController.animateTo( 0, diff --git a/app/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart b/app/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart index 5becf9f7..e079c611 100644 --- a/app/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart @@ -31,6 +31,7 @@ import 'package:weblibre/features/geckoview/features/tabs/presentation/controlle import 'package:weblibre/features/geckoview/features/tabs/presentation/dialogs/delete_container_dialog.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_sites.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/color_picker_dialog.dart'; +import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart'; import 'package:weblibre/features/user/domain/services/local_authentication.dart'; import 'package:weblibre/presentation/icons/tor_icons.dart'; @@ -168,7 +169,7 @@ class ContainerEditScreen extends HookConsumerWidget { width: 24, decoration: BoxDecoration( shape: BoxShape.circle, - color: selectedColor.value, + color: ContainerColors.preview(selectedColor.value), ), ), ), diff --git a/app/lib/features/geckoview/features/tabs/presentation/widgets/color_picker_dialog.dart b/app/lib/features/geckoview/features/tabs/presentation/widgets/color_picker_dialog.dart index 7ffcb4dd..63fd843e 100644 --- a/app/lib/features/geckoview/features/tabs/presentation/widgets/color_picker_dialog.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/widgets/color_picker_dialog.dart @@ -20,6 +20,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/material_color_picker.dart'; +import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart'; class ColorPickerDialog extends HookWidget { final Color initialColor; @@ -47,6 +48,7 @@ class ColorPickerDialog extends HookWidget { onColorChanged: (value) { selectedColor.value = value; }, + displayAlpha: ContainerColors.defaultAlpha, ), actions: [ TextButton( diff --git a/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart index 7b3e19d8..eb0af0b6 100644 --- a/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart @@ -33,6 +33,7 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart' import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_title.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/tab_drag_container_target.dart'; +import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/presentation/widgets/selectable_chips.dart'; @@ -98,7 +99,7 @@ class ContainerChips extends HookConsumerWidget { enableDelete: false, itemId: (container) => container.id, itemBackgroundColor: (container) => - container.color.withValues(alpha: 0.33), + ContainerColors.forChip(container.color), selectedBorderColor: Theme.of(context).colorScheme.primary, itemLabel: (container) => ContainerTitle(container: container), diff --git a/app/lib/features/geckoview/features/tabs/presentation/widgets/container_list_tile.dart b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_list_tile.dart index fc8e45fd..e4eac7cf 100644 --- a/app/lib/features/geckoview/features/tabs/presentation/widgets/container_list_tile.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_list_tile.dart @@ -21,6 +21,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_title.dart'; +import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart'; class ContainerListTile extends HookWidget { final ContainerData container; @@ -41,7 +42,9 @@ class ContainerListTile extends HookWidget { selectedTileColor: Theme.of(context).colorScheme.primaryContainer, child: ListTile( selected: isSelected, - leading: CircleAvatar(backgroundColor: container.color), + leading: CircleAvatar( + backgroundColor: ContainerColors.preview(container.color), + ), title: ContainerTitle(container: container), onTap: onTap, trailing: const Icon(Icons.chevron_right), diff --git a/app/lib/features/geckoview/features/tabs/presentation/widgets/material_color_picker.dart b/app/lib/features/geckoview/features/tabs/presentation/widgets/material_color_picker.dart index cf51ac81..48dca4ec 100644 --- a/app/lib/features/geckoview/features/tabs/presentation/widgets/material_color_picker.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/widgets/material_color_picker.dart @@ -32,6 +32,7 @@ class MaterialPicker extends StatefulWidget { this.onPrimaryChanged, this.enableLabel = false, this.portraitOnly = false, + this.displayAlpha, }); final Color pickerColor; @@ -39,6 +40,7 @@ class MaterialPicker extends StatefulWidget { final ValueChanged? onPrimaryChanged; final bool enableLabel; final bool portraitOnly; + final double? displayAlpha; @override State createState() => _MaterialPickerState(); @@ -124,6 +126,9 @@ class _MaterialPickerState extends State { const Padding(padding: EdgeInsets.only(left: 7)), ...colorTypes.map((List colors) { final Color colorType = colors[0]; + final Color displayColorType = widget.displayAlpha != null + ? colorType.withValues(alpha: widget.displayAlpha) + : colorType; return GestureDetector( onTap: () { if (widget.onPrimaryChanged != null) { @@ -142,7 +147,7 @@ class _MaterialPickerState extends State { width: 25, height: 25, decoration: BoxDecoration( - color: colorType, + color: displayColorType, shape: BoxShape.circle, boxShadow: _currentColorType == colors ? [ @@ -158,7 +163,7 @@ class _MaterialPickerState extends State { ) else BoxShadow( - color: colorType, + color: displayColorType, blurRadius: 10, ), ] @@ -211,6 +216,9 @@ class _MaterialPickerState extends State { Map colors, ) { final Color color = colors.keys.first; + final Color displayColor = widget.displayAlpha != null + ? color.withValues(alpha: widget.displayAlpha) + : color; return GestureDetector( onTap: () { setState(() => _currentShading = color); @@ -233,7 +241,7 @@ class _MaterialPickerState extends State { : (_currentShading == color ? 50 : 30), height: isPortrait ? 50 : 220, decoration: BoxDecoration( - color: color, + color: displayColor, boxShadow: _currentShading == color ? [ if ((color == Colors.white) || @@ -247,7 +255,7 @@ class _MaterialPickerState extends State { blurRadius: 10, ) else - BoxShadow(color: color, blurRadius: 10), + BoxShadow(color: displayColor, blurRadius: 10), ] : null, border: @@ -269,7 +277,7 @@ class _MaterialPickerState extends State { Text( ' ${colors.values.first}', style: TextStyle( - color: useWhiteForeground(color) + color: useWhiteForeground(displayColor) ? Colors.white : Colors.black, ), @@ -281,7 +289,7 @@ class _MaterialPickerState extends State { '#${color.toString().replaceFirst('Color(0xff', '').replaceFirst(')', '').toUpperCase()} ', style: TextStyle( color: - useWhiteForeground(color) + useWhiteForeground(displayColor) ? Colors.white : Colors.black, fontWeight: FontWeight.bold, @@ -306,7 +314,7 @@ class _MaterialPickerState extends State { child: Text( colors.values.first, style: TextStyle( - color: useWhiteForeground(color) + color: useWhiteForeground(displayColor) ? Colors.white : Colors.black, fontWeight: FontWeight.bold, diff --git a/app/lib/features/geckoview/features/tabs/utils/container_colors.dart b/app/lib/features/geckoview/features/tabs/utils/container_colors.dart new file mode 100644 index 00000000..d2e825ea --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/utils/container_colors.dart @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024-2025 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 . + */ +import 'package:flutter/material.dart'; + +/// Centralized helper for container color display and theming. +/// +/// This class handles the conversion of stored container colors (full opacity) +/// to their display variants (with transparency) for consistent appearance +/// across the application. +class ContainerColors { + ContainerColors._(); + + /// Default alpha value for container color display (33% opacity) + static const double defaultAlpha = 0.33; + + /// Returns the display color for container chips and backgrounds. + /// + /// Applies semi-transparent overlay that works well for backgrounds + /// while maintaining color distinction between containers. + /// + /// [baseColor] The stored container color (typically full opacity) + static Color forChip(Color baseColor) { + return baseColor.withValues(alpha: defaultAlpha); + } + + /// Returns the display color for container app bar backgrounds. + /// + /// Uses the same transparency as chips for visual consistency. + /// + /// [baseColor] The stored container color (typically full opacity) + static Color forAppBar(Color baseColor) { + return baseColor.withValues(alpha: defaultAlpha); + } + + /// Returns the display color with theme-aware blending. + /// + /// Uses Material Design's color blending algorithm for proper color mixing + /// with the surface color, ensuring better visual appearance across themes. + /// + /// [baseColor] The stored container color (typically full opacity) + /// [surface] The surface color to blend with (typically from theme) + static Color forSurface(Color baseColor, Color surface) { + return Color.alphaBlend( + baseColor.withValues(alpha: defaultAlpha), + surface, + ); + } + + /// Returns the preview color showing how the color will appear in the UI. + /// + /// This is useful in color pickers to show users the actual appearance + /// before they confirm their selection. + /// + /// [baseColor] The color being previewed + static Color preview(Color baseColor) { + return baseColor.withValues(alpha: defaultAlpha); + } + + /// Returns the full opacity version of a container color. + /// + /// Useful when you need the original color for comparison or display + /// in contexts where full opacity is needed. + /// + /// [baseColor] The color to ensure has full opacity + static Color fullOpacity(Color baseColor) { + return baseColor.withValues(alpha: 1.0); + } +}