refactor: unify app theme surfaces by scene

This commit is contained in:
Haitao Pan 2026-03-30 15:31:01 +08:00
parent 91618c8866
commit 9bcc4b1fac
3 changed files with 108 additions and 28 deletions

View File

@ -25,10 +25,12 @@ class _XWorkmateAppState extends State<XWorkmateApp> {
);
late final AppController _controller;
late final AppThemeSurface _themeSurface;
@override
void initState() {
super.initState();
_themeSurface = resolveAppThemeSurface();
_controller = AppController(
uiFeatureManifest: widget.featureManifest ?? UiFeatureManifest.fallback(),
);
@ -88,8 +90,8 @@ class _XWorkmateAppState extends State<XWorkmateApp> {
supportedLocales: const [Locale('zh'), Locale('en')],
localizationsDelegates: GlobalMaterialLocalizations.delegates,
themeMode: _controller.themeMode,
theme: AppTheme.light(),
darkTheme: AppTheme.dark(),
theme: AppTheme.light(surface: _themeSurface),
darkTheme: AppTheme.dark(surface: _themeSurface),
home: AppShell(controller: _controller),
);
},

View File

@ -3,6 +3,23 @@ import 'package:flutter/material.dart';
import 'app_palette.dart';
enum AppThemeSurface { desktop, web, mobile }
AppThemeSurface resolveAppThemeSurface({
TargetPlatform? platform,
bool isWeb = kIsWeb,
}) {
if (isWeb) {
return AppThemeSurface.web;
}
final resolvedPlatform = platform ?? defaultTargetPlatform;
if (resolvedPlatform == TargetPlatform.iOS ||
resolvedPlatform == TargetPlatform.android) {
return AppThemeSurface.mobile;
}
return AppThemeSurface.desktop;
}
// Default theme token set: simple
class SimpleSpacing {
SimpleSpacing._();
@ -166,31 +183,36 @@ class AppSizes {
}
class AppTheme {
static ThemeData light({TargetPlatform? platform}) => _theme(
static ThemeData light({
TargetPlatform? platform,
AppThemeSurface? surface,
}) => _theme(
brightness: Brightness.light,
palette: AppPalette.light,
platform: platform,
surface: surface,
);
static ThemeData dark({TargetPlatform? platform}) => _theme(
static ThemeData dark({
TargetPlatform? platform,
AppThemeSurface? surface,
}) => _theme(
brightness: Brightness.dark,
palette: AppPalette.dark,
platform: platform,
surface: surface,
);
static ThemeData _theme({
required Brightness brightness,
required AppPalette palette,
TargetPlatform? platform,
AppThemeSurface? surface,
}) {
final resolvedPlatform = platform ?? defaultTargetPlatform;
final isDesktop =
resolvedPlatform == TargetPlatform.macOS ||
resolvedPlatform == TargetPlatform.windows ||
resolvedPlatform == TargetPlatform.linux;
final isMobile =
resolvedPlatform == TargetPlatform.iOS ||
resolvedPlatform == TargetPlatform.android;
final resolvedSurface =
surface ?? resolveAppThemeSurface(platform: resolvedPlatform);
final scene = _AppThemeSceneConfig.fromSurface(resolvedSurface);
final colorScheme =
ColorScheme.fromSeed(
seedColor: palette.accent,
@ -229,16 +251,14 @@ class AppTheme {
final tunedTextTheme = _textTheme(
base.textTheme,
palette: palette,
isMobile: isMobile,
useCompactDisplay: scene.useCompactDisplay,
);
return base.copyWith(
platform: resolvedPlatform,
splashFactory: NoSplash.splashFactory,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: isDesktop
? const VisualDensity(horizontal: -1, vertical: -1)
: VisualDensity.standard,
visualDensity: scene.visualDensity,
dividerColor: palette.strokeSoft,
hoverColor: palette.hover,
textTheme: tunedTextTheme,
@ -285,9 +305,7 @@ class AppTheme {
minimumSize: WidgetStatePropertyAll(
Size(
0,
isDesktop
? AppSizes.buttonHeightDesktop
: AppSizes.buttonHeightMobile,
scene.buttonHeight,
),
),
padding: const WidgetStatePropertyAll(
@ -318,9 +336,7 @@ class AppTheme {
),
minimumSize: Size(
0,
isDesktop
? AppSizes.buttonHeightDesktop
: AppSizes.buttonHeightMobile,
scene.buttonHeight,
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0),
shape: RoundedRectangleBorder(
@ -337,9 +353,7 @@ class AppTheme {
),
minimumSize: Size(
0,
isDesktop
? AppSizes.buttonHeightDesktop
: AppSizes.buttonHeightMobile,
scene.buttonHeight,
),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
@ -446,7 +460,7 @@ class AppTheme {
static TextTheme _textTheme(
TextTheme base, {
required AppPalette palette,
required bool isMobile,
required bool useCompactDisplay,
}) {
TextStyle withUiFont(TextStyle? style) {
return (style ?? const TextStyle()).copyWith(
@ -459,10 +473,10 @@ class AppTheme {
return base.copyWith(
displaySmall: withUiFont(
base.displaySmall?.copyWith(
fontSize: isMobile ? 24 : AppTypography.displaySize,
fontSize: useCompactDisplay ? 24 : AppTypography.displaySize,
fontWeight: AppTypography.displayWeight,
letterSpacing: isMobile ? -0.24 : -0.32,
height: isMobile ? 28 / 24 : AppTypography.displayHeight,
letterSpacing: useCompactDisplay ? -0.24 : -0.32,
height: useCompactDisplay ? 28 / 24 : AppTypography.displayHeight,
color: palette.textPrimary,
),
),
@ -553,3 +567,35 @@ class AppTheme {
);
}
}
class _AppThemeSceneConfig {
const _AppThemeSceneConfig({
required this.visualDensity,
required this.buttonHeight,
required this.useCompactDisplay,
});
final VisualDensity visualDensity;
final double buttonHeight;
final bool useCompactDisplay;
factory _AppThemeSceneConfig.fromSurface(AppThemeSurface surface) {
return switch (surface) {
AppThemeSurface.desktop => const _AppThemeSceneConfig(
visualDensity: VisualDensity(horizontal: -1, vertical: -1),
buttonHeight: AppSizes.buttonHeightDesktop,
useCompactDisplay: false,
),
AppThemeSurface.web => const _AppThemeSceneConfig(
visualDensity: VisualDensity(horizontal: -1, vertical: -1),
buttonHeight: AppSizes.buttonHeightDesktop,
useCompactDisplay: false,
),
AppThemeSurface.mobile => const _AppThemeSceneConfig(
visualDensity: VisualDensity.standard,
buttonHeight: AppSizes.buttonHeightMobile,
useCompactDisplay: true,
),
};
}
}

View File

@ -6,6 +6,29 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/theme/app_theme.dart';
void main() {
test('AppTheme resolves desktop, web, and mobile surfaces explicitly', () {
expect(
resolveAppThemeSurface(platform: TargetPlatform.macOS, isWeb: false),
AppThemeSurface.desktop,
);
expect(
resolveAppThemeSurface(platform: TargetPlatform.windows, isWeb: false),
AppThemeSurface.desktop,
);
expect(
resolveAppThemeSurface(platform: TargetPlatform.android, isWeb: false),
AppThemeSurface.mobile,
);
expect(
resolveAppThemeSurface(platform: TargetPlatform.iOS, isWeb: false),
AppThemeSurface.mobile,
);
expect(
resolveAppThemeSurface(platform: TargetPlatform.macOS, isWeb: true),
AppThemeSurface.web,
);
});
test('AppTheme uses compact mobile typography on iOS and Android', () {
final iosTheme = AppTheme.light(platform: TargetPlatform.iOS);
final androidTheme = AppTheme.light(platform: TargetPlatform.android);
@ -29,12 +52,21 @@ void main() {
test('AppTheme keeps larger display typography on desktop surfaces', () {
final desktopTheme = AppTheme.light(platform: TargetPlatform.macOS);
final webTheme = AppTheme.light(
platform: TargetPlatform.macOS,
surface: AppThemeSurface.web,
);
expect(desktopTheme.textTheme.displaySmall?.fontSize, 28);
expect(webTheme.textTheme.displaySmall?.fontSize, 28);
expect(
desktopTheme.filledButtonTheme.style?.minimumSize?.resolve({})?.height,
AppSizes.buttonHeightDesktop,
);
expect(
webTheme.filledButtonTheme.style?.minimumSize?.resolve({})?.height,
AppSizes.buttonHeightDesktop,
);
});
test('AppTheme matches calm compact workspace baseline tokens', () {