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> {
|
abstract class _$TabRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
+5
-1
@@ -248,7 +248,11 @@ class _ContainerHeader extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final containerColor = container.color;
|
final containerColor = container.color;
|
||||||
final containerPalette = ContainerColors.palette(context, containerColor);
|
final containerPalette = ContainerColors.palette(
|
||||||
|
context,
|
||||||
|
containerColor,
|
||||||
|
useCustomColor: container.metadata.useCustomColor,
|
||||||
|
);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: 112,
|
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';
|
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
||||||
|
|
||||||
class CompactAppBarTitle extends ConsumerWidget {
|
class CompactAppBarTitle extends ConsumerWidget {
|
||||||
const CompactAppBarTitle({super.key, this.containerColor});
|
const CompactAppBarTitle({
|
||||||
|
super.key,
|
||||||
|
this.containerColor,
|
||||||
|
this.useCustomColor = false,
|
||||||
|
});
|
||||||
|
|
||||||
final Color? containerColor;
|
final Color? containerColor;
|
||||||
|
final bool useCustomColor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -77,6 +82,7 @@ class CompactAppBarTitle extends ConsumerWidget {
|
|||||||
siteSettingsBadgeState: siteSettingsBadgeState,
|
siteSettingsBadgeState: siteSettingsBadgeState,
|
||||||
longPressUrlCopy: settings.tabBarLongPressUrlCopy,
|
longPressUrlCopy: settings.tabBarLongPressUrlCopy,
|
||||||
containerColor: containerColor,
|
containerColor: containerColor,
|
||||||
|
useCustomColor: useCustomColor,
|
||||||
sandboxSourceUri: sandboxSourceUri,
|
sandboxSourceUri: sandboxSourceUri,
|
||||||
onSiteSettingsTap: () {
|
onSiteSettingsTap: () {
|
||||||
ref
|
ref
|
||||||
@@ -105,6 +111,7 @@ class CompactAppBarTitleView extends StatelessWidget {
|
|||||||
this.tabIcon,
|
this.tabIcon,
|
||||||
this.longPressUrlCopy = true,
|
this.longPressUrlCopy = true,
|
||||||
this.containerColor,
|
this.containerColor,
|
||||||
|
this.useCustomColor = false,
|
||||||
this.sandboxSourceUri,
|
this.sandboxSourceUri,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,6 +123,7 @@ class CompactAppBarTitleView extends StatelessWidget {
|
|||||||
final Widget? tabIcon;
|
final Widget? tabIcon;
|
||||||
final bool longPressUrlCopy;
|
final bool longPressUrlCopy;
|
||||||
final Color? containerColor;
|
final Color? containerColor;
|
||||||
|
final bool useCustomColor;
|
||||||
final Uri? sandboxSourceUri;
|
final Uri? sandboxSourceUri;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -124,7 +132,11 @@ class CompactAppBarTitleView extends StatelessWidget {
|
|||||||
final appColors = AppColors.of(context);
|
final appColors = AppColors.of(context);
|
||||||
final containerColor = this.containerColor;
|
final containerColor = this.containerColor;
|
||||||
final containerPalette = containerColor != null
|
final containerPalette = containerColor != null
|
||||||
? ContainerColors.palette(context, containerColor)
|
? ContainerColors.palette(
|
||||||
|
context,
|
||||||
|
containerColor,
|
||||||
|
useCustomColor: useCustomColor,
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
@@ -241,9 +253,14 @@ class CompactAppBarTitleView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AppBarTitle extends ConsumerWidget {
|
class AppBarTitle extends ConsumerWidget {
|
||||||
const AppBarTitle({super.key, this.containerColor});
|
const AppBarTitle({
|
||||||
|
super.key,
|
||||||
|
this.containerColor,
|
||||||
|
this.useCustomColor = false,
|
||||||
|
});
|
||||||
|
|
||||||
final Color? containerColor;
|
final Color? containerColor;
|
||||||
|
final bool useCustomColor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -278,6 +295,7 @@ class AppBarTitle extends ConsumerWidget {
|
|||||||
siteSettingsBadgeState: siteSettingsBadgeState,
|
siteSettingsBadgeState: siteSettingsBadgeState,
|
||||||
longPressUrlCopy: settings.tabBarLongPressUrlCopy,
|
longPressUrlCopy: settings.tabBarLongPressUrlCopy,
|
||||||
containerColor: containerColor,
|
containerColor: containerColor,
|
||||||
|
useCustomColor: useCustomColor,
|
||||||
sandboxSourceUri: sandboxSourceUri,
|
sandboxSourceUri: sandboxSourceUri,
|
||||||
onSiteSettingsTap: () {
|
onSiteSettingsTap: () {
|
||||||
ref
|
ref
|
||||||
@@ -306,6 +324,7 @@ class AppBarTitleView extends StatelessWidget {
|
|||||||
required this.longPressUrlCopy,
|
required this.longPressUrlCopy,
|
||||||
this.tabIcon,
|
this.tabIcon,
|
||||||
this.containerColor,
|
this.containerColor,
|
||||||
|
this.useCustomColor = false,
|
||||||
this.sandboxSourceUri,
|
this.sandboxSourceUri,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -317,6 +336,7 @@ class AppBarTitleView extends StatelessWidget {
|
|||||||
final Widget? tabIcon;
|
final Widget? tabIcon;
|
||||||
final bool longPressUrlCopy;
|
final bool longPressUrlCopy;
|
||||||
final Color? containerColor;
|
final Color? containerColor;
|
||||||
|
final bool useCustomColor;
|
||||||
final Uri? sandboxSourceUri;
|
final Uri? sandboxSourceUri;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -325,7 +345,11 @@ class AppBarTitleView extends StatelessWidget {
|
|||||||
final appColors = AppColors.of(context);
|
final appColors = AppColors.of(context);
|
||||||
final containerColor = this.containerColor;
|
final containerColor = this.containerColor;
|
||||||
final containerPalette = containerColor != null
|
final containerPalette = containerColor != null
|
||||||
? ContainerColors.palette(context, containerColor)
|
? ContainerColors.palette(
|
||||||
|
context,
|
||||||
|
containerColor,
|
||||||
|
useCustomColor: useCustomColor,
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
|
|||||||
+42
-6
@@ -236,6 +236,11 @@ class BrowserTabBar extends HookConsumerWidget {
|
|||||||
selectedTabId,
|
selectedTabId,
|
||||||
).select((data) => data.value?.color),
|
).select((data) => data.value?.color),
|
||||||
);
|
);
|
||||||
|
final containerUseCustomColor = ref.watch(
|
||||||
|
watchTabContainerDataProvider(
|
||||||
|
selectedTabId,
|
||||||
|
).select((data) => data.value?.metadata.useCustomColor ?? false),
|
||||||
|
);
|
||||||
|
|
||||||
final quickTabSwitcherMode = settings.effectiveUiQuickTabSwitcherMode();
|
final quickTabSwitcherMode = settings.effectiveUiQuickTabSwitcherMode();
|
||||||
|
|
||||||
@@ -251,8 +256,14 @@ class BrowserTabBar extends HookConsumerWidget {
|
|||||||
displayedSheet is! ViewTabsSheet)
|
displayedSheet is! ViewTabsSheet)
|
||||||
? containerColor
|
? containerColor
|
||||||
: null;
|
: null;
|
||||||
|
final effectiveUseCustomColor =
|
||||||
|
effectiveContainerColor != null && containerUseCustomColor;
|
||||||
final effectiveContainerPalette = effectiveContainerColor != null
|
final effectiveContainerPalette = effectiveContainerColor != null
|
||||||
? ContainerColors.palette(context, effectiveContainerColor)
|
? ContainerColors.palette(
|
||||||
|
context,
|
||||||
|
effectiveContainerColor,
|
||||||
|
useCustomColor: effectiveUseCustomColor,
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return BrowserTabBarView(
|
return BrowserTabBarView(
|
||||||
@@ -264,8 +275,14 @@ class BrowserTabBar extends HookConsumerWidget {
|
|||||||
backgroundColor: effectiveContainerPalette?.surfaceColor,
|
backgroundColor: effectiveContainerPalette?.surfaceColor,
|
||||||
title: showTabTitle
|
title: showTabTitle
|
||||||
? settings.tabBarLayout == TabBarLayout.compact
|
? settings.tabBarLayout == TabBarLayout.compact
|
||||||
? CompactAppBarTitle(containerColor: effectiveContainerColor)
|
? CompactAppBarTitle(
|
||||||
: AppBarTitle(containerColor: effectiveContainerColor)
|
containerColor: effectiveContainerColor,
|
||||||
|
useCustomColor: effectiveUseCustomColor,
|
||||||
|
)
|
||||||
|
: AppBarTitle(
|
||||||
|
containerColor: effectiveContainerColor,
|
||||||
|
useCustomColor: effectiveUseCustomColor,
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
actions: [
|
actions: [
|
||||||
const PinnedAddonBar(),
|
const PinnedAddonBar(),
|
||||||
@@ -457,6 +474,7 @@ class BrowserTabBarView extends StatelessWidget {
|
|||||||
|
|
||||||
class QuickTabSwitcherItem with FastEquatable {
|
class QuickTabSwitcherItem with FastEquatable {
|
||||||
final Color? color;
|
final Color? color;
|
||||||
|
final bool useCustomColor;
|
||||||
final String id;
|
final String id;
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
final TabMode tabMode;
|
final TabMode tabMode;
|
||||||
@@ -478,6 +496,7 @@ class QuickTabSwitcherItem with FastEquatable {
|
|||||||
required this.title,
|
required this.title,
|
||||||
required this.url,
|
required this.url,
|
||||||
required this.avatar,
|
required this.avatar,
|
||||||
|
this.useCustomColor = false,
|
||||||
this.isSandbox = false,
|
this.isSandbox = false,
|
||||||
this.depth = 0,
|
this.depth = 0,
|
||||||
});
|
});
|
||||||
@@ -485,6 +504,7 @@ class QuickTabSwitcherItem with FastEquatable {
|
|||||||
@override
|
@override
|
||||||
List<Object?> get hashParameters => [
|
List<Object?> get hashParameters => [
|
||||||
color,
|
color,
|
||||||
|
useCustomColor,
|
||||||
id,
|
id,
|
||||||
isActive,
|
isActive,
|
||||||
tabMode,
|
tabMode,
|
||||||
@@ -577,6 +597,7 @@ class QuickTabSwitcher extends HookConsumerWidget {
|
|||||||
: state.$1.titleOrAuthority;
|
: state.$1.titleOrAuthority;
|
||||||
return QuickTabSwitcherItem(
|
return QuickTabSwitcherItem(
|
||||||
color: state.$2?.color,
|
color: state.$2?.color,
|
||||||
|
useCustomColor: state.$2?.metadata.useCustomColor ?? false,
|
||||||
id: state.$1.id,
|
id: state.$1.id,
|
||||||
isActive: state.$1.id == selectedTabId,
|
isActive: state.$1.id == selectedTabId,
|
||||||
title: displayTitle,
|
title: displayTitle,
|
||||||
@@ -910,16 +931,26 @@ class QuickTabSwitcherView extends StatelessWidget {
|
|||||||
final color? when isSelected => ContainerColors.palette(
|
final color? when isSelected => ContainerColors.palette(
|
||||||
context,
|
context,
|
||||||
color,
|
color,
|
||||||
|
useCustomColor: item.useCustomColor,
|
||||||
).selectedBackgroundColor,
|
).selectedBackgroundColor,
|
||||||
final color? => ContainerColors.palette(context, color).backgroundColor,
|
final color? => ContainerColors.palette(
|
||||||
|
context,
|
||||||
|
color,
|
||||||
|
useCustomColor: item.useCustomColor,
|
||||||
|
).backgroundColor,
|
||||||
null => null,
|
null => null,
|
||||||
},
|
},
|
||||||
side: (item, isSelected) => switch (item.color) {
|
side: (item, isSelected) => switch (item.color) {
|
||||||
final color? when isSelected => ContainerColors.palette(
|
final color? when isSelected => ContainerColors.palette(
|
||||||
context,
|
context,
|
||||||
color,
|
color,
|
||||||
|
useCustomColor: item.useCustomColor,
|
||||||
).selectedBorderSide,
|
).selectedBorderSide,
|
||||||
final color? => ContainerColors.palette(context, color).borderSide,
|
final color? => ContainerColors.palette(
|
||||||
|
context,
|
||||||
|
color,
|
||||||
|
useCustomColor: item.useCustomColor,
|
||||||
|
).borderSide,
|
||||||
null => null,
|
null => null,
|
||||||
},
|
},
|
||||||
labelPadding: (item) =>
|
labelPadding: (item) =>
|
||||||
@@ -1015,8 +1046,13 @@ class QuickTabSwitcherView extends StatelessWidget {
|
|||||||
? ContainerColors.palette(
|
? ContainerColors.palette(
|
||||||
context,
|
context,
|
||||||
color,
|
color,
|
||||||
|
useCustomColor: item.useCustomColor,
|
||||||
).selectedForegroundColor
|
).selectedForegroundColor
|
||||||
: ContainerColors.palette(context, color).foregroundColor,
|
: ContainerColors.palette(
|
||||||
|
context,
|
||||||
|
color,
|
||||||
|
useCustomColor: item.useCustomColor,
|
||||||
|
).foregroundColor,
|
||||||
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
|
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
|
||||||
),
|
),
|
||||||
child: row,
|
child: row,
|
||||||
|
|||||||
+2
@@ -67,6 +67,8 @@ class RecentTabsSection extends ConsumerWidget {
|
|||||||
iconSize: UrlListTile.iconSize,
|
iconSize: UrlListTile.iconSize,
|
||||||
),
|
),
|
||||||
containerColor: containerData?.color,
|
containerColor: containerData?.color,
|
||||||
|
useCustomColor:
|
||||||
|
containerData?.metadata.useCustomColor ?? false,
|
||||||
onTap: () => onTabSelected(tabState.id),
|
onTap: () => onTabSelected(tabState.id),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ class ContainerMetadata with FastEquatable {
|
|||||||
@JsonKey(defaultValue: false)
|
@JsonKey(defaultValue: false)
|
||||||
final bool bypassGlobalProxy;
|
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;
|
final List<Uri>? assignedSites;
|
||||||
|
|
||||||
ContainerMetadata({
|
ContainerMetadata({
|
||||||
@@ -62,6 +68,7 @@ class ContainerMetadata with FastEquatable {
|
|||||||
required this.clearDataOnExit,
|
required this.clearDataOnExit,
|
||||||
required this.excludeFromIndex,
|
required this.excludeFromIndex,
|
||||||
required this.bypassGlobalProxy,
|
required this.bypassGlobalProxy,
|
||||||
|
required this.useCustomColor,
|
||||||
required this.assignedSites,
|
required this.assignedSites,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,6 +79,7 @@ class ContainerMetadata with FastEquatable {
|
|||||||
bool? clearDataOnExit,
|
bool? clearDataOnExit,
|
||||||
bool? excludeFromIndex,
|
bool? excludeFromIndex,
|
||||||
bool? bypassGlobalProxy,
|
bool? bypassGlobalProxy,
|
||||||
|
bool? useCustomColor,
|
||||||
List<Uri>? assignedSites,
|
List<Uri>? assignedSites,
|
||||||
}) : this(
|
}) : this(
|
||||||
iconData: iconData,
|
iconData: iconData,
|
||||||
@@ -80,6 +88,7 @@ class ContainerMetadata with FastEquatable {
|
|||||||
clearDataOnExit: clearDataOnExit ?? false,
|
clearDataOnExit: clearDataOnExit ?? false,
|
||||||
excludeFromIndex: excludeFromIndex ?? false,
|
excludeFromIndex: excludeFromIndex ?? false,
|
||||||
bypassGlobalProxy: bypassGlobalProxy ?? false,
|
bypassGlobalProxy: bypassGlobalProxy ?? false,
|
||||||
|
useCustomColor: useCustomColor ?? false,
|
||||||
assignedSites: assignedSites,
|
assignedSites: assignedSites,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -98,6 +107,7 @@ class ContainerMetadata with FastEquatable {
|
|||||||
clearDataOnExit,
|
clearDataOnExit,
|
||||||
excludeFromIndex,
|
excludeFromIndex,
|
||||||
bypassGlobalProxy,
|
bypassGlobalProxy,
|
||||||
|
useCustomColor,
|
||||||
assignedSites,
|
assignedSites,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ abstract class _$ContainerMetadataCWProxy {
|
|||||||
|
|
||||||
ContainerMetadata bypassGlobalProxy(bool bypassGlobalProxy);
|
ContainerMetadata bypassGlobalProxy(bool bypassGlobalProxy);
|
||||||
|
|
||||||
|
ContainerMetadata useCustomColor(bool useCustomColor);
|
||||||
|
|
||||||
ContainerMetadata assignedSites(List<Uri>? assignedSites);
|
ContainerMetadata assignedSites(List<Uri>? assignedSites);
|
||||||
|
|
||||||
/// Creates a new instance with the provided field values.
|
/// Creates a new instance with the provided field values.
|
||||||
@@ -35,6 +37,7 @@ abstract class _$ContainerMetadataCWProxy {
|
|||||||
bool clearDataOnExit,
|
bool clearDataOnExit,
|
||||||
bool excludeFromIndex,
|
bool excludeFromIndex,
|
||||||
bool bypassGlobalProxy,
|
bool bypassGlobalProxy,
|
||||||
|
bool useCustomColor,
|
||||||
List<Uri>? assignedSites,
|
List<Uri>? assignedSites,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -69,6 +72,10 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
|||||||
ContainerMetadata bypassGlobalProxy(bool bypassGlobalProxy) =>
|
ContainerMetadata bypassGlobalProxy(bool bypassGlobalProxy) =>
|
||||||
call(bypassGlobalProxy: bypassGlobalProxy);
|
call(bypassGlobalProxy: bypassGlobalProxy);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ContainerMetadata useCustomColor(bool useCustomColor) =>
|
||||||
|
call(useCustomColor: useCustomColor);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ContainerMetadata assignedSites(List<Uri>? assignedSites) =>
|
ContainerMetadata assignedSites(List<Uri>? assignedSites) =>
|
||||||
call(assignedSites: assignedSites);
|
call(assignedSites: assignedSites);
|
||||||
@@ -88,6 +95,7 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
|||||||
Object? clearDataOnExit = const $CopyWithPlaceholder(),
|
Object? clearDataOnExit = const $CopyWithPlaceholder(),
|
||||||
Object? excludeFromIndex = const $CopyWithPlaceholder(),
|
Object? excludeFromIndex = const $CopyWithPlaceholder(),
|
||||||
Object? bypassGlobalProxy = const $CopyWithPlaceholder(),
|
Object? bypassGlobalProxy = const $CopyWithPlaceholder(),
|
||||||
|
Object? useCustomColor = const $CopyWithPlaceholder(),
|
||||||
Object? assignedSites = const $CopyWithPlaceholder(),
|
Object? assignedSites = const $CopyWithPlaceholder(),
|
||||||
}) {
|
}) {
|
||||||
return ContainerMetadata(
|
return ContainerMetadata(
|
||||||
@@ -121,6 +129,12 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
|||||||
? _value.bypassGlobalProxy
|
? _value.bypassGlobalProxy
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
: bypassGlobalProxy as bool,
|
: bypassGlobalProxy as bool,
|
||||||
|
useCustomColor:
|
||||||
|
useCustomColor == const $CopyWithPlaceholder() ||
|
||||||
|
useCustomColor == null
|
||||||
|
? _value.useCustomColor
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: useCustomColor as bool,
|
||||||
assignedSites: assignedSites == const $CopyWithPlaceholder()
|
assignedSites: assignedSites == const $CopyWithPlaceholder()
|
||||||
? _value.assignedSites
|
? _value.assignedSites
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
@@ -262,6 +276,7 @@ ContainerMetadata _$ContainerMetadataFromJson(Map<String, dynamic> json) =>
|
|||||||
clearDataOnExit: json['clearDataOnExit'] as bool? ?? false,
|
clearDataOnExit: json['clearDataOnExit'] as bool? ?? false,
|
||||||
excludeFromIndex: json['excludeFromIndex'] as bool? ?? false,
|
excludeFromIndex: json['excludeFromIndex'] as bool? ?? false,
|
||||||
bypassGlobalProxy: json['bypassGlobalProxy'] as bool? ?? false,
|
bypassGlobalProxy: json['bypassGlobalProxy'] as bool? ?? false,
|
||||||
|
useCustomColor: json['useCustomColor'] as bool? ?? false,
|
||||||
assignedSites: (json['assignedSites'] as List<dynamic>?)
|
assignedSites: (json['assignedSites'] as List<dynamic>?)
|
||||||
?.map((e) => Uri.parse(e as String))
|
?.map((e) => Uri.parse(e as String))
|
||||||
.toList(),
|
.toList(),
|
||||||
@@ -279,6 +294,7 @@ Map<String, dynamic> _$ContainerMetadataToJson(
|
|||||||
'clearDataOnExit': instance.clearDataOnExit,
|
'clearDataOnExit': instance.clearDataOnExit,
|
||||||
'excludeFromIndex': instance.excludeFromIndex,
|
'excludeFromIndex': instance.excludeFromIndex,
|
||||||
'bypassGlobalProxy': instance.bypassGlobalProxy,
|
'bypassGlobalProxy': instance.bypassGlobalProxy,
|
||||||
|
'useCustomColor': instance.useCustomColor,
|
||||||
'assignedSites': instance.assignedSites?.map((e) => e.toString()).toList(),
|
'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
|
* 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/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
import 'dart:math';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:nullability/nullability.dart';
|
import 'package:nullability/nullability.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:weblibre/core/uuid.dart';
|
import 'package:weblibre/core/uuid.dart';
|
||||||
@@ -299,22 +299,12 @@ class ContainerRepository extends _$ContainerRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Color> unusedRandomContainerColor() async {
|
Future<Color> unusedRandomContainerColor() async {
|
||||||
final usedColors = await getDistinctColors();
|
final usedColors = (await getDistinctColors()).toSet();
|
||||||
final unusedColorTypes = colorTypes.where((colors) {
|
final unused = containerSeedColors
|
||||||
return !shadingTypes(
|
.where((color) => !usedColors.contains(color))
|
||||||
colors,
|
|
||||||
).any((shade) => usedColors.contains(shade.keys.first));
|
|
||||||
}).toList();
|
|
||||||
final availableColors =
|
|
||||||
(unusedColorTypes.isNotEmpty ? unusedColorTypes : colorTypes).flattened
|
|
||||||
.toList();
|
.toList();
|
||||||
|
final pool = unused.isNotEmpty ? unused : containerSeedColors;
|
||||||
Color randomColor;
|
return pool[Random().nextInt(pool.length)];
|
||||||
do {
|
|
||||||
randomColor = randomColorShade(availableColors);
|
|
||||||
} while (usedColors.contains(randomColor));
|
|
||||||
|
|
||||||
return randomColor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ContainerData> createNewContainer() async {
|
Future<ContainerData> createNewContainer() async {
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ final class ContainerRepositoryProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$containerRepositoryHash() =>
|
String _$containerRepositoryHash() =>
|
||||||
r'14c77b4f3ed21d9511d5bf507db013b02a122595';
|
r'b967f8948baf4347346a1369ad48a476da69e3a2';
|
||||||
|
|
||||||
abstract class _$ContainerRepository extends $Notifier<void> {
|
abstract class _$ContainerRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
+12
-4
@@ -88,6 +88,7 @@ class ContainerEditScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final selectedColor = useState(initialContainer.color);
|
final selectedColor = useState(initialContainer.color);
|
||||||
|
final useCustomColor = useState(initialContainer.metadata.useCustomColor);
|
||||||
final selectedIcon = useState(initialContainer.metadata.iconData);
|
final selectedIcon = useState(initialContainer.metadata.iconData);
|
||||||
final contextualIdentity = useState(
|
final contextualIdentity = useState(
|
||||||
initialContainer.metadata.contextualIdentity,
|
initialContainer.metadata.contextualIdentity,
|
||||||
@@ -129,6 +130,7 @@ class ContainerEditScreen extends HookConsumerWidget {
|
|||||||
contextualIdentity.value != null &&
|
contextualIdentity.value != null &&
|
||||||
proxyConnectionId.value == null &&
|
proxyConnectionId.value == null &&
|
||||||
bypassGlobalProxy.value,
|
bypassGlobalProxy.value,
|
||||||
|
useCustomColor: useCustomColor.value,
|
||||||
assignedSites: assignedSites.value,
|
assignedSites: assignedSites.value,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -160,13 +162,17 @@ class ContainerEditScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> openColorPicker() async {
|
Future<void> openColorPicker() async {
|
||||||
final color = await showDialog<Color?>(
|
final result = await showDialog<ColorPickerResult?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => ColorPickerDialog(selectedColor.value),
|
builder: (context) => ColorPickerDialog(
|
||||||
|
selectedColor.value,
|
||||||
|
initialUseCustomColor: useCustomColor.value,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (color != null) {
|
if (result != null) {
|
||||||
selectedColor.value = color;
|
selectedColor.value = result.color;
|
||||||
|
useCustomColor.value = result.useCustomColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +185,7 @@ class ContainerEditScreen extends HookConsumerWidget {
|
|||||||
heightFactor: 0.92,
|
heightFactor: 0.92,
|
||||||
child: ContainerIconPickerSheet(
|
child: ContainerIconPickerSheet(
|
||||||
selectedColor: selectedColor.value,
|
selectedColor: selectedColor.value,
|
||||||
|
useCustomColor: useCustomColor.value,
|
||||||
selectedIcon: resolveContainerIcon(selectedIcon.value),
|
selectedIcon: resolveContainerIcon(selectedIcon.value),
|
||||||
onSelected: (iconData) => Navigator.of(context).pop(iconData),
|
onSelected: (iconData) => Navigator.of(context).pop(iconData),
|
||||||
),
|
),
|
||||||
@@ -245,6 +252,7 @@ class ContainerEditScreen extends HookConsumerWidget {
|
|||||||
final previewPalette = ContainerColors.palette(
|
final previewPalette = ContainerColors.palette(
|
||||||
context,
|
context,
|
||||||
selectedColor.value,
|
selectedColor.value,
|
||||||
|
useCustomColor: useCustomColor.value,
|
||||||
);
|
);
|
||||||
final assignedSiteCount = assignedSites.value?.length ?? 0;
|
final assignedSiteCount = assignedSites.value?.length ?? 0;
|
||||||
final canPickProxy =
|
final canPickProxy =
|
||||||
|
|||||||
+5
-1
@@ -183,7 +183,11 @@ class _ContainerCard extends HookConsumerWidget {
|
|||||||
final colorScheme = theme.colorScheme;
|
final colorScheme = theme.colorScheme;
|
||||||
final containerColor = container.color;
|
final containerColor = container.color;
|
||||||
final tabCount = container.tabCount ?? 0;
|
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 proxyOptions = ref.watch(proxyConnectionOptionsProvider);
|
||||||
final proxyOptionsState = ref.watch(singboxProxyProfilesRepositoryProvider);
|
final proxyOptionsState = ref.watch(singboxProxyProfilesRepositoryProvider);
|
||||||
final proxyOptionsLoading =
|
final proxyOptionsLoading =
|
||||||
|
|||||||
+5
-1
@@ -240,7 +240,11 @@ class _SelectionContainerCard extends ConsumerWidget {
|
|||||||
final colorScheme = theme.colorScheme;
|
final colorScheme = theme.colorScheme;
|
||||||
final containerColor = container.color;
|
final containerColor = container.color;
|
||||||
final tabCount = container.tabCount ?? 0;
|
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 proxyOptions = ref.watch(proxyConnectionOptionsProvider);
|
||||||
final proxyOptionsState = ref.watch(singboxProxyProfilesRepositoryProvider);
|
final proxyOptionsState = ref.watch(singboxProxyProfilesRepositoryProvider);
|
||||||
|
|||||||
+172
-19
@@ -19,53 +19,206 @@
|
|||||||
*/
|
*/
|
||||||
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/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';
|
import 'package:weblibre/features/geckoview/features/tabs/utils/container_colors.dart';
|
||||||
|
|
||||||
|
typedef ColorPickerResult = ({Color color, bool useCustomColor});
|
||||||
|
|
||||||
class ColorPickerDialog extends HookWidget {
|
class ColorPickerDialog extends HookWidget {
|
||||||
final Color initialColor;
|
final Color initialColor;
|
||||||
|
final bool initialUseCustomColor;
|
||||||
|
|
||||||
const ColorPickerDialog(this.initialColor, {super.key});
|
const ColorPickerDialog(
|
||||||
|
this.initialColor, {
|
||||||
|
this.initialUseCustomColor = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final selectedColor = useState<Color>(initialColor);
|
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(
|
return AlertDialog(
|
||||||
titlePadding: const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 16.0),
|
titlePadding: const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 16.0),
|
||||||
contentPadding: const EdgeInsets.only(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
left: 20.0,
|
horizontal: 20.0,
|
||||||
right: 20.0,
|
vertical: 8.0,
|
||||||
bottom: 24.0,
|
|
||||||
),
|
),
|
||||||
insetPadding: const EdgeInsets.symmetric(
|
insetPadding: const EdgeInsets.symmetric(
|
||||||
horizontal: 20.0,
|
horizontal: 20.0,
|
||||||
vertical: 24.0,
|
vertical: 24.0,
|
||||||
),
|
),
|
||||||
title: const Text('Select Color'),
|
title: const Text('Select Color'),
|
||||||
content: MaterialPicker(
|
content: _ContainerColorGrid(
|
||||||
pickerColor: selectedColor.value,
|
selectedColor: selectedColor.value,
|
||||||
onColorChanged: (value) {
|
useCustomColor: useCustom.value,
|
||||||
selectedColor.value = value;
|
onSeedSelected: (color) {
|
||||||
},
|
selectedColor.value = color;
|
||||||
displayColorBuilder: (context, color) {
|
useCustom.value = false;
|
||||||
return ContainerColors.palette(context, color).containerColor;
|
|
||||||
},
|
},
|
||||||
|
onCustomTapped: openCustomPicker,
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () => Navigator.pop<ColorPickerResult?>(context),
|
||||||
Navigator.pop<Color?>(context);
|
|
||||||
},
|
|
||||||
child: const Text('Cancel'),
|
child: const Text('Cancel'),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () => Navigator.pop<ColorPickerResult?>(
|
||||||
Navigator.pop<Color?>(context, selectedColor.value);
|
context,
|
||||||
},
|
(color: selectedColor.value, useCustomColor: useCustom.value),
|
||||||
|
),
|
||||||
child: const Text('Select'),
|
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 isSelected = selectedContainer != null;
|
||||||
final accentColor = selectedContainer?.color ?? colorScheme.primary;
|
final accentColor = selectedContainer?.color ?? colorScheme.primary;
|
||||||
final showSelectedHighlight = isSelected && emphasizeSelection;
|
final showSelectedHighlight = isSelected && emphasizeSelection;
|
||||||
final palette = ContainerColors.palette(context, accentColor);
|
final palette = ContainerColors.palette(
|
||||||
|
context,
|
||||||
|
accentColor,
|
||||||
|
useCustomColor: selectedContainer?.metadata.useCustomColor ?? false,
|
||||||
|
);
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onLongPress: isSelected
|
onLongPress: isSelected
|
||||||
|
|||||||
+10
-2
@@ -31,7 +31,11 @@ Widget? buildContainerChipAvatar(
|
|||||||
bool isSelected, {
|
bool isSelected, {
|
||||||
double size = 18,
|
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(
|
return chipContainerIcon(container.metadata.iconData).mapNotNull(
|
||||||
(iconData) => Icon(
|
(iconData) => Icon(
|
||||||
@@ -48,7 +52,11 @@ Widget buildContainerChipLabel(
|
|||||||
bool isSelected, {
|
bool isSelected, {
|
||||||
Widget? trailing,
|
Widget? trailing,
|
||||||
}) {
|
}) {
|
||||||
final palette = ContainerColors.palette(context, container.color);
|
final palette = ContainerColors.palette(
|
||||||
|
context,
|
||||||
|
container.color,
|
||||||
|
useCustomColor: container.metadata.useCustomColor,
|
||||||
|
);
|
||||||
final foregroundColor = isSelected
|
final foregroundColor = isSelected
|
||||||
? palette.selectedForegroundColor
|
? palette.selectedForegroundColor
|
||||||
: palette.foregroundColor;
|
: 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/inline_count_badge.dart';
|
||||||
import 'package:weblibre/presentation/widgets/selectable_chips.dart';
|
import 'package:weblibre/presentation/widgets/selectable_chips.dart';
|
||||||
|
|
||||||
ContainerColorPalette _palette(BuildContext context, Color color) {
|
ContainerColorPalette _palette(
|
||||||
return ContainerColors.palette(context, color);
|
BuildContext context,
|
||||||
|
Color color, {
|
||||||
|
bool useCustomColor = false,
|
||||||
|
}) {
|
||||||
|
return ContainerColors.palette(
|
||||||
|
context,
|
||||||
|
color,
|
||||||
|
useCustomColor: useCustomColor,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Color _chipColor(BuildContext context, Color color, bool isSelected) {
|
Color _chipColor(
|
||||||
final palette = _palette(context, color);
|
BuildContext context,
|
||||||
|
Color color,
|
||||||
|
bool isSelected, {
|
||||||
|
bool useCustomColor = false,
|
||||||
|
}) {
|
||||||
|
final palette = _palette(context, color, useCustomColor: useCustomColor);
|
||||||
return isSelected ? palette.selectedBackgroundColor : palette.backgroundColor;
|
return isSelected ? palette.selectedBackgroundColor : palette.backgroundColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
BorderSide _chipSide(BuildContext context, Color color, bool isSelected) {
|
BorderSide _chipSide(
|
||||||
final palette = _palette(context, color);
|
BuildContext context,
|
||||||
|
Color color,
|
||||||
|
bool isSelected, {
|
||||||
|
bool useCustomColor = false,
|
||||||
|
}) {
|
||||||
|
final palette = _palette(context, color, useCustomColor: useCustomColor);
|
||||||
return isSelected ? palette.selectedBorderSide : palette.borderSide;
|
return isSelected ? palette.selectedBorderSide : palette.borderSide;
|
||||||
}
|
}
|
||||||
|
|
||||||
InlineCountBadge _countBadge(BuildContext context, Color color, int count) {
|
InlineCountBadge _countBadge(
|
||||||
final palette = _palette(context, color);
|
BuildContext context,
|
||||||
|
Color color,
|
||||||
|
int count, {
|
||||||
|
bool useCustomColor = false,
|
||||||
|
}) {
|
||||||
|
final palette = _palette(context, color, useCustomColor: useCustomColor);
|
||||||
return InlineCountBadge(
|
return InlineCountBadge(
|
||||||
count: count,
|
count: count,
|
||||||
backgroundColor: palette.badgeBackgroundColor,
|
backgroundColor: palette.badgeBackgroundColor,
|
||||||
@@ -404,10 +427,18 @@ class ContainerChips extends HookConsumerWidget {
|
|||||||
cacheExtent: 500,
|
cacheExtent: 500,
|
||||||
itemId: (container) => container.id,
|
itemId: (container) => container.id,
|
||||||
decoration: SelectableChipDecoration(
|
decoration: SelectableChipDecoration(
|
||||||
color: (container, isSelected) =>
|
color: (container, isSelected) => _chipColor(
|
||||||
_chipColor(context, container.color, isSelected),
|
context,
|
||||||
side: (container, isSelected) =>
|
container.color,
|
||||||
_chipSide(context, container.color, isSelected),
|
isSelected,
|
||||||
|
useCustomColor: container.metadata.useCustomColor,
|
||||||
|
),
|
||||||
|
side: (container, isSelected) => _chipSide(
|
||||||
|
context,
|
||||||
|
container.color,
|
||||||
|
isSelected,
|
||||||
|
useCustomColor: container.metadata.useCustomColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
itemAvatar: (container) {
|
itemAvatar: (container) {
|
||||||
final isSelected =
|
final isSelected =
|
||||||
@@ -432,7 +463,13 @@ class ContainerChips extends HookConsumerWidget {
|
|||||||
container,
|
container,
|
||||||
isSelected,
|
isSelected,
|
||||||
trailing: count != null && count > 0
|
trailing: count != null && count > 0
|
||||||
? _countBadge(context, container.color, count)
|
? _countBadge(
|
||||||
|
context,
|
||||||
|
container.color,
|
||||||
|
count,
|
||||||
|
useCustomColor:
|
||||||
|
container.metadata.useCustomColor,
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
+7
-1
@@ -42,17 +42,23 @@ class ContainerIconPickerSheet extends HookWidget {
|
|||||||
required this.selectedColor,
|
required this.selectedColor,
|
||||||
required this.selectedIcon,
|
required this.selectedIcon,
|
||||||
required this.onSelected,
|
required this.onSelected,
|
||||||
|
this.useCustomColor = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Color selectedColor;
|
final Color selectedColor;
|
||||||
final IconData selectedIcon;
|
final IconData selectedIcon;
|
||||||
|
final bool useCustomColor;
|
||||||
final ValueChanged<IconData> onSelected;
|
final ValueChanged<IconData> onSelected;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final palette = ContainerColors.palette(context, selectedColor);
|
final palette = ContainerColors.palette(
|
||||||
|
context,
|
||||||
|
selectedColor,
|
||||||
|
useCustomColor: useCustomColor,
|
||||||
|
);
|
||||||
final searchController = useTextEditingController();
|
final searchController = useTextEditingController();
|
||||||
useListenable(searchController);
|
useListenable(searchController);
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -38,7 +38,11 @@ class ContainerListTile extends HookWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final palette = ContainerColors.palette(context, container.color);
|
final palette = ContainerColors.palette(
|
||||||
|
context,
|
||||||
|
container.color,
|
||||||
|
useCustomColor: container.metadata.useCustomColor,
|
||||||
|
);
|
||||||
|
|
||||||
return ListTileTheme(
|
return ListTileTheme(
|
||||||
selectedColor: palette.onContainerColor,
|
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
|
* 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/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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
|
// Evenly spaced HCT hues. ColorScheme.fromSeed extracts the seed's hue and
|
||||||
/// the background color, and optional bias.
|
// normalizes tone, so spacing in HCT (not HSL) guarantees each swatch yields
|
||||||
///
|
// a perceptually distinct primaryContainer.
|
||||||
/// Reference:
|
final List<Color> containerSeedColors = List<Color>.unmodifiable(
|
||||||
///
|
List.generate(_hueCount, (i) {
|
||||||
/// Old: https://www.w3.org/TR/WCAG20-TECHS/G18.html
|
final hue = i * 360.0 / _hueCount;
|
||||||
///
|
return Color(Hct.from(hue, _seedChroma, _seedTone).toInt());
|
||||||
/// 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(),
|
|
||||||
);
|
|
||||||
} 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/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:material_color_utilities/material_color_utilities.dart';
|
||||||
|
|
||||||
class ContainerColorPalette {
|
class ContainerColorPalette {
|
||||||
const ContainerColorPalette({
|
const ContainerColorPalette({
|
||||||
@@ -65,8 +66,17 @@ class ContainerColorPalette {
|
|||||||
|
|
||||||
/// Centralized helper for container color display and theming.
|
/// Centralized helper for container color display and theming.
|
||||||
///
|
///
|
||||||
/// This class converts the stored container seed color into Material 3 roles
|
/// Converts the stored container color into Material 3 roles used
|
||||||
/// used consistently across the application.
|
/// 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 {
|
class ContainerColors {
|
||||||
ContainerColors._();
|
ContainerColors._();
|
||||||
|
|
||||||
@@ -74,55 +84,100 @@ class ContainerColors {
|
|||||||
static const double surfaceHighAlpha = 0.28;
|
static const double surfaceHighAlpha = 0.28;
|
||||||
static const double outlineBorderAlpha = 0.5;
|
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 theme = Theme.of(context);
|
||||||
final appScheme = theme.colorScheme;
|
final appScheme = theme.colorScheme;
|
||||||
final containerScheme = ColorScheme.fromSeed(
|
final fullColor = fullOpacity(color);
|
||||||
seedColor: fullOpacity(seedColor),
|
|
||||||
|
final containerColor = useCustomColor
|
||||||
|
? fullColor
|
||||||
|
: ColorScheme.fromSeed(
|
||||||
|
seedColor: fullColor,
|
||||||
brightness: theme.brightness,
|
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(
|
final surfaceColor = Color.alphaBlend(
|
||||||
containerScheme.primaryContainer.withValues(alpha: surfaceAlpha),
|
containerColor.withValues(alpha: surfaceAlpha),
|
||||||
appScheme.surfaceContainer,
|
appScheme.surfaceContainer,
|
||||||
);
|
);
|
||||||
final surfaceHighColor = Color.alphaBlend(
|
final surfaceHighColor = Color.alphaBlend(
|
||||||
containerScheme.primaryContainer.withValues(alpha: surfaceHighAlpha),
|
containerColor.withValues(alpha: surfaceHighAlpha),
|
||||||
appScheme.surfaceContainerHighest,
|
appScheme.surfaceContainerHighest,
|
||||||
);
|
);
|
||||||
final outlineColor = containerScheme.primary.withValues(
|
final outlineColor = accentColor.withValues(alpha: outlineBorderAlpha);
|
||||||
alpha: outlineBorderAlpha,
|
|
||||||
);
|
|
||||||
|
|
||||||
return ContainerColorPalette(
|
return ContainerColorPalette(
|
||||||
accentColor: containerScheme.primary,
|
accentColor: accentColor,
|
||||||
onAccentColor: containerScheme.onPrimary,
|
onAccentColor: onAccentColor,
|
||||||
containerColor: containerScheme.primaryContainer,
|
containerColor: containerColor,
|
||||||
onContainerColor: containerScheme.onPrimaryContainer,
|
onContainerColor: onContainerColor,
|
||||||
surfaceColor: surfaceColor,
|
surfaceColor: surfaceColor,
|
||||||
surfaceHighColor: surfaceHighColor,
|
surfaceHighColor: surfaceHighColor,
|
||||||
outlineColor: outlineColor,
|
outlineColor: outlineColor,
|
||||||
backgroundColor: surfaceColor,
|
backgroundColor: surfaceColor,
|
||||||
selectedBackgroundColor: containerScheme.primaryContainer,
|
selectedBackgroundColor: containerColor,
|
||||||
borderSide: BorderSide(color: outlineColor),
|
borderSide: BorderSide(color: outlineColor),
|
||||||
selectedBorderSide: const BorderSide(color: Colors.transparent),
|
selectedBorderSide: const BorderSide(color: Colors.transparent),
|
||||||
foregroundColor: appScheme.onSurfaceVariant,
|
foregroundColor: appScheme.onSurfaceVariant,
|
||||||
selectedForegroundColor: containerScheme.onPrimaryContainer,
|
selectedForegroundColor: onContainerColor,
|
||||||
badgeBackgroundColor: containerScheme.primary,
|
badgeBackgroundColor: accentColor,
|
||||||
badgeForegroundColor: containerScheme.onPrimary,
|
badgeForegroundColor: onAccentColor,
|
||||||
avatarColor: containerScheme.primary,
|
avatarColor: accentColor,
|
||||||
selectedAvatarColor: containerScheme.onPrimaryContainer,
|
selectedAvatarColor: onContainerColor,
|
||||||
avatarBackgroundColor: surfaceHighColor,
|
avatarBackgroundColor: surfaceHighColor,
|
||||||
avatarForegroundColor: containerScheme.primary,
|
avatarForegroundColor: accentColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the full opacity version of a container color.
|
/// 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) {
|
static Color fullOpacity(Color baseColor) {
|
||||||
return baseColor.withValues(alpha: 1.0);
|
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? leading;
|
||||||
final Widget? trailing;
|
final Widget? trailing;
|
||||||
final Color? containerColor;
|
final Color? containerColor;
|
||||||
|
final bool useCustomColor;
|
||||||
final bool showHttpScheme;
|
final bool showHttpScheme;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ class UrlListTile extends StatelessWidget {
|
|||||||
this.leading,
|
this.leading,
|
||||||
this.trailing,
|
this.trailing,
|
||||||
this.containerColor,
|
this.containerColor,
|
||||||
|
this.useCustomColor = false,
|
||||||
this.showHttpScheme = true,
|
this.showHttpScheme = true,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
});
|
});
|
||||||
@@ -50,7 +52,11 @@ class UrlListTile extends StatelessWidget {
|
|||||||
final textTheme = Theme.of(context).textTheme;
|
final textTheme = Theme.of(context).textTheme;
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final containerPalette = containerColor != null
|
final containerPalette = containerColor != null
|
||||||
? ContainerColors.palette(context, containerColor!)
|
? ContainerColors.palette(
|
||||||
|
context,
|
||||||
|
containerColor!,
|
||||||
|
useCustomColor: useCustomColor,
|
||||||
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
|
|||||||
Reference in New Issue
Block a user