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:
@@ -10,7 +10,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTask"
|
||||||
android:taskAffinity=""
|
android:taskAffinity=""
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
@@ -28,6 +28,18 @@
|
|||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!-- Lets other apps' share sheets ("Share to...") send a file here
|
||||||
|
to be uploaded, via receive_sharing_intent. -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<data android:mimeType="*/*"/>
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND_MULTIPLE"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<data android:mimeType="*/*"/>
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
|||||||
@@ -19,6 +19,21 @@ subprojects {
|
|||||||
project.evaluationDependsOn(":app")
|
project.evaluationDependsOn(":app")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// receive_sharing_intent declares Java 11 for its Java sources but leaves
|
||||||
|
// its Kotlin compilation to default to whatever JDK runs the build - with
|
||||||
|
// JDK 17 that mismatch (Java 11 vs Kotlin 17) fails with "Inconsistent JVM
|
||||||
|
// Target Compatibility". Pin just this plugin's Kotlin target down to match
|
||||||
|
// its own declared Java target, rather than touching every subproject.
|
||||||
|
project(":receive_sharing_intent") {
|
||||||
|
afterEvaluate {
|
||||||
|
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tasks.register<Delete>("clean") {
|
tasks.register<Delete>("clean") {
|
||||||
delete(rootProject.layout.buildDirectory)
|
delete(rootProject.layout.buildDirectory)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'package:dynamic_color/dynamic_color.dart';
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||||
import 'models/app_tab.dart';
|
import 'models/app_tab.dart';
|
||||||
import 'providers/server_provider.dart';
|
import 'providers/server_provider.dart';
|
||||||
import 'theme/app_theme.dart';
|
import 'theme/app_theme.dart';
|
||||||
import 'views/login_view.dart';
|
import 'views/login_view.dart';
|
||||||
import 'views/search_view.dart';
|
import 'views/search_view.dart';
|
||||||
|
import 'views/share_upload_view.dart';
|
||||||
import 'widgets/app_tab_view_builder.dart';
|
import 'widgets/app_tab_view_builder.dart';
|
||||||
import 'widgets/floating_bottom_bar.dart';
|
import 'widgets/floating_bottom_bar.dart';
|
||||||
|
|
||||||
@@ -92,6 +95,7 @@ class MainShellView extends StatefulWidget {
|
|||||||
class _MainShellViewState extends State<MainShellView> {
|
class _MainShellViewState extends State<MainShellView> {
|
||||||
late AppTab _currentTab;
|
late AppTab _currentTab;
|
||||||
late final Map<AppTab, ScrollController> _scrollControllers;
|
late final Map<AppTab, ScrollController> _scrollControllers;
|
||||||
|
StreamSubscription<List<SharedMediaFile>>? _shareSub;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -103,6 +107,32 @@ class _MainShellViewState extends State<MainShellView> {
|
|||||||
// session restore (which also has to hit the network) does, so by the
|
// session restore (which also has to hit the network) does, so by the
|
||||||
// time this shell mounts `defaultTab` already reflects the saved value.
|
// time this shell mounts `defaultTab` already reflects the saved value.
|
||||||
_currentTab = context.read<ServerProvider>().defaultTab;
|
_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
|
@override
|
||||||
@@ -110,6 +140,7 @@ class _MainShellViewState extends State<MainShellView> {
|
|||||||
for (final c in _scrollControllers.values) {
|
for (final c in _scrollControllers.values) {
|
||||||
c.dispose();
|
c.dispose();
|
||||||
}
|
}
|
||||||
|
_shareSub?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
@@ -76,6 +77,7 @@ class ServerProvider extends ChangeNotifier with WidgetsBindingObserver {
|
|||||||
static const _prefShowHiddenPhotos = 'ui_show_hidden_photos';
|
static const _prefShowHiddenPhotos = 'ui_show_hidden_photos';
|
||||||
static const _prefSortField = 'ui_sort_field';
|
static const _prefSortField = 'ui_sort_field';
|
||||||
static const _prefSortAscending = 'ui_sort_ascending';
|
static const _prefSortAscending = 'ui_sort_ascending';
|
||||||
|
static const _prefFolderSort = 'ui_folder_sort';
|
||||||
static const _prefTabOrder = 'ui_tab_order';
|
static const _prefTabOrder = 'ui_tab_order';
|
||||||
static const _prefHiddenTabs = 'ui_hidden_tabs';
|
static const _prefHiddenTabs = 'ui_hidden_tabs';
|
||||||
static const _prefDefaultTab = 'ui_default_tab';
|
static const _prefDefaultTab = 'ui_default_tab';
|
||||||
@@ -130,8 +132,17 @@ class ServerProvider extends ChangeNotifier with WidgetsBindingObserver {
|
|||||||
StorageScope _storageScope = StorageScope.cloud;
|
StorageScope _storageScope = StorageScope.cloud;
|
||||||
bool _showHiddenFiles = false;
|
bool _showHiddenFiles = false;
|
||||||
bool _showHiddenPhotos = false;
|
bool _showHiddenPhotos = false;
|
||||||
FileSortField _sortField = FileSortField.name;
|
// Photos tab sort - a single global setting (Photos has no folder concept,
|
||||||
bool _sortAscending = true;
|
// it spans the whole account).
|
||||||
|
FileSortField _photosSortField = FileSortField.name;
|
||||||
|
bool _photosSortAscending = true;
|
||||||
|
|
||||||
|
// Files tab sort - unlinked from Photos and remembered per folder path
|
||||||
|
// (like Windows Explorer's per-folder view settings), so switching
|
||||||
|
// folders can restore a different sort than the parent. Falls back to
|
||||||
|
// name/ascending for any folder with no saved entry.
|
||||||
|
final Map<String, FileSortField> _folderSortField = {};
|
||||||
|
final Map<String, bool> _folderSortAscending = {};
|
||||||
|
|
||||||
// Bottom nav tab configuration
|
// Bottom nav tab configuration
|
||||||
List<AppTab> _tabOrder = AppTab.values.toList();
|
List<AppTab> _tabOrder = AppTab.values.toList();
|
||||||
@@ -248,8 +259,12 @@ class ServerProvider extends ChangeNotifier with WidgetsBindingObserver {
|
|||||||
StorageScope get storageScope => _storageScope;
|
StorageScope get storageScope => _storageScope;
|
||||||
bool get showHiddenFiles => _showHiddenFiles;
|
bool get showHiddenFiles => _showHiddenFiles;
|
||||||
bool get showHiddenPhotos => _showHiddenPhotos;
|
bool get showHiddenPhotos => _showHiddenPhotos;
|
||||||
FileSortField get sortField => _sortField;
|
FileSortField get photosSortField => _photosSortField;
|
||||||
bool get sortAscending => _sortAscending;
|
bool get photosSortAscending => _photosSortAscending;
|
||||||
|
FileSortField get filesSortField =>
|
||||||
|
_folderSortField[_currentFolderPath] ?? FileSortField.name;
|
||||||
|
bool get filesSortAscending =>
|
||||||
|
_folderSortAscending[_currentFolderPath] ?? true;
|
||||||
CachePolicy get cachePolicy => _cachePolicy;
|
CachePolicy get cachePolicy => _cachePolicy;
|
||||||
int get cacheIntervalMinutes => _cacheIntervalMinutes;
|
int get cacheIntervalMinutes => _cacheIntervalMinutes;
|
||||||
|
|
||||||
@@ -270,8 +285,8 @@ class ServerProvider extends ChangeNotifier with WidgetsBindingObserver {
|
|||||||
/// and trigger downloads/previews directly.
|
/// and trigger downloads/previews directly.
|
||||||
NextcloudService? get service => _service;
|
NextcloudService? get service => _service;
|
||||||
|
|
||||||
int _compareItems(NextcloudItem a, NextcloudItem b) {
|
int _compareItems(NextcloudItem a, NextcloudItem b, FileSortField field) {
|
||||||
switch (_sortField) {
|
switch (field) {
|
||||||
case FileSortField.name:
|
case FileSortField.name:
|
||||||
return a.name.toLowerCase().compareTo(b.name.toLowerCase());
|
return a.name.toLowerCase().compareTo(b.name.toLowerCase());
|
||||||
case FileSortField.dateCreated:
|
case FileSortField.dateCreated:
|
||||||
@@ -324,11 +339,12 @@ class ServerProvider extends ChangeNotifier with WidgetsBindingObserver {
|
|||||||
showHidden: _showHiddenFiles,
|
showHidden: _showHiddenFiles,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final field = filesSortField;
|
||||||
final folders = filtered.where((i) => i.isFolder).toList()
|
final folders = filtered.where((i) => i.isFolder).toList()
|
||||||
..sort(_compareItems);
|
..sort((a, b) => _compareItems(a, b, field));
|
||||||
final files = filtered.where((i) => !i.isFolder).toList()
|
final files = filtered.where((i) => !i.isFolder).toList()
|
||||||
..sort(_compareItems);
|
..sort((a, b) => _compareItems(a, b, field));
|
||||||
return _sortAscending
|
return filesSortAscending
|
||||||
? [...folders, ...files]
|
? [...folders, ...files]
|
||||||
: [...folders.reversed, ...files.reversed];
|
: [...folders.reversed, ...files.reversed];
|
||||||
}
|
}
|
||||||
@@ -341,8 +357,8 @@ class ServerProvider extends ChangeNotifier with WidgetsBindingObserver {
|
|||||||
media,
|
media,
|
||||||
showFavoritesOnly: _showFavoritesOnlyPhotos,
|
showFavoritesOnly: _showFavoritesOnlyPhotos,
|
||||||
showHidden: _showHiddenPhotos,
|
showHidden: _showHiddenPhotos,
|
||||||
)..sort(_compareItems);
|
)..sort((a, b) => _compareItems(a, b, _photosSortField));
|
||||||
return _sortAscending ? filtered : filtered.reversed.toList();
|
return _photosSortAscending ? filtered : filtered.reversed.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isMediaLoading => _isMediaLoading;
|
bool get isMediaLoading => _isMediaLoading;
|
||||||
@@ -428,12 +444,34 @@ class ServerProvider extends ChangeNotifier with WidgetsBindingObserver {
|
|||||||
prefs.getBool(_prefShowHiddenPhotos) ?? _showHiddenPhotos;
|
prefs.getBool(_prefShowHiddenPhotos) ?? _showHiddenPhotos;
|
||||||
final sortFieldName = prefs.getString(_prefSortField);
|
final sortFieldName = prefs.getString(_prefSortField);
|
||||||
if (sortFieldName != null) {
|
if (sortFieldName != null) {
|
||||||
_sortField = FileSortField.values.firstWhere(
|
_photosSortField = FileSortField.values.firstWhere(
|
||||||
(f) => f.name == sortFieldName,
|
(f) => f.name == sortFieldName,
|
||||||
orElse: () => FileSortField.name,
|
orElse: () => FileSortField.name,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_sortAscending = prefs.getBool(_prefSortAscending) ?? _sortAscending;
|
_photosSortAscending =
|
||||||
|
prefs.getBool(_prefSortAscending) ?? _photosSortAscending;
|
||||||
|
|
||||||
|
final folderSortJson = prefs.getString(_prefFolderSort);
|
||||||
|
if (folderSortJson != null) {
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(folderSortJson) as Map<String, dynamic>;
|
||||||
|
for (final entry in decoded.entries) {
|
||||||
|
final value = entry.value as Map<String, dynamic>;
|
||||||
|
final fieldName = value['field'] as String?;
|
||||||
|
if (fieldName != null) {
|
||||||
|
_folderSortField[entry.key] = FileSortField.values.firstWhere(
|
||||||
|
(f) => f.name == fieldName,
|
||||||
|
orElse: () => FileSortField.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final ascending = value['ascending'] as bool?;
|
||||||
|
if (ascending != null) _folderSortAscending[entry.key] = ascending;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[ServerProvider] Folder sort restore failed: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final savedOrderNames = prefs.getStringList(_prefTabOrder);
|
final savedOrderNames = prefs.getStringList(_prefTabOrder);
|
||||||
if (savedOrderNames != null) {
|
if (savedOrderNames != null) {
|
||||||
@@ -879,17 +917,48 @@ class ServerProvider extends ChangeNotifier with WidgetsBindingObserver {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setSortField(FileSortField field) {
|
void setPhotosSortField(FileSortField field) {
|
||||||
if (_sortField == field) return;
|
if (_photosSortField == field) return;
|
||||||
_sortField = field;
|
_photosSortField = field;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
_prefsFuture.then((p) => p.setString(_prefSortField, field.name));
|
_prefsFuture.then((p) => p.setString(_prefSortField, field.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleSortOrder() {
|
void togglePhotosSortOrder() {
|
||||||
_sortAscending = !_sortAscending;
|
_photosSortAscending = !_photosSortAscending;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
_prefsFuture.then((p) => p.setBool(_prefSortAscending, _sortAscending));
|
_prefsFuture.then(
|
||||||
|
(p) => p.setBool(_prefSortAscending, _photosSortAscending),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _persistFolderSort() {
|
||||||
|
final combined = <String, dynamic>{};
|
||||||
|
for (final path in {
|
||||||
|
..._folderSortField.keys,
|
||||||
|
..._folderSortAscending.keys,
|
||||||
|
}) {
|
||||||
|
combined[path] = {
|
||||||
|
'field': _folderSortField[path]?.name,
|
||||||
|
'ascending': _folderSortAscending[path],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_prefsFuture.then(
|
||||||
|
(p) => p.setString(_prefFolderSort, jsonEncode(combined)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFilesSortField(FileSortField field) {
|
||||||
|
if (filesSortField == field) return;
|
||||||
|
_folderSortField[_currentFolderPath] = field;
|
||||||
|
notifyListeners();
|
||||||
|
_persistFolderSort();
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleFilesSortOrder() {
|
||||||
|
_folderSortAscending[_currentFolderPath] = !filesSortAscending;
|
||||||
|
notifyListeners();
|
||||||
|
_persistFolderSort();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setTabOrder(List<AppTab> order) {
|
void setTabOrder(List<AppTab> order) {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class AccountView extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
|
child: Scrollbar(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 32),
|
padding: const EdgeInsets.fromLTRB(20, 8, 20, 32),
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
@@ -429,6 +430,7 @@ class AccountView extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:math' as math;
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:file_saver/file_saver.dart';
|
import 'package:file_saver/file_saver.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:marquee/marquee.dart';
|
||||||
import 'package:open_file/open_file.dart';
|
import 'package:open_file/open_file.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@@ -312,24 +313,43 @@ class _FileViewerScreenState extends State<FileViewerScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IgnorePointer(
|
AnimatedSlide(
|
||||||
|
duration: const Duration(milliseconds: 220),
|
||||||
|
curve: Curves.easeInOutCubic,
|
||||||
|
offset: _controlsVisible ? Offset.zero : const Offset(0, -1.4),
|
||||||
|
child: IgnorePointer(
|
||||||
ignoring: !_controlsVisible,
|
ignoring: !_controlsVisible,
|
||||||
child: AnimatedOpacity(
|
|
||||||
opacity: _controlsVisible ? 1 : 0,
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(left: 20, top: 12),
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 0),
|
||||||
child: FrostedGlassContainer(
|
child: FrostedGlassContainer(
|
||||||
opacity: provider.bottomBarOpacity,
|
opacity: provider.bottomBarOpacity,
|
||||||
blurSigma: provider.bottomBarBlur,
|
blurSigma: provider.bottomBarBlur,
|
||||||
child: SizedBox(
|
child: Padding(
|
||||||
width: 48,
|
padding: const EdgeInsets.symmetric(
|
||||||
height: 48,
|
horizontal: 6,
|
||||||
child: IconButton(
|
vertical: 6,
|
||||||
icon: const Icon(Icons.arrow_back_rounded),
|
),
|
||||||
color: colorScheme.onSurfaceVariant,
|
child: Row(
|
||||||
onPressed: () => Navigator.pop(context),
|
children: [
|
||||||
|
_ActionIconButton(
|
||||||
|
icon: Icons.arrow_back_rounded,
|
||||||
|
tooltip: 'Back',
|
||||||
|
onTap: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Expanded(
|
||||||
|
child: _MarqueeTitle(
|
||||||
|
text: _currentItem.name,
|
||||||
|
style: Theme.of(context).textTheme.titleSmall
|
||||||
|
?.copyWith(
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -525,8 +545,8 @@ class _ActionIconButton extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final fg = onTap == null
|
final fg = onTap == null
|
||||||
? colorScheme.onSurfaceVariant.withValues(alpha: 0.4)
|
? colorScheme.onSurface.withValues(alpha: 0.4)
|
||||||
: (color ?? colorScheme.onSurfaceVariant);
|
: (color ?? colorScheme.onSurface);
|
||||||
|
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: tooltip,
|
message: tooltip,
|
||||||
@@ -542,6 +562,53 @@ class _ActionIconButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The media viewer's title: a plain, single-line ellipsized [Text] for
|
||||||
|
/// names that fit, or an auto-scrolling [Marquee] for names too long for
|
||||||
|
/// the available width - measured once via [TextPainter] rather than
|
||||||
|
/// always marqueeing, so a short filename just sits still like normal.
|
||||||
|
class _MarqueeTitle extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
final TextStyle? style;
|
||||||
|
|
||||||
|
const _MarqueeTitle({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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _ImagePreview extends StatefulWidget {
|
class _ImagePreview extends StatefulWidget {
|
||||||
final String url;
|
final String url;
|
||||||
final Map<String, String> headers;
|
final Map<String, String> headers;
|
||||||
@@ -770,7 +837,7 @@ class _VideoPreviewState extends State<_VideoPreview> {
|
|||||||
animation: controller,
|
animation: controller,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final fg = colorScheme.onSurfaceVariant;
|
final fg = colorScheme.onSurface;
|
||||||
// Same frosted-glass treatment (and user opacity/blur
|
// Same frosted-glass treatment (and user opacity/blur
|
||||||
// settings) as the back button and media action bar, so the
|
// settings) as the back button and media action bar, so the
|
||||||
// transport controls match the rest of the app's chrome.
|
// transport controls match the rest of the app's chrome.
|
||||||
@@ -788,14 +855,14 @@ class _VideoPreviewState extends State<_VideoPreview> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
_ActionIconButton(
|
||||||
icon: Icon(
|
icon: controller.value.isPlaying
|
||||||
controller.value.isPlaying
|
|
||||||
? Icons.pause_rounded
|
? Icons.pause_rounded
|
||||||
: Icons.play_arrow_rounded,
|
: Icons.play_arrow_rounded,
|
||||||
color: fg,
|
tooltip: controller.value.isPlaying
|
||||||
),
|
? 'Pause'
|
||||||
onPressed: () => controller.value.isPlaying
|
: 'Play',
|
||||||
|
onTap: () => controller.value.isPlaying
|
||||||
? controller.pause()
|
? controller.pause()
|
||||||
: controller.play(),
|
: controller.play(),
|
||||||
),
|
),
|
||||||
@@ -806,14 +873,14 @@ class _VideoPreviewState extends State<_VideoPreview> {
|
|||||||
style: TextStyle(color: fg, fontSize: 13),
|
style: TextStyle(color: fg, fontSize: 13),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
_ActionIconButton(
|
||||||
icon: Icon(
|
icon: controller.value.volume == 0
|
||||||
controller.value.volume == 0
|
|
||||||
? Icons.volume_off_rounded
|
? Icons.volume_off_rounded
|
||||||
: Icons.volume_up_rounded,
|
: Icons.volume_up_rounded,
|
||||||
color: fg,
|
tooltip: controller.value.volume == 0
|
||||||
),
|
? 'Unmute'
|
||||||
onPressed: () => controller.setVolume(
|
: 'Mute',
|
||||||
|
onTap: () => controller.setVolume(
|
||||||
controller.value.volume == 0 ? 1 : 0,
|
controller.value.volume == 0 ? 1 : 0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -350,19 +350,21 @@ class _FilesViewState extends State<FilesView> {
|
|||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
provider.sortAscending
|
provider.filesSortAscending
|
||||||
? Icons.arrow_upward_rounded
|
? Icons.arrow_upward_rounded
|
||||||
: Icons.arrow_downward_rounded,
|
: Icons.arrow_downward_rounded,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
tooltip: provider.sortAscending ? 'Ascending' : 'Descending',
|
tooltip: provider.filesSortAscending
|
||||||
onPressed: provider.toggleSortOrder,
|
? 'Ascending'
|
||||||
|
: 'Descending',
|
||||||
|
onPressed: provider.toggleFilesSortOrder,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SortMenuButton(
|
child: SortMenuButton(
|
||||||
field: provider.sortField,
|
field: provider.filesSortField,
|
||||||
onChanged: provider.setSortField,
|
onChanged: provider.setFilesSortField,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ToggleIconButton(
|
ToggleIconButton(
|
||||||
|
|||||||
@@ -116,19 +116,21 @@ class _PhotosViewState extends State<PhotosView> {
|
|||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
provider.sortAscending
|
provider.photosSortAscending
|
||||||
? Icons.arrow_upward_rounded
|
? Icons.arrow_upward_rounded
|
||||||
: Icons.arrow_downward_rounded,
|
: Icons.arrow_downward_rounded,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
tooltip: provider.sortAscending ? 'Ascending' : 'Descending',
|
tooltip: provider.photosSortAscending
|
||||||
onPressed: provider.toggleSortOrder,
|
? 'Ascending'
|
||||||
|
: 'Descending',
|
||||||
|
onPressed: provider.togglePhotosSortOrder,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SortMenuButton(
|
child: SortMenuButton(
|
||||||
field: provider.sortField,
|
field: provider.photosSortField,
|
||||||
onChanged: provider.setSortField,
|
onChanged: provider.setPhotosSortField,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ToggleIconButton(
|
ToggleIconButton(
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class SearchView extends StatefulWidget {
|
|||||||
class _SearchViewState extends State<SearchView> {
|
class _SearchViewState extends State<SearchView> {
|
||||||
final _controller = TextEditingController();
|
final _controller = TextEditingController();
|
||||||
final _focusNode = FocusNode();
|
final _focusNode = FocusNode();
|
||||||
|
final _resultsScrollController = ScrollController();
|
||||||
Timer? _debounce;
|
Timer? _debounce;
|
||||||
|
|
||||||
List<NextcloudItem> _results = [];
|
List<NextcloudItem> _results = [];
|
||||||
@@ -30,6 +31,7 @@ class _SearchViewState extends State<SearchView> {
|
|||||||
_debounce?.cancel();
|
_debounce?.cancel();
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
_focusNode.dispose();
|
_focusNode.dispose();
|
||||||
|
_resultsScrollController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,14 +187,21 @@ class _SearchViewState extends State<SearchView> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
return Scrollbar(
|
||||||
|
controller: _resultsScrollController,
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: _resultsScrollController,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
itemCount: _results.length,
|
itemCount: _results.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = _results[index];
|
final item = _results[index];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Icon(_iconFor(item.type), color: colorScheme.primary),
|
leading: Icon(_iconFor(item.type), color: colorScheme.primary),
|
||||||
title: Text(item.name, maxLines: 1, overflow: TextOverflow.ellipsis),
|
title: Text(
|
||||||
|
item.name,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
item.isFolder
|
item.isFolder
|
||||||
? item.path
|
? item.path
|
||||||
@@ -203,6 +212,7 @@ class _SearchViewState extends State<SearchView> {
|
|||||||
onTap: () => _openResult(item),
|
onTap: () => _openResult(item),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
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 '../widgets/breadcrumbs.dart';
|
||||||
|
|
||||||
|
/// Shown when another app shares one or more files to Noo (Android's
|
||||||
|
/// "Share to..." sheet). Lets the user browse to a destination folder, then
|
||||||
|
/// uploads every shared file into it via the same WebDAV upload path as the
|
||||||
|
/// Files tab's own "Upload File" action.
|
||||||
|
class ShareUploadView extends StatefulWidget {
|
||||||
|
final List<SharedMediaFile> files;
|
||||||
|
|
||||||
|
const ShareUploadView({super.key, required this.files});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ShareUploadView> createState() => _ShareUploadViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShareUploadViewState extends State<ShareUploadView> {
|
||||||
|
bool _uploading = false;
|
||||||
|
int _currentFileIndex = 0;
|
||||||
|
double? _currentFileProgress;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Shared files have no relationship to wherever the user was last
|
||||||
|
// browsing, so start the destination picker fresh at the root.
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) context.read<ServerProvider>().navigateToAbsoluteFolder('/');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _uploadHere(ServerProvider provider) async {
|
||||||
|
setState(() => _uploading = true);
|
||||||
|
|
||||||
|
var anyFailed = false;
|
||||||
|
for (var i = 0; i < widget.files.length; i++) {
|
||||||
|
final file = widget.files[i];
|
||||||
|
setState(() {
|
||||||
|
_currentFileIndex = i;
|
||||||
|
_currentFileProgress = 0;
|
||||||
|
});
|
||||||
|
final name = p.basename(file.path);
|
||||||
|
final success = await provider.uploadFileFromPath(
|
||||||
|
name,
|
||||||
|
file.path,
|
||||||
|
onProgress: (sent, total) {
|
||||||
|
if (total > 0 && mounted) {
|
||||||
|
setState(() => _currentFileProgress = sent / total);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!success) anyFailed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
provider.requestTab(AppTab.files);
|
||||||
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
anyFailed
|
||||||
|
? 'Some files failed to upload'
|
||||||
|
: widget.files.length == 1
|
||||||
|
? 'Uploaded ${p.basename(widget.files.first.path)}'
|
||||||
|
: 'Uploaded ${widget.files.length} files',
|
||||||
|
),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
final provider = context.watch<ServerProvider>();
|
||||||
|
final hasBreadcrumbs = provider.pathStack.length > 1;
|
||||||
|
final currentPath = provider.pathStack.last;
|
||||||
|
final currentLabel = currentPath == '/'
|
||||||
|
? 'Home'
|
||||||
|
: currentPath.split('/').where((s) => s.isNotEmpty).last;
|
||||||
|
final folders = provider.items.where((i) => i.isFolder).toList();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
widget.files.length == 1
|
||||||
|
? 'Upload ${p.basename(widget.files.first.path)}'
|
||||||
|
: 'Upload ${widget.files.length} files',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: _uploading
|
||||||
|
? Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(
|
||||||
|
value: _currentFileProgress,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
widget.files.length == 1
|
||||||
|
? 'Uploading ${p.basename(widget.files[_currentFileIndex].path)}…'
|
||||||
|
: 'Uploading ${_currentFileIndex + 1} of ${widget.files.length}…',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
children: [
|
||||||
|
if (hasBreadcrumbs)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||||
|
child: Breadcrumbs(
|
||||||
|
pathStack: provider.pathStack,
|
||||||
|
onTap: (index) => provider.navigateToPathIndex(index),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: provider.isLoading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: folders.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Text(
|
||||||
|
'No subfolders here',
|
||||||
|
style: TextStyle(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Scrollbar(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: folders.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final folder = folders[index];
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.folder_rounded,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
),
|
||||||
|
title: Text(folder.name),
|
||||||
|
trailing: const Icon(
|
||||||
|
Icons.chevron_right_rounded,
|
||||||
|
),
|
||||||
|
onTap: () =>
|
||||||
|
provider.navigateToFolder(folder.path),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
bottomNavigationBar: _uploading
|
||||||
|
? null
|
||||||
|
: SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: FilledButton.icon(
|
||||||
|
onPressed: () => _uploadHere(provider),
|
||||||
|
icon: const Icon(Icons.upload_rounded),
|
||||||
|
label: Text('Upload to $currentLabel'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,7 +53,8 @@ class _DetailsActivityTabState extends State<DetailsActivityTab> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
return Scrollbar(
|
||||||
|
child: ListView.builder(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
|
||||||
itemCount: _activities.length,
|
itemCount: _activities.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
@@ -137,6 +138,7 @@ class _DetailsActivityTabState extends State<DetailsActivityTab> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ class DetailsInfoTab extends StatelessWidget {
|
|||||||
('Favorite', item.isFavorite ? 'Yes' : 'No'),
|
('Favorite', item.isFavorite ? 'Yes' : 'No'),
|
||||||
];
|
];
|
||||||
|
|
||||||
return ListView.separated(
|
return Scrollbar(
|
||||||
|
controller: scrollController,
|
||||||
|
child: ListView.separated(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
|
||||||
itemCount: rows.length,
|
itemCount: rows.length,
|
||||||
@@ -59,6 +61,7 @@ class DetailsInfoTab extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,8 @@ class _DetailsVersionsTabState extends State<DetailsVersionsTab> {
|
|||||||
..._versions,
|
..._versions,
|
||||||
];
|
];
|
||||||
|
|
||||||
return ListView.separated(
|
return Scrollbar(
|
||||||
|
child: ListView.separated(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
|
||||||
itemCount: items.length,
|
itemCount: items.length,
|
||||||
separatorBuilder: (context, index) => const SizedBox(height: 4),
|
separatorBuilder: (context, index) => const SizedBox(height: 4),
|
||||||
@@ -155,6 +156,7 @@ class _DetailsVersionsTabState extends State<DetailsVersionsTab> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,7 +299,9 @@ class _ShareSheetState extends State<ShareSheet> {
|
|||||||
.toList();
|
.toList();
|
||||||
final internalLink = internalLinkFor(provider.serverUrl, widget.item.id);
|
final internalLink = internalLinkFor(provider.serverUrl, widget.item.id);
|
||||||
|
|
||||||
return ListView(
|
return Scrollbar(
|
||||||
|
controller: widget.scrollController,
|
||||||
|
child: ListView(
|
||||||
controller: widget.scrollController,
|
controller: widget.scrollController,
|
||||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
|
||||||
children: [
|
children: [
|
||||||
@@ -329,7 +331,9 @@ class _ShareSheetState extends State<ShareSheet> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16)),
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: colorScheme.surfaceContainerLow,
|
fillColor: colorScheme.surfaceContainerLow,
|
||||||
),
|
),
|
||||||
@@ -381,7 +385,9 @@ class _ShareSheetState extends State<ShareSheet> {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Type an email',
|
hintText: 'Type an email',
|
||||||
prefixIcon: const Icon(Icons.email_outlined),
|
prefixIcon: const Icon(Icons.email_outlined),
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(16)),
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: colorScheme.surfaceContainerLow,
|
fillColor: colorScheme.surfaceContainerLow,
|
||||||
),
|
),
|
||||||
@@ -401,6 +407,7 @@ class _ShareSheetState extends State<ShareSheet> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,14 +161,16 @@ class _SyncedHeaderScaffoldState extends State<SyncedHeaderScaffold> {
|
|||||||
children: [
|
children: [
|
||||||
NotificationListener<ScrollNotification>(
|
NotificationListener<ScrollNotification>(
|
||||||
onNotification: _handleScrollNotification,
|
onNotification: _handleScrollNotification,
|
||||||
|
child: Scrollbar(
|
||||||
|
controller: widget.scrollController,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
controller: widget.scrollController,
|
controller: widget.scrollController,
|
||||||
// Plain BouncingScrollPhysics only bounces/overscrolls reliably
|
// Plain BouncingScrollPhysics only bounces/overscrolls
|
||||||
// once content already fills the viewport — with too little
|
// reliably once content already fills the viewport — with
|
||||||
// content (e.g. a single item) the pull gesture can fail to
|
// too little content (e.g. a single item) the pull gesture
|
||||||
// register at all. AlwaysScrollableScrollPhysics keeps the pull
|
// can fail to register at all. AlwaysScrollableScrollPhysics
|
||||||
// (and therefore the sync header) working regardless of content
|
// keeps the pull (and therefore the sync header) working
|
||||||
// length.
|
// regardless of content length.
|
||||||
physics: const AlwaysScrollableScrollPhysics(
|
physics: const AlwaysScrollableScrollPhysics(
|
||||||
parent: BouncingScrollPhysics(),
|
parent: BouncingScrollPhysics(),
|
||||||
),
|
),
|
||||||
@@ -204,6 +206,7 @@ class _SyncedHeaderScaffoldState extends State<SyncedHeaderScaffold> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
// The classic Material pull-to-refresh "bubble" — a floating
|
// The classic Material pull-to-refresh "bubble" — a floating
|
||||||
// circular spinner, shown while a pull-triggered sync is in
|
// circular spinner, shown while a pull-triggered sync is in
|
||||||
// flight. Positioned just under the status bar/toolbar area so it
|
// flight. Positioned just under the status bar/toolbar area so it
|
||||||
|
|||||||
@@ -161,6 +161,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
fading_edge_scrollview:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fading_edge_scrollview
|
||||||
|
sha256: "1f84fe3ea8e251d00d5735e27502a6a250e4aa3d3b330d3fdcb475af741464ef"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.1.1"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -456,6 +464,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
marquee:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: marquee
|
||||||
|
sha256: a87e7e80c5d21434f90ad92add9f820cf68be374b226404fe881d2bba7be0862
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -704,6 +720,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.1"
|
||||||
|
receive_sharing_intent:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: receive_sharing_intent
|
||||||
|
sha256: ec76056e4d258ad708e76d85591d933678625318e411564dcb9059048ca3a593
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.8.1"
|
||||||
record_use:
|
record_use:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ dependencies:
|
|||||||
share_plus: ^13.3.0
|
share_plus: ^13.3.0
|
||||||
material_symbols_icons: ^4.2960.0
|
material_symbols_icons: ^4.2960.0
|
||||||
shared_preferences: ^2.5.5
|
shared_preferences: ^2.5.5
|
||||||
|
receive_sharing_intent: ^1.8.1
|
||||||
|
marquee: ^2.3.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user