From f6d651a98ff5df874533370c53f12eb1261c27ec Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 11 May 2025 22:05:54 +0800 Subject: [PATCH] :sparkles: Stickers & packs --- assets/i18n/en-US.json | 29 +- lib/models/post.dart | 14 + lib/models/post.freezed.dart | 145 +++++ lib/models/post.g.dart | 18 + lib/models/sticker.dart | 42 ++ lib/models/sticker.freezed.dart | 395 ++++++++++++ lib/models/sticker.g.dart | 70 ++ lib/pods/websocket.dart | 25 +- lib/route.dart | 22 + lib/route.gr.dart | 610 +++++++++++++----- lib/screens/account.dart | 10 +- lib/screens/account/me/update.dart | 7 +- lib/screens/chat/chat.dart | 8 +- lib/screens/creators/hub.dart | 252 ++++++++ lib/screens/creators/hub.g.dart | 153 +++++ .../creators/stickers/pack_detail.dart | 406 ++++++++++++ .../stickers/pack_detail.freezed.dart | 145 +++++ .../creators/stickers/pack_detail.g.dart | 277 ++++++++ lib/screens/creators/stickers/stickers.dart | 299 +++++++++ lib/screens/creators/stickers/stickers.g.dart | 151 +++++ lib/screens/posts/compose.dart | 184 +++--- lib/screens/realm/realms.dart | 7 +- lib/widgets/alert.dart | 83 +++ lib/widgets/content/cloud_file_picker.dart | 312 +++++++++ pubspec.yaml | 2 +- 25 files changed, 3424 insertions(+), 242 deletions(-) create mode 100644 lib/models/sticker.dart create mode 100644 lib/models/sticker.freezed.dart create mode 100644 lib/models/sticker.g.dart create mode 100644 lib/screens/creators/hub.dart create mode 100644 lib/screens/creators/hub.g.dart create mode 100644 lib/screens/creators/stickers/pack_detail.dart create mode 100644 lib/screens/creators/stickers/pack_detail.freezed.dart create mode 100644 lib/screens/creators/stickers/pack_detail.g.dart create mode 100644 lib/screens/creators/stickers/stickers.dart create mode 100644 lib/screens/creators/stickers/stickers.g.dart create mode 100644 lib/widgets/content/cloud_file_picker.dart diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 6f7fc44..c4b03bd 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -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" } diff --git a/lib/models/post.dart b/lib/models/post.dart index d8d1d47..ae7fae1 100644 --- a/lib/models/post.dart +++ b/lib/models/post.dart @@ -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 json) => + _$SnPublisherStatsFromJson(json); +} + @freezed abstract class ReactInfo with _$ReactInfo { const factory ReactInfo({required String icon, required int attitude}) = diff --git a/lib/models/post.freezed.dart b/lib/models/post.freezed.dart index ae720fb..5b59d10 100644 --- a/lib/models/post.freezed.dart +++ b/lib/models/post.freezed.dart @@ -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 get copyWith => _$SnPublisherStatsCopyWithImpl(this as SnPublisherStats, _$identity); + + /// Serializes this SnPublisherStats to a JSON map. + Map 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 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 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 { diff --git a/lib/models/post.g.dart b/lib/models/post.g.dart index 490a3a6..8df7188 100644 --- a/lib/models/post.g.dart +++ b/lib/models/post.g.dart @@ -126,3 +126,21 @@ Map _$SnPublisherToJson(_SnPublisher instance) => 'updated_at': instance.updatedAt.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(), }; + +_SnPublisherStats _$SnPublisherStatsFromJson(Map 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 _$SnPublisherStatsToJson(_SnPublisherStats instance) => + { + 'posts_created': instance.postsCreated, + 'sticker_packs_created': instance.stickerPacksCreated, + 'stickers_created': instance.stickersCreated, + 'upvote_received': instance.upvoteReceived, + 'downvote_received': instance.downvoteReceived, + }; diff --git a/lib/models/sticker.dart b/lib/models/sticker.dart new file mode 100644 index 0000000..3246c04 --- /dev/null +++ b/lib/models/sticker.dart @@ -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 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 json) => + _$SnStickerPackFromJson(json); +} diff --git a/lib/models/sticker.freezed.dart b/lib/models/sticker.freezed.dart new file mode 100644 index 0000000..76121ae --- /dev/null +++ b/lib/models/sticker.freezed.dart @@ -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 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 get copyWith => _$SnStickerCopyWithImpl(this as SnSticker, _$identity); + + /// Serializes this SnSticker to a JSON map. + Map 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 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 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 get copyWith => _$SnStickerPackCopyWithImpl(this as SnStickerPack, _$identity); + + /// Serializes this SnStickerPack to a JSON map. + Map 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 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 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 diff --git a/lib/models/sticker.g.dart b/lib/models/sticker.g.dart new file mode 100644 index 0000000..ea08eda --- /dev/null +++ b/lib/models/sticker.g.dart @@ -0,0 +1,70 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sticker.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_SnSticker _$SnStickerFromJson(Map 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), + packId: json['pack_id'] as String, + pack: + json['pack'] == null + ? null + : SnStickerPack.fromJson(json['pack'] as Map), + 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 _$SnStickerToJson(_SnSticker instance) => + { + '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 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), + 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 _$SnStickerPackToJson(_SnStickerPack instance) => + { + '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(), + }; diff --git a/lib/pods/websocket.dart b/lib/pods/websocket.dart index c3d0b16..1174b6f 100644 --- a/lib/pods/websocket.dart +++ b/lib/pods/websocket.dart @@ -50,16 +50,31 @@ class WebSocketService { Stream get dataStream => _streamController.stream; Stream get statusStream => _statusStreamController.stream; - Future connect(String url, String atk) async { + Future 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 { 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; diff --git a/lib/route.dart b/lib/route.dart index e630d9f..ffac467 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -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', + ), ]; } diff --git a/lib/route.gr.dart b/lib/route.gr.dart index c1f884c..494b2c3 100644 --- a/lib/route.gr.dart +++ b/lib/route.gr.dart @@ -9,35 +9,38 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i19; -import 'package:flutter/material.dart' as _i20; -import 'package:island/models/post.dart' as _i21; +import 'package:auto_route/auto_route.dart' as _i22; +import 'package:flutter/material.dart' as _i23; +import 'package:island/models/post.dart' as _i24; import 'package:island/screens/account.dart' as _i2; -import 'package:island/screens/account/me.dart' as _i12; -import 'package:island/screens/account/me/event_calendar.dart' as _i11; -import 'package:island/screens/account/me/publishers.dart' as _i7; -import 'package:island/screens/account/me/update.dart' as _i18; +import 'package:island/screens/account/me.dart' as _i15; +import 'package:island/screens/account/me/event_calendar.dart' as _i14; +import 'package:island/screens/account/me/publishers.dart' as _i8; +import 'package:island/screens/account/me/update.dart' as _i21; import 'package:island/screens/account/profile.dart' as _i1; import 'package:island/screens/auth/create_account.dart' as _i6; -import 'package:island/screens/auth/login.dart' as _i10; -import 'package:island/screens/auth/tabs.dart' as _i17; +import 'package:island/screens/auth/login.dart' as _i13; +import 'package:island/screens/auth/tabs.dart' as _i20; import 'package:island/screens/chat/chat.dart' as _i4; import 'package:island/screens/chat/room.dart' as _i5; import 'package:island/screens/chat/room_detail.dart' as _i3; -import 'package:island/screens/explore.dart' as _i9; -import 'package:island/screens/posts/compose.dart' as _i13; -import 'package:island/screens/posts/detail.dart' as _i14; -import 'package:island/screens/realm/detail.dart' as _i15; -import 'package:island/screens/realm/realms.dart' as _i8; -import 'package:island/screens/settings.dart' as _i16; +import 'package:island/screens/creators/hub.dart' as _i7; +import 'package:island/screens/creators/stickers/pack_detail.dart' as _i11; +import 'package:island/screens/creators/stickers/stickers.dart' as _i10; +import 'package:island/screens/explore.dart' as _i12; +import 'package:island/screens/posts/compose.dart' as _i16; +import 'package:island/screens/posts/detail.dart' as _i17; +import 'package:island/screens/realm/detail.dart' as _i18; +import 'package:island/screens/realm/realms.dart' as _i9; +import 'package:island/screens/settings.dart' as _i19; /// generated route for /// [_i1.AccountProfileScreen] -class AccountProfileRoute extends _i19.PageRouteInfo { +class AccountProfileRoute extends _i22.PageRouteInfo { AccountProfileRoute({ - _i20.Key? key, + _i23.Key? key, required String name, - List<_i19.PageRouteInfo>? children, + List<_i22.PageRouteInfo>? children, }) : super( AccountProfileRoute.name, args: AccountProfileRouteArgs(key: key, name: name), @@ -47,7 +50,7 @@ class AccountProfileRoute extends _i19.PageRouteInfo { static const String name = 'AccountProfileRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; @@ -63,7 +66,7 @@ class AccountProfileRoute extends _i19.PageRouteInfo { class AccountProfileRouteArgs { const AccountProfileRouteArgs({this.key, required this.name}); - final _i20.Key? key; + final _i23.Key? key; final String name; @@ -75,13 +78,13 @@ class AccountProfileRouteArgs { /// generated route for /// [_i2.AccountScreen] -class AccountRoute extends _i19.PageRouteInfo { - const AccountRoute({List<_i19.PageRouteInfo>? children}) +class AccountRoute extends _i22.PageRouteInfo { + const AccountRoute({List<_i22.PageRouteInfo>? children}) : super(AccountRoute.name, initialChildren: children); static const String name = 'AccountRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { return const _i2.AccountScreen(); @@ -91,11 +94,11 @@ class AccountRoute extends _i19.PageRouteInfo { /// generated route for /// [_i3.ChatDetailScreen] -class ChatDetailRoute extends _i19.PageRouteInfo { +class ChatDetailRoute extends _i22.PageRouteInfo { ChatDetailRoute({ - _i20.Key? key, + _i23.Key? key, required int id, - List<_i19.PageRouteInfo>? children, + List<_i22.PageRouteInfo>? children, }) : super( ChatDetailRoute.name, args: ChatDetailRouteArgs(key: key, id: id), @@ -105,7 +108,7 @@ class ChatDetailRoute extends _i19.PageRouteInfo { static const String name = 'ChatDetailRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; @@ -120,7 +123,7 @@ class ChatDetailRoute extends _i19.PageRouteInfo { class ChatDetailRouteArgs { const ChatDetailRouteArgs({this.key, required this.id}); - final _i20.Key? key; + final _i23.Key? key; final int id; @@ -132,13 +135,13 @@ class ChatDetailRouteArgs { /// generated route for /// [_i4.ChatListScreen] -class ChatListRoute extends _i19.PageRouteInfo { - const ChatListRoute({List<_i19.PageRouteInfo>? children}) +class ChatListRoute extends _i22.PageRouteInfo { + const ChatListRoute({List<_i22.PageRouteInfo>? children}) : super(ChatListRoute.name, initialChildren: children); static const String name = 'ChatListRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { return const _i4.ChatListScreen(); @@ -148,11 +151,11 @@ class ChatListRoute extends _i19.PageRouteInfo { /// generated route for /// [_i5.ChatRoomScreen] -class ChatRoomRoute extends _i19.PageRouteInfo { +class ChatRoomRoute extends _i22.PageRouteInfo { ChatRoomRoute({ - _i20.Key? key, + _i23.Key? key, required int id, - List<_i19.PageRouteInfo>? children, + List<_i22.PageRouteInfo>? children, }) : super( ChatRoomRoute.name, args: ChatRoomRouteArgs(key: key, id: id), @@ -162,7 +165,7 @@ class ChatRoomRoute extends _i19.PageRouteInfo { static const String name = 'ChatRoomRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; @@ -177,7 +180,7 @@ class ChatRoomRoute extends _i19.PageRouteInfo { class ChatRoomRouteArgs { const ChatRoomRouteArgs({this.key, required this.id}); - final _i20.Key? key; + final _i23.Key? key; final int id; @@ -189,13 +192,13 @@ class ChatRoomRouteArgs { /// generated route for /// [_i6.CreateAccountScreen] -class CreateAccountRoute extends _i19.PageRouteInfo { - const CreateAccountRoute({List<_i19.PageRouteInfo>? children}) +class CreateAccountRoute extends _i22.PageRouteInfo { + const CreateAccountRoute({List<_i22.PageRouteInfo>? children}) : super(CreateAccountRoute.name, initialChildren: children); static const String name = 'CreateAccountRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { return const _i6.CreateAccountScreen(); @@ -203,10 +206,26 @@ class CreateAccountRoute extends _i19.PageRouteInfo { ); } +/// generated route for +/// [_i7.CreatorHubScreen] +class CreatorHubRoute extends _i22.PageRouteInfo { + const CreatorHubRoute({List<_i22.PageRouteInfo>? children}) + : super(CreatorHubRoute.name, initialChildren: children); + + static const String name = 'CreatorHubRoute'; + + static _i22.PageInfo page = _i22.PageInfo( + name, + builder: (data) { + return const _i7.CreatorHubScreen(); + }, + ); +} + /// generated route for /// [_i4.EditChatScreen] -class EditChatRoute extends _i19.PageRouteInfo { - EditChatRoute({_i20.Key? key, int? id, List<_i19.PageRouteInfo>? children}) +class EditChatRoute extends _i22.PageRouteInfo { + EditChatRoute({_i23.Key? key, int? id, List<_i22.PageRouteInfo>? children}) : super( EditChatRoute.name, args: EditChatRouteArgs(key: key, id: id), @@ -216,7 +235,7 @@ class EditChatRoute extends _i19.PageRouteInfo { static const String name = 'EditChatRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; @@ -231,7 +250,7 @@ class EditChatRoute extends _i19.PageRouteInfo { class EditChatRouteArgs { const EditChatRouteArgs({this.key, this.id}); - final _i20.Key? key; + final _i23.Key? key; final int? id; @@ -242,12 +261,12 @@ class EditChatRouteArgs { } /// generated route for -/// [_i7.EditPublisherScreen] -class EditPublisherRoute extends _i19.PageRouteInfo { +/// [_i8.EditPublisherScreen] +class EditPublisherRoute extends _i22.PageRouteInfo { EditPublisherRoute({ - _i20.Key? key, + _i23.Key? key, String? name, - List<_i19.PageRouteInfo>? children, + List<_i22.PageRouteInfo>? children, }) : super( EditPublisherRoute.name, args: EditPublisherRouteArgs(key: key, name: name), @@ -257,14 +276,14 @@ class EditPublisherRoute extends _i19.PageRouteInfo { static const String name = 'EditPublisherRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => EditPublisherRouteArgs(name: pathParams.optString('id')), ); - return _i7.EditPublisherScreen(key: args.key, name: args.name); + return _i8.EditPublisherScreen(key: args.key, name: args.name); }, ); } @@ -272,7 +291,7 @@ class EditPublisherRoute extends _i19.PageRouteInfo { class EditPublisherRouteArgs { const EditPublisherRouteArgs({this.key, this.name}); - final _i20.Key? key; + final _i23.Key? key; final String? name; @@ -283,12 +302,12 @@ class EditPublisherRouteArgs { } /// generated route for -/// [_i8.EditRealmScreen] -class EditRealmRoute extends _i19.PageRouteInfo { +/// [_i9.EditRealmScreen] +class EditRealmRoute extends _i22.PageRouteInfo { EditRealmRoute({ - _i20.Key? key, + _i23.Key? key, String? slug, - List<_i19.PageRouteInfo>? children, + List<_i22.PageRouteInfo>? children, }) : super( EditRealmRoute.name, args: EditRealmRouteArgs(key: key, slug: slug), @@ -298,14 +317,14 @@ class EditRealmRoute extends _i19.PageRouteInfo { static const String name = 'EditRealmRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => EditRealmRouteArgs(slug: pathParams.optString('slug')), ); - return _i8.EditRealmScreen(key: args.key, slug: args.slug); + return _i9.EditRealmScreen(key: args.key, slug: args.slug); }, ); } @@ -313,7 +332,7 @@ class EditRealmRoute extends _i19.PageRouteInfo { class EditRealmRouteArgs { const EditRealmRouteArgs({this.key, this.slug}); - final _i20.Key? key; + final _i23.Key? key; final String? slug; @@ -324,94 +343,211 @@ class EditRealmRouteArgs { } /// generated route for -/// [_i9.ExploreScreen] -class ExploreRoute extends _i19.PageRouteInfo { - const ExploreRoute({List<_i19.PageRouteInfo>? children}) +/// [_i10.EditStickerPacksScreen] +class EditStickerPacksRoute + extends _i22.PageRouteInfo { + EditStickerPacksRoute({ + _i23.Key? key, + required String pubName, + String? packId, + List<_i22.PageRouteInfo>? children, + }) : super( + EditStickerPacksRoute.name, + args: EditStickerPacksRouteArgs( + key: key, + pubName: pubName, + packId: packId, + ), + rawPathParams: {'name': pubName, 'packId': packId}, + initialChildren: children, + ); + + static const String name = 'EditStickerPacksRoute'; + + static _i22.PageInfo page = _i22.PageInfo( + name, + builder: (data) { + final pathParams = data.inheritedPathParams; + final args = data.argsAs( + orElse: + () => EditStickerPacksRouteArgs( + pubName: pathParams.getString('name'), + packId: pathParams.optString('packId'), + ), + ); + return _i10.EditStickerPacksScreen( + key: args.key, + pubName: args.pubName, + packId: args.packId, + ); + }, + ); +} + +class EditStickerPacksRouteArgs { + const EditStickerPacksRouteArgs({ + this.key, + required this.pubName, + this.packId, + }); + + final _i23.Key? key; + + final String pubName; + + final String? packId; + + @override + String toString() { + return 'EditStickerPacksRouteArgs{key: $key, pubName: $pubName, packId: $packId}'; + } +} + +/// generated route for +/// [_i11.EditStickersScreen] +class EditStickersRoute extends _i22.PageRouteInfo { + EditStickersRoute({ + _i23.Key? key, + required String packId, + required String? id, + List<_i22.PageRouteInfo>? children, + }) : super( + EditStickersRoute.name, + args: EditStickersRouteArgs(key: key, packId: packId, id: id), + rawPathParams: {'packId': packId, 'id': id}, + initialChildren: children, + ); + + static const String name = 'EditStickersRoute'; + + static _i22.PageInfo page = _i22.PageInfo( + name, + builder: (data) { + final pathParams = data.inheritedPathParams; + final args = data.argsAs( + orElse: + () => EditStickersRouteArgs( + packId: pathParams.getString('packId'), + id: pathParams.optString('id'), + ), + ); + return _i11.EditStickersScreen( + key: args.key, + packId: args.packId, + id: args.id, + ); + }, + ); +} + +class EditStickersRouteArgs { + const EditStickersRouteArgs({ + this.key, + required this.packId, + required this.id, + }); + + final _i23.Key? key; + + final String packId; + + final String? id; + + @override + String toString() { + return 'EditStickersRouteArgs{key: $key, packId: $packId, id: $id}'; + } +} + +/// generated route for +/// [_i12.ExploreScreen] +class ExploreRoute extends _i22.PageRouteInfo { + const ExploreRoute({List<_i22.PageRouteInfo>? children}) : super(ExploreRoute.name, initialChildren: children); static const String name = 'ExploreRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { - return const _i9.ExploreScreen(); + return const _i12.ExploreScreen(); }, ); } /// generated route for -/// [_i10.LoginScreen] -class LoginRoute extends _i19.PageRouteInfo { - const LoginRoute({List<_i19.PageRouteInfo>? children}) +/// [_i13.LoginScreen] +class LoginRoute extends _i22.PageRouteInfo { + const LoginRoute({List<_i22.PageRouteInfo>? children}) : super(LoginRoute.name, initialChildren: children); static const String name = 'LoginRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { - return const _i10.LoginScreen(); + return const _i13.LoginScreen(); }, ); } /// generated route for -/// [_i7.ManagedPublisherScreen] -class ManagedPublisherRoute extends _i19.PageRouteInfo { - const ManagedPublisherRoute({List<_i19.PageRouteInfo>? children}) +/// [_i8.ManagedPublisherScreen] +class ManagedPublisherRoute extends _i22.PageRouteInfo { + const ManagedPublisherRoute({List<_i22.PageRouteInfo>? children}) : super(ManagedPublisherRoute.name, initialChildren: children); static const String name = 'ManagedPublisherRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { - return const _i7.ManagedPublisherScreen(); + return const _i8.ManagedPublisherScreen(); }, ); } /// generated route for -/// [_i11.MyselfEventCalendarScreen] -class MyselfEventCalendarRoute extends _i19.PageRouteInfo { - const MyselfEventCalendarRoute({List<_i19.PageRouteInfo>? children}) +/// [_i14.MyselfEventCalendarScreen] +class MyselfEventCalendarRoute extends _i22.PageRouteInfo { + const MyselfEventCalendarRoute({List<_i22.PageRouteInfo>? children}) : super(MyselfEventCalendarRoute.name, initialChildren: children); static const String name = 'MyselfEventCalendarRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { - return const _i11.MyselfEventCalendarScreen(); + return const _i14.MyselfEventCalendarScreen(); }, ); } /// generated route for -/// [_i12.MyselfProfileScreen] -class MyselfProfileRoute extends _i19.PageRouteInfo { - const MyselfProfileRoute({List<_i19.PageRouteInfo>? children}) +/// [_i15.MyselfProfileScreen] +class MyselfProfileRoute extends _i22.PageRouteInfo { + const MyselfProfileRoute({List<_i22.PageRouteInfo>? children}) : super(MyselfProfileRoute.name, initialChildren: children); static const String name = 'MyselfProfileRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { - return const _i12.MyselfProfileScreen(); + return const _i15.MyselfProfileScreen(); }, ); } /// generated route for /// [_i4.NewChatScreen] -class NewChatRoute extends _i19.PageRouteInfo { - const NewChatRoute({List<_i19.PageRouteInfo>? children}) +class NewChatRoute extends _i22.PageRouteInfo { + const NewChatRoute({List<_i22.PageRouteInfo>? children}) : super(NewChatRoute.name, initialChildren: children); static const String name = 'NewChatRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { return const _i4.NewChatScreen(); @@ -420,44 +556,130 @@ class NewChatRoute extends _i19.PageRouteInfo { } /// generated route for -/// [_i7.NewPublisherScreen] -class NewPublisherRoute extends _i19.PageRouteInfo { - const NewPublisherRoute({List<_i19.PageRouteInfo>? children}) +/// [_i8.NewPublisherScreen] +class NewPublisherRoute extends _i22.PageRouteInfo { + const NewPublisherRoute({List<_i22.PageRouteInfo>? children}) : super(NewPublisherRoute.name, initialChildren: children); static const String name = 'NewPublisherRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { - return const _i7.NewPublisherScreen(); + return const _i8.NewPublisherScreen(); }, ); } /// generated route for -/// [_i8.NewRealmScreen] -class NewRealmRoute extends _i19.PageRouteInfo { - const NewRealmRoute({List<_i19.PageRouteInfo>? children}) +/// [_i9.NewRealmScreen] +class NewRealmRoute extends _i22.PageRouteInfo { + const NewRealmRoute({List<_i22.PageRouteInfo>? children}) : super(NewRealmRoute.name, initialChildren: children); static const String name = 'NewRealmRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { - return const _i8.NewRealmScreen(); + return const _i9.NewRealmScreen(); }, ); } /// generated route for -/// [_i13.PostComposeScreen] -class PostComposeRoute extends _i19.PageRouteInfo { +/// [_i10.NewStickerPacksScreen] +class NewStickerPacksRoute + extends _i22.PageRouteInfo { + NewStickerPacksRoute({ + _i23.Key? key, + required String pubName, + List<_i22.PageRouteInfo>? children, + }) : super( + NewStickerPacksRoute.name, + args: NewStickerPacksRouteArgs(key: key, pubName: pubName), + rawPathParams: {'name': pubName}, + initialChildren: children, + ); + + static const String name = 'NewStickerPacksRoute'; + + static _i22.PageInfo page = _i22.PageInfo( + name, + builder: (data) { + final pathParams = data.inheritedPathParams; + final args = data.argsAs( + orElse: + () => + NewStickerPacksRouteArgs(pubName: pathParams.getString('name')), + ); + return _i10.NewStickerPacksScreen(key: args.key, pubName: args.pubName); + }, + ); +} + +class NewStickerPacksRouteArgs { + const NewStickerPacksRouteArgs({this.key, required this.pubName}); + + final _i23.Key? key; + + final String pubName; + + @override + String toString() { + return 'NewStickerPacksRouteArgs{key: $key, pubName: $pubName}'; + } +} + +/// generated route for +/// [_i11.NewStickersScreen] +class NewStickersRoute extends _i22.PageRouteInfo { + NewStickersRoute({ + _i23.Key? key, + required String packId, + List<_i22.PageRouteInfo>? children, + }) : super( + NewStickersRoute.name, + args: NewStickersRouteArgs(key: key, packId: packId), + rawPathParams: {'packId': packId}, + initialChildren: children, + ); + + static const String name = 'NewStickersRoute'; + + static _i22.PageInfo page = _i22.PageInfo( + name, + builder: (data) { + final pathParams = data.inheritedPathParams; + final args = data.argsAs( + orElse: + () => NewStickersRouteArgs(packId: pathParams.getString('packId')), + ); + return _i11.NewStickersScreen(key: args.key, packId: args.packId); + }, + ); +} + +class NewStickersRouteArgs { + const NewStickersRouteArgs({this.key, required this.packId}); + + final _i23.Key? key; + + final String packId; + + @override + String toString() { + return 'NewStickersRouteArgs{key: $key, packId: $packId}'; + } +} + +/// generated route for +/// [_i16.PostComposeScreen] +class PostComposeRoute extends _i22.PageRouteInfo { PostComposeRoute({ - _i20.Key? key, - _i21.SnPost? originalPost, - List<_i19.PageRouteInfo>? children, + _i23.Key? key, + _i24.SnPost? originalPost, + List<_i22.PageRouteInfo>? children, }) : super( PostComposeRoute.name, args: PostComposeRouteArgs(key: key, originalPost: originalPost), @@ -466,13 +688,13 @@ class PostComposeRoute extends _i19.PageRouteInfo { static const String name = 'PostComposeRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { final args = data.argsAs( orElse: () => const PostComposeRouteArgs(), ); - return _i13.PostComposeScreen( + return _i16.PostComposeScreen( key: args.key, originalPost: args.originalPost, ); @@ -483,9 +705,9 @@ class PostComposeRoute extends _i19.PageRouteInfo { class PostComposeRouteArgs { const PostComposeRouteArgs({this.key, this.originalPost}); - final _i20.Key? key; + final _i23.Key? key; - final _i21.SnPost? originalPost; + final _i24.SnPost? originalPost; @override String toString() { @@ -494,12 +716,12 @@ class PostComposeRouteArgs { } /// generated route for -/// [_i14.PostDetailScreen] -class PostDetailRoute extends _i19.PageRouteInfo { +/// [_i17.PostDetailScreen] +class PostDetailRoute extends _i22.PageRouteInfo { PostDetailRoute({ - _i20.Key? key, + _i23.Key? key, required int id, - List<_i19.PageRouteInfo>? children, + List<_i22.PageRouteInfo>? children, }) : super( PostDetailRoute.name, args: PostDetailRouteArgs(key: key, id: id), @@ -509,14 +731,14 @@ class PostDetailRoute extends _i19.PageRouteInfo { static const String name = 'PostDetailRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => PostDetailRouteArgs(id: pathParams.getInt('id')), ); - return _i14.PostDetailScreen(key: args.key, id: args.id); + return _i17.PostDetailScreen(key: args.key, id: args.id); }, ); } @@ -524,7 +746,7 @@ class PostDetailRoute extends _i19.PageRouteInfo { class PostDetailRouteArgs { const PostDetailRouteArgs({this.key, required this.id}); - final _i20.Key? key; + final _i23.Key? key; final int id; @@ -535,12 +757,12 @@ class PostDetailRouteArgs { } /// generated route for -/// [_i13.PostEditScreen] -class PostEditRoute extends _i19.PageRouteInfo { +/// [_i16.PostEditScreen] +class PostEditRoute extends _i22.PageRouteInfo { PostEditRoute({ - _i20.Key? key, + _i23.Key? key, required int id, - List<_i19.PageRouteInfo>? children, + List<_i22.PageRouteInfo>? children, }) : super( PostEditRoute.name, args: PostEditRouteArgs(key: key, id: id), @@ -550,14 +772,14 @@ class PostEditRoute extends _i19.PageRouteInfo { static const String name = 'PostEditRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => PostEditRouteArgs(id: pathParams.getInt('id')), ); - return _i13.PostEditScreen(key: args.key, id: args.id); + return _i16.PostEditScreen(key: args.key, id: args.id); }, ); } @@ -565,7 +787,7 @@ class PostEditRoute extends _i19.PageRouteInfo { class PostEditRouteArgs { const PostEditRouteArgs({this.key, required this.id}); - final _i20.Key? key; + final _i23.Key? key; final int id; @@ -576,12 +798,12 @@ class PostEditRouteArgs { } /// generated route for -/// [_i15.RealmDetailScreen] -class RealmDetailRoute extends _i19.PageRouteInfo { +/// [_i18.RealmDetailScreen] +class RealmDetailRoute extends _i22.PageRouteInfo { RealmDetailRoute({ - _i20.Key? key, + _i23.Key? key, required String slug, - List<_i19.PageRouteInfo>? children, + List<_i22.PageRouteInfo>? children, }) : super( RealmDetailRoute.name, args: RealmDetailRouteArgs(key: key, slug: slug), @@ -591,14 +813,14 @@ class RealmDetailRoute extends _i19.PageRouteInfo { static const String name = 'RealmDetailRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { final pathParams = data.inheritedPathParams; final args = data.argsAs( orElse: () => RealmDetailRouteArgs(slug: pathParams.getString('slug')), ); - return _i15.RealmDetailScreen(key: args.key, slug: args.slug); + return _i18.RealmDetailScreen(key: args.key, slug: args.slug); }, ); } @@ -606,7 +828,7 @@ class RealmDetailRoute extends _i19.PageRouteInfo { class RealmDetailRouteArgs { const RealmDetailRouteArgs({this.key, required this.slug}); - final _i20.Key? key; + final _i23.Key? key; final String slug; @@ -617,65 +839,163 @@ class RealmDetailRouteArgs { } /// generated route for -/// [_i8.RealmListScreen] -class RealmListRoute extends _i19.PageRouteInfo { - const RealmListRoute({List<_i19.PageRouteInfo>? children}) +/// [_i9.RealmListScreen] +class RealmListRoute extends _i22.PageRouteInfo { + const RealmListRoute({List<_i22.PageRouteInfo>? children}) : super(RealmListRoute.name, initialChildren: children); static const String name = 'RealmListRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { - return const _i8.RealmListScreen(); + return const _i9.RealmListScreen(); }, ); } /// generated route for -/// [_i16.SettingsScreen] -class SettingsRoute extends _i19.PageRouteInfo { - const SettingsRoute({List<_i19.PageRouteInfo>? children}) +/// [_i19.SettingsScreen] +class SettingsRoute extends _i22.PageRouteInfo { + const SettingsRoute({List<_i22.PageRouteInfo>? children}) : super(SettingsRoute.name, initialChildren: children); static const String name = 'SettingsRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { - return const _i16.SettingsScreen(); + return const _i19.SettingsScreen(); }, ); } /// generated route for -/// [_i17.TabsScreen] -class TabsRoute extends _i19.PageRouteInfo { - const TabsRoute({List<_i19.PageRouteInfo>? children}) +/// [_i11.StickerPackDetailScreen] +class StickerPackDetailRoute + extends _i22.PageRouteInfo { + StickerPackDetailRoute({ + _i23.Key? key, + required String pubName, + required String id, + List<_i22.PageRouteInfo>? children, + }) : super( + StickerPackDetailRoute.name, + args: StickerPackDetailRouteArgs(key: key, pubName: pubName, id: id), + rawPathParams: {'name': pubName, 'packId': id}, + initialChildren: children, + ); + + static const String name = 'StickerPackDetailRoute'; + + static _i22.PageInfo page = _i22.PageInfo( + name, + builder: (data) { + final pathParams = data.inheritedPathParams; + final args = data.argsAs( + orElse: + () => StickerPackDetailRouteArgs( + pubName: pathParams.getString('name'), + id: pathParams.getString('packId'), + ), + ); + return _i11.StickerPackDetailScreen( + key: args.key, + pubName: args.pubName, + id: args.id, + ); + }, + ); +} + +class StickerPackDetailRouteArgs { + const StickerPackDetailRouteArgs({ + this.key, + required this.pubName, + required this.id, + }); + + final _i23.Key? key; + + final String pubName; + + final String id; + + @override + String toString() { + return 'StickerPackDetailRouteArgs{key: $key, pubName: $pubName, id: $id}'; + } +} + +/// generated route for +/// [_i10.StickersScreen] +class StickersRoute extends _i22.PageRouteInfo { + StickersRoute({ + _i23.Key? key, + required String pubName, + List<_i22.PageRouteInfo>? children, + }) : super( + StickersRoute.name, + args: StickersRouteArgs(key: key, pubName: pubName), + rawPathParams: {'name': pubName}, + initialChildren: children, + ); + + static const String name = 'StickersRoute'; + + static _i22.PageInfo page = _i22.PageInfo( + name, + builder: (data) { + final pathParams = data.inheritedPathParams; + final args = data.argsAs( + orElse: () => StickersRouteArgs(pubName: pathParams.getString('name')), + ); + return _i10.StickersScreen(key: args.key, pubName: args.pubName); + }, + ); +} + +class StickersRouteArgs { + const StickersRouteArgs({this.key, required this.pubName}); + + final _i23.Key? key; + + final String pubName; + + @override + String toString() { + return 'StickersRouteArgs{key: $key, pubName: $pubName}'; + } +} + +/// generated route for +/// [_i20.TabsScreen] +class TabsRoute extends _i22.PageRouteInfo { + const TabsRoute({List<_i22.PageRouteInfo>? children}) : super(TabsRoute.name, initialChildren: children); static const String name = 'TabsRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { - return const _i17.TabsScreen(); + return const _i20.TabsScreen(); }, ); } /// generated route for -/// [_i18.UpdateProfileScreen] -class UpdateProfileRoute extends _i19.PageRouteInfo { - const UpdateProfileRoute({List<_i19.PageRouteInfo>? children}) +/// [_i21.UpdateProfileScreen] +class UpdateProfileRoute extends _i22.PageRouteInfo { + const UpdateProfileRoute({List<_i22.PageRouteInfo>? children}) : super(UpdateProfileRoute.name, initialChildren: children); static const String name = 'UpdateProfileRoute'; - static _i19.PageInfo page = _i19.PageInfo( + static _i22.PageInfo page = _i22.PageInfo( name, builder: (data) { - return const _i18.UpdateProfileScreen(); + return const _i21.UpdateProfileScreen(); }, ); } diff --git a/lib/screens/account.dart b/lib/screens/account.dart index 96b56e1..14280f5 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -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: [ @@ -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), diff --git a/lib/screens/account/me/update.dart b/lib/screens/account/me/update.dart index 4d9e262..6cf0103 100644 --- a/lib/screens/account/me/update.dart +++ b/lib/screens/account/me/update.dart @@ -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); } } diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index 9c6f011..45a953f 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -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; } } diff --git a/lib/screens/creators/hub.dart b/lib/screens/creators/hub.dart new file mode 100644 index 0000000..90a4bc9 --- /dev/null +++ b/lib/screens/creators/hub.dart @@ -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 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( + publishers.value?.firstOrNull, + ); + + final publishersMenu = publishers.when( + data: + (data) => + data + .map( + (item) => DropdownMenuItem( + 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( + 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(), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/creators/hub.g.dart b/lib/screens/creators/hub.g.dart new file mode 100644 index 0000000..694c03c --- /dev/null +++ b/lib/screens/creators/hub.g.dart @@ -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> { + /// 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? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'publisherStatsProvider'; +} + +/// See also [publisherStats]. +class PublisherStatsProvider + extends AutoDisposeFutureProvider { + /// 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 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 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 { + /// The parameter `uname` of this provider. + String? get uname; +} + +class _PublisherStatsProviderElement + extends AutoDisposeFutureProviderElement + 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 diff --git a/lib/screens/creators/stickers/pack_detail.dart b/lib/screens/creators/stickers/pack_detail.dart new file mode 100644 index 0000000..a775be8 --- /dev/null +++ b/lib/screens/creators/stickers/pack_detail.dart @@ -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> stickerPackContent(Ref ref, String packId) async { + final apiClient = ref.watch(apiClientProvider); + final resp = await apiClient.get('/stickers/$packId/content'); + return resp.data + .map((e) => SnSticker.fromJson(e)) + .cast() + .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 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 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(), []); + + final image = useState(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 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), + ); + } +} diff --git a/lib/screens/creators/stickers/pack_detail.freezed.dart b/lib/screens/creators/stickers/pack_detail.freezed.dart new file mode 100644 index 0000000..d59a860 --- /dev/null +++ b/lib/screens/creators/stickers/pack_detail.freezed.dart @@ -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 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 get copyWith => _$StickerWithPackQueryCopyWithImpl(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 diff --git a/lib/screens/creators/stickers/pack_detail.g.dart b/lib/screens/creators/stickers/pack_detail.g.dart new file mode 100644 index 0000000..57780fe --- /dev/null +++ b/lib/screens/creators/stickers/pack_detail.g.dart @@ -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>> { + /// 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? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'stickerPackContentProvider'; +} + +/// See also [stickerPackContent]. +class StickerPackContentProvider + extends AutoDisposeFutureProvider> { + /// 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> 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> 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> { + /// The parameter `packId` of this provider. + String get packId; +} + +class _StickerPackContentProviderElement + extends AutoDisposeFutureProviderElement> + 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> { + /// 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? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'stickerPackStickerProvider'; +} + +/// See also [stickerPackSticker]. +class StickerPackStickerProvider extends AutoDisposeFutureProvider { + /// 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 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 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 { + /// The parameter `query` of this provider. + StickerWithPackQuery? get query; +} + +class _StickerPackStickerProviderElement + extends AutoDisposeFutureProviderElement + 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 diff --git a/lib/screens/creators/stickers/stickers.dart b/lib/screens/creators/stickers/stickers.dart new file mode 100644 index 0000000..d57f0fd --- /dev/null +++ b/lib/screens/creators/stickers/stickers.dart @@ -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> +>((ref) { + return StickerPacksNotifier(ref.watch(apiClientProvider)); +}); + +class StickerPacksNotifier + extends StateNotifier>> { + 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 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() + .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 fetchMore() async { + if (state.valueOrNull == null || state.valueOrNull!.length >= total) return; + await fetchStickers(); + } + + Future refresh() async { + offset = 0; + state = const AsyncValue.loading(); + await fetchStickers(); + } +} + +@riverpod +Future 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(), []); + 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 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), + ); + } +} diff --git a/lib/screens/creators/stickers/stickers.g.dart b/lib/screens/creators/stickers/stickers.g.dart new file mode 100644 index 0000000..d3bae95 --- /dev/null +++ b/lib/screens/creators/stickers/stickers.g.dart @@ -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> { + /// 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? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'stickerPackProvider'; +} + +/// See also [stickerPack]. +class StickerPackProvider extends AutoDisposeFutureProvider { + /// 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 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 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 { + /// The parameter `packId` of this provider. + String? get packId; +} + +class _StickerPackProviderElement + extends AutoDisposeFutureProviderElement + 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 diff --git a/lib/screens/posts/compose.dart b/lib/screens/posts/compose.dart index e7e18da..08893e7 100644 --- a/lib/screens/posts/compose.dart +++ b/lib/screens/posts/compose.dart @@ -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,100 +464,105 @@ class AttachmentPreview extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - InkWell( - borderRadius: BorderRadius.circular(8), - child: const Icon( - Symbols.delete, - size: 14, - color: Colors.white, - ).padding(horizontal: 8, vertical: 6), - onTap: () { - onDelete?.call(); - }, - ), - SizedBox( - height: 26, - child: const VerticalDivider( - width: 0.3, - color: Colors.white, - thickness: 0.3, + if (onDelete != null) + InkWell( + borderRadius: BorderRadius.circular(8), + child: const Icon( + Symbols.delete, + size: 14, + color: Colors.white, + ).padding(horizontal: 8, vertical: 6), + onTap: () { + onDelete?.call(); + }, + ), + if (onDelete != null && onMove != null) + SizedBox( + height: 26, + child: const VerticalDivider( + width: 0.3, + color: Colors.white, + thickness: 0.3, + ), + ).padding(horizontal: 2), + if (onMove != null) + InkWell( + borderRadius: BorderRadius.circular(8), + child: const Icon( + Symbols.keyboard_arrow_up, + size: 14, + color: Colors.white, + ).padding(horizontal: 8, vertical: 6), + onTap: () { + onMove?.call(-1); + }, + ), + if (onMove != null) + InkWell( + borderRadius: BorderRadius.circular(8), + child: const Icon( + Symbols.keyboard_arrow_down, + size: 14, + color: Colors.white, + ).padding(horizontal: 8, vertical: 6), + onTap: () { + onMove?.call(1); + }, ), - ).padding(horizontal: 2), - InkWell( - borderRadius: BorderRadius.circular(8), - child: const Icon( - Symbols.keyboard_arrow_up, - size: 14, - color: Colors.white, - ).padding(horizontal: 8, vertical: 6), - onTap: () { - onMove?.call(-1); - }, - ), - InkWell( - borderRadius: BorderRadius.circular(8), - child: const Icon( - Symbols.keyboard_arrow_down, - size: 14, - color: Colors.white, - ).padding(horizontal: 8, vertical: 6), - onTap: () { - onMove?.call(1); - }, - ), ], ), ), ), ), ), - Positioned( - top: 8, - right: 8, - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () => onRequestUpload?.call(), - child: ClipRRect( + if (onRequestUpload != null) + Positioned( + top: 8, + right: 8, + child: InkWell( borderRadius: BorderRadius.circular(8), - child: Container( - color: Colors.black.withOpacity(0.5), - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: - (item.isOnCloud) - ? Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Symbols.cloud, - size: 16, - color: Colors.white, - ), - const Gap(8), - Text( - 'On-cloud', - style: TextStyle(color: Colors.white), - ), - ], - ) - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Symbols.cloud_off, - size: 16, - color: Colors.white, - ), - const Gap(8), - Text( - 'On-device', - style: TextStyle(color: Colors.white), - ), - ], - ), + onTap: () => onRequestUpload?.call(), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Container( + color: Colors.black.withOpacity(0.5), + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: + (item.isOnCloud) + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Symbols.cloud, + size: 16, + color: Colors.white, + ), + const Gap(8), + Text( + 'On-cloud', + style: TextStyle(color: Colors.white), + ), + ], + ) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Symbols.cloud_off, + size: 16, + color: Colors.white, + ), + const Gap(8), + Text( + 'On-device', + style: TextStyle(color: Colors.white), + ), + ], + ), + ), ), ), ), - ), ], ), ), diff --git a/lib/screens/realm/realms.dart b/lib/screens/realm/realms.dart index 532897e..5874390 100644 --- a/lib/screens/realm/realms.dart +++ b/lib/screens/realm/realms.dart @@ -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; } } diff --git a/lib/widgets/alert.dart b/lib/widgets/alert.dart index 8b29b56..7675619 100644 --- a/lib/widgets/alert.dart +++ b/lib/widgets/alert.dart @@ -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 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(); +} diff --git a/lib/widgets/content/cloud_file_picker.dart b/lib/widgets/content/cloud_file_picker.dart new file mode 100644 index 0000000..5ab3be0 --- /dev/null +++ b/lib/widgets/content/cloud_file_picker.dart @@ -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>([]); + + final uploadPosition = useState(null); + final uploadProgress = useState(null); + + final uploadOverallProgress = useMemoized(() { + 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 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 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), + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 44ecf6b..4e2b810 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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