🚚 Update files layout of pods

This commit is contained in:
2025-12-06 17:31:12 +08:00
parent 91da9768c1
commit 240509ceff
48 changed files with 2534 additions and 2778 deletions

View File

@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/models/file_pool.dart';
import 'package:island/pods/file_pool.dart';
import 'package:island/pods/drive/file_pool.dart';
import 'package:island/widgets/content/attachment_preview.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/post/compose_shared.dart';
@@ -79,13 +79,12 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
children: [
DropdownButtonFormField<String>(
value: selectedPoolId,
items:
pools.map((pool) {
return DropdownMenuItem<String>(
value: pool.id,
child: Text(pool.name),
);
}).toList(),
items: pools.map((pool) {
return DropdownMenuItem<String>(
value: pool.id,
child: Text(pool.name),
);
}).toList(),
onChanged: (value) {
setState(() {
selectedPoolId = value;
@@ -140,10 +139,9 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color:
Theme.of(
context,
).colorScheme.errorContainer,
color: Theme.of(
context,
).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Column(
@@ -155,23 +153,22 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
Icon(
Symbols.warning,
size: 18,
color:
Theme.of(
context,
).colorScheme.error,
color: Theme.of(
context,
).colorScheme.error,
),
const Gap(8),
Text(
'uploadConstraints'.tr(),
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(
color:
Theme.of(
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: Theme.of(
context,
).colorScheme.error,
fontWeight: FontWeight.w600,
),
fontWeight: FontWeight.w600,
),
),
],
),
@@ -183,28 +180,28 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
_formatFileSize(maxFileSize),
],
),
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(
color:
Theme.of(
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: Theme.of(
context,
).colorScheme.error,
),
),
),
],
if (!typeAccepted) ...[
const Gap(4),
Text(
'fileTypeNotAccepted'.tr(),
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(
color:
Theme.of(
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: Theme.of(
context,
).colorScheme.error,
),
),
),
],
],
@@ -229,10 +226,9 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
),
],
),
style:
Theme.of(
context,
).textTheme.bodyMedium,
style: Theme.of(
context,
).textTheme.bodyMedium,
).fontSize(13),
),
],
@@ -300,8 +296,8 @@ class _AttachmentUploaderSheetState extends State<AttachmentUploaderSheet> {
final maxFileSize = selectedPool.policyConfig?['max_file_size'] as int?;
final fileSizeExceeded = maxFileSize != null && fileSize > maxFileSize;
final acceptTypes =
(selectedPool.policyConfig?['accept_types'] as List?)?.cast<String>();
final acceptTypes = (selectedPool.policyConfig?['accept_types'] as List?)
?.cast<String>();
final mimeType =
attachment.data.mimeType ??
ComposeLogic.getMimeTypeFromFileType(attachment.type);

View File

@@ -12,8 +12,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file_list_item.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_pool.dart';
import 'package:island/pods/drive/file_list.dart';
import 'package:island/pods/drive/file_pool.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/file_uploader.dart';
import 'package:island/services/responsive.dart';
@@ -123,45 +123,39 @@ class FileListView extends HookConsumerWidget {
notifier: unindexedFileListProvider.notifier,
isRefreshable: false,
isSliver: true,
contentBuilder:
(data, footer) =>
data.isEmpty
? SliverToBoxAdapter(
child: _buildEmptyUnindexedFilesHint(ref),
)
: _buildUnindexedFileListContent(
data,
ref,
context,
viewMode,
isSelectionMode,
selectedFileIds,
currentVisibleItems,
footer,
),
contentBuilder: (data, footer) => data.isEmpty
? SliverToBoxAdapter(child: _buildEmptyUnindexedFilesHint(ref))
: _buildUnindexedFileListContent(
data,
ref,
context,
viewMode,
isSelectionMode,
selectedFileIds,
currentVisibleItems,
footer,
),
),
_ => PaginationWidget(
provider: indexedCloudFileListProvider,
notifier: indexedCloudFileListProvider.notifier,
isRefreshable: false,
isSliver: true,
contentBuilder:
(data, footer) =>
data.isEmpty
? SliverToBoxAdapter(
child: _buildEmptyDirectoryHint(ref, currentPath),
)
: _buildFileListContent(
data,
ref,
context,
currentPath,
viewMode,
isSelectionMode,
selectedFileIds,
currentVisibleItems,
footer,
),
contentBuilder: (data, footer) => data.isEmpty
? SliverToBoxAdapter(
child: _buildEmptyDirectoryHint(ref, currentPath),
)
: _buildFileListContent(
data,
ref,
context,
currentPath,
viewMode,
isSelectionMode,
selectedFileIds,
currentVisibleItems,
footer,
),
),
};
@@ -177,11 +171,10 @@ class FileListView extends HookConsumerWidget {
style: TextStyle(fontWeight: FontWeight.bold),
);
} else {
final pathParts =
currentPath.value
.split('/')
.where((part) => part.isNotEmpty)
.toList();
final pathParts = currentPath.value
.split('/')
.where((part) => part.isNotEmpty)
.toList();
final breadcrumbs = <Widget>[];
// Add root
@@ -266,10 +259,9 @@ class FileListView extends HookConsumerWidget {
dragging.value = false;
},
child: Container(
color:
dragging.value
? Theme.of(context).primaryColor.withOpacity(0.1)
: null,
color: dragging.value
? Theme.of(context).primaryColor.withOpacity(0.1)
: null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -302,28 +294,25 @@ class FileListView extends HookConsumerWidget {
? 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('/')}';
}
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,
@@ -342,16 +331,13 @@ class FileListView extends HookConsumerWidget {
? Symbols.view_module
: Symbols.list,
),
onPressed:
() =>
viewMode.value =
viewMode.value == FileListViewMode.list
? FileListViewMode.waterfall
: FileListViewMode.list,
tooltip:
onPressed: () => viewMode.value =
viewMode.value == FileListViewMode.list
? 'Switch to Waterfall View'
: 'Switch to List View',
? FileListViewMode.waterfall
: FileListViewMode.list,
tooltip: viewMode.value == FileListViewMode.list
? 'Switch to Waterfall View'
: 'Switch to List View',
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
@@ -363,12 +349,11 @@ class FileListView extends HookConsumerWidget {
? Symbols.close
: Symbols.select_check_box,
),
onPressed:
() => isSelectionMode.value = !isSelectionMode.value,
tooltip:
isSelectionMode.value
? 'Exit Selection Mode'
: 'Enter Selection Mode',
onPressed: () =>
isSelectionMode.value = !isSelectionMode.value,
tooltip: isSelectionMode.value
? 'Exit Selection Mode'
: 'Enter Selection Mode',
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
@@ -377,9 +362,8 @@ class FileListView extends HookConsumerWidget {
if (mode.value == FileListMode.normal)
IconButton(
icon: const Icon(Symbols.create_new_folder),
onPressed:
() =>
onShowCreateDirectory(ref.context, currentPath),
onPressed: () =>
onShowCreateDirectory(ref.context, currentPath),
tooltip: 'Create Directory',
visualDensity: const VisualDensity(
horizontal: -4,
@@ -397,10 +381,9 @@ class FileListView extends HookConsumerWidget {
recycled.value = !recycled.value;
unindexedNotifier.setRecycled(recycled.value);
},
tooltip:
recycled.value
? 'Show Active Files'
: 'Show Recycle Bin',
tooltip: recycled.value
? 'Show Active Files'
: 'Show Recycle Bin',
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
@@ -429,12 +412,14 @@ class FileListView extends HookConsumerWidget {
if (mode.value == FileListMode.normal && currentPath.value == '/')
_buildUnindexedFilesEntry(ref).padding(bottom: 12),
Expanded(
child: CustomScrollView(
slivers: [bodyWidget, const SliverGap(12)],
).padding(
horizontal:
viewMode.value == FileListViewMode.waterfall ? 12 : null,
),
child:
CustomScrollView(
slivers: [bodyWidget, const SliverGap(12)],
).padding(
horizontal: viewMode.value == FileListViewMode.waterfall
? 12
: null,
),
),
if (isSelectionMode.value)
Material(
@@ -457,16 +442,15 @@ class FileListView extends HookConsumerWidget {
const Gap(12),
OutlinedButton(
onPressed: () {
final allIds =
currentVisibleItems.value
.expand(
(item) => item.maybeMap(
file: (f) => [f.fileIndex.id],
unindexedFile: (u) => [u.file.id],
orElse: () => <String>[],
),
)
.toSet();
final allIds = currentVisibleItems.value
.expand(
(item) => item.maybeMap(
file: (f) => [f.fileIndex.id],
unindexedFile: (u) => [u.file.id],
orElse: () => <String>[],
),
)
.toSet();
if (allIds
.difference(selectedFileIds.value)
@@ -482,16 +466,16 @@ class FileListView extends HookConsumerWidget {
currentVisibleItems.value.isEmpty
? 'Select All'
: currentVisibleItems.value
.expand(
(item) => item.maybeMap(
file: (f) => [f.fileIndex.id],
unindexedFile: (u) => [u.file.id],
orElse: () => <String>[],
),
)
.toSet()
.difference(selectedFileIds.value)
.isEmpty
.expand(
(item) => item.maybeMap(
file: (f) => [f.fileIndex.id],
unindexedFile: (u) => [u.file.id],
orElse: () => <String>[],
),
)
.toSet()
.difference(selectedFileIds.value)
.isEmpty
? 'Deselect All'
: 'Select All',
),
@@ -502,47 +486,46 @@ class FileListView extends HookConsumerWidget {
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',
isDanger: true,
onPressed: selectedFileIds.value.isNotEmpty
? () async {
final confirmed = await showConfirmAlert(
'Are you sure you want to delete the selected files?',
'Delete Selected Files',
isDanger: true,
);
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(),
},
);
if (!confirmed) return;
final count = resp.data['count'] as int;
selectedFileIds.value.clear();
isSelectionMode.value = false;
ref.invalidate(
mode.value == FileListMode.normal
? indexedCloudFileListProvider
: unindexedFileListProvider,
);
showSnackBar('Deleted $count files.');
} catch (e) {
showSnackBar(
'Failed to delete selected files.',
);
} finally {
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
? indexedCloudFileListProvider
: unindexedFileListProvider,
);
showSnackBar('Deleted $count files.');
} catch (e) {
showSnackBar(
'Failed to delete selected files.',
);
} finally {
if (context.mounted) {
hideLoadingModal(context);
}
hideLoadingModal(context);
}
}
: null,
}
: null,
),
],
),
@@ -584,26 +567,24 @@ class FileListView extends HookConsumerWidget {
final item = items[index];
return item.map(
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:
(folderItem) =>
_buildWaterfallFolderTile(folderItem, currentPath, 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: (folderItem) =>
_buildWaterfallFolderTile(folderItem, currentPath, context),
unindexedFile: (unindexedFileItem) {
// Should not happen
return const SizedBox.shrink();
@@ -620,47 +601,44 @@ class FileListView extends HookConsumerWidget {
}
final item = items[index];
return item.map(
file:
(fileItem) => _buildIndexedListTile(
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:
(folderItem) => ListTile(
leading: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: SizedBox(
height: 48,
width: 48,
child: const Icon(Symbols.folder, fill: 1).center(),
),
),
title: Text(
folderItem.folderName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: const Text('folder').tr(),
onTap: () {
final newPath =
currentPath.value == '/'
? '/${folderItem.folderName}'
: '${currentPath.value}/${folderItem.folderName}';
currentPath.value = newPath;
},
file: (fileItem) => _buildIndexedListTile(
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: (folderItem) => ListTile(
leading: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: SizedBox(
height: 48,
width: 48,
child: const Icon(Symbols.folder, fill: 1).center(),
),
),
title: Text(
folderItem.folderName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: const Text('folder').tr(),
onTap: () {
final newPath = currentPath.value == '/'
? '/${folderItem.folderName}'
: '${currentPath.value}/${folderItem.folderName}';
currentPath.value = newPath;
},
),
unindexedFile: (unindexedFileItem) {
// Should not happen in normal mode
return const SizedBox.shrink();
@@ -705,10 +683,9 @@ class FileListView extends HookConsumerWidget {
ValueNotifier<String> currentPath,
) {
return Card(
margin:
viewMode.value == FileListViewMode.waterfall
? const EdgeInsets.fromLTRB(0, 0, 0, 16)
: 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(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
child: Column(
@@ -748,8 +725,8 @@ class FileListView extends HookConsumerWidget {
),
const Gap(12),
OutlinedButton.icon(
onPressed:
() => onShowCreateDirectory(ref.context, currentPath),
onPressed: () =>
onShowCreateDirectory(ref.context, currentPath),
icon: const Icon(Symbols.create_new_folder),
label: const Text('Create Directory'),
),
@@ -822,8 +799,9 @@ class FileListView extends HookConsumerWidget {
VoidCallback? toggleSelection,
) {
final meta = file.fileMeta is Map ? (file.fileMeta as Map) : const {};
final ratio =
meta['ratio'] is num ? (meta['ratio'] as num).toDouble() : 1.0;
final ratio = meta['ratio'] is num
? (meta['ratio'] as num).toDouble()
: 1.0;
final itemType = file.mimeType?.split('/').first;
final uri =
'${ref.read(apiClientProvider).options.baseUrl}/drive/files/${file.id}';
@@ -851,22 +829,20 @@ class FileListView extends HookConsumerWidget {
.read(apiClientProvider)
.get(uri)
.then((response) => response.data as String),
builder:
(context, snapshot) =>
snapshot.hasData
? SingleChildScrollView(
padding: EdgeInsets.all(24),
child: Text(
snapshot.data!,
style: const TextStyle(
fontSize: 9,
fontFamily: 'monospace',
),
maxLines: 20,
overflow: TextOverflow.ellipsis,
),
)
: const Center(child: CircularProgressIndicator()),
builder: (context, snapshot) => snapshot.hasData
? SingleChildScrollView(
padding: EdgeInsets.all(24),
child: Text(
snapshot.data!,
style: const TextStyle(
fontSize: 9,
fontFamily: 'monospace',
),
maxLines: 20,
overflow: TextOverflow.ellipsis,
),
)
: const Center(child: CircularProgressIndicator()),
),
);
break;
@@ -961,10 +937,9 @@ class FileListView extends HookConsumerWidget {
return InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {
final newPath =
currentPath.value == '/'
? '/${folderItem.folderName}'
: '${currentPath.value}/${folderItem.folderName}';
final newPath = currentPath.value == '/'
? '/${folderItem.folderName}'
: '${currentPath.value}/${folderItem.folderName}';
currentPath.value = newPath;
},
child: Container(
@@ -1038,8 +1013,8 @@ class FileListView extends HookConsumerWidget {
// Should not happen in unindexed mode
return const SizedBox.shrink();
},
unindexedFile:
(unindexedFileItem) => _buildWaterfallUnindexedFileTile(
unindexedFile: (unindexedFileItem) =>
_buildWaterfallUnindexedFileTile(
unindexedFileItem,
ref,
context,
@@ -1077,25 +1052,22 @@ class FileListView extends HookConsumerWidget {
// Should not happen in unindexed mode
return const SizedBox.shrink();
},
unindexedFile:
(unindexedFileItem) => _buildUnindexedListTile(
unindexedFileItem,
ref,
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);
}
},
),
unindexedFile: (unindexedFileItem) => _buildUnindexedListTile(
unindexedFileItem,
ref,
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);
}
},
),
);
},
),
@@ -1130,10 +1102,9 @@ class FileListView extends HookConsumerWidget {
),
],
),
title:
file.name.isEmpty
? Text('untitled').tr().italic()
: Text(file.name, maxLines: 1, overflow: TextOverflow.ellipsis),
title: file.name.isEmpty
? Text('untitled').tr().italic()
: Text(file.name, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text(formatFileSize(file.size)),
onTap: () {
if (isSelectionMode) {
@@ -1199,10 +1170,9 @@ class FileListView extends HookConsumerWidget {
),
],
),
title:
file.name.isEmpty
? Text('untitled').tr().italic()
: Text(file.name, maxLines: 1, overflow: TextOverflow.ellipsis),
title: file.name.isEmpty
? Text('untitled').tr().italic()
: Text(file.name, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text(formatFileSize(file.size)),
onTap: () {
if (isSelectionMode) {
@@ -1289,10 +1259,9 @@ class FileListView extends HookConsumerWidget {
Widget _buildEmptyUnindexedFilesHint(WidgetRef ref) {
return Card(
margin:
viewMode.value == FileListViewMode.waterfall
? EdgeInsets.zero
: const EdgeInsets.fromLTRB(12, 0, 12, 0),
margin: viewMode.value == FileListViewMode.waterfall
? EdgeInsets.zero
: const EdgeInsets.fromLTRB(12, 0, 12, 0),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
child: Column(
@@ -1395,19 +1364,18 @@ class FileListView extends HookConsumerWidget {
ObjectRef<Timer?> queryDebounceTimer,
) {
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)),
),
),
],
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>>[],
);
@@ -1416,17 +1384,16 @@ class FileListView extends HookConsumerWidget {
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);
}
},
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,
@@ -1493,19 +1460,17 @@ class FileListView extends HookConsumerWidget {
final orderDropdown = DropdownButtonHideUnderline(
child: DropdownButton2<String>(
value: order.value,
items:
['date', 'size', 'name']
.map(
(e) => DropdownMenuItem(
value: e,
child:
Text(
e == 'date' ? e : 'file${e.capitalizeEachWord()}',
style: const TextStyle(fontSize: 14),
).tr(),
),
)
.toList(),
items: ['date', 'size', 'name']
.map(
(e) => DropdownMenuItem(
value: e,
child: Text(
e == 'date' ? e : 'file${e.capitalizeEachWord()}',
style: const TextStyle(fontSize: 14),
).tr(),
),
)
.toList(),
onChanged: (value) => order.value = value,
customButton: Container(
height: 28,
@@ -1517,13 +1482,12 @@ class FileListView extends HookConsumerWidget {
borderRadius: BorderRadius.circular(8),
),
child: Center(
child:
Text(
(order.value ?? 'date') == 'date'
? (order.value ?? 'date')
: 'file${order.value?.capitalizeEachWord()}',
style: const TextStyle(fontSize: 12),
).tr(),
child: Text(
(order.value ?? 'date') == 'date'
? (order.value ?? 'date')
: 'file${order.value?.capitalizeEachWord()}',
style: const TextStyle(fontSize: 12),
).tr(),
),
),
buttonStyleData: const ButtonStyleData(

View File

@@ -24,7 +24,7 @@ import 'package:island/widgets/post/compose_link_attachments.dart';
import 'package:island/widgets/post/compose_poll.dart';
import 'package:island/widgets/post/compose_fund.dart';
import 'package:island/widgets/post/compose_recorder.dart';
import 'package:island/pods/file_pool.dart';
import 'package:island/pods/drive/file_pool.dart';
import 'package:pasteboard/pasteboard.dart';
import 'package:island/talker.dart';
@@ -108,8 +108,8 @@ class ComposeLogic {
String? pollId;
String? fundId;
if (originalPost?.meta?['embeds'] is List) {
final embeds =
(originalPost!.meta!['embeds'] as List).cast<Map<String, dynamic>>();
final embeds = (originalPost!.meta!['embeds'] as List)
.cast<Map<String, dynamic>>();
try {
final pollEmbed = embeds.firstWhere((e) => e['type'] == 'poll');
pollId = pollEmbed['id'];
@@ -202,11 +202,10 @@ class ComposeLogic {
final attachment = state.attachments.value[i];
if (attachment.data is! SnCloudFile) {
try {
final cloudFile =
await FileUploader.createCloudFile(
ref: ref,
fileData: attachment,
).future;
final cloudFile = await FileUploader.createCloudFile(
ref: ref,
fileData: attachment,
).future;
if (cloudFile != null) {
// Update attachments list with cloud file
final clone = List.of(state.attachments.value);
@@ -242,11 +241,10 @@ class ComposeLogic {
repliedPost: null,
forwardedPostId: null,
forwardedPost: null,
attachments:
state.attachments.value
.map((e) => e.data)
.whereType<SnCloudFile>()
.toList(),
attachments: state.attachments.value
.map((e) => e.data)
.whereType<SnCloudFile>()
.toList(),
publisher: SnPublisher(
id: '',
type: 0,
@@ -315,11 +313,10 @@ class ComposeLogic {
repliedPost: null,
forwardedPostId: null,
forwardedPost: null,
attachments:
state.attachments.value
.map((e) => e.data)
.whereType<SnCloudFile>()
.toList(),
attachments: state.attachments.value
.map((e) => e.data)
.whereType<SnCloudFile>()
.toList(),
publisher: SnPublisher(
id: '',
type: 0,
@@ -501,11 +498,10 @@ class ComposeLogic {
UniversalFile value,
int index,
) {
state.attachments.value =
state.attachments.value.mapIndexed((idx, ele) {
if (idx == index) return value;
return ele;
}).toList();
state.attachments.value = state.attachments.value.mapIndexed((idx, ele) {
if (idx == index) return value;
return ele;
}).toList();
}
static Future<void> uploadAttachment(
@@ -528,22 +524,20 @@ class ComposeLogic {
final pools = await ref.read(poolsProvider.future);
final selectedPoolId = resolveDefaultPoolId(ref, pools);
cloudFile =
await FileUploader.createCloudFile(
ref: ref,
fileData: attachment,
poolId: poolId ?? selectedPoolId,
mode:
attachment.type == UniversalFileType.file
? FileUploadMode.generic
: FileUploadMode.mediaSafe,
onProgress: (progress, _) {
state.attachmentProgress.value = {
...state.attachmentProgress.value,
index: progress ?? 0.0,
};
},
).future;
cloudFile = await FileUploader.createCloudFile(
ref: ref,
fileData: attachment,
poolId: poolId ?? selectedPoolId,
mode: attachment.type == UniversalFileType.file
? FileUploadMode.generic
: FileUploadMode.mediaSafe,
onProgress: (progress, _) {
state.attachmentProgress.value = {
...state.attachmentProgress.value,
index: progress ?? 0.0,
};
},
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
@@ -713,11 +707,10 @@ class ComposeLogic {
if (state.slugController.text.isNotEmpty)
'slug': state.slugController.text,
'visibility': state.visibility.value,
'attachments':
state.attachments.value
.where((e) => e.isOnCloud)
.map((e) => e.data.id)
.toList(),
'attachments': state.attachments.value
.where((e) => e.isOnCloud)
.map((e) => e.data.id)
.toList(),
'type': state.postType,
if (repliedPost != null) 'replied_post_id': repliedPost.id,
if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id,

View File

@@ -1,81 +1,12 @@
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/paging.dart';
import 'package:island/pods/post/post_list.dart';
import 'package:island/widgets/paging/pagination_list.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/post_item_creator.dart';
import 'package:island/widgets/post/post_item_skeleton.dart';
part 'post_list.freezed.dart';
@freezed
sealed class PostListQuery with _$PostListQuery {
const factory PostListQuery({
String? pubName,
String? realm,
int? type,
List<String>? categories,
List<String>? tags,
bool? pinned,
@Default(false) bool shuffle,
bool? includeReplies,
bool? mediaOnly,
String? queryTerm,
String? order,
int? periodStart,
int? periodEnd,
@Default(true) bool orderDesc,
}) = _PostListQuery;
}
final postListNotifierProvider = AsyncNotifierProvider.autoDispose
.family<PostListNotifier, List<SnPost>, PostListQuery>(
PostListNotifier.new,
);
class PostListNotifier extends AsyncNotifier<List<SnPost>>
with AsyncPaginationController<SnPost> {
final PostListQuery arg;
PostListNotifier(this.arg);
static const int pageSize = 20;
@override
Future<List<SnPost>> fetch() async {
final client = ref.read(apiClientProvider);
final queryParams = {
'offset': fetchedCount,
'take': pageSize,
'replies': arg.includeReplies,
'orderDesc': arg.orderDesc,
if (arg.shuffle) 'shuffle': arg.shuffle,
if (arg.pubName != null) 'pub': arg.pubName,
if (arg.realm != null) 'realm': arg.realm,
if (arg.type != null) 'type': arg.type,
if (arg.tags != null) 'tags': arg.tags,
if (arg.categories != null) 'categories': arg.categories,
if (arg.pinned != null) 'pinned': arg.pinned,
if (arg.order != null) 'order': arg.order,
if (arg.periodStart != null) 'periodStart': arg.periodStart,
if (arg.periodEnd != null) 'periodEnd': arg.periodEnd,
if (arg.queryTerm != null) 'query': arg.queryTerm,
if (arg.mediaOnly != null) 'media': arg.mediaOnly,
};
final response = await client.get(
'/sphere/posts',
queryParameters: queryParams,
);
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
return data.map((json) => SnPost.fromJson(json)).toList();
}
}
/// Defines which post item widget to use in the list
enum PostItemType {
/// Regular post item with user information

View File

@@ -1,320 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'post_list.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$PostListQuery {
String? get pubName; String? get realm; int? get type; List<String>? get categories; List<String>? get tags; bool? get pinned; bool get shuffle; bool? get includeReplies; bool? get mediaOnly; String? get queryTerm; String? get order; int? get periodStart; int? get periodEnd; bool get orderDesc;
/// Create a copy of PostListQuery
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$PostListQueryCopyWith<PostListQuery> get copyWith => _$PostListQueryCopyWithImpl<PostListQuery>(this as PostListQuery, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is PostListQuery&&(identical(other.pubName, pubName) || other.pubName == pubName)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.pinned, pinned) || other.pinned == pinned)&&(identical(other.shuffle, shuffle) || other.shuffle == shuffle)&&(identical(other.includeReplies, includeReplies) || other.includeReplies == includeReplies)&&(identical(other.mediaOnly, mediaOnly) || other.mediaOnly == mediaOnly)&&(identical(other.queryTerm, queryTerm) || other.queryTerm == queryTerm)&&(identical(other.order, order) || other.order == order)&&(identical(other.periodStart, periodStart) || other.periodStart == periodStart)&&(identical(other.periodEnd, periodEnd) || other.periodEnd == periodEnd)&&(identical(other.orderDesc, orderDesc) || other.orderDesc == orderDesc));
}
@override
int get hashCode => Object.hash(runtimeType,pubName,realm,type,const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(tags),pinned,shuffle,includeReplies,mediaOnly,queryTerm,order,periodStart,periodEnd,orderDesc);
@override
String toString() {
return 'PostListQuery(pubName: $pubName, realm: $realm, type: $type, categories: $categories, tags: $tags, pinned: $pinned, shuffle: $shuffle, includeReplies: $includeReplies, mediaOnly: $mediaOnly, queryTerm: $queryTerm, order: $order, periodStart: $periodStart, periodEnd: $periodEnd, orderDesc: $orderDesc)';
}
}
/// @nodoc
abstract mixin class $PostListQueryCopyWith<$Res> {
factory $PostListQueryCopyWith(PostListQuery value, $Res Function(PostListQuery) _then) = _$PostListQueryCopyWithImpl;
@useResult
$Res call({
String? pubName, String? realm, int? type, List<String>? categories, List<String>? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc
});
}
/// @nodoc
class _$PostListQueryCopyWithImpl<$Res>
implements $PostListQueryCopyWith<$Res> {
_$PostListQueryCopyWithImpl(this._self, this._then);
final PostListQuery _self;
final $Res Function(PostListQuery) _then;
/// Create a copy of PostListQuery
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? pubName = freezed,Object? realm = freezed,Object? type = freezed,Object? categories = freezed,Object? tags = freezed,Object? pinned = freezed,Object? shuffle = null,Object? includeReplies = freezed,Object? mediaOnly = freezed,Object? queryTerm = freezed,Object? order = freezed,Object? periodStart = freezed,Object? periodEnd = freezed,Object? orderDesc = null,}) {
return _then(_self.copyWith(
pubName: freezed == pubName ? _self.pubName : pubName // ignore: cast_nullable_to_non_nullable
as String?,realm: freezed == realm ? _self.realm : realm // ignore: cast_nullable_to_non_nullable
as String?,type: freezed == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int?,categories: freezed == categories ? _self.categories : categories // ignore: cast_nullable_to_non_nullable
as List<String>?,tags: freezed == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
as List<String>?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable
as bool?,shuffle: null == shuffle ? _self.shuffle : shuffle // ignore: cast_nullable_to_non_nullable
as bool,includeReplies: freezed == includeReplies ? _self.includeReplies : includeReplies // ignore: cast_nullable_to_non_nullable
as bool?,mediaOnly: freezed == mediaOnly ? _self.mediaOnly : mediaOnly // ignore: cast_nullable_to_non_nullable
as bool?,queryTerm: freezed == queryTerm ? _self.queryTerm : queryTerm // ignore: cast_nullable_to_non_nullable
as String?,order: freezed == order ? _self.order : order // ignore: cast_nullable_to_non_nullable
as String?,periodStart: freezed == periodStart ? _self.periodStart : periodStart // ignore: cast_nullable_to_non_nullable
as int?,periodEnd: freezed == periodEnd ? _self.periodEnd : periodEnd // ignore: cast_nullable_to_non_nullable
as int?,orderDesc: null == orderDesc ? _self.orderDesc : orderDesc // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// Adds pattern-matching-related methods to [PostListQuery].
extension PostListQueryPatterns on PostListQuery {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _PostListQuery value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _PostListQuery() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _PostListQuery value) $default,){
final _that = this;
switch (_that) {
case _PostListQuery():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _PostListQuery value)? $default,){
final _that = this;
switch (_that) {
case _PostListQuery() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? pubName, String? realm, int? type, List<String>? categories, List<String>? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _PostListQuery() when $default != null:
return $default(_that.pubName,_that.realm,_that.type,_that.categories,_that.tags,_that.pinned,_that.shuffle,_that.includeReplies,_that.mediaOnly,_that.queryTerm,_that.order,_that.periodStart,_that.periodEnd,_that.orderDesc);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? pubName, String? realm, int? type, List<String>? categories, List<String>? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc) $default,) {final _that = this;
switch (_that) {
case _PostListQuery():
return $default(_that.pubName,_that.realm,_that.type,_that.categories,_that.tags,_that.pinned,_that.shuffle,_that.includeReplies,_that.mediaOnly,_that.queryTerm,_that.order,_that.periodStart,_that.periodEnd,_that.orderDesc);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? pubName, String? realm, int? type, List<String>? categories, List<String>? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc)? $default,) {final _that = this;
switch (_that) {
case _PostListQuery() when $default != null:
return $default(_that.pubName,_that.realm,_that.type,_that.categories,_that.tags,_that.pinned,_that.shuffle,_that.includeReplies,_that.mediaOnly,_that.queryTerm,_that.order,_that.periodStart,_that.periodEnd,_that.orderDesc);case _:
return null;
}
}
}
/// @nodoc
class _PostListQuery implements PostListQuery {
const _PostListQuery({this.pubName, this.realm, this.type, final List<String>? categories, final List<String>? tags, this.pinned, this.shuffle = false, this.includeReplies, this.mediaOnly, this.queryTerm, this.order, this.periodStart, this.periodEnd, this.orderDesc = true}): _categories = categories,_tags = tags;
@override final String? pubName;
@override final String? realm;
@override final int? type;
final List<String>? _categories;
@override List<String>? get categories {
final value = _categories;
if (value == null) return null;
if (_categories is EqualUnmodifiableListView) return _categories;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
final List<String>? _tags;
@override List<String>? get tags {
final value = _tags;
if (value == null) return null;
if (_tags is EqualUnmodifiableListView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override final bool? pinned;
@override@JsonKey() final bool shuffle;
@override final bool? includeReplies;
@override final bool? mediaOnly;
@override final String? queryTerm;
@override final String? order;
@override final int? periodStart;
@override final int? periodEnd;
@override@JsonKey() final bool orderDesc;
/// Create a copy of PostListQuery
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$PostListQueryCopyWith<_PostListQuery> get copyWith => __$PostListQueryCopyWithImpl<_PostListQuery>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PostListQuery&&(identical(other.pubName, pubName) || other.pubName == pubName)&&(identical(other.realm, realm) || other.realm == realm)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.pinned, pinned) || other.pinned == pinned)&&(identical(other.shuffle, shuffle) || other.shuffle == shuffle)&&(identical(other.includeReplies, includeReplies) || other.includeReplies == includeReplies)&&(identical(other.mediaOnly, mediaOnly) || other.mediaOnly == mediaOnly)&&(identical(other.queryTerm, queryTerm) || other.queryTerm == queryTerm)&&(identical(other.order, order) || other.order == order)&&(identical(other.periodStart, periodStart) || other.periodStart == periodStart)&&(identical(other.periodEnd, periodEnd) || other.periodEnd == periodEnd)&&(identical(other.orderDesc, orderDesc) || other.orderDesc == orderDesc));
}
@override
int get hashCode => Object.hash(runtimeType,pubName,realm,type,const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_tags),pinned,shuffle,includeReplies,mediaOnly,queryTerm,order,periodStart,periodEnd,orderDesc);
@override
String toString() {
return 'PostListQuery(pubName: $pubName, realm: $realm, type: $type, categories: $categories, tags: $tags, pinned: $pinned, shuffle: $shuffle, includeReplies: $includeReplies, mediaOnly: $mediaOnly, queryTerm: $queryTerm, order: $order, periodStart: $periodStart, periodEnd: $periodEnd, orderDesc: $orderDesc)';
}
}
/// @nodoc
abstract mixin class _$PostListQueryCopyWith<$Res> implements $PostListQueryCopyWith<$Res> {
factory _$PostListQueryCopyWith(_PostListQuery value, $Res Function(_PostListQuery) _then) = __$PostListQueryCopyWithImpl;
@override @useResult
$Res call({
String? pubName, String? realm, int? type, List<String>? categories, List<String>? tags, bool? pinned, bool shuffle, bool? includeReplies, bool? mediaOnly, String? queryTerm, String? order, int? periodStart, int? periodEnd, bool orderDesc
});
}
/// @nodoc
class __$PostListQueryCopyWithImpl<$Res>
implements _$PostListQueryCopyWith<$Res> {
__$PostListQueryCopyWithImpl(this._self, this._then);
final _PostListQuery _self;
final $Res Function(_PostListQuery) _then;
/// Create a copy of PostListQuery
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? pubName = freezed,Object? realm = freezed,Object? type = freezed,Object? categories = freezed,Object? tags = freezed,Object? pinned = freezed,Object? shuffle = null,Object? includeReplies = freezed,Object? mediaOnly = freezed,Object? queryTerm = freezed,Object? order = freezed,Object? periodStart = freezed,Object? periodEnd = freezed,Object? orderDesc = null,}) {
return _then(_PostListQuery(
pubName: freezed == pubName ? _self.pubName : pubName // ignore: cast_nullable_to_non_nullable
as String?,realm: freezed == realm ? _self.realm : realm // ignore: cast_nullable_to_non_nullable
as String?,type: freezed == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int?,categories: freezed == categories ? _self._categories : categories // ignore: cast_nullable_to_non_nullable
as List<String>?,tags: freezed == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable
as List<String>?,pinned: freezed == pinned ? _self.pinned : pinned // ignore: cast_nullable_to_non_nullable
as bool?,shuffle: null == shuffle ? _self.shuffle : shuffle // ignore: cast_nullable_to_non_nullable
as bool,includeReplies: freezed == includeReplies ? _self.includeReplies : includeReplies // ignore: cast_nullable_to_non_nullable
as bool?,mediaOnly: freezed == mediaOnly ? _self.mediaOnly : mediaOnly // ignore: cast_nullable_to_non_nullable
as bool?,queryTerm: freezed == queryTerm ? _self.queryTerm : queryTerm // ignore: cast_nullable_to_non_nullable
as String?,order: freezed == order ? _self.order : order // ignore: cast_nullable_to_non_nullable
as String?,periodStart: freezed == periodStart ? _self.periodStart : periodStart // ignore: cast_nullable_to_non_nullable
as int?,periodEnd: freezed == periodEnd ? _self.periodEnd : periodEnd // ignore: cast_nullable_to_non_nullable
as int?,orderDesc: null == orderDesc ? _self.orderDesc : orderDesc // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
// dart format on

View File

@@ -113,10 +113,7 @@ return $default(_that);case _:
final _that = this;
switch (_that) {
case _ReactionListQuery():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
@@ -175,10 +172,7 @@ return $default(_that.symbol,_that.postId);case _:
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String symbol, String postId) $default,) {final _that = this;
switch (_that) {
case _ReactionListQuery():
return $default(_that.symbol,_that.postId);case _:
throw StateError('Unexpected subclass');
}
return $default(_that.symbol,_that.postId);}
}
/// A variant of `when` that fallback to returning `null`
///

View File

@@ -0,0 +1,271 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart';
class PostFilterWidget extends StatelessWidget {
final TabController categoryTabController;
final ValueNotifier<bool?> includeReplies;
final ValueNotifier<bool> mediaOnly;
final ValueNotifier<String?> queryTerm;
final ValueNotifier<String?> order;
final ValueNotifier<bool> orderDesc;
final ValueNotifier<int?> periodStart;
final ValueNotifier<int?> periodEnd;
final ValueNotifier<bool> showAdvancedFilters;
final bool hideSearch;
const PostFilterWidget({
super.key,
required this.categoryTabController,
required this.includeReplies,
required this.mediaOnly,
required this.queryTerm,
required this.order,
required this.orderDesc,
required this.periodStart,
required this.periodEnd,
required this.showAdvancedFilters,
this.hideSearch = false,
});
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
children: [
TabBar(
controller: categoryTabController,
dividerColor: Colors.transparent,
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
tabs: [
Tab(text: 'all'.tr()),
Tab(text: 'postTypePost'.tr()),
Tab(text: 'postArticle'.tr()),
],
),
const Divider(height: 1),
Column(
children: [
Row(
children: [
Expanded(
child: CheckboxListTile(
title: Text('reply'.tr()),
value: includeReplies.value,
tristate: true,
onChanged: (value) {
// Cycle through: null -> false -> true -> null
if (includeReplies.value == null) {
includeReplies.value = false;
} else if (includeReplies.value == false) {
includeReplies.value = true;
} else {
includeReplies.value = null;
}
},
dense: true,
controlAffinity: ListTileControlAffinity.leading,
secondary: const Icon(Symbols.reply),
),
),
Expanded(
child: CheckboxListTile(
title: Text('attachments'.tr()),
value: mediaOnly.value,
onChanged: (value) {
if (value != null) {
mediaOnly.value = value;
}
},
dense: true,
controlAffinity: ListTileControlAffinity.leading,
secondary: const Icon(Symbols.attachment),
),
),
],
),
CheckboxListTile(
title: Text('descendingOrder'.tr()),
value: orderDesc.value,
onChanged: (value) {
if (value != null) {
orderDesc.value = value;
}
},
dense: true,
controlAffinity: ListTileControlAffinity.leading,
secondary: const Icon(Symbols.sort),
),
],
),
const Divider(height: 1),
ListTile(
title: Text('advancedFilters'.tr()),
leading: const Icon(Symbols.filter_list),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(const Radius.circular(8)),
),
trailing: Icon(
showAdvancedFilters.value
? Symbols.expand_less
: Symbols.expand_more,
),
onTap: () {
showAdvancedFilters.value = !showAdvancedFilters.value;
},
),
if (showAdvancedFilters.value) ...[
const Divider(height: 1),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!hideSearch)
TextField(
decoration: InputDecoration(
labelText: 'search'.tr(),
hintText: 'searchPosts'.tr(),
prefixIcon: const Icon(Symbols.search),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
onChanged: (value) {
queryTerm.value = value.isEmpty ? null : value;
},
),
if (!hideSearch) const Gap(12),
DropdownButtonFormField<String>(
decoration: InputDecoration(
labelText: 'sortBy'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
value: order.value,
items: [
DropdownMenuItem(value: 'date', child: Text('date'.tr())),
DropdownMenuItem(
value: 'popularity',
child: Text('popularity'.tr()),
),
],
onChanged: (value) {
order.value = value;
},
),
const Gap(12),
Row(
children: [
Expanded(
child: InkWell(
onTap: () async {
final pickedDate = await showDatePicker(
context: context,
initialDate: periodStart.value != null
? DateTime.fromMillisecondsSinceEpoch(
periodStart.value! * 1000,
)
: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime.now().add(
const Duration(days: 365),
),
);
if (pickedDate != null) {
periodStart.value =
pickedDate.millisecondsSinceEpoch ~/ 1000;
}
},
child: InputDecorator(
decoration: InputDecoration(
labelText: 'fromDate'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
suffixIcon: const Icon(Symbols.calendar_today),
),
child: Text(
periodStart.value != null
? DateTime.fromMillisecondsSinceEpoch(
periodStart.value! * 1000,
).toString().split(' ')[0]
: 'selectDate'.tr(),
),
),
),
),
const Gap(8),
Expanded(
child: InkWell(
onTap: () async {
final pickedDate = await showDatePicker(
context: context,
initialDate: periodEnd.value != null
? DateTime.fromMillisecondsSinceEpoch(
periodEnd.value! * 1000,
)
: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime.now().add(
const Duration(days: 365),
),
);
if (pickedDate != null) {
periodEnd.value =
pickedDate.millisecondsSinceEpoch ~/ 1000;
}
},
child: InputDecorator(
decoration: InputDecoration(
labelText: 'toDate'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
suffixIcon: const Icon(Symbols.calendar_today),
),
child: Text(
periodEnd.value != null
? DateTime.fromMillisecondsSinceEpoch(
periodEnd.value! * 1000,
).toString().split(' ')[0]
: 'selectDate'.tr(),
),
),
),
),
],
),
],
),
),
],
],
),
);
}
}

View File

@@ -5,7 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/drive_task.dart';
import 'package:island/pods/upload_tasks.dart';
import 'package:island/pods/drive/upload_tasks.dart';
import 'package:island/services/responsive.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -223,14 +223,14 @@ class _UploadOverlayContent extends HookConsumerWidget {
if (!isExpanded && activeTasks.isNotEmpty)
Text(
_getOverallProgressText(activeTasks),
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(
color:
Theme.of(
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
),
],
),
@@ -244,10 +244,9 @@ class _UploadOverlayContent extends HookConsumerWidget {
child: CircularProgressIndicator(
value: _getOverallProgress(activeTasks),
strokeWidth: 3,
backgroundColor:
Theme.of(
context,
).colorScheme.surfaceContainerHighest,
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
),
@@ -263,8 +262,8 @@ class _UploadOverlayContent extends HookConsumerWidget {
size: 20,
),
),
onPressed:
() => onExpansionChanged?.call(!isExpanded),
onPressed: () =>
onExpansionChanged?.call(!isExpanded),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
@@ -297,20 +296,18 @@ class _UploadOverlayContent extends HookConsumerWidget {
leading: Icon(
Symbols.clear_all,
size: 18,
color:
Theme.of(
context,
).colorScheme.onSurfaceVariant,
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
onTap: () {
taskNotifier.clearCompletedTasks();
onExpansionChanged?.call(false);
},
tileColor:
Theme.of(
context,
).colorScheme.surfaceContainerHighest,
tileColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
),
@@ -326,17 +323,17 @@ class _UploadOverlayContent extends HookConsumerWidget {
leading: Icon(
Symbols.clear_all,
size: 18,
color:
Theme.of(context).colorScheme.error,
color: Theme.of(
context,
).colorScheme.error,
),
onTap: () {
taskNotifier.clearAllTasks();
onExpansionChanged?.call(false);
},
tileColor:
Theme.of(
context,
).colorScheme.surfaceContainerHighest,
tileColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
),
@@ -556,8 +553,9 @@ class _UploadTaskTileState extends State<UploadTaskTile>
child: CircularProgressIndicator(
value: widget.task.progress,
strokeWidth: 2.5,
backgroundColor:
Theme.of(context).colorScheme.surfaceContainerHighest,
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
),
),
@@ -604,10 +602,9 @@ class _UploadTaskTileState extends State<UploadTaskTile>
color = Theme.of(context).colorScheme.secondary;
break;
case DriveTaskStatus.inProgress:
icon =
widget.task.type == 'FileDownload'
? Symbols.download
: Symbols.upload;
icon = widget.task.type == 'FileDownload'
? Symbols.download
: Symbols.upload;
color = Theme.of(context).colorScheme.primary;
break;
case DriveTaskStatus.paused: