Add GET_CONTENT picker support, fix account/login bugs, restyle selection UI
Build APK / build (push) Successful in 7m40s
Build APK / build (push) Successful in 7m40s
- Let Noo be picked as a source (Files + Photos) by other apps' native file/photo pickers via Android's GET_CONTENT intent, mirroring the existing "Share to Noo" plumbing: MainActivity.kt/PickIntentService/ PickRequest, ServerProvider.pickRequest/confirmPick/cancelPick, and pick-mode tap handling in FilesView/PhotosView. While picking, the bottom nav and MoreTabsButton are restricted to Files/Photos only. - Fix two account bugs: testConnection() failures no longer delete a still-valid stored app password (only an actual 401 does), and switchAccount() now retries a nominally-active-but-logged-out account instead of no-op'ing - both were silently locking users out of "Continue as" with no way back in except removing and re-adding the account (now exposed via login_view.dart's inline remove button). - Fix the login WebView: clear cookies before every load so "Add Account" can't silently reuse an existing session, and push it as a non-animated route to avoid a known WebView-as-PlatformView black-screen- on-pop issue under Impeller. - Redesign the Files/Photos selection toolbar to take over the top app bar entirely (SyncedHeaderScaffold.selectionBar) instead of sitting as a second bar under the controls row, with a horizontally-scrollable, uniformly-colored actions row and the same one-shot scroll-hint nudge the controls row uses. - Redesign ShareUploadView to share the same SyncedHeaderScaffold chrome as every tab, with the uploading-file summary (marqueed via the new shared MarqueeTitle widget) and upload button grouped into one rounded/elevated bottom sheet. - Restyle ShareSheet to open on the same DetailsHeader every other per-item sheet uses, and fix a couple of small design-language drifts (hardcoded text style, a non-rounded icon). - Settings' account card: icon buttons with tooltips instead of text buttons for refresh/logout/remove, divider removed.
This commit is contained in:
@@ -73,13 +73,25 @@ class DetailsSheet extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Icon/name/size/date row shared by [DetailsSheet] and the media viewer's
|
||||
/// Icon/name/size/date row shared by [DetailsSheet], the media viewer's
|
||||
/// collapsed "peek" state (which shows just this, before the user drags the
|
||||
/// sheet open far enough to reveal the tabs).
|
||||
/// sheet open far enough to reveal the tabs), and [ShareSheet]'s own title
|
||||
/// (so every per-item bottom sheet opens on the same header instead of
|
||||
/// [ShareSheet] alone using a bare title `Text`).
|
||||
class DetailsHeader extends StatelessWidget {
|
||||
final NextcloudItem item;
|
||||
|
||||
const DetailsHeader({super.key, required this.item});
|
||||
/// Defaults to this widget's own standalone inset ([DetailsSheet]); a
|
||||
/// caller whose surrounding scroll view already applies horizontal
|
||||
/// padding (e.g. [ShareSheet]'s `ListView`) should pass a padding with
|
||||
/// zero left/right to avoid doubling it up.
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
const DetailsHeader({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.padding = const EdgeInsets.fromLTRB(20, 12, 20, 8),
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -87,7 +99,7 @@ class DetailsHeader extends StatelessWidget {
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 8),
|
||||
padding: padding,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:marquee/marquee.dart';
|
||||
|
||||
/// A plain, single-line ellipsized [Text] for content that fits, or an
|
||||
/// auto-scrolling [Marquee] for text too long for the available width -
|
||||
/// measured once via [TextPainter] rather than always marqueeing, so short
|
||||
/// text just sits still like normal.
|
||||
class MarqueeTitle extends StatelessWidget {
|
||||
final String text;
|
||||
final TextStyle? style;
|
||||
|
||||
const MarqueeTitle({super.key, required this.text, required this.style});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final painter = TextPainter(
|
||||
text: TextSpan(text: text, style: style),
|
||||
maxLines: 1,
|
||||
textDirection: Directionality.of(context),
|
||||
)..layout(maxWidth: double.infinity);
|
||||
|
||||
if (painter.width <= constraints.maxWidth) {
|
||||
return Text(
|
||||
text,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: painter.height,
|
||||
child: Marquee(
|
||||
text: text,
|
||||
style: style,
|
||||
blankSpace: 48,
|
||||
velocity: 30,
|
||||
startPadding: 0,
|
||||
pauseAfterRound: const Duration(seconds: 1),
|
||||
fadingEdgeStartFraction: 0.1,
|
||||
fadingEdgeEndFraction: 0.15,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,13 @@ class MoreTabsButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<ServerProvider>();
|
||||
|
||||
// While picking (another app's GET_CONTENT request), MainShellView
|
||||
// already restricts the bottom nav to Files/Photos - this button would
|
||||
// otherwise be the one remaining way to reach a tab that isn't a valid
|
||||
// pick source (Trash, Shares, ...), so it hides entirely rather than
|
||||
// just disabling.
|
||||
if (provider.isPicking) return const SizedBox.shrink();
|
||||
|
||||
return PopupMenuButton<AppTab>(
|
||||
icon: const Icon(Icons.apps_rounded),
|
||||
tooltip: 'More tabs',
|
||||
|
||||
@@ -5,6 +5,7 @@ import '../models/nextcloud_item.dart';
|
||||
import '../models/nextcloud_share.dart';
|
||||
import '../models/nextcloud_sharee.dart';
|
||||
import '../providers/server_provider.dart';
|
||||
import 'details/details_sheet.dart' show DetailsHeader;
|
||||
import 'gradual_bottom_sheet.dart';
|
||||
|
||||
IconData _shareTypeIcon(ShareType type) {
|
||||
@@ -217,12 +218,15 @@ class _ShareSheetState extends State<ShareSheet> {
|
||||
}
|
||||
|
||||
Widget _sectionHeader(String label) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 15),
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Icon(
|
||||
@@ -301,17 +305,16 @@ class _ShareSheetState extends State<ShareSheet> {
|
||||
|
||||
return ListView(
|
||||
controller: widget.scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 24),
|
||||
children: [
|
||||
Text(
|
||||
widget.item.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
// Same icon/name/meta header as DetailsSheet, so this and its
|
||||
// sibling per-item bottom sheet open on identical chrome - just a
|
||||
// bare title `Text` here read as under-designed by comparison.
|
||||
DetailsHeader(
|
||||
item: widget.item,
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 8),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 8),
|
||||
_sectionHeader('Internal shares'),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
@@ -380,7 +383,7 @@ class _ShareSheetState extends State<ShareSheet> {
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type an email',
|
||||
prefixIcon: const Icon(Icons.email_outlined),
|
||||
prefixIcon: const Icon(Icons.email_rounded),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16)),
|
||||
filled: true,
|
||||
fillColor: colorScheme.surfaceContainerLow,
|
||||
|
||||
@@ -41,6 +41,15 @@ class SyncedHeaderScaffold extends StatefulWidget {
|
||||
/// `provider.fetchTrash` for a tab backed by different data.
|
||||
final Future<void> Function()? onRefresh;
|
||||
|
||||
/// When non-null (a tab is mid-selection), this takes over the pinned top
|
||||
/// bar entirely - replacing the sync-status chip, [actions], and the
|
||||
/// pull-to-reveal quota panel - instead of appearing as a second app bar
|
||||
/// further down in [contentSlivers]. Selection is a modal-ish state (you
|
||||
/// came here to act on specific items), so it reads better as the one
|
||||
/// thing at the very top than as a strip sandwiched under the normal
|
||||
/// chrome.
|
||||
final Widget? selectionBar;
|
||||
|
||||
const SyncedHeaderScaffold({
|
||||
super.key,
|
||||
required this.scrollController,
|
||||
@@ -48,6 +57,7 @@ class SyncedHeaderScaffold extends StatefulWidget {
|
||||
required this.actions,
|
||||
required this.contentSlivers,
|
||||
this.onRefresh,
|
||||
this.selectionBar,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -175,22 +185,32 @@ class _SyncedHeaderScaffoldState extends State<SyncedHeaderScaffold> {
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
stretch: true,
|
||||
expandedHeight: _headerLocked ? 190 : kToolbarHeight,
|
||||
stretch: widget.selectionBar == null,
|
||||
expandedHeight: widget.selectionBar != null
|
||||
? kToolbarHeight
|
||||
: (_headerLocked ? 190 : kToolbarHeight),
|
||||
collapsedHeight: kToolbarHeight,
|
||||
backgroundColor: colorScheme.surfaceContainer,
|
||||
surfaceTintColor: colorScheme.surfaceContainer,
|
||||
scrolledUnderElevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
leadingWidth: _headerLocked ? 56 : 160,
|
||||
leading: leadingWidget,
|
||||
actions: widget.actions,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: _SyncedStretchPanel(
|
||||
provider: provider,
|
||||
forceVisible: _headerLocked,
|
||||
),
|
||||
),
|
||||
leadingWidth: widget.selectionBar != null
|
||||
? 0
|
||||
: (_headerLocked ? 56 : 160),
|
||||
leading: widget.selectionBar != null ? null : leadingWidget,
|
||||
titleSpacing: widget.selectionBar != null ? 0 : null,
|
||||
title: widget.selectionBar,
|
||||
actions: widget.selectionBar != null
|
||||
? const []
|
||||
: widget.actions,
|
||||
flexibleSpace: widget.selectionBar != null
|
||||
? null
|
||||
: FlexibleSpaceBar(
|
||||
background: _SyncedStretchPanel(
|
||||
provider: provider,
|
||||
forceVisible: _headerLocked,
|
||||
),
|
||||
),
|
||||
),
|
||||
DecoratedSliver(
|
||||
decoration: BoxDecoration(
|
||||
|
||||
Reference in New Issue
Block a user