Noo design system rebuild #1

Merged
ayushya merged 31 commits from noo-design-system-rebuild into main 2026-10-01 09:23:58 -04:00
4 changed files with 187 additions and 115 deletions
Showing only changes of commit c9889c69b8 - Show all commits
+1 -1
View File
@@ -78,7 +78,7 @@ Catalog:
| Folder | Components | | Folder | Components |
|---|---| |---|---|
| `core/` | `NooButton`, `NooFab`, `NooChip`, `NooSegmentedControl`, `NooToggle`, `NooSearchField`, `NooAvatar`, `NooBadge`, `NooProgressBar` | | `core/` | `NooButton`, `NooFab`, `NooChip`, `NooSegmentedControl`, `NooToggle`, `NooSearchField`, `NooAvatar`, `NooBadge`, `NooProgressBar` |
| `lists/` | `NooGroupedList`, `NooSettingsRow`, `NooTabOrderRow`, `NooBanner`, `NooSummaryCard` | | `lists/` | `NooGroupedList`, `NooSettingsRow`, `NooTabOrderRow`, `NooBanner`, `NooSummaryCard`, `NooSelectionBar` |
| `files/` | `NooFileKind` (spec §1.2 tiles; `NooFileKind.from(name:, mimeType:, isDirectory:)`), `NooFileTile`, `NooStatusIcon`/`NooSyncStatus`, `NooFileRow` (mobile 64px), `NooFileTableHeader`/`NooFileTableRow` (desktop), `NooSwipeAction` | | `files/` | `NooFileKind` (spec §1.2 tiles; `NooFileKind.from(name:, mimeType:, isDirectory:)`), `NooFileTile`, `NooStatusIcon`/`NooSyncStatus`, `NooFileRow` (mobile 64px), `NooFileTableHeader`/`NooFileTableRow` (desktop), `NooSwipeAction` |
| `media/` | `NooGridCard`, `NooPhotoTile` (video badge, selection), `NooPhotoGroupHeader`/`NooPhotoGrid` (sliver, or `.box`), `NooActivityItem`, `NooStatCard` | | `media/` | `NooGridCard`, `NooPhotoTile` (video badge, selection), `NooPhotoGroupHeader`/`NooPhotoGrid` (sliver, or `.box`), `NooActivityItem`, `NooStatCard` |
| `nav/` | `NooBottomBar`, `NooTopBar` (a `PreferredSizeWidget`) with `NooTopBarButton`/`NooTopBarBack`, `NooDrawer` with its `Account`/`Storage`/`Item`/`Link` parts, `NooSidebar` with `NooSidebarItem`/`Divider`/`Account`/`Storage`, `NooToolbar` | | `nav/` | `NooBottomBar`, `NooTopBar` (a `PreferredSizeWidget`) with `NooTopBarButton`/`NooTopBarBack`, `NooDrawer` with its `Account`/`Storage`/`Item`/`Link` parts, `NooSidebar` with `NooSidebarItem`/`Divider`/`Account`/`Storage`, `NooToolbar` |
+37 -15
View File
@@ -11,7 +11,6 @@ import '../widgets/noo/core/noo_segmented_control.dart';
import '../widgets/noo/files/noo_file_kind.dart'; import '../widgets/noo/files/noo_file_kind.dart';
import '../widgets/noo/files/noo_file_row.dart'; import '../widgets/noo/files/noo_file_row.dart';
import '../widgets/noo/files/noo_file_table.dart'; import '../widgets/noo/files/noo_file_table.dart';
import '../widgets/noo/lists/noo_grouped_list.dart';
import '../widgets/noo/noo_layout.dart'; import '../widgets/noo/noo_layout.dart';
import '../widgets/tabs/tab_state_slivers.dart'; import '../widgets/tabs/tab_state_slivers.dart';
@@ -162,24 +161,47 @@ class _SharesViewState extends State<SharesView> {
c.setSharedWithMe(scope == _ShareScope.withYou); c.setSharedWithMe(scope == _ShareScope.withYou);
} }
// A lazily-built `SliverList`, not `NooGroupedList` (its own `Column`
// isn't lazy - see files_view.dart's `_buildMobileRow` doc comment, and
// trash_view.dart's `_buildMobileList`, which had the same bug: a heavy
// account's full share list built eagerly up front). Each row still
// reads as one continuous radius-20 card via per-row corner rounding +
// a 1px `line` divider.
Widget _buildMobileList(BuildContext context, List<NextcloudShare> shares) { Widget _buildMobileList(BuildContext context, List<NextcloudShare> shares) {
final colors = context.nooColors;
return SliverPadding( return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: NooSpace.sm), padding: const EdgeInsets.symmetric(horizontal: NooSpace.sm),
sliver: SliverToBoxAdapter( sliver: SliverList(
child: NooGroupedList( delegate: SliverChildBuilderDelegate((context, index) {
children: [ final share = shares[index];
for (final share in shares) final row = NooFileRow(
NooFileRow( kind: NooFileKind.from(name: share.name, isDirectory: share.isFolder),
kind: NooFileKind.from(name: share.name, isDirectory: share.isFolder), name: share.name,
name: share.name, meta: '${share.ownerDisplayName} · ${_permissionLabel(share.permissions)}',
meta: '${share.ownerDisplayName} · ${_permissionLabel(share.permissions)}', iosStyle: NooLayout.iosStyle(context),
iosStyle: NooLayout.iosStyle(context), onTap: share.isFolder ? () => _openFolder(context, share) : null,
onTap: share.isFolder ? () => _openFolder(context, share) : null, trailing: Icon(_shareTypeIcon(share.shareType), size: 16),
trailing: Icon(_shareTypeIcon(share.shareType), size: 16), onMore: () => _confirmUnshare(context, share),
onMore: () => _confirmUnshare(context, share), );
final isFirst = index == 0;
final isLast = index == shares.length - 1;
return Column(
children: [
ClipRRect(
borderRadius: BorderRadius.vertical(
top: isFirst
? const Radius.circular(NooRadii.card)
: Radius.zero,
bottom: isLast
? const Radius.circular(NooRadii.card)
: Radius.zero,
),
child: row,
), ),
], if (!isLast) Container(height: 1, color: colors.line),
), ],
);
}, childCount: shares.length),
), ),
); );
} }
+50 -28
View File
@@ -11,7 +11,6 @@ import '../widgets/noo/files/noo_file_row.dart';
import '../widgets/noo/files/noo_file_table.dart'; import '../widgets/noo/files/noo_file_table.dart';
import '../widgets/noo/files/noo_file_tile.dart'; import '../widgets/noo/files/noo_file_tile.dart';
import '../widgets/noo/lists/noo_banner.dart'; import '../widgets/noo/lists/noo_banner.dart';
import '../widgets/noo/lists/noo_grouped_list.dart';
import '../widgets/noo/noo_layout.dart'; import '../widgets/noo/noo_layout.dart';
import '../widgets/tabs/tab_location.dart'; import '../widgets/tabs/tab_location.dart';
import '../widgets/tabs/tab_state_slivers.dart'; import '../widgets/tabs/tab_state_slivers.dart';
@@ -103,38 +102,61 @@ class _TrashViewState extends State<TrashView> {
); );
} }
// A lazily-built `SliverList`, not `NooGroupedList` (its own `Column`
// isn't lazy - see files_view.dart's `_buildMobileRow` doc comment for
// the same reasoning): Trash can hold hundreds of items, and building
// every row eagerly up front is what made this tab laggy. Each row still
// reads as one continuous radius-20 card via per-row corner rounding +
// a 1px `line` divider, matching `NooGroupedList`'s look.
Widget _buildMobileList(BuildContext context, List<NextcloudItem> trash) { Widget _buildMobileList(BuildContext context, List<NextcloudItem> trash) {
final colors = context.nooColors;
return SliverPadding( return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: NooSpace.sm), padding: const EdgeInsets.symmetric(horizontal: NooSpace.sm),
sliver: SliverToBoxAdapter( sliver: SliverList(
child: NooGroupedList( delegate: SliverChildBuilderDelegate((context, index) {
children: [ final item = trash[index];
for (final item in trash) final row = NooFileRow(
NooFileRow( kind: NooFileKind.from(
kind: NooFileKind.from( name: item.name,
name: item.name, mimeType: item.mimeType,
mimeType: item.mimeType, isDirectory: item.isFolder,
isDirectory: item.isFolder, ),
name: item.name,
meta: _meta(item),
iosStyle: NooLayout.iosStyle(context),
trailing: SizedBox.square(
dimension: 40,
child: Semantics(
button: true,
label: 'Restore',
child: InkResponse(
onTap: () => _restore(context, item),
radius: 20,
child: const Icon(LucideIcons.rotateCcw),
), ),
name: item.name,
meta: _meta(item),
iosStyle: NooLayout.iosStyle(context),
trailing: SizedBox.square(
dimension: 40,
child: Semantics(
button: true,
label: 'Restore',
child: InkResponse(
onTap: () => _restore(context, item),
radius: 20,
child: const Icon(LucideIcons.rotateCcw),
),
),
),
onMore: () => _confirmDeleteForever(context, item),
), ),
], ),
), onMore: () => _confirmDeleteForever(context, item),
);
final isFirst = index == 0;
final isLast = index == trash.length - 1;
return Column(
children: [
ClipRRect(
borderRadius: BorderRadius.vertical(
top: isFirst
? const Radius.circular(NooRadii.card)
: Radius.zero,
bottom: isLast
? const Radius.circular(NooRadii.card)
: Radius.zero,
),
child: row,
),
if (!isLast) Container(height: 1, color: colors.line),
],
);
}, childCount: trash.length),
), ),
); );
} }
+99 -71
View File
@@ -93,33 +93,49 @@ class FilesControlsRow extends StatelessWidget {
showNooSheet( showNooSheet(
context, context,
children: [ children: [
NooSegmentedControl<bool>( // `showNooSheet`'s `children` are built once, up front - a bare
fill: true, // checkmark/value here would freeze at whatever it was when the
value: files.sortAscendingFor(folderPath), // sheet opened, since tapping a row calls `files.set...` and
onChanged: (asc) { // notifies the controller, not this already-built widget tree.
if (asc != files.sortAscendingFor(folderPath)) { // `ListenableBuilder` re-runs its `builder` on every
files.toggleSortOrderFor(folderPath); // `FilesController.notifyListeners()` instead, so the selection
} // updates live - see the identical fix in `_showFilterSheet`.
}, ListenableBuilder(
options: const [ listenable: files,
NooSegmentOption(value: true, icon: LucideIcons.arrowUp, label: 'Ascending'), builder: (context, _) => Column(
NooSegmentOption(value: false, icon: LucideIcons.arrowDown, label: 'Descending'), mainAxisSize: MainAxisSize.min,
], children: [
), NooSegmentedControl<bool>(
NooGroupedList( fill: true,
children: [ value: files.sortAscendingFor(folderPath),
for (final field in FileSortField.values) onChanged: (asc) {
NooSettingsRow( if (asc != files.sortAscendingFor(folderPath)) {
label: Text(sortFieldLabel(field)), files.toggleSortOrderFor(folderPath);
trailing: field == files.sortFieldFor(folderPath) }
? Icon(LucideIcons.check, size: 18, color: context.nooColors.accentText)
: null,
onTap: () {
files.setSortFieldFor(folderPath, field);
Navigator.pop(context);
}, },
options: const [
NooSegmentOption(value: true, icon: LucideIcons.arrowUp, label: 'Ascending'),
NooSegmentOption(value: false, icon: LucideIcons.arrowDown, label: 'Descending'),
],
), ),
], const SizedBox(height: 22),
NooGroupedList(
children: [
for (final field in FileSortField.values)
NooSettingsRow(
label: Text(sortFieldLabel(field)),
trailing: field == files.sortFieldFor(folderPath)
? Icon(LucideIcons.check, size: 18, color: context.nooColors.accentText)
: null,
onTap: () {
files.setSortFieldFor(folderPath, field);
Navigator.pop(context);
},
),
],
),
],
),
), ),
], ],
); );
@@ -129,54 +145,66 @@ class FilesControlsRow extends StatelessWidget {
showNooSheet( showNooSheet(
context, context,
children: [ children: [
NooGroupedList( // See `_showSortSheet`'s comment: without this, none of the
label: 'Show', // checkmarks/toggles below would move until the sheet is closed
children: [ // and reopened.
NooSettingsRow( ListenableBuilder(
label: const Text('Files and folders'), listenable: files,
trailing: files.filesTypeFilter == FilesTypeFilter.all builder: (context, _) => Column(
? Icon(LucideIcons.check, size: 18, color: context.nooColors.accentText) mainAxisSize: MainAxisSize.min,
: null, children: [
onTap: () => files.setFilesTypeFilter(FilesTypeFilter.all), NooGroupedList(
), label: 'Show',
NooSettingsRow( children: [
label: const Text('Files only'), NooSettingsRow(
trailing: files.filesTypeFilter == FilesTypeFilter.filesOnly label: const Text('Files and folders'),
? Icon(LucideIcons.check, size: 18, color: context.nooColors.accentText) trailing: files.filesTypeFilter == FilesTypeFilter.all
: null, ? Icon(LucideIcons.check, size: 18, color: context.nooColors.accentText)
onTap: () => files.setFilesTypeFilter(FilesTypeFilter.filesOnly), : null,
), onTap: () => files.setFilesTypeFilter(FilesTypeFilter.all),
NooSettingsRow(
label: const Text('Folders only'),
trailing: files.filesTypeFilter == FilesTypeFilter.foldersOnly
? Icon(LucideIcons.check, size: 18, color: context.nooColors.accentText)
: null,
onTap: () => files.setFilesTypeFilter(FilesTypeFilter.foldersOnly),
),
],
),
NooGroupedList(
children: [
NooSettingsRow(
icon: LucideIcons.eye,
label: const Text('Show hidden files'),
trailing: NooToggle(
checked: files.showHiddenFiles,
onChanged: (_) => files.toggleShowHiddenFiles(),
),
),
if (showStorageScope)
NooSettingsRow(
icon: LucideIcons.hardDrive,
label: const Text('External storage'),
trailing: NooToggle(
checked: files.storageScope == StorageScope.external,
onChanged: (external) => files.setStorageScope(
external ? StorageScope.external : StorageScope.cloud,
), ),
), NooSettingsRow(
label: const Text('Files only'),
trailing: files.filesTypeFilter == FilesTypeFilter.filesOnly
? Icon(LucideIcons.check, size: 18, color: context.nooColors.accentText)
: null,
onTap: () => files.setFilesTypeFilter(FilesTypeFilter.filesOnly),
),
NooSettingsRow(
label: const Text('Folders only'),
trailing: files.filesTypeFilter == FilesTypeFilter.foldersOnly
? Icon(LucideIcons.check, size: 18, color: context.nooColors.accentText)
: null,
onTap: () => files.setFilesTypeFilter(FilesTypeFilter.foldersOnly),
),
],
), ),
], const SizedBox(height: 22),
NooGroupedList(
children: [
NooSettingsRow(
icon: LucideIcons.eye,
label: const Text('Show hidden files'),
trailing: NooToggle(
checked: files.showHiddenFiles,
onChanged: (_) => files.toggleShowHiddenFiles(),
),
),
if (showStorageScope)
NooSettingsRow(
icon: LucideIcons.hardDrive,
label: const Text('External storage'),
trailing: NooToggle(
checked: files.storageScope == StorageScope.external,
onChanged: (external) => files.setStorageScope(
external ? StorageScope.external : StorageScope.cloud,
),
),
),
],
),
],
),
), ),
], ],
); );