Ability to crop image

This commit is contained in:
2025-08-04 22:08:18 +08:00
parent ba269dbbb8
commit edd760fbcb
20 changed files with 605 additions and 444 deletions

View File

@@ -6,9 +6,11 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:island/models/file.dart';
import 'package:island/services/file.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:super_context_menu/super_context_menu.dart';
class AttachmentPreview extends StatelessWidget {
final UniversalFile item;
@@ -16,6 +18,7 @@ class AttachmentPreview extends StatelessWidget {
final Function(int)? onMove;
final Function? onDelete;
final Function? onInsert;
final Function(UniversalFile)? onUpdate;
final Function? onRequestUpload;
const AttachmentPreview({
super.key,
@@ -24,6 +27,7 @@ class AttachmentPreview extends StatelessWidget {
this.onRequestUpload,
this.onMove,
this.onDelete,
this.onUpdate,
this.onInsert,
});
@@ -37,217 +41,249 @@ class AttachmentPreview extends StatelessWidget {
: 1.0;
if (ratio == 0) ratio = 1.0;
return AspectRatio(
aspectRatio: ratio,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
fit: StackFit.expand,
final contentWidget = ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: Column(
children: [
Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Builder(
builder: (context) {
if (item.isOnCloud) {
return CloudFileWidget(item: item.data);
} else if (item.data is XFile) {
if (item.type == UniversalFileType.image) {
final file = item.data as XFile;
if (file.path.isEmpty) {
return FutureBuilder<Uint8List>(
future: file.readAsBytes(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Image.memory(snapshot.data!);
}
return const Center(
child: CircularProgressIndicator(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Colors.black.withOpacity(0.5),
child: Material(
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (onDelete != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: Icon(
item.isLink ? Symbols.link_off : Symbols.delete,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onDelete?.call();
},
),
if (onDelete != null && onMove != null)
SizedBox(
height: 26,
child: const VerticalDivider(
width: 0.3,
color: Colors.white,
thickness: 0.3,
),
).padding(horizontal: 2),
if (onMove != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.keyboard_arrow_up,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onMove?.call(-1);
},
),
if (onMove != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.keyboard_arrow_down,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onMove?.call(1);
},
),
if (onInsert != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.add,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onInsert?.call();
},
),
],
),
),
),
),
if (onRequestUpload != null)
InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => onRequestUpload?.call(),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Colors.black.withOpacity(0.5),
padding: EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
child:
(item.isOnCloud)
? Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.cloud,
size: 16,
color: Colors.white,
),
const Gap(8),
Text(
'On-cloud',
style: TextStyle(color: Colors.white),
),
],
)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.cloud_off,
size: 16,
color: Colors.white,
),
const Gap(8),
Text(
'On-device',
style: TextStyle(color: Colors.white),
),
],
),
),
),
),
],
).padding(horizontal: 12, vertical: 8),
AspectRatio(
aspectRatio: ratio,
child: Stack(
fit: StackFit.expand,
children: [
Builder(
key: ValueKey(item.hashCode),
builder: (context) {
if (item.isOnCloud) {
return CloudFileWidget(item: item.data);
} else if (item.data is XFile) {
final file = item.data as XFile;
if (file.path.isEmpty) {
return FutureBuilder<Uint8List>(
future: file.readAsBytes(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Image.memory(snapshot.data!);
}
return const Center(
child: CircularProgressIndicator(),
);
},
);
}
switch (item.type) {
case UniversalFileType.image:
return kIsWeb
? Image.network(file.path)
: Image.file(File(file.path));
default:
return Column(
children: [
const Icon(Symbols.document_scanner),
Text(file.name),
],
);
},
);
}
} else if (item is List<int> || item is Uint8List) {
switch (item.type) {
case UniversalFileType.image:
return Image.memory(item.data);
default:
return Column(
children: [const Icon(Symbols.document_scanner)],
);
}
}
return kIsWeb
? Image.network(file.path)
: Image.file(File(file.path));
} else {
return Center(
child: Text(
'Preview is not supported for ${item.type}',
textAlign: TextAlign.center,
return Placeholder();
},
),
if (progress != null)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.3),
padding: EdgeInsets.symmetric(
horizontal: 40,
vertical: 16,
),
);
}
} else if (item is List<int> || item is Uint8List) {
if (item.type == UniversalFileType.image) {
return Image.memory(item.data);
} else {
return Center(
child: Text(
'Preview is not supported for ${item.type}',
textAlign: TextAlign.center,
),
);
}
}
return Placeholder();
},
),
),
if (progress != null)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.3),
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (progress != null)
Text(
'${progress!.toStringAsFixed(2)}%',
style: TextStyle(color: Colors.white),
)
else
Text(
'uploading'.tr(),
style: TextStyle(color: Colors.white),
),
Gap(6),
Center(
child: LinearProgressIndicator(
value: progress != null ? progress! / 100.0 : null,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (progress != null)
Text(
'${progress!.toStringAsFixed(2)}%',
style: TextStyle(color: Colors.white),
)
else
Text(
'uploading'.tr(),
style: TextStyle(color: Colors.white),
),
Gap(6),
Center(
child: LinearProgressIndicator(
value:
progress != null ? progress! / 100.0 : null,
),
),
],
),
),
],
),
),
),
Positioned(
left: 8,
top: 8,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Colors.black.withOpacity(0.5),
child: Material(
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (onDelete != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.delete,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onDelete?.call();
},
),
if (onDelete != null && onMove != null)
SizedBox(
height: 26,
child: const VerticalDivider(
width: 0.3,
color: Colors.white,
thickness: 0.3,
),
).padding(horizontal: 2),
if (onMove != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.keyboard_arrow_up,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onMove?.call(-1);
},
),
if (onMove != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.keyboard_arrow_down,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onMove?.call(1);
},
),
if (onInsert != null)
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
Symbols.add,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onInsert?.call();
},
),
],
),
),
),
],
),
),
if (onRequestUpload != null)
Positioned(
top: 8,
right: 8,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => onRequestUpload?.call(),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Colors.black.withOpacity(0.5),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child:
(item.isOnCloud)
? Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.cloud,
size: 16,
color: Colors.white,
),
const Gap(8),
Text(
'On-cloud',
style: TextStyle(color: Colors.white),
),
],
)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.cloud_off,
size: 16,
color: Colors.white,
),
const Gap(8),
Text(
'On-device',
style: TextStyle(color: Colors.white),
),
],
),
),
),
),
),
],
),
),
);
return ContextMenuWidget(
menuProvider:
(MenuRequest request) => Menu(
children: [
if (item.isOnDevice && item.type == UniversalFileType.image)
MenuAction(
title: 'crop'.tr(),
image: MenuImage.icon(Symbols.crop),
callback: () async {
final result = await cropImage(
context,
image: item.data,
replacePath: true,
);
if (result == null) return;
onUpdate?.call(item.copyWith(data: result));
},
),
],
),
child: contentWidget,
);
}
}

View File

@@ -112,51 +112,57 @@ class CloudFileList extends HookConsumerWidget {
constraints: BoxConstraints(maxHeight: maxHeight, minWidth: maxWidth),
child: AspectRatio(
aspectRatio: calculateAspectRatio(),
child: CarouselView(
padding: padding,
itemSnapping: true,
itemExtent: math.min(
MediaQuery.of(context).size.width * 0.85,
maxWidth * 0.85,
),
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(16)),
),
children: [
for (var i = 0; i < files.length; i++)
Stack(
children: [
_CloudFileListEntry(
file: files[i],
heroTag: heroTags[i],
isImage: files[i].mimeType?.startsWith('image') ?? false,
disableZoomIn: disableZoomIn,
),
Positioned(
bottom: 12,
left: 16,
child: Text('${i + 1}/${files.length}')
.textColor(Colors.white)
.textShadow(
color: Colors.black54,
offset: Offset(1, 1),
blurRadius: 3,
),
),
],
child: Padding(
padding: padding ?? EdgeInsets.zero,
child: CarouselView(
itemSnapping: true,
itemExtent: math.min(
math.min(
MediaQuery.of(context).size.width * 0.75,
maxWidth * 0.75,
),
],
onTap: (i) {
if (!(files[i].mimeType?.startsWith('image') ?? false)) {
return;
}
if (!disableZoomIn) {
context.pushTransparentRoute(
CloudFileZoomIn(item: files[i], heroTag: heroTags[i]),
rootNavigator: true,
);
}
},
640,
),
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(16)),
),
children: [
for (var i = 0; i < files.length; i++)
Stack(
children: [
_CloudFileListEntry(
file: files[i],
heroTag: heroTags[i],
isImage:
files[i].mimeType?.startsWith('image') ?? false,
disableZoomIn: disableZoomIn,
),
Positioned(
bottom: 12,
left: 16,
child: Text('${i + 1}/${files.length}')
.textColor(Colors.white)
.textShadow(
color: Colors.black54,
offset: Offset(1, 1),
blurRadius: 3,
),
),
],
),
],
onTap: (i) {
if (!(files[i].mimeType?.startsWith('image') ?? false)) {
return;
}
if (!disableZoomIn) {
context.pushTransparentRoute(
CloudFileZoomIn(item: files[i], heroTag: heroTags[i]),
rootNavigator: true,
);
}
},
),
),
),
);

View File

@@ -1,84 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
typedef ContextMenuBuilder =
Widget Function(BuildContext context, Offset offset);
class ContextMenuRegion extends HookWidget {
final Offset? mobileAnchor;
final Widget child;
final ContextMenuBuilder contextMenuBuilder;
const ContextMenuRegion({
super.key,
required this.child,
required this.contextMenuBuilder,
this.mobileAnchor,
});
@override
Widget build(BuildContext context) {
final contextMenuController = useMemoized(() => ContextMenuController());
final mobileOffset = useState<Offset?>(null);
bool canBeTouchScreen = switch (defaultTargetPlatform) {
TargetPlatform.android || TargetPlatform.iOS => true,
_ => false,
};
void showMenu(Offset position) {
contextMenuController.show(
context: context,
contextMenuBuilder: (BuildContext context) {
return contextMenuBuilder(context, position);
},
);
}
void hideMenu() {
contextMenuController.remove();
}
void onSecondaryTapUp(TapUpDetails details) {
showMenu(details.globalPosition);
}
void onTap() {
if (!contextMenuController.isShown) {
return;
}
hideMenu();
}
void onLongPressStart(LongPressStartDetails details) {
mobileOffset.value = details.globalPosition;
}
void onLongPress() {
assert(mobileOffset.value != null);
showMenu(mobileAnchor ?? mobileOffset.value!);
mobileOffset.value = null;
}
useEffect(() {
return () {
hideMenu();
};
}, []);
return TapRegion(
behavior: HitTestBehavior.opaque,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onSecondaryTapUp: onSecondaryTapUp,
onTap: onTap,
onLongPress: canBeTouchScreen ? onLongPress : null,
onLongPressStart: canBeTouchScreen ? onLongPressStart : null,
child: child,
),
onTapOutside: (_) {
hideMenu();
},
);
}
}

View File

@@ -0,0 +1,193 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart';
part 'compose_link_attachments.g.dart';
@riverpod
class CloudFileListNotifier extends _$CloudFileListNotifier
with CursorPagingNotifierMixin<SnCloudFile> {
@override
Future<CursorPagingData<SnCloudFile>> build() => fetch(cursor: null);
@override
Future<CursorPagingData<SnCloudFile>> fetch({required String? cursor}) async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final take = 20;
final queryParameters = {'offset': offset, 'take': take};
final response = await client.get(
'/drive/files/me',
queryParameters: queryParameters,
);
final List<SnCloudFile> items =
(response.data as List)
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
.toList();
final total = int.parse(response.headers.value('X-Total') ?? '0');
final hasMore = offset + items.length < total;
final nextCursor = hasMore ? (offset + items.length).toString() : null;
return CursorPagingData(
items: items,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
class ComposeLinkAttachment extends HookConsumerWidget {
const ComposeLinkAttachment({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final idController = useTextEditingController();
final errorMessage = useState<String?>(null);
return SheetScaffold(
heightFactor: 0.6,
titleText: 'linkAttachment'.tr(),
child: DefaultTabController(
length: 2,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TabBar(
tabs: [
Tab(text: 'attachmentsRecentUploads'.tr()),
Tab(text: 'attachmentsManualInput'.tr()),
],
),
Expanded(
child: TabBarView(
children: [
PagingHelperView(
provider: cloudFileListNotifierProvider,
futureRefreshable: cloudFileListNotifierProvider.future,
notifierRefreshable: cloudFileListNotifierProvider.notifier,
contentBuilder:
(data, widgetCount, endItemView) => ListView.builder(
padding: EdgeInsets.zero,
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final item = data.items[index];
final itemType =
item.mimeType?.split('/').firstOrNull;
return ListTile(
leading: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
child: SizedBox(
height: 48,
width: 48,
child: switch (itemType) {
'image' => CloudImageWidget(file: item),
'audio' =>
const Icon(
Symbols.audio_file,
fill: 1,
).center(),
'video' =>
const Icon(
Symbols.video_file,
fill: 1,
).center(),
_ =>
const Icon(
Symbols.body_system,
fill: 1,
).center(),
},
),
),
title:
item.name.isEmpty
? Text('untitled').tr().italic()
: Text(item.name),
onTap: () {
Navigator.pop(context, item);
},
);
},
),
),
SingleChildScrollView(
child: Column(
children: [
TextField(
controller: idController,
decoration: InputDecoration(
labelText: 'fileId'.tr(),
helperText: 'fileIdHint'.tr(),
helperMaxLines: 3,
errorText: errorMessage.value,
border: OutlineInputBorder(),
),
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(16),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
icon: const Icon(Symbols.add),
label: Text('add'.tr()),
onPressed: () async {
final fileId = idController.text.trim();
if (fileId.isEmpty) {
errorMessage.value = 'fileIdCannotBeEmpty'.tr();
return;
}
try {
final client = ref.read(apiClientProvider);
final response = await client.get(
'/drive/files/$fileId/info',
);
final SnCloudFile cloudFile =
SnCloudFile.fromJson(response.data);
if (context.mounted) {
Navigator.of(context).pop(cloudFile);
}
} catch (e) {
errorMessage.value = 'failedToFetchFile'.tr(
args: [e.toString()],
);
}
},
),
),
],
).padding(horizontal: 24, vertical: 24),
),
],
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,31 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'compose_link_attachments.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$cloudFileListNotifierHash() =>
r'e2c8a076a9e635c7b43a87d00f78775427ba6334';
/// See also [CloudFileListNotifier].
@ProviderFor(CloudFileListNotifier)
final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
CloudFileListNotifier,
CursorPagingData<SnCloudFile>
>.internal(
CloudFileListNotifier.new,
name: r'cloudFileListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$cloudFileListNotifierHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$CloudFileListNotifier =
AutoDisposeAsyncNotifier<CursorPagingData<SnCloudFile>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -126,7 +126,7 @@ class ComposeRecorder extends HookConsumerWidget {
),
),
),
),
).padding(horizontal: 24),
const Gap(12),
IconButton.filled(
onPressed: recording.value ? stopRecord : startRecord,

View File

@@ -3,7 +3,6 @@ import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart';
@@ -14,11 +13,9 @@ import 'package:island/pods/network.dart';
import 'package:island/services/file.dart';
import 'package:island/services/compose_storage_db.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/post/compose_link_attachments.dart';
import 'package:island/widgets/post/compose_recorder.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:pasteboard/pasteboard.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:textfield_tags/textfield_tags.dart';
import 'dart:async';
import 'dart:developer';
@@ -424,88 +421,39 @@ class ComposeLogic {
ComposeState state,
BuildContext context,
) async {
final TextEditingController idController = TextEditingController();
String? errorMessage;
await showModalBottomSheet(
final cloudFile = await showModalBottomSheet<SnCloudFile?>(
context: context,
builder: (BuildContext dialogContext) {
return StatefulBuilder(
builder: (context, setState) {
return SheetScaffold(
titleText: 'linkAttachment'.tr(),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: idController,
decoration: InputDecoration(
labelText: 'fileId'.tr(),
helperText: 'fileIdHint'.tr(),
helperMaxLines: 3,
errorText: errorMessage,
border: OutlineInputBorder(),
),
),
const Gap(16),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
icon: const Icon(Symbols.add),
label: Text('add'.tr()),
onPressed: () async {
final fileId = idController.text.trim();
if (fileId.isEmpty) {
setState(() {
errorMessage = 'fileIdCannotBeEmpty'.tr();
});
return;
}
try {
final client = ref.read(apiClientProvider);
final response = await client.get(
'/drive/files/$fileId/info',
);
final SnCloudFile cloudFile = SnCloudFile.fromJson(
response.data,
);
state.attachments.value = [
...state.attachments.value,
UniversalFile(
data: cloudFile,
type: switch (cloudFile.mimeType
?.split('/')
.firstOrNull) {
'image' => UniversalFileType.image,
'video' => UniversalFileType.video,
'audio' => UniversalFileType.audio,
_ => UniversalFileType.file,
},
),
];
if (context.mounted) {
Navigator.of(dialogContext).pop();
}
} catch (e) {
setState(() {
errorMessage = 'failedToFetchFile'.tr(
args: [e.toString()],
);
});
}
},
),
),
],
).padding(horizontal: 24, vertical: 24),
);
},
);
},
useRootNavigator: true,
isScrollControlled: true,
builder: (context) => ComposeLinkAttachment(),
);
if (cloudFile == null) return;
state.attachments.value = [
...state.attachments.value,
UniversalFile(
data: cloudFile,
type: switch (cloudFile.mimeType?.split('/').firstOrNull) {
'image' => UniversalFileType.image,
'video' => UniversalFileType.video,
'audio' => UniversalFileType.audio,
_ => UniversalFileType.file,
},
isLink: true,
),
];
}
static void updateAttachment(
ComposeState state,
UniversalFile value,
int index,
) {
state.attachments.value =
state.attachments.value.mapIndexed((idx, ele) {
if (idx == index) return value;
return ele;
}).toList();
}
static Future<void> uploadAttachment(
@@ -581,7 +529,7 @@ class ComposeLogic {
int index,
) async {
final attachment = state.attachments.value[index];
if (attachment.isOnCloud) {
if (attachment.isOnCloud && !attachment.isLink) {
final client = ref.watch(apiClientProvider);
await client.delete('/drive/files/${attachment.data.id}');
}

View File

@@ -9,7 +9,6 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/embed.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/translate.dart';
import 'package:island/pods/userinfo.dart';
@@ -179,7 +178,7 @@ class PostActionableItem extends HookConsumerWidget {
callback: () {
showShareSheetLink(
context: context,
link: '${ref.read(serverUrlProvider)}/posts/${item.id}',
link: 'https://solian.app/posts/${item.id}',
title: 'sharePost'.tr(),
toSystem: true,
);
@@ -410,7 +409,9 @@ class PostItem extends HookConsumerWidget {
if (!isFullPost && item.type == 1)
Container(
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor),
border: Border.all(
color: Theme.of(context).dividerColor.withOpacity(0.5),
),
borderRadius: const BorderRadius.all(Radius.circular(16)),
),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
@@ -596,7 +597,7 @@ Widget _buildReferencePost(
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
color: Theme.of(context).dividerColor.withOpacity(0.5),
),
),
child: Column(
@@ -912,7 +913,9 @@ class PostReplyPreview extends HookConsumerWidget {
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLow,
border: Border.all(color: Theme.of(context).dividerColor),
border: Border.all(
color: Theme.of(context).dividerColor.withOpacity(0.5),
),
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: Column(

View File

@@ -83,7 +83,7 @@ class PublisherCard extends ConsumerWidget {
color: Colors.white,
fontWeight: FontWeight.bold,
),
maxLines: 2,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],

View File

@@ -86,7 +86,7 @@ class RealmCard extends ConsumerWidget {
color: Colors.white,
fontWeight: FontWeight.bold,
),
maxLines: 2,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],

View File

@@ -93,7 +93,7 @@ class WebArticleCard extends StatelessWidget {
fontWeight: FontWeight.bold,
height: 1.3,
),
maxLines: showDetails ? 3 : 2,
maxLines: showDetails ? 3 : 1,
overflow: TextOverflow.ellipsis,
),
if (showDetails &&
@@ -125,6 +125,8 @@ class WebArticleCard extends StatelessWidget {
fontSize: 9,
color: Colors.white70,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),