From 01ec1134cbde4ce4e0a069056d466e49d2ba10f3 Mon Sep 17 00:00:00 2001 From: Davide Bianco Date: Sat, 11 Mar 2023 19:33:47 +0100 Subject: [PATCH] files: Yarufy the app --- lib/backend/folder_provider.dart | 19 +- lib/backend/utils.dart | 7 +- lib/main.dart | 180 ++++++++++-------- lib/widgets/breadcrumbs_bar.dart | 95 ++++----- lib/widgets/context_menu.dart | 36 ++-- lib/widgets/drive_list.dart | 70 ++++--- lib/widgets/entity_context_menu.dart | 7 +- lib/widgets/grid.dart | 39 ++-- lib/widgets/separated_flex.dart | 56 ++++++ lib/widgets/side_pane.dart | 12 +- lib/widgets/tab_strip.dart | 31 ++- lib/widgets/table.dart | 5 +- lib/widgets/workspace.dart | 67 ++++--- linux/flutter/generated_plugin_registrant.cc | 20 ++ linux/flutter/generated_plugins.cmake | 5 + linux/my_application.cc | 4 +- pubspec.lock | 114 ++++++++++- pubspec.yaml | 5 + .../flutter/generated_plugin_registrant.cc | 6 + windows/flutter/generated_plugins.cmake | 2 + 20 files changed, 512 insertions(+), 268 deletions(-) create mode 100644 lib/widgets/separated_flex.dart diff --git a/lib/backend/folder_provider.dart b/lib/backend/folder_provider.dart index cdd02f6..9bc22ce 100644 --- a/lib/backend/folder_provider.dart +++ b/lib/backend/folder_provider.dart @@ -21,6 +21,7 @@ import 'package:files/backend/utils.dart'; import 'package:flutter/material.dart'; import 'package:windows_path_provider/windows_path_provider.dart'; import 'package:xdg_directories/xdg_directories.dart'; +import 'package:yaru_icons/yaru_icons.dart'; class FolderProvider { final List _folders; @@ -129,15 +130,15 @@ class SideDestination { } const Map _icons = { - FolderType.home: Icons.home_filled, - FolderType.desktop: Icons.desktop_windows, - FolderType.documents: Icons.note_outlined, - FolderType.pictures: Icons.photo_library_outlined, - FolderType.download: Icons.file_download, - FolderType.videos: Icons.videocam_outlined, - FolderType.music: Icons.music_note_outlined, - FolderType.publicShare: Icons.public_outlined, - FolderType.templates: Icons.file_copy_outlined, + FolderType.home: YaruIcons.home, + FolderType.desktop: YaruIcons.desktop, + FolderType.documents: YaruIcons.document, + FolderType.pictures: YaruIcons.image, + FolderType.download: YaruIcons.download, + FolderType.videos: YaruIcons.video, + FolderType.music: YaruIcons.music_note, + FolderType.publicShare: YaruIcons.globe, + FolderType.templates: YaruIcons.document_new, }; String windowsFolderToString(WindowsFolder folder) { diff --git a/lib/backend/utils.dart b/lib/backend/utils.dart index 29b346d..f9258ef 100644 --- a/lib/backend/utils.dart +++ b/lib/backend/utils.dart @@ -6,6 +6,7 @@ import 'package:files/backend/providers.dart'; import 'package:flutter/material.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:mime/mime.dart'; +import 'package:yaru_icons/yaru_icons.dart'; class Utils { Utils._(); @@ -52,10 +53,10 @@ class Utils { final String? mime = lookupMimeType(path); if (mime != null) { - return iconsPerMime[mime] ?? Icons.insert_drive_file_outlined; + return iconsPerMime[mime] ?? YaruIcons.document_filled; } - return Icons.insert_drive_file_outlined; + return YaruIcons.document_filled; } static IconData iconForFolder(String path) { @@ -64,7 +65,7 @@ class Utils { ? folderProvider.getIconForType(builtinFolder.type) : null; - return builtinFolderIcon ?? Icons.folder; + return builtinFolderIcon ?? YaruIcons.folder_filled; } static String getEntityName(String path) { diff --git a/lib/main.dart b/lib/main.dart index f8e5384..c632607 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,59 +20,49 @@ import 'package:files/widgets/side_pane.dart'; import 'package:files/widgets/tab_strip.dart'; import 'package:files/widgets/workspace.dart'; import 'package:flutter/material.dart'; +import 'package:yaru/yaru.dart'; +import 'package:yaru_widgets/yaru_widgets.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); + await YaruWindowTitleBar.ensureInitialized(); + await YaruWindow.ensureInitialized(); await initProviders(); await driveProvider.init(); runApp(const Files()); } +ThemeData? _applyThemeValues(ThemeData? theme) { + return theme?.copyWith( + outlinedButtonTheme: OutlinedButtonThemeData( + style: theme.outlinedButtonTheme.style?.merge( + OutlinedButton.styleFrom( + backgroundColor: theme.colorScheme.surfaceVariant, + ), + ), + ), + ); +} + class Files extends StatelessWidget { const Files({super.key}); @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Files', - theme: ThemeData( - colorScheme: const ColorScheme( - primary: Colors.deepOrange, - secondary: Colors.deepOrange, - background: Color(0xFF161616), - surface: Color(0xFF212121), - error: Colors.red, - onPrimary: Colors.white, - onSecondary: Colors.white, - onBackground: Colors.white, - onSurface: Colors.white, - onError: Colors.white, - brightness: Brightness.dark, - ), - scrollbarTheme: ScrollbarThemeData( - thumbVisibility: const MaterialStatePropertyAll(true), - trackVisibility: MaterialStateProperty.resolveWith( - (states) => states.contains(MaterialState.hovered), + return YaruTheme( + builder: (context, value, child) { + return MaterialApp( + title: 'Files', + theme: _applyThemeValues(value.theme), + darkTheme: _applyThemeValues(value.darkTheme), + scrollBehavior: const MaterialScrollBehavior().copyWith( + scrollbars: false, ), - trackBorderColor: MaterialStateProperty.all(Colors.transparent), - crossAxisMargin: 0, - mainAxisMargin: 0, - radius: Radius.zero, - ), - menuTheme: const MenuThemeData( - style: MenuStyle( - padding: MaterialStatePropertyAll( - EdgeInsets.symmetric(vertical: 16), - ), - ), - ), - ), - scrollBehavior: const MaterialScrollBehavior().copyWith( - scrollbars: false, - ), - debugShowCheckedModeBanner: false, - home: const FilesHome(), + debugShowCheckedModeBanner: false, + home: const FilesHome(), + ); + }, ); } } @@ -94,47 +84,81 @@ class _FilesHomeState extends State { @override Widget build(BuildContext context) { - return Row( - children: [ - SidePane( - destinations: folderProvider.destinations, - workspace: workspaces[currentWorkspace], - onNewTab: (String tabPath) { - workspaces.add(WorkspaceController(initialDir: tabPath)); - currentWorkspace = workspaces.length - 1; - setState(() {}); - }, - ), - Expanded( - child: Material( - color: Theme.of(context).colorScheme.background, - child: Column( - children: [ - SizedBox( - height: 56, - child: TabStrip( - tabs: workspaces, - selectedTab: currentWorkspace, - allowClosing: workspaces.length > 1, - onTabChanged: (index) => - setState(() => currentWorkspace = index), - onTabClosed: (index) { - workspaces.removeAt(index); - if (index < workspaces.length) { - currentWorkspace = index; - } else if (index - 1 >= 0) { - currentWorkspace = index - 1; - } - setState(() {}); - }, - onNewTab: () { - workspaces - .add(WorkspaceController(initialDir: currentDir)); - currentWorkspace = workspaces.length - 1; - setState(() {}); + return Material( + color: Theme.of(context).colorScheme.background, + child: Column( + children: [ + GestureDetector( + onPanStart: (details) => YaruWindow.drag(context), + child: SizedBox( + height: 56, + child: TabStrip( + tabs: workspaces, + selectedTab: currentWorkspace, + allowClosing: workspaces.length > 1, + onTabChanged: (index) => + setState(() => currentWorkspace = index), + onTabClosed: (index) { + workspaces.removeAt(index); + if (index < workspaces.length) { + currentWorkspace = index; + } else if (index - 1 >= 0) { + currentWorkspace = index - 1; + } + setState(() {}); + }, + onNewTab: () { + workspaces.add(WorkspaceController(initialDir: currentDir)); + currentWorkspace = workspaces.length - 1; + setState(() {}); + }, + trailing: [ + const SizedBox(width: 16), + YaruWindowControl( + type: YaruWindowControlType.minimize, + onTap: () => YaruWindow.minimize(context), + ), + const SizedBox(width: 8), + StreamBuilder( + stream: YaruWindow.states(context), + builder: (context, snapshot) { + final bool maximized = + snapshot.data?.isMaximized ?? false; + + return YaruWindowControl( + type: maximized + ? YaruWindowControlType.restore + : YaruWindowControlType.maximize, + onTap: () => maximized + ? YaruWindow.restore(context) + : YaruWindow.maximize(context), + ); }, ), + const SizedBox(width: 8), + YaruWindowControl( + type: YaruWindowControlType.close, + onTap: () => YaruWindow.close(context), + ), + const SizedBox(width: 16), + ], + ), + ), + ), + const Divider(thickness: 1, height: 1), + Expanded( + child: Row( + children: [ + SidePane( + destinations: folderProvider.destinations, + workspace: workspaces[currentWorkspace], + onNewTab: (String tabPath) { + workspaces.add(WorkspaceController(initialDir: tabPath)); + currentWorkspace = workspaces.length - 1; + setState(() {}); + }, ), + const VerticalDivider(thickness: 1, width: 1), Expanded( child: FilesWorkspace( key: ValueKey(currentWorkspace), @@ -144,8 +168,8 @@ class _FilesHomeState extends State { ], ), ), - ), - ], + ], + ), ); } } diff --git a/lib/widgets/breadcrumbs_bar.dart b/lib/widgets/breadcrumbs_bar.dart index 6a1c7cd..ceb901a 100644 --- a/lib/widgets/breadcrumbs_bar.dart +++ b/lib/widgets/breadcrumbs_bar.dart @@ -65,7 +65,7 @@ class _BreadcrumbsBarState extends State { Widget build(BuildContext context) { return SizedBox.expand( child: Material( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, child: SizedBox.expand( child: Row( children: [ @@ -75,9 +75,14 @@ class _BreadcrumbsBarState extends State { child: Padding( padding: const EdgeInsets.all(8.0), child: Material( - color: Theme.of(context).colorScheme.surface, + color: Theme.of(context).colorScheme.surfaceVariant, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), + borderRadius: BorderRadius.circular(6), + side: !focusNode.hasFocus + ? BorderSide( + color: Theme.of(context).colorScheme.outline, + ) + : BorderSide.none, ), clipBehavior: Clip.antiAlias, child: _LoadingIndicator( @@ -110,8 +115,8 @@ class _BreadcrumbsBarState extends State { return TextField( decoration: const InputDecoration( border: InputBorder.none, - contentPadding: EdgeInsets.symmetric(horizontal: 8), - isCollapsed: true, + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 13), + isDense: true, ), focusNode: focusNode, controller: controller, @@ -150,7 +155,7 @@ class _BreadcrumbsBarState extends State { ); } - return ListView.separated( + return ListView.builder( scrollDirection: Axis.horizontal, itemBuilder: (context, index) { final bool isInsideBuiltin = builtinFolder != null && @@ -174,7 +179,6 @@ class _BreadcrumbsBarState extends State { ); }, itemCount: actualParts.length, - separatorBuilder: (context, index) => const VerticalDivider(width: 2), ); } } @@ -203,10 +207,10 @@ class _BreadcrumbChip extends StatelessWidget { children: [ Container( alignment: Alignment.center, - padding: const EdgeInsetsDirectional.only(start: 12, end: 4), + padding: const EdgeInsets.symmetric(horizontal: 12), child: childOverride ?? Text(path.integralParts.last), ), - const Icon(Icons.chevron_right, size: 16), + const VerticalDivider(width: 2, thickness: 2), ], ), onTap: () => onTap?.call(path.toPath()), @@ -250,6 +254,13 @@ class _LoadingIndicatorState extends State<_LoadingIndicator> ); } + @override + void dispose() { + fadeController.dispose(); + progressController.dispose(); + super.dispose(); + } + @override void didUpdateWidget(covariant _LoadingIndicator old) { super.didUpdateWidget(old); @@ -260,7 +271,7 @@ class _LoadingIndicatorState extends State<_LoadingIndicator> Future _updateController(_LoadingIndicator old) async { if (widget.progress != old.progress) { if (widget.progress != null && old.progress == null) { - fadeController.forward(); + fadeController.value = 1; progressController.animateTo(widget.progress!); } else if (widget.progress == null && old.progress != null) { await fadeController.reverse(); @@ -280,55 +291,27 @@ class _LoadingIndicatorState extends State<_LoadingIndicator> return AnimatedBuilder( animation: Listenable.merge([progressController, fadeController]), builder: (context, child) { - return CustomPaint( - painter: _LoadingIndicatorPainter( - progress: progressController.value, - opacity: fadeController.value, - color: Theme.of(context).colorScheme.primary, - ), - child: child, + return Stack( + children: [ + child!, + Positioned.directional( + textDirection: Directionality.of(context), + top: 12, + bottom: 12, + end: 12, + width: 16, + child: FadeTransition( + opacity: fadeController, + child: CircularProgressIndicator( + value: progressController.value, + strokeWidth: 2, + ), + ), + ), + ], ); }, child: widget.child, ); } } - -class _LoadingIndicatorPainter extends CustomPainter { - final double progress; - final double opacity; - final Color color; - - const _LoadingIndicatorPainter({ - required this.progress, - required this.opacity, - required this.color, - }); - - @override - void paint(Canvas canvas, Size size) { - final Rect baseRect = Offset.zero & size; - final Rect drawingRect = Offset.zero & - Size( - size.width * progress, - size.height, - ); - - canvas.drawRect( - baseRect, - Paint()..color = color.withOpacity(opacity * 0.2), - ); - - canvas.drawRect( - drawingRect, - Paint()..color = color.withOpacity(opacity * 0.6), - ); - } - - @override - bool shouldRepaint(covariant _LoadingIndicatorPainter old) { - return progress != old.progress || - opacity != old.opacity || - color.value != old.color.value; - } -} diff --git a/lib/widgets/context_menu.dart b/lib/widgets/context_menu.dart index dca7359..90486b2 100644 --- a/lib/widgets/context_menu.dart +++ b/lib/widgets/context_menu.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:yaru_icons/yaru_icons.dart'; +import 'package:yaru_widgets/yaru_widgets.dart'; class ContextMenu extends StatefulWidget { final List entries; @@ -93,7 +95,7 @@ class SubmenuMenuItem extends BaseContextMenuItem { @override Widget? buildTrailing(BuildContext context) { - return const Icon(Icons.chevron_right, size: 16); + return const Icon(YaruIcons.go_next, size: 16); } @override @@ -132,7 +134,7 @@ class ContextMenuItem extends BaseContextMenuItem { return MenuItemButton( leadingIcon: leading != null ? IconTheme.merge( - data: Theme.of(context).iconTheme.copyWith(size: 16), + data: Theme.of(context).iconTheme.copyWith(size: 20), child: leading, ) : null, @@ -181,17 +183,11 @@ class RadioMenuItem extends ContextMenuItem { Widget? buildTrailing(BuildContext context) { return ExcludeFocus( child: IgnorePointer( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: Checkbox.width, - maxWidth: Checkbox.width, - ), - child: Radio( - groupValue: groupValue, - value: value, - onChanged: onChanged, - toggleable: toggleable, - ), + child: YaruRadio( + groupValue: groupValue, + value: value, + onChanged: onChanged, + toggleable: toggleable, ), ), ); @@ -234,16 +230,10 @@ class CheckboxMenuItem extends ContextMenuItem { Widget? buildTrailing(BuildContext context) { return ExcludeFocus( child: IgnorePointer( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: Checkbox.width, - maxWidth: Checkbox.width, - ), - child: Checkbox( - value: value, - onChanged: onChanged, - tristate: tristate, - ), + child: YaruCheckbox( + value: value, + onChanged: onChanged, + tristate: tristate, ), ), ); diff --git a/lib/widgets/drive_list.dart b/lib/widgets/drive_list.dart index e3b43e3..90b7b27 100644 --- a/lib/widgets/drive_list.dart +++ b/lib/widgets/drive_list.dart @@ -2,9 +2,12 @@ import 'dart:async'; import 'dart:convert'; import 'package:files/backend/providers.dart'; +import 'package:files/widgets/separated_flex.dart'; import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; import 'package:udisks/udisks.dart'; +import 'package:yaru_icons/yaru_icons.dart'; +import 'package:yaru_widgets/yaru_widgets.dart'; class DriveList extends StatelessWidget { final ValueChanged? onDriveTap; @@ -16,7 +19,10 @@ class DriveList extends StatelessWidget { return AnimatedBuilder( animation: driveProvider, builder: (context, _) { - return Column( + return SeparatedFlex.vertical( + separator: SizedBox( + height: YaruMasterDetailTheme.of(context).tileSpacing ?? 0, + ), children: driveProvider.blockDevices .where( (e) => @@ -96,35 +102,41 @@ class _DriveTileState extends State<_DriveTile> { ? widget.blockDevice.hintName : null; - return ListTile( - dense: true, - leading: Icon( - widget.blockDevice.drive?.ejectable == true ? Icons.usb : Icons.storage, - size: 20, - ), - title: Text( - idLabel ?? hintName ?? "${filesize(widget.blockDevice.size, 1)} drive", + return ListTileTheme.merge( + contentPadding: const EdgeInsets.only(left: 16, right: 8), + child: YaruMasterTile( + leading: Icon( + widget.blockDevice.drive?.ejectable == true + ? YaruIcons.usb_stick + : YaruIcons.drive_harddisk, + ), + title: Text( + idLabel ?? + hintName ?? + "${filesize(widget.blockDevice.size, 1)} drive", + ), + subtitle: mountPoint != null ? Text(mountPoint) : null, + trailing: mountPoint != null + ? YaruOptionButton( + onPressed: () async { + await widget.blockDevice.filesystem!.unmount(); + setState(() {}); + }, + style: OutlinedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.surface, + ), + child: const Icon(YaruIcons.eject), + ) + : null, + onTap: () async { + if (mountPoint == null) { + mountPoint = await widget.blockDevice.filesystem!.mount(); + setState(() {}); + } + + widget.onTap?.call(mountPoint!); + }, ), - subtitle: mountPoint != null ? Text(mountPoint) : null, - trailing: mountPoint != null - ? IconButton( - onPressed: () async { - await widget.blockDevice.filesystem!.unmount(); - setState(() {}); - }, - icon: const Icon(Icons.eject), - iconSize: 16, - splashRadius: 16, - ) - : null, - onTap: () async { - if (mountPoint == null) { - mountPoint = await widget.blockDevice.filesystem!.mount(); - setState(() {}); - } - - widget.onTap?.call(mountPoint!); - }, ); } } diff --git a/lib/widgets/entity_context_menu.dart b/lib/widgets/entity_context_menu.dart index 1561e11..50cb2de 100644 --- a/lib/widgets/entity_context_menu.dart +++ b/lib/widgets/entity_context_menu.dart @@ -1,6 +1,7 @@ import 'package:files/widgets/context_menu.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:yaru_icons/yaru_icons.dart'; class EntityContextMenu extends StatelessWidget { final Widget child; @@ -36,21 +37,21 @@ class EntityContextMenu extends StatelessWidget { ), const ContextMenuDivider(), ContextMenuItem( - leading: const Icon(Icons.file_copy_outlined), + leading: const Icon(YaruIcons.copy), child: const Text("Copy file"), onTap: onCopy, shortcut: const SingleActivator(LogicalKeyboardKey.keyC, control: true), ), ContextMenuItem( - leading: const Icon(Icons.cut_outlined), + leading: const Icon(YaruIcons.cut), child: const Text("Cut file"), onTap: onCut, shortcut: const SingleActivator(LogicalKeyboardKey.keyX, control: true), ), ContextMenuItem( - leading: const Icon(Icons.paste_outlined), + leading: const Icon(YaruIcons.paste), child: const Text("Paste file"), onTap: onPaste, shortcut: diff --git a/lib/widgets/grid.dart b/lib/widgets/grid.dart index 477e431..0aa3b45 100644 --- a/lib/widgets/grid.dart +++ b/lib/widgets/grid.dart @@ -67,22 +67,19 @@ class _FilesGridState extends State { return Draggable( data: entityInfo.entity, dragAnchorStrategy: (_, __, ___) => const Offset(32, 32), - feedback: DecoratedBox( - decoration: BoxDecoration( - color: - Theme.of(context).colorScheme.onSurface.withOpacity(0.2), - borderRadius: BorderRadius.circular(4), + feedback: Material( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), ), - child: Center( - child: Cell( - name: Utils.getEntityName(entityInfo.entity.path), - icon: entityInfo.isDirectory - ? Utils.iconForFolder(entityInfo.path) - : Utils.iconForPath(entityInfo.path), - iconColor: entityInfo.isDirectory - ? Theme.of(context).colorScheme.primary - : null, - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), + child: Icon( + entityInfo.isDirectory + ? Utils.iconForFolder(entityInfo.path) + : Utils.iconForPath(entityInfo.path), + color: entityInfo.isDirectory + ? Theme.of(context).colorScheme.primary + : null, + size: 64, ), ), child: FileCell( @@ -133,14 +130,14 @@ class FileCell extends StatelessWidget { return true; }, onAccept: (_) => onDropAccept?.call(entity.path), - builder: (context, _, __) => Container( + builder: (context, _, __) => Material( clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: selected - ? Theme.of(context).colorScheme.primary.withOpacity(0.2) - : Colors.transparent, - borderRadius: BorderRadius.circular(4), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), ), + color: selected + ? Theme.of(context).colorScheme.primary.withOpacity(0.2) + : Colors.transparent, child: TimedInkwell( onTap: () => onTap?.call(entity), onDoubleTap: () { diff --git a/lib/widgets/separated_flex.dart b/lib/widgets/separated_flex.dart new file mode 100644 index 0000000..5e15f03 --- /dev/null +++ b/lib/widgets/separated_flex.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +class SeparatedFlex extends StatelessWidget { + final List children; + final Widget separator; + final Axis axis; + final MainAxisAlignment mainAxisAlignment; + final CrossAxisAlignment crossAxisAlignment; + final MainAxisSize mainAxisSize; + + const SeparatedFlex({ + required this.children, + required this.separator, + required this.axis, + this.mainAxisAlignment = MainAxisAlignment.start, + this.crossAxisAlignment = CrossAxisAlignment.start, + this.mainAxisSize = MainAxisSize.max, + }); + + const SeparatedFlex.horizontal({ + required this.children, + required this.separator, + this.mainAxisAlignment = MainAxisAlignment.start, + this.crossAxisAlignment = CrossAxisAlignment.start, + this.mainAxisSize = MainAxisSize.max, + }) : axis = Axis.horizontal; + + const SeparatedFlex.vertical({ + required this.children, + required this.separator, + this.mainAxisAlignment = MainAxisAlignment.start, + this.crossAxisAlignment = CrossAxisAlignment.start, + this.mainAxisSize = MainAxisSize.max, + }) : axis = Axis.vertical; + + @override + Widget build(BuildContext context) { + final List separatedChildren = children.isNotEmpty + ? List.generate(children.length * 2 - 1, (index) { + if (index.isEven) { + return children[index ~/ 2]; + } else { + return separator; + } + }) + : []; + + return Flex( + direction: axis, + mainAxisAlignment: mainAxisAlignment, + crossAxisAlignment: crossAxisAlignment, + mainAxisSize: mainAxisSize, + children: separatedChildren, + ); + } +} diff --git a/lib/widgets/side_pane.dart b/lib/widgets/side_pane.dart index a11d39c..a7a64f7 100644 --- a/lib/widgets/side_pane.dart +++ b/lib/widgets/side_pane.dart @@ -3,6 +3,7 @@ import 'package:files/backend/workspace.dart'; import 'package:files/widgets/context_menu.dart'; import 'package:files/widgets/drive_list.dart'; import 'package:flutter/material.dart'; +import 'package:yaru_widgets/yaru_widgets.dart'; typedef NewTabCallback = void Function(String); @@ -55,14 +56,16 @@ class _SidePaneState extends State { child: Material( color: Theme.of(context).colorScheme.surface, child: ListView.separated( - padding: const EdgeInsets.only(top: 56), + padding: const EdgeInsets.only(top: 16), itemCount: widget.destinations.length + 1, separatorBuilder: (context, index) { if (index == widget.destinations.length - 1) { return const Divider(); } - return const SizedBox(); + return SizedBox( + height: YaruMasterDetailTheme.of(context).tileSpacing ?? 0, + ); }, itemBuilder: (context, index) { if (index == widget.destinations.length) { @@ -86,13 +89,10 @@ class _SidePaneState extends State { enabled: false, ), ], - child: ListTile( - dense: true, + child: YaruMasterTile( leading: Icon(widget.destinations[index].icon), selected: widget.workspace.currentDir == widget.destinations[index].path, - selectedTileColor: - Theme.of(context).colorScheme.primary.withOpacity(0.1), title: Text( widget.destinations[index].label, ), diff --git a/lib/widgets/tab_strip.dart b/lib/widgets/tab_strip.dart index d3ef9e3..31e2cda 100644 --- a/lib/widgets/tab_strip.dart +++ b/lib/widgets/tab_strip.dart @@ -2,6 +2,8 @@ import 'package:files/backend/utils.dart'; import 'package:files/backend/workspace.dart'; import 'package:files/widgets/context_menu.dart'; import 'package:flutter/material.dart'; +import 'package:yaru_icons/yaru_icons.dart'; +import 'package:yaru_widgets/yaru_widgets.dart'; class TabStrip extends StatelessWidget { final List tabs; @@ -10,6 +12,7 @@ class TabStrip extends StatelessWidget { final ValueChanged? onTabChanged; final ValueChanged? onTabClosed; final VoidCallback? onNewTab; + final List trailing; const TabStrip({ required this.tabs, @@ -18,6 +21,7 @@ class TabStrip extends StatelessWidget { this.onTabChanged, this.onTabClosed, this.onNewTab, + this.trailing = const [], super.key, }); @@ -26,6 +30,11 @@ class TabStrip extends StatelessWidget { return SizedBox.expand( child: Row( children: [ + const SizedBox(width: 8), + YaruOptionButton( + onPressed: onNewTab, + child: const Icon(YaruIcons.plus), + ), Expanded( child: ListView.separated( itemBuilder: (context, index) => ContextMenu( @@ -55,10 +64,14 @@ class TabStrip extends StatelessWidget { padding: const EdgeInsets.all(8), ), ), - IconButton( - onPressed: onNewTab, - icon: const Icon(Icons.add), - ), + if (trailing.isNotEmpty) + const VerticalDivider( + indent: 12, + endIndent: 12, + width: 1, + thickness: 1, + ), + ...trailing, ], ), ); @@ -108,10 +121,11 @@ class _TabState extends State<_Tab> { height: double.infinity, child: Material( color: widget.selected - ? Theme.of(context).colorScheme.surface - : Theme.of(context).colorScheme.background, + ? Theme.of(context).colorScheme.surfaceVariant + : Theme.of(context).colorScheme.surface, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), + borderRadius: BorderRadius.circular(6), + side: BorderSide(color: Theme.of(context).colorScheme.outline), ), clipBehavior: Clip.antiAlias, child: InkWell( @@ -133,9 +147,10 @@ class _TabState extends State<_Tab> { duration: const Duration(milliseconds: 200), child: IconButton( onPressed: widget.allowClosing ? widget.onClosed : null, - icon: const Icon(Icons.close), + icon: const Icon(YaruIcons.window_close), iconSize: 16, splashRadius: 16, + hoverColor: Theme.of(context).colorScheme.primary, padding: EdgeInsets.zero, constraints: BoxConstraints.tight(const Size.square(16)), ), diff --git a/lib/widgets/table.dart b/lib/widgets/table.dart index 41da4b9..a2a3e47 100644 --- a/lib/widgets/table.dart +++ b/lib/widgets/table.dart @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:path/path.dart' as p; import 'package:recase/recase.dart'; +import 'package:yaru_icons/yaru_icons.dart'; typedef HeaderTapCallback = void Function( bool newAscending, @@ -137,7 +138,7 @@ class FilesTable extends StatelessWidget { return const Offset(32, 32); }, feedback: Material( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), color: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), child: Icon( row.entity.isDirectory @@ -202,7 +203,7 @@ class FilesTable extends StatelessWidget { ), if (columnIndex == index) Icon( - ascending ? Icons.arrow_downward : Icons.arrow_upward, + ascending ? YaruIcons.go_down : YaruIcons.go_up, size: 16, ), ], diff --git a/lib/widgets/workspace.dart b/lib/widgets/workspace.dart index 0f07db1..9d3636c 100644 --- a/lib/widgets/workspace.dart +++ b/lib/widgets/workspace.dart @@ -17,6 +17,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:yaru_icons/yaru_icons.dart'; +import 'package:yaru_widgets/yaru_widgets.dart'; class FilesWorkspace extends StatefulWidget { final WorkspaceController controller; @@ -242,7 +244,7 @@ class _FilesWorkspaceState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - height: 48, + height: 56, child: _WorkspaceTopbar( controller: controller, popupBuilder: _getMenuEntries, @@ -293,7 +295,7 @@ class _FilesWorkspaceState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - Icons.folder_open_outlined, + YaruIcons.folder, size: 80, ), Text( @@ -403,41 +405,34 @@ class _WorkspaceTopbar extends StatelessWidget { }, leading: [ _HistoryModifierIconButton( - icon: Icons.arrow_back, + icon: YaruIcons.go_previous, onPressed: controller.goToPreviousHistoryEntry, controller: controller, onHistoryOffsetChanged: controller.setHistoryOffset, enabled: controller.canGoToPreviousHistoryEntry, + type: _HistoryModifierIconButtonType.left, ), _HistoryModifierIconButton( - icon: Icons.arrow_forward, + icon: YaruIcons.go_next, onPressed: controller.goToNextHistoryEntry, controller: controller, onHistoryOffsetChanged: controller.setHistoryOffset, enabled: controller.canGoToNextHistoryEntry, + type: _HistoryModifierIconButtonType.right, ), - IconButton( - icon: const Icon( - Icons.arrow_upward, - size: 20, - color: Colors.white, - ), + const SizedBox(width: 8), + YaruOptionButton( onPressed: () { final PathParts backDir = PathParts.parse(controller.currentDir); controller.changeCurrentDir( backDir.toPath(backDir.parts.length - 1), ); }, - splashRadius: 16, + child: const Icon(YaruIcons.go_up, size: 20), ), ], actions: [ - IconButton( - icon: Icon( - viewIcon, - size: 20, - color: Colors.white, - ), + YaruOptionButton( onPressed: () { switch (controller.view) { case WorkspaceView.table: @@ -448,22 +443,22 @@ class _WorkspaceTopbar extends StatelessWidget { break; } }, - splashRadius: 16, + child: Icon(viewIcon), ), + if (popupBuilder != null) const SizedBox(width: 8), if (popupBuilder != null) MenuAnchor( menuChildren: popupBuilder!(context) .map((e) => e.buildWrapper(context)) .toList(), + alignmentOffset: const Offset(-8, 8), builder: (context, controller, child) { - return IconButton( + return YaruOptionButton( onPressed: () { if (controller.isOpen) return controller.close(); controller.open(); }, - icon: const Icon(Icons.more_vert), - iconSize: 20, - splashRadius: 16, + child: const Icon(YaruIcons.menu), ); }, ), @@ -475,20 +470,23 @@ class _WorkspaceTopbar extends StatelessWidget { IconData get viewIcon { switch (controller.view) { case WorkspaceView.grid: - return Icons.grid_view_outlined; + return YaruIcons.app_grid; case WorkspaceView.table: default: - return Icons.list_outlined; + return YaruIcons.unordered_list; } } } +enum _HistoryModifierIconButtonType { left, right } + class _HistoryModifierIconButton extends StatelessWidget { final IconData icon; final VoidCallback onPressed; final bool enabled; final ValueChanged? onHistoryOffsetChanged; final WorkspaceController controller; + final _HistoryModifierIconButtonType type; const _HistoryModifierIconButton({ required this.icon, @@ -496,6 +494,7 @@ class _HistoryModifierIconButton extends StatelessWidget { this.enabled = true, this.onHistoryOffsetChanged, required this.controller, + required this.type, }); @override @@ -518,10 +517,24 @@ class _HistoryModifierIconButton extends StatelessWidget { ), ) .toList(), - child: IconButton( - icon: Icon(icon, size: 20), + child: YaruOptionButton( onPressed: enabled ? onPressed : null, - splashRadius: 16, + style: OutlinedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.horizontal( + left: type == _HistoryModifierIconButtonType.left + ? const Radius.circular(6) + : Radius.zero, + right: type == _HistoryModifierIconButtonType.right + ? const Radius.circular(6) + : Radius.zero, + ), + ), + backgroundColor: enabled + ? Theme.of(context).colorScheme.surfaceVariant + : Theme.of(context).colorScheme.surface, + ), + child: Icon(icon, size: 20), ), ); } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index bfc0d08..4dbe3af 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,14 +6,34 @@ #include "generated_plugin_registrant.h" +#include +#include #include +#include #include +#include +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) handy_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "HandyWindowPlugin"); + handy_window_plugin_register_with_registrar(handy_window_registrar); g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin"); isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) screen_retriever_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); + screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); + window_manager_plugin_register_with_registrar(window_manager_registrar); + g_autoptr(FlPluginRegistrar) yaru_window_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "YaruWindowLinuxPlugin"); + yaru_window_linux_plugin_register_with_registrar(yaru_window_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 6237f02..62bab64 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,8 +3,13 @@ # list(APPEND FLUTTER_PLUGIN_LIST + gtk + handy_window isar_flutter_libs + screen_retriever url_launcher_linux + window_manager + yaru_window_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/linux/my_application.cc b/linux/my_application.cc index a174ff2..a584f1b 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -49,17 +49,17 @@ static void my_application_activate(GApplication* application) { } gtk_window_set_default_size(window, 1280, 720); - gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); FlView* view = fl_view_new(project); - gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + gtk_widget_show(GTK_WIDGET(window)); + gtk_widget_show(GTK_WIDGET(view)); gtk_widget_grab_focus(GTK_WIDGET(view)); } diff --git a/pubspec.lock b/pubspec.lock index 30fcb85..2ab684d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -251,6 +251,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + gtk: + dependency: transitive + description: + name: gtk + sha256: "517560d6ec625c114cbdcde9223e5ee6418d30860377347ee1b0513399e7a3f5" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + handy_window: + dependency: "direct main" + description: + name: handy_window + sha256: "458a9f7d4ae23816e8f33c76596f943a04e7eff13d864e0867f3b40f1647d63d" + url: "https://pub.dev" + source: hosted + version: "0.3.1" http_multi_server: dependency: transitive description: @@ -523,6 +539,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "4931f226ca158123ccd765325e9fbf360bfed0af9b460a10f960f9bb13d58323" + url: "https://pub.dev" + source: hosted + version: "0.1.6" shelf: dependency: transitive description: @@ -737,6 +761,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.3" + window_manager: + dependency: transitive + description: + name: window_manager + sha256: "492806c69879f0d28e95472bbe5e8d5940ac8c6e99cc07052fe14946974555ba" + url: "https://pub.dev" + source: hosted + version: "0.3.1" windows_path_provider: dependency: "direct main" description: @@ -778,6 +810,86 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + yaru: + dependency: "direct main" + description: + name: yaru + sha256: "77e7b2096aca6b67edafeec4589f6af171d77735e0999bfc21728195057feab8" + url: "https://pub.dev" + source: hosted + version: "0.5.6" + yaru_color_generator: + dependency: transitive + description: + name: yaru_color_generator + sha256: "78b96cefc4eef763e4786f891ce336cdd55ef8edc55494c4bea2bc9d10ef9c96" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + yaru_colors: + dependency: transitive + description: + name: yaru_colors + sha256: "36c49111b812a00eccab265b613882b7d04f5af486c7163ef2da2344405b841f" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + yaru_icons: + dependency: "direct main" + description: + name: yaru_icons + sha256: "8ddd40522c882de898a493094f2f41687f7a0faaf3434b9c854a7605a53a2477" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + yaru_widgets: + dependency: "direct main" + description: + name: yaru_widgets + sha256: "4b5d187964bd589ca9fceda336230806e922b42515477abcea8befe1d10553d6" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + yaru_window: + dependency: "direct main" + description: + name: yaru_window + sha256: "18b3df2922a068e5480048335e2585c134e29ac77baec19b26fa32851910bf2f" + url: "https://pub.dev" + source: hosted + version: "0.1.1" + yaru_window_linux: + dependency: transitive + description: + name: yaru_window_linux + sha256: "356903ebcb70c34f732dbb66ac8b504adb8e92289cdd89da86bed8957f43de38" + url: "https://pub.dev" + source: hosted + version: "0.1.1" + yaru_window_manager: + dependency: transitive + description: + name: yaru_window_manager + sha256: a5ea9db86cbca6306fdf139245fcd84f0df1fed0aead3450d34a9fe7be4d3020 + url: "https://pub.dev" + source: hosted + version: "0.1.0" + yaru_window_platform_interface: + dependency: transitive + description: + name: yaru_window_platform_interface + sha256: "1a0256fc59cc46ad05de5840f01d548184ff900698c19dc24e6326c7911b0177" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + yaru_window_web: + dependency: transitive + description: + name: yaru_window_web + sha256: "77dacaaade6c2b5f94cf45b80f60c69876d62db02490e50dd025ce297cfc09ed" + url: "https://pub.dev" + source: hosted + version: "0.0.2" sdks: dart: ">=3.0.0-134.0.dev <4.0.0" - flutter: ">=3.3.0" + flutter: ">=3.7.0" diff --git a/pubspec.yaml b/pubspec.yaml index 37593a0..0b1d614 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: flutter: sdk: flutter + handy_window: ^0.3.1 intl: any isar: ^3.0.2 isar_flutter_libs: ^3.0.2 @@ -35,6 +36,10 @@ dependencies: git: url: https://github.com/HrX03/windows_path_provider.git xdg_directories: ^1.0.0 + yaru: ^0.5.6 + yaru_icons: ^1.0.4 + yaru_widgets: ^2.1.1 + yaru_window: ^0.1.1 dev_dependencies: build_runner: ^2.3.2 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 377a0c2..b128cea 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,14 +7,20 @@ #include "generated_plugin_registrant.h" #include +#include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { IsarFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); + ScreenRetrieverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); WindowsPathProviderPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("WindowsPathProviderPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 72f9f7f..72da449 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,7 +4,9 @@ list(APPEND FLUTTER_PLUGIN_LIST isar_flutter_libs + screen_retriever url_launcher_windows + window_manager windows_path_provider )