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
+31
View File
@@ -1,11 +1,14 @@
import 'dart:async';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'models/app_tab.dart';
import 'providers/server_provider.dart';
import 'theme/app_theme.dart';
import 'views/login_view.dart';
import 'views/search_view.dart';
import 'views/share_upload_view.dart';
import 'widgets/app_tab_view_builder.dart';
import 'widgets/floating_bottom_bar.dart';
@@ -92,6 +95,7 @@ class MainShellView extends StatefulWidget {
class _MainShellViewState extends State<MainShellView> {
late AppTab _currentTab;
late final Map<AppTab, ScrollController> _scrollControllers;
StreamSubscription<List<SharedMediaFile>>? _shareSub;
@override
void initState() {
@@ -103,6 +107,32 @@ class _MainShellViewState extends State<MainShellView> {
// session restore (which also has to hit the network) does, so by the
// time this shell mounts `defaultTab` already reflects the saved value.
_currentTab = context.read<ServerProvider>().defaultTab;
// Handles both a cold start via another app's "Share to..." sheet
// (getInitialMedia) and a share arriving while the app is already
// running (getMediaStream) - the plugin guarantees the stream doesn't
// re-emit whatever getInitialMedia already returned.
ReceiveSharingIntent.instance.getInitialMedia().then(_handleSharedFiles);
_shareSub = ReceiveSharingIntent.instance.getMediaStream().listen(
_handleSharedFiles,
);
}
void _handleSharedFiles(List<SharedMediaFile> files) {
final uploadable = files
.where(
(f) =>
f.type != SharedMediaType.text && f.type != SharedMediaType.url,
)
.toList();
if (uploadable.isEmpty) return;
ReceiveSharingIntent.instance.reset();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => ShareUploadView(files: uploadable)),
);
});
}
@override
@@ -110,6 +140,7 @@ class _MainShellViewState extends State<MainShellView> {
for (final c in _scrollControllers.values) {
c.dispose();
}
_shareSub?.cancel();
super.dispose();
}