Make the custom accent-color picker actually retint the app

NooColors (the color source for nearly the whole redesigned UI) only
had two states: the fixed violet palette, or a Material You dynamic
scheme when "Match wallpaper" was on. The 5-swatch custom accent
picker still wrote to SettingsController.seedColor, but nothing read
it back into NooColors - it only reached the underlying Material
ColorScheme, which the Noo-styled UI doesn't use, so every custom
swatch looked identically purple.

Add NooColors.fromSeed, which retints just the accent/accentText/
accentSoft roles from a chosen seed color via ColorScheme.fromSeed
(bg/surface/fg etc. stay the fixed warm-neutral palette, unlike
dynamic color which reshapes everything to match the wallpaper), and
wire it into AppTheme.light/dark. Add a "Default" (Noo violet) swatch
to the picker, and move SettingsController's initial seed color to
that same violet so a fresh install - or any device without dynamic
color - keeps today's look instead of picking up the leftover
Nextcloud-blue default from before seed colors did anything visible.
This commit is contained in:
2026-09-28 13:11:11 -04:00
parent c9889c69b8
commit 72472c771a
6 changed files with 149 additions and 34 deletions
+40
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:noo/theme/app_theme.dart';
import 'package:noo/theme/design_tokens.dart';
import 'package:noo/widgets/noo/core/noo_avatar.dart';
import 'package:noo/widgets/noo/core/noo_badge.dart';
@@ -17,6 +18,45 @@ import 'noo_test_utils.dart';
void main() {
setUpNooTests();
group('NooColors.fromSeed', () {
test('the default accent returns the untouched fixed palette', () {
expect(
identical(
NooColors.fromSeed(AppTheme.defaultAccent, Brightness.light),
NooColors.light,
),
isTrue,
);
expect(
identical(
NooColors.fromSeed(AppTheme.defaultAccent, Brightness.dark),
NooColors.dark,
),
isTrue,
);
});
test('a custom seed retints only the accent roles', () {
const seed = Color(0xFF009688); // Ocean Teal
final light = NooColors.fromSeed(seed, Brightness.light);
expect(light.accent, seed);
expect(light.accentText, seed);
expect(light.accentSoft, isNot(NooColors.light.accentSoft));
// The rest of the palette is untouched - a named accent choice
// retints the brand color, it doesn't reshape the surface.
expect(light.bg, NooColors.light.bg);
expect(light.surface, NooColors.light.surface);
expect(light.fg1, NooColors.light.fg1);
final dark = NooColors.fromSeed(seed, Brightness.dark);
expect(dark.accent, seed);
// Dark accentText is a lighter/desaturated tint for contrast, not
// the raw seed itself.
expect(dark.accentText, isNot(seed));
expect(dark.bg, NooColors.dark.bg);
});
});
group('NooAvatar', () {
testNooWidgets('current user uses accent-soft/accent-text', (
tester,
+12 -5
View File
@@ -15,10 +15,13 @@ void setUpNooTests() {
/// Themes built through the real [AppTheme] (non-dynamic), so the
/// [NooColors] extension is registered exactly as in the app. Built lazily
/// inside test bodies - AppTheme touches Google Fonts, which needs the test
/// binding to exist first.
/// binding to exist first. Seeded with [AppTheme.defaultAccent] (not just
/// any color) so component tests exercise the exact fixed [NooColors.light]/
/// `.dark` palette their color assertions are written against, rather than
/// a [NooColors.fromSeed] retint - see its doc comment.
ThemeData nooTheme(Brightness brightness) => brightness == Brightness.light
? AppTheme.light(AppTheme.defaultNextcloudBlue, useDynamicColor: false)
: AppTheme.dark(AppTheme.defaultNextcloudBlue, useDynamicColor: false);
? AppTheme.light(AppTheme.defaultAccent, useDynamicColor: false)
: AppTheme.dark(AppTheme.defaultAccent, useDynamicColor: false);
Future<void> pumpNoo(
WidgetTester tester,
@@ -43,7 +46,8 @@ Future<void> pumpNoo(
/// Runs [body] once per theme, each as its own named test.
void testNooWidgets(
String description,
Future<void> Function(WidgetTester tester, ThemeData theme, NooColors colors) body,
Future<void> Function(WidgetTester tester, ThemeData theme, NooColors colors)
body,
) {
for (final brightness in Brightness.values) {
testWidgets('$description (${brightness.name})', (tester) async {
@@ -59,7 +63,10 @@ BoxDecoration decorationOf(WidgetTester tester, Finder finder) {
final boxes = find.ancestor(
of: finder,
matching: find.byWidgetPredicate(
(w) => w is DecoratedBox && w.decoration is BoxDecoration && (w.decoration as BoxDecoration).color != null,
(w) =>
w is DecoratedBox &&
w.decoration is BoxDecoration &&
(w.decoration as BoxDecoration).color != null,
),
matchRoot: true,
);