✨ Post tags
This commit is contained in:
parent
312d68286e
commit
420588860a
@ -103,6 +103,7 @@
|
||||
"fieldPostContent": "What happened?!",
|
||||
"fieldPostTitle": "Title",
|
||||
"fieldPostDescription": "Description",
|
||||
"fieldPostTags": "Tags",
|
||||
"postPublish": "Publish",
|
||||
"postPosted": "Post has been posted.",
|
||||
"postPublishedAt": "Published At",
|
||||
|
@ -103,6 +103,7 @@
|
||||
"fieldPostContent": "发生什么事了?!",
|
||||
"fieldPostTitle": "标题",
|
||||
"fieldPostDescription": "描述",
|
||||
"fieldPostTags": "标签",
|
||||
"postPublish": "发布",
|
||||
"postPublishedAt": "发布于",
|
||||
"postPublishedUntil": "取消发布于",
|
||||
|
@ -107,7 +107,7 @@ PODS:
|
||||
- flutter_udid (0.0.1):
|
||||
- Flutter
|
||||
- SAMKeychain
|
||||
- flutter_webrtc (0.11.8):
|
||||
- flutter_webrtc (0.12.2):
|
||||
- Flutter
|
||||
- WebRTC-SDK (= 125.6422.05)
|
||||
- GoogleAppMeasurement (11.4.0):
|
||||
@ -341,7 +341,7 @@ SPEC CHECKSUMS:
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a
|
||||
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
||||
flutter_webrtc: 4f730f3d58a28b0afdea039c8bf4a0f616a6b20c
|
||||
flutter_webrtc: 043d1b47e9795158dd97dc84f1c152cd0e98b93b
|
||||
GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
|
@ -172,6 +172,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
SnPublisher? publisher;
|
||||
SnPost? editingPost, repostingPost, replyingPost;
|
||||
|
||||
List<String> tags = List.empty();
|
||||
List<PostWriteMedia> attachments = List.empty(growable: true);
|
||||
DateTime? publishedAt, publishedUntil;
|
||||
|
||||
@ -195,6 +196,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
contentController.text = post.body['content'] ?? '';
|
||||
publishedAt = post.publishedAt;
|
||||
publishedUntil = post.publishedUntil;
|
||||
tags = List.from(post.tags.map((ele) => ele.alias));
|
||||
attachments.addAll(
|
||||
post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? [],
|
||||
);
|
||||
@ -290,6 +292,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
.where((e) => e.attachment != null)
|
||||
.map((e) => e.attachment!.rid)
|
||||
.toList(),
|
||||
'tags': tags.map((ele) => {'alias': ele}).toList(),
|
||||
if (publishedAt != null)
|
||||
'published_at': publishedAt!.toUtc().toIso8601String(),
|
||||
if (publishedUntil != null)
|
||||
@ -351,6 +354,11 @@ class PostWriteController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setTags(List<String> value) {
|
||||
tags = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setIsBusy(bool value) {
|
||||
isBusy = value;
|
||||
notifyListeners();
|
||||
|
@ -112,7 +112,7 @@ class SnPostContentProvider {
|
||||
Future<SnPost> getPost(dynamic id) async {
|
||||
final resp = await _sn.client.get('/cgi/co/posts/$id');
|
||||
final out = _preloadRelatedDataSingle(
|
||||
SnPost.fromJson(resp.data['data']),
|
||||
SnPost.fromJson(resp.data),
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
@ -141,7 +141,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
||||
),
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: PostWriteController.kTitleMap[widget.mode]!,
|
||||
text: PostWriteController.kTitleMap[widget.mode]!.tr(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
|
@ -18,7 +18,7 @@ class SnPost with _$SnPost {
|
||||
required String language,
|
||||
required String? alias,
|
||||
required String? aliasPrefix,
|
||||
@Default([]) List<dynamic> tags,
|
||||
@Default([]) List<SnPostTag> tags,
|
||||
@Default([]) List<dynamic> categories,
|
||||
required List<SnPost>? replies,
|
||||
required int? replyId,
|
||||
@ -50,6 +50,23 @@ class SnPost with _$SnPost {
|
||||
};
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnPostTag with _$SnPostTag {
|
||||
const factory SnPostTag({
|
||||
required int id,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required dynamic deletedAt,
|
||||
required String alias,
|
||||
required String name,
|
||||
required String description,
|
||||
required dynamic posts,
|
||||
}) = _SnPostTag;
|
||||
|
||||
factory SnPostTag.fromJson(Map<String, Object?> json) =>
|
||||
_$SnPostTagFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SnPostPreload with _$SnPostPreload {
|
||||
const factory SnPostPreload({
|
||||
|
@ -29,7 +29,7 @@ mixin _$SnPost {
|
||||
String get language => throw _privateConstructorUsedError;
|
||||
String? get alias => throw _privateConstructorUsedError;
|
||||
String? get aliasPrefix => throw _privateConstructorUsedError;
|
||||
List<dynamic> get tags => throw _privateConstructorUsedError;
|
||||
List<SnPostTag> get tags => throw _privateConstructorUsedError;
|
||||
List<dynamic> get categories => throw _privateConstructorUsedError;
|
||||
List<SnPost>? get replies => throw _privateConstructorUsedError;
|
||||
int? get replyId => throw _privateConstructorUsedError;
|
||||
@ -76,7 +76,7 @@ abstract class $SnPostCopyWith<$Res> {
|
||||
String language,
|
||||
String? alias,
|
||||
String? aliasPrefix,
|
||||
List<dynamic> tags,
|
||||
List<SnPostTag> tags,
|
||||
List<dynamic> categories,
|
||||
List<SnPost>? replies,
|
||||
int? replyId,
|
||||
@ -193,7 +193,7 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
||||
tags: null == tags
|
||||
? _value.tags
|
||||
: tags // ignore: cast_nullable_to_non_nullable
|
||||
as List<dynamic>,
|
||||
as List<SnPostTag>,
|
||||
categories: null == categories
|
||||
? _value.categories
|
||||
: categories // ignore: cast_nullable_to_non_nullable
|
||||
@ -361,7 +361,7 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
|
||||
String language,
|
||||
String? alias,
|
||||
String? aliasPrefix,
|
||||
List<dynamic> tags,
|
||||
List<SnPostTag> tags,
|
||||
List<dynamic> categories,
|
||||
List<SnPost>? replies,
|
||||
int? replyId,
|
||||
@ -481,7 +481,7 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
||||
tags: null == tags
|
||||
? _value._tags
|
||||
: tags // ignore: cast_nullable_to_non_nullable
|
||||
as List<dynamic>,
|
||||
as List<SnPostTag>,
|
||||
categories: null == categories
|
||||
? _value._categories
|
||||
: categories // ignore: cast_nullable_to_non_nullable
|
||||
@ -583,7 +583,7 @@ class _$SnPostImpl extends _SnPost {
|
||||
required this.language,
|
||||
required this.alias,
|
||||
required this.aliasPrefix,
|
||||
final List<dynamic> tags = const [],
|
||||
final List<SnPostTag> tags = const [],
|
||||
final List<dynamic> categories = const [],
|
||||
required final List<SnPost>? replies,
|
||||
required this.replyId,
|
||||
@ -640,10 +640,10 @@ class _$SnPostImpl extends _SnPost {
|
||||
final String? alias;
|
||||
@override
|
||||
final String? aliasPrefix;
|
||||
final List<dynamic> _tags;
|
||||
final List<SnPostTag> _tags;
|
||||
@override
|
||||
@JsonKey()
|
||||
List<dynamic> get tags {
|
||||
List<SnPostTag> get tags {
|
||||
if (_tags is EqualUnmodifiableListView) return _tags;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_tags);
|
||||
@ -852,7 +852,7 @@ abstract class _SnPost extends SnPost {
|
||||
required final String language,
|
||||
required final String? alias,
|
||||
required final String? aliasPrefix,
|
||||
final List<dynamic> tags,
|
||||
final List<SnPostTag> tags,
|
||||
final List<dynamic> categories,
|
||||
required final List<SnPost>? replies,
|
||||
required final int? replyId,
|
||||
@ -897,7 +897,7 @@ abstract class _SnPost extends SnPost {
|
||||
@override
|
||||
String? get aliasPrefix;
|
||||
@override
|
||||
List<dynamic> get tags;
|
||||
List<SnPostTag> get tags;
|
||||
@override
|
||||
List<dynamic> get categories;
|
||||
@override
|
||||
@ -949,6 +949,310 @@ abstract class _SnPost extends SnPost {
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
SnPostTag _$SnPostTagFromJson(Map<String, dynamic> json) {
|
||||
return _SnPostTag.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnPostTag {
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||
dynamic get deletedAt => throw _privateConstructorUsedError;
|
||||
String get alias => throw _privateConstructorUsedError;
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
String get description => throw _privateConstructorUsedError;
|
||||
dynamic get posts => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this SnPostTag to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of SnPostTag
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$SnPostTagCopyWith<SnPostTag> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $SnPostTagCopyWith<$Res> {
|
||||
factory $SnPostTagCopyWith(SnPostTag value, $Res Function(SnPostTag) then) =
|
||||
_$SnPostTagCopyWithImpl<$Res, SnPostTag>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
dynamic deletedAt,
|
||||
String alias,
|
||||
String name,
|
||||
String description,
|
||||
dynamic posts});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$SnPostTagCopyWithImpl<$Res, $Val extends SnPostTag>
|
||||
implements $SnPostTagCopyWith<$Res> {
|
||||
_$SnPostTagCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of SnPostTag
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? alias = null,
|
||||
Object? name = null,
|
||||
Object? description = null,
|
||||
Object? posts = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
alias: null == alias
|
||||
? _value.alias
|
||||
: alias // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
description: null == description
|
||||
? _value.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
posts: freezed == posts
|
||||
? _value.posts
|
||||
: posts // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$SnPostTagImplCopyWith<$Res>
|
||||
implements $SnPostTagCopyWith<$Res> {
|
||||
factory _$$SnPostTagImplCopyWith(
|
||||
_$SnPostTagImpl value, $Res Function(_$SnPostTagImpl) then) =
|
||||
__$$SnPostTagImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int id,
|
||||
DateTime createdAt,
|
||||
DateTime updatedAt,
|
||||
dynamic deletedAt,
|
||||
String alias,
|
||||
String name,
|
||||
String description,
|
||||
dynamic posts});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$SnPostTagImplCopyWithImpl<$Res>
|
||||
extends _$SnPostTagCopyWithImpl<$Res, _$SnPostTagImpl>
|
||||
implements _$$SnPostTagImplCopyWith<$Res> {
|
||||
__$$SnPostTagImplCopyWithImpl(
|
||||
_$SnPostTagImpl _value, $Res Function(_$SnPostTagImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of SnPostTag
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? createdAt = null,
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? alias = null,
|
||||
Object? name = null,
|
||||
Object? description = null,
|
||||
Object? posts = freezed,
|
||||
}) {
|
||||
return _then(_$SnPostTagImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
createdAt: null == createdAt
|
||||
? _value.createdAt
|
||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
updatedAt: null == updatedAt
|
||||
? _value.updatedAt
|
||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
deletedAt: freezed == deletedAt
|
||||
? _value.deletedAt
|
||||
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
alias: null == alias
|
||||
? _value.alias
|
||||
: alias // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
description: null == description
|
||||
? _value.description
|
||||
: description // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
posts: freezed == posts
|
||||
? _value.posts
|
||||
: posts // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$SnPostTagImpl implements _SnPostTag {
|
||||
const _$SnPostTagImpl(
|
||||
{required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.alias,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.posts});
|
||||
|
||||
factory _$SnPostTagImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SnPostTagImplFromJson(json);
|
||||
|
||||
@override
|
||||
final int id;
|
||||
@override
|
||||
final DateTime createdAt;
|
||||
@override
|
||||
final DateTime updatedAt;
|
||||
@override
|
||||
final dynamic deletedAt;
|
||||
@override
|
||||
final String alias;
|
||||
@override
|
||||
final String name;
|
||||
@override
|
||||
final String description;
|
||||
@override
|
||||
final dynamic posts;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPostTag(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, alias: $alias, name: $name, description: $description, posts: $posts)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$SnPostTagImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.createdAt, createdAt) ||
|
||||
other.createdAt == createdAt) &&
|
||||
(identical(other.updatedAt, updatedAt) ||
|
||||
other.updatedAt == updatedAt) &&
|
||||
const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
|
||||
(identical(other.alias, alias) || other.alias == alias) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.description, description) ||
|
||||
other.description == description) &&
|
||||
const DeepCollectionEquality().equals(other.posts, posts));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
id,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
const DeepCollectionEquality().hash(deletedAt),
|
||||
alias,
|
||||
name,
|
||||
description,
|
||||
const DeepCollectionEquality().hash(posts));
|
||||
|
||||
/// Create a copy of SnPostTag
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$SnPostTagImplCopyWith<_$SnPostTagImpl> get copyWith =>
|
||||
__$$SnPostTagImplCopyWithImpl<_$SnPostTagImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$SnPostTagImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SnPostTag implements SnPostTag {
|
||||
const factory _SnPostTag(
|
||||
{required final int id,
|
||||
required final DateTime createdAt,
|
||||
required final DateTime updatedAt,
|
||||
required final dynamic deletedAt,
|
||||
required final String alias,
|
||||
required final String name,
|
||||
required final String description,
|
||||
required final dynamic posts}) = _$SnPostTagImpl;
|
||||
|
||||
factory _SnPostTag.fromJson(Map<String, dynamic> json) =
|
||||
_$SnPostTagImpl.fromJson;
|
||||
|
||||
@override
|
||||
int get id;
|
||||
@override
|
||||
DateTime get createdAt;
|
||||
@override
|
||||
DateTime get updatedAt;
|
||||
@override
|
||||
dynamic get deletedAt;
|
||||
@override
|
||||
String get alias;
|
||||
@override
|
||||
String get name;
|
||||
@override
|
||||
String get description;
|
||||
@override
|
||||
dynamic get posts;
|
||||
|
||||
/// Create a copy of SnPostTag
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$SnPostTagImplCopyWith<_$SnPostTagImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
SnPostPreload _$SnPostPreloadFromJson(Map<String, dynamic> json) {
|
||||
return _SnPostPreload.fromJson(json);
|
||||
}
|
||||
|
@ -18,7 +18,10 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
|
||||
language: json['language'] as String,
|
||||
alias: json['alias'] as String?,
|
||||
aliasPrefix: json['alias_prefix'] as String?,
|
||||
tags: json['tags'] as List<dynamic>? ?? const [],
|
||||
tags: (json['tags'] as List<dynamic>?)
|
||||
?.map((e) => SnPostTag.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
categories: json['categories'] as List<dynamic>? ?? const [],
|
||||
replies: (json['replies'] as List<dynamic>?)
|
||||
?.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
|
||||
@ -76,7 +79,7 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
|
||||
'language': instance.language,
|
||||
'alias': instance.alias,
|
||||
'alias_prefix': instance.aliasPrefix,
|
||||
'tags': instance.tags,
|
||||
'tags': instance.tags.map((e) => e.toJson()).toList(),
|
||||
'categories': instance.categories,
|
||||
'replies': instance.replies?.map((e) => e.toJson()).toList(),
|
||||
'reply_id': instance.replyId,
|
||||
@ -100,6 +103,30 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
|
||||
'preload': instance.preload?.toJson(),
|
||||
};
|
||||
|
||||
_$SnPostTagImpl _$$SnPostTagImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnPostTagImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'],
|
||||
alias: json['alias'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
posts: json['posts'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$SnPostTagImplToJson(_$SnPostTagImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt,
|
||||
'alias': instance.alias,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'posts': instance.posts,
|
||||
};
|
||||
|
||||
_$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) =>
|
||||
_$SnPostPreloadImpl(
|
||||
thumbnail: json['thumbnail'] == null
|
||||
|
@ -61,6 +61,8 @@ class PostItem extends StatelessWidget {
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
if (data.tags.isNotEmpty)
|
||||
_PostTagsList(data: data).padding(horizontal: 16, bottom: 6),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -388,6 +390,32 @@ class _PostQuoteContent extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _PostTagsList extends StatelessWidget {
|
||||
final SnPost data;
|
||||
const _PostTagsList({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: data.tags
|
||||
.map(
|
||||
(ele) => InkWell(
|
||||
child: Text(
|
||||
'#${ele.alias}',
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
).fontSize(13),
|
||||
onTap: () {},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
).opacity(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostTruncatedHint extends StatelessWidget {
|
||||
final SnPost data;
|
||||
const _PostTruncatedHint({super.key, required this.data});
|
||||
|
@ -5,6 +5,7 @@ import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/controllers/post_write_controller.dart';
|
||||
import 'package:surface/widgets/post/post_tags_field.dart';
|
||||
|
||||
class PostMetaEditor extends StatelessWidget {
|
||||
final PostWriteController controller;
|
||||
@ -69,6 +70,14 @@ class PostMetaEditor extends StatelessWidget {
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).padding(horizontal: 24),
|
||||
const Gap(4),
|
||||
PostTagsField(
|
||||
initialTags: controller.tags,
|
||||
labelText: 'fieldPostTags'.tr(),
|
||||
onUpdate: (value) {
|
||||
controller.setTags(value);
|
||||
},
|
||||
).padding(horizontal: 24),
|
||||
const Gap(12),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.event_available),
|
||||
|
204
lib/widgets/post/post_tags_field.dart
Normal file
204
lib/widgets/post/post_tags_field.dart
Normal file
@ -0,0 +1,204 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
|
||||
class PostTagsField extends StatefulWidget {
|
||||
final List<String>? initialTags;
|
||||
final String labelText;
|
||||
final Function(List<String>) onUpdate;
|
||||
|
||||
const PostTagsField({
|
||||
super.key,
|
||||
this.initialTags,
|
||||
required this.labelText,
|
||||
required this.onUpdate,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PostTagsField> createState() => _PostTagsFieldState();
|
||||
}
|
||||
|
||||
class _PostTagsFieldState extends State<PostTagsField> {
|
||||
static const List<String> kTagsDividers = [' ', ','];
|
||||
|
||||
late final _Debounceable<List<String>?, String> _debouncedSearch;
|
||||
|
||||
final List<String> _currentTags = List.empty(growable: true);
|
||||
|
||||
String? _currentSearchProbe;
|
||||
List<String> _lastAutocompleteResult = List.empty();
|
||||
TextEditingController? _textEditingController;
|
||||
|
||||
Future<List<String>?> _searchTags(String probe) async {
|
||||
_currentSearchProbe = probe;
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get(
|
||||
'/cgi/co/tags?take=10&probe=$_currentSearchProbe',
|
||||
);
|
||||
|
||||
if (_currentSearchProbe != probe) {
|
||||
return null;
|
||||
}
|
||||
_currentSearchProbe = null;
|
||||
|
||||
return resp.data.map((x) => x['alias']).toList().cast<String>();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_debouncedSearch = _debounce<List<String>?, String>(_searchTags);
|
||||
if (widget.initialTags != null) {
|
||||
_currentTags.addAll(widget.initialTags!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Autocomplete<String>(
|
||||
optionsBuilder: (TextEditingValue textEditingValue) async {
|
||||
final result = await _debouncedSearch(textEditingValue.text);
|
||||
if (result == null) {
|
||||
return _lastAutocompleteResult;
|
||||
}
|
||||
_lastAutocompleteResult = result;
|
||||
return result;
|
||||
},
|
||||
onSelected: (String value) {
|
||||
if (value.isEmpty) return;
|
||||
if (!_currentTags.contains(value)) {
|
||||
setState(() => _currentTags.add(value));
|
||||
}
|
||||
_textEditingController?.clear();
|
||||
widget.onUpdate(_currentTags);
|
||||
},
|
||||
fieldViewBuilder: (context, controller, focusNode, onSubmitted) {
|
||||
_textEditingController = controller;
|
||||
return TextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
label: Text(widget.labelText),
|
||||
border: const UnderlineInputBorder(),
|
||||
prefixIconConstraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.75,
|
||||
),
|
||||
prefixIcon: _currentTags.isNotEmpty
|
||||
? SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: _currentTags.map((String tag) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(20.0),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10.0, vertical: 4.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
InkWell(
|
||||
child: Text(
|
||||
'#$tag',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
InkWell(
|
||||
child: const Icon(
|
||||
Icons.cancel,
|
||||
size: 14.0,
|
||||
color: Color.fromARGB(255, 233, 233, 233),
|
||||
),
|
||||
onTap: () {
|
||||
setState(() => _currentTags.remove(tag));
|
||||
widget.onUpdate(_currentTags);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onChanged: (value) {
|
||||
for (final divider in kTagsDividers) {
|
||||
if (value.endsWith(divider)) {
|
||||
final tagValue = value.substring(0, value.length - 1);
|
||||
if (tagValue.isEmpty) return;
|
||||
if (!_currentTags.contains(tagValue)) {
|
||||
setState(() => _currentTags.add(tagValue));
|
||||
}
|
||||
controller.clear();
|
||||
widget.onUpdate(_currentTags);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
onSubmitted();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
typedef _Debounceable<S, T> = Future<S?> Function(T parameter);
|
||||
|
||||
_Debounceable<S, T> _debounce<S, T>(_Debounceable<S?, T> function) {
|
||||
_DebounceTimer? debounceTimer;
|
||||
|
||||
return (T parameter) async {
|
||||
if (debounceTimer != null && !debounceTimer!.isCompleted) {
|
||||
debounceTimer!.cancel();
|
||||
}
|
||||
debounceTimer = _DebounceTimer();
|
||||
try {
|
||||
await debounceTimer!.future;
|
||||
} catch (error) {
|
||||
if (error is _CancelException) {
|
||||
return null;
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
return function(parameter);
|
||||
};
|
||||
}
|
||||
|
||||
class _DebounceTimer {
|
||||
_DebounceTimer() {
|
||||
_timer = Timer(const Duration(milliseconds: 500), _onComplete);
|
||||
}
|
||||
|
||||
late final Timer _timer;
|
||||
final Completer<void> _completer = Completer<void>();
|
||||
|
||||
void _onComplete() {
|
||||
_completer.complete();
|
||||
}
|
||||
|
||||
Future<void> get future => _completer.future;
|
||||
|
||||
bool get isCompleted => _completer.isCompleted;
|
||||
|
||||
void cancel() {
|
||||
_timer.cancel();
|
||||
_completer.completeError(const _CancelException());
|
||||
}
|
||||
}
|
||||
|
||||
class _CancelException implements Exception {
|
||||
const _CancelException();
|
||||
}
|
Loading…
Reference in New Issue
Block a user