✨ Selection and batch operations in file list
This commit is contained in:
@@ -1336,5 +1336,7 @@
|
|||||||
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
"fundCreateNewHint": "Create a new fund for your message. Select recipients and amount.",
|
||||||
"amountOfSplits": "Amount of Splits",
|
"amountOfSplits": "Amount of Splits",
|
||||||
"enterNumberOfSplits": "Enter Splits Amount",
|
"enterNumberOfSplits": "Enter Splits Amount",
|
||||||
"orCreateWith": "Or\ncreate with"
|
"orCreateWith": "Or\ncreate with",
|
||||||
|
"unindexedFiles": "Unindexed files",
|
||||||
|
"folder": "Folder"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -451,7 +451,7 @@ class PollEditorScreen extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
Navigator.of(context).pop();
|
if (context.mounted) Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Future<void> _showSetTokenDialog(BuildContext context, WidgetRef ref) async {
|
|||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
hintText: 'Enter access token',
|
hintText: 'Enter access token',
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
|||||||
@@ -67,6 +67,51 @@ class FileListView extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (usage == null) return const SizedBox.shrink();
|
if (usage == null) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
final unindexedNotifier = ref.read(
|
||||||
|
unindexedFileListNotifierProvider.notifier,
|
||||||
|
);
|
||||||
|
final cloudNotifier = ref.read(cloudFileListNotifierProvider.notifier);
|
||||||
|
final recycled = useState<bool>(false);
|
||||||
|
final poolsAsync = ref.watch(poolsProvider);
|
||||||
|
final isSelectionMode = useState<bool>(false);
|
||||||
|
final selectedFileIds = useState<Set<String>>({});
|
||||||
|
final query = useState<String?>(null);
|
||||||
|
final order = useState<String?>('date');
|
||||||
|
final orderDesc = useState<bool>(true);
|
||||||
|
final queryDebounceTimer = useRef<Timer?>(null);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (mode.value == FileListMode.unindexed) {
|
||||||
|
isSelectionMode.value = false;
|
||||||
|
selectedFileIds.value.clear();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [mode.value]);
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
// Sync query, order, and orderDesc filters
|
||||||
|
if (mode.value == FileListMode.unindexed) {
|
||||||
|
unindexedNotifier.setQuery(query.value);
|
||||||
|
unindexedNotifier.setOrder(order.value);
|
||||||
|
unindexedNotifier.setOrderDesc(orderDesc.value);
|
||||||
|
} else {
|
||||||
|
cloudNotifier.setQuery(query.value);
|
||||||
|
cloudNotifier.setOrder(order.value);
|
||||||
|
cloudNotifier.setOrderDesc(orderDesc.value);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [query.value, order.value, orderDesc.value, mode.value]);
|
||||||
|
|
||||||
final isRefreshing = ref.watch(
|
final isRefreshing = ref.watch(
|
||||||
mode.value == FileListMode.normal
|
mode.value == FileListMode.normal
|
||||||
? cloudFileListNotifierProvider.select((value) => value.isLoading)
|
? cloudFileListNotifierProvider.select((value) => value.isLoading)
|
||||||
@@ -93,6 +138,8 @@ class FileListView extends HookConsumerWidget {
|
|||||||
ref,
|
ref,
|
||||||
context,
|
context,
|
||||||
viewMode,
|
viewMode,
|
||||||
|
isSelectionMode,
|
||||||
|
selectedFileIds,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_ => PagingHelperSliverView(
|
_ => PagingHelperSliverView(
|
||||||
@@ -113,45 +160,12 @@ class FileListView extends HookConsumerWidget {
|
|||||||
context,
|
context,
|
||||||
currentPath,
|
currentPath,
|
||||||
viewMode,
|
viewMode,
|
||||||
|
isSelectionMode,
|
||||||
|
selectedFileIds,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
final unindexedNotifier = ref.read(
|
|
||||||
unindexedFileListNotifierProvider.notifier,
|
|
||||||
);
|
|
||||||
final cloudNotifier = ref.read(cloudFileListNotifierProvider.notifier);
|
|
||||||
final recycled = useState<bool>(false);
|
|
||||||
final poolsAsync = ref.watch(poolsProvider);
|
|
||||||
final query = useState<String?>(null);
|
|
||||||
final order = useState<String?>('date');
|
|
||||||
final orderDesc = useState<bool>(true);
|
|
||||||
final queryDebounceTimer = useRef<Timer?>(null);
|
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
// Sync query, order, and orderDesc filters
|
|
||||||
if (mode.value == FileListMode.unindexed) {
|
|
||||||
unindexedNotifier.setQuery(query.value);
|
|
||||||
unindexedNotifier.setOrder(order.value);
|
|
||||||
unindexedNotifier.setOrderDesc(orderDesc.value);
|
|
||||||
} else {
|
|
||||||
cloudNotifier.setQuery(query.value);
|
|
||||||
cloudNotifier.setOrder(order.value);
|
|
||||||
cloudNotifier.setOrderDesc(orderDesc.value);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [query.value, order.value, orderDesc.value, mode.value]);
|
|
||||||
|
|
||||||
late Widget pathContent;
|
late Widget pathContent;
|
||||||
if (mode.value == FileListMode.unindexed) {
|
if (mode.value == FileListMode.unindexed) {
|
||||||
pathContent = const Text(
|
pathContent = const Text(
|
||||||
@@ -344,6 +358,23 @@ class FileListView extends HookConsumerWidget {
|
|||||||
vertical: -4,
|
vertical: -4,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
isSelectionMode.value
|
||||||
|
? Symbols.close
|
||||||
|
: Symbols.select_check_box,
|
||||||
|
),
|
||||||
|
onPressed:
|
||||||
|
() => isSelectionMode.value = !isSelectionMode.value,
|
||||||
|
tooltip:
|
||||||
|
isSelectionMode.value
|
||||||
|
? 'Exit Selection Mode'
|
||||||
|
: 'Enter Selection Mode',
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -4,
|
||||||
|
),
|
||||||
|
),
|
||||||
if (mode.value == FileListMode.normal)
|
if (mode.value == FileListMode.normal)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.create_new_folder),
|
icon: const Icon(Symbols.create_new_folder),
|
||||||
@@ -406,6 +437,75 @@ class FileListView extends HookConsumerWidget {
|
|||||||
viewMode.value == FileListViewMode.waterfall ? 12 : null,
|
viewMode.value == FileListViewMode.waterfall ? 12 : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (isSelectionMode.value)
|
||||||
|
Material(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
elevation: 8,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
isSelectionMode.value = false;
|
||||||
|
selectedFileIds.value.clear();
|
||||||
|
},
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Text('${selectedFileIds.value.length} selected'),
|
||||||
|
const Spacer(),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Symbols.delete),
|
||||||
|
label: const Text('Delete'),
|
||||||
|
onPressed:
|
||||||
|
selectedFileIds.value.isNotEmpty
|
||||||
|
? () async {
|
||||||
|
final confirmed = await showConfirmAlert(
|
||||||
|
'Are you sure you want to delete the selected files?',
|
||||||
|
'Delete Selected Files',
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
if (context.mounted) {
|
||||||
|
showLoadingModal(context);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final resp = await client.post(
|
||||||
|
'/drive/files/batches/delete',
|
||||||
|
data: {
|
||||||
|
'file_ids':
|
||||||
|
selectedFileIds.value.toList(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final count = resp.data['count'] as int;
|
||||||
|
selectedFileIds.value.clear();
|
||||||
|
isSelectionMode.value = false;
|
||||||
|
ref.invalidate(
|
||||||
|
mode.value == FileListMode.normal
|
||||||
|
? cloudFileListNotifierProvider
|
||||||
|
: unindexedFileListNotifierProvider,
|
||||||
|
);
|
||||||
|
showSnackBar('Deleted $count files.');
|
||||||
|
} catch (e) {
|
||||||
|
showSnackBar(
|
||||||
|
'Failed to delete selected files.',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (context.mounted) {
|
||||||
|
hideLoadingModal(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -420,6 +520,8 @@ class FileListView extends HookConsumerWidget {
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
ValueNotifier<String> currentPath,
|
ValueNotifier<String> currentPath,
|
||||||
ValueNotifier<FileListViewMode> currentViewMode,
|
ValueNotifier<FileListViewMode> currentViewMode,
|
||||||
|
ValueNotifier<bool> isSelectionMode,
|
||||||
|
ValueNotifier<Set<String>> selectedFileIds,
|
||||||
) {
|
) {
|
||||||
return switch (currentViewMode.value) {
|
return switch (currentViewMode.value) {
|
||||||
// Waterfall mode
|
// Waterfall mode
|
||||||
@@ -440,7 +542,23 @@ class FileListView extends HookConsumerWidget {
|
|||||||
|
|
||||||
final item = items[index];
|
final item = items[index];
|
||||||
return item.map(
|
return item.map(
|
||||||
file: (fileItem) => _buildWaterfallFileTile(fileItem, ref, context),
|
file:
|
||||||
|
(fileItem) => _buildWaterfallFileTile(
|
||||||
|
fileItem,
|
||||||
|
ref,
|
||||||
|
context,
|
||||||
|
isSelectionMode.value,
|
||||||
|
selectedFileIds.value.contains(fileItem.fileIndex.id),
|
||||||
|
() {
|
||||||
|
if (selectedFileIds.value.contains(fileItem.fileIndex.id)) {
|
||||||
|
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||||
|
..remove(fileItem.fileIndex.id);
|
||||||
|
} else {
|
||||||
|
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||||
|
..add(fileItem.fileIndex.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
folder:
|
folder:
|
||||||
(folderItem) =>
|
(folderItem) =>
|
||||||
_buildWaterfallFolderTile(folderItem, currentPath, context),
|
_buildWaterfallFolderTile(folderItem, currentPath, context),
|
||||||
@@ -461,58 +579,23 @@ class FileListView extends HookConsumerWidget {
|
|||||||
|
|
||||||
final item = items[index];
|
final item = items[index];
|
||||||
return item.map(
|
return item.map(
|
||||||
file: (fileItem) {
|
file:
|
||||||
final file = fileItem.fileIndex.file;
|
(fileItem) => _buildIndexedListTile(
|
||||||
return ListTile(
|
fileItem,
|
||||||
leading: ClipRRect(
|
ref,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
context,
|
||||||
child: SizedBox(
|
isSelectionMode.value,
|
||||||
height: 48,
|
selectedFileIds.value.contains(fileItem.fileIndex.id),
|
||||||
width: 48,
|
() {
|
||||||
child: getFileIcon(file, size: 24),
|
if (selectedFileIds.value.contains(fileItem.fileIndex.id)) {
|
||||||
),
|
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||||
),
|
..remove(fileItem.fileIndex.id);
|
||||||
title:
|
} else {
|
||||||
file.name.isEmpty
|
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||||
? Text('untitled').tr().italic()
|
..add(fileItem.fileIndex.id);
|
||||||
: Text(
|
|
||||||
file.name,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
subtitle: Text(formatFileSize(file.size)),
|
|
||||||
onTap: () {
|
|
||||||
context.push('/files/${fileItem.fileIndex.id}', extra: file);
|
|
||||||
},
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: const Icon(Symbols.delete),
|
|
||||||
onPressed: () async {
|
|
||||||
final confirmed = await showConfirmAlert(
|
|
||||||
'confirmDeleteFile'.tr(),
|
|
||||||
'deleteFile'.tr(),
|
|
||||||
);
|
|
||||||
if (!confirmed) return;
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
showLoadingModal(context);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
await client.delete(
|
|
||||||
'/drive/index/remove/${fileItem.fileIndex.id}',
|
|
||||||
);
|
|
||||||
ref.invalidate(cloudFileListNotifierProvider);
|
|
||||||
} catch (e) {
|
|
||||||
showSnackBar('failedToDeleteFile'.tr());
|
|
||||||
} finally {
|
|
||||||
if (context.mounted) {
|
|
||||||
hideLoadingModal(context);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
folder:
|
folder:
|
||||||
(folderItem) => ListTile(
|
(folderItem) => ListTile(
|
||||||
leading: ClipRRect(
|
leading: ClipRRect(
|
||||||
@@ -528,7 +611,7 @@ class FileListView extends HookConsumerWidget {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
subtitle: const Text('Folder'),
|
subtitle: const Text('folder').tr(),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final newPath =
|
final newPath =
|
||||||
currentPath.value == '/'
|
currentPath.value == '/'
|
||||||
@@ -639,6 +722,9 @@ class FileListView extends HookConsumerWidget {
|
|||||||
FileItem fileItem,
|
FileItem fileItem,
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
|
bool isSelectionMode,
|
||||||
|
bool isSelected,
|
||||||
|
VoidCallback? toggleSelection,
|
||||||
) {
|
) {
|
||||||
return _buildWaterfallFileTileBase(
|
return _buildWaterfallFileTileBase(
|
||||||
fileItem.fileIndex.file,
|
fileItem.fileIndex.file,
|
||||||
@@ -674,6 +760,9 @@ class FileListView extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
isSelectionMode,
|
||||||
|
isSelected,
|
||||||
|
toggleSelection,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,6 +772,9 @@ class FileListView extends HookConsumerWidget {
|
|||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
List<Widget>? actions,
|
List<Widget>? actions,
|
||||||
|
bool isSelectionMode,
|
||||||
|
bool isSelected,
|
||||||
|
VoidCallback? toggleSelection,
|
||||||
) {
|
) {
|
||||||
final meta = file.fileMeta is Map ? (file.fileMeta as Map) : const {};
|
final meta = file.fileMeta is Map ? (file.fileMeta as Map) : const {};
|
||||||
final ratio =
|
final ratio =
|
||||||
@@ -750,7 +842,11 @@ class FileListView extends HookConsumerWidget {
|
|||||||
return InkWell(
|
return InkWell(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.push(getRoutePath(), extra: file);
|
if (isSelectionMode && toggleSelection != null) {
|
||||||
|
toggleSelection();
|
||||||
|
} else {
|
||||||
|
context.push(getRoutePath(), extra: file);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -776,7 +872,13 @@ class FileListView extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
getFileIcon(file, size: 24, tinyPreview: false),
|
if (isSelectionMode)
|
||||||
|
Checkbox(
|
||||||
|
value: isSelected,
|
||||||
|
onChanged: (value) => toggleSelection?.call(),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
getFileIcon(file, size: 24, tinyPreview: false),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -861,6 +963,8 @@ class FileListView extends HookConsumerWidget {
|
|||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
ValueNotifier<FileListViewMode> currentViewMode,
|
ValueNotifier<FileListViewMode> currentViewMode,
|
||||||
|
ValueNotifier<bool> isSelectionMode,
|
||||||
|
ValueNotifier<Set<String>> selectedFileIds,
|
||||||
) {
|
) {
|
||||||
return switch (currentViewMode.value) {
|
return switch (currentViewMode.value) {
|
||||||
// Waterfall mode
|
// Waterfall mode
|
||||||
@@ -894,6 +998,19 @@ class FileListView extends HookConsumerWidget {
|
|||||||
unindexedFileItem,
|
unindexedFileItem,
|
||||||
ref,
|
ref,
|
||||||
context,
|
context,
|
||||||
|
isSelectionMode.value,
|
||||||
|
selectedFileIds.value.contains(unindexedFileItem.file.id),
|
||||||
|
() {
|
||||||
|
if (selectedFileIds.value.contains(
|
||||||
|
unindexedFileItem.file.id,
|
||||||
|
)) {
|
||||||
|
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||||
|
..remove(unindexedFileItem.file.id);
|
||||||
|
} else {
|
||||||
|
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||||
|
..add(unindexedFileItem.file.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}, childCount: widgetCount),
|
}, childCount: widgetCount),
|
||||||
@@ -917,10 +1034,23 @@ class FileListView extends HookConsumerWidget {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
unindexedFile:
|
unindexedFile:
|
||||||
(unindexedFileItem) => _buildListUnindexedFileTile(
|
(unindexedFileItem) => _buildUnindexedListTile(
|
||||||
unindexedFileItem,
|
unindexedFileItem,
|
||||||
ref,
|
ref,
|
||||||
context,
|
context,
|
||||||
|
isSelectionMode.value,
|
||||||
|
selectedFileIds.value.contains(unindexedFileItem.file.id),
|
||||||
|
() {
|
||||||
|
if (selectedFileIds.value.contains(
|
||||||
|
unindexedFileItem.file.id,
|
||||||
|
)) {
|
||||||
|
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||||
|
..remove(unindexedFileItem.file.id);
|
||||||
|
} else {
|
||||||
|
selectedFileIds.value = Set.from(selectedFileIds.value)
|
||||||
|
..add(unindexedFileItem.file.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -928,10 +1058,149 @@ class FileListView extends HookConsumerWidget {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildIndexedListTile(
|
||||||
|
FileItem fileItem,
|
||||||
|
WidgetRef ref,
|
||||||
|
BuildContext context,
|
||||||
|
bool isSelectionMode,
|
||||||
|
bool isSelected,
|
||||||
|
VoidCallback toggleSelection,
|
||||||
|
) {
|
||||||
|
final file = fileItem.fileIndex.file;
|
||||||
|
return ListTile(
|
||||||
|
leading: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (isSelectionMode)
|
||||||
|
Checkbox(
|
||||||
|
value: isSelected,
|
||||||
|
onChanged: (value) => toggleSelection(),
|
||||||
|
),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 48,
|
||||||
|
width: 48,
|
||||||
|
child: getFileIcon(file, size: 24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
title:
|
||||||
|
file.name.isEmpty
|
||||||
|
? Text('untitled').tr().italic()
|
||||||
|
: Text(file.name, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
|
subtitle: Text(formatFileSize(file.size)),
|
||||||
|
onTap: () {
|
||||||
|
if (isSelectionMode) {
|
||||||
|
toggleSelection();
|
||||||
|
} else {
|
||||||
|
context.push('/files/${fileItem.fileIndex.id}', extra: file);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Symbols.delete),
|
||||||
|
onPressed: () async {
|
||||||
|
final confirmed = await showConfirmAlert(
|
||||||
|
'confirmDeleteFile'.tr(),
|
||||||
|
'deleteFile'.tr(),
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
showLoadingModal(context);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
await client.delete('/drive/index/remove/${fileItem.fileIndex.id}');
|
||||||
|
ref.invalidate(cloudFileListNotifierProvider);
|
||||||
|
} catch (e) {
|
||||||
|
showSnackBar('failedToDeleteFile'.tr());
|
||||||
|
} finally {
|
||||||
|
if (context.mounted) {
|
||||||
|
hideLoadingModal(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUnindexedListTile(
|
||||||
|
UnindexedFileItem unindexedFileItem,
|
||||||
|
WidgetRef ref,
|
||||||
|
BuildContext context,
|
||||||
|
bool isSelectionMode,
|
||||||
|
bool isSelected,
|
||||||
|
VoidCallback toggleSelection,
|
||||||
|
) {
|
||||||
|
final file = unindexedFileItem.file;
|
||||||
|
return ListTile(
|
||||||
|
leading: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (isSelectionMode)
|
||||||
|
Checkbox(
|
||||||
|
value: isSelected,
|
||||||
|
onChanged: (value) => toggleSelection(),
|
||||||
|
),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 48,
|
||||||
|
width: 48,
|
||||||
|
child: getFileIcon(file, size: 24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
title:
|
||||||
|
file.name.isEmpty
|
||||||
|
? Text('untitled').tr().italic()
|
||||||
|
: Text(file.name, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
|
subtitle: Text(formatFileSize(file.size)),
|
||||||
|
onTap: () {
|
||||||
|
if (isSelectionMode) {
|
||||||
|
toggleSelection();
|
||||||
|
} else {
|
||||||
|
context.push('/files/${file.id}', extra: file);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Symbols.delete),
|
||||||
|
onPressed: () async {
|
||||||
|
final confirmed = await showConfirmAlert(
|
||||||
|
'confirmDeleteFile'.tr(),
|
||||||
|
'deleteFile'.tr(),
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
showLoadingModal(context);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
await client.delete('/drive/files/${file.id}');
|
||||||
|
ref.invalidate(unindexedFileListNotifierProvider);
|
||||||
|
} catch (e) {
|
||||||
|
showSnackBar('failedToDeleteFile'.tr());
|
||||||
|
} finally {
|
||||||
|
if (context.mounted) {
|
||||||
|
hideLoadingModal(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildWaterfallUnindexedFileTile(
|
Widget _buildWaterfallUnindexedFileTile(
|
||||||
UnindexedFileItem unindexedFileItem,
|
UnindexedFileItem unindexedFileItem,
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
|
bool isSelectionMode,
|
||||||
|
bool isSelected,
|
||||||
|
VoidCallback? toggleSelection,
|
||||||
) {
|
) {
|
||||||
return _buildWaterfallFileTileBase(
|
return _buildWaterfallFileTileBase(
|
||||||
unindexedFileItem.file,
|
unindexedFileItem.file,
|
||||||
@@ -965,57 +1234,9 @@ class FileListView extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
isSelectionMode,
|
||||||
}
|
isSelected,
|
||||||
|
toggleSelection,
|
||||||
Widget _buildListUnindexedFileTile(
|
|
||||||
UnindexedFileItem unindexedFileItem,
|
|
||||||
WidgetRef ref,
|
|
||||||
BuildContext context,
|
|
||||||
) {
|
|
||||||
final file = unindexedFileItem.file;
|
|
||||||
return ListTile(
|
|
||||||
leading: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: SizedBox(
|
|
||||||
height: 48,
|
|
||||||
width: 48,
|
|
||||||
child: getFileIcon(file, size: 24),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title:
|
|
||||||
file.name.isEmpty
|
|
||||||
? Text('untitled').tr().italic()
|
|
||||||
: Text(file.name, maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
||||||
subtitle: Text(formatFileSize(file.size)),
|
|
||||||
onTap: () {
|
|
||||||
context.push('/files/${file.id}', extra: file);
|
|
||||||
},
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: const Icon(Symbols.delete),
|
|
||||||
onPressed: () async {
|
|
||||||
final confirmed = await showConfirmAlert(
|
|
||||||
'confirmDeleteFile'.tr(),
|
|
||||||
'deleteFile'.tr(),
|
|
||||||
);
|
|
||||||
if (!confirmed) return;
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
showLoadingModal(context);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
await client.delete('/drive/files/${file.id}');
|
|
||||||
ref.invalidate(unindexedFileListNotifierProvider);
|
|
||||||
} catch (e) {
|
|
||||||
showSnackBar('failedToDeleteFile'.tr());
|
|
||||||
} finally {
|
|
||||||
if (context.mounted) {
|
|
||||||
hideLoadingModal(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -457,7 +457,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
maxLines: 6,
|
maxLines: 6,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -281,8 +281,8 @@ class ComposeFundSheet extends HookConsumerWidget {
|
|||||||
// Return the fund that was just created (but not yet paid)
|
// Return the fund that was just created (but not yet paid)
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
hideLoadingModal(context);
|
hideLoadingModal(context);
|
||||||
|
Navigator.of(context).pop(fund);
|
||||||
}
|
}
|
||||||
Navigator.of(context).pop(fund);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,10 +327,10 @@ class ComposeFundSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
hideLoadingModal(context);
|
hideLoadingModal(context);
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
).pop(updatedFund);
|
||||||
}
|
}
|
||||||
Navigator.of(
|
|
||||||
context,
|
|
||||||
).pop(updatedFund);
|
|
||||||
} else {
|
} else {
|
||||||
isPushing.value = false;
|
isPushing.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -458,7 +458,7 @@ class FundClaimDialog extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Remaining amount
|
// Remaining amount
|
||||||
Text(
|
Text(
|
||||||
'${fund.remainingAmount.toStringAsFixed(2)} ${fund.currency} / ${remainingSplits} splits',
|
'${fund.remainingAmount.toStringAsFixed(2)} ${fund.currency} / $remainingSplits splits',
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
|||||||
Reference in New Issue
Block a user