Compare commits
11 Commits
3.3.0+146
...
f92cfafda4
| Author | SHA1 | Date | |
|---|---|---|---|
|
f92cfafda4
|
|||
|
fa208b44d7
|
|||
|
94adecafbb
|
|||
|
0303ef4a93
|
|||
|
c2b18ce10b
|
|||
|
0767bb53ce
|
|||
|
b233f9a410
|
|||
|
256024fb46
|
|||
|
4a80aaf24d
|
|||
|
aafd160c44
|
|||
|
4a800725e3
|
@@ -11,12 +11,18 @@ part 'file_list.g.dart';
|
|||||||
class CloudFileListNotifier extends _$CloudFileListNotifier
|
class CloudFileListNotifier extends _$CloudFileListNotifier
|
||||||
with CursorPagingNotifierMixin<FileListItem> {
|
with CursorPagingNotifierMixin<FileListItem> {
|
||||||
String _currentPath = '/';
|
String _currentPath = '/';
|
||||||
|
String? _poolId;
|
||||||
|
|
||||||
void setPath(String path) {
|
void setPath(String path) {
|
||||||
_currentPath = path;
|
_currentPath = path;
|
||||||
ref.invalidateSelf();
|
ref.invalidateSelf();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setPool(String? poolId) {
|
||||||
|
_poolId = poolId;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
||||||
|
|
||||||
@@ -26,9 +32,15 @@ class CloudFileListNotifier extends _$CloudFileListNotifier
|
|||||||
}) async {
|
}) async {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
|
|
||||||
|
final queryParameters = <String, String>{'path': _currentPath};
|
||||||
|
|
||||||
|
if (_poolId != null) {
|
||||||
|
queryParameters['pool'] = _poolId!;
|
||||||
|
}
|
||||||
|
|
||||||
final response = await client.get(
|
final response = await client.get(
|
||||||
'/drive/index/browse',
|
'/drive/index/browse',
|
||||||
queryParameters: {'path': _currentPath},
|
queryParameters: queryParameters,
|
||||||
);
|
);
|
||||||
|
|
||||||
final List<String> folders =
|
final List<String> folders =
|
||||||
@@ -58,6 +70,19 @@ Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
|
|||||||
@riverpod
|
@riverpod
|
||||||
class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
||||||
with CursorPagingNotifierMixin<FileListItem> {
|
with CursorPagingNotifierMixin<FileListItem> {
|
||||||
|
String? _poolId;
|
||||||
|
bool _recycled = false;
|
||||||
|
|
||||||
|
void setPool(String? poolId) {
|
||||||
|
_poolId = poolId;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setRecycled(bool recycled) {
|
||||||
|
_recycled = recycled;
|
||||||
|
ref.invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
||||||
|
|
||||||
@@ -70,9 +95,22 @@ class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
|||||||
final offset = cursor != null ? int.tryParse(cursor) ?? 0 : 0;
|
final offset = cursor != null ? int.tryParse(cursor) ?? 0 : 0;
|
||||||
const take = 50; // Default page size
|
const take = 50; // Default page size
|
||||||
|
|
||||||
|
final queryParameters = <String, String>{
|
||||||
|
'take': take.toString(),
|
||||||
|
'offset': offset.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_poolId != null) {
|
||||||
|
queryParameters['pool'] = _poolId!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_recycled) {
|
||||||
|
queryParameters['recycled'] = _recycled.toString();
|
||||||
|
}
|
||||||
|
|
||||||
final response = await client.get(
|
final response = await client.get(
|
||||||
'/drive/index/unindexed',
|
'/drive/index/unindexed',
|
||||||
queryParameters: {'take': take.toString(), 'offset': offset.toString()},
|
queryParameters: queryParameters,
|
||||||
);
|
);
|
||||||
|
|
||||||
final total = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0;
|
final total = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0;
|
||||||
|
|||||||
@@ -258,6 +258,24 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
|||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void updateDownloadProgress(
|
||||||
|
String taskId,
|
||||||
|
int downloadedBytes,
|
||||||
|
int totalBytes,
|
||||||
|
) {
|
||||||
|
state =
|
||||||
|
state.map((task) {
|
||||||
|
if (task.taskId == taskId) {
|
||||||
|
return task.copyWith(
|
||||||
|
fileSize: totalBytes,
|
||||||
|
uploadedBytes: downloadedBytes,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
void removeTask(String taskId) {
|
void removeTask(String taskId) {
|
||||||
state = state.where((task) => task.taskId != taskId).toList();
|
state = state.where((task) => task.taskId != taskId).toList();
|
||||||
}
|
}
|
||||||
@@ -291,6 +309,27 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String addLocalDownloadTask(SnCloudFile item) {
|
||||||
|
final taskId =
|
||||||
|
'download-${item.id}-${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
final task = DriveTask(
|
||||||
|
id: taskId,
|
||||||
|
taskId: taskId,
|
||||||
|
fileName: item.name,
|
||||||
|
contentType: item.mimeType ?? '',
|
||||||
|
fileSize: 0,
|
||||||
|
uploadedBytes: 0,
|
||||||
|
totalChunks: 1,
|
||||||
|
uploadedChunks: 0,
|
||||||
|
status: DriveTaskStatus.inProgress,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
type: 'FileDownload',
|
||||||
|
);
|
||||||
|
state = [...state, task];
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_websocketSubscription?.cancel();
|
_websocketSubscription?.cancel();
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/pods/upload_tasks.dart';
|
||||||
|
import 'package:island/models/drive_task.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
@@ -76,7 +78,7 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
}, [animationController]);
|
}, [animationController]);
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
isNoBackground: true,
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
@@ -86,26 +88,47 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
title: Text(item.name.isEmpty ? 'File Details' : item.name),
|
title: Text(item.name.isEmpty ? 'File Details' : item.name),
|
||||||
actions: _buildAppBarActions(context, ref, showInfoSheet),
|
actions: _buildAppBarActions(context, ref, showInfoSheet),
|
||||||
),
|
),
|
||||||
body: AnimatedBuilder(
|
body: LayoutBuilder(
|
||||||
animation: animation,
|
builder: (context, constraints) {
|
||||||
builder: (context, child) {
|
return AnimatedBuilder(
|
||||||
return Row(
|
animation: animation,
|
||||||
children: [
|
builder: (context, child) {
|
||||||
// Main content area
|
return Stack(
|
||||||
Expanded(child: _buildContent(context, ref, serverUrl)),
|
children: [
|
||||||
// Animated drawer panel
|
// Main content area - resizes with animation
|
||||||
if (isWide)
|
Positioned(
|
||||||
SizedBox(
|
left: 0,
|
||||||
height: double.infinity,
|
top: 0,
|
||||||
width: animation.value * 400, // Max width of 400px
|
bottom: 0,
|
||||||
child: Container(
|
width: constraints.maxWidth - animation.value * 400,
|
||||||
child:
|
child: _buildContent(context, ref, serverUrl),
|
||||||
animation.value > 0.1
|
|
||||||
? FileInfoSheet(item: item, onClose: showInfoSheet)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
),
|
||||||
),
|
// Animated drawer panel - overlays
|
||||||
],
|
if (isWide)
|
||||||
|
Positioned(
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: 400,
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: Offset((1 - animation.value) * 400, 0),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 400,
|
||||||
|
child: Material(
|
||||||
|
color:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
elevation: 8,
|
||||||
|
child: FileInfoSheet(
|
||||||
|
item: item,
|
||||||
|
onClose: showInfoSheet,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -187,6 +210,9 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _downloadFile(WidgetRef ref) async {
|
Future<void> _downloadFile(WidgetRef ref) async {
|
||||||
|
final taskId = ref
|
||||||
|
.read(uploadTasksProvider.notifier)
|
||||||
|
.addLocalDownloadTask(item);
|
||||||
try {
|
try {
|
||||||
showSnackBar('Downloading file...');
|
showSnackBar('Downloading file...');
|
||||||
|
|
||||||
@@ -202,14 +228,34 @@ class FileDetailScreen extends HookConsumerWidget {
|
|||||||
'/drive/files/${item.id}',
|
'/drive/files/${item.id}',
|
||||||
filePath,
|
filePath,
|
||||||
queryParameters: {'original': true},
|
queryParameters: {'original': true},
|
||||||
|
onReceiveProgress: (count, total) {
|
||||||
|
if (total > 0) {
|
||||||
|
ref
|
||||||
|
.read(uploadTasksProvider.notifier)
|
||||||
|
.updateDownloadProgress(taskId, count, total);
|
||||||
|
ref
|
||||||
|
.read(uploadTasksProvider.notifier)
|
||||||
|
.updateTransmissionProgress(taskId, count / total);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await FileSaver.instance.saveFile(
|
await FileSaver.instance.saveFile(
|
||||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
||||||
file: File(filePath),
|
file: File(filePath),
|
||||||
);
|
);
|
||||||
|
ref
|
||||||
|
.read(uploadTasksProvider.notifier)
|
||||||
|
.updateTaskStatus(taskId, DriveTaskStatus.completed);
|
||||||
showSnackBar('File saved to downloads');
|
showSnackBar('File saved to downloads');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
ref
|
||||||
|
.read(uploadTasksProvider.notifier)
|
||||||
|
.updateTaskStatus(
|
||||||
|
taskId,
|
||||||
|
DriveTaskStatus.failed,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
);
|
||||||
showErrorAlert(e);
|
showErrorAlert(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:cross_file/cross_file.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
|
import 'package:island/models/file_pool.dart';
|
||||||
import 'package:island/pods/file_list.dart';
|
import 'package:island/pods/file_list.dart';
|
||||||
import 'package:island/services/file_uploader.dart';
|
import 'package:island/services/file_uploader.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
@@ -23,6 +25,7 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
// Path navigation state
|
// Path navigation state
|
||||||
final currentPath = useState<String>('/');
|
final currentPath = useState<String>('/');
|
||||||
final mode = useState<FileListMode>(FileListMode.normal);
|
final mode = useState<FileListMode>(FileListMode.normal);
|
||||||
|
final selectedPool = useState<SnFilePool?>(null);
|
||||||
|
|
||||||
final usageAsync = ref.watch(billingUsageProvider);
|
final usageAsync = ref.watch(billingUsageProvider);
|
||||||
final quotaAsync = ref.watch(billingQuotaProvider);
|
final quotaAsync = ref.watch(billingQuotaProvider);
|
||||||
@@ -32,7 +35,7 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
isNoBackground: false,
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Files'),
|
title: Text('files').tr(),
|
||||||
leading: const PageBackButton(),
|
leading: const PageBackButton(),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -55,8 +58,13 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
usage: usage,
|
usage: usage,
|
||||||
quota: quota,
|
quota: quota,
|
||||||
currentPath: currentPath,
|
currentPath: currentPath,
|
||||||
|
selectedPool: selectedPool,
|
||||||
onPickAndUpload:
|
onPickAndUpload:
|
||||||
() => _pickAndUploadFile(ref, currentPath.value),
|
() => _pickAndUploadFile(
|
||||||
|
ref,
|
||||||
|
currentPath.value,
|
||||||
|
selectedPool.value?.id,
|
||||||
|
),
|
||||||
onShowCreateDirectory: _showCreateDirectoryDialog,
|
onShowCreateDirectory: _showCreateDirectoryDialog,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
viewMode: viewMode,
|
viewMode: viewMode,
|
||||||
@@ -70,7 +78,11 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pickAndUploadFile(WidgetRef ref, String currentPath) async {
|
Future<void> _pickAndUploadFile(
|
||||||
|
WidgetRef ref,
|
||||||
|
String currentPath,
|
||||||
|
String? poolId,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final result = await FilePicker.platform.pickFiles(
|
||||||
allowMultiple: true,
|
allowMultiple: true,
|
||||||
@@ -92,6 +104,7 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
fileData: universalFile,
|
fileData: universalFile,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
path: currentPath,
|
path: currentPath,
|
||||||
|
poolId: poolId,
|
||||||
onProgress: (progress, _) {
|
onProgress: (progress, _) {
|
||||||
// Progress is handled by the upload tasks system
|
// Progress is handled by the upload tasks system
|
||||||
if (progress != null) {
|
if (progress != null) {
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ class AccountName extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
account.nick,
|
textOverride ?? account.nick,
|
||||||
style: nameStyle,
|
style: nameStyle,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
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:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
@@ -89,19 +90,39 @@ class ImageFileContent extends HookConsumerWidget {
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: PhotoView(
|
child: Listener(
|
||||||
backgroundDecoration: BoxDecoration(
|
onPointerSignal: (pointerSignal) {
|
||||||
color: Colors.black.withOpacity(0.9),
|
try {
|
||||||
|
// Handle mouse wheel zoom - cast to dynamic to access scrollDelta
|
||||||
|
final delta =
|
||||||
|
(pointerSignal as dynamic).scrollDelta.dy as double?;
|
||||||
|
if (delta != null && delta != 0) {
|
||||||
|
final currentScale = photoViewController.scale ?? 1.0;
|
||||||
|
// Adjust scale based on scroll direction (invert for natural zoom)
|
||||||
|
final newScale =
|
||||||
|
delta > 0 ? currentScale * 0.9 : currentScale * 1.1;
|
||||||
|
// Clamp scale to reasonable bounds
|
||||||
|
final clampedScale = newScale.clamp(0.1, 10.0);
|
||||||
|
photoViewController.scale = clampedScale;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore non-scroll events
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: PhotoView(
|
||||||
|
backgroundDecoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.9),
|
||||||
|
),
|
||||||
|
controller: photoViewController,
|
||||||
|
imageProvider: CloudImageWidget.provider(
|
||||||
|
fileId: item.id,
|
||||||
|
serverUrl: ref.watch(serverUrlProvider),
|
||||||
|
original: showOriginal.value,
|
||||||
|
),
|
||||||
|
customSize: MediaQuery.of(context).size,
|
||||||
|
basePosition: Alignment.center,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
),
|
),
|
||||||
controller: photoViewController,
|
|
||||||
imageProvider: CloudImageWidget.provider(
|
|
||||||
fileId: item.id,
|
|
||||||
serverUrl: ref.watch(serverUrlProvider),
|
|
||||||
original: showOriginal.value,
|
|
||||||
),
|
|
||||||
customSize: MediaQuery.of(context).size,
|
|
||||||
basePosition: Alignment.center,
|
|
||||||
filterQuality: FilterQuality.high,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Controls overlay
|
// Controls overlay
|
||||||
@@ -245,68 +266,57 @@ class GenericFileContent extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Container(
|
child: Column(
|
||||||
margin: const EdgeInsets.all(32),
|
mainAxisSize: MainAxisSize.min,
|
||||||
padding: const EdgeInsets.all(32),
|
children: [
|
||||||
decoration: BoxDecoration(
|
Icon(
|
||||||
border: Border.all(
|
Symbols.insert_drive_file,
|
||||||
color: Theme.of(context).colorScheme.outline,
|
size: 64,
|
||||||
width: 1,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(16),
|
const Gap(16),
|
||||||
),
|
Text(
|
||||||
child: Column(
|
item.name,
|
||||||
mainAxisSize: MainAxisSize.min,
|
style: TextStyle(
|
||||||
children: [
|
fontSize: 20,
|
||||||
Icon(
|
fontWeight: FontWeight.bold,
|
||||||
Symbols.insert_drive_file,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
size: 64,
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
formatFileSize(item.size),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
const Gap(16),
|
),
|
||||||
Text(
|
const Gap(24),
|
||||||
item.name,
|
Row(
|
||||||
style: TextStyle(
|
mainAxisSize: MainAxisSize.min,
|
||||||
fontSize: 20,
|
children: [
|
||||||
fontWeight: FontWeight.bold,
|
FilledButton.icon(
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
onPressed: downloadFile,
|
||||||
|
icon: const Icon(Symbols.download),
|
||||||
|
label: Text('download').tr(),
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
const Gap(16),
|
||||||
),
|
OutlinedButton.icon(
|
||||||
const Gap(8),
|
onPressed: () {
|
||||||
Text(
|
showModalBottomSheet(
|
||||||
formatFileSize(item.size),
|
useRootNavigator: true,
|
||||||
style: TextStyle(
|
context: context,
|
||||||
fontSize: 16,
|
isScrollControlled: true,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
builder: (context) => FileInfoSheet(item: item),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.info),
|
||||||
|
label: Text('info').tr(),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
const Gap(24),
|
),
|
||||||
Row(
|
],
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
FilledButton.icon(
|
|
||||||
onPressed: downloadFile,
|
|
||||||
icon: const Icon(Symbols.download),
|
|
||||||
label: Text('download'),
|
|
||||||
),
|
|
||||||
const Gap(16),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
useRootNavigator: true,
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
builder: (context) => FileInfoSheet(item: item),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(Symbols.info),
|
|
||||||
label: Text('info'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:island/pods/network.dart';
|
|
||||||
import 'package:island/talker.dart';
|
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:media_kit_video/media_kit_video.dart';
|
import 'package:media_kit_video/media_kit_video.dart';
|
||||||
|
|
||||||
@@ -28,28 +25,12 @@ class _UniversalVideoState extends ConsumerState<UniversalVideo> {
|
|||||||
VideoController? _videoController;
|
VideoController? _videoController;
|
||||||
|
|
||||||
void _openVideo() async {
|
void _openVideo() async {
|
||||||
final url = widget.uri;
|
|
||||||
MediaKit.ensureInitialized();
|
MediaKit.ensureInitialized();
|
||||||
|
|
||||||
_player = Player();
|
_player = Player();
|
||||||
_videoController = VideoController(_player!);
|
_videoController = VideoController(_player!);
|
||||||
|
|
||||||
String? uri;
|
_player!.open(Media(widget.uri), play: widget.autoplay);
|
||||||
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
|
|
||||||
if (inCacheInfo == null) {
|
|
||||||
talker.info('[MediaPlayer] Miss cache: $url');
|
|
||||||
final token = ref.watch(tokenProvider)?.token;
|
|
||||||
DefaultCacheManager().downloadFile(
|
|
||||||
url,
|
|
||||||
authHeaders: {'Authorization': 'AtField $token'},
|
|
||||||
);
|
|
||||||
uri = url;
|
|
||||||
} else {
|
|
||||||
uri = inCacheInfo.file.path;
|
|
||||||
talker.info('[MediaPlayer] Hit cache: $url');
|
|
||||||
}
|
|
||||||
|
|
||||||
_player!.open(Media(uri), play: widget.autoplay);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:desktop_drop/desktop_drop.dart';
|
import 'package:desktop_drop/desktop_drop.dart';
|
||||||
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
@@ -8,7 +9,9 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file_list_item.dart';
|
import 'package:island/models/file_list_item.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
|
import 'package:island/models/file_pool.dart';
|
||||||
import 'package:island/pods/file_list.dart';
|
import 'package:island/pods/file_list.dart';
|
||||||
|
import 'package:island/pods/file_pool.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/services/file_uploader.dart';
|
import 'package:island/services/file_uploader.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
@@ -29,6 +32,7 @@ class FileListView extends HookConsumerWidget {
|
|||||||
final Map<String, dynamic>? usage;
|
final Map<String, dynamic>? usage;
|
||||||
final Map<String, dynamic>? quota;
|
final Map<String, dynamic>? quota;
|
||||||
final ValueNotifier<String> currentPath;
|
final ValueNotifier<String> currentPath;
|
||||||
|
final ValueNotifier<SnFilePool?> selectedPool;
|
||||||
final VoidCallback onPickAndUpload;
|
final VoidCallback onPickAndUpload;
|
||||||
final Function(BuildContext, ValueNotifier<String>) onShowCreateDirectory;
|
final Function(BuildContext, ValueNotifier<String>) onShowCreateDirectory;
|
||||||
final ValueNotifier<FileListMode> mode;
|
final ValueNotifier<FileListMode> mode;
|
||||||
@@ -38,6 +42,7 @@ class FileListView extends HookConsumerWidget {
|
|||||||
required this.usage,
|
required this.usage,
|
||||||
required this.quota,
|
required this.quota,
|
||||||
required this.currentPath,
|
required this.currentPath,
|
||||||
|
required this.selectedPool,
|
||||||
required this.onPickAndUpload,
|
required this.onPickAndUpload,
|
||||||
required this.onShowCreateDirectory,
|
required this.onShowCreateDirectory,
|
||||||
required this.mode,
|
required this.mode,
|
||||||
@@ -59,6 +64,14 @@ class FileListView extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (usage == null) return const SizedBox.shrink();
|
if (usage == null) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
final isRefreshing = ref.watch(
|
||||||
|
mode.value == FileListMode.normal
|
||||||
|
? cloudFileListNotifierProvider.select((value) => value.isLoading)
|
||||||
|
: unindexedFileListNotifierProvider.select(
|
||||||
|
(value) => value.isLoading,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final bodyWidget = switch (mode.value) {
|
final bodyWidget = switch (mode.value) {
|
||||||
FileListMode.unindexed => PagingHelperSliverView(
|
FileListMode.unindexed => PagingHelperSliverView(
|
||||||
provider: unindexedFileListNotifierProvider,
|
provider: unindexedFileListNotifierProvider,
|
||||||
@@ -101,6 +114,152 @@ class FileListView extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
final unindexedNotifier = ref.read(
|
||||||
|
unindexedFileListNotifierProvider.notifier,
|
||||||
|
);
|
||||||
|
final cloudNotifier = ref.read(cloudFileListNotifierProvider.notifier);
|
||||||
|
final recycled = useState<bool>(false);
|
||||||
|
final poolsAsync = ref.watch(poolsProvider);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
// Sync pool when mode or selectedPool changes
|
||||||
|
if (mode.value == FileListMode.unindexed) {
|
||||||
|
unindexedNotifier.setPool(selectedPool.value?.id);
|
||||||
|
} else {
|
||||||
|
cloudNotifier.setPool(selectedPool.value?.id);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [selectedPool.value, mode.value]);
|
||||||
|
|
||||||
|
final poolDropdownItems = poolsAsync.when(
|
||||||
|
data:
|
||||||
|
(pools) => [
|
||||||
|
const DropdownMenuItem<SnFilePool>(
|
||||||
|
value: null,
|
||||||
|
child: Text('All Pools', style: TextStyle(fontSize: 14)),
|
||||||
|
),
|
||||||
|
...pools.map(
|
||||||
|
(p) => DropdownMenuItem<SnFilePool>(
|
||||||
|
value: p,
|
||||||
|
child: Text(p.name, style: const TextStyle(fontSize: 14)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
loading: () => const <DropdownMenuItem<SnFilePool>>[],
|
||||||
|
error: (err, stack) => const <DropdownMenuItem<SnFilePool>>[],
|
||||||
|
);
|
||||||
|
|
||||||
|
final poolDropdown = DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton2<SnFilePool>(
|
||||||
|
value: selectedPool.value,
|
||||||
|
items: poolDropdownItems,
|
||||||
|
onChanged:
|
||||||
|
isRefreshing
|
||||||
|
? null
|
||||||
|
: (value) {
|
||||||
|
selectedPool.value = value;
|
||||||
|
if (mode.value == FileListMode.unindexed) {
|
||||||
|
unindexedNotifier.setPool(value?.id);
|
||||||
|
} else {
|
||||||
|
cloudNotifier.setPool(value?.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
customButton: Container(
|
||||||
|
height: 28,
|
||||||
|
width: 200,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(ref.context).colorScheme.outline,
|
||||||
|
),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
spacing: 6,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.pool, size: 16),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
selectedPool.value?.name ?? 'All files',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
).fontSize(12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).height(24),
|
||||||
|
),
|
||||||
|
buttonStyleData: const ButtonStyleData(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
height: 28,
|
||||||
|
width: 200,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dropdownStyleData: const DropdownStyleData(maxHeight: 200),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
late Widget pathContent;
|
||||||
|
if (mode.value == FileListMode.unindexed) {
|
||||||
|
pathContent = const Text(
|
||||||
|
'Unindexed Files',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
);
|
||||||
|
} else if (currentPath.value == '/') {
|
||||||
|
pathContent = const Text(
|
||||||
|
'Root Directory',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final pathParts =
|
||||||
|
currentPath.value
|
||||||
|
.split('/')
|
||||||
|
.where((part) => part.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
final breadcrumbs = <Widget>[];
|
||||||
|
|
||||||
|
// Add root
|
||||||
|
breadcrumbs.add(
|
||||||
|
InkWell(
|
||||||
|
onTap: () => currentPath.value = '/',
|
||||||
|
child: const Text('Root'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add path parts
|
||||||
|
String currentPathBuilder = '';
|
||||||
|
for (int i = 0; i < pathParts.length; i++) {
|
||||||
|
currentPathBuilder += '/${pathParts[i]}';
|
||||||
|
final path = currentPathBuilder;
|
||||||
|
|
||||||
|
breadcrumbs.add(const Text(' / '));
|
||||||
|
if (i == pathParts.length - 1) {
|
||||||
|
// Current directory
|
||||||
|
breadcrumbs.add(
|
||||||
|
Text(
|
||||||
|
pathParts[i],
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Clickable parent directory
|
||||||
|
breadcrumbs.add(
|
||||||
|
InkWell(
|
||||||
|
onTap: () => currentPath.value = path,
|
||||||
|
child: Text(pathParts[i]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pathContent = Wrap(
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: breadcrumbs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return DropTarget(
|
return DropTarget(
|
||||||
onDragDone: (details) async {
|
onDragDone: (details) async {
|
||||||
dragging.value = false;
|
dragging.value = false;
|
||||||
@@ -115,7 +274,8 @@ class FileListView extends HookConsumerWidget {
|
|||||||
final completer = FileUploader.createCloudFile(
|
final completer = FileUploader.createCloudFile(
|
||||||
fileData: universalFile,
|
fileData: universalFile,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
path: currentPath.value,
|
path: mode.value == FileListMode.normal ? currentPath.value : null,
|
||||||
|
poolId: selectedPool.value?.id,
|
||||||
onProgress: (progress, _) {
|
onProgress: (progress, _) {
|
||||||
// Progress is handled by the upload tasks system
|
// Progress is handled by the upload tasks system
|
||||||
if (progress != null) {
|
if (progress != null) {
|
||||||
@@ -147,9 +307,130 @@ class FileListView extends HookConsumerWidget {
|
|||||||
? Theme.of(context).primaryColor.withOpacity(0.1)
|
? Theme.of(context).primaryColor.withOpacity(0.1)
|
||||||
: null,
|
: null,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Gap(8),
|
const Gap(12),
|
||||||
_buildPathNavigation(ref, currentPath),
|
poolDropdown.padding(horizontal: 16),
|
||||||
|
const Gap(6),
|
||||||
|
Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
mode.value == FileListMode.unindexed
|
||||||
|
? Symbols.inventory_2
|
||||||
|
: currentPath.value != '/'
|
||||||
|
? Symbols.arrow_back
|
||||||
|
: Symbols.folder,
|
||||||
|
),
|
||||||
|
onPressed:
|
||||||
|
isRefreshing
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
if (mode.value == FileListMode.unindexed) {
|
||||||
|
mode.value = FileListMode.normal;
|
||||||
|
currentPath.value = '/';
|
||||||
|
} else {
|
||||||
|
final pathParts =
|
||||||
|
currentPath.value
|
||||||
|
.split('/')
|
||||||
|
.where((part) => part.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
if (pathParts.isNotEmpty) {
|
||||||
|
pathParts.removeLast();
|
||||||
|
currentPath.value =
|
||||||
|
pathParts.isEmpty
|
||||||
|
? '/'
|
||||||
|
: '/${pathParts.join('/')}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: AbsorbPointer(
|
||||||
|
absorbing: isRefreshing,
|
||||||
|
child: pathContent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
viewMode.value == FileListViewMode.list
|
||||||
|
? Symbols.view_module
|
||||||
|
: Symbols.list,
|
||||||
|
),
|
||||||
|
onPressed:
|
||||||
|
() =>
|
||||||
|
viewMode.value =
|
||||||
|
viewMode.value == FileListViewMode.list
|
||||||
|
? FileListViewMode.waterfall
|
||||||
|
: FileListViewMode.list,
|
||||||
|
tooltip:
|
||||||
|
viewMode.value == FileListViewMode.list
|
||||||
|
? 'Switch to Waterfall View'
|
||||||
|
: 'Switch to List View',
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (mode.value == FileListMode.normal)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.create_new_folder),
|
||||||
|
onPressed:
|
||||||
|
() =>
|
||||||
|
onShowCreateDirectory(ref.context, currentPath),
|
||||||
|
tooltip: 'Create Directory',
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (mode.value == FileListMode.unindexed)
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
recycled.value
|
||||||
|
? Symbols.delete_forever
|
||||||
|
: Symbols.restore_from_trash,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
recycled.value = !recycled.value;
|
||||||
|
unindexedNotifier.setRecycled(recycled.value);
|
||||||
|
},
|
||||||
|
tooltip:
|
||||||
|
recycled.value
|
||||||
|
? 'Show Active Files'
|
||||||
|
: 'Show Recycle Bin',
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.upload_file),
|
||||||
|
onPressed: onPickAndUpload,
|
||||||
|
tooltip: 'Upload File',
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).padding(horizontal: 8),
|
||||||
|
if (mode.value == FileListMode.unindexed && recycled.value)
|
||||||
|
_buildClearRecycledButton(ref).padding(horizontal: 8),
|
||||||
|
if (isRefreshing)
|
||||||
|
const LinearProgressIndicator(
|
||||||
|
minHeight: 4,
|
||||||
|
).padding(horizontal: 16, top: 6, bottom: 4),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
if (mode.value == FileListMode.normal && currentPath.value == '/')
|
if (mode.value == FileListMode.normal && currentPath.value == '/')
|
||||||
_buildUnindexedFilesEntry(ref).padding(bottom: 12),
|
_buildUnindexedFilesEntry(ref).padding(bottom: 12),
|
||||||
@@ -302,155 +583,6 @@ class FileListView extends HookConsumerWidget {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPathNavigation(
|
|
||||||
WidgetRef ref,
|
|
||||||
ValueNotifier<String> currentPath,
|
|
||||||
) {
|
|
||||||
Widget pathContent;
|
|
||||||
if (mode.value == FileListMode.unindexed) {
|
|
||||||
pathContent = Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Unindexed Files',
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else if (currentPath.value == '/') {
|
|
||||||
pathContent = Text(
|
|
||||||
'Root Directory',
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
final pathParts =
|
|
||||||
currentPath.value
|
|
||||||
.split('/')
|
|
||||||
.where((part) => part.isNotEmpty)
|
|
||||||
.toList();
|
|
||||||
final breadcrumbs = <Widget>[];
|
|
||||||
|
|
||||||
// Add root
|
|
||||||
breadcrumbs.add(
|
|
||||||
InkWell(onTap: () => currentPath.value = '/', child: Text('Root')),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add path parts
|
|
||||||
String currentPathBuilder = '';
|
|
||||||
for (int i = 0; i < pathParts.length; i++) {
|
|
||||||
currentPathBuilder += '/${pathParts[i]}';
|
|
||||||
final path = currentPathBuilder;
|
|
||||||
|
|
||||||
breadcrumbs.add(const Text(' / '));
|
|
||||||
if (i == pathParts.length - 1) {
|
|
||||||
// Current directory
|
|
||||||
breadcrumbs.add(
|
|
||||||
Text(pathParts[i], style: TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Clickable parent directory
|
|
||||||
breadcrumbs.add(
|
|
||||||
InkWell(
|
|
||||||
onTap: () => currentPath.value = path,
|
|
||||||
child: Text(pathParts[i]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pathContent = Wrap(
|
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
|
||||||
children: breadcrumbs,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SizedBox(
|
|
||||||
height: 64,
|
|
||||||
child: Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
mode.value == FileListMode.unindexed
|
|
||||||
? Symbols.inventory_2
|
|
||||||
: currentPath.value != '/'
|
|
||||||
? Symbols.arrow_back
|
|
||||||
: Symbols.folder,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
if (mode.value == FileListMode.unindexed) {
|
|
||||||
mode.value = FileListMode.normal;
|
|
||||||
currentPath.value = '/';
|
|
||||||
} else {
|
|
||||||
final pathParts =
|
|
||||||
currentPath.value
|
|
||||||
.split('/')
|
|
||||||
.where((part) => part.isNotEmpty)
|
|
||||||
.toList();
|
|
||||||
if (pathParts.isNotEmpty) {
|
|
||||||
pathParts.removeLast();
|
|
||||||
currentPath.value =
|
|
||||||
pathParts.isEmpty ? '/' : '/${pathParts.join('/')}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
visualDensity: const VisualDensity(
|
|
||||||
horizontal: -4,
|
|
||||||
vertical: -4,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
Expanded(child: pathContent),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
viewMode.value == FileListViewMode.list
|
|
||||||
? Symbols.view_module
|
|
||||||
: Symbols.list,
|
|
||||||
),
|
|
||||||
onPressed:
|
|
||||||
() =>
|
|
||||||
viewMode.value =
|
|
||||||
viewMode.value == FileListViewMode.list
|
|
||||||
? FileListViewMode.waterfall
|
|
||||||
: FileListViewMode.list,
|
|
||||||
tooltip:
|
|
||||||
viewMode.value == FileListViewMode.list
|
|
||||||
? 'Switch to Waterfall View'
|
|
||||||
: 'Switch to List View',
|
|
||||||
visualDensity: const VisualDensity(
|
|
||||||
horizontal: -4,
|
|
||||||
vertical: -4,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (mode.value == FileListMode.normal) ...[
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.create_new_folder),
|
|
||||||
onPressed:
|
|
||||||
() => onShowCreateDirectory(ref.context, currentPath),
|
|
||||||
tooltip: 'Create Directory',
|
|
||||||
visualDensity: const VisualDensity(
|
|
||||||
horizontal: -4,
|
|
||||||
vertical: -4,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Symbols.upload_file),
|
|
||||||
onPressed: onPickAndUpload,
|
|
||||||
tooltip: 'Upload File',
|
|
||||||
visualDensity: const VisualDensity(
|
|
||||||
horizontal: -4,
|
|
||||||
vertical: -4,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).padding(horizontal: 8),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildUnindexedFilesEntry(WidgetRef ref) {
|
Widget _buildUnindexedFilesEntry(WidgetRef ref) {
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -485,7 +617,10 @@ class FileListView extends HookConsumerWidget {
|
|||||||
ValueNotifier<String> currentPath,
|
ValueNotifier<String> currentPath,
|
||||||
) {
|
) {
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.fromLTRB(12, 0, 12, 16),
|
margin:
|
||||||
|
viewMode.value == FileListViewMode.waterfall
|
||||||
|
? const EdgeInsets.fromLTRB(0, 0, 0, 16)
|
||||||
|
: const EdgeInsets.fromLTRB(12, 0, 12, 16),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -922,7 +1057,10 @@ class FileListView extends HookConsumerWidget {
|
|||||||
|
|
||||||
Widget _buildEmptyUnindexedFilesHint(WidgetRef ref) {
|
Widget _buildEmptyUnindexedFilesHint(WidgetRef ref) {
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.fromLTRB(16, 0, 16, 0),
|
margin:
|
||||||
|
viewMode.value == FileListViewMode.waterfall
|
||||||
|
? EdgeInsets.zero
|
||||||
|
: const EdgeInsets.fromLTRB(12, 0, 12, 0),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -954,4 +1092,59 @@ class FileListView extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildClearRecycledButton(WidgetRef ref) {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
spacing: 16,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.recycling).padding(horizontal: 2),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text('Clear All Recycled Files').bold(),
|
||||||
|
const Text(
|
||||||
|
'Permanently delete all marked recycled files to free up space.',
|
||||||
|
).fontSize(13),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Symbols.delete_forever),
|
||||||
|
label: const Text('Clear'),
|
||||||
|
onPressed: () async {
|
||||||
|
final confirmed = await showConfirmAlert(
|
||||||
|
'Are you sure you want to clear all recycled files?',
|
||||||
|
'Clear Recycled Files',
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
if (ref.context.mounted) {
|
||||||
|
showLoadingModal(ref.context);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final response = await client.delete(
|
||||||
|
'/drive/files/me/recycle',
|
||||||
|
);
|
||||||
|
final count = response.data['count'] as int? ?? 0;
|
||||||
|
showSnackBar('Cleared $count recycled files.');
|
||||||
|
ref.invalidate(unindexedFileListNotifierProvider);
|
||||||
|
} catch (e) {
|
||||||
|
showSnackBar('Failed to clear recycled files.');
|
||||||
|
} finally {
|
||||||
|
if (ref.context.mounted) {
|
||||||
|
hideLoadingModal(ref.context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
@@ -29,7 +28,11 @@ class UploadOverlay extends HookConsumerWidget {
|
|||||||
.toList()
|
.toList()
|
||||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt)); // Newest first
|
..sort((a, b) => b.createdAt.compareTo(a.createdAt)); // Newest first
|
||||||
|
|
||||||
final isVisible = activeTasks.isNotEmpty;
|
final isVisibleOverride = useState<bool?>(null);
|
||||||
|
final pendingHide = useState(false);
|
||||||
|
final isVisible =
|
||||||
|
(isVisibleOverride.value ?? activeTasks.isNotEmpty) &&
|
||||||
|
!pendingHide.value;
|
||||||
final slideController = useAnimationController(
|
final slideController = useAnimationController(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
);
|
);
|
||||||
@@ -256,6 +259,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
ref
|
ref
|
||||||
.read(uploadTasksProvider.notifier)
|
.read(uploadTasksProvider.notifier)
|
||||||
.clearCompletedTasks();
|
.clearCompletedTasks();
|
||||||
|
isExpanded.value = false;
|
||||||
},
|
},
|
||||||
tileColor:
|
tileColor:
|
||||||
Theme.of(
|
Theme.of(
|
||||||
@@ -318,6 +322,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
IconData _getOverallStatusIcon(List<DriveTask> tasks) {
|
IconData _getOverallStatusIcon(List<DriveTask> tasks) {
|
||||||
if (tasks.isEmpty) return Symbols.upload;
|
if (tasks.isEmpty) return Symbols.upload;
|
||||||
|
|
||||||
|
final hasDownload = tasks.any((task) => task.type == 'FileDownload');
|
||||||
final hasInProgress = tasks.any(
|
final hasInProgress = tasks.any(
|
||||||
(task) => task.status == DriveTaskStatus.inProgress,
|
(task) => task.status == DriveTaskStatus.inProgress,
|
||||||
);
|
);
|
||||||
@@ -339,6 +344,9 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Priority order: in progress > pending > paused > failed > completed
|
// Priority order: in progress > pending > paused > failed > completed
|
||||||
if (hasInProgress) {
|
if (hasInProgress) {
|
||||||
|
if (hasDownload) {
|
||||||
|
return Symbols.download;
|
||||||
|
}
|
||||||
return Symbols.upload;
|
return Symbols.upload;
|
||||||
} else if (hasPending) {
|
} else if (hasPending) {
|
||||||
return Symbols.schedule;
|
return Symbols.schedule;
|
||||||
@@ -356,6 +364,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
String _getOverallStatusText(List<DriveTask> tasks) {
|
String _getOverallStatusText(List<DriveTask> tasks) {
|
||||||
if (tasks.isEmpty) return '0 tasks';
|
if (tasks.isEmpty) return '0 tasks';
|
||||||
|
|
||||||
|
final hasDownload = tasks.any((task) => task.type == 'FileDownload');
|
||||||
final hasInProgress = tasks.any(
|
final hasInProgress = tasks.any(
|
||||||
(task) => task.status == DriveTaskStatus.inProgress,
|
(task) => task.status == DriveTaskStatus.inProgress,
|
||||||
);
|
);
|
||||||
@@ -377,7 +386,11 @@ class _UploadOverlayContent extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Priority order: in progress > pending > paused > failed > completed
|
// Priority order: in progress > pending > paused > failed > completed
|
||||||
if (hasInProgress) {
|
if (hasInProgress) {
|
||||||
return '${tasks.length} ${'uploading'.tr()}';
|
if (hasDownload) {
|
||||||
|
return '${tasks.length} ${'downloading'.tr()}';
|
||||||
|
} else {
|
||||||
|
return '${tasks.length} ${'uploading'.tr()}';
|
||||||
|
}
|
||||||
} else if (hasPending) {
|
} else if (hasPending) {
|
||||||
return '${tasks.length} ${'pending'.tr()}';
|
return '${tasks.length} ${'pending'.tr()}';
|
||||||
} else if (hasPaused) {
|
} else if (hasPaused) {
|
||||||
@@ -519,7 +532,10 @@ class _UploadTaskTileState extends State<UploadTaskTile>
|
|||||||
color = Theme.of(context).colorScheme.secondary;
|
color = Theme.of(context).colorScheme.secondary;
|
||||||
break;
|
break;
|
||||||
case DriveTaskStatus.inProgress:
|
case DriveTaskStatus.inProgress:
|
||||||
icon = Symbols.upload;
|
icon =
|
||||||
|
widget.task.type == 'FileDownload'
|
||||||
|
? Symbols.download
|
||||||
|
: Symbols.upload;
|
||||||
color = Theme.of(context).colorScheme.primary;
|
color = Theme.of(context).colorScheme.primary;
|
||||||
break;
|
break;
|
||||||
case DriveTaskStatus.paused:
|
case DriveTaskStatus.paused:
|
||||||
|
|||||||
Reference in New Issue
Block a user