Posting with attachment

This commit is contained in:
LittleSheep 2025-04-26 15:41:57 +08:00
parent 4af64ae89b
commit d4538a9ef6
12 changed files with 401 additions and 39 deletions

View File

@ -8,23 +8,22 @@ plugins {
android { android {
namespace = "dev.solsynth.solian" namespace = "dev.solsynth.solian"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion ndkVersion = "27.0.12077973"
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString() jvmTarget = JavaVersion.VERSION_17.toString()
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "dev.solsynth.solian" applicationId = "dev.solsynth.solian"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = 26
targetSdk = flutter.targetSdkVersion targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName versionName = flutter.versionName

View File

@ -1,12 +1,29 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<application <application
android:label="island" android:label="Solian"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleInstance"
android:taskAffinity="" android:taskAffinity=""
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"

View File

@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker_android/image_picker_android.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/theme.dart'; import 'package:island/pods/theme.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
@ -12,6 +13,7 @@ import 'package:island/pods/userinfo.dart';
import 'package:island/route.dart'; import 'package:island/route.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -28,6 +30,14 @@ void main() async {
}); });
} }
if (!kIsWeb && Platform.isAndroid) {
final ImagePickerPlatform imagePickerImplementation =
ImagePickerPlatform.instance;
if (imagePickerImplementation is ImagePickerAndroid) {
imagePickerImplementation.useAndroidPhotoPicker = true;
}
}
runApp( runApp(
ProviderScope( ProviderScope(
overrides: [sharedPreferencesProvider.overrideWithValue(prefs)], overrides: [sharedPreferencesProvider.overrideWithValue(prefs)],

View File

@ -14,7 +14,7 @@ abstract class SnCloudFile with _$SnCloudFile {
required String? mimeType, required String? mimeType,
required String? hash, required String? hash,
required int size, required int size,
required DateTime uploadedAt, required DateTime? uploadedAt,
required String? uploadedTo, required String? uploadedTo,
required int usedCount, required int usedCount,
required DateTime createdAt, required DateTime createdAt,

View File

@ -16,7 +16,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$SnCloudFile { mixin _$SnCloudFile {
String get id; String get name; String? get description; Map<String, dynamic>? get fileMeta; Map<String, dynamic>? get userMeta; String? get mimeType; String? get hash; int get size; DateTime get uploadedAt; String? get uploadedTo; int get usedCount; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; String get id; String get name; String? get description; Map<String, dynamic>? get fileMeta; Map<String, dynamic>? get userMeta; String? get mimeType; String? get hash; int get size; DateTime? get uploadedAt; String? get uploadedTo; int get usedCount; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnCloudFile /// Create a copy of SnCloudFile
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -49,7 +49,7 @@ abstract mixin class $SnCloudFileCopyWith<$Res> {
factory $SnCloudFileCopyWith(SnCloudFile value, $Res Function(SnCloudFile) _then) = _$SnCloudFileCopyWithImpl; factory $SnCloudFileCopyWith(SnCloudFile value, $Res Function(SnCloudFile) _then) = _$SnCloudFileCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, String? mimeType, String? hash, int size, DateTime uploadedAt, String? uploadedTo, int usedCount, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, int usedCount, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
}); });
@ -66,7 +66,7 @@ class _$SnCloudFileCopyWithImpl<$Res>
/// Create a copy of SnCloudFile /// Create a copy of SnCloudFile
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = null,Object? uploadedTo = freezed,Object? usedCount = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? usedCount = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
@ -76,8 +76,8 @@ as Map<String, dynamic>?,userMeta: freezed == userMeta ? _self.userMeta : userMe
as Map<String, dynamic>?,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable as Map<String, dynamic>?,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable
as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable
as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
as int,uploadedAt: null == uploadedAt ? _self.uploadedAt : uploadedAt // ignore: cast_nullable_to_non_nullable as int,uploadedAt: freezed == uploadedAt ? _self.uploadedAt : uploadedAt // ignore: cast_nullable_to_non_nullable
as DateTime,uploadedTo: freezed == uploadedTo ? _self.uploadedTo : uploadedTo // ignore: cast_nullable_to_non_nullable as DateTime?,uploadedTo: freezed == uploadedTo ? _self.uploadedTo : uploadedTo // ignore: cast_nullable_to_non_nullable
as String?,usedCount: null == usedCount ? _self.usedCount : usedCount // ignore: cast_nullable_to_non_nullable as String?,usedCount: null == usedCount ? _self.usedCount : usedCount // ignore: cast_nullable_to_non_nullable
as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
@ -120,7 +120,7 @@ class _SnCloudFile implements SnCloudFile {
@override final String? mimeType; @override final String? mimeType;
@override final String? hash; @override final String? hash;
@override final int size; @override final int size;
@override final DateTime uploadedAt; @override final DateTime? uploadedAt;
@override final String? uploadedTo; @override final String? uploadedTo;
@override final int usedCount; @override final int usedCount;
@override final DateTime createdAt; @override final DateTime createdAt;
@ -160,7 +160,7 @@ abstract mixin class _$SnCloudFileCopyWith<$Res> implements $SnCloudFileCopyWith
factory _$SnCloudFileCopyWith(_SnCloudFile value, $Res Function(_SnCloudFile) _then) = __$SnCloudFileCopyWithImpl; factory _$SnCloudFileCopyWith(_SnCloudFile value, $Res Function(_SnCloudFile) _then) = __$SnCloudFileCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, String? mimeType, String? hash, int size, DateTime uploadedAt, String? uploadedTo, int usedCount, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt String id, String name, String? description, Map<String, dynamic>? fileMeta, Map<String, dynamic>? userMeta, String? mimeType, String? hash, int size, DateTime? uploadedAt, String? uploadedTo, int usedCount, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
}); });
@ -177,7 +177,7 @@ class __$SnCloudFileCopyWithImpl<$Res>
/// Create a copy of SnCloudFile /// Create a copy of SnCloudFile
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = null,Object? uploadedTo = freezed,Object? usedCount = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = freezed,Object? fileMeta = freezed,Object? userMeta = freezed,Object? mimeType = freezed,Object? hash = freezed,Object? size = null,Object? uploadedAt = freezed,Object? uploadedTo = freezed,Object? usedCount = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnCloudFile( return _then(_SnCloudFile(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
@ -187,8 +187,8 @@ as Map<String, dynamic>?,userMeta: freezed == userMeta ? _self._userMeta : userM
as Map<String, dynamic>?,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable as Map<String, dynamic>?,mimeType: freezed == mimeType ? _self.mimeType : mimeType // ignore: cast_nullable_to_non_nullable
as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable as String?,hash: freezed == hash ? _self.hash : hash // ignore: cast_nullable_to_non_nullable
as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable as String?,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
as int,uploadedAt: null == uploadedAt ? _self.uploadedAt : uploadedAt // ignore: cast_nullable_to_non_nullable as int,uploadedAt: freezed == uploadedAt ? _self.uploadedAt : uploadedAt // ignore: cast_nullable_to_non_nullable
as DateTime,uploadedTo: freezed == uploadedTo ? _self.uploadedTo : uploadedTo // ignore: cast_nullable_to_non_nullable as DateTime?,uploadedTo: freezed == uploadedTo ? _self.uploadedTo : uploadedTo // ignore: cast_nullable_to_non_nullable
as String?,usedCount: null == usedCount ? _self.usedCount : usedCount // ignore: cast_nullable_to_non_nullable as String?,usedCount: null == usedCount ? _self.usedCount : usedCount // ignore: cast_nullable_to_non_nullable
as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable

View File

@ -15,7 +15,10 @@ _SnCloudFile _$SnCloudFileFromJson(Map<String, dynamic> json) => _SnCloudFile(
mimeType: json['mime_type'] as String?, mimeType: json['mime_type'] as String?,
hash: json['hash'] as String?, hash: json['hash'] as String?,
size: (json['size'] as num).toInt(), size: (json['size'] as num).toInt(),
uploadedAt: DateTime.parse(json['uploaded_at'] as String), uploadedAt:
json['uploaded_at'] == null
? null
: DateTime.parse(json['uploaded_at'] as String),
uploadedTo: json['uploaded_to'] as String?, uploadedTo: json['uploaded_to'] as String?,
usedCount: (json['used_count'] as num).toInt(), usedCount: (json['used_count'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
@ -36,7 +39,7 @@ Map<String, dynamic> _$SnCloudFileToJson(_SnCloudFile instance) =>
'mime_type': instance.mimeType, 'mime_type': instance.mimeType,
'hash': instance.hash, 'hash': instance.hash,
'size': instance.size, 'size': instance.size,
'uploaded_at': instance.uploadedAt.toIso8601String(), 'uploaded_at': instance.uploadedAt?.toIso8601String(),
'uploaded_to': instance.uploadedTo, 'uploaded_to': instance.uploadedTo,
'used_count': instance.usedCount, 'used_count': instance.usedCount,
'created_at': instance.createdAt.toIso8601String(), 'created_at': instance.createdAt.toIso8601String(),

View File

@ -121,8 +121,8 @@ Future<ThemeData> createAppTheme(
TargetPlatform.windows: ZoomPageTransitionsBuilder(), TargetPlatform.windows: ZoomPageTransitionsBuilder(),
}, },
), ),
progressIndicatorTheme: ProgressIndicatorThemeData(), progressIndicatorTheme: ProgressIndicatorThemeData(year2023: false),
sliderTheme: SliderThemeData(), sliderTheme: SliderThemeData(year2023: false),
); );
} }

View File

@ -1,12 +1,18 @@
import 'package:auto_route/annotations.dart'; import 'dart:io';
import 'dart:typed_data';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/screens/account/me/publishers.dart'; import 'package:island/screens/account/me/publishers.dart';
import 'package:island/services/file.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
@ -30,10 +36,69 @@ class PostComposeScreen extends HookConsumerWidget {
return null; return null;
}, [publishers]); }, [publishers]);
// Contains the XFile, ByteData, or SnCloudFile
final attachments = useState<List<dynamic>>([]);
final contentController = useTextEditingController(); final contentController = useTextEditingController();
final submitting = useState(false); final submitting = useState(false);
Future<void> pickAttachment() async {
final result = await ref
.watch(imagePickerProvider)
.pickMultipleMedia(requestFullMetadata: true);
attachments.value = [...attachments.value, ...result];
}
final attachmentProgress = useState<Map<int, double>>({});
Future<void> uploadAttachment(int index) async {
final attachment = attachments.value[index];
if (attachment is SnCloudFile) return;
final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) throw ArgumentError('Access token is null');
attachmentProgress.value = {...attachmentProgress.value, index: 0};
final cloudFile =
await putMediaToCloud(
fileData: attachment,
atk: atk,
baseUrl: baseUrl,
filename: attachment.name ?? 'Post media',
mimetype: attachment.mimeType ?? 'image/jpeg',
onProgress: (progress, estimate) {
attachmentProgress.value = {
...attachmentProgress.value,
index: progress,
};
},
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
final clone = List.of(attachments.value);
clone[index] = cloudFile;
attachments.value = clone;
attachmentProgress.value = attachmentProgress.value..remove(index);
}
Future<void> deleteAttachment(int index) async {
final attachment = attachments.value[index];
if (attachment is SnCloudFile) {
final client = ref.watch(apiClientProvider);
await client.delete('/files/${attachment.id}');
}
final clone = List.of(attachments.value);
clone.removeAt(index);
attachments.value = clone;
}
Future<void> performAction() async { Future<void> performAction() async {
if (!contentController.text.isNotEmpty) { if (!contentController.text.isNotEmpty) {
return; return;
@ -41,8 +106,25 @@ class PostComposeScreen extends HookConsumerWidget {
try { try {
submitting.value = true; submitting.value = true;
await Future.wait(
attachments.value
.where((e) => e is! SnCloudFile)
.map((e) => uploadAttachment(e)),
);
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
await client.post('/posts', data: {'content': contentController.text}); await client.post(
'/posts',
data: {
'content': contentController.text,
'attachments':
attachments.value
.whereType<SnCloudFile>()
.map((e) => e.id)
.toList(),
},
);
if (context.mounted) { if (context.mounted) {
context.maybePop(true); context.maybePop(true);
} }
@ -75,23 +157,258 @@ class PostComposeScreen extends HookConsumerWidget {
ProfilePictureWidget( ProfilePictureWidget(
item: currentPublisher.value?.picture, item: currentPublisher.value?.picture,
radius: 24, radius: 24,
), ).padding(top: 16),
Expanded( Expanded(
child: TextField( child: SingleChildScrollView(
padding: EdgeInsets.symmetric(vertical: 16),
child: Column(
children: [
TextField(
controller: contentController, controller: contentController,
decoration: InputDecoration.collapsed( decoration: InputDecoration.collapsed(
hintText: 'What\'s happened?!', hintText: 'What\'s happened?!',
), ),
maxLines: null, maxLines: null,
onTapOutside: onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(), (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,
children: [
for (
var idx = 0;
idx < attachments.value.length;
idx++
)
_AttachmentPreview(
item: attachments.value[idx],
progress: attachmentProgress.value[idx],
onRequestUpload: () => uploadAttachment(idx),
onDelete: () => deleteAttachment(idx),
onMove: (delta) {
if (idx + delta < 0 ||
idx + delta >= attachments.value.length) {
return;
}
final clone = List.of(attachments.value);
clone.insert(
idx + delta,
clone.removeAt(idx),
);
attachments.value = clone;
},
),
],
),
],
),
), ),
), ),
], ],
).padding(all: 16), ).padding(horizontal: 16),
),
Material(
elevation: 2,
child: Row(
children: [
IconButton(
onPressed: pickAttachment,
icon: const Icon(LucideIcons.imagePlus),
color: Theme.of(context).colorScheme.primary,
),
],
).padding(
bottom: MediaQuery.of(context).padding.bottom,
horizontal: 16,
top: 8,
),
), ),
], ],
), ),
); );
} }
} }
class _AttachmentPreview extends StatelessWidget {
final dynamic item;
final double? progress;
final Function(int)? onMove;
final Function? onDelete;
final Function? onRequestUpload;
const _AttachmentPreview({
this.item,
this.progress,
this.onRequestUpload,
this.onMove,
this.onDelete,
});
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 1,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Stack(
fit: StackFit.expand,
children: [
Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Builder(
builder: (context) {
if (item is SnCloudFile) {
return CloudFileWidget(item: item);
} else if (item is XFile) {
if (item.mimeType?.startsWith('image') ?? false) {
return Image.file(File(item.path));
} else {
return Center(
child: Text(
'Preview is not supported for ${item.mimeType}',
),
);
}
} else if (item is List<int> || item is Uint8List) {
return Image.memory(item);
}
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: [
Text(
'Uploading...',
style: TextStyle(color: Colors.white),
),
Gap(4),
Center(child: LinearProgressIndicator(value: progress)),
],
),
),
),
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: [
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
LucideIcons.trash,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onDelete?.call();
},
),
SizedBox(
height: 26,
child: const VerticalDivider(
width: 0.3,
color: Colors.white,
thickness: 0.3,
),
).padding(horizontal: 2),
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
LucideIcons.arrowUp,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onMove?.call(-1);
},
),
InkWell(
borderRadius: BorderRadius.circular(8),
child: const Icon(
LucideIcons.arrowDown,
size: 14,
color: Colors.white,
).padding(horizontal: 8, vertical: 6),
onTap: () {
onMove?.call(1);
},
),
],
),
),
),
),
),
Positioned(
top: 8,
right: 8,
child: Material(
color: Colors.transparent,
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 is SnCloudFile)
? Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
LucideIcons.cloud,
size: 16,
color: Colors.white,
),
const Gap(8),
Text(
'On-cloud',
style: TextStyle(color: Colors.white),
),
],
)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
LucideIcons.cloudOff,
size: 16,
color: Colors.white,
),
const Gap(8),
Text(
'On-device',
style: TextStyle(color: Colors.white),
),
],
),
),
),
),
),
),
],
),
),
);
}
}

View File

@ -44,7 +44,10 @@ class CloudFileList extends StatelessWidget {
if (allImages) { if (allImages) {
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxHeight), constraints: BoxConstraints(
maxHeight: maxHeight,
minWidth: double.infinity,
),
child: AspectRatio( child: AspectRatio(
aspectRatio: calculateAspectRatio(), aspectRatio: calculateAspectRatio(),
child: CarouselView( child: CarouselView(
@ -60,7 +63,10 @@ class CloudFileList extends StatelessWidget {
} }
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxHeight), constraints: BoxConstraints(
maxHeight: maxHeight,
minWidth: double.infinity,
),
child: AspectRatio( child: AspectRatio(
aspectRatio: calculateAspectRatio(), aspectRatio: calculateAspectRatio(),
child: ListView.separated( child: ListView.separated(

View File

@ -6,6 +6,14 @@
<true/> <true/>
<key>com.apple.security.cs.allow-jit</key> <key>com.apple.security.cs.allow-jit</key>
<true/> <true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key> <key>com.apple.security.network.server</key>
<true/> <true/>
</dict> </dict>

View File

@ -774,7 +774,7 @@ packages:
source: hosted source: hosted
version: "1.1.2" version: "1.1.2"
image_picker_android: image_picker_android:
dependency: transitive dependency: "direct main"
description: description:
name: image_picker_android name: image_picker_android
sha256: "317a5d961cec5b34e777b9252393f2afbd23084aa6e60fcf601dcf6341b9ebeb" sha256: "317a5d961cec5b34e777b9252393f2afbd23084aa6e60fcf601dcf6341b9ebeb"
@ -814,7 +814,7 @@ packages:
source: hosted source: hosted
version: "0.2.1+2" version: "0.2.1+2"
image_picker_platform_interface: image_picker_platform_interface:
dependency: transitive dependency: "direct main"
description: description:
name: image_picker_platform_interface name: image_picker_platform_interface
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"

View File

@ -76,6 +76,8 @@ dependencies:
image_picker: ^1.1.2 image_picker: ^1.1.2
file_picker: ^10.1.2 file_picker: ^10.1.2
riverpod_annotation: ^2.6.1 riverpod_annotation: ^2.6.1
image_picker_platform_interface: ^2.10.1
image_picker_android: ^0.8.12+23
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: