diff --git a/packages/app/integration_test/screenshot_test.dart b/packages/app/integration_test/screenshot_test.dart index 63b56b01..a8f96aa8 100644 --- a/packages/app/integration_test/screenshot_test.dart +++ b/packages/app/integration_test/screenshot_test.dart @@ -95,7 +95,7 @@ Future runTestApp( }) async { await runNeon( getAppImplementations: getAppImplementations, - branding: getNeonBranding(), + theme: neonTheme, bindingOverride: binding, sharedPreferencesOverride: MemorySharedPreferences(), account: account, diff --git a/packages/app/lib/branding.dart b/packages/app/lib/branding.dart index e8e421d6..13382f2f 100644 --- a/packages/app/lib/branding.dart +++ b/packages/app/lib/branding.dart @@ -1,12 +1,23 @@ +import 'dart:ui'; + import 'package:flutter_svg/flutter_svg.dart'; -import 'package:neon/models.dart'; - -Branding getNeonBranding() => Branding( - name: 'Nextcloud Neon', - logo: SvgPicture.asset( - 'assets/logo.svg', - width: 100, - height: 100, - ), - legalese: 'Copyright © 2023, provokateurin\nUnder GPLv3 license', - ); +import 'package:neon/theme.dart'; + +final neonTheme = NeonTheme( + branding: branding, + colorScheme: colorScheme, +); + +final branding = Branding( + name: 'Nextcloud Neon', + logo: SvgPicture.asset( + 'assets/logo.svg', + width: 100, + height: 100, + ), + legalese: 'Copyright © 2023, provokateurin\nUnder GPLv3 license', +); + +const colorScheme = NeonColorScheme( + primary: Color(0xFFF37736), +); diff --git a/packages/app/lib/main.dart b/packages/app/lib/main.dart index 2a2e91b7..fcfdac75 100644 --- a/packages/app/lib/main.dart +++ b/packages/app/lib/main.dart @@ -5,6 +5,6 @@ import 'package:neon/neon.dart'; Future main() async { await runNeon( getAppImplementations: getAppImplementations, - branding: getNeonBranding(), + theme: neonTheme, ); } diff --git a/packages/neon/neon/lib/models.dart b/packages/neon/neon/lib/models.dart index 76d9197c..3c441f6e 100644 --- a/packages/neon/neon/lib/models.dart +++ b/packages/neon/neon/lib/models.dart @@ -1,4 +1,3 @@ export 'package:neon/src/models/account.dart'; export 'package:neon/src/models/app_implementation.dart'; -export 'package:neon/src/models/branding.dart'; export 'package:neon/src/models/notifications_interface.dart'; diff --git a/packages/neon/neon/lib/neon.dart b/packages/neon/neon/lib/neon.dart index 4177a1a6..7ce1372c 100644 --- a/packages/neon/neon/lib/neon.dart +++ b/packages/neon/neon/lib/neon.dart @@ -10,8 +10,8 @@ import 'package:neon/src/blocs/next_push.dart'; import 'package:neon/src/blocs/push_notifications.dart'; import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/app_implementation.dart'; -import 'package:neon/src/models/branding.dart'; import 'package:neon/src/platform/platform.dart'; +import 'package:neon/src/theme/neon.dart'; import 'package:neon/src/utils/global_options.dart'; import 'package:neon/src/utils/request_manager.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -24,7 +24,7 @@ late final String neonUserAgent; Future runNeon({ required final Iterable Function(SharedPreferences, RequestManager, NeonPlatform) getAppImplementations, - required final Branding branding, + required final NeonTheme theme, final WidgetsBinding? bindingOverride, final SharedPreferences? sharedPreferencesOverride, final Account? account, @@ -115,11 +115,8 @@ Future runNeon({ Provider( create: (final _) => packageInfo, ), - Provider( - create: (final _) => branding, - ), ], - child: const NeonApp(), + child: NeonApp(neonTheme: theme), ), ); } diff --git a/packages/neon/neon/lib/src/app.dart b/packages/neon/neon/lib/src/app.dart index e143f6e2..e7340ef5 100644 --- a/packages/neon/neon/lib/src/app.dart +++ b/packages/neon/neon/lib/src/app.dart @@ -16,11 +16,12 @@ import 'package:neon/src/models/push_notification.dart'; import 'package:neon/src/platform/platform.dart'; import 'package:neon/src/router.dart'; import 'package:neon/src/settings/widgets/option_builder.dart'; +import 'package:neon/src/theme/neon.dart'; +import 'package:neon/src/theme/theme.dart'; import 'package:neon/src/utils/global.dart'; import 'package:neon/src/utils/global_options.dart'; import 'package:neon/src/utils/localizations.dart'; import 'package:neon/src/utils/push_utils.dart'; -import 'package:neon/src/utils/theme.dart'; import 'package:provider/provider.dart'; import 'package:quick_actions/quick_actions.dart'; import 'package:tray_manager/tray_manager.dart' as tray; @@ -28,9 +29,12 @@ import 'package:window_manager/window_manager.dart'; class NeonApp extends StatefulWidget { const NeonApp({ + required this.neonTheme, super.key, }); + final NeonTheme neonTheme; + @override State createState() => _NeonAppState(); } @@ -291,6 +295,7 @@ class _NeonAppState extends State with WidgetsBindingObserver, tray.Tra keepOriginalAccentColor: themeKeepOriginalAccentColor, oledAsDark: themeOLEDAsDark, appThemes: _appImplementations.map((final a) => a.theme).whereNotNull(), + neonTheme: widget.neonTheme, ); return MaterialApp.router( diff --git a/packages/neon/neon/lib/src/models/branding.dart b/packages/neon/neon/lib/src/models/branding.dart deleted file mode 100644 index 6116b675..00000000 --- a/packages/neon/neon/lib/src/models/branding.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter/widgets.dart'; - -@immutable -class Branding { - const Branding({ - required this.name, - required this.logo, - this.legalese, - }); - - final String name; - - final Widget logo; - - final String? legalese; -} diff --git a/packages/neon/neon/lib/src/pages/login.dart b/packages/neon/neon/lib/src/pages/login.dart index b8629315..48e95e7d 100644 --- a/packages/neon/neon/lib/src/pages/login.dart +++ b/packages/neon/neon/lib/src/pages/login.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; import 'package:neon/l10n/localizations.dart'; -import 'package:neon/src/models/branding.dart'; import 'package:neon/src/router.dart'; -import 'package:neon/src/utils/theme.dart'; +import 'package:neon/src/theme/branding.dart'; +import 'package:neon/src/theme/dialog.dart'; import 'package:neon/src/utils/validators.dart'; import 'package:neon/src/widgets/nextcloud_logo.dart'; -import 'package:provider/provider.dart'; class LoginPage extends StatefulWidget { const LoginPage({ @@ -52,7 +51,7 @@ class _LoginPageState extends State { @override Widget build(final BuildContext context) { - final branding = Provider.of(context, listen: false); + final branding = Branding.of(context); return Scaffold( resizeToAvoidBottomInset: true, appBar: AppBar( @@ -62,7 +61,6 @@ class _LoginPageState extends State { child: ConstrainedBox( constraints: NeonDialogTheme.of(context).constraints, child: Scrollbar( - interactive: true, child: SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 20), primary: true, @@ -76,10 +74,12 @@ class _LoginPageState extends State { const SizedBox( height: 30, ), - Text(AppLocalizations.of(context).loginWorksWith), - const SizedBox( - height: 20, - ), + if (branding.showLoginWithNextcloud) ...[ + Text(AppLocalizations.of(context).loginWorksWith), + const SizedBox( + height: 20, + ), + ], const NextcloudLogo(), Form( key: _formKey, diff --git a/packages/neon/neon/lib/src/pages/login_check_account.dart b/packages/neon/neon/lib/src/pages/login_check_account.dart index 6c296f0f..9d35a2eb 100644 --- a/packages/neon/neon/lib/src/pages/login_check_account.dart +++ b/packages/neon/neon/lib/src/pages/login_check_account.dart @@ -6,7 +6,7 @@ import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/blocs/login_check_account.dart'; import 'package:neon/src/models/account.dart'; import 'package:neon/src/router.dart'; -import 'package:neon/src/utils/theme.dart'; +import 'package:neon/src/theme/dialog.dart'; import 'package:neon/src/widgets/account_tile.dart'; import 'package:neon/src/widgets/exception.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart'; diff --git a/packages/neon/neon/lib/src/pages/login_check_server_status.dart b/packages/neon/neon/lib/src/pages/login_check_server_status.dart index 785c5e33..1fd94472 100644 --- a/packages/neon/neon/lib/src/pages/login_check_server_status.dart +++ b/packages/neon/neon/lib/src/pages/login_check_server_status.dart @@ -3,7 +3,7 @@ import 'package:neon/l10n/localizations.dart'; import 'package:neon/src/bloc/result.dart'; import 'package:neon/src/bloc/result_builder.dart'; import 'package:neon/src/blocs/login_check_server_status.dart'; -import 'package:neon/src/utils/theme.dart'; +import 'package:neon/src/theme/dialog.dart'; import 'package:neon/src/widgets/exception.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart'; import 'package:neon/src/widgets/validation_tile.dart'; diff --git a/packages/neon/neon/lib/src/pages/settings.dart b/packages/neon/neon/lib/src/pages/settings.dart index fa82ec24..80d0e06f 100644 --- a/packages/neon/neon/lib/src/pages/settings.dart +++ b/packages/neon/neon/lib/src/pages/settings.dart @@ -8,7 +8,6 @@ import 'package:neon/l10n/localizations.dart'; import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/app_implementation.dart'; -import 'package:neon/src/models/branding.dart'; import 'package:neon/src/platform/platform.dart'; import 'package:neon/src/router.dart'; import 'package:neon/src/settings/widgets/account_settings_tile.dart'; @@ -19,6 +18,7 @@ import 'package:neon/src/settings/widgets/settings_category.dart'; import 'package:neon/src/settings/widgets/settings_list.dart'; import 'package:neon/src/settings/widgets/settings_tile.dart'; import 'package:neon/src/settings/widgets/text_settings_tile.dart'; +import 'package:neon/src/theme/branding.dart'; import 'package:neon/src/utils/confirmation_dialog.dart'; import 'package:neon/src/utils/global_options.dart'; import 'package:neon/src/utils/save_file.dart'; @@ -228,7 +228,7 @@ class _SettingsPageState extends State { ), title: Text(AppLocalizations.of(context).licenses), onTap: () async { - final branding = Provider.of(context, listen: false); + final branding = Branding.of(context); showLicensePage( context: context, applicationName: branding.name, diff --git a/packages/neon/neon/lib/src/settings/widgets/settings_list.dart b/packages/neon/neon/lib/src/settings/widgets/settings_list.dart index dd0c6c7f..cc69d2ff 100644 --- a/packages/neon/neon/lib/src/settings/widgets/settings_list.dart +++ b/packages/neon/neon/lib/src/settings/widgets/settings_list.dart @@ -14,7 +14,6 @@ class SettingsList extends StatelessWidget { @override Widget build(final BuildContext context) => Scrollbar( - interactive: true, child: ListView( primary: true, padding: const EdgeInsets.all(20), diff --git a/packages/neon/neon/lib/src/theme/branding.dart b/packages/neon/neon/lib/src/theme/branding.dart new file mode 100644 index 00000000..77fc3b1f --- /dev/null +++ b/packages/neon/neon/lib/src/theme/branding.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:neon/src/theme/neon.dart'; + +/// Custom app branding +/// +/// Descendant widgets obtain the current [Branding] object using +/// `Branding.of(context)`. Instances of [Branding] can be customized with +/// [Branding.copyWith]. +@immutable +class Branding { + /// Creates a custom branding + const Branding({ + required this.name, + required this.logo, + this.legalese, + this.showLoginWithNextcloud = false, + }); + + /// App name + final String name; + + /// Logo of the app shown on various places in the app. + final Widget logo; + + /// A string to show in small print. + /// + /// Typically this is a copyright notice shown as the [AboutDialog.applicationLegalese]. + final String? legalese; + + /// Whether to show the Nextcloud logo on the LoginPage + final bool showLoginWithNextcloud; + + /// Creates a copy of this object but with the given fields replaced with the + /// new values. + Branding copyWith({ + final String? name, + final Widget? logo, + final String? legalese, + }) => + Branding( + name: name ?? this.name, + logo: logo ?? this.logo, + legalese: legalese ?? this.legalese, + ); + + /// The data from the closest [Branding] instance given the build context. + static Branding of(final BuildContext context) => Theme.of(context).extension()!.branding; + + @override + int get hashCode => Object.hashAll([ + name, + logo, + legalese, + ]); + + @override + bool operator ==(final Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is Branding && name == other.name && logo == other.logo && legalese == other.legalese; + } +} diff --git a/packages/neon/neon/lib/src/theme/color_scheme.dart b/packages/neon/neon/lib/src/theme/color_scheme.dart new file mode 100644 index 00000000..77ab527c --- /dev/null +++ b/packages/neon/neon/lib/src/theme/color_scheme.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:neon/src/theme/colors.dart'; +import 'package:neon/src/theme/neon.dart'; + +/// A ColorScheme used in the [NeonTheme]. +@immutable +class NeonColorScheme { + const NeonColorScheme({ + this.primary = NcColors.primary, + }); + + /// Primary color used throughout the app. + /// + /// See [ColorScheme.primary] + final Color primary; + + /// Creates a copy of this object but with the given fields replaced with the + /// new values. + NeonColorScheme copyWith({ + final Color? primary, + final Color? oledBackground, + }) => + NeonColorScheme( + primary: primary ?? this.primary, + ); + + /// The data from the closest [NeonColorScheme] instance given the build context. + static NeonColorScheme of(final BuildContext context) => Theme.of(context).extension()!.colorScheme; + + /// Linearly interpolate between two [NeonColorScheme]s. + /// + /// {@macro dart.ui.shadow.lerp} + // ignore: prefer_constructors_over_static_methods + static NeonColorScheme lerp(final NeonColorScheme a, final NeonColorScheme b, final double t) { + if (identical(a, b)) { + return a; + } + return NeonColorScheme( + primary: Color.lerp(a.primary, b.primary, t)!, + ); + } + + @override + int get hashCode => Object.hashAll([ + primary, + ]); + + @override + bool operator ==(final Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is NeonColorScheme && other.primary == primary; + } +} diff --git a/packages/neon/neon/lib/src/theme/colors.dart b/packages/neon/neon/lib/src/theme/colors.dart new file mode 100644 index 00000000..46366fab --- /dev/null +++ b/packages/neon/neon/lib/src/theme/colors.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:nextcloud/nextcloud.dart'; + +/// [Color] constants which represent Nextcloud's +/// [color palette](https://docs.nextcloud.com/server/latest/developer_manual/design/foundations.html#color). +abstract final class NcColors { + /// Nextcloud blue. + /// + /// The default primary clolor as specified by the + /// [design guidlines](https://docs.nextcloud.com/server/latest/developer_manual/design/foundations.html#primary-color). + static const Color primary = Color(0xFF0082C9); + + /// The [ColorScheme.background] color used on OLED devices. + /// + /// This color is only used at the users discretion. + static const Color oledBackground = Colors.black; + + /// Color of a starred item. + static const Color starredColor = Colors.yellow; + + /// Color used to emphasise declining actions. + /// + /// Usually used in conjunction with [NcColors.accept]. + static const Color decline = Colors.red; + + /// Color used to emphasise accepting actions. + /// + /// Usually used in conjunction with [NcColors.decline]. + static const Color accept = Colors.green; +} + +/// [UserStatusType] color mapping. +extension UserStatusTypeColors on UserStatusType { + /// The color for the user status. + Color? get color => switch (this) { + UserStatusType.online => const Color(0xFF49B382), + UserStatusType.away => const Color(0xFFF4A331), + UserStatusType.dnd => const Color(0xFFED484C), + _ => null, + }; +} diff --git a/packages/neon/neon/lib/src/theme/dialog.dart b/packages/neon/neon/lib/src/theme/dialog.dart new file mode 100644 index 00000000..fccb0e22 --- /dev/null +++ b/packages/neon/neon/lib/src/theme/dialog.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:neon/src/theme/neon.dart'; +import 'package:neon/src/widgets/dialog.dart'; + +/// Defines a theme for [NeonDialog] widgets. +/// +/// Descendant widgets obtain the current [NeonDialogTheme] object using +/// `NeonDialogTheme.of(context)`. Instances of [NeonDialogTheme] can be customized with +/// [NeonDialogTheme.copyWith]. +@immutable +class NeonDialogTheme { + /// Creates a dialog theme that can be used for [NeonTheme.dialogTheme]. + const NeonDialogTheme({ + this.constraints = const BoxConstraints( + minWidth: 280, + maxWidth: 560, + ), + }); + + /// Used to configure the [BoxConstraints] for the [NeonDialog] widget. + /// + /// This value should also be used on [Dialog.fullscreen] and other similar pages. + /// By default it follows the default [m3 dialog specification](https://m3.material.io/components/dialogs/specs). + final BoxConstraints constraints; + + /// Creates a copy of this object but with the given fields replaced with the + /// new values. + NeonDialogTheme copyWith({ + final BoxConstraints? constraints, + }) => + NeonDialogTheme( + constraints: constraints ?? this.constraints, + ); + + /// The data from the closest [NeonDialogTheme] instance given the build context. + static NeonDialogTheme of(final BuildContext context) => Theme.of(context).extension()!.dialogTheme; + + /// Linearly interpolate between two [NeonDialogTheme]s. + /// + /// {@macro dart.ui.shadow.lerp} + // ignore: prefer_constructors_over_static_methods + static NeonDialogTheme lerp(final NeonDialogTheme a, final NeonDialogTheme b, final double t) { + if (identical(a, b)) { + return a; + } + return NeonDialogTheme( + constraints: BoxConstraints.lerp(a.constraints, b.constraints, t)!, + ); + } + + @override + int get hashCode => constraints.hashCode; + + @override + bool operator ==(final Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is NeonDialogTheme && other.constraints == constraints; + } +} diff --git a/packages/neon/neon/lib/src/theme/neon.dart b/packages/neon/neon/lib/src/theme/neon.dart new file mode 100644 index 00000000..915bf8b0 --- /dev/null +++ b/packages/neon/neon/lib/src/theme/neon.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:neon/src/theme/branding.dart'; +import 'package:neon/src/theme/color_scheme.dart'; +import 'package:neon/src/theme/dialog.dart'; +import 'package:neon/src/widgets/dialog.dart'; + +/// Defines the configuration of the overall visual [Theme] for the app +/// or a widget subtree within the app. +/// +/// It is typically only needed to provide a [branding]. +@immutable +class NeonTheme extends ThemeExtension { + /// Create a [NeonTheme] that's used to configure a [Theme]. + const NeonTheme({ + required this.branding, + this.colorScheme = const NeonColorScheme(), + this.dialogTheme = const NeonDialogTheme(), + }); + + /// A theme for customizing the Branding of the app. + /// + /// This is the value returned from [Branding.of]. + final Branding branding; + + /// A color scheme for customizing the default appearance of the app. + /// + /// This is the value returned from [NeonColorScheme.of]. + final NeonColorScheme colorScheme; + + /// A theme for customizing the visual properties of [NeonDialog]s. + /// + /// This is the value returned from [NeonDialogTheme.of]. + final NeonDialogTheme dialogTheme; + + @override + NeonTheme copyWith({ + final Branding? branding, + final NeonColorScheme? colorScheme, + final NeonDialogTheme? dialogTheme, + }) => + NeonTheme( + branding: branding ?? this.branding, + colorScheme: colorScheme ?? this.colorScheme, + dialogTheme: dialogTheme ?? this.dialogTheme, + ); + + @override + NeonTheme lerp(final NeonTheme? other, final double t) { + if (other is! NeonTheme) { + return this; + } + return NeonTheme( + branding: branding, + colorScheme: NeonColorScheme.lerp(colorScheme, other.colorScheme, t), + dialogTheme: NeonDialogTheme.lerp(dialogTheme, other.dialogTheme, t), + ); + } +} diff --git a/packages/neon/neon/lib/src/theme/theme.dart b/packages/neon/neon/lib/src/theme/theme.dart new file mode 100644 index 00000000..2f9aa685 --- /dev/null +++ b/packages/neon/neon/lib/src/theme/theme.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:meta/meta.dart'; +import 'package:neon/src/theme/colors.dart'; +import 'package:neon/src/theme/neon.dart'; +import 'package:neon/src/utils/hex_color.dart'; +import 'package:nextcloud/nextcloud.dart'; + +@internal +@immutable +class AppTheme { + const AppTheme( + this.nextcloudTheme, { + required this.neonTheme, + final bool keepOriginalAccentColor = false, + this.oledAsDark = false, + this.appThemes, + }) : keepOriginalAccentColor = nextcloudTheme == null || keepOriginalAccentColor; + + final CoreServerCapabilities_Ocs_Data_Capabilities_Theming? nextcloudTheme; + final bool keepOriginalAccentColor; + final bool oledAsDark; + final Iterable? appThemes; + final NeonTheme neonTheme; + + ColorScheme _buildColorScheme(final Brightness brightness) { + final primary = nextcloudTheme?.color != null ? HexColor(nextcloudTheme!.color!) : neonTheme.colorScheme.primary; + final keepOriginalAccentColorOverride = keepOriginalAccentColor ? primary : null; + final oledBackgroundOverride = oledAsDark && brightness == Brightness.dark ? NcColors.oledBackground : null; + + return ColorScheme.fromSeed( + seedColor: primary, + brightness: brightness, + ).copyWith( + background: oledBackgroundOverride, + primary: keepOriginalAccentColorOverride, + secondary: keepOriginalAccentColorOverride, + ); + } + + ThemeData _getTheme(final Brightness brightness) { + final colorScheme = _buildColorScheme(brightness); + + return ThemeData( + useMaterial3: true, + colorScheme: colorScheme, + scaffoldBackgroundColor: colorScheme.background, + cardColor: colorScheme.background, // For LicensePage + snackBarTheme: _snackBarTheme, + dividerTheme: _dividerTheme, + scrollbarTheme: _scrollbarTheme, + extensions: [ + neonTheme, + ...?appThemes, + ], + ); + } + + ThemeData get lightTheme => _getTheme(Brightness.light); + ThemeData get darkTheme => _getTheme(Brightness.dark); + + static const _snackBarTheme = SnackBarThemeData( + behavior: SnackBarBehavior.floating, + ); + + static const _dividerTheme = DividerThemeData( + thickness: 1.5, + space: 30, + ); + + static const _scrollbarTheme = ScrollbarThemeData( + interactive: true, + ); +} diff --git a/packages/neon/neon/lib/src/utils/confirmation_dialog.dart b/packages/neon/neon/lib/src/utils/confirmation_dialog.dart index 28e5e54a..0f077cf7 100644 --- a/packages/neon/neon/lib/src/utils/confirmation_dialog.dart +++ b/packages/neon/neon/lib/src/utils/confirmation_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:neon/l10n/localizations.dart'; +import 'package:neon/theme.dart'; Future showConfirmationDialog(final BuildContext context, final String title) async => await showDialog( @@ -10,7 +11,7 @@ Future showConfirmationDialog(final BuildContext context, final String tit actions: [ ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, + backgroundColor: NcColors.decline, foregroundColor: Theme.of(context).colorScheme.onPrimary, ), onPressed: () { @@ -20,7 +21,7 @@ Future showConfirmationDialog(final BuildContext context, final String tit ), ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, + backgroundColor: NcColors.accept, foregroundColor: Theme.of(context).colorScheme.onPrimary, ), onPressed: () { diff --git a/packages/neon/neon/lib/src/utils/push_utils.dart b/packages/neon/neon/lib/src/utils/push_utils.dart index d9003f3a..b82f78b4 100644 --- a/packages/neon/neon/lib/src/utils/push_utils.dart +++ b/packages/neon/neon/lib/src/utils/push_utils.dart @@ -14,10 +14,10 @@ import 'package:neon/src/models/account.dart'; import 'package:neon/src/models/push_notification.dart'; import 'package:neon/src/platform/platform.dart'; import 'package:neon/src/settings/models/storage.dart'; +import 'package:neon/src/theme/colors.dart'; import 'package:neon/src/utils/global.dart'; import 'package:neon/src/utils/localizations.dart'; import 'package:neon/src/utils/request_manager.dart'; -import 'package:neon/src/utils/theme.dart'; import 'package:nextcloud/nextcloud.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -124,7 +124,7 @@ class PushUtils { Canvas(recorder) ..scale(scale) ..drawPicture(pictureInfo.picture) - ..drawColor(themePrimaryColor, BlendMode.srcIn); + ..drawColor(NcColors.primary, BlendMode.srcIn); pictureInfo.picture.dispose(); @@ -164,7 +164,7 @@ class PushUtils { icon: '@mipmap/ic_launcher', largeIcon: largeIconBitmap, when: when?.millisecondsSinceEpoch, - color: themePrimaryColor, + color: NcColors.primary, category: pushNotification.type == 'voip' ? AndroidNotificationCategory.call : null, importance: Importance.max, priority: pushNotification.priority == 'high' diff --git a/packages/neon/neon/lib/src/utils/theme.dart b/packages/neon/neon/lib/src/utils/theme.dart deleted file mode 100644 index aa76cc9c..00000000 --- a/packages/neon/neon/lib/src/utils/theme.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:meta/meta.dart'; -import 'package:neon/src/utils/hex_color.dart'; -import 'package:neon/src/widgets/dialog.dart'; -import 'package:nextcloud/nextcloud.dart'; - -@internal -const themePrimaryColor = Color(0xFFF37736); - -@internal -@immutable -class AppTheme { - AppTheme( - this.nextcloudTheme, { - final bool keepOriginalAccentColor = false, - this.oledAsDark = false, - this.appThemes, - }) : keepOriginalAccentColor = nextcloudTheme == null || keepOriginalAccentColor; - - final CoreServerCapabilities_Ocs_Data_Capabilities_Theming? nextcloudTheme; - final bool keepOriginalAccentColor; - final bool oledAsDark; - final Iterable? appThemes; - - late final _primaryColor = nextcloudTheme?.color != null ? HexColor(nextcloudTheme!.color!) : themePrimaryColor; - late final _keepOriginalAccentColorOverride = keepOriginalAccentColor ? _primaryColor : null; - - ColorScheme _buildColorScheme(final Brightness brightness) { - final oledBackgroundOverride = oledAsDark && brightness == Brightness.dark ? Colors.black : null; - - return ColorScheme.fromSeed( - seedColor: _primaryColor, - brightness: brightness, - ).copyWith( - background: oledBackgroundOverride, - primary: _keepOriginalAccentColorOverride, - secondary: _keepOriginalAccentColorOverride, - ); - } - - ThemeData _getTheme(final Brightness brightness) { - final colorScheme = _buildColorScheme(brightness); - - return ThemeData( - useMaterial3: true, - colorScheme: colorScheme, - scaffoldBackgroundColor: colorScheme.background, - cardColor: colorScheme.background, // For LicensePage - snackBarTheme: _snackBarTheme, - dividerTheme: _dividerTheme, - extensions: [ - const NeonTheme(), - ...?appThemes, - ], - ); - } - - ThemeData get lightTheme => _getTheme(Brightness.light); - ThemeData get darkTheme => _getTheme(Brightness.dark); - - static const _snackBarTheme = SnackBarThemeData( - behavior: SnackBarBehavior.floating, - ); - - static const _dividerTheme = DividerThemeData( - thickness: 1.5, - space: 30, - ); -} - -/// Defines the configuration of the overall visual [Theme] for a NeonApp -/// or a widget subtree within the app. -@internal -@immutable -class NeonTheme extends ThemeExtension { - /// Create a [NeonTheme] that's used to configure a [Theme]. - const NeonTheme({ - this.dialogTheme = const NeonDialogTheme(), - }); - - /// A theme for customizing the visual properties of [NeonDialog]s. - /// - /// This is the value returned from [NeonDialogTheme.of]. - final NeonDialogTheme dialogTheme; - - @override - NeonTheme copyWith({ - final NeonDialogTheme? dialogTheme, - }) => - NeonTheme( - dialogTheme: dialogTheme ?? this.dialogTheme, - ); - - @override - NeonTheme lerp(final NeonTheme? other, final double t) { - if (other is! NeonTheme) { - return this; - } - return NeonTheme( - dialogTheme: NeonDialogTheme.lerp(dialogTheme, other.dialogTheme, t), - ); - } -} - -/// Defines a theme for [NeonDialog] widgets. -/// -/// Descendant widgets obtain the current [NeonDialogTheme] object using -/// `NeonDialogTheme.of(context)`. Instances of [NeonDialogTheme] can be customized with -/// [NeonDialogTheme.copyWith]. -@immutable -class NeonDialogTheme { - /// Creates a dialog theme that can be used for [NeonTheme.dialogTheme]. - const NeonDialogTheme({ - this.constraints = const BoxConstraints( - minWidth: 280, - maxWidth: 560, - ), - }); - - /// Used to configure the [BoxConstraints] for the [NeonDialog] widget. - /// - /// This value should also be used on [Dialog.fullscreen] and other similar pages. - /// By default it follows the default [m3 dialog specification](https://m3.material.io/components/dialogs/specs). - final BoxConstraints constraints; - - /// Creates a copy of this object but with the given fields replaced with the - /// new values. - NeonDialogTheme copyWith({ - final BoxConstraints? constraints, - }) => - NeonDialogTheme( - constraints: constraints ?? this.constraints, - ); - - /// The data from the closest [NeonDialogTheme] instance given the build context. - static NeonDialogTheme of(final BuildContext context) => Theme.of(context).extension()!.dialogTheme; - - /// Linearly interpolate between two neon dialog themes. - /// - /// {@macro dart.ui.shadow.lerp} - // ignore: prefer_constructors_over_static_methods - static NeonDialogTheme lerp(final NeonDialogTheme a, final NeonDialogTheme b, final double t) { - if (identical(a, b)) { - return a; - } - return NeonDialogTheme( - constraints: BoxConstraints.lerp(a.constraints, b.constraints, t)!, - ); - } - - @override - int get hashCode => constraints.hashCode; - - @override - bool operator ==(final Object other) { - if (identical(this, other)) { - return true; - } - if (other.runtimeType != runtimeType) { - return false; - } - return other is NeonDialogTheme && other.constraints == constraints; - } -} diff --git a/packages/neon/neon/lib/src/widgets/image_wrapper.dart b/packages/neon/neon/lib/src/widgets/image_wrapper.dart index a080b809..f77b967c 100644 --- a/packages/neon/neon/lib/src/widgets/image_wrapper.dart +++ b/packages/neon/neon/lib/src/widgets/image_wrapper.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; class NeonImageWrapper extends StatelessWidget { const NeonImageWrapper({ required this.child, - required this.color, + this.color = Colors.white, this.size, this.borderRadius, super.key, @@ -16,7 +16,7 @@ class NeonImageWrapper extends StatelessWidget { @override Widget build(final BuildContext context) => ClipRRect( - borderRadius: borderRadius ?? BorderRadius.zero, + borderRadius: borderRadius, child: ColorFiltered( colorFilter: ColorFilter.mode(color, BlendMode.dstATop), child: SizedBox.fromSize( diff --git a/packages/neon/neon/lib/src/widgets/list_view.dart b/packages/neon/neon/lib/src/widgets/list_view.dart index 65adec54..62025757 100644 --- a/packages/neon/neon/lib/src/widgets/list_view.dart +++ b/packages/neon/neon/lib/src/widgets/list_view.dart @@ -41,7 +41,6 @@ class NeonListView extends StatelessWidget { ), Expanded( child: Scrollbar( - interactive: true, child: ListView( primary: true, key: scrollKey != null ? PageStorageKey(scrollKey!) : null, diff --git a/packages/neon/neon/lib/src/widgets/user_avatar.dart b/packages/neon/neon/lib/src/widgets/user_avatar.dart index 55285bb9..69964f19 100644 --- a/packages/neon/neon/lib/src/widgets/user_avatar.dart +++ b/packages/neon/neon/lib/src/widgets/user_avatar.dart @@ -8,6 +8,7 @@ import 'package:neon/src/bloc/result.dart'; import 'package:neon/src/bloc/result_builder.dart'; import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/models/account.dart'; +import 'package:neon/src/theme/colors.dart'; import 'package:neon/src/widgets/cached_image.dart'; import 'package:nextcloud/nextcloud.dart'; import 'package:provider/provider.dart'; @@ -119,7 +120,7 @@ class _UserAvatarState extends State { } else if (result.hasData) { decoration = BoxDecoration( shape: BoxShape.circle, - color: _userStatusToColor(result.data!.status), + color: result.data!.status.color, ); } @@ -136,11 +137,4 @@ class _UserAvatarState extends State { ), ); } - - Color? _userStatusToColor(final UserStatusType userStatusType) => switch (userStatusType) { - UserStatusType.online => const Color(0xFF49B382), - UserStatusType.away => const Color(0xFFF4A331), - UserStatusType.dnd => const Color(0xFFED484C), - _ => null, - }; } diff --git a/packages/neon/neon/lib/theme.dart b/packages/neon/neon/lib/theme.dart new file mode 100644 index 00000000..8b4281ab --- /dev/null +++ b/packages/neon/neon/lib/theme.dart @@ -0,0 +1,5 @@ +export 'src/theme/branding.dart'; +export 'src/theme/color_scheme.dart'; +export 'src/theme/colors.dart'; +export 'src/theme/dialog.dart'; +export 'src/theme/neon.dart'; diff --git a/packages/neon/neon_files/lib/neon_files.dart b/packages/neon/neon_files/lib/neon_files.dart index 68280d18..246b507a 100644 --- a/packages/neon/neon_files/lib/neon_files.dart +++ b/packages/neon/neon_files/lib/neon_files.dart @@ -17,6 +17,7 @@ import 'package:neon/models.dart'; import 'package:neon/platform.dart'; import 'package:neon/settings.dart'; import 'package:neon/sort_box.dart'; +import 'package:neon/theme.dart'; import 'package:neon/utils.dart'; import 'package:neon/widgets.dart'; import 'package:neon_files/l10n/localizations.dart'; diff --git a/packages/neon/neon_files/lib/pages/details.dart b/packages/neon/neon_files/lib/pages/details.dart index eeb44e80..f51f87b6 100644 --- a/packages/neon/neon_files/lib/pages/details.dart +++ b/packages/neon/neon_files/lib/pages/details.dart @@ -17,7 +17,6 @@ class FilesDetailsPage extends StatelessWidget { title: Text(details.name), ), body: Scrollbar( - interactive: true, child: ListView( primary: true, children: [ diff --git a/packages/neon/neon_files/lib/widgets/browser_view.dart b/packages/neon/neon_files/lib/widgets/browser_view.dart index 5a360487..b6e61d00 100644 --- a/packages/neon/neon_files/lib/widgets/browser_view.dart +++ b/packages/neon/neon_files/lib/widgets/browser_view.dart @@ -283,7 +283,7 @@ class _FilesBrowserViewState extends State { child: Icon( Icons.star, size: 14, - color: Colors.yellow, + color: NcColors.starredColor, ), ), ], diff --git a/packages/neon/neon_files/lib/widgets/file_preview.dart b/packages/neon/neon_files/lib/widgets/file_preview.dart index 7a610e78..33fbda24 100644 --- a/packages/neon/neon_files/lib/widgets/file_preview.dart +++ b/packages/neon/neon_files/lib/widgets/file_preview.dart @@ -49,7 +49,6 @@ class FilePreview extends StatelessWidget { ); if (withBackground) { return NeonImageWrapper( - color: Colors.white, borderRadius: borderRadius, child: child, ); diff --git a/packages/neon/neon_news/lib/widgets/feed_icon.dart b/packages/neon/neon_news/lib/widgets/feed_icon.dart index cc0b78d1..ea336f30 100644 --- a/packages/neon/neon_news/lib/widgets/feed_icon.dart +++ b/packages/neon/neon_news/lib/widgets/feed_icon.dart @@ -17,7 +17,6 @@ class NewsFeedIcon extends StatelessWidget { final faviconLink = feed.faviconLink; return NeonImageWrapper( - color: Colors.white, size: Size.square(size), borderRadius: borderRadius, child: faviconLink != null && faviconLink.isNotEmpty