Release 1.0.3: refine tablet layouts, pointer selection and account sync cleanup
Build iOS / build (push) Successful in 4m45s
Build App Bundle / build (push) Successful in 7m7s

This commit is contained in:
Ayushya Amitabh
2026-10-09 00:36:42 -04:00
parent 79085874a1
commit 0a4608e6c3
67 changed files with 1740 additions and 451 deletions
+120 -33
View File
@@ -5,9 +5,11 @@ import '../widgets/noo/lists/noo_grouped_list.dart';
import '../widgets/noo/lists/noo_settings_row.dart';
import '../widgets/noo/nav/noo_top_bar.dart';
import '../widgets/noo/nav/noo_toolbar.dart';
import '../widgets/noo/nav/noo_sidebar.dart';
import '../widgets/noo/noo_layout.dart';
import '../widgets/settings/settings_account_card.dart';
import '../widgets/settings/settings_accounts.dart';
import '../widgets/settings/settings_about.dart';
import '../widgets/settings/settings_action_bar.dart';
import '../widgets/settings/settings_appearance.dart';
import '../widgets/settings/settings_file_sync.dart';
@@ -15,35 +17,20 @@ import '../widgets/settings/settings_files_cache.dart';
import '../widgets/settings/settings_security.dart';
import '../widgets/settings/settings_swipe.dart';
import '../widgets/settings/settings_tabs.dart';
import '../widgets/settings/settings_section.dart';
/// Settings, pushed on top of the shell (DESIGN_SYSTEM.md 4's 9-section
/// order: account card, accounts, security, file sync, files cache,
/// appearance, tabs, action bar, swipe on a file).
///
/// Every layout (including wide tablet, in a centered 640px column) is
/// a two-level menu, the way native iOS/Android Settings apps
/// work: [SettingsAccountCard] (the account summary, not a settings picker)
/// stays pinned at the top of a single top-level list ([_MobileMenu]), and
/// every other section becomes one tappable [NooSettingsRow] - icon, title,
/// chevron - in a [NooGroupedList] below it. Tapping a row pushes a new
/// [_SettingsCategoryScreen] with its own [NooTopBar]/[NooTopBarBack],
/// containing just that section's content full-screen. This replaces an
/// earlier design where every section rendered inline in one long
/// collapsible-sections column (`_MobileList`, since removed) - and before
/// that, a trailing jump rail (an even earlier version) that scrolled that
/// *same* page to an anchor. Both were rejected: the jump rail added a
/// second, redundant way to navigate on top of plain scrolling without
/// shortening the page, and the collapsible-sections column still left a
/// long page to scroll past even collapsed. A genuinely separate pushed
/// screen per category removes the scroll-depth problem outright, so
/// neither a jump rail nor per-section collapsing is needed any more - see
/// [SettingsSection]'s doc comment for how that reflects in its mobile
/// layout. See each `widgets/settings/*.dart` file for a section's own
/// content and any setting that had to be slotted in or grouped under
/// "Advanced appearance".
class AccountView extends StatelessWidget {
/// Settings uses pushed category screens on phones and an inline category
/// sidebar with a rounded content pane on tablets.
class AccountView extends StatefulWidget {
const AccountView({super.key});
@override
State<AccountView> createState() => _AccountViewState();
}
class _AccountViewState extends State<AccountView> {
int selected = 0;
static final _categories = <_SettingsCategory>[
_SettingsCategory(
title: 'Accounts',
@@ -85,6 +72,11 @@ class AccountView extends StatelessWidget {
icon: LucideIcons.chevronsLeftRight,
builder: (_) => const SettingsSwipeSection(),
),
_SettingsCategory(
title: 'About',
icon: LucideIcons.info,
builder: (_) => const SettingsAboutSection(),
),
];
@override
@@ -92,6 +84,101 @@ class AccountView extends StatelessWidget {
final colors = context.nooColors;
final desktop = NooLayout.isDesktop(context);
if (desktop) {
final category = _categories[selected];
final duration = MediaQuery.disableAnimationsOf(context)
? Duration.zero
: const Duration(milliseconds: 260);
return Scaffold(
backgroundColor: colors.bg,
body: SafeArea(
child: Row(
children: [
NooSidebar(
windowControls: Row(
children: [
IconButton(
icon: const Icon(LucideIcons.arrowLeft),
tooltip: 'Back',
onPressed: () => Navigator.maybePop(context),
),
Expanded(
child: Text(
'Settings',
style: NooText.title.copyWith(color: colors.fg1),
),
),
],
),
account: SettingsAccountCard(
compact: true,
onManage: () => setState(() => selected = 0),
),
groupItems: true,
items: [
for (var index = 0; index < _categories.length; index++)
if (_categories[index].title != 'Swipe on a file')
NooSidebarItem(
icon: _categories[index].icon,
label: _categories[index].title,
selected: selected == index,
onTap: () => setState(() => selected = index),
),
],
),
Expanded(
child: Column(
children: [
NooToolbar(
title: category.title,
backgroundColor: colors.bg,
framed: false,
),
Expanded(
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 16),
child: Material(
color: colors.surface2,
borderRadius: BorderRadius.circular(NooRadii.card),
clipBehavior: Clip.antiAlias,
child: AnimatedSwitcher(
duration: duration,
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
transitionBuilder: (child, animation) =>
FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween(
begin: const Offset(0, .025),
end: Offset.zero,
).animate(animation),
child: child,
),
),
child: ListView(
key: ValueKey(category.title),
padding: const EdgeInsets.all(12),
children: [
SettingsCategoryHeading(
title: category.title,
child: category.builder(context),
),
],
),
),
),
),
),
],
),
),
],
),
),
);
}
return Scaffold(
backgroundColor: colors.bg,
appBar: desktop
@@ -184,12 +271,7 @@ class _MobileMenu extends StatelessWidget {
}
}
/// A pushed, single-category Settings screen: [NooTopBar] titled with the
/// category, a back button, and just that section's own content - the
/// pushed half of [AccountView]'s mobile menu-then-screen pattern. Always
/// built in a mobile-width context (desktop never opens this screen; it
/// shows every section inline in its own grid instead), so the section
/// widgets inside render their normal mobile layout unchanged.
/// A pushed category screen used by phone navigation.
class _SettingsCategoryScreen extends StatelessWidget {
final _SettingsCategory category;
const _SettingsCategoryScreen({required this.category});
@@ -223,7 +305,12 @@ class _SettingsCategoryScreen extends StatelessWidget {
NooSpace.xxl,
),
physics: const BouncingScrollPhysics(),
children: [category.builder(context)],
children: [
SettingsCategoryHeading(
title: category.title,
child: category.builder(context),
),
],
),
),
),
+2 -13
View File
@@ -41,7 +41,6 @@ class ActivityView extends StatelessWidget {
final files = context.watch<FilesController>();
final session = context.watch<SessionController>();
final activities = files.activities;
final isDesktop = NooLayout.isDesktop(context);
final groups = groupByCalendarDay<NextcloudActivity>(
activities,
(a) => a.timestamp,
@@ -116,22 +115,12 @@ class ActivityView extends StatelessWidget {
);
return ColoredBox(
color: colors.bg,
color: NooLayout.contentBackground(context),
child: RefreshIndicator(
color: colors.accent,
backgroundColor: colors.surface,
onRefresh: () => context.read<FilesController>().refreshData(),
// "Desktop limits it to 760px wide" (DESIGN_SYSTEM.md §4) - centered
// rather than left-aligned, so a wide window doesn't stretch the
// feed's short sentences edge to edge.
child: isDesktop
? Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 760),
child: scrollView,
),
)
: scrollView,
child: scrollView,
),
);
}
+5 -9
View File
@@ -400,6 +400,7 @@ class _FavoritesViewState extends State<FavoritesView> {
favorite: item.isFavorite,
iosStyle: NooLayout.iosStyle(context),
selected: isSelected,
onSelectionToggle: () => _toggleSelection(item),
thumbnail: _thumbnailFor(context, item, extent: NooSizes.rowMobile),
onTap: () {
if (_isSelecting) {
@@ -434,6 +435,7 @@ class _FavoritesViewState extends State<FavoritesView> {
col3: item.isFolder ? null : formatBytes(item.size),
favorite: item.isFavorite,
selected: isSelected,
onSelectionToggle: () => _toggleSelection(item),
thumbnail: _thumbnailFor(
context,
item,
@@ -477,6 +479,7 @@ class _FavoritesViewState extends State<FavoritesView> {
thumbnailHeight: thumbnailHeight,
thumbnail: _thumbnailFor(context, item, extent: thumbnailHeight),
selected: isSelected,
onSelectionToggle: () => _toggleSelection(item),
verticalOverflowIcon: !NooLayout.iosStyle(context),
onTap: () {
if (_isSelecting) {
@@ -600,14 +603,7 @@ class _FavoritesViewState extends State<FavoritesView> {
vertical: NooSpace.xs,
),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isDesktop
? 5
: NooLayout.gridColumns(context, phone: 2, minTile: 180),
crossAxisSpacing: isDesktop ? 16 : 10,
mainAxisSpacing: isDesktop ? 16 : 10,
childAspectRatio: isDesktop ? 0.92 : 0.85,
),
gridDelegate: NooLayout.fileGridDelegate(context),
delegate: SliverChildBuilderDelegate((context, index) {
return _buildGridCard(
context,
@@ -680,7 +676,7 @@ class _FavoritesViewState extends State<FavoritesView> {
? _buildSelectionBar(context, selectedItems)
: null,
child: ColoredBox(
color: colors.bg,
color: NooLayout.contentBackground(context),
child: RefreshIndicator(
color: colors.accent,
backgroundColor: colors.surface,
+1 -3
View File
@@ -729,9 +729,7 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
default:
if (_isPdf) {
return MediaPdfPreview(
topInset: _controlsVisible
? MediaQuery.paddingOf(context).top + 60
: 0,
topInset: MediaQuery.paddingOf(context).top + 60,
item: widget.item,
session: session,
localPathResolver: widget.localPathResolver,
+24 -17
View File
@@ -745,14 +745,7 @@ class _FilesViewState extends State<FilesView> {
NooSpace.lg,
),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isDesktop
? 5
: NooLayout.gridColumns(context, phone: 2, minTile: 180),
childAspectRatio: isDesktop ? 1.05 : 0.92,
crossAxisSpacing: isDesktop ? 16 : 10,
mainAxisSpacing: isDesktop ? 16 : 10,
),
gridDelegate: NooLayout.fileGridDelegate(context),
delegate: SliverChildBuilderDelegate((context, index) {
final item = items[index];
return _FolderEnterAnimation(
@@ -971,7 +964,7 @@ class _FilesViewState extends State<FilesView> {
? _buildSelectionBar(context, selectedItems)
: null,
child: ColoredBox(
color: colors.bg,
color: NooLayout.contentBackground(context),
child: RefreshIndicator(
color: colors.accent,
backgroundColor: colors.surface,
@@ -1061,14 +1054,16 @@ class _FilesViewState extends State<FilesView> {
tone: hasConflicts
? NooSummaryCardTone.danger
: NooSummaryCardTone.normal,
action: NooButton(
variant: NooButtonVariant.tonal,
size: NooButtonSize.compact,
icon: LucideIcons.refreshCw,
disabled: sync.isSyncingNow,
onTap: () => sync.syncOnPull(),
child: const Text('Sync now'),
),
action: sync.syncEverything || sync.syncedPaths.isNotEmpty
? NooButton(
variant: NooButtonVariant.tonal,
size: NooButtonSize.compact,
icon: LucideIcons.refreshCw,
disabled: sync.isSyncingNow,
onTap: () => sync.syncOnPull(),
child: const Text('Sync now'),
)
: null,
),
);
}
@@ -1360,6 +1355,10 @@ class _FilesViewState extends State<FilesView> {
favorite: item.isFavorite,
iosStyle: NooLayout.iosStyle(context),
selected: isSelected,
onSelectionToggle:
_offline || (picking && !pick.pickRequest!.allowMultiple)
? null
: () => _toggleSelection(item),
thumbnail: _rowThumbnail(context, item, session, NooFileTileSize.row),
onTap: () => _handleItemTap(context, item, picking: picking),
onLongPress: _offline || (picking && !pick.pickRequest!.allowMultiple)
@@ -1447,6 +1446,10 @@ class _FilesViewState extends State<FilesView> {
statuses: _nooStatuses(status),
favorite: item.isFavorite,
selected: isSelected,
onSelectionToggle:
_offline || (picking && !pick.pickRequest!.allowMultiple)
? null
: () => _toggleSelection(item),
thumbnail: _rowThumbnail(context, item, session, NooFileTileSize.desktop),
onTap: () => _handleItemTap(context, item, picking: picking),
onLongPress: _offline || (picking && !pick.pickRequest!.allowMultiple)
@@ -1549,6 +1552,10 @@ class _FilesViewState extends State<FilesView> {
: null,
thumbnailHeight: NooLayout.isDesktop(context) ? 118 : 104,
selected: isSelected,
onSelectionToggle:
_offline || (picking && !pick.pickRequest!.allowMultiple)
? null
: () => _toggleSelection(item),
onTap: () => _handleItemTap(context, item, picking: picking),
onLongPress: _offline || (picking && !pick.pickRequest!.allowMultiple)
? null
+62 -19
View File
@@ -37,7 +37,9 @@ class _LoginViewState extends State<LoginView> {
void initState() {
super.initState();
if (widget.isAddingAccount) {
_originalActiveAccountId = context.read<SessionController>().activeAccountId;
_originalActiveAccountId = context
.read<SessionController>()
.activeAccountId;
}
}
@@ -50,12 +52,17 @@ class _LoginViewState extends State<LoginView> {
void _handleContinue() {
final url = _urlController.text.trim();
if (url.isEmpty) {
setState(() => _localError = 'Please enter your Nextcloud server address');
setState(
() => _localError = 'Please enter your Nextcloud server address',
);
return;
}
setState(() => _localError = null);
FocusScope.of(context).unfocus();
context.read<SessionController>().startLoginFlow(url, addAccount: widget.isAddingAccount);
context.read<SessionController>().startLoginFlow(
url,
addAccount: widget.isAddingAccount,
);
}
/// Resumes a saved account with one tap (see [SessionController.switchAccount]).
@@ -73,7 +80,7 @@ class _LoginViewState extends State<LoginView> {
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Text("Couldn't sign in as ${account.username}"),
content: Text("Couldn't sign in as ${account.label}"),
behavior: SnackBarBehavior.floating,
),
);
@@ -97,7 +104,8 @@ class _LoginViewState extends State<LoginView> {
final colors = context.nooColors;
final session = context.watch<SessionController>();
final isAwaitingBrowser = session.loginFlowStatus == LoginFlowStatus.awaitingBrowser;
final isAwaitingBrowser =
session.loginFlowStatus == LoginFlowStatus.awaitingBrowser;
final isInitiating = session.loginFlowStatus == LoginFlowStatus.initiating;
// A new/refreshed account has just become active - pop back to
@@ -117,7 +125,9 @@ class _LoginViewState extends State<LoginView> {
// SessionController.startLoginFlow's doc comment for why. Push it the
// moment there's a URL to show; _webViewPushed resets once that route
// pops (cancelled or done) so a retry after cancelling pushes it again.
if (!_webViewPushed && isAwaitingBrowser && session.pendingLoginUrl != null) {
if (!_webViewPushed &&
isAwaitingBrowser &&
session.pendingLoginUrl != null) {
_webViewPushed = true;
final url = session.pendingLoginUrl!;
WidgetsBinding.instance.addPostFrameCallback((_) async {
@@ -142,7 +152,10 @@ class _LoginViewState extends State<LoginView> {
}
final errorMessage =
_localError ?? (session.loginFlowStatus == LoginFlowStatus.error ? session.errorMessage : null);
_localError ??
(session.loginFlowStatus == LoginFlowStatus.error
? session.errorMessage
: null);
final body = Scaffold(
backgroundColor: colors.bg,
@@ -167,7 +180,8 @@ class _LoginViewState extends State<LoginView> {
// deleting it, specifically so it can be resumed
// from here with one tap - no need to repeat
// Login Flow v2.
if (!widget.isAddingAccount && session.accounts.isNotEmpty) ...[
if (!widget.isAddingAccount &&
session.accounts.isNotEmpty) ...[
_SavedAccountsSection(
accounts: session.accounts,
onSelect: (account) => _continueAsAccount(account),
@@ -179,7 +193,10 @@ class _LoginViewState extends State<LoginView> {
Expanded(child: Divider(color: colors.line)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text('or', style: NooText.meta.copyWith(color: colors.fg3)),
child: Text(
'or',
style: NooText.meta.copyWith(color: colors.fg3),
),
),
Expanded(child: Divider(color: colors.line)),
],
@@ -188,7 +205,8 @@ class _LoginViewState extends State<LoginView> {
],
_ServerForm(
urlController: _urlController,
isLoading: isInitiating || session.isLoading || isAwaitingBrowser,
isLoading:
isInitiating || session.isLoading || isAwaitingBrowser,
errorMessage: errorMessage,
onContinue: _handleContinue,
),
@@ -246,11 +264,19 @@ class _ServerForm extends StatelessWidget {
// in-app, rather than the full-color launcher icon.
child: ColorFiltered(
colorFilter: ColorFilter.mode(colors.fg1, BlendMode.srcIn),
child: Image.asset('assets/icon/app_icon_monochrome.png', width: 80, height: 80),
child: Image.asset(
'assets/icon/app_icon_monochrome.png',
width: 80,
height: 80,
),
),
),
const SizedBox(height: 24),
Text('Noo', textAlign: TextAlign.center, style: NooText.largeTitle.copyWith(color: colors.fg1)),
Text(
'Noo',
textAlign: TextAlign.center,
style: NooText.largeTitle.copyWith(color: colors.fg1),
),
const SizedBox(height: 6),
Text(
'Connect to your self-hosted server',
@@ -317,7 +343,11 @@ class _SavedAccountsSection extends StatelessWidget {
final ValueChanged<SavedAccount> onSelect;
final ValueChanged<SavedAccount> onRemove;
const _SavedAccountsSection({required this.accounts, required this.onSelect, required this.onRemove});
const _SavedAccountsSection({
required this.accounts,
required this.onSelect,
required this.onRemove,
});
@override
Widget build(BuildContext context) {
@@ -328,7 +358,10 @@ class _SavedAccountsSection extends StatelessWidget {
children: [
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 8),
child: Text('Continue as', style: NooText.label.copyWith(color: colors.fg2)),
child: Text(
'Continue as',
style: NooText.label.copyWith(color: colors.fg2),
),
),
ClipRRect(
borderRadius: BorderRadius.circular(NooRadii.card),
@@ -358,7 +391,11 @@ class _SavedAccountRow extends StatelessWidget {
final VoidCallback onTap;
final VoidCallback onRemove;
const _SavedAccountRow({required this.account, required this.onTap, required this.onRemove});
const _SavedAccountRow({
required this.account,
required this.onTap,
required this.onRemove,
});
@override
Widget build(BuildContext context) {
@@ -369,18 +406,21 @@ class _SavedAccountRow extends StatelessWidget {
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: NooSpace.md, vertical: 10),
padding: const EdgeInsets.symmetric(
horizontal: NooSpace.md,
vertical: 10,
),
child: Row(
spacing: 12,
children: [
NooAvatar(initials: accountInitial(account.username), size: 36),
NooAvatar(initials: accountInitial(account.label), size: 36),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 2,
children: [
Text(
account.username,
account.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: NooText.bodyL.copyWith(
@@ -404,7 +444,10 @@ class _SavedAccountRow extends StatelessWidget {
width: 36,
height: 36,
alignment: Alignment.center,
decoration: BoxDecoration(color: colors.surface2, shape: BoxShape.circle),
decoration: BoxDecoration(
color: colors.surface2,
shape: BoxShape.circle,
),
child: Icon(LucideIcons.x, size: 16, color: colors.fg2),
),
),
+1 -8
View File
@@ -283,14 +283,7 @@ class _MoveCopyDestinationPickerState extends State<MoveCopyDestinationPicker> {
SliverPadding(
padding: EdgeInsets.symmetric(horizontal: gutter),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: desktop
? 5
: NooLayout.gridColumns(context, phone: 2, minTile: 180),
childAspectRatio: desktop ? 1.05 : 0.92,
crossAxisSpacing: desktop ? 16 : 10,
mainAxisSpacing: desktop ? 16 : 10,
),
gridDelegate: NooLayout.fileGridDelegate(context),
delegate: SliverChildBuilderDelegate(
(context, index) => _buildGridCard(folders[index]),
childCount: folders.length,
+5 -8
View File
@@ -355,13 +355,7 @@ class _PhotosViewState extends State<PhotosView> {
),
NooPhotoGrid(
itemCount: group.items.length,
columns: isDesktop
? NooPhotoGrid.desktopColumns
: NooLayout.gridColumns(
context,
phone: NooPhotoGrid.mobileColumns,
minTile: 130,
),
minTileWidth: 150,
gap: isDesktop ? NooPhotoGrid.desktopGap : NooPhotoGrid.mobileGap,
padding: EdgeInsets.symmetric(
horizontal: isDesktop ? NooLayout.gutter(context) : 0,
@@ -393,7 +387,7 @@ class _PhotosViewState extends State<PhotosView> {
? _buildSelectionBar(context, pick, selectedItems)
: null,
child: ColoredBox(
color: colors.bg,
color: NooLayout.contentBackground(context),
child: RefreshIndicator(
color: colors.accent,
backgroundColor: colors.surface,
@@ -653,6 +647,9 @@ class _PhotosViewState extends State<PhotosView> {
return NooPhotoTile(
selected: isSelected,
onSelectionToggle: pick.isPicking && !pick.pickRequest!.allowMultiple
? null
: () => _toggleSelection(photo),
selectionMode: _isSelecting,
// No real duration source (no video-metadata extraction anywhere in
// the app) - an empty string still marks it as a video and shows the
+1 -1
View File
@@ -83,7 +83,7 @@ class _RecentViewState extends State<RecentView> {
];
return ColoredBox(
color: colors.bg,
color: NooLayout.contentBackground(context),
child: RefreshIndicator(
color: colors.accent,
backgroundColor: colors.surface,
+24 -13
View File
@@ -123,18 +123,24 @@ class _SearchViewState extends State<SearchView> {
return Scaffold(
backgroundColor: colors.bg,
appBar: desktop
? NooToolbar(
titleWidget: _buildField(onSurface: true),
actions: [
IconTheme.merge(
data: IconThemeData(color: colors.fg1, size: 24),
child: NooTopBarButton(
icon: LucideIcons.x,
tooltip: 'Close',
onTap: () => Navigator.pop(context),
),
? PreferredSize(
preferredSize: const Size.fromHeight(NooToolbar.outerHeight),
child: SafeArea(
bottom: false,
child: NooToolbar(
titleWidget: _buildField(onSurface: true),
actions: [
IconTheme.merge(
data: IconThemeData(color: colors.fg1, size: 24),
child: NooTopBarButton(
icon: LucideIcons.x,
tooltip: 'Close',
onTap: () => Navigator.pop(context),
),
),
],
),
],
),
)
: null,
body: SafeArea(
@@ -223,7 +229,9 @@ class _SearchViewState extends State<SearchView> {
title: 'No results found',
);
}
return desktop ? _buildDesktopResults(context) : _buildMobileResults(context);
return desktop
? _buildDesktopResults(context)
: _buildMobileResults(context);
}
Widget _buildMessage(
@@ -304,7 +312,10 @@ class _SearchViewState extends State<SearchView> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const NooFileTableHeader(col2Label: 'Modified', col3Label: 'Location'),
const NooFileTableHeader(
col2Label: 'Modified',
col3Label: 'Location',
),
for (final item in _results)
NooFileTableRow(
kind: NooFileKind.from(
+1 -8
View File
@@ -208,14 +208,7 @@ class _ShareUploadViewState extends State<ShareUploadView> {
SliverPadding(
padding: EdgeInsets.symmetric(horizontal: gutter),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: desktop
? 5
: NooLayout.gridColumns(context, phone: 2, minTile: 180),
childAspectRatio: desktop ? 1.05 : 0.92,
crossAxisSpacing: desktop ? 16 : 10,
mainAxisSpacing: desktop ? 16 : 10,
),
gridDelegate: NooLayout.fileGridDelegate(context),
delegate: SliverChildBuilderDelegate(
(context, index) => _buildGridCard(context, folders[index]),
childCount: folders.length,
+1 -1
View File
@@ -129,7 +129,7 @@ class _SharesViewState extends State<SharesView> {
];
return ColoredBox(
color: colors.bg,
color: NooLayout.contentBackground(context),
child: RefreshIndicator(
color: colors.accent,
backgroundColor: colors.surface,
+25 -91
View File
@@ -9,7 +9,6 @@ import '../widgets/noo/core/noo_button.dart';
import '../widgets/noo/files/noo_file_kind.dart';
import '../widgets/noo/files/noo_file_row.dart';
import '../widgets/noo/files/noo_file_table.dart';
import '../widgets/noo/files/noo_file_tile.dart';
import '../widgets/noo/lists/noo_banner.dart';
import '../widgets/noo/noo_layout.dart';
import '../widgets/tabs/tab_location.dart';
@@ -98,7 +97,7 @@ class _TrashViewState extends State<TrashView> {
];
return ColoredBox(
color: colors.bg,
color: NooLayout.contentBackground(context),
child: RefreshIndicator(
color: colors.accent,
backgroundColor: colors.surface,
@@ -335,17 +334,7 @@ class _TrashViewState extends State<TrashView> {
}
}
/// Desktop trash row: like `NooFileTableRow`, but its 120px last column
/// holds a tonal "Restore" button plus a danger-tonal "Delete" button
/// (permanent delete) instead of status icons - `NooFileTableRow` has no
/// slot for screen-specific actions there. A labelled pill, not a bare
/// overflow icon: it's the row's only destructive action, not a menu of
/// several, and desktop already prefers labelled buttons to bare icons
/// (see `NooSelectionBar`'s own desktop actions). Column widths
/// (180/160/120) are matched by hand to `NooFileTableHeader`'s (private in
/// `noo_file_table.dart`) so this still lines up under it; promoting an
/// optional `actions` slot onto `NooFileTableRow` would let this fold back
/// into the shared component.
/// Trash actions use the same responsive columns as the other file tables.
class _TrashDesktopRow extends StatelessWidget {
final NooFileKind kind;
final String name;
@@ -364,83 +353,28 @@ class _TrashDesktopRow extends StatelessWidget {
});
@override
Widget build(BuildContext context) {
final colors = context.nooColors;
final metaStyle = NooText.meta.copyWith(color: colors.fg3);
return SizedBox(
height: NooSizes.rowDesktop,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: NooSpace.sm),
child: Row(
children: [
Expanded(
child: Row(
children: [
NooFileTile(kind: kind, size: NooFileTileSize.desktop),
const SizedBox(width: NooSpace.sm),
Expanded(
child: Text(
name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: NooText.body.copyWith(
fontSize: 14,
fontWeight: FontWeight.w500,
color: colors.fg1,
),
),
),
const SizedBox(width: NooSpace.md),
],
),
),
SizedBox(
width: 180,
child: Text(
col2,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: metaStyle,
),
),
SizedBox(
width: 160,
child: Text(
col3,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: metaStyle,
),
),
SizedBox(
width: 120,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerRight,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
NooButton(
variant: NooButtonVariant.tonal,
size: NooButtonSize.xs,
onTap: onRestore,
child: const Text('Restore'),
),
const SizedBox(width: 4),
NooButton(
variant: NooButtonVariant.danger,
size: NooButtonSize.xs,
onTap: onDelete,
child: const Text('Delete'),
),
],
),
),
),
],
Widget build(BuildContext context) => NooFileTableRow(
kind: kind,
name: name,
col2: col2,
col3: col3,
actions: Row(
mainAxisSize: MainAxisSize.min,
children: [
NooButton(
variant: NooButtonVariant.tonal,
size: NooButtonSize.xs,
onTap: onRestore,
child: const Text('Restore'),
),
),
);
}
const SizedBox(width: 4),
NooButton(
variant: NooButtonVariant.danger,
size: NooButtonSize.xs,
onTap: onDelete,
child: const Text('Delete'),
),
],
),
);
}