Release 1.0.3: refine tablet layouts, pointer selection and account sync cleanup
This commit is contained in:
+120
-33
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user