✨ Post tags
This commit is contained in:
parent
312d68286e
commit
420588860a
@ -103,6 +103,7 @@
|
|||||||
"fieldPostContent": "What happened?!",
|
"fieldPostContent": "What happened?!",
|
||||||
"fieldPostTitle": "Title",
|
"fieldPostTitle": "Title",
|
||||||
"fieldPostDescription": "Description",
|
"fieldPostDescription": "Description",
|
||||||
|
"fieldPostTags": "Tags",
|
||||||
"postPublish": "Publish",
|
"postPublish": "Publish",
|
||||||
"postPosted": "Post has been posted.",
|
"postPosted": "Post has been posted.",
|
||||||
"postPublishedAt": "Published At",
|
"postPublishedAt": "Published At",
|
||||||
|
@ -103,6 +103,7 @@
|
|||||||
"fieldPostContent": "发生什么事了?!",
|
"fieldPostContent": "发生什么事了?!",
|
||||||
"fieldPostTitle": "标题",
|
"fieldPostTitle": "标题",
|
||||||
"fieldPostDescription": "描述",
|
"fieldPostDescription": "描述",
|
||||||
|
"fieldPostTags": "标签",
|
||||||
"postPublish": "发布",
|
"postPublish": "发布",
|
||||||
"postPublishedAt": "发布于",
|
"postPublishedAt": "发布于",
|
||||||
"postPublishedUntil": "取消发布于",
|
"postPublishedUntil": "取消发布于",
|
||||||
|
@ -107,7 +107,7 @@ PODS:
|
|||||||
- flutter_udid (0.0.1):
|
- flutter_udid (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain
|
- SAMKeychain
|
||||||
- flutter_webrtc (0.11.8):
|
- flutter_webrtc (0.12.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
- WebRTC-SDK (= 125.6422.05)
|
- WebRTC-SDK (= 125.6422.05)
|
||||||
- GoogleAppMeasurement (11.4.0):
|
- GoogleAppMeasurement (11.4.0):
|
||||||
@ -341,7 +341,7 @@ SPEC CHECKSUMS:
|
|||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a
|
flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a
|
||||||
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
||||||
flutter_webrtc: 4f730f3d58a28b0afdea039c8bf4a0f616a6b20c
|
flutter_webrtc: 043d1b47e9795158dd97dc84f1c152cd0e98b93b
|
||||||
GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e
|
GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||||
|
@ -172,6 +172,7 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
SnPublisher? publisher;
|
SnPublisher? publisher;
|
||||||
SnPost? editingPost, repostingPost, replyingPost;
|
SnPost? editingPost, repostingPost, replyingPost;
|
||||||
|
|
||||||
|
List<String> tags = List.empty();
|
||||||
List<PostWriteMedia> attachments = List.empty(growable: true);
|
List<PostWriteMedia> attachments = List.empty(growable: true);
|
||||||
DateTime? publishedAt, publishedUntil;
|
DateTime? publishedAt, publishedUntil;
|
||||||
|
|
||||||
@ -195,6 +196,7 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
contentController.text = post.body['content'] ?? '';
|
contentController.text = post.body['content'] ?? '';
|
||||||
publishedAt = post.publishedAt;
|
publishedAt = post.publishedAt;
|
||||||
publishedUntil = post.publishedUntil;
|
publishedUntil = post.publishedUntil;
|
||||||
|
tags = List.from(post.tags.map((ele) => ele.alias));
|
||||||
attachments.addAll(
|
attachments.addAll(
|
||||||
post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? [],
|
post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? [],
|
||||||
);
|
);
|
||||||
@ -290,6 +292,7 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
.where((e) => e.attachment != null)
|
.where((e) => e.attachment != null)
|
||||||
.map((e) => e.attachment!.rid)
|
.map((e) => e.attachment!.rid)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
'tags': tags.map((ele) => {'alias': ele}).toList(),
|
||||||
if (publishedAt != null)
|
if (publishedAt != null)
|
||||||
'published_at': publishedAt!.toUtc().toIso8601String(),
|
'published_at': publishedAt!.toUtc().toIso8601String(),
|
||||||
if (publishedUntil != null)
|
if (publishedUntil != null)
|
||||||
@ -351,6 +354,11 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setTags(List<String> value) {
|
||||||
|
tags = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void setIsBusy(bool value) {
|
void setIsBusy(bool value) {
|
||||||
isBusy = value;
|
isBusy = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
@ -112,7 +112,7 @@ class SnPostContentProvider {
|
|||||||
Future<SnPost> getPost(dynamic id) async {
|
Future<SnPost> getPost(dynamic id) async {
|
||||||
final resp = await _sn.client.get('/cgi/co/posts/$id');
|
final resp = await _sn.client.get('/cgi/co/posts/$id');
|
||||||
final out = _preloadRelatedDataSingle(
|
final out = _preloadRelatedDataSingle(
|
||||||
SnPost.fromJson(resp.data['data']),
|
SnPost.fromJson(resp.data),
|
||||||
);
|
);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
@ -141,7 +141,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
),
|
),
|
||||||
const TextSpan(text: '\n'),
|
const TextSpan(text: '\n'),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: PostWriteController.kTitleMap[widget.mode]!,
|
text: PostWriteController.kTitleMap[widget.mode]!.tr(),
|
||||||
style: Theme.of(context)
|
style: Theme.of(context)
|
||||||
.textTheme
|
.textTheme
|
||||||
.bodySmall!
|
.bodySmall!
|
||||||
|
@ -18,7 +18,7 @@ class SnPost with _$SnPost {
|
|||||||
required String language,
|
required String language,
|
||||||
required String? alias,
|
required String? alias,
|
||||||
required String? aliasPrefix,
|
required String? aliasPrefix,
|
||||||
@Default([]) List<dynamic> tags,
|
@Default([]) List<SnPostTag> tags,
|
||||||
@Default([]) List<dynamic> categories,
|
@Default([]) List<dynamic> categories,
|
||||||
required List<SnPost>? replies,
|
required List<SnPost>? replies,
|
||||||
required int? replyId,
|
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
|
@freezed
|
||||||
class SnPostPreload with _$SnPostPreload {
|
class SnPostPreload with _$SnPostPreload {
|
||||||
const factory SnPostPreload({
|
const factory SnPostPreload({
|
||||||
|
@ -29,7 +29,7 @@ mixin _$SnPost {
|
|||||||
String get language => throw _privateConstructorUsedError;
|
String get language => throw _privateConstructorUsedError;
|
||||||
String? get alias => throw _privateConstructorUsedError;
|
String? get alias => throw _privateConstructorUsedError;
|
||||||
String? get aliasPrefix => 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<dynamic> get categories => throw _privateConstructorUsedError;
|
||||||
List<SnPost>? get replies => throw _privateConstructorUsedError;
|
List<SnPost>? get replies => throw _privateConstructorUsedError;
|
||||||
int? get replyId => throw _privateConstructorUsedError;
|
int? get replyId => throw _privateConstructorUsedError;
|
||||||
@ -76,7 +76,7 @@ abstract class $SnPostCopyWith<$Res> {
|
|||||||
String language,
|
String language,
|
||||||
String? alias,
|
String? alias,
|
||||||
String? aliasPrefix,
|
String? aliasPrefix,
|
||||||
List<dynamic> tags,
|
List<SnPostTag> tags,
|
||||||
List<dynamic> categories,
|
List<dynamic> categories,
|
||||||
List<SnPost>? replies,
|
List<SnPost>? replies,
|
||||||
int? replyId,
|
int? replyId,
|
||||||
@ -193,7 +193,7 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
|||||||
tags: null == tags
|
tags: null == tags
|
||||||
? _value.tags
|
? _value.tags
|
||||||
: tags // ignore: cast_nullable_to_non_nullable
|
: tags // ignore: cast_nullable_to_non_nullable
|
||||||
as List<dynamic>,
|
as List<SnPostTag>,
|
||||||
categories: null == categories
|
categories: null == categories
|
||||||
? _value.categories
|
? _value.categories
|
||||||
: categories // ignore: cast_nullable_to_non_nullable
|
: categories // ignore: cast_nullable_to_non_nullable
|
||||||
@ -361,7 +361,7 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
|
|||||||
String language,
|
String language,
|
||||||
String? alias,
|
String? alias,
|
||||||
String? aliasPrefix,
|
String? aliasPrefix,
|
||||||
List<dynamic> tags,
|
List<SnPostTag> tags,
|
||||||
List<dynamic> categories,
|
List<dynamic> categories,
|
||||||
List<SnPost>? replies,
|
List<SnPost>? replies,
|
||||||
int? replyId,
|
int? replyId,
|
||||||
@ -481,7 +481,7 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
|||||||
tags: null == tags
|
tags: null == tags
|
||||||
? _value._tags
|
? _value._tags
|
||||||
: tags // ignore: cast_nullable_to_non_nullable
|
: tags // ignore: cast_nullable_to_non_nullable
|
||||||
as List<dynamic>,
|
as List<SnPostTag>,
|
||||||
categories: null == categories
|
categories: null == categories
|
||||||
? _value._categories
|
? _value._categories
|
||||||
: categories // ignore: cast_nullable_to_non_nullable
|
: categories // ignore: cast_nullable_to_non_nullable
|
||||||
@ -583,7 +583,7 @@ class _$SnPostImpl extends _SnPost {
|
|||||||
required this.language,
|
required this.language,
|
||||||
required this.alias,
|
required this.alias,
|
||||||
required this.aliasPrefix,
|
required this.aliasPrefix,
|
||||||
final List<dynamic> tags = const [],
|
final List<SnPostTag> tags = const [],
|
||||||
final List<dynamic> categories = const [],
|
final List<dynamic> categories = const [],
|
||||||
required final List<SnPost>? replies,
|
required final List<SnPost>? replies,
|
||||||
required this.replyId,
|
required this.replyId,
|
||||||
@ -640,10 +640,10 @@ class _$SnPostImpl extends _SnPost {
|
|||||||
final String? alias;
|
final String? alias;
|
||||||
@override
|
@override
|
||||||
final String? aliasPrefix;
|
final String? aliasPrefix;
|
||||||
final List<dynamic> _tags;
|
final List<SnPostTag> _tags;
|
||||||
@override
|
@override
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
List<dynamic> get tags {
|
List<SnPostTag> get tags {
|
||||||
if (_tags is EqualUnmodifiableListView) return _tags;
|
if (_tags is EqualUnmodifiableListView) return _tags;
|
||||||
// ignore: implicit_dynamic_type
|
// ignore: implicit_dynamic_type
|
||||||
return EqualUnmodifiableListView(_tags);
|
return EqualUnmodifiableListView(_tags);
|
||||||
@ -852,7 +852,7 @@ abstract class _SnPost extends SnPost {
|
|||||||
required final String language,
|
required final String language,
|
||||||
required final String? alias,
|
required final String? alias,
|
||||||
required final String? aliasPrefix,
|
required final String? aliasPrefix,
|
||||||
final List<dynamic> tags,
|
final List<SnPostTag> tags,
|
||||||
final List<dynamic> categories,
|
final List<dynamic> categories,
|
||||||
required final List<SnPost>? replies,
|
required final List<SnPost>? replies,
|
||||||
required final int? replyId,
|
required final int? replyId,
|
||||||
@ -897,7 +897,7 @@ abstract class _SnPost extends SnPost {
|
|||||||
@override
|
@override
|
||||||
String? get aliasPrefix;
|
String? get aliasPrefix;
|
||||||
@override
|
@override
|
||||||
List<dynamic> get tags;
|
List<SnPostTag> get tags;
|
||||||
@override
|
@override
|
||||||
List<dynamic> get categories;
|
List<dynamic> get categories;
|
||||||
@override
|
@override
|
||||||
@ -949,6 +949,310 @@ abstract class _SnPost extends SnPost {
|
|||||||
throw _privateConstructorUsedError;
|
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) {
|
SnPostPreload _$SnPostPreloadFromJson(Map<String, dynamic> json) {
|
||||||
return _SnPostPreload.fromJson(json);
|
return _SnPostPreload.fromJson(json);
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,10 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
|
|||||||
language: json['language'] as String,
|
language: json['language'] as String,
|
||||||
alias: json['alias'] as String?,
|
alias: json['alias'] as String?,
|
||||||
aliasPrefix: json['alias_prefix'] 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 [],
|
categories: json['categories'] as List<dynamic>? ?? const [],
|
||||||
replies: (json['replies'] as List<dynamic>?)
|
replies: (json['replies'] as List<dynamic>?)
|
||||||
?.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
|
?.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
|
||||||
@ -76,7 +79,7 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
|
|||||||
'language': instance.language,
|
'language': instance.language,
|
||||||
'alias': instance.alias,
|
'alias': instance.alias,
|
||||||
'alias_prefix': instance.aliasPrefix,
|
'alias_prefix': instance.aliasPrefix,
|
||||||
'tags': instance.tags,
|
'tags': instance.tags.map((e) => e.toJson()).toList(),
|
||||||
'categories': instance.categories,
|
'categories': instance.categories,
|
||||||
'replies': instance.replies?.map((e) => e.toJson()).toList(),
|
'replies': instance.replies?.map((e) => e.toJson()).toList(),
|
||||||
'reply_id': instance.replyId,
|
'reply_id': instance.replyId,
|
||||||
@ -100,6 +103,30 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
|
|||||||
'preload': instance.preload?.toJson(),
|
'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 _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) =>
|
||||||
_$SnPostPreloadImpl(
|
_$SnPostPreloadImpl(
|
||||||
thumbnail: json['thumbnail'] == null
|
thumbnail: json['thumbnail'] == null
|
||||||
|
@ -61,6 +61,8 @@ class PostItem extends StatelessWidget {
|
|||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 4,
|
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 {
|
class _PostTruncatedHint extends StatelessWidget {
|
||||||
final SnPost data;
|
final SnPost data;
|
||||||
const _PostTruncatedHint({super.key, required this.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:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/controllers/post_write_controller.dart';
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
|
import 'package:surface/widgets/post/post_tags_field.dart';
|
||||||
|
|
||||||
class PostMetaEditor extends StatelessWidget {
|
class PostMetaEditor extends StatelessWidget {
|
||||||
final PostWriteController controller;
|
final PostWriteController controller;
|
||||||
@ -69,6 +70,14 @@ class PostMetaEditor extends StatelessWidget {
|
|||||||
onTapOutside: (_) =>
|
onTapOutside: (_) =>
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
).padding(horizontal: 24),
|
).padding(horizontal: 24),
|
||||||
|
const Gap(4),
|
||||||
|
PostTagsField(
|
||||||
|
initialTags: controller.tags,
|
||||||
|
labelText: 'fieldPostTags'.tr(),
|
||||||
|
onUpdate: (value) {
|
||||||
|
controller.setTags(value);
|
||||||
|
},
|
||||||
|
).padding(horizontal: 24),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Symbols.event_available),
|
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