improve container color selector
This commit is contained in:
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$tabRepositoryHash() => r'7d41038ac5a20ecf6de0645b99231fb8b2f6963a';
|
||||
String _$tabRepositoryHash() => r'd7b67460388c264f477325266a3299fe6df2df07';
|
||||
|
||||
abstract class _$TabRepository extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
+5
-1
@@ -248,7 +248,11 @@ class _ContainerHeader extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final containerColor = container.color;
|
||||
final containerPalette = ContainerColors.palette(context, containerColor);
|
||||
final containerPalette = ContainerColors.palette(
|
||||
context,
|
||||
containerColor,
|
||||
useCustomColor: container.metadata.useCustomColor,
|
||||
);
|
||||
|
||||
return Container(
|
||||
width: 112,
|
||||
|
||||
+28
-4
@@ -40,9 +40,14 @@ import 'package:weblibre/features/web_search/domain/controllers/sandbox_capture_
|
||||
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
||||
|
||||
class CompactAppBarTitle extends ConsumerWidget {
|
||||
const CompactAppBarTitle({super.key, this.containerColor});
|
||||
const CompactAppBarTitle({
|
||||
super.key,
|
||||
this.containerColor,
|
||||
this.useCustomColor = false,
|
||||
});
|
||||
|
||||
final Color? containerColor;
|
||||
final bool useCustomColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -77,6 +82,7 @@ class CompactAppBarTitle extends ConsumerWidget {
|
||||
siteSettingsBadgeState: siteSettingsBadgeState,
|
||||
longPressUrlCopy: settings.tabBarLongPressUrlCopy,
|
||||
containerColor: containerColor,
|
||||
useCustomColor: useCustomColor,
|
||||
sandboxSourceUri: sandboxSourceUri,
|
||||
onSiteSettingsTap: () {
|
||||
ref
|
||||
@@ -105,6 +111,7 @@ class CompactAppBarTitleView extends StatelessWidget {
|
||||
this.tabIcon,
|
||||
this.longPressUrlCopy = true,
|
||||
this.containerColor,
|
||||
this.useCustomColor = false,
|
||||
this.sandboxSourceUri,
|
||||
});
|
||||
|
||||
@@ -116,6 +123,7 @@ class CompactAppBarTitleView extends StatelessWidget {
|
||||
final Widget? tabIcon;
|
||||
final bool longPressUrlCopy;
|
||||
final Color? containerColor;
|
||||
final bool useCustomColor;
|
||||
final Uri? sandboxSourceUri;
|
||||
|
||||
@override
|
||||
@@ -124,7 +132,11 @@ class CompactAppBarTitleView extends StatelessWidget {
|
||||
final appColors = AppColors.of(context);
|
||||
final containerColor = this.containerColor;
|
||||
final containerPalette = containerColor != null
|
||||
? ContainerColors.palette(context, containerColor)
|
||||
? ContainerColors.palette(
|
||||
context,
|
||||
containerColor,
|
||||
useCustomColor: useCustomColor,
|
||||
)
|
||||
: null;
|
||||
|
||||
return Row(
|
||||
@@ -241,9 +253,14 @@ class CompactAppBarTitleView extends StatelessWidget {
|
||||
}
|
||||
|
||||
class AppBarTitle extends ConsumerWidget {
|
||||
const AppBarTitle({super.key, this.containerColor});
|
||||
const AppBarTitle({
|
||||
super.key,
|
||||
this.containerColor,
|
||||
this.useCustomColor = false,
|
||||
});
|
||||
|
||||
final Color? containerColor;
|
||||
final bool useCustomColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -278,6 +295,7 @@ class AppBarTitle extends ConsumerWidget {
|
||||
siteSettingsBadgeState: siteSettingsBadgeState,
|
||||
longPressUrlCopy: settings.tabBarLongPressUrlCopy,
|
||||
containerColor: containerColor,
|
||||
useCustomColor: useCustomColor,
|
||||
sandboxSourceUri: sandboxSourceUri,
|
||||
onSiteSettingsTap: () {
|
||||
ref
|
||||
@@ -306,6 +324,7 @@ class AppBarTitleView extends StatelessWidget {
|
||||
required this.longPressUrlCopy,
|
||||
this.tabIcon,
|
||||
this.containerColor,
|
||||
this.useCustomColor = false,
|
||||
this.sandboxSourceUri,
|
||||
});
|
||||
|
||||
@@ -317,6 +336,7 @@ class AppBarTitleView extends StatelessWidget {
|
||||
final Widget? tabIcon;
|
||||
final bool longPressUrlCopy;
|
||||
final Color? containerColor;
|
||||
final bool useCustomColor;
|
||||
final Uri? sandboxSourceUri;
|
||||
|
||||
@override
|
||||
@@ -325,7 +345,11 @@ class AppBarTitleView extends StatelessWidget {
|
||||
final appColors = AppColors.of(context);
|
||||
final containerColor = this.containerColor;
|
||||
final containerPalette = containerColor != null
|
||||
? ContainerColors.palette(context, containerColor)
|
||||
? ContainerColors.palette(
|
||||
context,
|
||||
containerColor,
|
||||
useCustomColor: useCustomColor,
|
||||
)
|
||||
: null;
|
||||
|
||||
return Row(
|
||||
|
||||
+42
-6
@@ -236,6 +236,11 @@ class BrowserTabBar extends HookConsumerWidget {
|
||||
selectedTabId,
|
||||
).select((data) => data.value?.color),
|
||||
);
|
||||
final containerUseCustomColor = ref.watch(
|
||||
watchTabContainerDataProvider(
|
||||
selectedTabId,
|
||||
).select((data) => data.value?.metadata.useCustomColor ?? false),
|
||||
);
|
||||
|
||||
final quickTabSwitcherMode = settings.effectiveUiQuickTabSwitcherMode();
|
||||
|
||||
@@ -251,8 +256,14 @@ class BrowserTabBar extends HookConsumerWidget {
|
||||
displayedSheet is! ViewTabsSheet)
|
||||
? containerColor
|
||||
: null;
|
||||
final effectiveUseCustomColor =
|
||||
effectiveContainerColor != null && containerUseCustomColor;
|
||||
final effectiveContainerPalette = effectiveContainerColor != null
|
||||
? ContainerColors.palette(context, effectiveContainerColor)
|
||||
? ContainerColors.palette(
|
||||
context,
|
||||
effectiveContainerColor,
|
||||
useCustomColor: effectiveUseCustomColor,
|
||||
)
|
||||
: null;
|
||||
|
||||
return BrowserTabBarView(
|
||||
@@ -264,8 +275,14 @@ class BrowserTabBar extends HookConsumerWidget {
|
||||
backgroundColor: effectiveContainerPalette?.surfaceColor,
|
||||
title: showTabTitle
|
||||
? settings.tabBarLayout == TabBarLayout.compact
|
||||
? CompactAppBarTitle(containerColor: effectiveContainerColor)
|
||||
: AppBarTitle(containerColor: effectiveContainerColor)
|
||||
? CompactAppBarTitle(
|
||||
containerColor: effectiveContainerColor,
|
||||
useCustomColor: effectiveUseCustomColor,
|
||||
)
|
||||
: AppBarTitle(
|
||||
containerColor: effectiveContainerColor,
|
||||
useCustomColor: effectiveUseCustomColor,
|
||||
)
|
||||
: null,
|
||||
actions: [
|
||||
const PinnedAddonBar(),
|
||||
@@ -457,6 +474,7 @@ class BrowserTabBarView extends StatelessWidget {
|
||||
|
||||
class QuickTabSwitcherItem with FastEquatable {
|
||||
final Color? color;
|
||||
final bool useCustomColor;
|
||||
final String id;
|
||||
final bool isActive;
|
||||
final TabMode tabMode;
|
||||
@@ -478,6 +496,7 @@ class QuickTabSwitcherItem with FastEquatable {
|
||||
required this.title,
|
||||
required this.url,
|
||||
required this.avatar,
|
||||
this.useCustomColor = false,
|
||||
this.isSandbox = false,
|
||||
this.depth = 0,
|
||||
});
|
||||
@@ -485,6 +504,7 @@ class QuickTabSwitcherItem with FastEquatable {
|
||||
@override
|
||||
List<Object?> get hashParameters => [
|
||||
color,
|
||||
useCustomColor,
|
||||
id,
|
||||
isActive,
|
||||
tabMode,
|
||||
@@ -577,6 +597,7 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
||||
: state.$1.titleOrAuthority;
|
||||
return QuickTabSwitcherItem(
|
||||
color: state.$2?.color,
|
||||
useCustomColor: state.$2?.metadata.useCustomColor ?? false,
|
||||
id: state.$1.id,
|
||||
isActive: state.$1.id == selectedTabId,
|
||||
title: displayTitle,
|
||||
@@ -910,16 +931,26 @@ class QuickTabSwitcherView extends StatelessWidget {
|
||||
final color? when isSelected => ContainerColors.palette(
|
||||
context,
|
||||
color,
|
||||
useCustomColor: item.useCustomColor,
|
||||
).selectedBackgroundColor,
|
||||
final color? => ContainerColors.palette(context, color).backgroundColor,
|
||||
final color? => ContainerColors.palette(
|
||||
context,
|
||||
color,
|
||||
useCustomColor: item.useCustomColor,
|
||||
).backgroundColor,
|
||||
null => null,
|
||||
},
|
||||
side: (item, isSelected) => switch (item.color) {
|
||||
final color? when isSelected => ContainerColors.palette(
|
||||
context,
|
||||
color,
|
||||
useCustomColor: item.useCustomColor,
|
||||
).selectedBorderSide,
|
||||
final color? => ContainerColors.palette(context, color).borderSide,
|
||||
final color? => ContainerColors.palette(
|
||||
context,
|
||||
color,
|
||||
useCustomColor: item.useCustomColor,
|
||||
).borderSide,
|
||||
null => null,
|
||||
},
|
||||
labelPadding: (item) =>
|
||||
@@ -1015,8 +1046,13 @@ class QuickTabSwitcherView extends StatelessWidget {
|
||||
? ContainerColors.palette(
|
||||
context,
|
||||
color,
|
||||
useCustomColor: item.useCustomColor,
|
||||
).selectedForegroundColor
|
||||
: ContainerColors.palette(context, color).foregroundColor,
|
||||
: ContainerColors.palette(
|
||||
context,
|
||||
color,
|
||||
useCustomColor: item.useCustomColor,
|
||||
).foregroundColor,
|
||||
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
|
||||
),
|
||||
child: row,
|
||||
|
||||
+2
@@ -67,6 +67,8 @@ class RecentTabsSection extends ConsumerWidget {
|
||||
iconSize: UrlListTile.iconSize,
|
||||
),
|
||||
containerColor: containerData?.color,
|
||||
useCustomColor:
|
||||
containerData?.metadata.useCustomColor ?? false,
|
||||
onTap: () => onTabSelected(tabState.id),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -53,6 +53,12 @@ class ContainerMetadata with FastEquatable {
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool bypassGlobalProxy;
|
||||
|
||||
// When true, ContainerData.color is used directly as primaryContainer
|
||||
// instead of being fed through ColorScheme.fromSeed. Lets power users pick
|
||||
// any color (including dark/black) at the cost of M3 harmonization.
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool useCustomColor;
|
||||
|
||||
final List<Uri>? assignedSites;
|
||||
|
||||
ContainerMetadata({
|
||||
@@ -62,6 +68,7 @@ class ContainerMetadata with FastEquatable {
|
||||
required this.clearDataOnExit,
|
||||
required this.excludeFromIndex,
|
||||
required this.bypassGlobalProxy,
|
||||
required this.useCustomColor,
|
||||
required this.assignedSites,
|
||||
});
|
||||
|
||||
@@ -72,6 +79,7 @@ class ContainerMetadata with FastEquatable {
|
||||
bool? clearDataOnExit,
|
||||
bool? excludeFromIndex,
|
||||
bool? bypassGlobalProxy,
|
||||
bool? useCustomColor,
|
||||
List<Uri>? assignedSites,
|
||||
}) : this(
|
||||
iconData: iconData,
|
||||
@@ -80,6 +88,7 @@ class ContainerMetadata with FastEquatable {
|
||||
clearDataOnExit: clearDataOnExit ?? false,
|
||||
excludeFromIndex: excludeFromIndex ?? false,
|
||||
bypassGlobalProxy: bypassGlobalProxy ?? false,
|
||||
useCustomColor: useCustomColor ?? false,
|
||||
assignedSites: assignedSites,
|
||||
);
|
||||
|
||||
@@ -98,6 +107,7 @@ class ContainerMetadata with FastEquatable {
|
||||
clearDataOnExit,
|
||||
excludeFromIndex,
|
||||
bypassGlobalProxy,
|
||||
useCustomColor,
|
||||
assignedSites,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ abstract class _$ContainerMetadataCWProxy {
|
||||
|
||||
ContainerMetadata bypassGlobalProxy(bool bypassGlobalProxy);
|
||||
|
||||
ContainerMetadata useCustomColor(bool useCustomColor);
|
||||
|
||||
ContainerMetadata assignedSites(List<Uri>? assignedSites);
|
||||
|
||||
/// Creates a new instance with the provided field values.
|
||||
@@ -35,6 +37,7 @@ abstract class _$ContainerMetadataCWProxy {
|
||||
bool clearDataOnExit,
|
||||
bool excludeFromIndex,
|
||||
bool bypassGlobalProxy,
|
||||
bool useCustomColor,
|
||||
List<Uri>? assignedSites,
|
||||
});
|
||||
}
|
||||
@@ -69,6 +72,10 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
||||
ContainerMetadata bypassGlobalProxy(bool bypassGlobalProxy) =>
|
||||
call(bypassGlobalProxy: bypassGlobalProxy);
|
||||
|
||||
@override
|
||||
ContainerMetadata useCustomColor(bool useCustomColor) =>
|
||||
call(useCustomColor: useCustomColor);
|
||||
|
||||
@override
|
||||
ContainerMetadata assignedSites(List<Uri>? assignedSites) =>
|
||||
call(assignedSites: assignedSites);
|
||||
@@ -88,6 +95,7 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
||||
Object? clearDataOnExit = const $CopyWithPlaceholder(),
|
||||
Object? excludeFromIndex = const $CopyWithPlaceholder(),
|
||||
Object? bypassGlobalProxy = const $CopyWithPlaceholder(),
|
||||
Object? useCustomColor = const $CopyWithPlaceholder(),
|
||||
Object? assignedSites = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return ContainerMetadata(
|
||||
@@ -121,6 +129,12 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
||||
? _value.bypassGlobalProxy
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: bypassGlobalProxy as bool,
|
||||
useCustomColor:
|
||||
useCustomColor == const $CopyWithPlaceholder() ||
|
||||
useCustomColor == null
|
||||
? _value.useCustomColor
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: useCustomColor as bool,
|
||||
assignedSites: assignedSites == const $CopyWithPlaceholder()
|
||||
? _value.assignedSites
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
@@ -262,6 +276,7 @@ ContainerMetadata _$ContainerMetadataFromJson(Map<String, dynamic> json) =>
|
||||
clearDataOnExit: json['clearDataOnExit'] as bool? ?? false,
|
||||
excludeFromIndex: json['excludeFromIndex'] as bool? ?? false,
|
||||
bypassGlobalProxy: json['bypassGlobalProxy'] as bool? ?? false,
|
||||
useCustomColor: json['useCustomColor'] as bool? ?? false,
|
||||
assignedSites: (json['assignedSites'] as List<dynamic>?)
|
||||
?.map((e) => Uri.parse(e as String))
|
||||
.toList(),
|
||||
@@ -279,6 +294,7 @@ Map<String, dynamic> _$ContainerMetadataToJson(
|
||||
'clearDataOnExit': instance.clearDataOnExit,
|
||||
'excludeFromIndex': instance.excludeFromIndex,
|
||||
'bypassGlobalProxy': instance.bypassGlobalProxy,
|
||||
'useCustomColor': instance.useCustomColor,
|
||||
'assignedSites': instance.assignedSites?.map((e) => e.toString()).toList(),
|
||||
};
|
||||
|
||||
|
||||
+6
-16
@@ -17,9 +17,9 @@
|
||||
* 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';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/core/uuid.dart';
|
||||
@@ -299,22 +299,12 @@ class ContainerRepository extends _$ContainerRepository {
|
||||
}
|
||||
|
||||
Future<Color> unusedRandomContainerColor() async {
|
||||
final usedColors = await getDistinctColors();
|
||||
final unusedColorTypes = colorTypes.where((colors) {
|
||||
return !shadingTypes(
|
||||
colors,
|
||||
).any((shade) => usedColors.contains(shade.keys.first));
|
||||
}).toList();
|
||||
final availableColors =
|
||||
(unusedColorTypes.isNotEmpty ? unusedColorTypes : colorTypes).flattened
|
||||
final usedColors = (await getDistinctColors()).toSet();
|
||||
final unused = containerSeedColors
|
||||
.where((color) => !usedColors.contains(color))
|
||||
.toList();
|
||||
|
||||
Color randomColor;
|
||||
do {
|
||||
randomColor = randomColorShade(availableColors);
|
||||
} while (usedColors.contains(randomColor));
|
||||
|
||||
return randomColor;
|
||||
final pool = unused.isNotEmpty ? unused : containerSeedColors;
|
||||
return pool[Random().nextInt(pool.length)];
|
||||
}
|
||||
|
||||
Future<ContainerData> createNewContainer() async {
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ final class ContainerRepositoryProvider
|
||||
}
|
||||
|
||||
String _$containerRepositoryHash() =>
|
||||
r'14c77b4f3ed21d9511d5bf507db013b02a122595';
|
||||
r'b967f8948baf4347346a1369ad48a476da69e3a2';
|
||||
|
||||
abstract class _$ContainerRepository extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
+12
-4
@@ -88,6 +88,7 @@ class ContainerEditScreen extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
final selectedColor = useState(initialContainer.color);
|
||||
final useCustomColor = useState(initialContainer.metadata.useCustomColor);
|
||||
final selectedIcon = useState(initialContainer.metadata.iconData);
|
||||
final contextualIdentity = useState(
|
||||
initialContainer.metadata.contextualIdentity,
|
||||
@@ -129,6 +130,7 @@ class ContainerEditScreen extends HookConsumerWidget {
|
||||
contextualIdentity.value != null &&
|
||||
proxyConnectionId.value == null &&
|
||||
bypassGlobalProxy.value,
|
||||
useCustomColor: useCustomColor.value,
|
||||
assignedSites: assignedSites.value,
|
||||
),
|
||||
);
|
||||
@@ -160,13 +162,17 @@ class ContainerEditScreen extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
Future<void> openColorPicker() async {
|
||||
final color = await showDialog<Color?>(
|
||||
final result = await showDialog<ColorPickerResult?>(
|
||||
context: context,
|
||||
builder: (context) => ColorPickerDialog(selectedColor.value),
|
||||
builder: (context) => ColorPickerDialog(
|
||||
selectedColor.value,
|
||||
initialUseCustomColor: useCustomColor.value,
|
||||
),
|
||||
);
|
||||
|
||||
if (color != null) {
|
||||
selectedColor.value = color;
|
||||
if (result != null) {
|
||||
selectedColor.value = result.color;
|
||||
useCustomColor.value = result.useCustomColor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +185,7 @@ class ContainerEditScreen extends HookConsumerWidget {
|
||||
heightFactor: 0.92,
|
||||
child: ContainerIconPickerSheet(
|
||||
selectedColor: selectedColor.value,
|
||||
useCustomColor: useCustomColor.value,
|
||||
selectedIcon: resolveContainerIcon(selectedIcon.value),
|
||||
onSelected: (iconData) => Navigator.of(context).pop(iconData),
|
||||
),
|
||||
@@ -245,6 +252,7 @@ class ContainerEditScreen extends HookConsumerWidget {
|
||||
final previewPalette = ContainerColors.palette(
|
||||
context,
|
||||
selectedColor.value,
|
||||
useCustomColor: useCustomColor.value,
|
||||
);
|
||||
final assignedSiteCount = assignedSites.value?.length ?? 0;
|
||||
final canPickProxy =
|
||||
|
||||
+5
-1
@@ -183,7 +183,11 @@ class _ContainerCard extends HookConsumerWidget {
|
||||
final colorScheme = theme.colorScheme;
|
||||
final containerColor = container.color;
|
||||
final tabCount = container.tabCount ?? 0;
|
||||
final palette = ContainerColors.palette(context, containerColor);
|
||||
final palette = ContainerColors.palette(
|
||||
context,
|
||||
containerColor,
|
||||
useCustomColor: container.metadata.useCustomColor,
|
||||
);
|
||||
final proxyOptions = ref.watch(proxyConnectionOptionsProvider);
|
||||
final proxyOptionsState = ref.watch(singboxProxyProfilesRepositoryProvider);
|
||||
final proxyOptionsLoading =
|
||||
|
||||
+5
-1
@@ -240,7 +240,11 @@ class _SelectionContainerCard extends ConsumerWidget {
|
||||
final colorScheme = theme.colorScheme;
|
||||
final containerColor = container.color;
|
||||
final tabCount = container.tabCount ?? 0;
|
||||
final palette = ContainerColors.palette(context, containerColor);
|
||||
final palette = ContainerColors.palette(
|
||||
context,
|
||||
containerColor,
|
||||
useCustomColor: container.metadata.useCustomColor,
|
||||
);
|
||||
|
||||
final proxyOptions = ref.watch(proxyConnectionOptionsProvider);
|
||||
final proxyOptionsState = ref.watch(singboxProxyProfilesRepositoryProvider);
|
||||
|
||||
+172
-19
@@ -19,53 +19,206 @@
|
||||
*/
|
||||
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/presentation/widgets/custom_color_picker_dialog.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/utils/color_palette.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart';
|
||||
|
||||
typedef ColorPickerResult = ({Color color, bool useCustomColor});
|
||||
|
||||
class ColorPickerDialog extends HookWidget {
|
||||
final Color initialColor;
|
||||
final bool initialUseCustomColor;
|
||||
|
||||
const ColorPickerDialog(this.initialColor, {super.key});
|
||||
const ColorPickerDialog(
|
||||
this.initialColor, {
|
||||
this.initialUseCustomColor = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedColor = useState<Color>(initialColor);
|
||||
final useCustom = useState<bool>(initialUseCustomColor);
|
||||
|
||||
Future<void> openCustomPicker() async {
|
||||
final result = await showDialog<Color?>(
|
||||
context: context,
|
||||
builder: (_) => CustomColorPickerDialog(selectedColor.value),
|
||||
);
|
||||
if (result != null) {
|
||||
selectedColor.value = result;
|
||||
useCustom.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
titlePadding: const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 16.0),
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 20.0,
|
||||
right: 20.0,
|
||||
bottom: 24.0,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
insetPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20.0,
|
||||
vertical: 24.0,
|
||||
),
|
||||
title: const Text('Select Color'),
|
||||
content: MaterialPicker(
|
||||
pickerColor: selectedColor.value,
|
||||
onColorChanged: (value) {
|
||||
selectedColor.value = value;
|
||||
},
|
||||
displayColorBuilder: (context, color) {
|
||||
return ContainerColors.palette(context, color).containerColor;
|
||||
content: _ContainerColorGrid(
|
||||
selectedColor: selectedColor.value,
|
||||
useCustomColor: useCustom.value,
|
||||
onSeedSelected: (color) {
|
||||
selectedColor.value = color;
|
||||
useCustom.value = false;
|
||||
},
|
||||
onCustomTapped: openCustomPicker,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop<Color?>(context);
|
||||
},
|
||||
onPressed: () => Navigator.pop<ColorPickerResult?>(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop<Color?>(context, selectedColor.value);
|
||||
},
|
||||
onPressed: () => Navigator.pop<ColorPickerResult?>(
|
||||
context,
|
||||
(color: selectedColor.value, useCustomColor: useCustom.value),
|
||||
),
|
||||
child: const Text('Select'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ContainerColorGrid extends StatelessWidget {
|
||||
const _ContainerColorGrid({
|
||||
required this.selectedColor,
|
||||
required this.useCustomColor,
|
||||
required this.onSeedSelected,
|
||||
required this.onCustomTapped,
|
||||
});
|
||||
|
||||
final Color selectedColor;
|
||||
final bool useCustomColor;
|
||||
final ValueChanged<Color> onSeedSelected;
|
||||
final VoidCallback onCustomTapped;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final itemCount = containerSeedColors.length + 1;
|
||||
return SizedBox(
|
||||
width: 320,
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 6,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
),
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == containerSeedColors.length) {
|
||||
return _CustomSwatch(
|
||||
isSelected: useCustomColor,
|
||||
selectedColor: selectedColor,
|
||||
onTap: onCustomTapped,
|
||||
);
|
||||
}
|
||||
final seed = containerSeedColors[index];
|
||||
final palette = ContainerColors.palette(context, seed);
|
||||
final isSelected =
|
||||
!useCustomColor && seed.toARGB32() == selectedColor.toARGB32();
|
||||
return _Swatch(
|
||||
displayColor: palette.containerColor,
|
||||
checkColor: palette.onContainerColor,
|
||||
isSelected: isSelected,
|
||||
onTap: () => onSeedSelected(seed),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Swatch extends StatelessWidget {
|
||||
const _Swatch({
|
||||
required this.displayColor,
|
||||
required this.checkColor,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final Color displayColor;
|
||||
final Color checkColor;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkResponse(
|
||||
onTap: onTap,
|
||||
radius: 28,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: displayColor,
|
||||
shape: BoxShape.circle,
|
||||
border: isSelected
|
||||
? Border.all(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
width: 2,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: isSelected
|
||||
? Icon(Icons.check, size: 20, color: checkColor)
|
||||
: const SizedBox.expand(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CustomSwatch extends StatelessWidget {
|
||||
const _CustomSwatch({
|
||||
required this.isSelected,
|
||||
required this.selectedColor,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final bool isSelected;
|
||||
final Color selectedColor;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final palette = isSelected
|
||||
? ContainerColors.palette(
|
||||
context,
|
||||
selectedColor,
|
||||
useCustomColor: true,
|
||||
)
|
||||
: null;
|
||||
return InkResponse(
|
||||
onTap: onTap,
|
||||
radius: 28,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: palette?.containerColor ?? Colors.transparent,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? colorScheme.onSurface
|
||||
: colorScheme.outline.withValues(alpha: 0.5),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: isSelected
|
||||
? Icon(Icons.check, size: 20, color: palette!.onContainerColor)
|
||||
: Icon(
|
||||
Icons.colorize,
|
||||
size: 18,
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -61,7 +61,11 @@ class CompactContainerSelector extends ConsumerWidget {
|
||||
final isSelected = selectedContainer != null;
|
||||
final accentColor = selectedContainer?.color ?? colorScheme.primary;
|
||||
final showSelectedHighlight = isSelected && emphasizeSelection;
|
||||
final palette = ContainerColors.palette(context, accentColor);
|
||||
final palette = ContainerColors.palette(
|
||||
context,
|
||||
accentColor,
|
||||
useCustomColor: selectedContainer?.metadata.useCustomColor ?? false,
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
onLongPress: isSelected
|
||||
|
||||
+10
-2
@@ -31,7 +31,11 @@ Widget? buildContainerChipAvatar(
|
||||
bool isSelected, {
|
||||
double size = 18,
|
||||
}) {
|
||||
final palette = ContainerColors.palette(context, container.color);
|
||||
final palette = ContainerColors.palette(
|
||||
context,
|
||||
container.color,
|
||||
useCustomColor: container.metadata.useCustomColor,
|
||||
);
|
||||
|
||||
return chipContainerIcon(container.metadata.iconData).mapNotNull(
|
||||
(iconData) => Icon(
|
||||
@@ -48,7 +52,11 @@ Widget buildContainerChipLabel(
|
||||
bool isSelected, {
|
||||
Widget? trailing,
|
||||
}) {
|
||||
final palette = ContainerColors.palette(context, container.color);
|
||||
final palette = ContainerColors.palette(
|
||||
context,
|
||||
container.color,
|
||||
useCustomColor: container.metadata.useCustomColor,
|
||||
);
|
||||
final foregroundColor = isSelected
|
||||
? palette.selectedForegroundColor
|
||||
: palette.foregroundColor;
|
||||
|
||||
+50
-13
@@ -42,22 +42,45 @@ import 'package:weblibre/features/user/domain/repositories/general_settings.dart
|
||||
import 'package:weblibre/presentation/widgets/inline_count_badge.dart';
|
||||
import 'package:weblibre/presentation/widgets/selectable_chips.dart';
|
||||
|
||||
ContainerColorPalette _palette(BuildContext context, Color color) {
|
||||
return ContainerColors.palette(context, color);
|
||||
ContainerColorPalette _palette(
|
||||
BuildContext context,
|
||||
Color color, {
|
||||
bool useCustomColor = false,
|
||||
}) {
|
||||
return ContainerColors.palette(
|
||||
context,
|
||||
color,
|
||||
useCustomColor: useCustomColor,
|
||||
);
|
||||
}
|
||||
|
||||
Color _chipColor(BuildContext context, Color color, bool isSelected) {
|
||||
final palette = _palette(context, color);
|
||||
Color _chipColor(
|
||||
BuildContext context,
|
||||
Color color,
|
||||
bool isSelected, {
|
||||
bool useCustomColor = false,
|
||||
}) {
|
||||
final palette = _palette(context, color, useCustomColor: useCustomColor);
|
||||
return isSelected ? palette.selectedBackgroundColor : palette.backgroundColor;
|
||||
}
|
||||
|
||||
BorderSide _chipSide(BuildContext context, Color color, bool isSelected) {
|
||||
final palette = _palette(context, color);
|
||||
BorderSide _chipSide(
|
||||
BuildContext context,
|
||||
Color color,
|
||||
bool isSelected, {
|
||||
bool useCustomColor = false,
|
||||
}) {
|
||||
final palette = _palette(context, color, useCustomColor: useCustomColor);
|
||||
return isSelected ? palette.selectedBorderSide : palette.borderSide;
|
||||
}
|
||||
|
||||
InlineCountBadge _countBadge(BuildContext context, Color color, int count) {
|
||||
final palette = _palette(context, color);
|
||||
InlineCountBadge _countBadge(
|
||||
BuildContext context,
|
||||
Color color,
|
||||
int count, {
|
||||
bool useCustomColor = false,
|
||||
}) {
|
||||
final palette = _palette(context, color, useCustomColor: useCustomColor);
|
||||
return InlineCountBadge(
|
||||
count: count,
|
||||
backgroundColor: palette.badgeBackgroundColor,
|
||||
@@ -404,10 +427,18 @@ class ContainerChips extends HookConsumerWidget {
|
||||
cacheExtent: 500,
|
||||
itemId: (container) => container.id,
|
||||
decoration: SelectableChipDecoration(
|
||||
color: (container, isSelected) =>
|
||||
_chipColor(context, container.color, isSelected),
|
||||
side: (container, isSelected) =>
|
||||
_chipSide(context, container.color, isSelected),
|
||||
color: (container, isSelected) => _chipColor(
|
||||
context,
|
||||
container.color,
|
||||
isSelected,
|
||||
useCustomColor: container.metadata.useCustomColor,
|
||||
),
|
||||
side: (container, isSelected) => _chipSide(
|
||||
context,
|
||||
container.color,
|
||||
isSelected,
|
||||
useCustomColor: container.metadata.useCustomColor,
|
||||
),
|
||||
),
|
||||
itemAvatar: (container) {
|
||||
final isSelected =
|
||||
@@ -432,7 +463,13 @@ class ContainerChips extends HookConsumerWidget {
|
||||
container,
|
||||
isSelected,
|
||||
trailing: count != null && count > 0
|
||||
? _countBadge(context, container.color, count)
|
||||
? _countBadge(
|
||||
context,
|
||||
container.color,
|
||||
count,
|
||||
useCustomColor:
|
||||
container.metadata.useCustomColor,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
|
||||
+7
-1
@@ -42,17 +42,23 @@ class ContainerIconPickerSheet extends HookWidget {
|
||||
required this.selectedColor,
|
||||
required this.selectedIcon,
|
||||
required this.onSelected,
|
||||
this.useCustomColor = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Color selectedColor;
|
||||
final IconData selectedIcon;
|
||||
final bool useCustomColor;
|
||||
final ValueChanged<IconData> onSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final palette = ContainerColors.palette(context, selectedColor);
|
||||
final palette = ContainerColors.palette(
|
||||
context,
|
||||
selectedColor,
|
||||
useCustomColor: useCustomColor,
|
||||
);
|
||||
final searchController = useTextEditingController();
|
||||
useListenable(searchController);
|
||||
|
||||
|
||||
+5
-1
@@ -38,7 +38,11 @@ class ContainerListTile extends HookWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = ContainerColors.palette(context, container.color);
|
||||
final palette = ContainerColors.palette(
|
||||
context,
|
||||
container.color,
|
||||
useCustomColor: container.metadata.useCustomColor,
|
||||
);
|
||||
|
||||
return ListTileTheme(
|
||||
selectedColor: palette.onContainerColor,
|
||||
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* 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 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart';
|
||||
|
||||
/// Freeform color picker for power users.
|
||||
///
|
||||
/// Returns a [Color] (full opacity) intended to be used as the container's
|
||||
/// stored color with `useCustomColor: true`. The preview swatch shows the
|
||||
/// actual `containerColor` that [ContainerColors.palette] will produce in
|
||||
/// custom mode, so what the user sees is what the chip will look like.
|
||||
class CustomColorPickerDialog extends HookWidget {
|
||||
final Color initialColor;
|
||||
|
||||
const CustomColorPickerDialog(this.initialColor, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hsl = useState<HSLColor>(HSLColor.fromColor(initialColor));
|
||||
final hexController = useTextEditingController(text: _toHex(initialColor));
|
||||
|
||||
void updateHsl(HSLColor next) {
|
||||
hsl.value = next;
|
||||
final hex = _toHex(next.toColor());
|
||||
if (hexController.text.toUpperCase() != hex) {
|
||||
hexController.text = hex;
|
||||
}
|
||||
}
|
||||
|
||||
void onHexSubmitted(String value) {
|
||||
final parsed = _parseHex(value);
|
||||
if (parsed != null) {
|
||||
hsl.value = HSLColor.fromColor(parsed);
|
||||
hexController.text = _toHex(parsed);
|
||||
} else {
|
||||
hexController.text = _toHex(hsl.value.toColor());
|
||||
}
|
||||
}
|
||||
|
||||
final color = hsl.value.toColor();
|
||||
final palette = ContainerColors.palette(
|
||||
context,
|
||||
color,
|
||||
useCustomColor: true,
|
||||
);
|
||||
|
||||
return AlertDialog(
|
||||
titlePadding: const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 16.0),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
title: const Text('Custom Color'),
|
||||
content: SizedBox(
|
||||
width: 320,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_PreviewSwatch(palette: palette),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: hexController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Hex',
|
||||
prefixText: '#',
|
||||
isDense: true,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp('[0-9a-fA-F]')),
|
||||
LengthLimitingTextInputFormatter(6),
|
||||
],
|
||||
onSubmitted: onHexSubmitted,
|
||||
onChanged: (value) {
|
||||
if (value.length == 6) onHexSubmitted(value);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_GradientSlider(
|
||||
label: 'Hue',
|
||||
value: hsl.value.hue,
|
||||
max: 360,
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0xFFFF0000),
|
||||
Color(0xFFFFFF00),
|
||||
Color(0xFF00FF00),
|
||||
Color(0xFF00FFFF),
|
||||
Color(0xFF0000FF),
|
||||
Color(0xFFFF00FF),
|
||||
Color(0xFFFF0000),
|
||||
],
|
||||
),
|
||||
onChanged: (v) => updateHsl(hsl.value.withHue(v)),
|
||||
),
|
||||
_GradientSlider(
|
||||
label: 'Saturation',
|
||||
value: hsl.value.saturation,
|
||||
max: 1,
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
HSLColor.fromAHSL(
|
||||
1,
|
||||
hsl.value.hue,
|
||||
0,
|
||||
hsl.value.lightness,
|
||||
).toColor(),
|
||||
HSLColor.fromAHSL(
|
||||
1,
|
||||
hsl.value.hue,
|
||||
1,
|
||||
hsl.value.lightness,
|
||||
).toColor(),
|
||||
],
|
||||
),
|
||||
onChanged: (v) => updateHsl(hsl.value.withSaturation(v)),
|
||||
),
|
||||
_GradientSlider(
|
||||
label: 'Lightness',
|
||||
value: hsl.value.lightness,
|
||||
max: 1,
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.black,
|
||||
HSLColor.fromAHSL(
|
||||
1,
|
||||
hsl.value.hue,
|
||||
hsl.value.saturation,
|
||||
0.5,
|
||||
).toColor(),
|
||||
Colors.white,
|
||||
],
|
||||
),
|
||||
onChanged: (v) => updateHsl(hsl.value.withLightness(v)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop<Color?>(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop<Color?>(context, color),
|
||||
child: const Text('Select'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PreviewSwatch extends StatelessWidget {
|
||||
const _PreviewSwatch({required this.palette});
|
||||
|
||||
final ContainerColorPalette palette;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 96,
|
||||
height: 96,
|
||||
decoration: BoxDecoration(
|
||||
color: palette.containerColor,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: palette.outlineColor, width: 2),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(Icons.check, color: palette.onContainerColor, size: 32),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GradientSlider extends StatelessWidget {
|
||||
const _GradientSlider({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.max,
|
||||
required this.gradient,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final double value;
|
||||
final double max;
|
||||
final Gradient gradient;
|
||||
final ValueChanged<double> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: Theme.of(context).textTheme.labelSmall),
|
||||
SizedBox(
|
||||
height: 36,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
gradient: gradient,
|
||||
),
|
||||
),
|
||||
SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
activeTrackColor: Colors.transparent,
|
||||
inactiveTrackColor: Colors.transparent,
|
||||
overlayColor: Colors.transparent,
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 10,
|
||||
),
|
||||
),
|
||||
child: Slider(
|
||||
value: value.clamp(0, max).toDouble(),
|
||||
max: max,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _toHex(Color color) {
|
||||
final r = (color.r * 255).round().toRadixString(16).padLeft(2, '0');
|
||||
final g = (color.g * 255).round().toRadixString(16).padLeft(2, '0');
|
||||
final b = (color.b * 255).round().toRadixString(16).padLeft(2, '0');
|
||||
return '$r$g$b'.toUpperCase();
|
||||
}
|
||||
|
||||
Color? _parseHex(String value) {
|
||||
final cleaned = value.replaceAll('#', '').trim();
|
||||
if (cleaned.length != 6) return null;
|
||||
final parsed = int.tryParse(cleaned, radix: 16);
|
||||
if (parsed == null) return null;
|
||||
return Color(0xFF000000 | parsed);
|
||||
}
|
||||
-405
@@ -1,405 +0,0 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
// The Color Picker which contains Material Design Color Palette.
|
||||
import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/utils/color_palette.dart';
|
||||
|
||||
class MaterialPicker extends StatefulWidget {
|
||||
const MaterialPicker({
|
||||
super.key,
|
||||
required this.pickerColor,
|
||||
required this.onColorChanged,
|
||||
this.onPrimaryChanged,
|
||||
this.enableLabel = false,
|
||||
this.portraitOnly = false,
|
||||
this.displayAlpha,
|
||||
this.displayColorBuilder,
|
||||
});
|
||||
|
||||
final Color pickerColor;
|
||||
final ValueChanged<Color> onColorChanged;
|
||||
final ValueChanged<Color>? onPrimaryChanged;
|
||||
final bool enableLabel;
|
||||
final bool portraitOnly;
|
||||
final double? displayAlpha;
|
||||
final Color Function(BuildContext context, Color color)? displayColorBuilder;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _MaterialPickerState();
|
||||
}
|
||||
|
||||
class _MaterialPickerState extends State<MaterialPicker> {
|
||||
List<Color> _currentColorType = [Colors.red, Colors.redAccent];
|
||||
Color _currentShading = Colors.transparent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
for (final colors in colorTypes) {
|
||||
shadingTypes(colors).forEach((Map<Color, String> color) {
|
||||
if (widget.pickerColor.toARGB32() == color.keys.first.toARGB32()) {
|
||||
return setState(() {
|
||||
_currentColorType = colors;
|
||||
_currentShading = color.keys.first;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final disableAnimations = MediaQuery.disableAnimationsOf(context);
|
||||
final isPortrait =
|
||||
MediaQuery.of(context).orientation == Orientation.portrait ||
|
||||
widget.portraitOnly;
|
||||
|
||||
Color resolveDisplayColor(Color color) {
|
||||
final displayColorBuilder = widget.displayColorBuilder;
|
||||
if (displayColorBuilder != null) {
|
||||
return displayColorBuilder(context, color);
|
||||
}
|
||||
|
||||
return widget.displayAlpha != null
|
||||
? color.withValues(alpha: widget.displayAlpha)
|
||||
: color;
|
||||
}
|
||||
|
||||
Widget colorList() {
|
||||
return Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(),
|
||||
child: Container(
|
||||
margin: isPortrait
|
||||
? const EdgeInsets.only(right: 10)
|
||||
: const EdgeInsets.only(bottom: 10),
|
||||
width: isPortrait ? 60 : null,
|
||||
height: isPortrait ? null : 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (Theme.of(context).brightness == Brightness.light)
|
||||
? (Theme.of(context).brightness == Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38
|
||||
: Colors.black38,
|
||||
blurRadius: 10,
|
||||
),
|
||||
],
|
||||
border: isPortrait
|
||||
? Border(
|
||||
right: BorderSide(
|
||||
color: (Theme.of(context).brightness == Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38,
|
||||
),
|
||||
)
|
||||
: Border(
|
||||
top: BorderSide(
|
||||
color: (Theme.of(context).brightness == Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(
|
||||
context,
|
||||
).copyWith(dragDevices: PointerDeviceKind.values.toSet()),
|
||||
child: FadingScroll(
|
||||
fadingSize: 25,
|
||||
builder: (context, controller) {
|
||||
return ListView(
|
||||
controller: controller,
|
||||
scrollDirection: isPortrait ? Axis.vertical : Axis.horizontal,
|
||||
children: [
|
||||
if (isPortrait)
|
||||
const Padding(padding: EdgeInsets.only(top: 7))
|
||||
else
|
||||
const Padding(padding: EdgeInsets.only(left: 7)),
|
||||
...colorTypes.map((List<Color> colors) {
|
||||
final Color colorType = colors[0];
|
||||
final Color displayColorType = resolveDisplayColor(
|
||||
colorType,
|
||||
);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (widget.onPrimaryChanged != null) {
|
||||
widget.onPrimaryChanged!.call(colorType);
|
||||
}
|
||||
setState(() => _currentColorType = colors);
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
padding: isPortrait
|
||||
? const EdgeInsets.fromLTRB(0, 7, 0, 7)
|
||||
: const EdgeInsets.fromLTRB(7, 0, 7, 0),
|
||||
child: Align(
|
||||
child: AnimatedContainer(
|
||||
duration: disableAnimations
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 300),
|
||||
width: 25,
|
||||
height: 25,
|
||||
decoration: BoxDecoration(
|
||||
color: displayColorType,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: _currentColorType == colors
|
||||
? [
|
||||
if (colorType ==
|
||||
Theme.of(context).cardColor)
|
||||
BoxShadow(
|
||||
color:
|
||||
(Theme.of(context).brightness ==
|
||||
Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38,
|
||||
blurRadius: 10,
|
||||
)
|
||||
else
|
||||
BoxShadow(
|
||||
color: displayColorType,
|
||||
blurRadius: 10,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
border: colorType == Theme.of(context).cardColor
|
||||
? Border.all(
|
||||
color:
|
||||
(Theme.of(context).brightness ==
|
||||
Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (isPortrait)
|
||||
const Padding(padding: EdgeInsets.only(top: 5))
|
||||
else
|
||||
const Padding(padding: EdgeInsets.only(left: 5)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget shadingList() {
|
||||
return ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(
|
||||
context,
|
||||
).copyWith(dragDevices: PointerDeviceKind.values.toSet()),
|
||||
child: FadingScroll(
|
||||
fadingSize: 25,
|
||||
builder: (context, controller) {
|
||||
return ListView(
|
||||
controller: controller,
|
||||
scrollDirection: isPortrait ? Axis.vertical : Axis.horizontal,
|
||||
children: [
|
||||
if (isPortrait)
|
||||
const Padding(padding: EdgeInsets.only(top: 15))
|
||||
else
|
||||
const Padding(padding: EdgeInsets.only(left: 15)),
|
||||
...shadingTypes(_currentColorType).map((
|
||||
Map<Color, String> colors,
|
||||
) {
|
||||
final Color color = colors.keys.first;
|
||||
final Color displayColor = resolveDisplayColor(color);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() => _currentShading = color);
|
||||
widget.onColorChanged(color);
|
||||
},
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
margin: isPortrait
|
||||
? const EdgeInsets.only(right: 10)
|
||||
: const EdgeInsets.only(bottom: 10),
|
||||
padding: isPortrait
|
||||
? const EdgeInsets.fromLTRB(0, 7, 0, 7)
|
||||
: const EdgeInsets.fromLTRB(7, 0, 7, 0),
|
||||
child: Align(
|
||||
child: AnimatedContainer(
|
||||
curve: Curves.fastOutSlowIn,
|
||||
duration: disableAnimations
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 500),
|
||||
width: isPortrait
|
||||
? (_currentShading == color ? 250 : 230)
|
||||
: (_currentShading == color ? 50 : 30),
|
||||
height: isPortrait ? 50 : 220,
|
||||
decoration: BoxDecoration(
|
||||
color: displayColor,
|
||||
boxShadow: _currentShading == color
|
||||
? [
|
||||
if ((color == Colors.white) ||
|
||||
(color == Colors.black))
|
||||
BoxShadow(
|
||||
color:
|
||||
(Theme.of(context).brightness ==
|
||||
Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38,
|
||||
blurRadius: 10,
|
||||
)
|
||||
else
|
||||
BoxShadow(
|
||||
color: displayColor,
|
||||
blurRadius: 10,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
border:
|
||||
(color == Colors.white) ||
|
||||
(color == Colors.black)
|
||||
? Border.all(
|
||||
color:
|
||||
(Theme.of(context).brightness ==
|
||||
Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: widget.enableLabel
|
||||
? isPortrait
|
||||
? Row(
|
||||
children: [
|
||||
Text(
|
||||
' ${colors.values.first}',
|
||||
style: TextStyle(
|
||||
color:
|
||||
useWhiteForeground(
|
||||
displayColor,
|
||||
)
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
'#${color.toString().replaceFirst('Color(0xff', '').replaceFirst(')', '').toUpperCase()} ',
|
||||
style: TextStyle(
|
||||
color:
|
||||
useWhiteForeground(
|
||||
displayColor,
|
||||
)
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: AnimatedOpacity(
|
||||
duration: disableAnimations
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 300),
|
||||
opacity: _currentShading == color
|
||||
? 1
|
||||
: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16,
|
||||
),
|
||||
alignment: Alignment.topCenter,
|
||||
child: Text(
|
||||
colors.values.first,
|
||||
style: TextStyle(
|
||||
color:
|
||||
useWhiteForeground(
|
||||
displayColor,
|
||||
)
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
softWrap: false,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (isPortrait)
|
||||
const Padding(padding: EdgeInsets.only(top: 15))
|
||||
else
|
||||
const Padding(padding: EdgeInsets.only(left: 15)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isPortrait) {
|
||||
return SizedBox(
|
||||
width: 350,
|
||||
height: 500,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
colorList(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: shadingList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
width: 500,
|
||||
height: 300,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
colorList(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: shadingList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,117 +17,19 @@
|
||||
* 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';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_color_utilities/material_color_utilities.dart';
|
||||
|
||||
final _rnd = Random();
|
||||
const int _hueCount = 24;
|
||||
const double _seedChroma = 60.0;
|
||||
const double _seedTone = 60.0;
|
||||
|
||||
/// Check if is good condition to use white foreground color by passing
|
||||
/// the background color, and optional bias.
|
||||
///
|
||||
/// Reference:
|
||||
///
|
||||
/// Old: https://www.w3.org/TR/WCAG20-TECHS/G18.html
|
||||
///
|
||||
/// New: https://github.com/mchome/flutter_statusbarcolor/issues/40
|
||||
bool useWhiteForeground(Color backgroundColor, {double bias = 0.0}) {
|
||||
// Old:
|
||||
// return 1.05 / (color.computeLuminance() + 0.05) > 4.5;
|
||||
|
||||
// New:
|
||||
final v = sqrt(
|
||||
pow(backgroundColor.r, 2) * 0.299 +
|
||||
pow(backgroundColor.g, 2) * 0.587 +
|
||||
pow(backgroundColor.b, 2) * 0.114,
|
||||
).round();
|
||||
return v < (130 + bias);
|
||||
}
|
||||
|
||||
const List<List<Color>> colorTypes = [
|
||||
[Colors.red, Colors.redAccent],
|
||||
[Colors.pink, Colors.pinkAccent],
|
||||
[Colors.purple, Colors.purpleAccent],
|
||||
[Colors.deepPurple, Colors.deepPurpleAccent],
|
||||
[Colors.indigo, Colors.indigoAccent],
|
||||
[Colors.blue, Colors.blueAccent],
|
||||
[Colors.lightBlue, Colors.lightBlueAccent],
|
||||
[Colors.cyan, Colors.cyanAccent],
|
||||
[Colors.teal, Colors.tealAccent],
|
||||
[Colors.green, Colors.greenAccent],
|
||||
[Colors.lightGreen, Colors.lightGreenAccent],
|
||||
[Colors.lime, Colors.limeAccent],
|
||||
[Colors.yellow, Colors.yellowAccent],
|
||||
[Colors.amber, Colors.amberAccent],
|
||||
[Colors.orange, Colors.orangeAccent],
|
||||
[Colors.deepOrange, Colors.deepOrangeAccent],
|
||||
[Colors.brown],
|
||||
[Colors.grey],
|
||||
[Colors.blueGrey],
|
||||
[Colors.black],
|
||||
];
|
||||
|
||||
List<Map<Color, String>> shadingTypes(List<Color> colors) {
|
||||
final List<Map<Color, String>> result = [];
|
||||
|
||||
for (final Color colorType in colors) {
|
||||
if (colorType == Colors.grey) {
|
||||
result.addAll(
|
||||
[
|
||||
50,
|
||||
100,
|
||||
200,
|
||||
300,
|
||||
350,
|
||||
400,
|
||||
500,
|
||||
600,
|
||||
700,
|
||||
800,
|
||||
850,
|
||||
900,
|
||||
].map((int shade) => {Colors.grey[shade]!: shade.toString()}).toList(),
|
||||
// Evenly spaced HCT hues. ColorScheme.fromSeed extracts the seed's hue and
|
||||
// normalizes tone, so spacing in HCT (not HSL) guarantees each swatch yields
|
||||
// a perceptually distinct primaryContainer.
|
||||
final List<Color> containerSeedColors = List<Color>.unmodifiable(
|
||||
List.generate(_hueCount, (i) {
|
||||
final hue = i * 360.0 / _hueCount;
|
||||
return Color(Hct.from(hue, _seedChroma, _seedTone).toInt());
|
||||
}),
|
||||
);
|
||||
} else if (colorType == Colors.black || colorType == Colors.white) {
|
||||
result.addAll([
|
||||
{Colors.black: ''},
|
||||
{Colors.white: ''},
|
||||
]);
|
||||
} else if (colorType is MaterialAccentColor) {
|
||||
result.addAll(
|
||||
[
|
||||
100,
|
||||
200,
|
||||
400,
|
||||
700,
|
||||
].map((int shade) => {colorType[shade]!: 'A$shade'}).toList(),
|
||||
);
|
||||
} else if (colorType is MaterialColor) {
|
||||
result.addAll(
|
||||
[
|
||||
50,
|
||||
100,
|
||||
200,
|
||||
300,
|
||||
400,
|
||||
500,
|
||||
600,
|
||||
700,
|
||||
800,
|
||||
900,
|
||||
].map((int shade) => {colorType[shade]!: shade.toString()}).toList(),
|
||||
);
|
||||
} else {
|
||||
result.add({Colors.transparent: ''});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Color randomColorShade(List<Color> colors) {
|
||||
final color = colors[_rnd.nextInt(colors.length)];
|
||||
final shades = shadingTypes([color]);
|
||||
|
||||
return shades[_rnd.nextInt(shades.length)].keys.first;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_color_utilities/material_color_utilities.dart';
|
||||
|
||||
class ContainerColorPalette {
|
||||
const ContainerColorPalette({
|
||||
@@ -65,8 +66,17 @@ class ContainerColorPalette {
|
||||
|
||||
/// Centralized helper for container color display and theming.
|
||||
///
|
||||
/// This class converts the stored container seed color into Material 3 roles
|
||||
/// used consistently across the application.
|
||||
/// Converts the stored container color into Material 3 roles used
|
||||
/// consistently across the application. Supports two modes:
|
||||
///
|
||||
/// - **Seed mode** (default): the color is treated as a seed and fed through
|
||||
/// [ColorScheme.fromSeed], yielding an M3-harmonized palette. The actual
|
||||
/// container background depends on the theme brightness (T90 light / T30
|
||||
/// dark) — picking a dark seed does not produce a dark container.
|
||||
/// - **Custom mode** (`useCustomColor: true`): the color is used directly as
|
||||
/// `primaryContainer`. Accent/outline are derived via HCT tone shifts and
|
||||
/// on-colors via WCAG contrast. Enables true any-color choice (including
|
||||
/// black/dark grey) at the cost of strict M3 harmonization.
|
||||
class ContainerColors {
|
||||
ContainerColors._();
|
||||
|
||||
@@ -74,55 +84,100 @@ class ContainerColors {
|
||||
static const double surfaceHighAlpha = 0.28;
|
||||
static const double outlineBorderAlpha = 0.5;
|
||||
|
||||
static ContainerColorPalette palette(BuildContext context, Color seedColor) {
|
||||
static ContainerColorPalette palette(
|
||||
BuildContext context,
|
||||
Color color, {
|
||||
bool useCustomColor = false,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
final appScheme = theme.colorScheme;
|
||||
final containerScheme = ColorScheme.fromSeed(
|
||||
seedColor: fullOpacity(seedColor),
|
||||
final fullColor = fullOpacity(color);
|
||||
|
||||
final containerColor = useCustomColor
|
||||
? fullColor
|
||||
: ColorScheme.fromSeed(
|
||||
seedColor: fullColor,
|
||||
brightness: theme.brightness,
|
||||
);
|
||||
).primaryContainer;
|
||||
final accentColor = useCustomColor
|
||||
? _shiftTone(fullColor, theme.brightness)
|
||||
: ColorScheme.fromSeed(
|
||||
seedColor: fullColor,
|
||||
brightness: theme.brightness,
|
||||
).primary;
|
||||
final onContainerColor = useCustomColor
|
||||
? _contrastingForeground(containerColor)
|
||||
: ColorScheme.fromSeed(
|
||||
seedColor: fullColor,
|
||||
brightness: theme.brightness,
|
||||
).onPrimaryContainer;
|
||||
final onAccentColor = useCustomColor
|
||||
? _contrastingForeground(accentColor)
|
||||
: ColorScheme.fromSeed(
|
||||
seedColor: fullColor,
|
||||
brightness: theme.brightness,
|
||||
).onPrimary;
|
||||
|
||||
final surfaceColor = Color.alphaBlend(
|
||||
containerScheme.primaryContainer.withValues(alpha: surfaceAlpha),
|
||||
containerColor.withValues(alpha: surfaceAlpha),
|
||||
appScheme.surfaceContainer,
|
||||
);
|
||||
final surfaceHighColor = Color.alphaBlend(
|
||||
containerScheme.primaryContainer.withValues(alpha: surfaceHighAlpha),
|
||||
containerColor.withValues(alpha: surfaceHighAlpha),
|
||||
appScheme.surfaceContainerHighest,
|
||||
);
|
||||
final outlineColor = containerScheme.primary.withValues(
|
||||
alpha: outlineBorderAlpha,
|
||||
);
|
||||
final outlineColor = accentColor.withValues(alpha: outlineBorderAlpha);
|
||||
|
||||
return ContainerColorPalette(
|
||||
accentColor: containerScheme.primary,
|
||||
onAccentColor: containerScheme.onPrimary,
|
||||
containerColor: containerScheme.primaryContainer,
|
||||
onContainerColor: containerScheme.onPrimaryContainer,
|
||||
accentColor: accentColor,
|
||||
onAccentColor: onAccentColor,
|
||||
containerColor: containerColor,
|
||||
onContainerColor: onContainerColor,
|
||||
surfaceColor: surfaceColor,
|
||||
surfaceHighColor: surfaceHighColor,
|
||||
outlineColor: outlineColor,
|
||||
backgroundColor: surfaceColor,
|
||||
selectedBackgroundColor: containerScheme.primaryContainer,
|
||||
selectedBackgroundColor: containerColor,
|
||||
borderSide: BorderSide(color: outlineColor),
|
||||
selectedBorderSide: const BorderSide(color: Colors.transparent),
|
||||
foregroundColor: appScheme.onSurfaceVariant,
|
||||
selectedForegroundColor: containerScheme.onPrimaryContainer,
|
||||
badgeBackgroundColor: containerScheme.primary,
|
||||
badgeForegroundColor: containerScheme.onPrimary,
|
||||
avatarColor: containerScheme.primary,
|
||||
selectedAvatarColor: containerScheme.onPrimaryContainer,
|
||||
selectedForegroundColor: onContainerColor,
|
||||
badgeBackgroundColor: accentColor,
|
||||
badgeForegroundColor: onAccentColor,
|
||||
avatarColor: accentColor,
|
||||
selectedAvatarColor: onContainerColor,
|
||||
avatarBackgroundColor: surfaceHighColor,
|
||||
avatarForegroundColor: containerScheme.primary,
|
||||
avatarForegroundColor: accentColor,
|
||||
);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// Shifts the color in HCT to a tone suitable for use as an accent against
|
||||
/// the theme surface. Light theme uses T40 (darker); dark theme uses T80
|
||||
/// (lighter). Mirrors M3's primary tone targets.
|
||||
static Color _shiftTone(Color color, Brightness brightness) {
|
||||
final hct = Hct.fromInt(color.toARGB32());
|
||||
final targetTone = brightness == Brightness.light ? 40.0 : 80.0;
|
||||
return Color(Hct.from(hct.hue, hct.chroma, targetTone).toInt());
|
||||
}
|
||||
|
||||
/// Picks whichever of black or white has higher contrast against
|
||||
/// [background].
|
||||
static Color _contrastingForeground(Color background) {
|
||||
final blackContrast = _contrastRatio(background, Colors.black);
|
||||
final whiteContrast = _contrastRatio(background, Colors.white);
|
||||
return blackContrast >= whiteContrast ? Colors.black : Colors.white;
|
||||
}
|
||||
|
||||
static double _contrastRatio(Color a, Color b) {
|
||||
final aLuminance = a.computeLuminance();
|
||||
final bLuminance = b.computeLuminance();
|
||||
final lighter = aLuminance > bLuminance ? aLuminance : bLuminance;
|
||||
final darker = aLuminance > bLuminance ? bLuminance : aLuminance;
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class UrlListTile extends StatelessWidget {
|
||||
final Widget? leading;
|
||||
final Widget? trailing;
|
||||
final Color? containerColor;
|
||||
final bool useCustomColor;
|
||||
final bool showHttpScheme;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@@ -38,6 +39,7 @@ class UrlListTile extends StatelessWidget {
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.containerColor,
|
||||
this.useCustomColor = false,
|
||||
this.showHttpScheme = true,
|
||||
this.onTap,
|
||||
});
|
||||
@@ -50,7 +52,11 @@ class UrlListTile extends StatelessWidget {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final containerPalette = containerColor != null
|
||||
? ContainerColors.palette(context, containerColor!)
|
||||
? ContainerColors.palette(
|
||||
context,
|
||||
containerColor!,
|
||||
useCustomColor: useCustomColor,
|
||||
)
|
||||
: null;
|
||||
|
||||
return Container(
|
||||
|
||||
Reference in New Issue
Block a user