Compare commits

..

No commits in common. "ab73916795d509cdc3f8b7de1f4c3fac8b33ec59" and "302691f557c5a3a3ce82bdc7c3074d48568c1d50" have entirely different histories.

13 changed files with 62 additions and 365 deletions

View File

@ -30,7 +30,6 @@
"create": "Create", "create": "Create",
"preview": "Preview", "preview": "Preview",
"loading": "Loading...", "loading": "Loading...",
"delete": "Delete",
"fieldUsername": "Username", "fieldUsername": "Username",
"fieldNickname": "Nickname", "fieldNickname": "Nickname",
"fieldEmail": "Email address", "fieldEmail": "Email address",

View File

@ -30,7 +30,6 @@
"apply": "应用", "apply": "应用",
"create": "创建", "create": "创建",
"preview": "预览", "preview": "预览",
"delete": "删除",
"fieldUsername": "用户名", "fieldUsername": "用户名",
"fieldNickname": "显示名", "fieldNickname": "显示名",
"fieldEmail": "电子邮箱地址", "fieldEmail": "电子邮箱地址",

View File

@ -84,12 +84,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
distance: 75, distance: 75,
type: ExpandableFabType.up, type: ExpandableFabType.up,
childrenAnimation: ExpandableFabAnimation.none, childrenAnimation: ExpandableFabAnimation.none,
overlayStyle: ExpandableFabOverlayStyle( overlayStyle: ExpandableFabOverlayStyle(blur: 10),
color: Theme.of(context)
.colorScheme
.surface
.withAlpha((255 * 0.5).round()),
),
openButtonBuilder: RotateFloatingActionButtonBuilder( openButtonBuilder: RotateFloatingActionButtonBuilder(
child: const Icon(Symbols.add, size: 28), child: const Icon(Symbols.add, size: 28),
fabSize: ExpandableFabSize.regular, fabSize: ExpandableFabSize.regular,

View File

@ -4,19 +4,15 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_media_pending_list.dart';
import 'package:surface/widgets/post/post_meta_editor.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:provider/provider.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_meta_editor.dart';
class PostEditorScreen extends StatefulWidget { class PostEditorScreen extends StatefulWidget {
final String mode; final String mode;
@ -37,9 +33,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
SnPublisher? _publisher; SnPublisher? _publisher;
List<SnPublisher>? _publishers; List<SnPublisher>? _publishers;
final List<XFile> _selectedMedia = List.empty(growable: true);
final List<SnAttachment> _attachments = List.empty(growable: true);
void _fetchPublishers() async { void _fetchPublishers() async {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/publishers'); final resp = await sn.client.get('/cgi/co/publishers');
@ -56,82 +49,22 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
final TextEditingController _contentController = TextEditingController(); final TextEditingController _contentController = TextEditingController();
double? _progress;
static const kAttachmentProgressWeight = 0.9;
static const kPostingProgressWeight = 0.1;
void _performAction() async { void _performAction() async {
if (_isBusy || _publisher == null) return; if (_isBusy || _publisher == null) return;
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final attach = context.read<SnAttachmentProvider>();
setState(() { setState(() => _isBusy = true);
_progress = 0;
_isBusy = true;
});
// Uploading attachments
try { try {
for (int i = 0; i < _selectedMedia.length; i++) {
final media = _selectedMedia[i];
final place = await attach.chunkedUploadInitialize(
await media.length(),
media.name,
'interactive',
null,
);
final item = await attach.chunkedUploadParts(
media,
place.$1,
place.$2,
onProgress: (progress) {
// Calculate overall progress for attachments
setState(() {
_progress = ((i + progress) / _selectedMedia.length) *
kAttachmentProgressWeight;
});
},
);
_attachments.add(item);
}
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
setState(() => _isBusy = false);
return;
}
setState(() => _progress = kAttachmentProgressWeight);
// Posting the content
try {
final baseProgressVal = _progress!;
await sn.client.post('/cgi/co/${widget.mode}', data: { await sn.client.post('/cgi/co/${widget.mode}', data: {
'publisher': _publisher!.id, 'publisher': _publisher!.id,
'content': _contentController.value.text, 'content': _contentController.value.text,
'title': _title, 'title': _title,
'description': _description, 'description': _description,
'attachments': _attachments.map((e) => e.rid).toList(),
}, onSendProgress: (count, total) {
setState(() {
_progress =
baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
}); });
}, onReceiveProgress: (count, total) {
setState(() {
_progress = baseProgressVal +
(kPostingProgressWeight / 2) +
(count / total) * (kPostingProgressWeight / 2);
});
});
if (!mounted) return;
Navigator.pop(context, true); Navigator.pop(context, true);
} catch (err) { } catch (err) {
if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
} finally { } finally {
setState(() => _isBusy = false); setState(() => _isBusy = false);
@ -155,15 +88,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
}); });
} }
final _imagePicker = ImagePicker();
void _selectMedia() async {
final result = await _imagePicker.pickMultipleMedia();
if (result.isEmpty) return;
_selectedMedia.addAll(result);
setState(() {});
}
@override @override
void dispose() { void dispose() {
_contentController.dispose(); _contentController.dispose();
@ -324,29 +248,15 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
), ),
), ),
), ),
if (_selectedMedia.isNotEmpty)
PostMediaPendingList(
data: _selectedMedia,
onRemove: (idx) {
setState(() {
_selectedMedia.removeAt(idx);
});
},
).padding(bottom: 8),
Material( Material(
elevation: 2, elevation: 2,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (_isBusy && _progress != null) if (_isBusy)
TweenAnimationBuilder<double>( const LinearProgressIndicator(
tween: Tween(begin: 0, end: 1), minHeight: 2,
duration: Duration(milliseconds: 300), ),
builder: (context, value, _) =>
LinearProgressIndicator(value: value, minHeight: 2),
)
else if (_isBusy)
const LinearProgressIndicator(value: null, minHeight: 2),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@ -358,7 +268,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
child: Row( child: Row(
children: [ children: [
IconButton( IconButton(
onPressed: _isBusy ? null : _selectMedia, onPressed: () {},
icon: Icon( icon: Icon(
Symbols.add_photo_alternate, Symbols.add_photo_alternate,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,

View File

@ -21,6 +21,7 @@ class SnAttachment with _$SnAttachment {
required int refCount, required int refCount,
required dynamic fileChunks, required dynamic fileChunks,
required dynamic cleanedAt, required dynamic cleanedAt,
required Map<String, dynamic> metadata,
required bool isMature, required bool isMature,
required bool isAnalyzed, required bool isAnalyzed,
required bool isUploaded, required bool isUploaded,
@ -30,7 +31,6 @@ class SnAttachment with _$SnAttachment {
required SnAttachmentPool? pool, required SnAttachmentPool? pool,
required int poolId, required int poolId,
required int accountId, required int accountId,
@Default({}) Map<String, dynamic> metadata,
}) = _SnAttachment; }) = _SnAttachment;
factory SnAttachment.fromJson(Map<String, Object?> json) => factory SnAttachment.fromJson(Map<String, Object?> json) =>

View File

@ -35,6 +35,7 @@ mixin _$SnAttachment {
int get refCount => throw _privateConstructorUsedError; int get refCount => throw _privateConstructorUsedError;
dynamic get fileChunks => throw _privateConstructorUsedError; dynamic get fileChunks => throw _privateConstructorUsedError;
dynamic get cleanedAt => throw _privateConstructorUsedError; dynamic get cleanedAt => throw _privateConstructorUsedError;
Map<String, dynamic> get metadata => throw _privateConstructorUsedError;
bool get isMature => throw _privateConstructorUsedError; bool get isMature => throw _privateConstructorUsedError;
bool get isAnalyzed => throw _privateConstructorUsedError; bool get isAnalyzed => throw _privateConstructorUsedError;
bool get isUploaded => throw _privateConstructorUsedError; bool get isUploaded => throw _privateConstructorUsedError;
@ -44,7 +45,6 @@ mixin _$SnAttachment {
SnAttachmentPool? get pool => throw _privateConstructorUsedError; SnAttachmentPool? get pool => throw _privateConstructorUsedError;
int get poolId => throw _privateConstructorUsedError; int get poolId => throw _privateConstructorUsedError;
int get accountId => throw _privateConstructorUsedError; int get accountId => throw _privateConstructorUsedError;
Map<String, dynamic> get metadata => throw _privateConstructorUsedError;
/// Serializes this SnAttachment to a JSON map. /// Serializes this SnAttachment to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -78,6 +78,7 @@ abstract class $SnAttachmentCopyWith<$Res> {
int refCount, int refCount,
dynamic fileChunks, dynamic fileChunks,
dynamic cleanedAt, dynamic cleanedAt,
Map<String, dynamic> metadata,
bool isMature, bool isMature,
bool isAnalyzed, bool isAnalyzed,
bool isUploaded, bool isUploaded,
@ -86,8 +87,7 @@ abstract class $SnAttachmentCopyWith<$Res> {
dynamic refId, dynamic refId,
SnAttachmentPool? pool, SnAttachmentPool? pool,
int poolId, int poolId,
int accountId, int accountId});
Map<String, dynamic> metadata});
$SnAttachmentPoolCopyWith<$Res>? get pool; $SnAttachmentPoolCopyWith<$Res>? get pool;
} }
@ -122,6 +122,7 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment>
Object? refCount = null, Object? refCount = null,
Object? fileChunks = freezed, Object? fileChunks = freezed,
Object? cleanedAt = freezed, Object? cleanedAt = freezed,
Object? metadata = null,
Object? isMature = null, Object? isMature = null,
Object? isAnalyzed = null, Object? isAnalyzed = null,
Object? isUploaded = null, Object? isUploaded = null,
@ -131,7 +132,6 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment>
Object? pool = freezed, Object? pool = freezed,
Object? poolId = null, Object? poolId = null,
Object? accountId = null, Object? accountId = null,
Object? metadata = null,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
id: null == id id: null == id
@ -194,6 +194,10 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment>
? _value.cleanedAt ? _value.cleanedAt
: cleanedAt // ignore: cast_nullable_to_non_nullable : cleanedAt // ignore: cast_nullable_to_non_nullable
as dynamic, as dynamic,
metadata: null == metadata
? _value.metadata
: metadata // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
isMature: null == isMature isMature: null == isMature
? _value.isMature ? _value.isMature
: isMature // ignore: cast_nullable_to_non_nullable : isMature // ignore: cast_nullable_to_non_nullable
@ -230,10 +234,6 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment>
? _value.accountId ? _value.accountId
: accountId // ignore: cast_nullable_to_non_nullable : accountId // ignore: cast_nullable_to_non_nullable
as int, as int,
metadata: null == metadata
? _value.metadata
: metadata // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
) as $Val); ) as $Val);
} }
@ -276,6 +276,7 @@ abstract class _$$SnAttachmentImplCopyWith<$Res>
int refCount, int refCount,
dynamic fileChunks, dynamic fileChunks,
dynamic cleanedAt, dynamic cleanedAt,
Map<String, dynamic> metadata,
bool isMature, bool isMature,
bool isAnalyzed, bool isAnalyzed,
bool isUploaded, bool isUploaded,
@ -284,8 +285,7 @@ abstract class _$$SnAttachmentImplCopyWith<$Res>
dynamic refId, dynamic refId,
SnAttachmentPool? pool, SnAttachmentPool? pool,
int poolId, int poolId,
int accountId, int accountId});
Map<String, dynamic> metadata});
@override @override
$SnAttachmentPoolCopyWith<$Res>? get pool; $SnAttachmentPoolCopyWith<$Res>? get pool;
@ -319,6 +319,7 @@ class __$$SnAttachmentImplCopyWithImpl<$Res>
Object? refCount = null, Object? refCount = null,
Object? fileChunks = freezed, Object? fileChunks = freezed,
Object? cleanedAt = freezed, Object? cleanedAt = freezed,
Object? metadata = null,
Object? isMature = null, Object? isMature = null,
Object? isAnalyzed = null, Object? isAnalyzed = null,
Object? isUploaded = null, Object? isUploaded = null,
@ -328,7 +329,6 @@ class __$$SnAttachmentImplCopyWithImpl<$Res>
Object? pool = freezed, Object? pool = freezed,
Object? poolId = null, Object? poolId = null,
Object? accountId = null, Object? accountId = null,
Object? metadata = null,
}) { }) {
return _then(_$SnAttachmentImpl( return _then(_$SnAttachmentImpl(
id: null == id id: null == id
@ -391,6 +391,10 @@ class __$$SnAttachmentImplCopyWithImpl<$Res>
? _value.cleanedAt ? _value.cleanedAt
: cleanedAt // ignore: cast_nullable_to_non_nullable : cleanedAt // ignore: cast_nullable_to_non_nullable
as dynamic, as dynamic,
metadata: null == metadata
? _value._metadata
: metadata // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
isMature: null == isMature isMature: null == isMature
? _value.isMature ? _value.isMature
: isMature // ignore: cast_nullable_to_non_nullable : isMature // ignore: cast_nullable_to_non_nullable
@ -427,10 +431,6 @@ class __$$SnAttachmentImplCopyWithImpl<$Res>
? _value.accountId ? _value.accountId
: accountId // ignore: cast_nullable_to_non_nullable : accountId // ignore: cast_nullable_to_non_nullable
as int, as int,
metadata: null == metadata
? _value._metadata
: metadata // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
)); ));
} }
} }
@ -454,6 +454,7 @@ class _$SnAttachmentImpl implements _SnAttachment {
required this.refCount, required this.refCount,
required this.fileChunks, required this.fileChunks,
required this.cleanedAt, required this.cleanedAt,
required final Map<String, dynamic> metadata,
required this.isMature, required this.isMature,
required this.isAnalyzed, required this.isAnalyzed,
required this.isUploaded, required this.isUploaded,
@ -462,8 +463,7 @@ class _$SnAttachmentImpl implements _SnAttachment {
required this.refId, required this.refId,
required this.pool, required this.pool,
required this.poolId, required this.poolId,
required this.accountId, required this.accountId})
final Map<String, dynamic> metadata = const {}})
: _metadata = metadata; : _metadata = metadata;
factory _$SnAttachmentImpl.fromJson(Map<String, dynamic> json) => factory _$SnAttachmentImpl.fromJson(Map<String, dynamic> json) =>
@ -499,6 +499,14 @@ class _$SnAttachmentImpl implements _SnAttachment {
final dynamic fileChunks; final dynamic fileChunks;
@override @override
final dynamic cleanedAt; final dynamic cleanedAt;
final Map<String, dynamic> _metadata;
@override
Map<String, dynamic> get metadata {
if (_metadata is EqualUnmodifiableMapView) return _metadata;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_metadata);
}
@override @override
final bool isMature; final bool isMature;
@override @override
@ -517,18 +525,10 @@ class _$SnAttachmentImpl implements _SnAttachment {
final int poolId; final int poolId;
@override @override
final int accountId; final int accountId;
final Map<String, dynamic> _metadata;
@override
@JsonKey()
Map<String, dynamic> get metadata {
if (_metadata is EqualUnmodifiableMapView) return _metadata;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_metadata);
}
@override @override
String toString() { String toString() {
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, fileChunks: $fileChunks, cleanedAt: $cleanedAt, isMature: $isMature, isAnalyzed: $isAnalyzed, isUploaded: $isUploaded, isSelfRef: $isSelfRef, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, accountId: $accountId, metadata: $metadata)'; return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, fileChunks: $fileChunks, cleanedAt: $cleanedAt, metadata: $metadata, isMature: $isMature, isAnalyzed: $isAnalyzed, isUploaded: $isUploaded, isSelfRef: $isSelfRef, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, accountId: $accountId)';
} }
@override @override
@ -557,6 +557,7 @@ class _$SnAttachmentImpl implements _SnAttachment {
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other.fileChunks, fileChunks) && .equals(other.fileChunks, fileChunks) &&
const DeepCollectionEquality().equals(other.cleanedAt, cleanedAt) && const DeepCollectionEquality().equals(other.cleanedAt, cleanedAt) &&
const DeepCollectionEquality().equals(other._metadata, _metadata) &&
(identical(other.isMature, isMature) || (identical(other.isMature, isMature) ||
other.isMature == isMature) && other.isMature == isMature) &&
(identical(other.isAnalyzed, isAnalyzed) || (identical(other.isAnalyzed, isAnalyzed) ||
@ -570,8 +571,7 @@ class _$SnAttachmentImpl implements _SnAttachment {
(identical(other.pool, pool) || other.pool == pool) && (identical(other.pool, pool) || other.pool == pool) &&
(identical(other.poolId, poolId) || other.poolId == poolId) && (identical(other.poolId, poolId) || other.poolId == poolId) &&
(identical(other.accountId, accountId) || (identical(other.accountId, accountId) ||
other.accountId == accountId) && other.accountId == accountId));
const DeepCollectionEquality().equals(other._metadata, _metadata));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -593,6 +593,7 @@ class _$SnAttachmentImpl implements _SnAttachment {
refCount, refCount,
const DeepCollectionEquality().hash(fileChunks), const DeepCollectionEquality().hash(fileChunks),
const DeepCollectionEquality().hash(cleanedAt), const DeepCollectionEquality().hash(cleanedAt),
const DeepCollectionEquality().hash(_metadata),
isMature, isMature,
isAnalyzed, isAnalyzed,
isUploaded, isUploaded,
@ -601,8 +602,7 @@ class _$SnAttachmentImpl implements _SnAttachment {
const DeepCollectionEquality().hash(refId), const DeepCollectionEquality().hash(refId),
pool, pool,
poolId, poolId,
accountId, accountId
const DeepCollectionEquality().hash(_metadata)
]); ]);
/// Create a copy of SnAttachment /// Create a copy of SnAttachment
@ -638,6 +638,7 @@ abstract class _SnAttachment implements SnAttachment {
required final int refCount, required final int refCount,
required final dynamic fileChunks, required final dynamic fileChunks,
required final dynamic cleanedAt, required final dynamic cleanedAt,
required final Map<String, dynamic> metadata,
required final bool isMature, required final bool isMature,
required final bool isAnalyzed, required final bool isAnalyzed,
required final bool isUploaded, required final bool isUploaded,
@ -646,8 +647,7 @@ abstract class _SnAttachment implements SnAttachment {
required final dynamic refId, required final dynamic refId,
required final SnAttachmentPool? pool, required final SnAttachmentPool? pool,
required final int poolId, required final int poolId,
required final int accountId, required final int accountId}) = _$SnAttachmentImpl;
final Map<String, dynamic> metadata}) = _$SnAttachmentImpl;
factory _SnAttachment.fromJson(Map<String, dynamic> json) = factory _SnAttachment.fromJson(Map<String, dynamic> json) =
_$SnAttachmentImpl.fromJson; _$SnAttachmentImpl.fromJson;
@ -683,6 +683,8 @@ abstract class _SnAttachment implements SnAttachment {
@override @override
dynamic get cleanedAt; dynamic get cleanedAt;
@override @override
Map<String, dynamic> get metadata;
@override
bool get isMature; bool get isMature;
@override @override
bool get isAnalyzed; bool get isAnalyzed;
@ -700,8 +702,6 @@ abstract class _SnAttachment implements SnAttachment {
int get poolId; int get poolId;
@override @override
int get accountId; int get accountId;
@override
Map<String, dynamic> get metadata;
/// Create a copy of SnAttachment /// Create a copy of SnAttachment
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.

View File

@ -23,6 +23,7 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
refCount: (json['ref_count'] as num).toInt(), refCount: (json['ref_count'] as num).toInt(),
fileChunks: json['file_chunks'], fileChunks: json['file_chunks'],
cleanedAt: json['cleaned_at'], cleanedAt: json['cleaned_at'],
metadata: json['metadata'] as Map<String, dynamic>,
isMature: json['is_mature'] as bool, isMature: json['is_mature'] as bool,
isAnalyzed: json['is_analyzed'] as bool, isAnalyzed: json['is_analyzed'] as bool,
isUploaded: json['is_uploaded'] as bool, isUploaded: json['is_uploaded'] as bool,
@ -34,7 +35,6 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
: SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>), : SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>),
poolId: (json['pool_id'] as num).toInt(), poolId: (json['pool_id'] as num).toInt(),
accountId: (json['account_id'] as num).toInt(), accountId: (json['account_id'] as num).toInt(),
metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
); );
Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) => Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
@ -54,6 +54,7 @@ Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
'ref_count': instance.refCount, 'ref_count': instance.refCount,
'file_chunks': instance.fileChunks, 'file_chunks': instance.fileChunks,
'cleaned_at': instance.cleanedAt, 'cleaned_at': instance.cleanedAt,
'metadata': instance.metadata,
'is_mature': instance.isMature, 'is_mature': instance.isMature,
'is_analyzed': instance.isAnalyzed, 'is_analyzed': instance.isAnalyzed,
'is_uploaded': instance.isUploaded, 'is_uploaded': instance.isUploaded,
@ -63,7 +64,6 @@ Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
'pool': instance.pool?.toJson(), 'pool': instance.pool?.toJson(),
'pool_id': instance.poolId, 'pool_id': instance.poolId,
'account_id': instance.accountId, 'account_id': instance.accountId,
'metadata': instance.metadata,
}; };
_$SnAttachmentPoolImpl _$$SnAttachmentPoolImplFromJson( _$SnAttachmentPoolImpl _$$SnAttachmentPoolImplFromJson(

View File

@ -1,37 +0,0 @@
import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:uuid/uuid.dart';
class AttachmentDetailPopup extends StatelessWidget {
final SnAttachment data;
final String? heroTag;
const AttachmentDetailPopup({super.key, required this.data, this.heroTag});
@override
Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>();
final uuid = Uuid();
return DismissiblePage(
onDismissed: () {
Navigator.of(context).pop();
},
direction: DismissiblePageDismissDirection.down,
backgroundColor: Colors.transparent,
isFullScreen: true,
child: Hero(
tag: 'attachment-${data.rid}-${heroTag ?? uuid.v4()}',
child: PhotoView(
imageProvider: UniversalImage.provider(
sn.getAttachmentUrl(data.rid),
),
),
),
);
}
}

View File

@ -1,55 +1,25 @@
import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_detail.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:uuid/uuid.dart';
class AttachmentItem extends StatelessWidget { class AttachmentItem extends StatelessWidget {
final SnAttachment data; final SnAttachment data;
final bool isExpandable; const AttachmentItem({super.key, required this.data});
const AttachmentItem({
super.key,
required this.data,
this.isExpandable = false,
});
Widget _buildContent(BuildContext context, String heroTag) { @override
Widget build(BuildContext context) {
final tp = data.mimetype.split('/').firstOrNull; final tp = data.mimetype.split('/').firstOrNull;
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
switch (tp) { switch (tp) {
case 'image': case 'image':
return Hero( return AspectRatio(
tag: 'attachment-${data.rid}-$heroTag', aspectRatio: data.metadata['ratio']?.toDouble(),
child: UniversalImage( child: UniversalImage(sn.getAttachmentUrl(data.rid)),
sn.getAttachmentUrl(data.rid),
fit: BoxFit.cover,
),
); );
default: default:
return const Placeholder(); return const Placeholder();
} }
} }
@override
Widget build(BuildContext context) {
final uuid = Uuid();
final heroTag = uuid.v4();
if (isExpandable) {
return GestureDetector(
child: _buildContent(context, heroTag),
onTap: () {
context.pushTransparentRoute(
AttachmentDetailPopup(data: data, heroTag: heroTag),
rootNavigator: true,
);
},
);
}
return _buildContent(context, heroTag);
}
} }

View File

@ -1,9 +1,6 @@
import 'dart:math' as math;
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:surface/widgets/attachment/attachment_item.dart';
@ -11,16 +8,8 @@ class AttachmentList extends StatelessWidget {
final List<SnAttachment> data; final List<SnAttachment> data;
final bool? bordered; final bool? bordered;
final double? maxListHeight; final double? maxListHeight;
const AttachmentList({ const AttachmentList(
super.key, {super.key, required this.data, this.bordered, this.maxListHeight});
required this.data,
this.bordered,
this.maxListHeight,
});
static const double kMaxListItemWidth = 520;
static const BorderRadius kDefaultRadius =
BorderRadius.all(Radius.circular(8));
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -30,36 +19,11 @@ class AttachmentList extends StatelessWidget {
if (data.isEmpty) return const SizedBox.shrink(); if (data.isEmpty) return const SizedBox.shrink();
if (data.length == 1) { if (data.length == 1) {
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) {
return Container(
constraints: BoxConstraints(
maxWidth: math.min(
MediaQuery.of(context).size.width - 20,
kMaxListItemWidth,
),
),
decoration: BoxDecoration(
border: Border(top: borderSide, bottom: borderSide),
borderRadius: kDefaultRadius,
),
child: AspectRatio(
aspectRatio: data[0].metadata['ratio']?.toDouble() ?? 1,
child: ClipRRect(
borderRadius: kDefaultRadius,
child: AttachmentItem(data: data[0], isExpandable: true),
),
),
);
}
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border(top: borderSide, bottom: borderSide), border: Border(top: borderSide, bottom: borderSide),
), ),
child: AspectRatio( child: AttachmentItem(data: data[0]),
aspectRatio: data[0].metadata['ratio']?.toDouble() ?? 1,
child: AttachmentItem(data: data[0], isExpandable: true),
),
); );
} }
@ -71,23 +35,15 @@ class AttachmentList extends StatelessWidget {
shrinkWrap: true, shrinkWrap: true,
itemCount: data.length, itemCount: data.length,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
const radius = BorderRadius.all(Radius.circular(8));
return Container( return Container(
constraints: BoxConstraints(
maxWidth: math.min(
MediaQuery.of(context).size.width - 20,
kMaxListItemWidth,
),
),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border(top: borderSide, bottom: borderSide), border: Border(top: borderSide, bottom: borderSide),
borderRadius: kDefaultRadius, borderRadius: radius,
), ),
child: AspectRatio(
aspectRatio: data[idx].metadata['ratio']?.toDouble() ?? 1,
child: ClipRRect( child: ClipRRect(
borderRadius: kDefaultRadius, borderRadius: radius,
child: AttachmentItem(data: data[idx], isExpandable: true), child: AttachmentItem(data: data[idx]),
),
), ),
); );
}, },

View File

@ -1,67 +0,0 @@
import 'dart:io';
import 'package:cross_file/cross_file.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart';
class PostMediaPendingList extends StatelessWidget {
final List<XFile> data;
final Function(int idx)? onRemove;
const PostMediaPendingList({
super.key,
required this.data,
this.onRemove,
});
@override
Widget build(BuildContext context) {
return Container(
constraints: const BoxConstraints(maxHeight: 120),
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
separatorBuilder: (context, index) => const Gap(8),
itemCount: data.length,
itemBuilder: (context, idx) {
final file = data[idx];
return ContextMenuRegion(
contextMenu: ContextMenu(
entries: [
if (onRemove != null)
MenuItem(
label: 'delete'.tr(),
icon: Symbols.delete,
onSelected: () {
onRemove!(idx);
},
),
],
),
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: BorderRadius.circular(8),
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AspectRatio(
aspectRatio: 1,
child: kIsWeb
? Image.network(file.path, fit: BoxFit.cover)
: Image.file(File(file.path), fit: BoxFit.cover),
),
),
),
);
},
),
);
}
}

View File

@ -334,14 +334,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
dismissible_page:
dependency: "direct main"
description:
name: dismissible_page
sha256: "5b2316f770fe83583f770df1f6505cb19102081c5971979806e77f2e507a9958"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
dropdown_button2: dropdown_button2:
dependency: "direct main" dependency: "direct main"
description: description:
@ -475,14 +467,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.1" version: "3.4.1"
flutter_context_menu:
dependency: "direct main"
description:
name: flutter_context_menu
sha256: "4bc1dc30ae5aa705ed99ebbeb875898c6341a6d092397a566fecd5184b392380"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
flutter_expandable_fab: flutter_expandable_fab:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1042,14 +1026,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.2" version: "6.0.2"
photo_view:
dependency: "direct main"
description:
name: photo_view
sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e"
url: "https://pub.dev"
source: hosted
version: "0.15.0"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@ -1432,7 +1408,7 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
uuid: uuid:
dependency: "direct main" dependency: transitive
description: description:
name: uuid name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff

View File

@ -66,10 +66,6 @@ dependencies:
croppy: ^1.3.1 croppy: ^1.3.1
flutter_expandable_fab: ^2.3.0 flutter_expandable_fab: ^2.3.0
dropdown_button2: ^2.3.9 dropdown_button2: ^2.3.9
flutter_context_menu: ^0.2.0
dismissible_page: ^1.0.2
uuid: ^4.5.1
photo_view: ^0.15.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: