Add share-to-upload, per-folder sort, and media viewer polish

- Accept Android's "Share to..." intent (ACTION_SEND/SEND_MULTIPLE) to
  upload a shared file, with a new destination-folder picker screen
- Unlink Files/Photos sort settings and make Files remember sort
  per folder (like per-folder view settings), instead of one shared
  global sort field for both tabs
- Media viewer: show the file name (marquee-scrolling if too long) in
  the top bar next to a resized back button, slide the top bar out of
  view instead of fading it, and unify icon color/contrast across the
  top bar, bottom action bar, and video transport controls
- Add a scrollbar thumb to every scrollable list in the app (all tab
  views via SyncedHeaderScaffold, search results, settings, the new
  upload folder picker, share sheet, and the Details sheet's tabs)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 11:05:54 -04:00
co-authored by Claude Sonnet 5
parent d5675d853c
commit 5299ac5c2c
17 changed files with 1137 additions and 702 deletions
+99 -92
View File
@@ -299,108 +299,115 @@ class _ShareSheetState extends State<ShareSheet> {
.toList();
final internalLink = internalLinkFor(provider.serverUrl, widget.item.id);
return ListView(
return Scrollbar(
controller: widget.scrollController,
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
children: [
Text(
widget.item.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 16),
_sectionHeader('Internal shares'),
const SizedBox(height: 8),
TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Type names or teams',
prefixIcon: const Icon(Icons.search_rounded),
suffixIcon: _isSearching
? const Padding(
padding: EdgeInsets.all(14),
child: SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
)
: null,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16)),
filled: true,
fillColor: colorScheme.surfaceContainerLow,
),
onChanged: _search,
),
for (final sharee in _searchResults)
ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(
sharee.type == ShareeType.group
? Icons.groups_rounded
: Icons.person_rounded,
child: ListView(
controller: widget.scrollController,
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
children: [
Text(
widget.item.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
title: Text(sharee.label),
subtitle: sharee.subtitle != null ? Text(sharee.subtitle!) : null,
onTap: () => _addSharee(sharee),
),
for (final share in userGroupShares) _shareRow(share),
if (_inherited.isNotEmpty)
ExpansionTile(
tilePadding: EdgeInsets.zero,
leading: const Icon(Icons.more_horiz_rounded),
title: Text('Others with access (${_inherited.length})'),
children: [
for (final share in _inherited)
_shareRow(share, removable: false),
],
const SizedBox(height: 16),
_sectionHeader('Internal shares'),
const SizedBox(height: 8),
TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Type names or teams',
prefixIcon: const Icon(Icons.search_rounded),
suffixIcon: _isSearching
? const Padding(
padding: EdgeInsets.all(14),
child: SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
),
filled: true,
fillColor: colorScheme.surfaceContainerLow,
),
onChanged: _search,
),
if (internalLink != null)
for (final sharee in _searchResults)
ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(
sharee.type == ShareeType.group
? Icons.groups_rounded
: Icons.person_rounded,
),
title: Text(sharee.label),
subtitle: sharee.subtitle != null ? Text(sharee.subtitle!) : null,
onTap: () => _addSharee(sharee),
),
for (final share in userGroupShares) _shareRow(share),
if (_inherited.isNotEmpty)
ExpansionTile(
tilePadding: EdgeInsets.zero,
leading: const Icon(Icons.more_horiz_rounded),
title: Text('Others with access (${_inherited.length})'),
children: [
for (final share in _inherited)
_shareRow(share, removable: false),
],
),
if (internalLink != null)
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.link_rounded),
title: const Text('Internal link'),
subtitle: const Text('For people who already have access'),
trailing: IconButton(
icon: const Icon(Icons.copy_rounded),
tooltip: 'Copy link',
onPressed: () => _copy(internalLink, 'internal link'),
),
),
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 12),
_sectionHeader('External shares'),
const SizedBox(height: 8),
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'Type an email',
prefixIcon: const Icon(Icons.email_outlined),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
),
filled: true,
fillColor: colorScheme.surfaceContainerLow,
),
onSubmitted: (_) => _addEmailShare(),
),
for (final share in emailShares) _shareRow(share),
const SizedBox(height: 8),
for (final share in publicLinkShares) _shareRow(share, copyUrl: true),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.link_rounded),
title: const Text('Internal link'),
subtitle: const Text('For people who already have access'),
title: const Text('Create public link'),
trailing: IconButton(
icon: const Icon(Icons.copy_rounded),
tooltip: 'Copy link',
onPressed: () => _copy(internalLink, 'internal link'),
icon: const Icon(Icons.add_rounded),
tooltip: 'Create public link',
onPressed: _createPublicLink,
),
),
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 12),
_sectionHeader('External shares'),
const SizedBox(height: 8),
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'Type an email',
prefixIcon: const Icon(Icons.email_outlined),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16)),
filled: true,
fillColor: colorScheme.surfaceContainerLow,
),
onSubmitted: (_) => _addEmailShare(),
),
for (final share in emailShares) _shareRow(share),
const SizedBox(height: 8),
for (final share in publicLinkShares) _shareRow(share, copyUrl: true),
ListTile(
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.link_rounded),
title: const Text('Create public link'),
trailing: IconButton(
icon: const Icon(Icons.add_rounded),
tooltip: 'Create public link',
onPressed: _createPublicLink,
),
),
],
],
),
);
}
}