correctly preview and calculate container colors

This commit is contained in:
Fabian Freund
2026-01-01 09:48:41 +01:00
parent 40267a637f
commit c82bf5b442
7 changed files with 114 additions and 12 deletions
@@ -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/controllers/readerable.dart';
import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.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/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/tor/domain/services/tor_proxy.dart';
import 'package:weblibre/features/user/data/models/general_settings.dart'; import 'package:weblibre/features/user/data/models/general_settings.dart';
import 'package:weblibre/features/user/domain/providers.dart'; import 'package:weblibre/features/user/domain/providers.dart';
@@ -264,7 +265,7 @@ class BrowserTabBar extends HookConsumerWidget {
titleSpacing: 8.0, titleSpacing: 8.0,
backgroundColor: backgroundColor:
(containerColor != null && displayedSheet is! ViewTabsSheet) (containerColor != null && displayedSheet is! ViewTabsSheet)
? containerColor.withValues(alpha: 0.33) ? ContainerColors.forAppBar(containerColor)
: null, : null,
title: title:
(selectedTabId != null && displayedSheet is! ViewTabsSheet) (selectedTabId != null && displayedSheet is! ViewTabsSheet)
@@ -489,7 +490,8 @@ class QuickTabSwitcher extends HookConsumerWidget {
); );
}, },
itemAvatar: (item) => UrlIcon([item.url], iconSize: 16), 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 { onSelected: (item) async {
final animation = chipScrollController.animateTo( final animation = chipScrollController.animateTo(
0, 0,
@@ -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/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/screens/container_sites.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/color_picker_dialog.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/features/user/domain/services/local_authentication.dart';
import 'package:weblibre/presentation/icons/tor_icons.dart'; import 'package:weblibre/presentation/icons/tor_icons.dart';
@@ -168,7 +169,7 @@ class ContainerEditScreen extends HookConsumerWidget {
width: 24, width: 24,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: selectedColor.value, color: ContainerColors.preview(selectedColor.value),
), ),
), ),
), ),
@@ -20,6 +20,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/presentation/widgets/material_color_picker.dart';
import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart';
class ColorPickerDialog extends HookWidget { class ColorPickerDialog extends HookWidget {
final Color initialColor; final Color initialColor;
@@ -47,6 +48,7 @@ class ColorPickerDialog extends HookWidget {
onColorChanged: (value) { onColorChanged: (value) {
selectedColor.value = value; selectedColor.value = value;
}, },
displayAlpha: ContainerColors.defaultAlpha,
), ),
actions: [ actions: [
TextButton( TextButton(
@@ -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/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/container_title.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/tab_drag_container_target.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/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/widgets/selectable_chips.dart'; import 'package:weblibre/presentation/widgets/selectable_chips.dart';
@@ -98,7 +99,7 @@ class ContainerChips extends HookConsumerWidget {
enableDelete: false, enableDelete: false,
itemId: (container) => container.id, itemId: (container) => container.id,
itemBackgroundColor: (container) => itemBackgroundColor: (container) =>
container.color.withValues(alpha: 0.33), ContainerColors.forChip(container.color),
selectedBorderColor: Theme.of(context).colorScheme.primary, selectedBorderColor: Theme.of(context).colorScheme.primary,
itemLabel: (container) => itemLabel: (container) =>
ContainerTitle(container: container), ContainerTitle(container: container),
@@ -21,6 +21,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/data/models/container_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_title.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 { class ContainerListTile extends HookWidget {
final ContainerData container; final ContainerData container;
@@ -41,7 +42,9 @@ class ContainerListTile extends HookWidget {
selectedTileColor: Theme.of(context).colorScheme.primaryContainer, selectedTileColor: Theme.of(context).colorScheme.primaryContainer,
child: ListTile( child: ListTile(
selected: isSelected, selected: isSelected,
leading: CircleAvatar(backgroundColor: container.color), leading: CircleAvatar(
backgroundColor: ContainerColors.preview(container.color),
),
title: ContainerTitle(container: container), title: ContainerTitle(container: container),
onTap: onTap, onTap: onTap,
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
@@ -32,6 +32,7 @@ class MaterialPicker extends StatefulWidget {
this.onPrimaryChanged, this.onPrimaryChanged,
this.enableLabel = false, this.enableLabel = false,
this.portraitOnly = false, this.portraitOnly = false,
this.displayAlpha,
}); });
final Color pickerColor; final Color pickerColor;
@@ -39,6 +40,7 @@ class MaterialPicker extends StatefulWidget {
final ValueChanged<Color>? onPrimaryChanged; final ValueChanged<Color>? onPrimaryChanged;
final bool enableLabel; final bool enableLabel;
final bool portraitOnly; final bool portraitOnly;
final double? displayAlpha;
@override @override
State<StatefulWidget> createState() => _MaterialPickerState(); State<StatefulWidget> createState() => _MaterialPickerState();
@@ -124,6 +126,9 @@ class _MaterialPickerState extends State<MaterialPicker> {
const Padding(padding: EdgeInsets.only(left: 7)), const Padding(padding: EdgeInsets.only(left: 7)),
...colorTypes.map((List<Color> colors) { ...colorTypes.map((List<Color> colors) {
final Color colorType = colors[0]; final Color colorType = colors[0];
final Color displayColorType = widget.displayAlpha != null
? colorType.withValues(alpha: widget.displayAlpha)
: colorType;
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
if (widget.onPrimaryChanged != null) { if (widget.onPrimaryChanged != null) {
@@ -142,7 +147,7 @@ class _MaterialPickerState extends State<MaterialPicker> {
width: 25, width: 25,
height: 25, height: 25,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorType, color: displayColorType,
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: _currentColorType == colors boxShadow: _currentColorType == colors
? [ ? [
@@ -158,7 +163,7 @@ class _MaterialPickerState extends State<MaterialPicker> {
) )
else else
BoxShadow( BoxShadow(
color: colorType, color: displayColorType,
blurRadius: 10, blurRadius: 10,
), ),
] ]
@@ -211,6 +216,9 @@ class _MaterialPickerState extends State<MaterialPicker> {
Map<Color, String> colors, Map<Color, String> colors,
) { ) {
final Color color = colors.keys.first; final Color color = colors.keys.first;
final Color displayColor = widget.displayAlpha != null
? color.withValues(alpha: widget.displayAlpha)
: color;
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
setState(() => _currentShading = color); setState(() => _currentShading = color);
@@ -233,7 +241,7 @@ class _MaterialPickerState extends State<MaterialPicker> {
: (_currentShading == color ? 50 : 30), : (_currentShading == color ? 50 : 30),
height: isPortrait ? 50 : 220, height: isPortrait ? 50 : 220,
decoration: BoxDecoration( decoration: BoxDecoration(
color: color, color: displayColor,
boxShadow: _currentShading == color boxShadow: _currentShading == color
? [ ? [
if ((color == Colors.white) || if ((color == Colors.white) ||
@@ -247,7 +255,7 @@ class _MaterialPickerState extends State<MaterialPicker> {
blurRadius: 10, blurRadius: 10,
) )
else else
BoxShadow(color: color, blurRadius: 10), BoxShadow(color: displayColor, blurRadius: 10),
] ]
: null, : null,
border: border:
@@ -269,7 +277,7 @@ class _MaterialPickerState extends State<MaterialPicker> {
Text( Text(
' ${colors.values.first}', ' ${colors.values.first}',
style: TextStyle( style: TextStyle(
color: useWhiteForeground(color) color: useWhiteForeground(displayColor)
? Colors.white ? Colors.white
: Colors.black, : Colors.black,
), ),
@@ -281,7 +289,7 @@ class _MaterialPickerState extends State<MaterialPicker> {
'#${color.toString().replaceFirst('Color(0xff', '').replaceFirst(')', '').toUpperCase()} ', '#${color.toString().replaceFirst('Color(0xff', '').replaceFirst(')', '').toUpperCase()} ',
style: TextStyle( style: TextStyle(
color: color:
useWhiteForeground(color) useWhiteForeground(displayColor)
? Colors.white ? Colors.white
: Colors.black, : Colors.black,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -306,7 +314,7 @@ class _MaterialPickerState extends State<MaterialPicker> {
child: Text( child: Text(
colors.values.first, colors.values.first,
style: TextStyle( style: TextStyle(
color: useWhiteForeground(color) color: useWhiteForeground(displayColor)
? Colors.white ? Colors.white
: Colors.black, : Colors.black,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}