diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..9cc7ae9 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,78 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +env: + FLUTTER_VERSION: 3.16.5 + +jobs: + analyze: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{env.FLUTTER_VERSION}} + - run: flutter pub get + - run: flutter analyze + + format: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{env.FLUTTER_VERSION}} + - run: dart format --set-exit-if-changed . + + linux: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{env.FLUTTER_VERSION}} + - run: sudo apt update + - run: sudo apt install -y clang cmake curl libgtk-3-dev ninja-build pkg-config unzip + env: + DEBIAN_FRONTEND: noninteractive + - run: flutter pub get + - run: flutter build linux -v + working-directory: example + + pub: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{env.FLUTTER_VERSION}} + - run: flutter pub get + - run: flutter pub publish --dry-run + + test: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{env.FLUTTER_VERSION}} + - run: flutter test + + web: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{env.FLUTTER_VERSION}} + - run: flutter pub get + - run: flutter build web -v + working-directory: example \ No newline at end of file diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..e7dd969 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,25 @@ +name: Publish to GitHub Pages + +on: + push: + branches: + - main + +env: + FLUTTER_VERSION: 3.16.9 + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: subosito/flutter-action@v1 + with: + channel: 'stable' + flutter-version: ${{env.FLUTTER_VERSION}} + - uses: bluefireteam/flutter-gh-pages@v7 + with: + workingDir: example + baseHref: /yaru.dart/ + webRenderer: canvaskit + customArgs: --no-tree-shake-icons \ No newline at end of file diff --git a/example/lib/home.dart b/example/lib/home.dart index 4b3ea0c..3559323 100644 --- a/example/lib/home.dart +++ b/example/lib/home.dart @@ -1,2 +1 @@ -export 'src/home/color_disk.dart'; export 'src/home/home_page.dart'; diff --git a/example/lib/src/colors/colors_view.dart b/example/lib/src/colors/colors_view.dart index 6a2732b..663ff97 100644 --- a/example/lib/src/colors/colors_view.dart +++ b/example/lib/src/colors/colors_view.dart @@ -103,7 +103,7 @@ class ColorsView extends StatelessWidget { Text( colorName, style: TextStyle( - color: foregroundColor ?? foregroundColor?.contrastColor, + color: backgroundColor.contrastColor, fontSize: 9, ), ), @@ -113,7 +113,7 @@ class ColorsView extends StatelessWidget { .replaceAll('Color(0x', '#') .replaceAll(')', ''), style: TextStyle( - color: foregroundColor ?? backgroundColor.contrastColor, + color: backgroundColor.contrastColor, fontSize: 7, ), ), @@ -191,7 +191,10 @@ Map _getBaseColors(ThemeData theme) { Map getPrimaryColors(ThemeData theme) { return Map.fromEntries( Colors.accents.map( - (e) => MapEntry(e.toString(), (e, theme.colorScheme.onPrimary)), + (e) => MapEntry( + '#${e.value.toHex().toString()}', + (e, theme.colorScheme.onPrimary), + ), ), ); } diff --git a/example/lib/src/controls/chips.dart b/example/lib/src/controls/chips.dart index cb529d7..33b8ed6 100644 --- a/example/lib/src/controls/chips.dart +++ b/example/lib/src/controls/chips.dart @@ -13,7 +13,6 @@ class Chips extends StatelessWidget { children: [ const Chip(label: Text('Chip')), Chip( - deleteIcon: const Icon(Icons.close), label: const Text('Deletable Chip'), onDeleted: () {}, ), diff --git a/example/lib/src/home/color_disk.dart b/example/lib/src/home/color_disk.dart deleted file mode 100644 index b97c222..0000000 --- a/example/lib/src/home/color_disk.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; - -class ColorDisk extends StatelessWidget { - const ColorDisk({ - super.key, - this.onPressed, - required this.color, - required this.selected, - }); - - final VoidCallback? onPressed; - final Color color; - final bool selected; - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 42, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: TextButton( - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - shape: CircleBorder( - side: BorderSide( - color: selected - ? Theme.of(context).primaryColor - : Colors.transparent, - ), - ), - ), - onPressed: onPressed, - child: SizedBox( - height: 20, - width: 20, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(100), - color: color, - ), - ), - ), - ), - ), - ); - } -} diff --git a/example/lib/src/home/home_page.dart b/example/lib/src/home/home_page.dart index ad83067..98710f9 100644 --- a/example/lib/src/home/home_page.dart +++ b/example/lib/src/home/home_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:phoenix_theme/phoenix_theme.dart'; import '../../colors.dart'; import '../../containers.dart'; @@ -68,85 +69,7 @@ class HomePageState extends State { }, items: _items, ), - appBar: AppBar( - backgroundColor: Colors.transparent, - leading: Center( - child: IconButton( - onPressed: () => themePageScaffoldKey.currentState?.openDrawer(), - icon: const Icon(Icons.menu), - ), - ), - title: const Text('Phoenix Theme'), - actions: [ - IconButton( - onPressed: () => showSnack(context), - icon: const Icon(Icons.add), - ), - ValueListenableBuilder( - valueListenable: themeModeNotifier, - builder: (context, themeMode, widget) { - return PopupMenuButton( - onSelected: (v) => themeModeNotifier.value = v, - itemBuilder: (c) => ThemeMode.values - .map( - (e) => PopupMenuItem( - value: e, - child: Icon( - [ - Icons.lightbulb, - Icons.light_mode, - Icons.dark_mode, - ].elementAt(ThemeMode.values.indexOf(e)), - ), - ), - ) - .toList(), - icon: Icon( - [ - Icons.lightbulb, - Icons.light_mode, - Icons.dark_mode, - ].elementAt(ThemeMode.values.indexOf(themeMode)), - ), - ); - }, - ), - ValueListenableBuilder( - valueListenable: colorNotifier, - builder: (context, color, widget) { - return PopupMenuButton( - onSelected: (v) => colorNotifier.value = v, - itemBuilder: (c) => Colors.accents - .map( - (e) => PopupMenuItem( - value: e, - child: Container( - height: 30, - width: 30, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: e, - ), - ), - ), - ) - .toList(), - icon: Container( - height: 30, - width: 30, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: color, - ), - ), - ); - }, - ), - const SizedBox( - width: 10, - ), - ], - ), + appBar: const _AppBar(), body: LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth > 800) { @@ -208,6 +131,107 @@ class HomePageState extends State { } } +class _AppBar extends StatelessWidget implements PreferredSizeWidget { + const _AppBar(); + + @override + Widget build(BuildContext context) { + final show = context.mq.size.width > 800; + return Row( + children: [ + if (show) SizedBox(width: 80, child: AppBar()), + if (show) + const SizedBox( + height: kToolbarHeight, + child: VerticalDivider(), + ), + Expanded( + child: AppBar( + automaticallyImplyLeading: false, + title: const Text('Phoenix Theme'), + actions: [ + IconButton( + onPressed: () => showSnack(context), + icon: const Icon(Icons.add), + ), + ValueListenableBuilder( + valueListenable: themeModeNotifier, + builder: (context, themeMode, widget) { + return IconButton( + tooltip: '', + onPressed: () { + themeModeNotifier.value = themeMode == ThemeMode.dark + ? ThemeMode.system + : ThemeMode.values.elementAt( + ThemeMode.values.indexOf(themeMode) + 1, + ); + }, + icon: Icon( + [ + Icons.theater_comedy_rounded, + Icons.light_mode, + Icons.dark_mode, + ].elementAt(ThemeMode.values.indexOf(themeMode)), + ), + ); + }, + ), + ValueListenableBuilder( + valueListenable: colorNotifier, + builder: (context, color, widget) { + return PopupMenuButton( + onSelected: (v) => colorNotifier.value = v, + itemBuilder: (c) => Colors.accents + .map( + (e) => PopupMenuItem( + value: e, + child: Container( + height: 25, + width: 25, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: e, + border: Border.all( + color: e.scale( + lightness: + context.theme.isLight ? -0.5 : 0.5, + ), + ), + ), + ), + ), + ) + .toList(), + icon: Container( + height: 25, + width: 25, + decoration: BoxDecoration( + border: Border.all( + color: color.scale( + lightness: context.theme.isLight ? -0.5 : 0.5, + ), + ), + shape: BoxShape.circle, + color: color, + ), + ), + ); + }, + ), + const SizedBox( + width: 10, + ), + ], + ), + ), + ], + ); + } + + @override + Size get preferredSize => const Size(0, kToolbarHeight); +} + class _Drawer extends StatelessWidget { const _Drawer({ required this.items, diff --git a/lib/phoenix_theme.dart b/lib/phoenix_theme.dart index 64d0f97..e82c1de 100644 --- a/lib/phoenix_theme.dart +++ b/lib/phoenix_theme.dart @@ -1,4 +1,6 @@ library phoenix_theme; +export 'src/build_context_x.dart'; export 'src/color_x.dart'; export 'src/theme.dart'; +export 'src/theme_data_x.dart'; diff --git a/lib/src/build_context_x.dart b/lib/src/build_context_x.dart new file mode 100644 index 0000000..8046362 --- /dev/null +++ b/lib/src/build_context_x.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +extension BuildContextX on BuildContext { + ThemeData get theme => Theme.of(this); + MediaQueryData get mq => MediaQuery.of(this); +} diff --git a/lib/src/color_x.dart b/lib/src/color_x.dart index f40e673..a8d65af 100644 --- a/lib/src/color_x.dart +++ b/lib/src/color_x.dart @@ -152,6 +152,6 @@ extension ColorX on Color { : Colors.white; } -extension on int { +extension IntX on int { String toHex() => toRadixString(16).padLeft(2, '0'); } diff --git a/lib/src/theme.dart b/lib/src/theme.dart index 7f9d8ff..e47706d 100644 --- a/lib/src/theme.dart +++ b/lib/src/theme.dart @@ -1,24 +1,58 @@ import 'package:flutter/material.dart'; -import '../phoenix_theme.dart'; +import 'color_x.dart'; +import 'theme_data_x.dart'; + +const lightBase = Colors.white; +final darkBase = Colors.black.scale(lightness: 0.13); +final darkMenuBase = Colors.black.scale(lightness: 0.1); +const kContainerRadius = 10.0; +const kButtonRadius = 6.0; +const kMenuRadius = 8.0; ThemePair phoenixTheme({ required Color color, }) { + final darkScheme = _darkScheme(color); + final lightScheme = _lightScheme(color); + return ( lightTheme: ThemeData( - colorScheme: _lightScheme(color), + colorScheme: lightScheme, splashFactory: NoSplash.splashFactory, + dividerColor: _dividerColor(lightScheme), + ).copyWith( + menuTheme: _menuTheme(lightScheme), + popupMenuTheme: _popupMenuTheme(lightScheme), + dialogTheme: _dialogTheme(lightScheme), + dropdownMenuTheme: _dropdownMenuTheme(lightScheme), + sliderTheme: _sliderTheme(lightScheme), + dividerTheme: _dividerTheme(lightScheme), + progressIndicatorTheme: _progressIndicatorTheme(lightScheme), + switchTheme: _switchTheme(lightScheme), + navigationRailTheme: _naviRailTheme(lightScheme), + navigationBarTheme: _naviBarTheme(lightScheme), ), darkTheme: ThemeData( - colorScheme: _darkScheme(color), + colorScheme: darkScheme, splashFactory: NoSplash.splashFactory, + dividerColor: _dividerColor(darkScheme), + ).copyWith( + menuTheme: _menuTheme(darkScheme), + popupMenuTheme: _popupMenuTheme(darkScheme), + dialogTheme: _dialogTheme(darkScheme), + dropdownMenuTheme: _dropdownMenuTheme(darkScheme), + sliderTheme: _sliderTheme(darkScheme), + dividerTheme: _dividerTheme(darkScheme), + progressIndicatorTheme: _progressIndicatorTheme(darkScheme), + switchTheme: _switchTheme(darkScheme), + navigationRailTheme: _naviRailTheme(darkScheme), + navigationBarTheme: _naviBarTheme(darkScheme), ) ); } ColorScheme _darkScheme(Color color) { - final darkBase = Colors.black.scale(lightness: 0.2); return ColorScheme.fromSeed( seedColor: color, brightness: Brightness.dark, @@ -26,16 +60,15 @@ ColorScheme _darkScheme(Color color) { surfaceTint: darkBase, background: darkBase, surface: darkBase.scale( - lightness: 0.05, + lightness: 0.04, ), outline: darkBase.scale( - lightness: 0.2, + lightness: 0.28, ), ); } ColorScheme _lightScheme(Color color) { - const lightBase = Colors.white; return ColorScheme.fromSeed( seedColor: color, brightness: Brightness.light, @@ -43,12 +76,173 @@ ColorScheme _lightScheme(Color color) { surfaceTint: lightBase, background: lightBase, surface: lightBase.scale( - lightness: -0.05, + lightness: -0.04, ), outline: Colors.white.scale( - lightness: -0.2, + lightness: -0.3, + ), + ); +} + +DividerThemeData _dividerTheme(ColorScheme colorScheme) => DividerThemeData( + color: _dividerColor(colorScheme), + space: 1.0, + thickness: 1.0, + ); + +Color _dividerColor(ColorScheme colorScheme) { + return colorScheme.outline.scale(lightness: colorScheme.isLight ? 0.3 : -0.3); +} + +DialogTheme _dialogTheme(ColorScheme colorScheme) { + final bgColor = colorScheme.isLight ? lightBase : darkMenuBase; + return DialogTheme( + backgroundColor: bgColor, + surfaceTintColor: bgColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(kContainerRadius), + side: colorScheme.isLight + ? BorderSide.none + : BorderSide( + color: Colors.white.withOpacity(0.2), + ), + ), + ); +} + +PopupMenuThemeData _popupMenuTheme(ColorScheme colorScheme) { + final bgColor = colorScheme.isLight ? lightBase : darkMenuBase; + return PopupMenuThemeData( + color: bgColor, + surfaceTintColor: bgColor, + shape: OutlineInputBorder( + borderRadius: BorderRadius.circular(kContainerRadius), + borderSide: BorderSide( + color: colorScheme.onSurface.withOpacity( + colorScheme.isLight ? 0.3 : 0.2, + ), + width: 1, + ), ), ); } +MenuStyle _menuStyle(ColorScheme colorScheme) { + final bgColor = colorScheme.isLight ? lightBase : darkMenuBase; + + return MenuStyle( + surfaceTintColor: MaterialStateColor.resolveWith((states) => bgColor), + shape: MaterialStateProperty.resolveWith( + (states) => RoundedRectangleBorder( + side: BorderSide( + color: colorScheme.onSurface.withOpacity( + colorScheme.isLight ? 0.3 : 0.2, + ), + width: 1, + ), + borderRadius: BorderRadius.circular(kMenuRadius), + ), + ), + side: MaterialStateBorderSide.resolveWith( + (states) => BorderSide( + color: colorScheme.onSurface.withOpacity( + colorScheme.isLight ? 0.3 : 0.2, + ), + width: 1, + ), + ), + elevation: MaterialStateProperty.resolveWith((states) => 1), + backgroundColor: MaterialStateProperty.resolveWith((states) => bgColor), + ); +} + +MenuThemeData _menuTheme(ColorScheme colorScheme) { + return MenuThemeData( + style: _menuStyle(colorScheme), + ); +} + +DropdownMenuThemeData _dropdownMenuTheme(ColorScheme colorScheme) { + return DropdownMenuThemeData( + menuStyle: _menuStyle(colorScheme), + ); +} + +SliderThemeData _sliderTheme(ColorScheme colorScheme) { + return SliderThemeData( + thumbColor: Colors.white, + overlayShape: const RoundSliderOverlayShape( + overlayRadius: 13, + ), + overlayColor: + colorScheme.primary.withOpacity(colorScheme.isLight ? 0.4 : 0.7), + thumbShape: const RoundSliderThumbShape(elevation: 3.0), + inactiveTrackColor: colorScheme.onSurface.withOpacity(0.3), + ); +} + +SwitchThemeData _switchTheme(ColorScheme colorScheme) { + return SwitchThemeData( + trackOutlineColor: MaterialStateColor.resolveWith( + (states) => Colors.transparent, + ), + thumbColor: MaterialStateProperty.resolveWith( + (states) => _getSwitchThumbColor(states, colorScheme), + ), + trackColor: MaterialStateProperty.resolveWith( + (states) => _getSwitchTrackColor(states, colorScheme), + ), + ); +} + +Color _getSwitchThumbColor(Set states, ColorScheme colorScheme) { + if (states.contains(MaterialState.disabled)) { + if (states.contains(MaterialState.selected)) { + return colorScheme.onSurface.withOpacity(0.5); + } + return colorScheme.onSurface.withOpacity(0.5); + } else { + return colorScheme.onPrimary; + } +} + +Color _getSwitchTrackColor(Set states, ColorScheme colorScheme) { + final uncheckedColor = colorScheme.primary.withOpacity(.25); + final disabledUncheckedColor = colorScheme.onSurface.withOpacity(.15); + final disabledCheckedColor = colorScheme.onSurface.withOpacity(.18); + + if (states.contains(MaterialState.disabled)) { + if (states.contains(MaterialState.selected)) { + return disabledCheckedColor; + } + return disabledUncheckedColor; + } else { + if (states.contains(MaterialState.selected)) { + return colorScheme.primary; + } else { + return uncheckedColor; + } + } +} + +ProgressIndicatorThemeData _progressIndicatorTheme(ColorScheme colorScheme) { + return ProgressIndicatorThemeData( + circularTrackColor: colorScheme.primary.withOpacity(0.4), + linearTrackColor: colorScheme.primary.withOpacity(0.4), + ); +} + +NavigationRailThemeData _naviRailTheme(ColorScheme colorScheme) { + return NavigationRailThemeData( + indicatorColor: _indicatorColor(colorScheme), + ); +} + +NavigationBarThemeData _naviBarTheme(ColorScheme colorScheme) { + return NavigationBarThemeData(indicatorColor: _indicatorColor(colorScheme)); +} + +Color _indicatorColor(ColorScheme colorScheme) => + _dividerColor(colorScheme).withOpacity(0.8); + typedef ThemePair = ({ThemeData lightTheme, ThemeData darkTheme}); diff --git a/lib/src/theme_data_x.dart b/lib/src/theme_data_x.dart new file mode 100644 index 0000000..4cb002d --- /dev/null +++ b/lib/src/theme_data_x.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +extension ThemeDataX on ThemeData { + bool get isLight => colorScheme.isLight; +} + +extension ColorSchemeX on ColorScheme { + bool get isLight => brightness == Brightness.light; +}