Compare commits
23 Commits
f00135c4bf
...
3.1.0+118
Author | SHA1 | Date | |
---|---|---|---|
6b3338b885 | |||
bb00b1bc6a | |||
5e1a15ada2 | |||
9bdf8ba346 | |||
204c087f29 | |||
1def3e1895 | |||
550c74e544 | |||
a39565f012 | |||
aa9755e6a7 | |||
b25e8d661a | |||
4b253ac3ec | |||
5d1b875d3c | |||
e2e103fa67 | |||
43c90da4e3 | |||
fa210dd98f | |||
43d9ca92bf | |||
5e592c143f | |||
0c59816f26 | |||
19c2457895 | |||
af8d87857e | |||
d05f63a36a | |||
e2dc520012 | |||
cff9c15e31 |
@@ -12,7 +12,12 @@
|
||||
"package_name": "dev.solsynth.solian"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "961776991058-963m1qin2vtp8fv693b5fdrab5hmpl89.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDvFNudXYs29uDtcCv6pFR8h5tXBs90FYk"
|
||||
@@ -20,7 +25,20 @@
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "961776991058-963m1qin2vtp8fv693b5fdrab5hmpl89.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
},
|
||||
{
|
||||
"client_id": "961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com",
|
||||
"client_type": 2,
|
||||
"ios_info": {
|
||||
"bundle_id": "dev.solsynth.solian",
|
||||
"app_store_id": "6499032345"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -144,6 +144,7 @@
|
||||
"other": "{} attachments"
|
||||
},
|
||||
"edited": "Edited",
|
||||
"editedAt": "Edited at {}",
|
||||
"addVideo": "Add video",
|
||||
"addPhoto": "Add photo",
|
||||
"addAudio": "Add audio",
|
||||
@@ -768,5 +769,23 @@
|
||||
"addPack": "Add Pack",
|
||||
"removePack": "Remove Pack",
|
||||
"browseAndAddStickers": "Browse and add sticker packs",
|
||||
"stickerPack": "Sticker Pack"
|
||||
"stickerPack": "Sticker Pack",
|
||||
"postCategoryTechnology": "Technology",
|
||||
"postCategoryTravel": "Travel",
|
||||
"postCategoryFood": "Food",
|
||||
"postCategoryHealth": "Health",
|
||||
"postCategoryScience": "Science",
|
||||
"postCategorySports": "Sports",
|
||||
"postCategoryFinance": "Finance",
|
||||
"postCategoryLife": "Life",
|
||||
"postCategoryArt": "Art",
|
||||
"postCategoryStudy": "Study",
|
||||
"postCategoryGaming": "Gaming",
|
||||
"postCategoryProgramming": "Programming",
|
||||
"postCategoryMusic": "Music",
|
||||
"links": "Links",
|
||||
"addLink": "Add link",
|
||||
"linkKey": "Link Name",
|
||||
"linkValue": "URL",
|
||||
"debugOptions": "Debug Options"
|
||||
}
|
||||
|
@@ -120,6 +120,7 @@
|
||||
"other": "{}个附件"
|
||||
},
|
||||
"edited": "已编辑",
|
||||
"editedAt": "编辑于 {}",
|
||||
"addVideo": "添加视频",
|
||||
"addPhoto": "添加照片",
|
||||
"addFile": "添加文件",
|
||||
|
@@ -1 +1 @@
|
||||
{"flutter":{"platforms":{"android":{"default":{"projectId":"solian-0x001","appId":"1:961776991058:android:a8d3f7995b0b8e86f4188b","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"solian-0x001","appId":"1:961776991058:ios:727229d368cc47e1f4188b","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"solian-0x001","appId":"1:961776991058:ios:727229d368cc47e1f4188b","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"solian-0x001","configurations":{"android":"1:961776991058:android:a8d3f7995b0b8e86f4188b","ios":"1:961776991058:ios:727229d368cc47e1f4188b","macos":"1:961776991058:ios:727229d368cc47e1f4188b","web":"1:961776991058:web:b91d12f2892a5609f4188b","windows":"1:961776991058:web:3a912c0eb14028e5f4188b"}}}}}}
|
||||
{"flutter":{"platforms":{"android":{"default":{"projectId":"solian-0x001","appId":"1:961776991058:android:a8d3f7995b0b8e86f4188b","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"solian-0x001","appId":"1:961776991058:ios:727229d368cc47e1f4188b","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"solian-0x001","appId":"1:961776991058:ios:727229d368cc47e1f4188b","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"solian-0x001","configurations":{"android":"1:961776991058:android:a8d3f7995b0b8e86f4188b","ios":"1:961776991058:ios:727229d368cc47e1f4188b","macos":"1:961776991058:ios:727229d368cc47e1f4188b","web":"1:961776991058:web:3a912c0eb14028e5f4188b","windows":"1:961776991058:web:3a912c0eb14028e5f4188b"}}}}}}
|
@@ -2,6 +2,12 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CLIENT_ID</key>
|
||||
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
|
||||
<key>REVERSED_CLIENT_ID</key>
|
||||
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
|
||||
<key>ANDROID_CLIENT_ID</key>
|
||||
<string>961776991058-r4iv9qoio57ul7utbfpgfrda2etvtch8.apps.googleusercontent.com</string>
|
||||
<key>API_KEY</key>
|
||||
<string>AIzaSyCzQIyiYKoYHTpGXhN-IjgMML8z797WVD8</string>
|
||||
<key>GCM_SENDER_ID</key>
|
||||
|
@@ -29,10 +29,7 @@ class DefaultFirebaseOptions {
|
||||
case TargetPlatform.windows:
|
||||
return windows;
|
||||
case TargetPlatform.linux:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for linux - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
return windows;
|
||||
default:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions are not supported for this platform.',
|
||||
@@ -41,13 +38,13 @@ class DefaultFirebaseOptions {
|
||||
}
|
||||
|
||||
static const FirebaseOptions web = FirebaseOptions(
|
||||
apiKey: 'AIzaSyBKfIQpTouj5rXnlzkEieSlbAzepm4mgJE',
|
||||
appId: '1:961776991058:web:b91d12f2892a5609f4188b',
|
||||
apiKey: 'AIzaSyCfgOdlcr7h8x8j0WKx_S2wXnGkOopq320',
|
||||
appId: '1:961776991058:web:3a912c0eb14028e5f4188b',
|
||||
messagingSenderId: '961776991058',
|
||||
projectId: 'solian-0x001',
|
||||
authDomain: 'solian-0x001.firebaseapp.com',
|
||||
storageBucket: 'solian-0x001.firebasestorage.app',
|
||||
measurementId: 'G-XY3HHKG0PE',
|
||||
measurementId: 'G-JD1YEG9D6F',
|
||||
);
|
||||
|
||||
static const FirebaseOptions android = FirebaseOptions(
|
||||
@@ -64,6 +61,10 @@ class DefaultFirebaseOptions {
|
||||
messagingSenderId: '961776991058',
|
||||
projectId: 'solian-0x001',
|
||||
storageBucket: 'solian-0x001.firebasestorage.app',
|
||||
androidClientId:
|
||||
'961776991058-r4iv9qoio57ul7utbfpgfrda2etvtch8.apps.googleusercontent.com',
|
||||
iosClientId:
|
||||
'961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com',
|
||||
iosBundleId: 'dev.solsynth.solian',
|
||||
);
|
||||
|
||||
@@ -73,6 +74,10 @@ class DefaultFirebaseOptions {
|
||||
messagingSenderId: '961776991058',
|
||||
projectId: 'solian-0x001',
|
||||
storageBucket: 'solian-0x001.firebasestorage.app',
|
||||
androidClientId:
|
||||
'961776991058-r4iv9qoio57ul7utbfpgfrda2etvtch8.apps.googleusercontent.com',
|
||||
iosClientId:
|
||||
'961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com',
|
||||
iosBundleId: 'dev.solsynth.solian',
|
||||
);
|
||||
|
||||
@@ -85,5 +90,4 @@ class DefaultFirebaseOptions {
|
||||
storageBucket: 'solian-0x001.firebasestorage.app',
|
||||
measurementId: 'G-JD1YEG9D6F',
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,7 @@ import 'package:image_picker_platform_interface/image_picker_platform_interface.
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect;
|
||||
import 'package:island/services/update_service.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
@@ -53,10 +54,16 @@ void main() async {
|
||||
try {
|
||||
await langdetect.initLangDetect();
|
||||
await EasyLocalization.ensureInitialized();
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
|
||||
|
||||
if (kIsWeb || !Platform.isLinux) {
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
FirebaseMessaging.onBackgroundMessage(
|
||||
_firebaseMessagingBackgroundHandler,
|
||||
);
|
||||
}
|
||||
|
||||
log("[SplashScreen] Firebase is ready!");
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
@@ -137,6 +144,15 @@ void main() async {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Schedule update check shortly after startup, when a context is available.
|
||||
// Uses the global overlay key to obtain a BuildContext safely.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final ctx = globalOverlay.currentContext;
|
||||
if (ctx != null) {
|
||||
UpdateService().checkForUpdates(ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Router will be provided through Riverpod
|
||||
|
@@ -36,8 +36,8 @@ sealed class SnPost with _$SnPost {
|
||||
@Default({}) Map<String, int> reactionsCount,
|
||||
@Default({}) Map<String, bool> reactionsMade,
|
||||
@Default([]) List<dynamic> reactions,
|
||||
@Default([]) List<PostTag> tags,
|
||||
@Default([]) List<PostCategory> categories,
|
||||
@Default([]) List<SnPostTag> tags,
|
||||
@Default([]) List<SnPostCategory> categories,
|
||||
@Default([]) List<dynamic> collections,
|
||||
@Default(null) DateTime? createdAt,
|
||||
@Default(null) DateTime? updatedAt,
|
||||
|
@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$SnPost {
|
||||
|
||||
String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; Map<String, bool> get reactionsMade; List<dynamic> get reactions; List<PostTag> get tags; List<PostCategory> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated;
|
||||
String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; Map<String, bool> get reactionsMade; List<dynamic> get reactions; List<SnPostTag> get tags; List<SnPostCategory> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated;
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -48,7 +48,7 @@ abstract mixin class $SnPostCopyWith<$Res> {
|
||||
factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
|
||||
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
|
||||
});
|
||||
|
||||
|
||||
@@ -94,8 +94,8 @@ as SnPublisher,reactionsCount: null == reactionsCount ? _self.reactionsCount : r
|
||||
as Map<String, int>,reactionsMade: null == reactionsMade ? _self.reactionsMade : reactionsMade // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, bool>,reactions: null == reactions ? _self.reactions : reactions // ignore: cast_nullable_to_non_nullable
|
||||
as List<dynamic>,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
|
||||
as List<PostTag>,categories: null == categories ? _self.categories : categories // ignore: cast_nullable_to_non_nullable
|
||||
as List<PostCategory>,collections: null == collections ? _self.collections : collections // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnPostTag>,categories: null == categories ? _self.categories : categories // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnPostCategory>,collections: null == collections ? _self.collections : collections // ignore: cast_nullable_to_non_nullable
|
||||
as List<dynamic>,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
@@ -227,7 +227,7 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnPost() when $default != null:
|
||||
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _:
|
||||
@@ -248,7 +248,7 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnPost():
|
||||
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);}
|
||||
@@ -265,7 +265,7 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnPost() when $default != null:
|
||||
return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _:
|
||||
@@ -280,7 +280,7 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnPost implements SnPost {
|
||||
const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.type = 0, final Map<String, dynamic>? meta, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, final List<SnCloudFile> attachments = const [], required this.publisher, final Map<String, int> reactionsCount = const {}, final Map<String, bool> reactionsMade = const {}, final List<dynamic> reactions = const [], final List<PostTag> tags = const [], final List<PostCategory> categories = const [], final List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactionsMade = reactionsMade,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections;
|
||||
const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.type = 0, final Map<String, dynamic>? meta, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, final List<SnCloudFile> attachments = const [], required this.publisher, final Map<String, int> reactionsCount = const {}, final Map<String, bool> reactionsMade = const {}, final List<dynamic> reactions = const [], final List<SnPostTag> tags = const [], final List<SnPostCategory> categories = const [], final List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactionsMade = reactionsMade,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections;
|
||||
factory _SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@@ -341,15 +341,15 @@ class _SnPost implements SnPost {
|
||||
return EqualUnmodifiableListView(_reactions);
|
||||
}
|
||||
|
||||
final List<PostTag> _tags;
|
||||
@override@JsonKey() List<PostTag> get tags {
|
||||
final List<SnPostTag> _tags;
|
||||
@override@JsonKey() List<SnPostTag> get tags {
|
||||
if (_tags is EqualUnmodifiableListView) return _tags;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_tags);
|
||||
}
|
||||
|
||||
final List<PostCategory> _categories;
|
||||
@override@JsonKey() List<PostCategory> get categories {
|
||||
final List<SnPostCategory> _categories;
|
||||
@override@JsonKey() List<SnPostCategory> get categories {
|
||||
if (_categories is EqualUnmodifiableListView) return _categories;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_categories);
|
||||
@@ -400,7 +400,7 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> {
|
||||
factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<PostTag> tags, List<PostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
|
||||
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
|
||||
});
|
||||
|
||||
|
||||
@@ -446,8 +446,8 @@ as SnPublisher,reactionsCount: null == reactionsCount ? _self._reactionsCount :
|
||||
as Map<String, int>,reactionsMade: null == reactionsMade ? _self._reactionsMade : reactionsMade // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, bool>,reactions: null == reactions ? _self._reactions : reactions // ignore: cast_nullable_to_non_nullable
|
||||
as List<dynamic>,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable
|
||||
as List<PostTag>,categories: null == categories ? _self._categories : categories // ignore: cast_nullable_to_non_nullable
|
||||
as List<PostCategory>,collections: null == collections ? _self._collections : collections // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnPostTag>,categories: null == categories ? _self._categories : categories // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnPostCategory>,collections: null == collections ? _self._collections : collections // ignore: cast_nullable_to_non_nullable
|
||||
as List<dynamic>,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
|
@@ -62,12 +62,12 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
|
||||
reactions: json['reactions'] as List<dynamic>? ?? const [],
|
||||
tags:
|
||||
(json['tags'] as List<dynamic>?)
|
||||
?.map((e) => PostTag.fromJson(e as Map<String, dynamic>))
|
||||
?.map((e) => SnPostTag.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
categories:
|
||||
(json['categories'] as List<dynamic>?)
|
||||
?.map((e) => PostCategory.fromJson(e as Map<String, dynamic>))
|
||||
?.map((e) => SnPostCategory.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
collections: json['collections'] as List<dynamic>? ?? const [],
|
||||
|
@@ -1,19 +1,30 @@
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/services/text.dart';
|
||||
|
||||
part 'post_category.freezed.dart';
|
||||
part 'post_category.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class PostCategory with _$PostCategory {
|
||||
const factory PostCategory({
|
||||
sealed class SnPostCategory with _$SnPostCategory {
|
||||
const SnPostCategory._();
|
||||
|
||||
const factory SnPostCategory({
|
||||
required String id,
|
||||
required String slug,
|
||||
String? name,
|
||||
@Default([]) List<SnPost> posts,
|
||||
}) = _PostCategory;
|
||||
}) = _SnPostCategory;
|
||||
|
||||
factory PostCategory.fromJson(Map<String, dynamic> json) =>
|
||||
_$PostCategoryFromJson(json);
|
||||
factory SnPostCategory.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnPostCategoryFromJson(json);
|
||||
|
||||
String get categoryDisplayTitle {
|
||||
final capitalizedSlug = slug.capitalizeEachWord();
|
||||
if ('postCategory$capitalizedSlug'.trExists()) {
|
||||
return 'postCategory$capitalizedSlug'.tr();
|
||||
}
|
||||
return name ?? slug;
|
||||
}
|
||||
}
|
||||
|
@@ -13,22 +13,22 @@ part of 'post_category.dart';
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$PostCategory {
|
||||
mixin _$SnPostCategory {
|
||||
|
||||
String get id; String get slug; String? get name; List<SnPost> get posts;
|
||||
/// Create a copy of PostCategory
|
||||
/// Create a copy of SnPostCategory
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$PostCategoryCopyWith<PostCategory> get copyWith => _$PostCategoryCopyWithImpl<PostCategory>(this as PostCategory, _$identity);
|
||||
$SnPostCategoryCopyWith<SnPostCategory> get copyWith => _$SnPostCategoryCopyWithImpl<SnPostCategory>(this as SnPostCategory, _$identity);
|
||||
|
||||
/// Serializes this PostCategory to a JSON map.
|
||||
/// Serializes this SnPostCategory to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is PostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -37,15 +37,15 @@ int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEqu
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PostCategory(id: $id, slug: $slug, name: $name, posts: $posts)';
|
||||
return 'SnPostCategory(id: $id, slug: $slug, name: $name, posts: $posts)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $PostCategoryCopyWith<$Res> {
|
||||
factory $PostCategoryCopyWith(PostCategory value, $Res Function(PostCategory) _then) = _$PostCategoryCopyWithImpl;
|
||||
abstract mixin class $SnPostCategoryCopyWith<$Res> {
|
||||
factory $SnPostCategoryCopyWith(SnPostCategory value, $Res Function(SnPostCategory) _then) = _$SnPostCategoryCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String slug, String? name, List<SnPost> posts
|
||||
@@ -56,14 +56,14 @@ $Res call({
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$PostCategoryCopyWithImpl<$Res>
|
||||
implements $PostCategoryCopyWith<$Res> {
|
||||
_$PostCategoryCopyWithImpl(this._self, this._then);
|
||||
class _$SnPostCategoryCopyWithImpl<$Res>
|
||||
implements $SnPostCategoryCopyWith<$Res> {
|
||||
_$SnPostCategoryCopyWithImpl(this._self, this._then);
|
||||
|
||||
final PostCategory _self;
|
||||
final $Res Function(PostCategory) _then;
|
||||
final SnPostCategory _self;
|
||||
final $Res Function(SnPostCategory) _then;
|
||||
|
||||
/// Create a copy of PostCategory
|
||||
/// Create a copy of SnPostCategory
|
||||
/// 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? name = freezed,Object? posts = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
@@ -78,8 +78,8 @@ as List<SnPost>,
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [PostCategory].
|
||||
extension PostCategoryPatterns on PostCategory {
|
||||
/// Adds pattern-matching-related methods to [SnPostCategory].
|
||||
extension SnPostCategoryPatterns on SnPostCategory {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
@@ -92,10 +92,10 @@ extension PostCategoryPatterns on PostCategory {
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _PostCategory value)? $default,{required TResult orElse(),}){
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnPostCategory value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PostCategory() when $default != null:
|
||||
case _SnPostCategory() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
@@ -114,10 +114,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _PostCategory value) $default,){
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnPostCategory value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PostCategory():
|
||||
case _SnPostCategory():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
@@ -132,10 +132,10 @@ return $default(_that);}
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _PostCategory value)? $default,){
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnPostCategory value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PostCategory() when $default != null:
|
||||
case _SnPostCategory() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
@@ -155,7 +155,7 @@ return $default(_that);case _:
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PostCategory() when $default != null:
|
||||
case _SnPostCategory() when $default != null:
|
||||
return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
|
||||
return orElse();
|
||||
|
||||
@@ -176,7 +176,7 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PostCategory():
|
||||
case _SnPostCategory():
|
||||
return $default(_that.id,_that.slug,_that.name,_that.posts);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
@@ -193,7 +193,7 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);}
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, String? name, List<SnPost> posts)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PostCategory() when $default != null:
|
||||
case _SnPostCategory() when $default != null:
|
||||
return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
|
||||
return null;
|
||||
|
||||
@@ -205,9 +205,9 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _PostCategory implements PostCategory {
|
||||
const _PostCategory({required this.id, required this.slug, this.name, final List<SnPost> posts = const []}): _posts = posts;
|
||||
factory _PostCategory.fromJson(Map<String, dynamic> json) => _$PostCategoryFromJson(json);
|
||||
class _SnPostCategory extends SnPostCategory {
|
||||
const _SnPostCategory({required this.id, required this.slug, this.name, final List<SnPost> posts = const []}): _posts = posts,super._();
|
||||
factory _SnPostCategory.fromJson(Map<String, dynamic> json) => _$SnPostCategoryFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final String slug;
|
||||
@@ -220,20 +220,20 @@ class _PostCategory implements PostCategory {
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of PostCategory
|
||||
/// Create a copy of SnPostCategory
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$PostCategoryCopyWith<_PostCategory> get copyWith => __$PostCategoryCopyWithImpl<_PostCategory>(this, _$identity);
|
||||
_$SnPostCategoryCopyWith<_SnPostCategory> get copyWith => __$SnPostCategoryCopyWithImpl<_SnPostCategory>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$PostCategoryToJson(this, );
|
||||
return _$SnPostCategoryToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPostCategory&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -242,15 +242,15 @@ int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEqu
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PostCategory(id: $id, slug: $slug, name: $name, posts: $posts)';
|
||||
return 'SnPostCategory(id: $id, slug: $slug, name: $name, posts: $posts)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$PostCategoryCopyWith<$Res> implements $PostCategoryCopyWith<$Res> {
|
||||
factory _$PostCategoryCopyWith(_PostCategory value, $Res Function(_PostCategory) _then) = __$PostCategoryCopyWithImpl;
|
||||
abstract mixin class _$SnPostCategoryCopyWith<$Res> implements $SnPostCategoryCopyWith<$Res> {
|
||||
factory _$SnPostCategoryCopyWith(_SnPostCategory value, $Res Function(_SnPostCategory) _then) = __$SnPostCategoryCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String slug, String? name, List<SnPost> posts
|
||||
@@ -261,17 +261,17 @@ $Res call({
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$PostCategoryCopyWithImpl<$Res>
|
||||
implements _$PostCategoryCopyWith<$Res> {
|
||||
__$PostCategoryCopyWithImpl(this._self, this._then);
|
||||
class __$SnPostCategoryCopyWithImpl<$Res>
|
||||
implements _$SnPostCategoryCopyWith<$Res> {
|
||||
__$SnPostCategoryCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _PostCategory _self;
|
||||
final $Res Function(_PostCategory) _then;
|
||||
final _SnPostCategory _self;
|
||||
final $Res Function(_SnPostCategory) _then;
|
||||
|
||||
/// Create a copy of PostCategory
|
||||
/// Create a copy of SnPostCategory
|
||||
/// 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? name = freezed,Object? posts = null,}) {
|
||||
return _then(_PostCategory(
|
||||
return _then(_SnPostCategory(
|
||||
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,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
|
@@ -6,8 +6,8 @@ part of 'post_category.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_PostCategory _$PostCategoryFromJson(Map<String, dynamic> json) =>
|
||||
_PostCategory(
|
||||
_SnPostCategory _$SnPostCategoryFromJson(Map<String, dynamic> json) =>
|
||||
_SnPostCategory(
|
||||
id: json['id'] as String,
|
||||
slug: json['slug'] as String,
|
||||
name: json['name'] as String?,
|
||||
@@ -18,7 +18,7 @@ _PostCategory _$PostCategoryFromJson(Map<String, dynamic> json) =>
|
||||
const [],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PostCategoryToJson(_PostCategory instance) =>
|
||||
Map<String, dynamic> _$SnPostCategoryToJson(_SnPostCategory instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'slug': instance.slug,
|
||||
|
@@ -1,4 +1,3 @@
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
|
||||
@@ -6,14 +5,14 @@ part 'post_tag.freezed.dart';
|
||||
part 'post_tag.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class PostTag with _$PostTag {
|
||||
const factory PostTag({
|
||||
sealed class SnPostTag with _$SnPostTag {
|
||||
const factory SnPostTag({
|
||||
required String id,
|
||||
required String slug,
|
||||
String? name,
|
||||
@Default([]) List<SnPost> posts,
|
||||
}) = _PostTag;
|
||||
}) = _SnPostTag;
|
||||
|
||||
factory PostTag.fromJson(Map<String, dynamic> json) =>
|
||||
_$PostTagFromJson(json);
|
||||
factory SnPostTag.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnPostTagFromJson(json);
|
||||
}
|
||||
|
@@ -13,22 +13,22 @@ part of 'post_tag.dart';
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$PostTag {
|
||||
mixin _$SnPostTag {
|
||||
|
||||
String get id; String get slug; String? get name; List<SnPost> get posts;
|
||||
/// Create a copy of PostTag
|
||||
/// Create a copy of SnPostTag
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$PostTagCopyWith<PostTag> get copyWith => _$PostTagCopyWithImpl<PostTag>(this as PostTag, _$identity);
|
||||
$SnPostTagCopyWith<SnPostTag> get copyWith => _$SnPostTagCopyWithImpl<SnPostTag>(this as SnPostTag, _$identity);
|
||||
|
||||
/// Serializes this PostTag to a JSON map.
|
||||
/// Serializes this SnPostTag to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is PostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other.posts, posts));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -37,15 +37,15 @@ int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEqu
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PostTag(id: $id, slug: $slug, name: $name, posts: $posts)';
|
||||
return 'SnPostTag(id: $id, slug: $slug, name: $name, posts: $posts)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $PostTagCopyWith<$Res> {
|
||||
factory $PostTagCopyWith(PostTag value, $Res Function(PostTag) _then) = _$PostTagCopyWithImpl;
|
||||
abstract mixin class $SnPostTagCopyWith<$Res> {
|
||||
factory $SnPostTagCopyWith(SnPostTag value, $Res Function(SnPostTag) _then) = _$SnPostTagCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String slug, String? name, List<SnPost> posts
|
||||
@@ -56,14 +56,14 @@ $Res call({
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$PostTagCopyWithImpl<$Res>
|
||||
implements $PostTagCopyWith<$Res> {
|
||||
_$PostTagCopyWithImpl(this._self, this._then);
|
||||
class _$SnPostTagCopyWithImpl<$Res>
|
||||
implements $SnPostTagCopyWith<$Res> {
|
||||
_$SnPostTagCopyWithImpl(this._self, this._then);
|
||||
|
||||
final PostTag _self;
|
||||
final $Res Function(PostTag) _then;
|
||||
final SnPostTag _self;
|
||||
final $Res Function(SnPostTag) _then;
|
||||
|
||||
/// Create a copy of PostTag
|
||||
/// Create a copy of SnPostTag
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = freezed,Object? posts = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
@@ -78,8 +78,8 @@ as List<SnPost>,
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [PostTag].
|
||||
extension PostTagPatterns on PostTag {
|
||||
/// Adds pattern-matching-related methods to [SnPostTag].
|
||||
extension SnPostTagPatterns on SnPostTag {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
@@ -92,10 +92,10 @@ extension PostTagPatterns on PostTag {
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _PostTag value)? $default,{required TResult orElse(),}){
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnPostTag value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PostTag() when $default != null:
|
||||
case _SnPostTag() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
@@ -114,10 +114,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _PostTag value) $default,){
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnPostTag value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PostTag():
|
||||
case _SnPostTag():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
@@ -132,10 +132,10 @@ return $default(_that);}
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _PostTag value)? $default,){
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnPostTag value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _PostTag() when $default != null:
|
||||
case _SnPostTag() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
@@ -155,7 +155,7 @@ return $default(_that);case _:
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PostTag() when $default != null:
|
||||
case _SnPostTag() when $default != null:
|
||||
return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
|
||||
return orElse();
|
||||
|
||||
@@ -176,7 +176,7 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, String? name, List<SnPost> posts) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PostTag():
|
||||
case _SnPostTag():
|
||||
return $default(_that.id,_that.slug,_that.name,_that.posts);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
@@ -193,7 +193,7 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);}
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, String? name, List<SnPost> posts)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _PostTag() when $default != null:
|
||||
case _SnPostTag() when $default != null:
|
||||
return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
|
||||
return null;
|
||||
|
||||
@@ -205,9 +205,9 @@ return $default(_that.id,_that.slug,_that.name,_that.posts);case _:
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _PostTag implements PostTag {
|
||||
const _PostTag({required this.id, required this.slug, this.name, final List<SnPost> posts = const []}): _posts = posts;
|
||||
factory _PostTag.fromJson(Map<String, dynamic> json) => _$PostTagFromJson(json);
|
||||
class _SnPostTag implements SnPostTag {
|
||||
const _SnPostTag({required this.id, required this.slug, this.name, final List<SnPost> posts = const []}): _posts = posts;
|
||||
factory _SnPostTag.fromJson(Map<String, dynamic> json) => _$SnPostTagFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final String slug;
|
||||
@@ -220,20 +220,20 @@ class _PostTag implements PostTag {
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of PostTag
|
||||
/// Create a copy of SnPostTag
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$PostTagCopyWith<_PostTag> get copyWith => __$PostTagCopyWithImpl<_PostTag>(this, _$identity);
|
||||
_$SnPostTagCopyWith<_SnPostTag> get copyWith => __$SnPostTagCopyWithImpl<_SnPostTag>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$PostTagToJson(this, );
|
||||
return _$SnPostTagToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPostTag&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&const DeepCollectionEquality().equals(other._posts, _posts));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -242,15 +242,15 @@ int get hashCode => Object.hash(runtimeType,id,slug,name,const DeepCollectionEqu
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PostTag(id: $id, slug: $slug, name: $name, posts: $posts)';
|
||||
return 'SnPostTag(id: $id, slug: $slug, name: $name, posts: $posts)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$PostTagCopyWith<$Res> implements $PostTagCopyWith<$Res> {
|
||||
factory _$PostTagCopyWith(_PostTag value, $Res Function(_PostTag) _then) = __$PostTagCopyWithImpl;
|
||||
abstract mixin class _$SnPostTagCopyWith<$Res> implements $SnPostTagCopyWith<$Res> {
|
||||
factory _$SnPostTagCopyWith(_SnPostTag value, $Res Function(_SnPostTag) _then) = __$SnPostTagCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String slug, String? name, List<SnPost> posts
|
||||
@@ -261,17 +261,17 @@ $Res call({
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$PostTagCopyWithImpl<$Res>
|
||||
implements _$PostTagCopyWith<$Res> {
|
||||
__$PostTagCopyWithImpl(this._self, this._then);
|
||||
class __$SnPostTagCopyWithImpl<$Res>
|
||||
implements _$SnPostTagCopyWith<$Res> {
|
||||
__$SnPostTagCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _PostTag _self;
|
||||
final $Res Function(_PostTag) _then;
|
||||
final _SnPostTag _self;
|
||||
final $Res Function(_SnPostTag) _then;
|
||||
|
||||
/// Create a copy of PostTag
|
||||
/// Create a copy of SnPostTag
|
||||
/// 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? name = freezed,Object? posts = null,}) {
|
||||
return _then(_PostTag(
|
||||
return _then(_SnPostTag(
|
||||
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,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||
|
@@ -6,7 +6,7 @@ part of 'post_tag.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_PostTag _$PostTagFromJson(Map<String, dynamic> json) => _PostTag(
|
||||
_SnPostTag _$SnPostTagFromJson(Map<String, dynamic> json) => _SnPostTag(
|
||||
id: json['id'] as String,
|
||||
slug: json['slug'] as String,
|
||||
name: json['name'] as String?,
|
||||
@@ -17,9 +17,10 @@ _PostTag _$PostTagFromJson(Map<String, dynamic> json) => _PostTag(
|
||||
const [],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PostTagToJson(_PostTag instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'slug': instance.slug,
|
||||
'name': instance.name,
|
||||
'posts': instance.posts.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
Map<String, dynamic> _$SnPostTagToJson(_SnPostTag instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'slug': instance.slug,
|
||||
'name': instance.name,
|
||||
'posts': instance.posts.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
@@ -38,6 +38,7 @@ sealed class SnAccountProfile with _$SnAccountProfile {
|
||||
@Default('') String location,
|
||||
@Default('') String timeZone,
|
||||
DateTime? birthday,
|
||||
@Default({}) Map<String, String> links,
|
||||
DateTime? lastSeenAt,
|
||||
SnAccountBadge? activeBadge,
|
||||
required int experience,
|
||||
|
@@ -350,7 +350,7 @@ $SnWalletSubscriptionRefCopyWith<$Res>? get perkSubscription {
|
||||
/// @nodoc
|
||||
mixin _$SnAccountProfile {
|
||||
|
||||
String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday; Map<String, String> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
/// Create a copy of SnAccountProfile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -363,16 +363,16 @@ $SnAccountProfileCopyWith<SnAccountProfile> get copyWith => _$SnAccountProfileCo
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other.links, links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(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.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
|
||||
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(links),lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
@@ -383,7 +383,7 @@ abstract mixin class $SnAccountProfileCopyWith<$Res> {
|
||||
factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@@ -400,7 +400,7 @@ class _$SnAccountProfileCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnAccountProfile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = 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,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
|
||||
@@ -412,7 +412,8 @@ as String,pronouns: null == pronouns ? _self.pronouns : pronouns // ignore: cast
|
||||
as String,location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
|
||||
as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable
|
||||
as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,links: null == links ? _self.links : links // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, String>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable
|
||||
as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
|
||||
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable
|
||||
@@ -553,10 +554,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAccountProfile() when $default != null:
|
||||
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -574,10 +575,10 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAccountProfile():
|
||||
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
@@ -591,10 +592,10 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAccountProfile() when $default != null:
|
||||
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -606,7 +607,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnAccountProfile implements SnAccountProfile {
|
||||
const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, this.lastSeenAt, this.activeBadge, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt});
|
||||
const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, final Map<String, String> links = const {}, this.lastSeenAt, this.activeBadge, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links;
|
||||
factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@@ -619,6 +620,13 @@ class _SnAccountProfile implements SnAccountProfile {
|
||||
@override@JsonKey() final String location;
|
||||
@override@JsonKey() final String timeZone;
|
||||
@override final DateTime? birthday;
|
||||
final Map<String, String> _links;
|
||||
@override@JsonKey() Map<String, String> get links {
|
||||
if (_links is EqualUnmodifiableMapView) return _links;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(_links);
|
||||
}
|
||||
|
||||
@override final DateTime? lastSeenAt;
|
||||
@override final SnAccountBadge? activeBadge;
|
||||
@override final int experience;
|
||||
@@ -644,16 +652,16 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other._links, _links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(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.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
|
||||
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(_links),lastSeenAt,activeBadge,experience,level,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
@@ -664,7 +672,7 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfi
|
||||
factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@@ -681,7 +689,7 @@ class __$SnAccountProfileCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnAccountProfile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnAccountProfile(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
|
||||
@@ -693,7 +701,8 @@ as String,pronouns: null == pronouns ? _self.pronouns : pronouns // ignore: cast
|
||||
as String,location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable
|
||||
as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable
|
||||
as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,links: null == links ? _self._links : links // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, String>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable
|
||||
as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
|
||||
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable
|
||||
|
@@ -62,6 +62,11 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
|
||||
json['birthday'] == null
|
||||
? null
|
||||
: DateTime.parse(json['birthday'] as String),
|
||||
links:
|
||||
(json['links'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, e as String),
|
||||
) ??
|
||||
const {},
|
||||
lastSeenAt:
|
||||
json['last_seen_at'] == null
|
||||
? null
|
||||
@@ -111,6 +116,7 @@ Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
|
||||
'location': instance.location,
|
||||
'time_zone': instance.timeZone,
|
||||
'birthday': instance.birthday?.toIso8601String(),
|
||||
'links': instance.links,
|
||||
'last_seen_at': instance.lastSeenAt?.toIso8601String(),
|
||||
'active_badge': instance.activeBadge?.toJson(),
|
||||
'experience': instance.experience,
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@@ -18,6 +17,8 @@ sealed class WebSocketState with _$WebSocketState {
|
||||
const factory WebSocketState.connected() = _Connected;
|
||||
const factory WebSocketState.connecting() = _Connecting;
|
||||
const factory WebSocketState.disconnected() = _Disconnected;
|
||||
const factory WebSocketState.serverDown() = _ServerDown;
|
||||
const factory WebSocketState.duplicateDevice() = _DuplicateDevice;
|
||||
const factory WebSocketState.error(String message) = _Error;
|
||||
}
|
||||
|
||||
@@ -49,7 +50,7 @@ class WebSocketService {
|
||||
Timer? _heartbeatTimer;
|
||||
|
||||
DateTime? _heartbeatAt;
|
||||
Duration? _heartbeatDelay;
|
||||
Duration? heartbeatDelay;
|
||||
|
||||
Stream<WebSocketPacket> get dataStream => _streamController.stream;
|
||||
Stream<WebSocketState> get statusStream => _statusStreamController.stream;
|
||||
@@ -81,15 +82,20 @@ class WebSocketService {
|
||||
final dataStr =
|
||||
data is Uint8List ? utf8.decode(data) : data.toString();
|
||||
final packet = WebSocketPacket.fromJson(jsonDecode(dataStr));
|
||||
if (packet.type == 'error.dupe') {
|
||||
_statusStreamController.sink.add(WebSocketState.duplicateDevice());
|
||||
_channel!.sink.close();
|
||||
return;
|
||||
}
|
||||
_streamController.sink.add(packet);
|
||||
log(
|
||||
"[WebSocket] Received packet: ${packet.type} ${packet.errorMessage}",
|
||||
);
|
||||
if (packet.type == 'pong' && _heartbeatAt != null) {
|
||||
var now = DateTime.now();
|
||||
_heartbeatDelay = now.difference(_heartbeatAt!);
|
||||
heartbeatDelay = now.difference(_heartbeatAt!);
|
||||
log(
|
||||
"[WebSocket] Server respond last heartbeat for ${_heartbeatDelay!.inMilliseconds} ms",
|
||||
"[WebSocket] Server respond last heartbeat for ${heartbeatDelay!.inMilliseconds} ms",
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@@ -61,13 +61,15 @@ extension WebSocketStatePatterns on WebSocketState {
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( _Connected value)? connected,TResult Function( _Connecting value)? connecting,TResult Function( _Disconnected value)? disconnected,TResult Function( _Error value)? error,required TResult orElse(),}){
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( _Connected value)? connected,TResult Function( _Connecting value)? connecting,TResult Function( _Disconnected value)? disconnected,TResult Function( _ServerDown value)? serverDown,TResult Function( _DuplicateDevice value)? duplicateDevice,TResult Function( _Error value)? error,required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _Connected() when connected != null:
|
||||
return connected(_that);case _Connecting() when connecting != null:
|
||||
return connecting(_that);case _Disconnected() when disconnected != null:
|
||||
return disconnected(_that);case _Error() when error != null:
|
||||
return disconnected(_that);case _ServerDown() when serverDown != null:
|
||||
return serverDown(_that);case _DuplicateDevice() when duplicateDevice != null:
|
||||
return duplicateDevice(_that);case _Error() when error != null:
|
||||
return error(_that);case _:
|
||||
return orElse();
|
||||
|
||||
@@ -86,13 +88,15 @@ return error(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( _Connected value) connected,required TResult Function( _Connecting value) connecting,required TResult Function( _Disconnected value) disconnected,required TResult Function( _Error value) error,}){
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( _Connected value) connected,required TResult Function( _Connecting value) connecting,required TResult Function( _Disconnected value) disconnected,required TResult Function( _ServerDown value) serverDown,required TResult Function( _DuplicateDevice value) duplicateDevice,required TResult Function( _Error value) error,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _Connected():
|
||||
return connected(_that);case _Connecting():
|
||||
return connecting(_that);case _Disconnected():
|
||||
return disconnected(_that);case _Error():
|
||||
return disconnected(_that);case _ServerDown():
|
||||
return serverDown(_that);case _DuplicateDevice():
|
||||
return duplicateDevice(_that);case _Error():
|
||||
return error(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
@@ -107,13 +111,15 @@ return error(_that);}
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( _Connected value)? connected,TResult? Function( _Connecting value)? connecting,TResult? Function( _Disconnected value)? disconnected,TResult? Function( _Error value)? error,}){
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( _Connected value)? connected,TResult? Function( _Connecting value)? connecting,TResult? Function( _Disconnected value)? disconnected,TResult? Function( _ServerDown value)? serverDown,TResult? Function( _DuplicateDevice value)? duplicateDevice,TResult? Function( _Error value)? error,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _Connected() when connected != null:
|
||||
return connected(_that);case _Connecting() when connecting != null:
|
||||
return connecting(_that);case _Disconnected() when disconnected != null:
|
||||
return disconnected(_that);case _Error() when error != null:
|
||||
return disconnected(_that);case _ServerDown() when serverDown != null:
|
||||
return serverDown(_that);case _DuplicateDevice() when duplicateDevice != null:
|
||||
return duplicateDevice(_that);case _Error() when error != null:
|
||||
return error(_that);case _:
|
||||
return null;
|
||||
|
||||
@@ -131,12 +137,14 @@ return error(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function()? connected,TResult Function()? connecting,TResult Function()? disconnected,TResult Function( String message)? error,required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function()? connected,TResult Function()? connecting,TResult Function()? disconnected,TResult Function()? serverDown,TResult Function()? duplicateDevice,TResult Function( String message)? error,required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Connected() when connected != null:
|
||||
return connected();case _Connecting() when connecting != null:
|
||||
return connecting();case _Disconnected() when disconnected != null:
|
||||
return disconnected();case _Error() when error != null:
|
||||
return disconnected();case _ServerDown() when serverDown != null:
|
||||
return serverDown();case _DuplicateDevice() when duplicateDevice != null:
|
||||
return duplicateDevice();case _Error() when error != null:
|
||||
return error(_that.message);case _:
|
||||
return orElse();
|
||||
|
||||
@@ -155,12 +163,14 @@ return error(_that.message);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function() connected,required TResult Function() connecting,required TResult Function() disconnected,required TResult Function( String message) error,}) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function() connected,required TResult Function() connecting,required TResult Function() disconnected,required TResult Function() serverDown,required TResult Function() duplicateDevice,required TResult Function( String message) error,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Connected():
|
||||
return connected();case _Connecting():
|
||||
return connecting();case _Disconnected():
|
||||
return disconnected();case _Error():
|
||||
return disconnected();case _ServerDown():
|
||||
return serverDown();case _DuplicateDevice():
|
||||
return duplicateDevice();case _Error():
|
||||
return error(_that.message);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
@@ -175,12 +185,14 @@ return error(_that.message);}
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function()? connected,TResult? Function()? connecting,TResult? Function()? disconnected,TResult? Function( String message)? error,}) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function()? connected,TResult? Function()? connecting,TResult? Function()? disconnected,TResult? Function()? serverDown,TResult? Function()? duplicateDevice,TResult? Function( String message)? error,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Connected() when connected != null:
|
||||
return connected();case _Connecting() when connecting != null:
|
||||
return connecting();case _Disconnected() when disconnected != null:
|
||||
return disconnected();case _Error() when error != null:
|
||||
return disconnected();case _ServerDown() when serverDown != null:
|
||||
return serverDown();case _DuplicateDevice() when duplicateDevice != null:
|
||||
return duplicateDevice();case _Error() when error != null:
|
||||
return error(_that.message);case _:
|
||||
return null;
|
||||
|
||||
@@ -303,6 +315,82 @@ String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||
|
||||
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _ServerDown with DiagnosticableTreeMixin implements WebSocketState {
|
||||
const _ServerDown();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'WebSocketState.serverDown'))
|
||||
;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerDown);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||
return 'WebSocketState.serverDown()';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _DuplicateDevice with DiagnosticableTreeMixin implements WebSocketState {
|
||||
const _DuplicateDevice();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'WebSocketState.duplicateDevice'))
|
||||
;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DuplicateDevice);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||
return 'WebSocketState.duplicateDevice()';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
|
@@ -7,6 +7,7 @@ import 'package:island/screens/developers/edit_app.dart';
|
||||
import 'package:island/screens/developers/new_app.dart';
|
||||
import 'package:island/screens/developers/hub.dart';
|
||||
import 'package:island/screens/discovery/articles.dart';
|
||||
import 'package:island/screens/posts/post_category_detail.dart';
|
||||
import 'package:island/screens/posts/post_search.dart';
|
||||
import 'package:island/widgets/app_wrapper.dart';
|
||||
import 'package:island/screens/tabs.dart';
|
||||
@@ -322,15 +323,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
builder: (context, state) => const AboutScreen(),
|
||||
),
|
||||
|
||||
GoRoute(
|
||||
name: 'reportDetail',
|
||||
path: '/safety/reports/me/:id',
|
||||
builder: (context, state) {
|
||||
final id = state.pathParameters['id']!;
|
||||
return AbuseReportDetailScreen(reportId: id);
|
||||
},
|
||||
),
|
||||
|
||||
// Main tabs with TabsScreen shell
|
||||
ShellRoute(
|
||||
navigatorKey: _tabsShellKey,
|
||||
@@ -357,6 +349,25 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
return PostDetailScreen(id: id);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'postCategoryDetail',
|
||||
path: '/posts/categories/:slug',
|
||||
builder: (context, state) {
|
||||
final slug = state.pathParameters['slug']!;
|
||||
return PostCategoryDetailScreen(slug: slug, isCategory: true);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'postTagDetail',
|
||||
path: '/posts/tags/:slug',
|
||||
builder: (context, state) {
|
||||
final slug = state.pathParameters['slug']!;
|
||||
return PostCategoryDetailScreen(
|
||||
slug: slug,
|
||||
isCategory: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'publisherProfile',
|
||||
path: '/publishers/:name',
|
||||
@@ -505,6 +516,14 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
path: '/safety/reports/me',
|
||||
builder: (context, state) => const AbuseReportListScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'reportDetail',
|
||||
path: '/safety/reports/me/:id',
|
||||
builder: (context, state) {
|
||||
final id = state.pathParameters['id']!;
|
||||
return AbuseReportDetailScreen(reportId: id);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
|
@@ -11,6 +11,8 @@ import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:island/services/update_service.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
@@ -100,196 +102,243 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _errorMessage != null
|
||||
? Center(child: Text(_errorMessage!))
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
// App Icon and Name
|
||||
CircleAvatar(
|
||||
radius: 50,
|
||||
backgroundColor: theme.colorScheme.primary.withOpacity(
|
||||
0.1,
|
||||
),
|
||||
child: Image.asset(
|
||||
'assets/icons/icon.png',
|
||||
width: 56,
|
||||
height: 56,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_packageInfo.appName,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'aboutScreenVersionInfo'.tr(
|
||||
args: [_packageInfo.version, _packageInfo.buildNumber],
|
||||
),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.textTheme.bodySmall?.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// App Info Card
|
||||
_buildSection(
|
||||
context,
|
||||
title: 'aboutScreenAppInfoSectionTitle'.tr(),
|
||||
: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 540),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Symbols.info,
|
||||
label: 'aboutScreenPackageNameLabel'.tr(),
|
||||
value: _packageInfo.packageName,
|
||||
),
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Symbols.update,
|
||||
label: 'aboutScreenVersionLabel'.tr(),
|
||||
value: _packageInfo.version,
|
||||
),
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Symbols.build,
|
||||
label: 'aboutScreenBuildNumberLabel'.tr(),
|
||||
value: _packageInfo.buildNumber,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (_deviceInfo != null) const SizedBox(height: 16),
|
||||
|
||||
if (_deviceInfo != null)
|
||||
_buildSection(
|
||||
context,
|
||||
title: 'Device Information',
|
||||
children: [
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Symbols.label,
|
||||
label: 'aboutDeviceName'.tr(),
|
||||
value: _deviceInfo?.data['name'],
|
||||
const SizedBox(height: 24),
|
||||
// App Icon and Name
|
||||
CircleAvatar(
|
||||
radius: 50,
|
||||
backgroundColor: theme.colorScheme.primary
|
||||
.withOpacity(0.1),
|
||||
child: Image.asset(
|
||||
'assets/icons/icon.png',
|
||||
width: 56,
|
||||
height: 56,
|
||||
),
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Symbols.fingerprint,
|
||||
label: 'aboutDeviceIdentifier'.tr(),
|
||||
value: _deviceUdid ?? 'N/A',
|
||||
copyable: true,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_packageInfo.appName,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Links Card
|
||||
_buildSection(
|
||||
context,
|
||||
title: 'aboutScreenLinksSectionTitle'.tr(),
|
||||
children: [
|
||||
_buildListTile(
|
||||
context,
|
||||
icon: Symbols.privacy_tip,
|
||||
title: 'aboutScreenPrivacyPolicyTitle'.tr(),
|
||||
onTap:
|
||||
() => _launchURL(
|
||||
'https://solsynth.dev/terms/privacy-policy',
|
||||
),
|
||||
),
|
||||
_buildListTile(
|
||||
context,
|
||||
icon: Symbols.description,
|
||||
title: 'aboutScreenTermsOfServiceTitle'.tr(),
|
||||
onTap:
|
||||
() => _launchURL(
|
||||
'https://solsynth.dev/terms/user-agreement',
|
||||
),
|
||||
),
|
||||
_buildListTile(
|
||||
context,
|
||||
icon: Symbols.code,
|
||||
title: 'aboutScreenOpenSourceLicensesTitle'.tr(),
|
||||
onTap: () {
|
||||
showLicensePage(
|
||||
context: context,
|
||||
applicationName: _packageInfo.appName,
|
||||
applicationVersion:
|
||||
'Version ${_packageInfo.version}',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Developer Info
|
||||
_buildSection(
|
||||
context,
|
||||
title: 'aboutScreenDeveloperSectionTitle'.tr(),
|
||||
children: [
|
||||
_buildListTile(
|
||||
context,
|
||||
icon: Symbols.email,
|
||||
title: 'aboutScreenContactUsTitle'.tr(),
|
||||
subtitle: 'lily@solsynth.dev',
|
||||
onTap: () => _launchURL('mailto:lily@solsynth.dev'),
|
||||
),
|
||||
_buildListTile(
|
||||
context,
|
||||
icon: Symbols.copyright,
|
||||
title: 'aboutScreenLicenseTitle'.tr(),
|
||||
subtitle: 'aboutScreenLicenseContent'.tr(
|
||||
args: [DateTime.now().year.toString()],
|
||||
Text(
|
||||
'aboutScreenVersionInfo'.tr(
|
||||
args: [
|
||||
_packageInfo.version,
|
||||
_packageInfo.buildNumber,
|
||||
],
|
||||
),
|
||||
onTap:
|
||||
() => _launchURL(
|
||||
'https://github.com/Solsynth/Solian/blob/v3/LICENSE.txt',
|
||||
),
|
||||
),
|
||||
if (kIsWeb || !(Platform.isMacOS || Platform.isIOS))
|
||||
_buildListTile(
|
||||
context,
|
||||
icon: Symbols.favorite,
|
||||
title: 'donate'.tr(),
|
||||
subtitle: 'donateDescription'.tr(),
|
||||
onTap: () {
|
||||
launchUrlString(
|
||||
'https://afdian.com/@littlesheep',
|
||||
);
|
||||
},
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.textTheme.bodySmall?.color,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Copyright
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'aboutScreenCopyright'.tr(
|
||||
args: [DateTime.now().year.toString()],
|
||||
// App Info Card
|
||||
_buildSection(
|
||||
context,
|
||||
title: 'aboutScreenAppInfoSectionTitle'.tr(),
|
||||
children: [
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Symbols.info,
|
||||
label: 'aboutScreenPackageNameLabel'.tr(),
|
||||
value: _packageInfo.packageName,
|
||||
),
|
||||
style: theme.textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(1),
|
||||
Text(
|
||||
'aboutScreenMadeWith'.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
).fontSize(10).opacity(0.8),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Symbols.update,
|
||||
label: 'aboutScreenVersionLabel'.tr(),
|
||||
value: _packageInfo.version,
|
||||
),
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Symbols.build,
|
||||
label: 'aboutScreenBuildNumberLabel'.tr(),
|
||||
value: _packageInfo.buildNumber,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Gap(MediaQuery.of(context).padding.bottom + 16),
|
||||
],
|
||||
if (_deviceInfo != null) const SizedBox(height: 16),
|
||||
|
||||
if (_deviceInfo != null)
|
||||
_buildSection(
|
||||
context,
|
||||
title: 'Device Information',
|
||||
children: [
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Symbols.label,
|
||||
label: 'aboutDeviceName'.tr(),
|
||||
value: _deviceInfo?.data['name'],
|
||||
),
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Symbols.fingerprint,
|
||||
label: 'aboutDeviceIdentifier'.tr(),
|
||||
value: _deviceUdid ?? 'N/A',
|
||||
copyable: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Links Card
|
||||
_buildSection(
|
||||
context,
|
||||
title: 'aboutScreenLinksSectionTitle'.tr(),
|
||||
children: [
|
||||
_buildListTile(
|
||||
context,
|
||||
icon: Symbols.system_update,
|
||||
title: 'Check for updates',
|
||||
onTap: () async {
|
||||
// Fetch latest release and show the unified sheet
|
||||
final svc = UpdateService();
|
||||
// Reuse service fetch + compare to decide content
|
||||
final release = await svc.fetchLatestRelease();
|
||||
if (release != null) {
|
||||
await svc.showUpdateSheet(context, release);
|
||||
} else {
|
||||
// Fallback: show a simple sheet indicating no info
|
||||
// Use your SheetScaffold for consistent styling
|
||||
// Show a minimal message
|
||||
// ignore: use_build_context_synchronously
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
showDragHandle: true,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surface,
|
||||
builder:
|
||||
(_) => const SheetScaffold(
|
||||
titleText: 'Update',
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Text(
|
||||
'Unable to fetch release info at this time.',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
_buildListTile(
|
||||
context,
|
||||
icon: Symbols.privacy_tip,
|
||||
title: 'aboutScreenPrivacyPolicyTitle'.tr(),
|
||||
onTap:
|
||||
() => _launchURL(
|
||||
'https://solsynth.dev/terms/privacy-policy',
|
||||
),
|
||||
),
|
||||
_buildListTile(
|
||||
context,
|
||||
icon: Symbols.description,
|
||||
title: 'aboutScreenTermsOfServiceTitle'.tr(),
|
||||
onTap:
|
||||
() => _launchURL(
|
||||
'https://solsynth.dev/terms/user-agreement',
|
||||
),
|
||||
),
|
||||
_buildListTile(
|
||||
context,
|
||||
icon: Symbols.code,
|
||||
title: 'aboutScreenOpenSourceLicensesTitle'.tr(),
|
||||
onTap: () {
|
||||
showLicensePage(
|
||||
context: context,
|
||||
applicationName: _packageInfo.appName,
|
||||
applicationVersion:
|
||||
'Version ${_packageInfo.version}',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Developer Info
|
||||
_buildSection(
|
||||
context,
|
||||
title: 'aboutScreenDeveloperSectionTitle'.tr(),
|
||||
children: [
|
||||
_buildListTile(
|
||||
context,
|
||||
icon: Symbols.email,
|
||||
title: 'aboutScreenContactUsTitle'.tr(),
|
||||
subtitle: 'lily@solsynth.dev',
|
||||
onTap:
|
||||
() => _launchURL('mailto:lily@solsynth.dev'),
|
||||
),
|
||||
_buildListTile(
|
||||
context,
|
||||
icon: Symbols.copyright,
|
||||
title: 'aboutScreenLicenseTitle'.tr(),
|
||||
subtitle: 'aboutScreenLicenseContent'.tr(
|
||||
args: [DateTime.now().year.toString()],
|
||||
),
|
||||
onTap:
|
||||
() => _launchURL(
|
||||
'https://github.com/Solsynth/Solian/blob/v3/LICENSE.txt',
|
||||
),
|
||||
),
|
||||
if (kIsWeb || !(Platform.isMacOS || Platform.isIOS))
|
||||
_buildListTile(
|
||||
context,
|
||||
icon: Symbols.favorite,
|
||||
title: 'donate'.tr(),
|
||||
subtitle: 'donateDescription'.tr(),
|
||||
onTap: () {
|
||||
launchUrlString(
|
||||
'https://afdian.com/@littlesheep',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Copyright
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'aboutScreenCopyright'.tr(
|
||||
args: [DateTime.now().year.toString()],
|
||||
),
|
||||
style: theme.textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(1),
|
||||
Text(
|
||||
'aboutScreenMadeWith'.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
).fontSize(10).opacity(0.8),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Gap(MediaQuery.of(context).padding.bottom + 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@@ -1,12 +1,8 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/message.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/screens/notification.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
@@ -15,6 +11,7 @@ import 'package:island/widgets/account/status.dart';
|
||||
import 'package:island/widgets/account/leveling_progress.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/debug_sheet.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
@@ -276,30 +273,6 @@ class AccountScreen extends HookConsumerWidget {
|
||||
context.pushNamed('accountSettings');
|
||||
},
|
||||
),
|
||||
if (kDebugMode) const Divider(height: 1).padding(vertical: 8),
|
||||
if (kDebugMode)
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.copy_all),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('Copy access token'),
|
||||
onTap: () async {
|
||||
final tk = ref.watch(tokenProvider);
|
||||
Clipboard.setData(ClipboardData(text: tk!.token));
|
||||
},
|
||||
),
|
||||
if (kDebugMode)
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.delete),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('Reset database'),
|
||||
onTap: () async {
|
||||
resetDatabase(ref);
|
||||
},
|
||||
),
|
||||
const Divider(height: 1).padding(vertical: 8),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
@@ -311,6 +284,19 @@ class AccountScreen extends HookConsumerWidget {
|
||||
context.pushNamed('about');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.bug_report),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('debugOptions').tr(),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => DebugSheet(),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.logout),
|
||||
|
@@ -3,6 +3,7 @@ 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:image_picker/image_picker.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
@@ -94,6 +95,11 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
final usernameController = useTextEditingController(text: user.value!.name);
|
||||
final nicknameController = useTextEditingController(text: user.value!.nick);
|
||||
final language = useState(user.value!.language);
|
||||
final links = useState<List<Map<String, String>>>(
|
||||
user.value!.profile.links.entries
|
||||
.map((e) => {'key': e.key, 'value': e.value})
|
||||
.toList(),
|
||||
);
|
||||
|
||||
void updateBasicInfo() async {
|
||||
if (!formKeyBasicInfo.currentState!.validate()) return;
|
||||
@@ -165,6 +171,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
'location': locationController.text,
|
||||
'time_zone': timeZoneController.text,
|
||||
'birthday': birthday.value?.toUtc().toIso8601String(),
|
||||
'links': {for (var e in links.value) e['key']!: e['value']!},
|
||||
},
|
||||
);
|
||||
final userNotifier = ref.read(userInfoProvider.notifier);
|
||||
@@ -558,6 +565,69 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
Text('links').tr().bold().fontSize(18).padding(top: 16),
|
||||
Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (var i = 0; i < links.value.length; i++)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: links.value[i]['key'],
|
||||
decoration: InputDecoration(
|
||||
labelText: 'linkKey'.tr(),
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: (value) {
|
||||
links.value[i]['key'] = value;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: links.value[i]['value'],
|
||||
decoration: InputDecoration(
|
||||
labelText: 'linkValue'.tr(),
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: (value) {
|
||||
links.value[i]['value'] = value;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () {
|
||||
links.value = List.from(links.value)
|
||||
..removeAt(i);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FilledButton.icon(
|
||||
onPressed: () {
|
||||
links.value = List.from(links.value)
|
||||
..add({'key': '', 'value': ''});
|
||||
},
|
||||
label: Text('addLink').tr(),
|
||||
icon: const Icon(Symbols.add),
|
||||
).padding(top: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
|
@@ -13,6 +13,7 @@ import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/services/color.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/services/text.dart';
|
||||
import 'package:island/services/time.dart';
|
||||
import 'package:island/services/timezone/native.dart';
|
||||
import 'package:island/widgets/account/account_name.dart';
|
||||
@@ -30,6 +31,7 @@ import 'package:palette_generator/palette_generator.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
part 'profile.g.dart';
|
||||
|
||||
@@ -350,6 +352,28 @@ class AccountProfileScreen extends HookConsumerWidget {
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
);
|
||||
|
||||
Widget accountProfileLinks(SnAccount data) => Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('links').tr().bold().padding(horizontal: 24, top: 12, bottom: 4),
|
||||
for (final link in data.profile.links.entries)
|
||||
ListTile(
|
||||
title: Text(link.key.capitalizeEachWord()),
|
||||
subtitle: Text(link.value),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
onTap: () {
|
||||
launchUrlString(link.value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget accountAction(SnAccount data) => Card(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -452,7 +476,7 @@ class AccountProfileScreen extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 8),
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
);
|
||||
|
||||
return account.when(
|
||||
@@ -509,9 +533,11 @@ class AccountProfileScreen extends HookConsumerWidget {
|
||||
SliverToBoxAdapter(child: accountBasicInfo(data)),
|
||||
if (data.badges.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: BadgeList(
|
||||
badges: data.badges,
|
||||
).padding(horizontal: 24, bottom: 24),
|
||||
child: Card(
|
||||
child: BadgeList(
|
||||
badges: data.badges,
|
||||
).padding(horizontal: 26, vertical: 20),
|
||||
).padding(left: 2, right: 4),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
@@ -521,9 +547,10 @@ class AccountProfileScreen extends HookConsumerWidget {
|
||||
level: data.profile.level,
|
||||
experience: data.profile.experience,
|
||||
progress: data.profile.levelingProgress,
|
||||
),
|
||||
).padding(left: 2, right: 4),
|
||||
if (data.profile.verification != null)
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: VerificationStatusCard(
|
||||
mark: data.profile.verification!,
|
||||
),
|
||||
@@ -534,6 +561,9 @@ class AccountProfileScreen extends HookConsumerWidget {
|
||||
SliverToBoxAdapter(
|
||||
child: accountProfileBio(data).padding(top: 4),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: accountProfileLinks(data),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: accountProfileDetail(data),
|
||||
),
|
||||
@@ -604,9 +634,11 @@ class AccountProfileScreen extends HookConsumerWidget {
|
||||
SliverToBoxAdapter(child: accountBasicInfo(data)),
|
||||
if (data.badges.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: BadgeList(
|
||||
badges: data.badges,
|
||||
).padding(horizontal: 24, bottom: 24),
|
||||
child: Card(
|
||||
child: BadgeList(
|
||||
badges: data.badges,
|
||||
).padding(horizontal: 26, vertical: 20),
|
||||
).padding(horizontal: 4),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
@@ -628,6 +660,11 @@ class AccountProfileScreen extends HookConsumerWidget {
|
||||
SliverToBoxAdapter(
|
||||
child: accountProfileBio(data).padding(horizontal: 4),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: accountProfileLinks(
|
||||
data,
|
||||
).padding(horizontal: 4),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: accountProfileDetail(
|
||||
data,
|
||||
|
@@ -1061,14 +1061,14 @@ class _ChatInput extends HookConsumerWidget {
|
||||
children: [
|
||||
if (attachments.isNotEmpty)
|
||||
SizedBox(
|
||||
height: 324,
|
||||
height: 280,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: attachments.length,
|
||||
itemBuilder: (context, idx) {
|
||||
return SizedBox(
|
||||
height: 320,
|
||||
height: 280,
|
||||
width: 280,
|
||||
child: AttachmentPreview(
|
||||
item: attachments[idx],
|
||||
|
@@ -18,7 +18,7 @@ part 'apps.g.dart';
|
||||
@riverpod
|
||||
Future<List<CustomApp>> customApps(Ref ref, String publisherName) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get('/developers/$publisherName/apps');
|
||||
final resp = await client.get('/develop/developers/$publisherName/apps');
|
||||
return resp.data.map((e) => CustomApp.fromJson(e)).cast<CustomApp>().toList();
|
||||
}
|
||||
|
||||
@@ -37,7 +37,10 @@ class CustomAppsScreen extends HookConsumerWidget {
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.add),
|
||||
onPressed: () {
|
||||
context.pushNamed('developerAppNew', pathParameters: {'name': publisherName});
|
||||
context.pushNamed(
|
||||
'developerAppNew',
|
||||
pathParameters: {'name': publisherName},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -121,7 +124,13 @@ class CustomAppsScreen extends HookConsumerWidget {
|
||||
],
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
context.pushNamed('developerAppEdit', pathParameters: {'name': publisherName, 'id': app.id});
|
||||
context.pushNamed(
|
||||
'developerAppEdit',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'id': app.id,
|
||||
},
|
||||
);
|
||||
} else if (value == 'delete') {
|
||||
showConfirmAlert(
|
||||
'deleteCustomAppHint'.tr(),
|
||||
@@ -130,7 +139,7 @@ class CustomAppsScreen extends HookConsumerWidget {
|
||||
if (confirm) {
|
||||
final client = ref.read(apiClientProvider);
|
||||
client.delete(
|
||||
'/developers/$publisherName/apps/${app.id}',
|
||||
'/develop/developers/$publisherName/apps/${app.id}',
|
||||
);
|
||||
ref.invalidate(
|
||||
customAppsProvider(publisherName),
|
||||
|
@@ -6,7 +6,7 @@ part of 'apps.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$customAppsHash() => r'1dec11573b9d987c3adbdf4732b3781a6f40172a';
|
||||
String _$customAppsHash() => r'c6ac78060eb51a2b208a749a81ecbe0a9c608ce1';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
@@ -24,7 +24,7 @@ part 'edit_app.g.dart';
|
||||
@riverpod
|
||||
Future<CustomApp?> customApp(Ref ref, String publisherName, String id) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get('/developers/$publisherName/apps/$id');
|
||||
final resp = await client.get('/develop/developers/$publisherName/apps/$id');
|
||||
return CustomApp.fromJson(resp.data);
|
||||
}
|
||||
|
||||
@@ -282,9 +282,15 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
: null,
|
||||
};
|
||||
if (isNew) {
|
||||
await client.post('/developers/$publisherName/apps', data: data);
|
||||
await client.post(
|
||||
'/develop/developers/$publisherName/apps',
|
||||
data: data,
|
||||
);
|
||||
} else {
|
||||
await client.patch('/developers/$publisherName/apps/$id', data: data);
|
||||
await client.patch(
|
||||
'/develop/developers/$publisherName/apps/$id',
|
||||
data: data,
|
||||
);
|
||||
}
|
||||
ref.invalidate(customAppsProvider(publisherName));
|
||||
if (context.mounted) {
|
||||
|
@@ -6,7 +6,7 @@ part of 'edit_app.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$customAppHash() => r'aa4d1fb803c47a99cbacf6d91481f4fce3fda457';
|
||||
String _$customAppHash() => r'42ad937b8439c793e3c5c35568bb5fa4da017df3';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
@@ -25,14 +25,14 @@ part 'hub.g.dart';
|
||||
Future<DeveloperStats?> developerStats(Ref ref, String? uname) async {
|
||||
if (uname == null) return null;
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get('/sphere/developers/$uname/stats');
|
||||
final resp = await apiClient.get('/develop/developers/$uname/stats');
|
||||
return DeveloperStats.fromJson(resp.data);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<List<SnPublisher>> developers(Ref ref) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get('/sphere/developers');
|
||||
final resp = await client.get('/develop/developers');
|
||||
return resp.data
|
||||
.map((e) => SnPublisher.fromJson(e))
|
||||
.cast<SnPublisher>()
|
||||
@@ -336,7 +336,7 @@ class _DeveloperEnrollmentSheet extends HookConsumerWidget {
|
||||
Future<void> enroll(SnPublisher publisher) async {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.post('/sphere/developers/${publisher.name}/enroll');
|
||||
await client.post('/develop/developers/${publisher.name}/enroll');
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ part of 'hub.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$developerStatsHash() => r'baa708f3586e8987e221cc8ab825d759658c0f55';
|
||||
String _$developerStatsHash() => r'45546f29ec7cd1a9c3a4e0f4e39275e78bf34755';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
@@ -149,7 +149,7 @@ class _DeveloperStatsProviderElement
|
||||
String? get uname => (origin as DeveloperStatsProvider).uname;
|
||||
}
|
||||
|
||||
String _$developersHash() => r'f11335fdf553c661110281edeec70ef89c64727d';
|
||||
String _$developersHash() => r'04f25db31f511f651a5add128d56631236ed0b39';
|
||||
|
||||
/// See also [developers].
|
||||
@ProviderFor(developers)
|
||||
|
@@ -12,11 +12,11 @@ import 'package:island/models/webfeed.dart';
|
||||
import 'package:island/pods/event_calendar.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/account/event_calendar.dart';
|
||||
import 'package:island/widgets/account/fortune_graph.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/widgets/check_in.dart';
|
||||
import 'package:island/widgets/post/post_featured.dart';
|
||||
import 'package:island/widgets/post/post_item.dart';
|
||||
import 'package:island/screens/tabs.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
@@ -70,15 +70,6 @@ class ExploreScreen extends HookConsumerWidget {
|
||||
final events = ref.watch(eventCalendarProvider(query.value));
|
||||
|
||||
final selectedDay = useState(now);
|
||||
|
||||
void onMonthChanged(int year, int month) {
|
||||
query.value = EventCalendarQuery(
|
||||
uname: query.value.uname,
|
||||
year: year,
|
||||
month: month,
|
||||
);
|
||||
}
|
||||
|
||||
// Function to handle day selection for synchronizing between widgets
|
||||
void onDaySelected(DateTime day) {
|
||||
selectedDay.value = day;
|
||||
@@ -224,20 +215,10 @@ class ExploreScreen extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
Card(
|
||||
margin: EdgeInsets.only(left: 8, right: 12, top: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
// Use the reusable EventCalendarWidget
|
||||
EventCalendarWidget(
|
||||
events: events,
|
||||
initialDate: now,
|
||||
showEventDetails: true,
|
||||
onMonthChanged: onMonthChanged,
|
||||
onDaySelected: onDaySelected,
|
||||
),
|
||||
],
|
||||
),
|
||||
PostFeaturedList().padding(
|
||||
left: 8,
|
||||
right: 12,
|
||||
top: 8,
|
||||
),
|
||||
FortuneGraphWidget(
|
||||
margin: EdgeInsets.only(left: 8, right: 12, top: 8),
|
||||
@@ -408,6 +389,10 @@ class _ActivityListView extends HookConsumerWidget {
|
||||
margin: EdgeInsets.only(left: 8, right: 8, bottom: 4),
|
||||
),
|
||||
),
|
||||
if (!contentOnly)
|
||||
SliverToBoxAdapter(
|
||||
child: PostFeaturedList().padding(horizontal: 8, bottom: 4, top: 4),
|
||||
),
|
||||
SliverList.builder(
|
||||
itemCount: widgetCount,
|
||||
itemBuilder: (context, index) {
|
||||
|
@@ -205,15 +205,7 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => ComposeSettingsSheet(
|
||||
visibility: state.visibility,
|
||||
tagsController: state.tagsController,
|
||||
categoriesController: state.categoriesController,
|
||||
onVisibilityChanged: () {
|
||||
// Trigger rebuild if needed
|
||||
},
|
||||
),
|
||||
builder: (context) => ComposeSettingsSheet(state: state),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -367,63 +359,63 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
|
||||
// Post content form
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: state.titleController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'postTitle'.tr(),
|
||||
border: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 8,
|
||||
),
|
||||
),
|
||||
style: theme.textTheme.titleMedium,
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
child: KeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
onKeyEvent:
|
||||
(event) => ComposeLogic.handleKeyPress(
|
||||
event,
|
||||
state,
|
||||
ref,
|
||||
context,
|
||||
originalPost: originalPost,
|
||||
repliedPost: repliedPost,
|
||||
forwardedPost: forwardedPost,
|
||||
),
|
||||
TextField(
|
||||
controller: state.descriptionController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'postDescription'.tr(),
|
||||
border: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.fromLTRB(
|
||||
8,
|
||||
4,
|
||||
8,
|
||||
12,
|
||||
),
|
||||
),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
),
|
||||
// Content field with borderless design
|
||||
KeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
onKeyEvent:
|
||||
(event) => ComposeLogic.handleKeyPress(
|
||||
event,
|
||||
state,
|
||||
ref,
|
||||
context,
|
||||
originalPost: originalPost,
|
||||
repliedPost: repliedPost,
|
||||
forwardedPost: forwardedPost,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: state.titleController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'postTitle'.tr(),
|
||||
border: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 8,
|
||||
),
|
||||
child: TextField(
|
||||
),
|
||||
style: theme.textTheme.titleMedium,
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
),
|
||||
TextField(
|
||||
controller: state.descriptionController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'postDescription'.tr(),
|
||||
border: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.fromLTRB(
|
||||
8,
|
||||
4,
|
||||
8,
|
||||
12,
|
||||
),
|
||||
),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
),
|
||||
// Content field with borderless design
|
||||
TextField(
|
||||
controller: state.contentController,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
@@ -441,23 +433,23 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
),
|
||||
),
|
||||
|
||||
const Gap(8),
|
||||
const Gap(8),
|
||||
|
||||
// Attachments preview
|
||||
if (state.attachments.value.isNotEmpty)
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWide = isWideScreen(context);
|
||||
return isWide
|
||||
? buildWideAttachmentGrid()
|
||||
: buildNarrowAttachmentList();
|
||||
},
|
||||
)
|
||||
else
|
||||
const SizedBox.shrink(),
|
||||
],
|
||||
// Attachments preview
|
||||
if (state.attachments.value.isNotEmpty)
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWide = isWideScreen(context);
|
||||
return isWide
|
||||
? buildWideAttachmentGrid()
|
||||
: buildNarrowAttachmentList();
|
||||
},
|
||||
)
|
||||
else
|
||||
const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@@ -138,15 +138,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => ComposeSettingsSheet(
|
||||
visibility: state.visibility,
|
||||
tagsController: state.tagsController,
|
||||
categoriesController: state.categoriesController,
|
||||
onVisibilityChanged: () {
|
||||
// Trigger rebuild if needed
|
||||
},
|
||||
),
|
||||
builder: (context) => ComposeSettingsSheet(state: state),
|
||||
);
|
||||
}
|
||||
|
||||
|
107
lib/screens/posts/post_category_detail.dart
Normal file
107
lib/screens/posts/post_category_detail.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post_category.dart';
|
||||
import 'package:island/models/post_tag.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/post/post_list.dart';
|
||||
import 'package:island/widgets/response.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'post_category_detail.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<SnPostCategory> postCategory(Ref ref, String slug) async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get('/sphere/posts/categories/$slug');
|
||||
return SnPostCategory.fromJson(resp.data);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<SnPostTag> postTag(Ref ref, String slug) async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get('/sphere/posts/tags/$slug');
|
||||
return SnPostTag.fromJson(resp.data);
|
||||
}
|
||||
|
||||
class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
final String slug;
|
||||
final bool isCategory;
|
||||
const PostCategoryDetailScreen({
|
||||
super.key,
|
||||
required this.slug,
|
||||
required this.isCategory,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final postCategory =
|
||||
isCategory ? ref.watch(postCategoryProvider(slug)) : null;
|
||||
final postTag = isCategory ? null : ref.watch(postTagProvider(slug));
|
||||
|
||||
final postFilterTitle =
|
||||
isCategory
|
||||
? postCategory?.value?.categoryDisplayTitle ?? 'loading'
|
||||
: postTag?.value?.name ?? postTag?.value?.slug ?? 'loading';
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(title: Text(postFilterTitle).tr()),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (isCategory)
|
||||
postCategory!.when(
|
||||
data:
|
||||
(category) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(category.categoryDisplayTitle).bold().fontSize(15),
|
||||
Text('A category'),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
error:
|
||||
(error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () => ref.invalidate(postCategoryProvider(slug)),
|
||||
),
|
||||
loading: () => ResponseLoadingWidget(),
|
||||
)
|
||||
else
|
||||
postTag!.when(
|
||||
data:
|
||||
(tag) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(tag.name ?? '#${tag.slug}').bold().fontSize(15),
|
||||
Text('A tag'),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
error:
|
||||
(error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () => ref.invalidate(postTagProvider(slug)),
|
||||
),
|
||||
loading: () => ResponseLoadingWidget(),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverGap(4),
|
||||
SliverPostList(
|
||||
categories: isCategory ? [slug] : null,
|
||||
tags: isCategory ? null : [slug],
|
||||
),
|
||||
SliverGap(MediaQuery.of(context).padding.bottom + 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
270
lib/screens/posts/post_category_detail.g.dart
Normal file
270
lib/screens/posts/post_category_detail.g.dart
Normal file
@@ -0,0 +1,270 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'post_category_detail.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$postCategoryHash() => r'0df2de729ba96819ee37377314615abef0c99547';
|
||||
|
||||
/// 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 [postCategory].
|
||||
@ProviderFor(postCategory)
|
||||
const postCategoryProvider = PostCategoryFamily();
|
||||
|
||||
/// See also [postCategory].
|
||||
class PostCategoryFamily extends Family<AsyncValue<SnPostCategory>> {
|
||||
/// See also [postCategory].
|
||||
const PostCategoryFamily();
|
||||
|
||||
/// See also [postCategory].
|
||||
PostCategoryProvider call(String slug) {
|
||||
return PostCategoryProvider(slug);
|
||||
}
|
||||
|
||||
@override
|
||||
PostCategoryProvider getProviderOverride(
|
||||
covariant PostCategoryProvider provider,
|
||||
) {
|
||||
return call(provider.slug);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'postCategoryProvider';
|
||||
}
|
||||
|
||||
/// See also [postCategory].
|
||||
class PostCategoryProvider extends AutoDisposeFutureProvider<SnPostCategory> {
|
||||
/// See also [postCategory].
|
||||
PostCategoryProvider(String slug)
|
||||
: this._internal(
|
||||
(ref) => postCategory(ref as PostCategoryRef, slug),
|
||||
from: postCategoryProvider,
|
||||
name: r'postCategoryProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$postCategoryHash,
|
||||
dependencies: PostCategoryFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
PostCategoryFamily._allTransitiveDependencies,
|
||||
slug: slug,
|
||||
);
|
||||
|
||||
PostCategoryProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.slug,
|
||||
}) : super.internal();
|
||||
|
||||
final String slug;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<SnPostCategory> Function(PostCategoryRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: PostCategoryProvider._internal(
|
||||
(ref) => create(ref as PostCategoryRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
slug: slug,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<SnPostCategory> createElement() {
|
||||
return _PostCategoryProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PostCategoryProvider && other.slug == slug;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, slug.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin PostCategoryRef on AutoDisposeFutureProviderRef<SnPostCategory> {
|
||||
/// The parameter `slug` of this provider.
|
||||
String get slug;
|
||||
}
|
||||
|
||||
class _PostCategoryProviderElement
|
||||
extends AutoDisposeFutureProviderElement<SnPostCategory>
|
||||
with PostCategoryRef {
|
||||
_PostCategoryProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get slug => (origin as PostCategoryProvider).slug;
|
||||
}
|
||||
|
||||
String _$postTagHash() => r'e050fdf9af81a843a9abd9cf979dd2672e0a2b93';
|
||||
|
||||
/// See also [postTag].
|
||||
@ProviderFor(postTag)
|
||||
const postTagProvider = PostTagFamily();
|
||||
|
||||
/// See also [postTag].
|
||||
class PostTagFamily extends Family<AsyncValue<SnPostTag>> {
|
||||
/// See also [postTag].
|
||||
const PostTagFamily();
|
||||
|
||||
/// See also [postTag].
|
||||
PostTagProvider call(String slug) {
|
||||
return PostTagProvider(slug);
|
||||
}
|
||||
|
||||
@override
|
||||
PostTagProvider getProviderOverride(covariant PostTagProvider provider) {
|
||||
return call(provider.slug);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'postTagProvider';
|
||||
}
|
||||
|
||||
/// See also [postTag].
|
||||
class PostTagProvider extends AutoDisposeFutureProvider<SnPostTag> {
|
||||
/// See also [postTag].
|
||||
PostTagProvider(String slug)
|
||||
: this._internal(
|
||||
(ref) => postTag(ref as PostTagRef, slug),
|
||||
from: postTagProvider,
|
||||
name: r'postTagProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$postTagHash,
|
||||
dependencies: PostTagFamily._dependencies,
|
||||
allTransitiveDependencies: PostTagFamily._allTransitiveDependencies,
|
||||
slug: slug,
|
||||
);
|
||||
|
||||
PostTagProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.slug,
|
||||
}) : super.internal();
|
||||
|
||||
final String slug;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<SnPostTag> Function(PostTagRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: PostTagProvider._internal(
|
||||
(ref) => create(ref as PostTagRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
slug: slug,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<SnPostTag> createElement() {
|
||||
return _PostTagProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PostTagProvider && other.slug == slug;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, slug.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin PostTagRef on AutoDisposeFutureProviderRef<SnPostTag> {
|
||||
/// The parameter `slug` of this provider.
|
||||
String get slug;
|
||||
}
|
||||
|
||||
class _PostTagProviderElement
|
||||
extends AutoDisposeFutureProviderElement<SnPostTag>
|
||||
with PostTagRef {
|
||||
_PostTagProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get slug => (origin as PostTagProvider).slug;
|
||||
}
|
||||
|
||||
// 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
|
@@ -99,7 +99,10 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
subscribing.value = true;
|
||||
try {
|
||||
await apiClient.post("/publishers/$name/subscribe", data: {'tier': 0});
|
||||
await apiClient.post(
|
||||
"/sphere/publishers/$name/subscribe",
|
||||
data: {'tier': 0},
|
||||
);
|
||||
ref.invalidate(publisherSubscriptionStatusProvider(name));
|
||||
HapticFeedback.heavyImpact();
|
||||
} catch (err) {
|
||||
@@ -113,7 +116,7 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
subscribing.value = true;
|
||||
try {
|
||||
await apiClient.post("/publishers/$name/unsubscribe");
|
||||
await apiClient.post("/sphere/publishers/$name/unsubscribe");
|
||||
ref.invalidate(publisherSubscriptionStatusProvider(name));
|
||||
HapticFeedback.heavyImpact();
|
||||
} catch (err) {
|
||||
|
228
lib/services/update_service.dart
Normal file
228
lib/services/update_service.dart
Normal file
@@ -0,0 +1,228 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
|
||||
/// Data model for a GitHub release we care about
|
||||
class GithubReleaseInfo {
|
||||
final String tagName; // e.g. 3.1.0+118
|
||||
final String name; // release title
|
||||
final String body; // changelog markdown
|
||||
final String htmlUrl; // release page
|
||||
final DateTime createdAt;
|
||||
|
||||
const GithubReleaseInfo({
|
||||
required this.tagName,
|
||||
required this.name,
|
||||
required this.body,
|
||||
required this.htmlUrl,
|
||||
required this.createdAt,
|
||||
});
|
||||
}
|
||||
|
||||
/// Parses version and build number from "x.y.z+build"
|
||||
class _ParsedVersion implements Comparable<_ParsedVersion> {
|
||||
final int major;
|
||||
final int minor;
|
||||
final int patch;
|
||||
final int build;
|
||||
|
||||
const _ParsedVersion(this.major, this.minor, this.patch, this.build);
|
||||
|
||||
static _ParsedVersion? tryParse(String input) {
|
||||
// Expect format like 0.0.0+00 (build after '+'). Allow missing build as 0.
|
||||
final partsPlus = input.split('+');
|
||||
final core = partsPlus[0].trim();
|
||||
final buildStr = partsPlus.length > 1 ? partsPlus[1].trim() : '0';
|
||||
final coreParts = core.split('.');
|
||||
if (coreParts.length != 3) return null;
|
||||
|
||||
final major = int.tryParse(coreParts[0]) ?? 0;
|
||||
final minor = int.tryParse(coreParts[1]) ?? 0;
|
||||
final patch = int.tryParse(coreParts[2]) ?? 0;
|
||||
final build = int.tryParse(buildStr) ?? 0;
|
||||
|
||||
return _ParsedVersion(major, minor, patch, build);
|
||||
}
|
||||
|
||||
@override
|
||||
int compareTo(_ParsedVersion other) {
|
||||
if (major != other.major) return major.compareTo(other.major);
|
||||
if (minor != other.minor) return minor.compareTo(other.minor);
|
||||
if (patch != other.patch) return patch.compareTo(other.patch);
|
||||
return build.compareTo(other.build);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '$major.$minor.$patch+$build';
|
||||
}
|
||||
|
||||
class UpdateService {
|
||||
UpdateService({Dio? dio})
|
||||
: _dio =
|
||||
dio ??
|
||||
Dio(
|
||||
BaseOptions(
|
||||
headers: {
|
||||
// Identify the app to GitHub; avoids some rate-limits and adds clarity
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'User-Agent': 'solian-update-checker',
|
||||
},
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
),
|
||||
);
|
||||
|
||||
final Dio _dio;
|
||||
|
||||
static const _releasesLatestApi =
|
||||
'https://api.github.com/repos/solsynth/solian/releases/latest';
|
||||
|
||||
/// Checks GitHub for the latest release and compares against the current app version.
|
||||
/// If update is available, shows a bottom sheet with changelog and an action to open release page.
|
||||
Future<void> checkForUpdates(BuildContext context) async {
|
||||
try {
|
||||
final release = await fetchLatestRelease();
|
||||
if (release == null) return;
|
||||
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
final localVersionStr = '${info.version}+${info.buildNumber}';
|
||||
|
||||
final latest = _ParsedVersion.tryParse(release.tagName);
|
||||
final local = _ParsedVersion.tryParse(localVersionStr);
|
||||
|
||||
if (latest == null || local == null) {
|
||||
// If parsing fails, do nothing silently
|
||||
return;
|
||||
}
|
||||
|
||||
final needsUpdate = latest.compareTo(local) > 0;
|
||||
if (!needsUpdate) return;
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
// Delay to ensure UI is ready (if called at startup)
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
await showUpdateSheet(context, release);
|
||||
} catch (_) {
|
||||
// Ignore errors (network, api, etc.)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// Manually show the update sheet with a provided release.
|
||||
/// Useful for About page or testing.
|
||||
Future<void> showUpdateSheet(
|
||||
BuildContext context,
|
||||
GithubReleaseInfo release,
|
||||
) async {
|
||||
if (!context.mounted) return;
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: true,
|
||||
builder:
|
||||
(ctx) => _UpdateSheet(
|
||||
release: release,
|
||||
onOpen: () async {
|
||||
final uri = Uri.parse(release.htmlUrl);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Fetch the latest release info from GitHub.
|
||||
/// Public so other screens (e.g., About) can manually trigger update checks.
|
||||
Future<GithubReleaseInfo?> fetchLatestRelease() async {
|
||||
final resp = await _dio.get(_releasesLatestApi);
|
||||
if (resp.statusCode != 200) return null;
|
||||
final data = resp.data as Map<String, dynamic>;
|
||||
|
||||
final tagName = (data['tag_name'] ?? '').toString();
|
||||
final name = (data['name'] ?? tagName).toString();
|
||||
final body = (data['body'] ?? '').toString();
|
||||
final htmlUrl = (data['html_url'] ?? '').toString();
|
||||
final createdAtStr = (data['created_at'] ?? '').toString();
|
||||
final createdAt = DateTime.tryParse(createdAtStr) ?? DateTime.now();
|
||||
|
||||
if (tagName.isEmpty || htmlUrl.isEmpty) return null;
|
||||
|
||||
return GithubReleaseInfo(
|
||||
tagName: tagName,
|
||||
name: name,
|
||||
body: body,
|
||||
htmlUrl: htmlUrl,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UpdateSheet extends StatelessWidget {
|
||||
const _UpdateSheet({required this.release, required this.onOpen});
|
||||
|
||||
final GithubReleaseInfo release;
|
||||
final VoidCallback onOpen;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return SheetScaffold(
|
||||
titleText: 'Update available',
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 16 + MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(release.name, style: theme.textTheme.titleMedium).bold(),
|
||||
Text(release.tagName).fontSize(12),
|
||||
],
|
||||
).padding(vertical: 16, horizontal: 16),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
child: SelectableText(
|
||||
release.body.isEmpty
|
||||
? 'No changelog provided.'
|
||||
: release.body,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed: onOpen,
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
label: const Text('Open release page'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -331,7 +331,7 @@ class _WebSocketIndicator extends HookConsumerWidget {
|
||||
final user = ref.watch(userInfoProvider);
|
||||
final websocketState = ref.watch(websocketStateProvider);
|
||||
final indicatorHeight =
|
||||
MediaQuery.of(context).padding.top + (isDesktop ? 27.5 : 60);
|
||||
MediaQuery.of(context).padding.top + (isDesktop ? 27.5 : 20);
|
||||
|
||||
Color indicatorColor;
|
||||
String indicatorText;
|
||||
@@ -343,7 +343,7 @@ class _WebSocketIndicator extends HookConsumerWidget {
|
||||
indicatorColor = Colors.teal;
|
||||
indicatorText = 'connectionReconnecting';
|
||||
} else {
|
||||
indicatorColor = Colors.orange;
|
||||
indicatorColor = Colors.red;
|
||||
indicatorText = 'connectionDisconnected';
|
||||
}
|
||||
|
||||
|
@@ -2,8 +2,10 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/services/notify.dart';
|
||||
import 'package:island/services/sharing_intent.dart';
|
||||
import 'package:island/widgets/content/network_status_sheet.dart';
|
||||
import 'package:island/widgets/tour/tour.dart';
|
||||
|
||||
class AppWrapper extends HookConsumerWidget {
|
||||
@@ -25,6 +27,27 @@ class AppWrapper extends HookConsumerWidget {
|
||||
};
|
||||
}, const []);
|
||||
|
||||
final wsNotifier = ref.watch(websocketStateProvider.notifier);
|
||||
final websocketState = ref.watch(websocketStateProvider);
|
||||
|
||||
final networkStateShowing = useState(false);
|
||||
|
||||
if (websocketState == WebSocketState.duplicateDevice()) {
|
||||
if (!networkStateShowing.value) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
networkStateShowing.value = true;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
isDismissible: false,
|
||||
builder:
|
||||
(context) =>
|
||||
NetworkStatusSheet(onReconnect: () => wsNotifier.connect()),
|
||||
).then((_) => networkStateShowing.value = false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return TourTriggerWidget(child: child);
|
||||
}
|
||||
}
|
||||
|
@@ -272,8 +272,96 @@ class AttachmentPreview extends HookConsumerWidget {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: Column(
|
||||
child: Stack(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: ratio,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Builder(
|
||||
key: ValueKey(item.hashCode),
|
||||
builder: (context) {
|
||||
if (item.isOnCloud) {
|
||||
return CloudFileWidget(item: item.data);
|
||||
} else if (item.data is XFile) {
|
||||
final file = item.data as XFile;
|
||||
if (file.path.isEmpty) {
|
||||
return FutureBuilder<Uint8List>(
|
||||
future: file.readAsBytes(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Image.memory(snapshot.data!);
|
||||
}
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
switch (item.type) {
|
||||
case UniversalFileType.image:
|
||||
return kIsWeb
|
||||
? Image.network(file.path)
|
||||
: Image.file(File(file.path));
|
||||
default:
|
||||
return Column(
|
||||
children: [
|
||||
const Icon(Symbols.document_scanner),
|
||||
Text(file.name),
|
||||
],
|
||||
);
|
||||
}
|
||||
} else if (item is List<int> || item is Uint8List) {
|
||||
switch (item.type) {
|
||||
case UniversalFileType.image:
|
||||
return Image.memory(item.data);
|
||||
default:
|
||||
return Column(
|
||||
children: [const Icon(Symbols.document_scanner)],
|
||||
);
|
||||
}
|
||||
}
|
||||
return Placeholder();
|
||||
},
|
||||
),
|
||||
if (progress != null)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 40,
|
||||
vertical: 16,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
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 != null ? progress! / 100.0 : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -397,94 +485,6 @@ class AttachmentPreview extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 12, vertical: 8),
|
||||
AspectRatio(
|
||||
aspectRatio: ratio,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Builder(
|
||||
key: ValueKey(item.hashCode),
|
||||
builder: (context) {
|
||||
if (item.isOnCloud) {
|
||||
return CloudFileWidget(item: item.data);
|
||||
} else if (item.data is XFile) {
|
||||
final file = item.data as XFile;
|
||||
if (file.path.isEmpty) {
|
||||
return FutureBuilder<Uint8List>(
|
||||
future: file.readAsBytes(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Image.memory(snapshot.data!);
|
||||
}
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
switch (item.type) {
|
||||
case UniversalFileType.image:
|
||||
return kIsWeb
|
||||
? Image.network(file.path)
|
||||
: Image.file(File(file.path));
|
||||
default:
|
||||
return Column(
|
||||
children: [
|
||||
const Icon(Symbols.document_scanner),
|
||||
Text(file.name),
|
||||
],
|
||||
);
|
||||
}
|
||||
} else if (item is List<int> || item is Uint8List) {
|
||||
switch (item.type) {
|
||||
case UniversalFileType.image:
|
||||
return Image.memory(item.data);
|
||||
default:
|
||||
return Column(
|
||||
children: [const Icon(Symbols.document_scanner)],
|
||||
);
|
||||
}
|
||||
}
|
||||
return Placeholder();
|
||||
},
|
||||
),
|
||||
if (progress != null)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 40,
|
||||
vertical: 16,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
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 != null ? progress! / 100.0 : null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@@ -145,6 +145,8 @@ class MarkdownTextContent extends HookConsumerWidget {
|
||||
);
|
||||
case 'stickers':
|
||||
final size = doesEnlargeSticker ? 96.0 : 24.0;
|
||||
final stickerUri =
|
||||
'$baseUrl/sphere/stickers/lookup/${uri.pathSegments[0]}/open';
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
@@ -155,8 +157,7 @@ class MarkdownTextContent extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
child: UniversalImage(
|
||||
uri:
|
||||
'$baseUrl/sphere/stickers/lookup/${uri.pathSegments[0]}/open',
|
||||
uri: stickerUri,
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.cover,
|
||||
|
81
lib/widgets/content/network_status_sheet.dart
Normal file
81
lib/widgets/content/network_status_sheet.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
|
||||
class NetworkStatusSheet extends HookConsumerWidget {
|
||||
final VoidCallback onReconnect;
|
||||
|
||||
const NetworkStatusSheet({super.key, required this.onReconnect});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final ws = ref.watch(websocketProvider);
|
||||
final wsState = ref.watch(websocketStateProvider);
|
||||
|
||||
return SheetScaffold(
|
||||
titleText:
|
||||
wsState == WebSocketState.connected()
|
||||
? 'Connection Status'
|
||||
: 'Connection Issue',
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
wsState.when(
|
||||
connected:
|
||||
() => Text(
|
||||
'Connected to server',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
connecting:
|
||||
() => Text(
|
||||
'Connecting to server...',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
disconnected:
|
||||
() => Text(
|
||||
'Disconnected from server',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
serverDown:
|
||||
() => Text(
|
||||
'The server is not available right now... Please try again later...',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
duplicateDevice:
|
||||
() => Text(
|
||||
'Another device has connected with the same account.',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
error:
|
||||
(message) => Text(
|
||||
'Connection error: $message',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (ws.heartbeatDelay != null)
|
||||
Text(
|
||||
'Last heartbeat: ${ws.heartbeatDelay!.inMilliseconds}ms',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Center(
|
||||
child: FilledButton.icon(
|
||||
icon: const Icon(Symbols.wifi),
|
||||
label: const Text('Reconnect'),
|
||||
onPressed: () {
|
||||
onReconnect();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
76
lib/widgets/debug_sheet.dart
Normal file
76
lib/widgets/debug_sheet.dart
Normal file
@@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/message.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/widgets/content/network_status_sheet.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class DebugSheet extends HookConsumerWidget {
|
||||
const DebugSheet({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final wsNotifier = ref.watch(websocketStateProvider.notifier);
|
||||
|
||||
return SheetScaffold(
|
||||
titleText: 'Debug',
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.wifi),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text('Connection Status'),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => NetworkStatusSheet(
|
||||
onReconnect: () => wsNotifier.connect(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.copy_all),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('Copy access token'),
|
||||
onTap: () async {
|
||||
final tk = ref.watch(tokenProvider);
|
||||
Clipboard.setData(ClipboardData(text: tk!.token));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.delete),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('Reset database'),
|
||||
onTap: () async {
|
||||
resetDatabase(ref);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.clear),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text('Clear cache'),
|
||||
onTap: () async {
|
||||
DefaultCacheManager().emptyCache();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:island/models/poll.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
|
||||
class PollSubmit extends ConsumerStatefulWidget {
|
||||
const PollSubmit({
|
||||
@@ -193,12 +195,11 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||
|
||||
// Only call onSubmit after server accepts
|
||||
widget.onSubmit(Map<String, dynamic>.unmodifiable(_answers));
|
||||
|
||||
showSnackBar('Poll answer has been submitted.');
|
||||
HapticFeedback.heavyImpact();
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('Failed to submit poll: $e')));
|
||||
}
|
||||
showErrorAlert(e);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
|
@@ -1,11 +1,29 @@
|
||||
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_category.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:island/widgets/post/compose_shared.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:textfield_tags/textfield_tags.dart';
|
||||
|
||||
part 'compose_settings_sheet.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<SnPostCategory>> postCategories(Ref ref) async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get('/sphere/posts/categories');
|
||||
return resp.data
|
||||
.map((e) => SnPostCategory.fromJson(e))
|
||||
.cast<SnPostCategory>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// A reusable widget for tag input fields with chip display
|
||||
class ChipTagInputField extends StatelessWidget {
|
||||
final InputFieldValues inputFieldValues;
|
||||
@@ -98,27 +116,20 @@ class ChipTagInputField extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class ComposeSettingsSheet extends HookWidget {
|
||||
final ValueNotifier<int> visibility;
|
||||
final VoidCallback? onVisibilityChanged;
|
||||
final StringTagController tagsController;
|
||||
final StringTagController categoriesController;
|
||||
class ComposeSettingsSheet extends HookConsumerWidget {
|
||||
final ComposeState state;
|
||||
|
||||
const ComposeSettingsSheet({
|
||||
super.key,
|
||||
required this.visibility,
|
||||
this.onVisibilityChanged,
|
||||
required this.tagsController,
|
||||
required this.categoriesController,
|
||||
});
|
||||
const ComposeSettingsSheet({super.key, required this.state});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
// Listen to visibility changes to trigger rebuilds
|
||||
final currentVisibility = useValueListenable(visibility);
|
||||
final currentVisibility = useValueListenable(state.visibility);
|
||||
final currentCategories = useValueListenable(state.categories);
|
||||
final postCategories = ref.watch(postCategoriesProvider);
|
||||
|
||||
IconData getVisibilityIcon(int visibilityValue) {
|
||||
switch (visibilityValue) {
|
||||
@@ -156,11 +167,10 @@ class ComposeSettingsSheet extends HookWidget {
|
||||
leading: Icon(icon),
|
||||
title: Text(textKey.tr()),
|
||||
onTap: () {
|
||||
visibility.value = value;
|
||||
onVisibilityChanged?.call();
|
||||
state.visibility.value = value;
|
||||
Navigator.pop(context);
|
||||
},
|
||||
selected: visibility.value == value,
|
||||
selected: state.visibility.value == value,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
);
|
||||
}
|
||||
@@ -206,6 +216,7 @@ class ComposeSettingsSheet extends HookWidget {
|
||||
|
||||
return SheetScaffold(
|
||||
titleText: 'postSettings'.tr(),
|
||||
heightFactor: 0.6,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
@@ -214,7 +225,7 @@ class ComposeSettingsSheet extends HookWidget {
|
||||
children: [
|
||||
// Tags field
|
||||
TextFieldTags(
|
||||
textfieldTagsController: tagsController,
|
||||
textfieldTagsController: state.tagsController,
|
||||
textSeparators: const [' ', ','],
|
||||
letterCase: LetterCase.normal,
|
||||
validator: (String tag) {
|
||||
@@ -233,22 +244,105 @@ class ComposeSettingsSheet extends HookWidget {
|
||||
),
|
||||
|
||||
// Categories field
|
||||
TextFieldTags(
|
||||
textfieldTagsController: categoriesController,
|
||||
textSeparators: const [' ', ','],
|
||||
letterCase: LetterCase.small,
|
||||
validator: (String tag) {
|
||||
if (tag.isEmpty) return 'No, cannot be empty';
|
||||
if (tag.contains(' ')) return 'Tags should be URL-safe';
|
||||
return null;
|
||||
},
|
||||
inputFieldBuilder: (context, inputFieldValues) {
|
||||
return ChipTagInputField(
|
||||
inputFieldValues: inputFieldValues,
|
||||
labelText: 'categories',
|
||||
hintText: 'categoriesHint',
|
||||
);
|
||||
// FIXME: Sometimes the entire dropdown crashes: 'package:flutter/src/rendering/stack.dart': Failed assertion: line 799 pos 12: 'firstChild == null || child != null': is not true.
|
||||
DropdownButtonFormField2<SnPostCategory>(
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 9),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
hint: Text('categories'.tr(), style: TextStyle(fontSize: 15)),
|
||||
items:
|
||||
(postCategories.value ?? <SnPostCategory>[]).map((item) {
|
||||
return DropdownMenuItem(
|
||||
value: item,
|
||||
enabled: false,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, menuSetState) {
|
||||
final isSelected = state.categories.value.contains(
|
||||
item,
|
||||
);
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
isSelected
|
||||
? state.categories.value =
|
||||
state.categories.value
|
||||
.where((e) => e != item)
|
||||
.toList()
|
||||
: state.categories.value = [
|
||||
...state.categories.value,
|
||||
item,
|
||||
];
|
||||
menuSetState(() {});
|
||||
},
|
||||
child: Container(
|
||||
height: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (isSelected)
|
||||
const Icon(Icons.check_box_outlined)
|
||||
else
|
||||
const Icon(Icons.check_box_outline_blank),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.categoryDisplayTitle,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
value: currentCategories.isEmpty ? null : currentCategories.last,
|
||||
onChanged: (_) {},
|
||||
selectedItemBuilder: (context) {
|
||||
return currentCategories.map((item) {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
for (final category in currentCategories)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
margin: const EdgeInsets.only(right: 4),
|
||||
child: Text(
|
||||
category.categoryDisplayTitle,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.only(left: 16, right: 8),
|
||||
height: 40,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 40,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
|
||||
// Visibility setting
|
||||
|
29
lib/widgets/post/compose_settings_sheet.g.dart
Normal file
29
lib/widgets/post/compose_settings_sheet.g.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'compose_settings_sheet.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$postCategoriesHash() => r'24337fe806d088b6468a350f62d5a5d40232a73c';
|
||||
|
||||
/// See also [postCategories].
|
||||
@ProviderFor(postCategories)
|
||||
final postCategoriesProvider =
|
||||
AutoDisposeFutureProvider<List<SnPostCategory>>.internal(
|
||||
postCategories,
|
||||
name: r'postCategoriesProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$postCategoriesHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef PostCategoriesRef = AutoDisposeFutureProviderRef<List<SnPostCategory>>;
|
||||
// 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
|
@@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/models/post_category.dart';
|
||||
import 'package:island/models/publisher.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
@@ -30,8 +31,8 @@ class ComposeState {
|
||||
final ValueNotifier<Map<int, double>> attachmentProgress;
|
||||
final ValueNotifier<SnPublisher?> currentPublisher;
|
||||
final ValueNotifier<bool> submitting;
|
||||
final ValueNotifier<List<SnPostCategory>> categories;
|
||||
StringTagController tagsController;
|
||||
StringTagController categoriesController;
|
||||
final String draftId;
|
||||
int postType;
|
||||
// Linked poll id for this compose session (nullable)
|
||||
@@ -48,7 +49,7 @@ class ComposeState {
|
||||
required this.currentPublisher,
|
||||
required this.submitting,
|
||||
required this.tagsController,
|
||||
required this.categoriesController,
|
||||
required this.categories,
|
||||
required this.draftId,
|
||||
this.postType = 0,
|
||||
String? pollId,
|
||||
@@ -80,11 +81,7 @@ class ComposeLogic {
|
||||
}) {
|
||||
final id = draftId ?? DateTime.now().millisecondsSinceEpoch.toString();
|
||||
final tagsController = StringTagController();
|
||||
final categoriesController = StringTagController();
|
||||
originalPost?.tags.forEach((x) => tagsController.addTag(x.slug));
|
||||
originalPost?.categories.forEach(
|
||||
(x) => categoriesController.addTag(x.slug),
|
||||
);
|
||||
return ComposeState(
|
||||
attachments: ValueNotifier<List<UniversalFile>>(
|
||||
originalPost?.attachments
|
||||
@@ -112,7 +109,9 @@ class ComposeLogic {
|
||||
attachmentProgress: ValueNotifier<Map<int, double>>({}),
|
||||
currentPublisher: ValueNotifier<SnPublisher?>(originalPost?.publisher),
|
||||
tagsController: tagsController,
|
||||
categoriesController: categoriesController,
|
||||
categories: ValueNotifier<List<SnPostCategory>>(
|
||||
originalPost?.categories ?? [],
|
||||
),
|
||||
draftId: id,
|
||||
postType: postType,
|
||||
// initialize without poll by default
|
||||
@@ -141,7 +140,7 @@ class ComposeLogic {
|
||||
attachmentProgress: ValueNotifier<Map<int, double>>({}),
|
||||
currentPublisher: ValueNotifier<SnPublisher?>(null),
|
||||
tagsController: tagsController,
|
||||
categoriesController: categoriesController,
|
||||
categories: ValueNotifier<List<SnPostCategory>>([]),
|
||||
draftId: draft.id,
|
||||
postType: postType,
|
||||
pollId: null,
|
||||
@@ -640,7 +639,7 @@ class ComposeLogic {
|
||||
if (repliedPost != null) 'replied_post_id': repliedPost.id,
|
||||
if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id,
|
||||
'tags': state.tagsController.getTags,
|
||||
'categories': state.categoriesController.getTags,
|
||||
'categories': state.categories.value.map((e) => e.slug).toList(),
|
||||
if (state.pollId.value != null) 'poll_id': state.pollId.value,
|
||||
};
|
||||
|
||||
@@ -697,7 +696,7 @@ class ComposeLogic {
|
||||
SnPost? repliedPost,
|
||||
SnPost? forwardedPost,
|
||||
}) {
|
||||
if (event is! RawKeyDownEvent) return;
|
||||
if (event is! KeyDownEvent) return;
|
||||
|
||||
final isPaste = event.logicalKey == LogicalKeyboardKey.keyV;
|
||||
final isSave = event.logicalKey == LogicalKeyboardKey.keyS;
|
||||
@@ -733,7 +732,7 @@ class ComposeLogic {
|
||||
state.attachmentProgress.dispose();
|
||||
state.currentPublisher.dispose();
|
||||
state.tagsController.dispose();
|
||||
state.categoriesController.dispose();
|
||||
state.categories.dispose();
|
||||
state.pollId.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -279,18 +279,14 @@ class _DraftItem extends StatelessWidget {
|
||||
|
||||
String _parseVisibility(int visibility) {
|
||||
switch (visibility) {
|
||||
case 0:
|
||||
return 'public'.tr();
|
||||
case 1:
|
||||
return 'unlisted'.tr();
|
||||
return 'postVisibilityFriends';
|
||||
case 2:
|
||||
return 'friends'.tr();
|
||||
return 'postVisibilityUnlisted';
|
||||
case 3:
|
||||
return 'selected'.tr();
|
||||
case 4:
|
||||
return 'private'.tr();
|
||||
return 'postVisibilityPrivate';
|
||||
default:
|
||||
return 'unknown'.tr();
|
||||
return 'postVisibilityPublic';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
105
lib/widgets/post/post_featured.dart
Normal file
105
lib/widgets/post/post_featured.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:island/widgets/post/post_item.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'post_featured.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<SnPost>> featuredPosts(Ref ref) async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get('/sphere/posts/featured');
|
||||
return resp.data.map((e) => SnPost.fromJson(e)).cast<SnPost>().toList();
|
||||
}
|
||||
|
||||
class PostFeaturedList extends HookConsumerWidget {
|
||||
const PostFeaturedList({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final featuredPostsAsync = ref.watch(featuredPostsProvider);
|
||||
|
||||
final pageViewController = usePageController();
|
||||
final pageViewCurrent = useState(0);
|
||||
|
||||
useEffect(() {
|
||||
pageViewController.addListener(() {
|
||||
pageViewCurrent.value = pageViewController.page?.round() ?? 0;
|
||||
});
|
||||
return null;
|
||||
}, [pageViewController]);
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Card(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
const Icon(Symbols.highlight),
|
||||
Text('Highlight Posts'),
|
||||
Spacer(),
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
pageViewController.animateToPage(
|
||||
pageViewCurrent.value - 1,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.arrow_left),
|
||||
),
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
pageViewController.animateToPage(
|
||||
pageViewCurrent.value + 1,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.arrow_right),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 8),
|
||||
featuredPostsAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(child: Text('Error: $error')),
|
||||
data: (posts) {
|
||||
return SizedBox(
|
||||
height: 320,
|
||||
child: PageView.builder(
|
||||
controller: pageViewController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: posts.length,
|
||||
itemBuilder: (context, index) {
|
||||
return SingleChildScrollView(
|
||||
child: PostActionableItem(
|
||||
item: posts[index],
|
||||
borderRadius: 8,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
28
lib/widgets/post/post_featured.g.dart
Normal file
28
lib/widgets/post/post_featured.g.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'post_featured.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$featuredPostsHash() => r'4b7fffb02eac72f5861b02af1b1e5da36b571698';
|
||||
|
||||
/// See also [featuredPosts].
|
||||
@ProviderFor(featuredPosts)
|
||||
final featuredPostsProvider = AutoDisposeFutureProvider<List<SnPost>>.internal(
|
||||
featuredPosts,
|
||||
name: r'featuredPostsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$featuredPostsHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef FeaturedPostsRef = AutoDisposeFutureProviderRef<List<SnPost>>;
|
||||
// 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
|
@@ -16,6 +16,7 @@ import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/screens/posts/compose.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/services/time.dart';
|
||||
import 'package:island/utils/mapping.dart';
|
||||
import 'package:island/widgets/account/account_name.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/cloud_file_collection.dart';
|
||||
@@ -314,6 +315,19 @@ class PostItem extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
String parseVisibility(int visibility) {
|
||||
switch (visibility) {
|
||||
case 1:
|
||||
return 'postVisibilityFriends';
|
||||
case 2:
|
||||
return 'postVisibilityUnlisted';
|
||||
case 3:
|
||||
return 'postVisibilityPrivate';
|
||||
default:
|
||||
return 'postVisibilityPublic';
|
||||
}
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -349,13 +363,27 @@ class PostItem extends HookConsumerWidget {
|
||||
Text('@${item.publisher.name}').fontSize(11),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
isFullPost
|
||||
? (item.publishedAt ?? item.createdAt)!.formatSystem()
|
||||
: (item.publishedAt ?? item.createdAt)!.formatRelative(
|
||||
context,
|
||||
),
|
||||
).fontSize(10),
|
||||
Row(
|
||||
spacing: 6,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
isFullPost
|
||||
? (item.publishedAt ?? item.createdAt)!
|
||||
.formatSystem()
|
||||
: (item.publishedAt ?? item.createdAt)!
|
||||
.formatRelative(context),
|
||||
).fontSize(10),
|
||||
if (item.editedAt != null)
|
||||
Text(
|
||||
'editedAt'.tr(args: [item.editedAt!.formatSystem()]),
|
||||
).fontSize(10),
|
||||
if (item.visibility != 0)
|
||||
Text(
|
||||
parseVisibility(item.visibility).tr(),
|
||||
).fontSize(10),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -543,39 +571,95 @@ class PostItem extends HookConsumerWidget {
|
||||
vertical: 4,
|
||||
),
|
||||
),
|
||||
if (item.tags.isNotEmpty)
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 2,
|
||||
children: [
|
||||
if (item.tags.isNotEmpty)
|
||||
Wrap(
|
||||
runAlignment: WrapAlignment.center,
|
||||
spacing: 8,
|
||||
children: [
|
||||
const Icon(Symbols.label, size: 16).padding(top: 2),
|
||||
for (final tag
|
||||
in isFullPost ? item.tags : item.tags.take(3))
|
||||
InkWell(
|
||||
child: Text('#${tag.name ?? tag.slug}'),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postTagDetail',
|
||||
pathParameters: {'slug': tag.slug},
|
||||
);
|
||||
},
|
||||
),
|
||||
if (!isFullPost && item.tags.length > 3)
|
||||
Text('+${item.tags.length - 3}').opacity(0.6),
|
||||
],
|
||||
),
|
||||
if (item.categories.isNotEmpty)
|
||||
Wrap(
|
||||
runAlignment: WrapAlignment.center,
|
||||
spacing: 8,
|
||||
children: [
|
||||
const Icon(Symbols.category, size: 16).padding(top: 2),
|
||||
for (final category
|
||||
in isFullPost
|
||||
? item.categories
|
||||
: item.categories.take(2))
|
||||
InkWell(
|
||||
child: Text(category.categoryDisplayTitle),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postCategoryDetail',
|
||||
pathParameters: {'slug': category.slug},
|
||||
);
|
||||
},
|
||||
),
|
||||
if (!isFullPost && item.categories.length > 2)
|
||||
Text('+${item.categories.length - 2}').opacity(0.6),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(horizontal: renderingPadding.horizontal + 4, top: 4),
|
||||
if (item.meta?['embeds'] != null)
|
||||
...((item.meta!['embeds'] as List<dynamic>).map(
|
||||
(embedData) => switch (embedData['type']) {
|
||||
'link' => EmbedLinkWidget(
|
||||
link: SnScrappedLink.fromJson(
|
||||
embedData as Map<String, dynamic>,
|
||||
),
|
||||
maxWidth: math.min(
|
||||
MediaQuery.of(context).size.width,
|
||||
kWideScreenWidth,
|
||||
),
|
||||
margin: EdgeInsets.only(
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
left: renderingPadding.horizontal,
|
||||
right: renderingPadding.horizontal,
|
||||
),
|
||||
),
|
||||
'poll' => Card(
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: renderingPadding.horizontal,
|
||||
vertical: 8,
|
||||
),
|
||||
child: PollSubmit(
|
||||
initialAnswers: embedData['poll']?['user_answer']?['answer'],
|
||||
stats: embedData['poll']?['stats'],
|
||||
poll: SnPollWithStats.fromJson(embedData['poll']),
|
||||
onSubmit: (_) {},
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
),
|
||||
_ => Text('Unable show embed: ${embedData['type']}'),
|
||||
},
|
||||
)),
|
||||
...((item.meta!['embeds'] as List<dynamic>)
|
||||
.map((embedData) => convertMapKeysToSnakeCase(embedData))
|
||||
.map(
|
||||
(embedData) => switch (embedData['type']) {
|
||||
'link' => EmbedLinkWidget(
|
||||
link: SnScrappedLink.fromJson(embedData),
|
||||
maxWidth: math.min(
|
||||
MediaQuery.of(context).size.width,
|
||||
kWideScreenWidth,
|
||||
),
|
||||
margin: EdgeInsets.only(
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
left: renderingPadding.horizontal,
|
||||
right: renderingPadding.horizontal,
|
||||
),
|
||||
),
|
||||
'poll' => Card(
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: renderingPadding.horizontal,
|
||||
vertical: 8,
|
||||
),
|
||||
child:
|
||||
embedData['poll'] == null
|
||||
? Text('Poll was not loaded...')
|
||||
: PollSubmit(
|
||||
initialAnswers:
|
||||
embedData['poll']?['user_answer']?['answer'],
|
||||
stats: embedData['poll']?['stats'],
|
||||
poll: SnPollWithStats.fromJson(embedData['poll']),
|
||||
onSubmit: (_) {},
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
),
|
||||
_ => Text('Unable show embed: ${embedData['type']}'),
|
||||
},
|
||||
)),
|
||||
if (isShowReference)
|
||||
_buildReferencePost(context, item, renderingPadding),
|
||||
if (item.repliesCount > 0 && isEmbedReply)
|
||||
@@ -781,10 +865,14 @@ class PostReplyPreview extends HookConsumerWidget {
|
||||
'/sphere/posts/${parent.id}/replies',
|
||||
queryParameters: {'offset': posts.value.length, 'take': pageSize},
|
||||
);
|
||||
posts.value = [
|
||||
...posts.value,
|
||||
...response.data.map((e) => SnPost.fromJson(e)),
|
||||
];
|
||||
try {
|
||||
posts.value = [
|
||||
...posts.value,
|
||||
...response.data.map((e) => SnPost.fromJson(e)),
|
||||
];
|
||||
} catch (_) {
|
||||
// ignore disposed
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
|
@@ -15,7 +15,12 @@ class PostListNotifier extends _$PostListNotifier
|
||||
static const int _pageSize = 20;
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<SnPost>> build(String? pubName, int? type) {
|
||||
Future<CursorPagingData<SnPost>> build(
|
||||
String? pubName, {
|
||||
int? type,
|
||||
List<String>? categories,
|
||||
List<String>? tags,
|
||||
}) {
|
||||
return fetch(cursor: null);
|
||||
}
|
||||
|
||||
@@ -29,6 +34,8 @@ class PostListNotifier extends _$PostListNotifier
|
||||
'take': _pageSize,
|
||||
if (pubName != null) 'pub': pubName,
|
||||
if (type != null) 'type': type,
|
||||
if (tags != null) 'tags': tags,
|
||||
if (categories != null) 'categories': categories,
|
||||
};
|
||||
|
||||
final response = await client.get(
|
||||
@@ -62,6 +69,8 @@ enum PostItemType {
|
||||
class SliverPostList extends HookConsumerWidget {
|
||||
final String? pubName;
|
||||
final int? type;
|
||||
final List<String>? categories;
|
||||
final List<String>? tags;
|
||||
final PostItemType itemType;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsets? padding;
|
||||
@@ -73,6 +82,8 @@ class SliverPostList extends HookConsumerWidget {
|
||||
super.key,
|
||||
this.pubName,
|
||||
this.type,
|
||||
this.categories,
|
||||
this.tags,
|
||||
this.itemType = PostItemType.regular,
|
||||
this.backgroundColor,
|
||||
this.padding,
|
||||
@@ -84,9 +95,26 @@ class SliverPostList extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return PagingHelperSliverView(
|
||||
provider: postListNotifierProvider(pubName, type),
|
||||
futureRefreshable: postListNotifierProvider(pubName, type).future,
|
||||
notifierRefreshable: postListNotifierProvider(pubName, type).notifier,
|
||||
provider: postListNotifierProvider(
|
||||
pubName,
|
||||
type: type,
|
||||
categories: categories,
|
||||
tags: tags,
|
||||
),
|
||||
futureRefreshable:
|
||||
postListNotifierProvider(
|
||||
pubName,
|
||||
type: type,
|
||||
categories: categories,
|
||||
tags: tags,
|
||||
).future,
|
||||
notifierRefreshable:
|
||||
postListNotifierProvider(
|
||||
pubName,
|
||||
type: type,
|
||||
categories: categories,
|
||||
tags: tags,
|
||||
).notifier,
|
||||
contentBuilder:
|
||||
(data, widgetCount, endItemView) => SliverList.builder(
|
||||
itemCount: widgetCount,
|
||||
|
@@ -6,7 +6,7 @@ part of 'post_list.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$postListNotifierHash() => r'78222d62957f85713d17aecd95af0305b764e86c';
|
||||
String _$postListNotifierHash() => r'2ca4f3cfbbcd04f3cc32e7f7bd511a5811042829';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
@@ -33,8 +33,15 @@ abstract class _$PostListNotifier
|
||||
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPost>> {
|
||||
late final String? pubName;
|
||||
late final int? type;
|
||||
late final List<String>? categories;
|
||||
late final List<String>? tags;
|
||||
|
||||
FutureOr<CursorPagingData<SnPost>> build(String? pubName, int? type);
|
||||
FutureOr<CursorPagingData<SnPost>> build(
|
||||
String? pubName, {
|
||||
int? type,
|
||||
List<String>? categories,
|
||||
List<String>? tags,
|
||||
});
|
||||
}
|
||||
|
||||
/// See also [PostListNotifier].
|
||||
@@ -48,15 +55,30 @@ class PostListNotifierFamily
|
||||
const PostListNotifierFamily();
|
||||
|
||||
/// See also [PostListNotifier].
|
||||
PostListNotifierProvider call(String? pubName, int? type) {
|
||||
return PostListNotifierProvider(pubName, type);
|
||||
PostListNotifierProvider call(
|
||||
String? pubName, {
|
||||
int? type,
|
||||
List<String>? categories,
|
||||
List<String>? tags,
|
||||
}) {
|
||||
return PostListNotifierProvider(
|
||||
pubName,
|
||||
type: type,
|
||||
categories: categories,
|
||||
tags: tags,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
PostListNotifierProvider getProviderOverride(
|
||||
covariant PostListNotifierProvider provider,
|
||||
) {
|
||||
return call(provider.pubName, provider.type);
|
||||
return call(
|
||||
provider.pubName,
|
||||
type: provider.type,
|
||||
categories: provider.categories,
|
||||
tags: provider.tags,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
@@ -82,24 +104,32 @@ class PostListNotifierProvider
|
||||
CursorPagingData<SnPost>
|
||||
> {
|
||||
/// See also [PostListNotifier].
|
||||
PostListNotifierProvider(String? pubName, int? type)
|
||||
: this._internal(
|
||||
() =>
|
||||
PostListNotifier()
|
||||
..pubName = pubName
|
||||
..type = type,
|
||||
from: postListNotifierProvider,
|
||||
name: r'postListNotifierProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$postListNotifierHash,
|
||||
dependencies: PostListNotifierFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
PostListNotifierFamily._allTransitiveDependencies,
|
||||
pubName: pubName,
|
||||
type: type,
|
||||
);
|
||||
PostListNotifierProvider(
|
||||
String? pubName, {
|
||||
int? type,
|
||||
List<String>? categories,
|
||||
List<String>? tags,
|
||||
}) : this._internal(
|
||||
() =>
|
||||
PostListNotifier()
|
||||
..pubName = pubName
|
||||
..type = type
|
||||
..categories = categories
|
||||
..tags = tags,
|
||||
from: postListNotifierProvider,
|
||||
name: r'postListNotifierProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$postListNotifierHash,
|
||||
dependencies: PostListNotifierFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
PostListNotifierFamily._allTransitiveDependencies,
|
||||
pubName: pubName,
|
||||
type: type,
|
||||
categories: categories,
|
||||
tags: tags,
|
||||
);
|
||||
|
||||
PostListNotifierProvider._internal(
|
||||
super._createNotifier, {
|
||||
@@ -110,16 +140,25 @@ class PostListNotifierProvider
|
||||
required super.from,
|
||||
required this.pubName,
|
||||
required this.type,
|
||||
required this.categories,
|
||||
required this.tags,
|
||||
}) : super.internal();
|
||||
|
||||
final String? pubName;
|
||||
final int? type;
|
||||
final List<String>? categories;
|
||||
final List<String>? tags;
|
||||
|
||||
@override
|
||||
FutureOr<CursorPagingData<SnPost>> runNotifierBuild(
|
||||
covariant PostListNotifier notifier,
|
||||
) {
|
||||
return notifier.build(pubName, type);
|
||||
return notifier.build(
|
||||
pubName,
|
||||
type: type,
|
||||
categories: categories,
|
||||
tags: tags,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -130,7 +169,9 @@ class PostListNotifierProvider
|
||||
() =>
|
||||
create()
|
||||
..pubName = pubName
|
||||
..type = type,
|
||||
..type = type
|
||||
..categories = categories
|
||||
..tags = tags,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
@@ -138,6 +179,8 @@ class PostListNotifierProvider
|
||||
debugGetCreateSourceHash: null,
|
||||
pubName: pubName,
|
||||
type: type,
|
||||
categories: categories,
|
||||
tags: tags,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -155,7 +198,9 @@ class PostListNotifierProvider
|
||||
bool operator ==(Object other) {
|
||||
return other is PostListNotifierProvider &&
|
||||
other.pubName == pubName &&
|
||||
other.type == type;
|
||||
other.type == type &&
|
||||
other.categories == categories &&
|
||||
other.tags == tags;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -163,6 +208,8 @@ class PostListNotifierProvider
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, pubName.hashCode);
|
||||
hash = _SystemHash.combine(hash, type.hashCode);
|
||||
hash = _SystemHash.combine(hash, categories.hashCode);
|
||||
hash = _SystemHash.combine(hash, tags.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
@@ -177,6 +224,12 @@ mixin PostListNotifierRef
|
||||
|
||||
/// The parameter `type` of this provider.
|
||||
int? get type;
|
||||
|
||||
/// The parameter `categories` of this provider.
|
||||
List<String>? get categories;
|
||||
|
||||
/// The parameter `tags` of this provider.
|
||||
List<String>? get tags;
|
||||
}
|
||||
|
||||
class _PostListNotifierProviderElement
|
||||
@@ -192,6 +245,11 @@ class _PostListNotifierProviderElement
|
||||
String? get pubName => (origin as PostListNotifierProvider).pubName;
|
||||
@override
|
||||
int? get type => (origin as PostListNotifierProvider).type;
|
||||
@override
|
||||
List<String>? get categories =>
|
||||
(origin as PostListNotifierProvider).categories;
|
||||
@override
|
||||
List<String>? get tags => (origin as PostListNotifierProvider).tags;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
@@ -2,6 +2,12 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CLIENT_ID</key>
|
||||
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
|
||||
<key>REVERSED_CLIENT_ID</key>
|
||||
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
|
||||
<key>ANDROID_CLIENT_ID</key>
|
||||
<string>961776991058-r4iv9qoio57ul7utbfpgfrda2etvtch8.apps.googleusercontent.com</string>
|
||||
<key>API_KEY</key>
|
||||
<string>AIzaSyCzQIyiYKoYHTpGXhN-IjgMML8z797WVD8</string>
|
||||
<key>GCM_SENDER_ID</key>
|
||||
|
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 3.1.0+117
|
||||
version: 3.1.0+118
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.2
|
||||
|
Reference in New Issue
Block a user