✨ Stickers & packs
This commit is contained in:
parent
e4c6477bba
commit
f6d651a98f
@ -95,6 +95,7 @@
|
||||
"edited": "Edited",
|
||||
"addVideo": "Add video",
|
||||
"addPhoto": "Add photo",
|
||||
"addFile": "Add file",
|
||||
"createDirectMessage": "New direct message",
|
||||
"react": "React",
|
||||
"reactions": {
|
||||
@ -151,5 +152,31 @@
|
||||
"settings": "Settings",
|
||||
"language": "Language",
|
||||
"settingsDisplayLanguage": "Display Language",
|
||||
"languageFollowSystem": "Follow System"
|
||||
"languageFollowSystem": "Follow System",
|
||||
"publisherUnselected": "Unselected",
|
||||
"postsCreatedCount": "Posts",
|
||||
"stickerPacksCreatedCount": "Sticker Packs",
|
||||
"stickersCreatedCount": "Stickers",
|
||||
"upvoteReceived": "Upvotes Recieved",
|
||||
"downvoteReceived": "Downvotes Recieved",
|
||||
"stickerPacks": "Sticker Packs",
|
||||
"createStickerPack": "Create a Sticker Pack",
|
||||
"editStickerPack": "Edit Sticker Pack",
|
||||
"deleteStickerPack": "Delete Sticker Pack",
|
||||
"deleteStickerPackHint": "Are you sure to delete this sticker pack? This action cannot be undone.",
|
||||
"stickerPackPrefix": "Prefix",
|
||||
"stickerPackPrefixHint": "The prefix will be added before each stickers' slug in this pack.",
|
||||
"stickers": "Stickers",
|
||||
"createSticker": "Create a Sticker",
|
||||
"editSticker": "Edit Sticker",
|
||||
"deleteSticker": "Delete Sticker",
|
||||
"deleteStickerHint": "Are you sure to delete this sticker? This action cannot be undone.",
|
||||
"stickerImage": "Image",
|
||||
"stickerSlug": "Slug",
|
||||
"stickerSlugHint": "The slug will be combined with the prefix to form the sticker's unique identifier.",
|
||||
"dataEmpty": "Nothing's here yet.",
|
||||
"pickFile": "Pick a file",
|
||||
"uploading": "Uploading",
|
||||
"uploadingProgress": "Uploading {} of {}",
|
||||
"uploadAll": "Upload All"
|
||||
}
|
||||
|
@ -64,6 +64,20 @@ abstract class SnPublisher with _$SnPublisher {
|
||||
_$SnPublisherFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SnPublisherStats with _$SnPublisherStats {
|
||||
const factory SnPublisherStats({
|
||||
required int postsCreated,
|
||||
required int stickerPacksCreated,
|
||||
required int stickersCreated,
|
||||
required int upvoteReceived,
|
||||
required int downvoteReceived,
|
||||
}) = _SnPublisherStats;
|
||||
|
||||
factory SnPublisherStats.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnPublisherStatsFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ReactInfo with _$ReactInfo {
|
||||
const factory ReactInfo({required String icon, required int attitude}) =
|
||||
|
@ -511,6 +511,151 @@ $SnCloudFileCopyWith<$Res>? get background {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnPublisherStats {
|
||||
|
||||
int get postsCreated; int get stickerPacksCreated; int get stickersCreated; int get upvoteReceived; int get downvoteReceived;
|
||||
/// Create a copy of SnPublisherStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPublisherStatsCopyWith<SnPublisherStats> get copyWith => _$SnPublisherStatsCopyWithImpl<SnPublisherStats>(this as SnPublisherStats, _$identity);
|
||||
|
||||
/// Serializes this SnPublisherStats to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublisherStats&&(identical(other.postsCreated, postsCreated) || other.postsCreated == postsCreated)&&(identical(other.stickerPacksCreated, stickerPacksCreated) || other.stickerPacksCreated == stickerPacksCreated)&&(identical(other.stickersCreated, stickersCreated) || other.stickersCreated == stickersCreated)&&(identical(other.upvoteReceived, upvoteReceived) || other.upvoteReceived == upvoteReceived)&&(identical(other.downvoteReceived, downvoteReceived) || other.downvoteReceived == downvoteReceived));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,postsCreated,stickerPacksCreated,stickersCreated,upvoteReceived,downvoteReceived);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPublisherStats(postsCreated: $postsCreated, stickerPacksCreated: $stickerPacksCreated, stickersCreated: $stickersCreated, upvoteReceived: $upvoteReceived, downvoteReceived: $downvoteReceived)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnPublisherStatsCopyWith<$Res> {
|
||||
factory $SnPublisherStatsCopyWith(SnPublisherStats value, $Res Function(SnPublisherStats) _then) = _$SnPublisherStatsCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
int postsCreated, int stickerPacksCreated, int stickersCreated, int upvoteReceived, int downvoteReceived
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnPublisherStatsCopyWithImpl<$Res>
|
||||
implements $SnPublisherStatsCopyWith<$Res> {
|
||||
_$SnPublisherStatsCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnPublisherStats _self;
|
||||
final $Res Function(SnPublisherStats) _then;
|
||||
|
||||
/// Create a copy of SnPublisherStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? postsCreated = null,Object? stickerPacksCreated = null,Object? stickersCreated = null,Object? upvoteReceived = null,Object? downvoteReceived = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
postsCreated: null == postsCreated ? _self.postsCreated : postsCreated // ignore: cast_nullable_to_non_nullable
|
||||
as int,stickerPacksCreated: null == stickerPacksCreated ? _self.stickerPacksCreated : stickerPacksCreated // ignore: cast_nullable_to_non_nullable
|
||||
as int,stickersCreated: null == stickersCreated ? _self.stickersCreated : stickersCreated // ignore: cast_nullable_to_non_nullable
|
||||
as int,upvoteReceived: null == upvoteReceived ? _self.upvoteReceived : upvoteReceived // ignore: cast_nullable_to_non_nullable
|
||||
as int,downvoteReceived: null == downvoteReceived ? _self.downvoteReceived : downvoteReceived // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnPublisherStats implements SnPublisherStats {
|
||||
const _SnPublisherStats({required this.postsCreated, required this.stickerPacksCreated, required this.stickersCreated, required this.upvoteReceived, required this.downvoteReceived});
|
||||
factory _SnPublisherStats.fromJson(Map<String, dynamic> json) => _$SnPublisherStatsFromJson(json);
|
||||
|
||||
@override final int postsCreated;
|
||||
@override final int stickerPacksCreated;
|
||||
@override final int stickersCreated;
|
||||
@override final int upvoteReceived;
|
||||
@override final int downvoteReceived;
|
||||
|
||||
/// Create a copy of SnPublisherStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnPublisherStatsCopyWith<_SnPublisherStats> get copyWith => __$SnPublisherStatsCopyWithImpl<_SnPublisherStats>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnPublisherStatsToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublisherStats&&(identical(other.postsCreated, postsCreated) || other.postsCreated == postsCreated)&&(identical(other.stickerPacksCreated, stickerPacksCreated) || other.stickerPacksCreated == stickerPacksCreated)&&(identical(other.stickersCreated, stickersCreated) || other.stickersCreated == stickersCreated)&&(identical(other.upvoteReceived, upvoteReceived) || other.upvoteReceived == upvoteReceived)&&(identical(other.downvoteReceived, downvoteReceived) || other.downvoteReceived == downvoteReceived));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,postsCreated,stickerPacksCreated,stickersCreated,upvoteReceived,downvoteReceived);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPublisherStats(postsCreated: $postsCreated, stickerPacksCreated: $stickerPacksCreated, stickersCreated: $stickersCreated, upvoteReceived: $upvoteReceived, downvoteReceived: $downvoteReceived)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnPublisherStatsCopyWith<$Res> implements $SnPublisherStatsCopyWith<$Res> {
|
||||
factory _$SnPublisherStatsCopyWith(_SnPublisherStats value, $Res Function(_SnPublisherStats) _then) = __$SnPublisherStatsCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
int postsCreated, int stickerPacksCreated, int stickersCreated, int upvoteReceived, int downvoteReceived
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnPublisherStatsCopyWithImpl<$Res>
|
||||
implements _$SnPublisherStatsCopyWith<$Res> {
|
||||
__$SnPublisherStatsCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnPublisherStats _self;
|
||||
final $Res Function(_SnPublisherStats) _then;
|
||||
|
||||
/// Create a copy of SnPublisherStats
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? postsCreated = null,Object? stickerPacksCreated = null,Object? stickersCreated = null,Object? upvoteReceived = null,Object? downvoteReceived = null,}) {
|
||||
return _then(_SnPublisherStats(
|
||||
postsCreated: null == postsCreated ? _self.postsCreated : postsCreated // ignore: cast_nullable_to_non_nullable
|
||||
as int,stickerPacksCreated: null == stickerPacksCreated ? _self.stickerPacksCreated : stickerPacksCreated // ignore: cast_nullable_to_non_nullable
|
||||
as int,stickersCreated: null == stickersCreated ? _self.stickersCreated : stickersCreated // ignore: cast_nullable_to_non_nullable
|
||||
as int,upvoteReceived: null == upvoteReceived ? _self.upvoteReceived : upvoteReceived // ignore: cast_nullable_to_non_nullable
|
||||
as int,downvoteReceived: null == downvoteReceived ? _self.downvoteReceived : downvoteReceived // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ReactInfo {
|
||||
|
||||
|
@ -126,3 +126,21 @@ Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
_SnPublisherStats _$SnPublisherStatsFromJson(Map<String, dynamic> json) =>
|
||||
_SnPublisherStats(
|
||||
postsCreated: (json['posts_created'] as num).toInt(),
|
||||
stickerPacksCreated: (json['sticker_packs_created'] as num).toInt(),
|
||||
stickersCreated: (json['stickers_created'] as num).toInt(),
|
||||
upvoteReceived: (json['upvote_received'] as num).toInt(),
|
||||
downvoteReceived: (json['downvote_received'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnPublisherStatsToJson(_SnPublisherStats instance) =>
|
||||
<String, dynamic>{
|
||||
'posts_created': instance.postsCreated,
|
||||
'sticker_packs_created': instance.stickerPacksCreated,
|
||||
'stickers_created': instance.stickersCreated,
|
||||
'upvote_received': instance.upvoteReceived,
|
||||
'downvote_received': instance.downvoteReceived,
|
||||
};
|
||||
|
42
lib/models/sticker.dart
Normal file
42
lib/models/sticker.dart
Normal file
@ -0,0 +1,42 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
|
||||
part 'sticker.freezed.dart';
|
||||
part 'sticker.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class SnSticker with _$SnSticker {
|
||||
const factory SnSticker({
|
||||
required String id,
|
||||
required String slug,
|
||||
required String imageId,
|
||||
required SnCloudFile image,
|
||||
required String packId,
|
||||
required SnStickerPack? pack,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
}) = _SnSticker;
|
||||
|
||||
factory SnSticker.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnStickerFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class SnStickerPack with _$SnStickerPack {
|
||||
const factory SnStickerPack({
|
||||
required String id,
|
||||
required String name,
|
||||
required String description,
|
||||
required String prefix,
|
||||
required int publisherId,
|
||||
required SnPublisher? publisher,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
}) = _SnStickerPack;
|
||||
|
||||
factory SnStickerPack.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnStickerPackFromJson(json);
|
||||
}
|
395
lib/models/sticker.freezed.dart
Normal file
395
lib/models/sticker.freezed.dart
Normal file
@ -0,0 +1,395 @@
|
||||
// dart format width=80
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'sticker.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnSticker {
|
||||
|
||||
String get id; String get slug; String get imageId; SnCloudFile get image; String get packId; SnStickerPack? get pack; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
/// Create a copy of SnSticker
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnStickerCopyWith<SnSticker> get copyWith => _$SnStickerCopyWithImpl<SnSticker>(this as SnSticker, _$identity);
|
||||
|
||||
/// Serializes this SnSticker to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnSticker&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.imageId, imageId) || other.imageId == imageId)&&(identical(other.image, image) || other.image == image)&&(identical(other.packId, packId) || other.packId == packId)&&(identical(other.pack, pack) || other.pack == pack)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,slug,imageId,image,packId,pack,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnSticker(id: $id, slug: $slug, imageId: $imageId, image: $image, packId: $packId, pack: $pack, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnStickerCopyWith<$Res> {
|
||||
factory $SnStickerCopyWith(SnSticker value, $Res Function(SnSticker) _then) = _$SnStickerCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String slug, String imageId, SnCloudFile image, String packId, SnStickerPack? pack, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
$SnCloudFileCopyWith<$Res> get image;$SnStickerPackCopyWith<$Res>? get pack;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnStickerCopyWithImpl<$Res>
|
||||
implements $SnStickerCopyWith<$Res> {
|
||||
_$SnStickerCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnSticker _self;
|
||||
final $Res Function(SnSticker) _then;
|
||||
|
||||
/// Create a copy of SnSticker
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? imageId = null,Object? image = null,Object? packId = null,Object? pack = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
|
||||
as String,imageId: null == imageId ? _self.imageId : imageId // ignore: cast_nullable_to_non_nullable
|
||||
as String,image: null == image ? _self.image : image // ignore: cast_nullable_to_non_nullable
|
||||
as SnCloudFile,packId: null == packId ? _self.packId : packId // ignore: cast_nullable_to_non_nullable
|
||||
as String,pack: freezed == pack ? _self.pack : pack // ignore: cast_nullable_to_non_nullable
|
||||
as SnStickerPack?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of SnSticker
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnCloudFileCopyWith<$Res> get image {
|
||||
|
||||
return $SnCloudFileCopyWith<$Res>(_self.image, (value) {
|
||||
return _then(_self.copyWith(image: value));
|
||||
});
|
||||
}/// Create a copy of SnSticker
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnStickerPackCopyWith<$Res>? get pack {
|
||||
if (_self.pack == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnStickerPackCopyWith<$Res>(_self.pack!, (value) {
|
||||
return _then(_self.copyWith(pack: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnSticker implements SnSticker {
|
||||
const _SnSticker({required this.id, required this.slug, required this.imageId, required this.image, required this.packId, required this.pack, required this.createdAt, required this.updatedAt, required this.deletedAt});
|
||||
factory _SnSticker.fromJson(Map<String, dynamic> json) => _$SnStickerFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final String slug;
|
||||
@override final String imageId;
|
||||
@override final SnCloudFile image;
|
||||
@override final String packId;
|
||||
@override final SnStickerPack? pack;
|
||||
@override final DateTime createdAt;
|
||||
@override final DateTime updatedAt;
|
||||
@override final DateTime? deletedAt;
|
||||
|
||||
/// Create a copy of SnSticker
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnStickerCopyWith<_SnSticker> get copyWith => __$SnStickerCopyWithImpl<_SnSticker>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnStickerToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnSticker&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.imageId, imageId) || other.imageId == imageId)&&(identical(other.image, image) || other.image == image)&&(identical(other.packId, packId) || other.packId == packId)&&(identical(other.pack, pack) || other.pack == pack)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,slug,imageId,image,packId,pack,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnSticker(id: $id, slug: $slug, imageId: $imageId, image: $image, packId: $packId, pack: $pack, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnStickerCopyWith<$Res> implements $SnStickerCopyWith<$Res> {
|
||||
factory _$SnStickerCopyWith(_SnSticker value, $Res Function(_SnSticker) _then) = __$SnStickerCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String slug, String imageId, SnCloudFile image, String packId, SnStickerPack? pack, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@override $SnCloudFileCopyWith<$Res> get image;@override $SnStickerPackCopyWith<$Res>? get pack;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnStickerCopyWithImpl<$Res>
|
||||
implements _$SnStickerCopyWith<$Res> {
|
||||
__$SnStickerCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnSticker _self;
|
||||
final $Res Function(_SnSticker) _then;
|
||||
|
||||
/// Create a copy of SnSticker
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? imageId = null,Object? image = null,Object? packId = null,Object? pack = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnSticker(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
|
||||
as String,imageId: null == imageId ? _self.imageId : imageId // ignore: cast_nullable_to_non_nullable
|
||||
as String,image: null == image ? _self.image : image // ignore: cast_nullable_to_non_nullable
|
||||
as SnCloudFile,packId: null == packId ? _self.packId : packId // ignore: cast_nullable_to_non_nullable
|
||||
as String,pack: freezed == pack ? _self.pack : pack // ignore: cast_nullable_to_non_nullable
|
||||
as SnStickerPack?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnSticker
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnCloudFileCopyWith<$Res> get image {
|
||||
|
||||
return $SnCloudFileCopyWith<$Res>(_self.image, (value) {
|
||||
return _then(_self.copyWith(image: value));
|
||||
});
|
||||
}/// Create a copy of SnSticker
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnStickerPackCopyWith<$Res>? get pack {
|
||||
if (_self.pack == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnStickerPackCopyWith<$Res>(_self.pack!, (value) {
|
||||
return _then(_self.copyWith(pack: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnStickerPack {
|
||||
|
||||
String get id; String get name; String get description; String get prefix; int get publisherId; SnPublisher? get publisher; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
/// Create a copy of SnStickerPack
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnStickerPackCopyWith<SnStickerPack> get copyWith => _$SnStickerPackCopyWithImpl<SnStickerPack>(this as SnStickerPack, _$identity);
|
||||
|
||||
/// Serializes this SnStickerPack to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnStickerPackCopyWith<$Res> {
|
||||
factory $SnStickerPackCopyWith(SnStickerPack value, $Res Function(SnStickerPack) _then) = _$SnStickerPackCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String name, String description, String prefix, int publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
$SnPublisherCopyWith<$Res>? get publisher;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnStickerPackCopyWithImpl<$Res>
|
||||
implements $SnStickerPackCopyWith<$Res> {
|
||||
_$SnStickerPackCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnStickerPack _self;
|
||||
final $Res Function(SnStickerPack) _then;
|
||||
|
||||
/// Create a copy of SnStickerPack
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String,prefix: null == prefix ? _self.prefix : prefix // ignore: cast_nullable_to_non_nullable
|
||||
as String,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
|
||||
as int,publisher: freezed == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable
|
||||
as SnPublisher?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of SnStickerPack
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPublisherCopyWith<$Res>? get publisher {
|
||||
if (_self.publisher == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnPublisherCopyWith<$Res>(_self.publisher!, (value) {
|
||||
return _then(_self.copyWith(publisher: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnStickerPack implements SnStickerPack {
|
||||
const _SnStickerPack({required this.id, required this.name, required this.description, required this.prefix, required this.publisherId, required this.publisher, required this.createdAt, required this.updatedAt, required this.deletedAt});
|
||||
factory _SnStickerPack.fromJson(Map<String, dynamic> json) => _$SnStickerPackFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final String name;
|
||||
@override final String description;
|
||||
@override final String prefix;
|
||||
@override final int publisherId;
|
||||
@override final SnPublisher? publisher;
|
||||
@override final DateTime createdAt;
|
||||
@override final DateTime updatedAt;
|
||||
@override final DateTime? deletedAt;
|
||||
|
||||
/// Create a copy of SnStickerPack
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnStickerPackCopyWith<_SnStickerPack> get copyWith => __$SnStickerPackCopyWithImpl<_SnStickerPack>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnStickerPackToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnStickerPack&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.prefix, prefix) || other.prefix == prefix)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,name,description,prefix,publisherId,publisher,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnStickerPack(id: $id, name: $name, description: $description, prefix: $prefix, publisherId: $publisherId, publisher: $publisher, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnStickerPackCopyWith<$Res> implements $SnStickerPackCopyWith<$Res> {
|
||||
factory _$SnStickerPackCopyWith(_SnStickerPack value, $Res Function(_SnStickerPack) _then) = __$SnStickerPackCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String name, String description, String prefix, int publisherId, SnPublisher? publisher, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@override $SnPublisherCopyWith<$Res>? get publisher;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnStickerPackCopyWithImpl<$Res>
|
||||
implements _$SnStickerPackCopyWith<$Res> {
|
||||
__$SnStickerPackCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnStickerPack _self;
|
||||
final $Res Function(_SnStickerPack) _then;
|
||||
|
||||
/// Create a copy of SnStickerPack
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = null,Object? prefix = null,Object? publisherId = null,Object? publisher = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnStickerPack(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String,prefix: null == prefix ? _self.prefix : prefix // ignore: cast_nullable_to_non_nullable
|
||||
as String,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
|
||||
as int,publisher: freezed == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable
|
||||
as SnPublisher?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnStickerPack
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPublisherCopyWith<$Res>? get publisher {
|
||||
if (_self.publisher == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnPublisherCopyWith<$Res>(_self.publisher!, (value) {
|
||||
return _then(_self.copyWith(publisher: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
70
lib/models/sticker.g.dart
Normal file
70
lib/models/sticker.g.dart
Normal file
@ -0,0 +1,70 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'sticker.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_SnSticker _$SnStickerFromJson(Map<String, dynamic> json) => _SnSticker(
|
||||
id: json['id'] as String,
|
||||
slug: json['slug'] as String,
|
||||
imageId: json['image_id'] as String,
|
||||
image: SnCloudFile.fromJson(json['image'] as Map<String, dynamic>),
|
||||
packId: json['pack_id'] as String,
|
||||
pack:
|
||||
json['pack'] == null
|
||||
? null
|
||||
: SnStickerPack.fromJson(json['pack'] as Map<String, dynamic>),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt:
|
||||
json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnStickerToJson(_SnSticker instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'slug': instance.slug,
|
||||
'image_id': instance.imageId,
|
||||
'image': instance.image.toJson(),
|
||||
'pack_id': instance.packId,
|
||||
'pack': instance.pack?.toJson(),
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
_SnStickerPack _$SnStickerPackFromJson(Map<String, dynamic> json) =>
|
||||
_SnStickerPack(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
prefix: json['prefix'] as String,
|
||||
publisherId: (json['publisher_id'] as num).toInt(),
|
||||
publisher:
|
||||
json['publisher'] == null
|
||||
? null
|
||||
: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt:
|
||||
json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnStickerPackToJson(_SnStickerPack instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'prefix': instance.prefix,
|
||||
'publisher_id': instance.publisherId,
|
||||
'publisher': instance.publisher?.toJson(),
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
};
|
@ -50,16 +50,31 @@ class WebSocketService {
|
||||
Stream<WebSocketPacket> get dataStream => _streamController.stream;
|
||||
Stream<WebSocketState> get statusStream => _statusStreamController.stream;
|
||||
|
||||
Future<void> connect(String url, String atk) async {
|
||||
Future<void> connect(String url, String atk, {Ref? ref}) async {
|
||||
_lastUrl = url;
|
||||
_lastAtk = atk;
|
||||
|
||||
if (ref != null) {
|
||||
final freshAtk = await getFreshAtk(
|
||||
ref.watch(tokenPairProvider),
|
||||
url.replaceFirst('ws', 'http').replaceFirst('/ws', ''),
|
||||
onRefreshed: (atk, rtk) {
|
||||
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
|
||||
ref.invalidate(tokenPairProvider);
|
||||
},
|
||||
);
|
||||
if (freshAtk != null) {
|
||||
atk = freshAtk;
|
||||
_lastAtk = freshAtk;
|
||||
}
|
||||
}
|
||||
|
||||
log('[WebSocket] Trying connecting to $url');
|
||||
try {
|
||||
_channel = IOWebSocketChannel.connect(
|
||||
Uri.parse(url),
|
||||
headers: {'Authorization': 'Bearer $atk'},
|
||||
);
|
||||
// TODO Fix the atk is expired when reconnecting
|
||||
await _channel!.ready;
|
||||
_statusStreamController.sink.add(WebSocketState.connected());
|
||||
_channel!.stream.listen(
|
||||
@ -141,7 +156,11 @@ class WebSocketStateNotifier extends StateNotifier<WebSocketState> {
|
||||
state = const WebSocketState.error('Unauthorized');
|
||||
return;
|
||||
}
|
||||
await service.connect('$baseUrl/ws'.replaceFirst('http', 'ws'), atk);
|
||||
await service.connect(
|
||||
'$baseUrl/ws'.replaceFirst('http', 'ws'),
|
||||
atk,
|
||||
ref: ref,
|
||||
);
|
||||
state = const WebSocketState.connected();
|
||||
service.statusStream.listen((event) {
|
||||
state = event;
|
||||
|
@ -45,5 +45,27 @@ class AppRouter extends RootStackRouter {
|
||||
AutoRoute(page: EditChatRoute.page, path: '/chat/:id/edit'),
|
||||
AutoRoute(page: ChatRoomRoute.page, path: '/chat/:id'),
|
||||
AutoRoute(page: ChatDetailRoute.page, path: '/chat/:id/detail'),
|
||||
AutoRoute(page: CreatorHubRoute.page, path: '/creators'),
|
||||
AutoRoute(page: StickersRoute.page, path: '/creators/:name/stickers'),
|
||||
AutoRoute(
|
||||
page: NewStickerPacksRoute.page,
|
||||
path: '/creators/:name/stickers/new',
|
||||
),
|
||||
AutoRoute(
|
||||
page: EditStickerPacksRoute.page,
|
||||
path: '/creators/:name/stickers/:packId/edit',
|
||||
),
|
||||
AutoRoute(
|
||||
page: StickerPackDetailRoute.page,
|
||||
path: '/creators/:name/stickers/:packId',
|
||||
),
|
||||
AutoRoute(
|
||||
page: NewStickersRoute.page,
|
||||
path: '/creators/:name/stickers/new',
|
||||
),
|
||||
AutoRoute(
|
||||
page: EditStickersRoute.page,
|
||||
path: '/creators/:name/stickers/:id/edit',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -28,7 +28,7 @@ class AccountScreen extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: const Text('Account')),
|
||||
appBar: AppBar(title: const Text('account').tr()),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
@ -102,9 +102,11 @@ class AccountScreen extends HookConsumerWidget {
|
||||
Text('creatorHubDescription').tr(),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
onTap: () {},
|
||||
),
|
||||
onTap: () {
|
||||
context.router.push(CreatorHubRoute());
|
||||
},
|
||||
),
|
||||
).height(140),
|
||||
),
|
||||
Expanded(
|
||||
child: Card(
|
||||
@ -120,7 +122,7 @@ class AccountScreen extends HookConsumerWidget {
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
).height(140),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 8),
|
||||
|
@ -26,6 +26,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
final submitting = useState(false);
|
||||
|
||||
void updateProfilePicture(String position) async {
|
||||
showLoadingModal(context);
|
||||
var result = await ref
|
||||
.read(imagePickerProvider)
|
||||
.pickImage(source: ImageSource.gallery);
|
||||
@ -41,7 +42,10 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
CropAspectRatio(height: 1, width: 1),
|
||||
],
|
||||
);
|
||||
if (result == null) return;
|
||||
if (result == null) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
|
||||
submitting.value = true;
|
||||
@ -78,6 +82,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -245,11 +245,16 @@ class EditChatScreen extends HookConsumerWidget {
|
||||
}, [chat]);
|
||||
|
||||
void setPicture(String position) async {
|
||||
showLoadingModal(context);
|
||||
var result = await ref
|
||||
.read(imagePickerProvider)
|
||||
.pickImage(source: ImageSource.gallery);
|
||||
if (result == null) return;
|
||||
if (result == null) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
|
||||
result = await cropImage(
|
||||
context,
|
||||
image: result,
|
||||
@ -295,6 +300,7 @@ class EditChatScreen extends HookConsumerWidget {
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
252
lib/screens/creators/hub.dart
Normal file
252
lib/screens/creators/hub.dart
Normal file
@ -0,0 +1,252 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/route.gr.dart';
|
||||
import 'package:island/screens/account/me/publishers.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'hub.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<SnPublisherStats?> publisherStats(Ref ref, String? uname) async {
|
||||
if (uname == null) return null;
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get('/publishers/$uname/stats');
|
||||
return SnPublisherStats.fromJson(resp.data);
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class CreatorHubScreen extends HookConsumerWidget {
|
||||
const CreatorHubScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final publishers = ref.watch(publishersManagedProvider);
|
||||
final currentPublisher = useState<SnPublisher?>(
|
||||
publishers.value?.firstOrNull,
|
||||
);
|
||||
|
||||
final publishersMenu = publishers.when(
|
||||
data:
|
||||
(data) =>
|
||||
data
|
||||
.map(
|
||||
(item) => DropdownMenuItem<SnPublisher>(
|
||||
value: item,
|
||||
child: ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: ProfilePictureWidget(
|
||||
radius: 16,
|
||||
fileId: item.pictureId,
|
||||
),
|
||||
title: Text(item.nick),
|
||||
subtitle: Text('@${item.name}'),
|
||||
trailing:
|
||||
currentPublisher.value?.id == item.id
|
||||
? const Icon(Icons.check)
|
||||
: null,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
loading: () => [],
|
||||
error: (_, __) => [],
|
||||
);
|
||||
|
||||
final publisherStats = ref.watch(
|
||||
publisherStatsProvider(currentPublisher.value?.name),
|
||||
);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('creatorHub').tr(),
|
||||
actions: [
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<SnPublisher>(
|
||||
alignment: Alignment.centerRight,
|
||||
value: currentPublisher.value,
|
||||
hint: CircleAvatar(
|
||||
radius: 16,
|
||||
child: Icon(
|
||||
Symbols.unknown_med,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
).center().padding(right: 8),
|
||||
items: [...publishersMenu],
|
||||
onChanged: (value) {
|
||||
currentPublisher.value = value;
|
||||
},
|
||||
selectedItemBuilder: (context) {
|
||||
return [
|
||||
ProfilePictureWidget(
|
||||
radius: 16,
|
||||
fileId: currentPublisher.value?.pictureId,
|
||||
).center().padding(right: 8),
|
||||
];
|
||||
},
|
||||
buttonStyleData: ButtonStyleData(
|
||||
height: 40,
|
||||
padding: const EdgeInsets.only(left: 14, right: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
dropdownStyleData: DropdownStyleData(
|
||||
width: 320,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 64,
|
||||
padding: EdgeInsets.only(left: 14, right: 14),
|
||||
),
|
||||
iconStyleData: IconStyleData(
|
||||
icon: Icon(Icons.arrow_drop_down),
|
||||
iconSize: 19,
|
||||
iconEnabledColor:
|
||||
Theme.of(context).appBarTheme.foregroundColor!,
|
||||
iconDisabledColor:
|
||||
Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
body: publisherStats.when(
|
||||
data:
|
||||
(stats) => SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
if (stats != null)
|
||||
_PublisherStatsWidget(
|
||||
stats: stats,
|
||||
).padding(vertical: 12, horizontal: 12),
|
||||
if (currentPublisher.value != null)
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
title: Text('stickers').tr(),
|
||||
trailing: Icon(Symbols.chevron_right),
|
||||
leading: const Icon(Symbols.sticky_note),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () {
|
||||
context.router.push(
|
||||
StickersRoute(pubName: currentPublisher.value!.name),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, __) => const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PublisherStatsWidget extends StatelessWidget {
|
||||
final SnPublisherStats stats;
|
||||
const _PublisherStatsWidget({required this.stats});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatsCard(
|
||||
context,
|
||||
stats.postsCreated.toString(),
|
||||
'postsCreatedCount',
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildStatsCard(
|
||||
context,
|
||||
stats.stickerPacksCreated.toString(),
|
||||
'stickerPacksCreatedCount',
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildStatsCard(
|
||||
context,
|
||||
stats.stickersCreated.toString(),
|
||||
'stickersCreatedCount',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatsCard(
|
||||
context,
|
||||
stats.upvoteReceived.toString(),
|
||||
'upvoteReceived',
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildStatsCard(
|
||||
context,
|
||||
stats.downvoteReceived.toString(),
|
||||
'downvoteReceived',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatsCard(
|
||||
BuildContext context,
|
||||
String statValue,
|
||||
String statLabel,
|
||||
) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: SizedBox(
|
||||
height: 100,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
statValue,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
statLabel,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
153
lib/screens/creators/hub.g.dart
Normal file
153
lib/screens/creators/hub.g.dart
Normal file
@ -0,0 +1,153 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'hub.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$publisherStatsHash() => r'315705881d116b2aeac93f94f5ee2bc816d9f0f6';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [publisherStats].
|
||||
@ProviderFor(publisherStats)
|
||||
const publisherStatsProvider = PublisherStatsFamily();
|
||||
|
||||
/// See also [publisherStats].
|
||||
class PublisherStatsFamily extends Family<AsyncValue<SnPublisherStats?>> {
|
||||
/// See also [publisherStats].
|
||||
const PublisherStatsFamily();
|
||||
|
||||
/// See also [publisherStats].
|
||||
PublisherStatsProvider call(String? uname) {
|
||||
return PublisherStatsProvider(uname);
|
||||
}
|
||||
|
||||
@override
|
||||
PublisherStatsProvider getProviderOverride(
|
||||
covariant PublisherStatsProvider provider,
|
||||
) {
|
||||
return call(provider.uname);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'publisherStatsProvider';
|
||||
}
|
||||
|
||||
/// See also [publisherStats].
|
||||
class PublisherStatsProvider
|
||||
extends AutoDisposeFutureProvider<SnPublisherStats?> {
|
||||
/// See also [publisherStats].
|
||||
PublisherStatsProvider(String? uname)
|
||||
: this._internal(
|
||||
(ref) => publisherStats(ref as PublisherStatsRef, uname),
|
||||
from: publisherStatsProvider,
|
||||
name: r'publisherStatsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$publisherStatsHash,
|
||||
dependencies: PublisherStatsFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
PublisherStatsFamily._allTransitiveDependencies,
|
||||
uname: uname,
|
||||
);
|
||||
|
||||
PublisherStatsProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.uname,
|
||||
}) : super.internal();
|
||||
|
||||
final String? uname;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<SnPublisherStats?> Function(PublisherStatsRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: PublisherStatsProvider._internal(
|
||||
(ref) => create(ref as PublisherStatsRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
uname: uname,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<SnPublisherStats?> createElement() {
|
||||
return _PublisherStatsProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PublisherStatsProvider && other.uname == uname;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, uname.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin PublisherStatsRef on AutoDisposeFutureProviderRef<SnPublisherStats?> {
|
||||
/// The parameter `uname` of this provider.
|
||||
String? get uname;
|
||||
}
|
||||
|
||||
class _PublisherStatsProviderElement
|
||||
extends AutoDisposeFutureProviderElement<SnPublisherStats?>
|
||||
with PublisherStatsRef {
|
||||
_PublisherStatsProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String? get uname => (origin as PublisherStatsProvider).uname;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
406
lib/screens/creators/stickers/pack_detail.dart
Normal file
406
lib/screens/creators/stickers/pack_detail.dart
Normal file
@ -0,0 +1,406 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/sticker.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/route.gr.dart';
|
||||
import 'package:island/screens/creators/stickers/stickers.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_file_picker.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:super_context_menu/super_context_menu.dart';
|
||||
|
||||
part 'pack_detail.g.dart';
|
||||
part 'pack_detail.freezed.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<SnSticker>> stickerPackContent(Ref ref, String packId) async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get('/stickers/$packId/content');
|
||||
return resp.data
|
||||
.map<SnSticker>((e) => SnSticker.fromJson(e))
|
||||
.cast<SnSticker>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class StickerPackDetailScreen extends HookConsumerWidget {
|
||||
final String id;
|
||||
final String pubName;
|
||||
const StickerPackDetailScreen({
|
||||
super.key,
|
||||
@PathParam('name') required this.pubName,
|
||||
@PathParam('packId') required this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final pack = ref.watch(stickerPackProvider(id));
|
||||
final packContent = ref.watch(stickerPackContentProvider(id));
|
||||
|
||||
Future<void> deleteSticker(SnSticker sticker) async {
|
||||
final confirm = await showConfirmAlert(
|
||||
'deleteStickerHint'.tr(),
|
||||
'deleteSticker'.tr(),
|
||||
);
|
||||
if (!confirm) return;
|
||||
if (!context.mounted) return;
|
||||
|
||||
try {
|
||||
showLoadingModal(context);
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
await apiClient.delete('/stickers/$id/content/${sticker.id}');
|
||||
ref.invalidate(stickerPackContentProvider(id));
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(pack.value?.name ?? 'loading'.tr()),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.add_circle),
|
||||
onPressed: () {
|
||||
AutoRouter.of(context).push(NewStickersRoute(packId: id)).then((
|
||||
value,
|
||||
) {
|
||||
if (value != null) {
|
||||
ref.invalidate(stickerPackContentProvider(id));
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
body: pack.when(
|
||||
data:
|
||||
(pack) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(pack!.description),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
const Icon(Symbols.folder, size: 16),
|
||||
Text(
|
||||
'${packContent.value?.length ?? 0}/24',
|
||||
style: GoogleFonts.robotoMono(),
|
||||
),
|
||||
],
|
||||
).opacity(0.85),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
const Icon(Symbols.sell, size: 16),
|
||||
Text(pack.prefix, style: GoogleFonts.robotoMono()),
|
||||
],
|
||||
).opacity(0.85),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
const Icon(Symbols.tag, size: 16),
|
||||
SelectableText(
|
||||
pack.id,
|
||||
style: GoogleFonts.robotoMono(),
|
||||
),
|
||||
],
|
||||
).opacity(0.85),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 24),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: packContent.when(
|
||||
data:
|
||||
(stickers) => RefreshIndicator(
|
||||
onRefresh:
|
||||
() => ref.refresh(
|
||||
stickerPackContentProvider(id).future,
|
||||
),
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 20,
|
||||
),
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 48,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
),
|
||||
itemCount: stickers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final sticker = stickers[index];
|
||||
return ContextMenuWidget(
|
||||
menuProvider: (_) {
|
||||
return Menu(
|
||||
children: [
|
||||
MenuAction(
|
||||
title: 'edit'.tr(),
|
||||
image: MenuImage.icon(Symbols.edit),
|
||||
callback: () {
|
||||
context.router
|
||||
.push(
|
||||
EditStickersRoute(
|
||||
packId: id,
|
||||
id: sticker.id,
|
||||
),
|
||||
)
|
||||
.then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(
|
||||
stickerPackContentProvider(
|
||||
id,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
title: 'delete'.tr(),
|
||||
image: MenuImage.icon(Symbols.delete),
|
||||
callback: () {
|
||||
deleteSticker(sticker);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: CloudImageWidget(
|
||||
fileId: sticker.imageId,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
error:
|
||||
(err, _) =>
|
||||
Text(
|
||||
'Error: $err',
|
||||
).textAlignment(TextAlign.center).center(),
|
||||
loading: () => const CircularProgressIndicator().center(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
error:
|
||||
(err, _) =>
|
||||
Text('Error: $err').textAlignment(TextAlign.center).center(),
|
||||
loading: () => const CircularProgressIndicator().center(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class StickerWithPackQuery with _$StickerWithPackQuery {
|
||||
const factory StickerWithPackQuery({
|
||||
required String packId,
|
||||
required String id,
|
||||
}) = _StickerWithPackQuery;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<SnSticker?> stickerPackSticker(
|
||||
Ref ref,
|
||||
StickerWithPackQuery? query,
|
||||
) async {
|
||||
if (query == null) return null;
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get(
|
||||
'/stickers/${query.packId}/content/${query.id}',
|
||||
);
|
||||
if (resp.data == null) return null;
|
||||
return SnSticker.fromJson(resp.data);
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class NewStickersScreen extends StatelessWidget {
|
||||
final String packId;
|
||||
const NewStickersScreen({
|
||||
super.key,
|
||||
@PathParam('packId') required this.packId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EditStickersScreen(packId: packId, id: null);
|
||||
}
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class EditStickersScreen extends HookConsumerWidget {
|
||||
final String packId;
|
||||
final String? id;
|
||||
const EditStickersScreen({
|
||||
super.key,
|
||||
@PathParam("packId") required this.packId,
|
||||
@PathParam("id") required this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final sticker = ref.watch(
|
||||
stickerPackStickerProvider(
|
||||
id == null ? null : StickerWithPackQuery(packId: packId, id: id!),
|
||||
),
|
||||
);
|
||||
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>(), []);
|
||||
|
||||
final image = useState<String?>(id == null ? '' : sticker.value?.imageId);
|
||||
final imageController = useTextEditingController(text: image.value);
|
||||
final slugController = useTextEditingController(
|
||||
text: id == null ? '' : sticker.value?.slug,
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
if (sticker.value != null) {
|
||||
image.value = sticker.value!.imageId;
|
||||
imageController.text = sticker.value!.imageId;
|
||||
slugController.text = sticker.value!.slug;
|
||||
}
|
||||
return null;
|
||||
}, [sticker]);
|
||||
|
||||
final submitting = useState(false);
|
||||
|
||||
Future<void> submit() async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
submitting.value = true;
|
||||
try {
|
||||
final resp = await apiClient.request(
|
||||
id == null
|
||||
? '/stickers/$packId/content'
|
||||
: '/stickers/$packId/content/$id',
|
||||
data: {'slug': slugController.text, 'image_id': imageController.text},
|
||||
options: Options(method: id == null ? 'POST' : 'PATCH'),
|
||||
);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context, SnSticker.fromJson(resp.data));
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(id == null ? 'createSticker' : 'editSticker').tr(),
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 96,
|
||||
width: 96,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
child:
|
||||
(image.value?.isEmpty ?? true)
|
||||
? const SizedBox.shrink()
|
||||
: CloudImageWidget(fileId: image.value!),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: imageController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'stickerImage'.tr(),
|
||||
border: const UnderlineInputBorder(),
|
||||
suffix: InkWell(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => CloudFilePicker(),
|
||||
).then((value) {
|
||||
if (value == null) return;
|
||||
image.value = value[0].id;
|
||||
imageController.text = image.value!;
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
child: const Icon(
|
||||
Symbols.cloud_upload,
|
||||
).padding(horizontal: 4),
|
||||
),
|
||||
),
|
||||
readOnly: true,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
TextFormField(
|
||||
controller: slugController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'stickerSlug'.tr(),
|
||||
helperText: 'stickerSlugHint'.tr(),
|
||||
border: const UnderlineInputBorder(),
|
||||
),
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(12),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: submitting.value ? null : submit,
|
||||
icon: const Icon(Symbols.save),
|
||||
label: Text(id == null ? 'create' : 'saveChanges').tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 24),
|
||||
);
|
||||
}
|
||||
}
|
145
lib/screens/creators/stickers/pack_detail.freezed.dart
Normal file
145
lib/screens/creators/stickers/pack_detail.freezed.dart
Normal file
@ -0,0 +1,145 @@
|
||||
// dart format width=80
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'pack_detail.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$StickerWithPackQuery {
|
||||
|
||||
String get packId; String get id;
|
||||
/// Create a copy of StickerWithPackQuery
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$StickerWithPackQueryCopyWith<StickerWithPackQuery> get copyWith => _$StickerWithPackQueryCopyWithImpl<StickerWithPackQuery>(this as StickerWithPackQuery, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is StickerWithPackQuery&&(identical(other.packId, packId) || other.packId == packId)&&(identical(other.id, id) || other.id == id));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,packId,id);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'StickerWithPackQuery(packId: $packId, id: $id)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $StickerWithPackQueryCopyWith<$Res> {
|
||||
factory $StickerWithPackQueryCopyWith(StickerWithPackQuery value, $Res Function(StickerWithPackQuery) _then) = _$StickerWithPackQueryCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String packId, String id
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$StickerWithPackQueryCopyWithImpl<$Res>
|
||||
implements $StickerWithPackQueryCopyWith<$Res> {
|
||||
_$StickerWithPackQueryCopyWithImpl(this._self, this._then);
|
||||
|
||||
final StickerWithPackQuery _self;
|
||||
final $Res Function(StickerWithPackQuery) _then;
|
||||
|
||||
/// Create a copy of StickerWithPackQuery
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? packId = null,Object? id = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
packId: null == packId ? _self.packId : packId // ignore: cast_nullable_to_non_nullable
|
||||
as String,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _StickerWithPackQuery implements StickerWithPackQuery {
|
||||
const _StickerWithPackQuery({required this.packId, required this.id});
|
||||
|
||||
|
||||
@override final String packId;
|
||||
@override final String id;
|
||||
|
||||
/// Create a copy of StickerWithPackQuery
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$StickerWithPackQueryCopyWith<_StickerWithPackQuery> get copyWith => __$StickerWithPackQueryCopyWithImpl<_StickerWithPackQuery>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _StickerWithPackQuery&&(identical(other.packId, packId) || other.packId == packId)&&(identical(other.id, id) || other.id == id));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,packId,id);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'StickerWithPackQuery(packId: $packId, id: $id)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$StickerWithPackQueryCopyWith<$Res> implements $StickerWithPackQueryCopyWith<$Res> {
|
||||
factory _$StickerWithPackQueryCopyWith(_StickerWithPackQuery value, $Res Function(_StickerWithPackQuery) _then) = __$StickerWithPackQueryCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String packId, String id
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$StickerWithPackQueryCopyWithImpl<$Res>
|
||||
implements _$StickerWithPackQueryCopyWith<$Res> {
|
||||
__$StickerWithPackQueryCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _StickerWithPackQuery _self;
|
||||
final $Res Function(_StickerWithPackQuery) _then;
|
||||
|
||||
/// Create a copy of StickerWithPackQuery
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? packId = null,Object? id = null,}) {
|
||||
return _then(_StickerWithPackQuery(
|
||||
packId: null == packId ? _self.packId : packId // ignore: cast_nullable_to_non_nullable
|
||||
as String,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
277
lib/screens/creators/stickers/pack_detail.g.dart
Normal file
277
lib/screens/creators/stickers/pack_detail.g.dart
Normal file
@ -0,0 +1,277 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'pack_detail.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$stickerPackContentHash() =>
|
||||
r'78de848fba1f341f217f8ae4b9eef2d8afa67964';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [stickerPackContent].
|
||||
@ProviderFor(stickerPackContent)
|
||||
const stickerPackContentProvider = StickerPackContentFamily();
|
||||
|
||||
/// See also [stickerPackContent].
|
||||
class StickerPackContentFamily extends Family<AsyncValue<List<SnSticker>>> {
|
||||
/// See also [stickerPackContent].
|
||||
const StickerPackContentFamily();
|
||||
|
||||
/// See also [stickerPackContent].
|
||||
StickerPackContentProvider call(String packId) {
|
||||
return StickerPackContentProvider(packId);
|
||||
}
|
||||
|
||||
@override
|
||||
StickerPackContentProvider getProviderOverride(
|
||||
covariant StickerPackContentProvider provider,
|
||||
) {
|
||||
return call(provider.packId);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'stickerPackContentProvider';
|
||||
}
|
||||
|
||||
/// See also [stickerPackContent].
|
||||
class StickerPackContentProvider
|
||||
extends AutoDisposeFutureProvider<List<SnSticker>> {
|
||||
/// See also [stickerPackContent].
|
||||
StickerPackContentProvider(String packId)
|
||||
: this._internal(
|
||||
(ref) => stickerPackContent(ref as StickerPackContentRef, packId),
|
||||
from: stickerPackContentProvider,
|
||||
name: r'stickerPackContentProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$stickerPackContentHash,
|
||||
dependencies: StickerPackContentFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
StickerPackContentFamily._allTransitiveDependencies,
|
||||
packId: packId,
|
||||
);
|
||||
|
||||
StickerPackContentProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.packId,
|
||||
}) : super.internal();
|
||||
|
||||
final String packId;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<List<SnSticker>> Function(StickerPackContentRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: StickerPackContentProvider._internal(
|
||||
(ref) => create(ref as StickerPackContentRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
packId: packId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<List<SnSticker>> createElement() {
|
||||
return _StickerPackContentProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is StickerPackContentProvider && other.packId == packId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, packId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin StickerPackContentRef on AutoDisposeFutureProviderRef<List<SnSticker>> {
|
||||
/// The parameter `packId` of this provider.
|
||||
String get packId;
|
||||
}
|
||||
|
||||
class _StickerPackContentProviderElement
|
||||
extends AutoDisposeFutureProviderElement<List<SnSticker>>
|
||||
with StickerPackContentRef {
|
||||
_StickerPackContentProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get packId => (origin as StickerPackContentProvider).packId;
|
||||
}
|
||||
|
||||
String _$stickerPackStickerHash() =>
|
||||
r'36f524c047e632236d5597aaaa8678ed86599602';
|
||||
|
||||
/// See also [stickerPackSticker].
|
||||
@ProviderFor(stickerPackSticker)
|
||||
const stickerPackStickerProvider = StickerPackStickerFamily();
|
||||
|
||||
/// See also [stickerPackSticker].
|
||||
class StickerPackStickerFamily extends Family<AsyncValue<SnSticker?>> {
|
||||
/// See also [stickerPackSticker].
|
||||
const StickerPackStickerFamily();
|
||||
|
||||
/// See also [stickerPackSticker].
|
||||
StickerPackStickerProvider call(StickerWithPackQuery? query) {
|
||||
return StickerPackStickerProvider(query);
|
||||
}
|
||||
|
||||
@override
|
||||
StickerPackStickerProvider getProviderOverride(
|
||||
covariant StickerPackStickerProvider provider,
|
||||
) {
|
||||
return call(provider.query);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'stickerPackStickerProvider';
|
||||
}
|
||||
|
||||
/// See also [stickerPackSticker].
|
||||
class StickerPackStickerProvider extends AutoDisposeFutureProvider<SnSticker?> {
|
||||
/// See also [stickerPackSticker].
|
||||
StickerPackStickerProvider(StickerWithPackQuery? query)
|
||||
: this._internal(
|
||||
(ref) => stickerPackSticker(ref as StickerPackStickerRef, query),
|
||||
from: stickerPackStickerProvider,
|
||||
name: r'stickerPackStickerProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$stickerPackStickerHash,
|
||||
dependencies: StickerPackStickerFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
StickerPackStickerFamily._allTransitiveDependencies,
|
||||
query: query,
|
||||
);
|
||||
|
||||
StickerPackStickerProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.query,
|
||||
}) : super.internal();
|
||||
|
||||
final StickerWithPackQuery? query;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<SnSticker?> Function(StickerPackStickerRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: StickerPackStickerProvider._internal(
|
||||
(ref) => create(ref as StickerPackStickerRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
query: query,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<SnSticker?> createElement() {
|
||||
return _StickerPackStickerProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is StickerPackStickerProvider && other.query == query;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, query.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin StickerPackStickerRef on AutoDisposeFutureProviderRef<SnSticker?> {
|
||||
/// The parameter `query` of this provider.
|
||||
StickerWithPackQuery? get query;
|
||||
}
|
||||
|
||||
class _StickerPackStickerProviderElement
|
||||
extends AutoDisposeFutureProviderElement<SnSticker?>
|
||||
with StickerPackStickerRef {
|
||||
_StickerPackStickerProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
StickerWithPackQuery? get query =>
|
||||
(origin as StickerPackStickerProvider).query;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
299
lib/screens/creators/stickers/stickers.dart
Normal file
299
lib/screens/creators/stickers/stickers.dart
Normal file
@ -0,0 +1,299 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/sticker.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/route.gr.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
part 'stickers.g.dart';
|
||||
|
||||
@RoutePage()
|
||||
class StickersScreen extends HookConsumerWidget {
|
||||
final String pubName;
|
||||
const StickersScreen({super.key, @PathParam("name") required this.pubName});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final stickersState = ref.watch(stickerPacksProvider);
|
||||
final stickersNotifier = ref.watch(stickerPacksProvider.notifier);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('stickers').tr(),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
context.router.push(NewStickerPacksRoute(pubName: pubName)).then((
|
||||
value,
|
||||
) {
|
||||
if (value != null) {
|
||||
stickersNotifier.refresh();
|
||||
}
|
||||
});
|
||||
},
|
||||
icon: const Icon(Symbols.add_circle),
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
body: stickersState.when(
|
||||
data:
|
||||
(stickers) => RefreshIndicator(
|
||||
onRefresh: stickersNotifier.refresh,
|
||||
child: InfiniteList(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: stickers.length,
|
||||
hasReachedMax: stickersNotifier.isReachedMax,
|
||||
isLoading: stickersNotifier.isLoading,
|
||||
onFetchData: stickersNotifier.fetchMore,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
title: Text(stickers[index].name),
|
||||
subtitle: Text(stickers[index].description),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
context.router.push(
|
||||
StickerPackDetailRoute(
|
||||
pubName: pubName,
|
||||
id: stickers[index].id,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
loading: () => const CircularProgressIndicator(),
|
||||
error: (error, stack) => Text('Error: $error'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final stickerPacksProvider = StateNotifierProvider<
|
||||
StickerPacksNotifier,
|
||||
AsyncValue<List<SnStickerPack>>
|
||||
>((ref) {
|
||||
return StickerPacksNotifier(ref.watch(apiClientProvider));
|
||||
});
|
||||
|
||||
class StickerPacksNotifier
|
||||
extends StateNotifier<AsyncValue<List<SnStickerPack>>> {
|
||||
final Dio _apiClient;
|
||||
StickerPacksNotifier(this._apiClient) : super(const AsyncValue.loading()) {
|
||||
fetchStickers();
|
||||
}
|
||||
|
||||
int offset = 0;
|
||||
int take = 20;
|
||||
int total = 0;
|
||||
|
||||
bool isLoading = false;
|
||||
bool get isReachedMax =>
|
||||
state.valueOrNull != null && state.valueOrNull!.length >= total;
|
||||
|
||||
Future<void> fetchStickers() async {
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'/stickers?offset=$offset&take=$take',
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
total = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final newStickers =
|
||||
response.data
|
||||
.map((e) => SnStickerPack.fromJson(e))
|
||||
.cast<SnStickerPack>()
|
||||
.toList();
|
||||
|
||||
state = AsyncValue.data(
|
||||
state.valueOrNull != null
|
||||
? [...state.value!, ...newStickers]
|
||||
: newStickers,
|
||||
);
|
||||
offset += take;
|
||||
} else {
|
||||
state = AsyncValue.error('Failed to load stickers', StackTrace.current);
|
||||
}
|
||||
} catch (err, stackTrace) {
|
||||
state = AsyncValue.error(err, stackTrace);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchMore() async {
|
||||
if (state.valueOrNull == null || state.valueOrNull!.length >= total) return;
|
||||
await fetchStickers();
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
offset = 0;
|
||||
state = const AsyncValue.loading();
|
||||
await fetchStickers();
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<SnStickerPack?> stickerPack(Ref ref, String? packId) async {
|
||||
if (packId == null) return null;
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get('/stickers/$packId');
|
||||
return SnStickerPack.fromJson(resp.data);
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class NewStickerPacksScreen extends HookConsumerWidget {
|
||||
final String pubName;
|
||||
const NewStickerPacksScreen({
|
||||
super.key,
|
||||
@PathParam("name") required this.pubName,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return EditStickerPacksScreen(pubName: pubName);
|
||||
}
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class EditStickerPacksScreen extends HookConsumerWidget {
|
||||
final String pubName;
|
||||
final String? packId;
|
||||
const EditStickerPacksScreen({
|
||||
super.key,
|
||||
@PathParam("name") required this.pubName,
|
||||
@PathParam("packId") this.packId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>(), []);
|
||||
final initialPack = ref.watch(stickerPackProvider(packId));
|
||||
|
||||
final nameController = useTextEditingController();
|
||||
final descriptionController = useTextEditingController();
|
||||
final prefixController = useTextEditingController();
|
||||
|
||||
useEffect(() {
|
||||
if (initialPack.value != null) {
|
||||
nameController.text = initialPack.value!.name;
|
||||
descriptionController.text = initialPack.value!.description;
|
||||
prefixController.text = initialPack.value!.prefix;
|
||||
}
|
||||
return null;
|
||||
}, [initialPack]);
|
||||
|
||||
final submitting = useState(false);
|
||||
|
||||
Future<void> submit() async {
|
||||
if (!(formKey.currentState?.validate() ?? false)) return;
|
||||
|
||||
try {
|
||||
submitting.value = true;
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.request(
|
||||
'/stickers',
|
||||
data: {
|
||||
'name': nameController.text,
|
||||
'description': descriptionController.text,
|
||||
'prefix': prefixController.text,
|
||||
},
|
||||
options: Options(
|
||||
method: packId == null ? 'POST' : 'PATCH',
|
||||
headers: {'X-Pub': pubName},
|
||||
),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
context.router.maybePop(SnStickerPack.fromJson(resp.data));
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title:
|
||||
Text(packId == null ? 'createStickerPack' : 'editStickerPack').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'name'.tr(),
|
||||
border: const UnderlineInputBorder(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'fieldCannotBeEmpty'.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'description'.tr(),
|
||||
border: const UnderlineInputBorder(),
|
||||
),
|
||||
minLines: 3,
|
||||
maxLines: null,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
TextFormField(
|
||||
controller: prefixController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'stickerPackPrefix'.tr(),
|
||||
border: const UnderlineInputBorder(),
|
||||
helperText: 'deleteStickerHint'.tr(),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'fieldCannotBeEmpty'.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(12),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: submitting.value ? null : submit,
|
||||
icon: const Icon(Symbols.save),
|
||||
label: Text(packId == null ? 'create'.tr() : 'saveChanges'.tr()),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
);
|
||||
}
|
||||
}
|
151
lib/screens/creators/stickers/stickers.g.dart
Normal file
151
lib/screens/creators/stickers/stickers.g.dart
Normal file
@ -0,0 +1,151 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'stickers.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$stickerPackHash() => r'4f70d26e695ba1d8c7273d12730f77da79361733';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [stickerPack].
|
||||
@ProviderFor(stickerPack)
|
||||
const stickerPackProvider = StickerPackFamily();
|
||||
|
||||
/// See also [stickerPack].
|
||||
class StickerPackFamily extends Family<AsyncValue<SnStickerPack?>> {
|
||||
/// See also [stickerPack].
|
||||
const StickerPackFamily();
|
||||
|
||||
/// See also [stickerPack].
|
||||
StickerPackProvider call(String? packId) {
|
||||
return StickerPackProvider(packId);
|
||||
}
|
||||
|
||||
@override
|
||||
StickerPackProvider getProviderOverride(
|
||||
covariant StickerPackProvider provider,
|
||||
) {
|
||||
return call(provider.packId);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'stickerPackProvider';
|
||||
}
|
||||
|
||||
/// See also [stickerPack].
|
||||
class StickerPackProvider extends AutoDisposeFutureProvider<SnStickerPack?> {
|
||||
/// See also [stickerPack].
|
||||
StickerPackProvider(String? packId)
|
||||
: this._internal(
|
||||
(ref) => stickerPack(ref as StickerPackRef, packId),
|
||||
from: stickerPackProvider,
|
||||
name: r'stickerPackProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$stickerPackHash,
|
||||
dependencies: StickerPackFamily._dependencies,
|
||||
allTransitiveDependencies: StickerPackFamily._allTransitiveDependencies,
|
||||
packId: packId,
|
||||
);
|
||||
|
||||
StickerPackProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.packId,
|
||||
}) : super.internal();
|
||||
|
||||
final String? packId;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<SnStickerPack?> Function(StickerPackRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: StickerPackProvider._internal(
|
||||
(ref) => create(ref as StickerPackRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
packId: packId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<SnStickerPack?> createElement() {
|
||||
return _StickerPackProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is StickerPackProvider && other.packId == packId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, packId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin StickerPackRef on AutoDisposeFutureProviderRef<SnStickerPack?> {
|
||||
/// The parameter `packId` of this provider.
|
||||
String? get packId;
|
||||
}
|
||||
|
||||
class _StickerPackProviderElement
|
||||
extends AutoDisposeFutureProviderElement<SnStickerPack?>
|
||||
with StickerPackRef {
|
||||
_StickerPackProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String? get packId => (origin as StickerPackProvider).packId;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
@ -436,8 +436,17 @@ class AttachmentPreview extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text('Uploading', style: TextStyle(color: Colors.white)),
|
||||
Gap(4),
|
||||
if (progress != null)
|
||||
Text(
|
||||
'${progress!.toStringAsFixed(2)}%',
|
||||
style: TextStyle(color: Colors.white),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
'uploading'.tr(),
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
Gap(6),
|
||||
Center(child: LinearProgressIndicator(value: progress)),
|
||||
],
|
||||
),
|
||||
@ -455,6 +464,7 @@ class AttachmentPreview extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (onDelete != null)
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: const Icon(
|
||||
@ -466,6 +476,7 @@ class AttachmentPreview extends StatelessWidget {
|
||||
onDelete?.call();
|
||||
},
|
||||
),
|
||||
if (onDelete != null && onMove != null)
|
||||
SizedBox(
|
||||
height: 26,
|
||||
child: const VerticalDivider(
|
||||
@ -474,6 +485,7 @@ class AttachmentPreview extends StatelessWidget {
|
||||
thickness: 0.3,
|
||||
),
|
||||
).padding(horizontal: 2),
|
||||
if (onMove != null)
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: const Icon(
|
||||
@ -485,6 +497,7 @@ class AttachmentPreview extends StatelessWidget {
|
||||
onMove?.call(-1);
|
||||
},
|
||||
),
|
||||
if (onMove != null)
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: const Icon(
|
||||
@ -502,6 +515,7 @@ class AttachmentPreview extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (onRequestUpload != null)
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
|
@ -159,6 +159,7 @@ class EditRealmScreen extends HookConsumerWidget {
|
||||
}, [realm]);
|
||||
|
||||
void setPicture(String position) async {
|
||||
showLoadingModal(context);
|
||||
var result = await ref
|
||||
.read(imagePickerProvider)
|
||||
.pickImage(source: ImageSource.gallery);
|
||||
@ -174,7 +175,10 @@ class EditRealmScreen extends HookConsumerWidget {
|
||||
CropAspectRatio(height: 1, width: 1),
|
||||
],
|
||||
);
|
||||
if (result == null) return;
|
||||
if (result == null) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
|
||||
submitting.value = true;
|
||||
@ -209,6 +213,7 @@ class EditRealmScreen extends HookConsumerWidget {
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,10 @@ import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_platform_alert/flutter_platform_alert.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
String _parseRemoteError(DioException err) {
|
||||
log('${err.requestOptions.method} ${err.requestOptions.uri} ${err.message}');
|
||||
@ -52,3 +55,83 @@ Future<bool> showConfirmAlert(String message, String title) async {
|
||||
);
|
||||
return result == AlertButton.okButton;
|
||||
}
|
||||
|
||||
OverlayEntry? _loadingOverlay;
|
||||
GlobalKey<_FadeOverlayState> _loadingOverlayKey = GlobalKey();
|
||||
|
||||
class _FadeOverlay extends StatefulWidget {
|
||||
const _FadeOverlay({super.key, required this.child});
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<_FadeOverlay> createState() => _FadeOverlayState();
|
||||
}
|
||||
|
||||
class _FadeOverlayState extends State<_FadeOverlay> {
|
||||
bool _visible = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() => _visible = true);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedOpacity(
|
||||
opacity: _visible ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showLoadingModal(BuildContext context) {
|
||||
if (_loadingOverlay != null) return;
|
||||
|
||||
_loadingOverlay = OverlayEntry(
|
||||
builder:
|
||||
(context) => _FadeOverlay(
|
||||
key: _loadingOverlayKey,
|
||||
child: Material(
|
||||
color: Colors.black54,
|
||||
child: Center(
|
||||
child: Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
elevation: 4,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(year2023: true),
|
||||
const Gap(24),
|
||||
Text('loading'.tr()),
|
||||
],
|
||||
).padding(all: 32),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_loadingOverlay!);
|
||||
}
|
||||
|
||||
void hideLoadingModal(BuildContext context) async {
|
||||
if (_loadingOverlay == null) return;
|
||||
|
||||
final entry = _loadingOverlay!;
|
||||
_loadingOverlay = null;
|
||||
|
||||
final state = entry.mounted ? _loadingOverlayKey.currentState : null;
|
||||
|
||||
if (state != null) {
|
||||
// ignore: invalid_use_of_protected_member
|
||||
state.setState(() => state._visible = false);
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
}
|
||||
|
||||
entry.remove();
|
||||
}
|
||||
|
312
lib/widgets/content/cloud_file_picker.dart
Normal file
312
lib/widgets/content/cloud_file_picker.dart
Normal file
@ -0,0 +1,312 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/posts/compose.dart';
|
||||
import 'package:island/services/file.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class CloudFilePicker extends HookConsumerWidget {
|
||||
final bool allowMultiple;
|
||||
const CloudFilePicker({super.key, this.allowMultiple = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final files = useState<List<UniversalFile>>([]);
|
||||
|
||||
final uploadPosition = useState<int?>(null);
|
||||
final uploadProgress = useState<double?>(null);
|
||||
|
||||
final uploadOverallProgress = useMemoized<double?>(() {
|
||||
if (uploadPosition.value == null || uploadProgress.value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate completed files (100% each) + current file progress
|
||||
final completedProgress = uploadPosition.value! * 100.0;
|
||||
final currentProgress = uploadProgress.value!;
|
||||
|
||||
// Calculate overall progress as percentage
|
||||
return (completedProgress + currentProgress) /
|
||||
(files.value.length * 100.0);
|
||||
}, [uploadPosition.value, uploadProgress.value, files.value.length]);
|
||||
|
||||
Future<void> startUpload() async {
|
||||
if (files.value.isEmpty) return;
|
||||
|
||||
final baseUrl = ref.read(serverUrlProvider);
|
||||
final atk = await getFreshAtk(
|
||||
ref.watch(tokenPairProvider),
|
||||
baseUrl,
|
||||
onRefreshed: (atk, rtk) {
|
||||
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
|
||||
ref.invalidate(tokenPairProvider);
|
||||
},
|
||||
);
|
||||
if (atk == null) throw Exception("Unauthorized");
|
||||
|
||||
List<SnCloudFile> result = List.empty(growable: true);
|
||||
|
||||
uploadProgress.value = 0;
|
||||
uploadPosition.value = 0;
|
||||
try {
|
||||
for (var idx = 0; idx < files.value.length; idx++) {
|
||||
uploadPosition.value = idx;
|
||||
final file = files.value[idx];
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
fileData: file.data,
|
||||
atk: atk,
|
||||
baseUrl: baseUrl,
|
||||
filename: file.data.name ?? 'Post media',
|
||||
mimetype:
|
||||
file.data.mimeType ??
|
||||
switch (file.type) {
|
||||
UniversalFileType.image => 'image/unknown',
|
||||
UniversalFileType.video => 'video/unknown',
|
||||
UniversalFileType.audio => 'audio/unknown',
|
||||
UniversalFileType.file => 'application/octet-stream',
|
||||
},
|
||||
onProgress: (progress, _) {
|
||||
uploadProgress.value = progress;
|
||||
},
|
||||
).future;
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
}
|
||||
result.add(cloudFile);
|
||||
}
|
||||
|
||||
if (context.mounted) Navigator.pop(context, result);
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
}
|
||||
}
|
||||
|
||||
void pickFile() async {
|
||||
showLoadingModal(context);
|
||||
final result = await FilePickerIO().pickFiles(
|
||||
allowMultiple: allowMultiple,
|
||||
);
|
||||
if (result == null) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
|
||||
final newFiles =
|
||||
result.files
|
||||
.map((e) => UniversalFile(data: e, type: UniversalFileType.file))
|
||||
.toList();
|
||||
|
||||
if (!allowMultiple) {
|
||||
files.value = newFiles;
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
startUpload();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
files.value = [...files.value, ...newFiles];
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
|
||||
void pickImage() async {
|
||||
showLoadingModal(context);
|
||||
final result =
|
||||
allowMultiple
|
||||
? await ref.read(imagePickerProvider).pickMultiImage()
|
||||
: [
|
||||
await ref
|
||||
.read(imagePickerProvider)
|
||||
.pickImage(source: ImageSource.gallery),
|
||||
];
|
||||
if (result.isEmpty) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
|
||||
final newFiles =
|
||||
result
|
||||
.map((e) => UniversalFile(data: e, type: UniversalFileType.image))
|
||||
.toList();
|
||||
|
||||
if (!allowMultiple) {
|
||||
files.value = newFiles;
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
startUpload();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
files.value = [...files.value, ...newFiles];
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
|
||||
void pickVideo() async {
|
||||
showLoadingModal(context);
|
||||
final result = await ref
|
||||
.read(imagePickerProvider)
|
||||
.pickVideo(source: ImageSource.gallery);
|
||||
if (result == null) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
|
||||
final newFile = UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.video,
|
||||
);
|
||||
|
||||
if (!allowMultiple) {
|
||||
files.value = [newFile];
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
startUpload();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
files.value = [...files.value, newFile];
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.5,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'pickFile'.tr(),
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: 16,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (uploadOverallProgress != null)
|
||||
Column(
|
||||
spacing: 6,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text('uploadingProgress')
|
||||
.tr(
|
||||
args: [
|
||||
((uploadPosition.value ?? 0) + 1).toString(),
|
||||
files.value.length.toString(),
|
||||
],
|
||||
)
|
||||
.opacity(0.85),
|
||||
LinearProgressIndicator(
|
||||
value: uploadOverallProgress,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (files.value.isNotEmpty)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: startUpload,
|
||||
icon: const Icon(Symbols.play_arrow),
|
||||
label: Text('uploadAll'.tr()),
|
||||
),
|
||||
),
|
||||
if (files.value.isNotEmpty)
|
||||
SizedBox(
|
||||
height: 280,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: files.value.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return AttachmentPreview(
|
||||
onDelete:
|
||||
uploadOverallProgress != null
|
||||
? null
|
||||
: () {
|
||||
files.value = [
|
||||
...files.value.where(
|
||||
(e) => e != files.value[idx],
|
||||
),
|
||||
];
|
||||
},
|
||||
item: files.value[idx],
|
||||
progress: null,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
),
|
||||
),
|
||||
Card(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
leading: const Icon(Symbols.photo),
|
||||
title: Text('addPhoto'.tr()),
|
||||
onTap: () => pickImage(),
|
||||
),
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
leading: const Icon(Symbols.video_call),
|
||||
title: Text('addVideo'.tr()),
|
||||
onTap: () => pickVideo(),
|
||||
),
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
leading: const Icon(Symbols.draft),
|
||||
title: Text('addFile'.tr()),
|
||||
onTap: () => pickFile(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(all: 24),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -73,7 +73,7 @@ dependencies:
|
||||
git: https://github.com/LittleSheep2Code/tus_client.git
|
||||
cross_file: ^0.3.4+2
|
||||
image_picker: ^1.1.2
|
||||
file_picker: ^10.1.2
|
||||
file_picker: ^10.1.7
|
||||
riverpod_annotation: ^2.6.1
|
||||
image_picker_platform_interface: ^2.10.1
|
||||
image_picker_android: ^0.8.12+23
|
||||
|
Loading…
x
Reference in New Issue
Block a user