diff --git a/lib/models/post.dart b/lib/models/post.dart index f2c5b7d4..db5166d0 100644 --- a/lib/models/post.dart +++ b/lib/models/post.dart @@ -74,15 +74,15 @@ sealed class SnPublisherStats with _$SnPublisherStats { } @freezed -sealed class SnSubscriptionStatus with _$SnSubscriptionStatus { - const factory SnSubscriptionStatus({ - required bool isSubscribed, +sealed class SnPublisherSubscription with _$SnPublisherSubscription { + const factory SnPublisherSubscription({ + required String accountId, required String publisherId, - required String publisherName, - }) = _SnSubscriptionStatus; + required SnPublisher publisher, + }) = _SnPublisherSubscription; - factory SnSubscriptionStatus.fromJson(Map json) => - _$SnSubscriptionStatusFromJson(json); + factory SnPublisherSubscription.fromJson(Map json) => + _$SnPublisherSubscriptionFromJson(json); } @freezed @@ -92,8 +92,9 @@ sealed class ReactInfo with _$ReactInfo { static String getTranslationKey(String templateKey) { final parts = templateKey.split('_'); - final camelCase = - parts.map((p) => p[0].toUpperCase() + p.substring(1)).join(); + final camelCase = parts + .map((p) => p[0].toUpperCase() + p.substring(1)) + .join(); return 'reaction$camelCase'; } } diff --git a/lib/models/post.freezed.dart b/lib/models/post.freezed.dart index acdafeb8..ba4e3d41 100644 --- a/lib/models/post.freezed.dart +++ b/lib/models/post.freezed.dart @@ -856,72 +856,81 @@ as int, /// @nodoc -mixin _$SnSubscriptionStatus { +mixin _$SnPublisherSubscription { - bool get isSubscribed; String get publisherId; String get publisherName; -/// Create a copy of SnSubscriptionStatus + String get accountId; String get publisherId; SnPublisher get publisher; +/// Create a copy of SnPublisherSubscription /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline') -$SnSubscriptionStatusCopyWith get copyWith => _$SnSubscriptionStatusCopyWithImpl(this as SnSubscriptionStatus, _$identity); +$SnPublisherSubscriptionCopyWith get copyWith => _$SnPublisherSubscriptionCopyWithImpl(this as SnPublisherSubscription, _$identity); - /// Serializes this SnSubscriptionStatus to a JSON map. + /// Serializes this SnPublisherSubscription to a JSON map. Map toJson(); @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is SnSubscriptionStatus&&(identical(other.isSubscribed, isSubscribed) || other.isSubscribed == isSubscribed)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisherName, publisherName) || other.publisherName == publisherName)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublisherSubscription&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,isSubscribed,publisherId,publisherName); +int get hashCode => Object.hash(runtimeType,accountId,publisherId,publisher); @override String toString() { - return 'SnSubscriptionStatus(isSubscribed: $isSubscribed, publisherId: $publisherId, publisherName: $publisherName)'; + return 'SnPublisherSubscription(accountId: $accountId, publisherId: $publisherId, publisher: $publisher)'; } } /// @nodoc -abstract mixin class $SnSubscriptionStatusCopyWith<$Res> { - factory $SnSubscriptionStatusCopyWith(SnSubscriptionStatus value, $Res Function(SnSubscriptionStatus) _then) = _$SnSubscriptionStatusCopyWithImpl; +abstract mixin class $SnPublisherSubscriptionCopyWith<$Res> { + factory $SnPublisherSubscriptionCopyWith(SnPublisherSubscription value, $Res Function(SnPublisherSubscription) _then) = _$SnPublisherSubscriptionCopyWithImpl; @useResult $Res call({ - bool isSubscribed, String publisherId, String publisherName + String accountId, String publisherId, SnPublisher publisher }); - +$SnPublisherCopyWith<$Res> get publisher; } /// @nodoc -class _$SnSubscriptionStatusCopyWithImpl<$Res> - implements $SnSubscriptionStatusCopyWith<$Res> { - _$SnSubscriptionStatusCopyWithImpl(this._self, this._then); +class _$SnPublisherSubscriptionCopyWithImpl<$Res> + implements $SnPublisherSubscriptionCopyWith<$Res> { + _$SnPublisherSubscriptionCopyWithImpl(this._self, this._then); - final SnSubscriptionStatus _self; - final $Res Function(SnSubscriptionStatus) _then; + final SnPublisherSubscription _self; + final $Res Function(SnPublisherSubscription) _then; -/// Create a copy of SnSubscriptionStatus +/// Create a copy of SnPublisherSubscription /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? isSubscribed = null,Object? publisherId = null,Object? publisherName = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? accountId = null,Object? publisherId = null,Object? publisher = null,}) { return _then(_self.copyWith( -isSubscribed: null == isSubscribed ? _self.isSubscribed : isSubscribed // ignore: cast_nullable_to_non_nullable -as bool,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable -as String,publisherName: null == publisherName ? _self.publisherName : publisherName // ignore: cast_nullable_to_non_nullable -as String, +accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable +as String,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable +as String,publisher: null == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable +as SnPublisher, )); } - +/// Create a copy of SnPublisherSubscription +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnPublisherCopyWith<$Res> get publisher { + + return $SnPublisherCopyWith<$Res>(_self.publisher, (value) { + return _then(_self.copyWith(publisher: value)); + }); +} } -/// Adds pattern-matching-related methods to [SnSubscriptionStatus]. -extension SnSubscriptionStatusPatterns on SnSubscriptionStatus { +/// Adds pattern-matching-related methods to [SnPublisherSubscription]. +extension SnPublisherSubscriptionPatterns on SnPublisherSubscription { /// A variant of `map` that fallback to returning `orElse`. /// /// It is equivalent to doing: @@ -934,10 +943,10 @@ extension SnSubscriptionStatusPatterns on SnSubscriptionStatus { /// } /// ``` -@optionalTypeArgs TResult maybeMap(TResult Function( _SnSubscriptionStatus value)? $default,{required TResult orElse(),}){ +@optionalTypeArgs TResult maybeMap(TResult Function( _SnPublisherSubscription value)? $default,{required TResult orElse(),}){ final _that = this; switch (_that) { -case _SnSubscriptionStatus() when $default != null: +case _SnPublisherSubscription() when $default != null: return $default(_that);case _: return orElse(); @@ -956,10 +965,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult map(TResult Function( _SnSubscriptionStatus value) $default,){ +@optionalTypeArgs TResult map(TResult Function( _SnPublisherSubscription value) $default,){ final _that = this; switch (_that) { -case _SnSubscriptionStatus(): +case _SnPublisherSubscription(): return $default(_that);} } /// A variant of `map` that fallback to returning `null`. @@ -974,10 +983,10 @@ return $default(_that);} /// } /// ``` -@optionalTypeArgs TResult? mapOrNull(TResult? Function( _SnSubscriptionStatus value)? $default,){ +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _SnPublisherSubscription value)? $default,){ final _that = this; switch (_that) { -case _SnSubscriptionStatus() when $default != null: +case _SnPublisherSubscription() when $default != null: return $default(_that);case _: return null; @@ -995,10 +1004,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( bool isSubscribed, String publisherId, String publisherName)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String accountId, String publisherId, SnPublisher publisher)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { -case _SnSubscriptionStatus() when $default != null: -return $default(_that.isSubscribed,_that.publisherId,_that.publisherName);case _: +case _SnPublisherSubscription() when $default != null: +return $default(_that.accountId,_that.publisherId,_that.publisher);case _: return orElse(); } @@ -1016,10 +1025,10 @@ return $default(_that.isSubscribed,_that.publisherId,_that.publisherName);case _ /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( bool isSubscribed, String publisherId, String publisherName) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String accountId, String publisherId, SnPublisher publisher) $default,) {final _that = this; switch (_that) { -case _SnSubscriptionStatus(): -return $default(_that.isSubscribed,_that.publisherId,_that.publisherName);} +case _SnPublisherSubscription(): +return $default(_that.accountId,_that.publisherId,_that.publisher);} } /// A variant of `when` that fallback to returning `null` /// @@ -1033,10 +1042,10 @@ return $default(_that.isSubscribed,_that.publisherId,_that.publisherName);} /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool isSubscribed, String publisherId, String publisherName)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String accountId, String publisherId, SnPublisher publisher)? $default,) {final _that = this; switch (_that) { -case _SnSubscriptionStatus() when $default != null: -return $default(_that.isSubscribed,_that.publisherId,_that.publisherName);case _: +case _SnPublisherSubscription() when $default != null: +return $default(_that.accountId,_that.publisherId,_that.publisher);case _: return null; } @@ -1047,74 +1056,83 @@ return $default(_that.isSubscribed,_that.publisherId,_that.publisherName);case _ /// @nodoc @JsonSerializable() -class _SnSubscriptionStatus implements SnSubscriptionStatus { - const _SnSubscriptionStatus({required this.isSubscribed, required this.publisherId, required this.publisherName}); - factory _SnSubscriptionStatus.fromJson(Map json) => _$SnSubscriptionStatusFromJson(json); +class _SnPublisherSubscription implements SnPublisherSubscription { + const _SnPublisherSubscription({required this.accountId, required this.publisherId, required this.publisher}); + factory _SnPublisherSubscription.fromJson(Map json) => _$SnPublisherSubscriptionFromJson(json); -@override final bool isSubscribed; +@override final String accountId; @override final String publisherId; -@override final String publisherName; +@override final SnPublisher publisher; -/// Create a copy of SnSubscriptionStatus +/// Create a copy of SnPublisherSubscription /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline') -_$SnSubscriptionStatusCopyWith<_SnSubscriptionStatus> get copyWith => __$SnSubscriptionStatusCopyWithImpl<_SnSubscriptionStatus>(this, _$identity); +_$SnPublisherSubscriptionCopyWith<_SnPublisherSubscription> get copyWith => __$SnPublisherSubscriptionCopyWithImpl<_SnPublisherSubscription>(this, _$identity); @override Map toJson() { - return _$SnSubscriptionStatusToJson(this, ); + return _$SnPublisherSubscriptionToJson(this, ); } @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnSubscriptionStatus&&(identical(other.isSubscribed, isSubscribed) || other.isSubscribed == isSubscribed)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisherName, publisherName) || other.publisherName == publisherName)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublisherSubscription&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,isSubscribed,publisherId,publisherName); +int get hashCode => Object.hash(runtimeType,accountId,publisherId,publisher); @override String toString() { - return 'SnSubscriptionStatus(isSubscribed: $isSubscribed, publisherId: $publisherId, publisherName: $publisherName)'; + return 'SnPublisherSubscription(accountId: $accountId, publisherId: $publisherId, publisher: $publisher)'; } } /// @nodoc -abstract mixin class _$SnSubscriptionStatusCopyWith<$Res> implements $SnSubscriptionStatusCopyWith<$Res> { - factory _$SnSubscriptionStatusCopyWith(_SnSubscriptionStatus value, $Res Function(_SnSubscriptionStatus) _then) = __$SnSubscriptionStatusCopyWithImpl; +abstract mixin class _$SnPublisherSubscriptionCopyWith<$Res> implements $SnPublisherSubscriptionCopyWith<$Res> { + factory _$SnPublisherSubscriptionCopyWith(_SnPublisherSubscription value, $Res Function(_SnPublisherSubscription) _then) = __$SnPublisherSubscriptionCopyWithImpl; @override @useResult $Res call({ - bool isSubscribed, String publisherId, String publisherName + String accountId, String publisherId, SnPublisher publisher }); - +@override $SnPublisherCopyWith<$Res> get publisher; } /// @nodoc -class __$SnSubscriptionStatusCopyWithImpl<$Res> - implements _$SnSubscriptionStatusCopyWith<$Res> { - __$SnSubscriptionStatusCopyWithImpl(this._self, this._then); +class __$SnPublisherSubscriptionCopyWithImpl<$Res> + implements _$SnPublisherSubscriptionCopyWith<$Res> { + __$SnPublisherSubscriptionCopyWithImpl(this._self, this._then); - final _SnSubscriptionStatus _self; - final $Res Function(_SnSubscriptionStatus) _then; + final _SnPublisherSubscription _self; + final $Res Function(_SnPublisherSubscription) _then; -/// Create a copy of SnSubscriptionStatus +/// Create a copy of SnPublisherSubscription /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? isSubscribed = null,Object? publisherId = null,Object? publisherName = null,}) { - return _then(_SnSubscriptionStatus( -isSubscribed: null == isSubscribed ? _self.isSubscribed : isSubscribed // ignore: cast_nullable_to_non_nullable -as bool,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable -as String,publisherName: null == publisherName ? _self.publisherName : publisherName // ignore: cast_nullable_to_non_nullable -as String, +@override @pragma('vm:prefer-inline') $Res call({Object? accountId = null,Object? publisherId = null,Object? publisher = null,}) { + return _then(_SnPublisherSubscription( +accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable +as String,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable +as String,publisher: null == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable +as SnPublisher, )); } - +/// Create a copy of SnPublisherSubscription +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnPublisherCopyWith<$Res> get publisher { + + return $SnPublisherCopyWith<$Res>(_self.publisher, (value) { + return _then(_self.copyWith(publisher: value)); + }); +} } /// @nodoc diff --git a/lib/models/post.g.dart b/lib/models/post.g.dart index 958c52bf..d74b0c42 100644 --- a/lib/models/post.g.dart +++ b/lib/models/post.g.dart @@ -158,20 +158,20 @@ Map _$SnPublisherStatsToJson(_SnPublisherStats instance) => 'downvote_received': instance.downvoteReceived, }; -_SnSubscriptionStatus _$SnSubscriptionStatusFromJson( +_SnPublisherSubscription _$SnPublisherSubscriptionFromJson( Map json, -) => _SnSubscriptionStatus( - isSubscribed: json['is_subscribed'] as bool, +) => _SnPublisherSubscription( + accountId: json['account_id'] as String, publisherId: json['publisher_id'] as String, - publisherName: json['publisher_name'] as String, + publisher: SnPublisher.fromJson(json['publisher'] as Map), ); -Map _$SnSubscriptionStatusToJson( - _SnSubscriptionStatus instance, +Map _$SnPublisherSubscriptionToJson( + _SnPublisherSubscription instance, ) => { - 'is_subscribed': instance.isSubscribed, + 'account_id': instance.accountId, 'publisher_id': instance.publisherId, - 'publisher_name': instance.publisherName, + 'publisher': instance.publisher.toJson(), }; _SnPostEmbedView _$SnPostEmbedViewFromJson(Map json) => diff --git a/lib/pods/config.g.dart b/lib/pods/config.g.dart index 14f3d5e7..95cab849 100644 --- a/lib/pods/config.g.dart +++ b/lib/pods/config.g.dart @@ -65,7 +65,7 @@ final class AppSettingsNotifierProvider } String _$appSettingsNotifierHash() => - r'8e6e901b8a91f9944e1f4dd5d96507e75cd1de81'; + r'ee6b67190f3db5d8cb8a9e438a444e91685927d4'; abstract class _$AppSettingsNotifier extends $Notifier { AppSettings build(); diff --git a/lib/pods/post/post_subscriptions.dart b/lib/pods/post/post_subscriptions.dart new file mode 100644 index 00000000..08035170 --- /dev/null +++ b/lib/pods/post/post_subscriptions.dart @@ -0,0 +1,16 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/models/post.dart'; +import 'package:island/pods/network.dart'; + +final subscriptionsProvider = FutureProvider>(( + ref, +) async { + final client = ref.read(apiClientProvider); + + final response = await client.get('/sphere/subscriptions'); + + return response.data + .map((json) => SnPublisherSubscription.fromJson(json)) + .cast() + .toList(); +}); diff --git a/lib/screens/creators/publishers_form.dart b/lib/screens/creators/publishers_form.dart index f64fff0f..7656a9bd 100644 --- a/lib/screens/creators/publishers_form.dart +++ b/lib/screens/creators/publishers_form.dart @@ -35,7 +35,7 @@ Future> publishersManaged(Ref ref) async { } @riverpod -Future publisher(Ref ref, String? identifier) async { +Future publisherNullable(Ref ref, String? identifier) async { if (identifier == null) return null; final client = ref.watch(apiClientProvider); final resp = await client.get('/sphere/publishers/$identifier'); @@ -93,14 +93,10 @@ class EditPublisherScreen extends HookConsumerWidget { submitting.value = true; try { - final cloudFile = - await FileUploader.createCloudFile( - ref: ref, - fileData: UniversalFile( - data: result, - type: UniversalFileType.image, - ), - ).future; + final cloudFile = await FileUploader.createCloudFile( + ref: ref, + fileData: UniversalFile(data: result, type: UniversalFileType.image), + ).future; if (cloudFile == null) { throw ArgumentError('Failed to upload the file...'); } @@ -118,7 +114,7 @@ class EditPublisherScreen extends HookConsumerWidget { } } - final publisher = ref.watch(publisherProvider(name)); + final publisher = ref.watch(publisherNullableProvider(name)); final formKey = useMemoized(GlobalKey.new, const []); final nameController = useTextEditingController( @@ -155,8 +151,8 @@ class EditPublisherScreen extends HookConsumerWidget { final resp = await client.request( '/sphere${name == null ? currentRealm.value == null - ? '/publishers/individual' - : '/publishers/organization/${currentRealm.value!.slug}' + ? '/publishers/individual' + : '/publishers/organization/${currentRealm.value!.slug}' : '/publishers/$name'}', data: { 'name': nameController.text, @@ -194,13 +190,12 @@ class EditPublisherScreen extends HookConsumerWidget { GestureDetector( child: Container( color: Theme.of(context).colorScheme.surfaceContainerHigh, - child: - background.value != null - ? CloudImageWidget( - fileId: background.value!, - fit: BoxFit.cover, - ) - : const SizedBox.shrink(), + child: background.value != null + ? CloudImageWidget( + fileId: background.value!, + fit: BoxFit.cover, + ) + : const SizedBox.shrink(), ), onTap: () { setPicture('background'); @@ -238,14 +233,14 @@ class EditPublisherScreen extends HookConsumerWidget { prefixText: '@', ), readOnly: name != null, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), TextFormField( controller: nickController, decoration: InputDecoration(labelText: 'nickname'.tr()), - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), TextFormField( controller: bioController, @@ -255,8 +250,8 @@ class EditPublisherScreen extends HookConsumerWidget { ), minLines: 3, maxLines: null, - onTapOutside: - (_) => FocusManager.instance.primaryFocus?.unfocus(), + onTapOutside: (_) => + FocusManager.instance.primaryFocus?.unfocus(), ), DropdownButtonFormField( value: currentRealm.value, @@ -267,22 +262,20 @@ class EditPublisherScreen extends HookConsumerWidget { child: Text('individual'.tr()), ), ...joinedRealms.maybeWhen( - data: - (realms) => realms.map( - (realm) => DropdownMenuItem( - value: realm, - child: Text(realm.name), - ), - ), + data: (realms) => realms.map( + (realm) => DropdownMenuItem( + value: realm, + child: Text(realm.name), + ), + ), orElse: () => [], ), ], - onChanged: - joinedRealms.isLoading - ? null - : (SnRealm? value) { - currentRealm.value = value; - }, + onChanged: joinedRealms.isLoading + ? null + : (SnRealm? value) { + currentRealm.value = value; + }, ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -307,20 +300,18 @@ class EditPublisherScreen extends HookConsumerWidget { currentRealm.value!.background?.id; } }, - label: - Text( - currentRealm.value == null - ? 'syncPublisher' - : 'syncPublisherRealm', - ).tr(), + label: Text( + currentRealm.value == null + ? 'syncPublisher' + : 'syncPublisherRealm', + ).tr(), icon: const Icon(Symbols.link), ), TextButton.icon( onPressed: submitting.value ? null : performAction, - label: - Text( - name == null ? 'create' : 'saveChanges', - ).tr(), + label: Text( + name == null ? 'create' : 'saveChanges', + ).tr(), icon: const Icon(Symbols.save), ), ], diff --git a/lib/screens/creators/publishers_form.g.dart b/lib/screens/creators/publishers_form.g.dart index 8fd657f6..411e90e4 100644 --- a/lib/screens/creators/publishers_form.g.dart +++ b/lib/screens/creators/publishers_form.g.dart @@ -50,10 +50,10 @@ final class PublishersManagedProvider String _$publishersManagedHash() => r'ea83759fed9bd5119738b4d09f12b4476959e0a3'; -@ProviderFor(publisher) -const publisherProvider = PublisherFamily._(); +@ProviderFor(publisherNullable) +const publisherNullableProvider = PublisherNullableFamily._(); -final class PublisherProvider +final class PublisherNullableProvider extends $FunctionalProvider< AsyncValue, @@ -61,23 +61,23 @@ final class PublisherProvider FutureOr > with $FutureModifier, $FutureProvider { - const PublisherProvider._({ - required PublisherFamily super.from, + const PublisherNullableProvider._({ + required PublisherNullableFamily super.from, required String? super.argument, }) : super( retry: null, - name: r'publisherProvider', + name: r'publisherNullableProvider', isAutoDispose: true, dependencies: null, $allTransitiveDependencies: null, ); @override - String debugGetCreateSourceHash() => _$publisherHash(); + String debugGetCreateSourceHash() => _$publisherNullableHash(); @override String toString() { - return r'publisherProvider' + return r'publisherNullableProvider' '' '($argument)'; } @@ -91,12 +91,12 @@ final class PublisherProvider @override FutureOr create(Ref ref) { final argument = this.argument as String?; - return publisher(ref, argument); + return publisherNullable(ref, argument); } @override bool operator ==(Object other) { - return other is PublisherProvider && other.argument == argument; + return other is PublisherNullableProvider && other.argument == argument; } @override @@ -105,22 +105,22 @@ final class PublisherProvider } } -String _$publisherHash() => r'18fb5c6b3d79dd8af4fbee108dec1a0e8a034038'; +String _$publisherNullableHash() => r'49b28083a2f351c5e5cde0b1a97f6c7503969041'; -final class PublisherFamily extends $Family +final class PublisherNullableFamily extends $Family with $FunctionalFamilyOverride, String?> { - const PublisherFamily._() + const PublisherNullableFamily._() : super( retry: null, - name: r'publisherProvider', + name: r'publisherNullableProvider', dependencies: null, $allTransitiveDependencies: null, isAutoDispose: true, ); - PublisherProvider call(String? identifier) => - PublisherProvider._(argument: identifier, from: this); + PublisherNullableProvider call(String? identifier) => + PublisherNullableProvider._(argument: identifier, from: this); @override - String toString() => r'publisherProvider'; + String toString() => r'publisherNullableProvider'; } diff --git a/lib/screens/posts/publisher_profile.dart b/lib/screens/posts/publisher_profile.dart index a08a2366..2439ba90 100644 --- a/lib/screens/posts/publisher_profile.dart +++ b/lib/screens/posts/publisher_profile.dart @@ -33,7 +33,7 @@ part 'publisher_profile.g.dart'; class _PublisherBasisWidget extends StatelessWidget { final SnPublisher data; - final AsyncValue subStatus; + final AsyncValue subStatus; final ValueNotifier subscribing; final VoidCallback subscribe; final VoidCallback unsubscribe; @@ -208,16 +208,16 @@ class _PublisherBasisWidget extends StatelessWidget { data: (status) => FilledButton.icon( onPressed: subscribing.value ? null - : (status.isSubscribed + : (status != null ? unsubscribe : subscribe), icon: Icon( - status.isSubscribed + status != null ? Symbols.remove_circle : Symbols.add_circle, ), label: Text( - status.isSubscribed + status != null ? 'unsubscribe' : 'subscribe', ).tr(), @@ -366,13 +366,16 @@ Future> publisherBadges(Ref ref, String pubName) async { } @riverpod -Future publisherSubscriptionStatus( +Future publisherSubscriptionStatus( Ref ref, String pubName, ) async { final apiClient = ref.watch(apiClientProvider); final resp = await apiClient.get("/sphere/publishers/$pubName/subscription"); - return SnSubscriptionStatus.fromJson(resp.data); + if (resp.statusCode == 200) { + return SnPublisherSubscription.fromJson(resp.data); + } + return null; } @riverpod diff --git a/lib/screens/posts/publisher_profile.g.dart b/lib/screens/posts/publisher_profile.g.dart index 080b2adf..786e20f9 100644 --- a/lib/screens/posts/publisher_profile.g.dart +++ b/lib/screens/posts/publisher_profile.g.dart @@ -168,13 +168,13 @@ const publisherSubscriptionStatusProvider = final class PublisherSubscriptionStatusProvider extends $FunctionalProvider< - AsyncValue, - SnSubscriptionStatus, - FutureOr + AsyncValue, + SnPublisherSubscription?, + FutureOr > with - $FutureModifier, - $FutureProvider { + $FutureModifier, + $FutureProvider { const PublisherSubscriptionStatusProvider._({ required PublisherSubscriptionStatusFamily super.from, required String super.argument, @@ -198,12 +198,12 @@ final class PublisherSubscriptionStatusProvider @$internal @override - $FutureProviderElement $createElement( + $FutureProviderElement $createElement( $ProviderPointer pointer, ) => $FutureProviderElement(pointer); @override - FutureOr create(Ref ref) { + FutureOr create(Ref ref) { final argument = this.argument as String; return publisherSubscriptionStatus(ref, argument); } @@ -221,10 +221,10 @@ final class PublisherSubscriptionStatusProvider } String _$publisherSubscriptionStatusHash() => - r'634262ce519e1c8288267df11e08e1d4acaa4a44'; + r'accf6a0cdf98f8b0474d94ac575e8b20448adc79'; final class PublisherSubscriptionStatusFamily extends $Family - with $FunctionalFamilyOverride, String> { + with $FunctionalFamilyOverride, String> { const PublisherSubscriptionStatusFamily._() : super( retry: null, diff --git a/lib/widgets/content/markdown.dart b/lib/widgets/content/markdown.dart index 2740181f..3251b959 100644 --- a/lib/widgets/content/markdown.dart +++ b/lib/widgets/content/markdown.dart @@ -12,7 +12,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/file.dart'; import 'package:island/pods/config.dart'; import 'package:island/screens/account/profile.dart'; -import 'package:island/screens/creators/publishers_form.dart'; +import 'package:island/screens/posts/publisher_profile.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_file_lightbox.dart'; @@ -64,14 +64,16 @@ class MarkdownTextContent extends HookConsumerWidget { final matches = stickerPattern.allMatches(content); // Content should only contain one sticker and nothing else (except whitespace) - final contentWithoutStickers = - content.replaceAll(stickerPattern, '').trim(); + final contentWithoutStickers = content + .replaceAll(stickerPattern, '') + .trim(); return matches.length == 1 && contentWithoutStickers.isEmpty; }, [content]); final isDark = Theme.of(context).brightness == Brightness.dark; - final config = - isDark ? MarkdownConfig.darkConfig : MarkdownConfig.defaultConfig; + final config = isDark + ? MarkdownConfig.darkConfig + : MarkdownConfig.defaultConfig; final onMentionTap = useCallback((String type, String id) { final fullPath = '/$type/$id'; @@ -128,11 +130,10 @@ class MarkdownTextContent extends HookConsumerWidget { ), ), TableConfig( - wrapper: - (child) => SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: child, - ), + wrapper: (child) => SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: child, + ), ), LinkConfig( style: @@ -203,8 +204,9 @@ class MarkdownTextContent extends HookConsumerWidget { ), child: Container( decoration: BoxDecoration( - color: - Theme.of(context).colorScheme.surfaceContainer, + color: Theme.of( + context, + ).colorScheme.surfaceContainer, borderRadius: const BorderRadius.all( Radius.circular(8), ), @@ -288,11 +290,10 @@ class _MetionInlineSyntax extends markdown.InlineSyntax { "c" => 'chat', _ => '', }; - final element = - markdown.Element('mention-chip', [markdown.Text(alias)]) - ..attributes['alias'] = alias - ..attributes['type'] = type - ..attributes['id'] = parts.last; + final element = markdown.Element('mention-chip', [markdown.Text(alias)]) + ..attributes['alias'] = alias + ..attributes['type'] = type + ..attributes['id'] = parts.last; parser.addNode(element); return true; @@ -373,18 +374,19 @@ class MentionChipGenerator extends SpanNodeGeneratorWithTag { required void Function(String type, String id) onTap, }) : super( tag: 'mention-chip', - generator: ( - markdown.Element element, - MarkdownConfig config, - WidgetVisitor visitor, - ) { - return MentionChipSpanNode( - attributes: element.attributes, - backgroundColor: backgroundColor, - foregroundColor: foregroundColor, - onTap: onTap, - ); - }, + generator: + ( + markdown.Element element, + MarkdownConfig config, + WidgetVisitor visitor, + ) { + return MentionChipSpanNode( + attributes: element.attributes, + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + onTap: onTap, + ); + }, ); } @@ -440,19 +442,17 @@ class MentionChipSpanNode extends SpanNode { builder: (context, ref, _) { final userData = ref.watch(accountProvider(parts.last)); return userData.when( - data: - (data) => ProfilePictureWidget( - file: data.profile.picture, - fallbackIcon: Symbols.person_rounded, - radius: 9, - ), + data: (data) => ProfilePictureWidget( + file: data.profile.picture, + fallbackIcon: Symbols.person_rounded, + radius: 9, + ), error: (_, _) => const Icon(Symbols.close), - loading: - () => const SizedBox( - width: 9, - height: 9, - child: CircularProgressIndicator(), - ), + loading: () => const SizedBox( + width: 9, + height: 9, + child: CircularProgressIndicator(), + ), ); }, ), @@ -460,19 +460,17 @@ class MentionChipSpanNode extends SpanNode { builder: (context, ref, _) { final pubData = ref.watch(publisherProvider(parts.last)); return pubData.when( - data: - (data) => ProfilePictureWidget( - file: data?.picture, - fallbackIcon: Symbols.design_services_rounded, - radius: 9, - ), + data: (data) => ProfilePictureWidget( + file: data.picture, + fallbackIcon: Symbols.design_services_rounded, + radius: 9, + ), error: (_, _) => const Icon(Symbols.close), - loading: - () => const SizedBox( - width: 9, - height: 9, - child: CircularProgressIndicator(), - ), + loading: () => const SizedBox( + width: 9, + height: 9, + child: CircularProgressIndicator(), + ), ); }, ), @@ -508,16 +506,17 @@ class HighlightGenerator extends SpanNodeGeneratorWithTag { HighlightGenerator({required Color highlightColor}) : super( tag: 'highlight', - generator: ( - markdown.Element element, - MarkdownConfig config, - WidgetVisitor visitor, - ) { - return HighlightSpanNode( - text: element.textContent, - highlightColor: highlightColor, - ); - }, + generator: + ( + markdown.Element element, + MarkdownConfig config, + WidgetVisitor visitor, + ) { + return HighlightSpanNode( + text: element.textContent, + highlightColor: highlightColor, + ); + }, ); } @@ -545,20 +544,21 @@ class SpoilerGenerator extends SpanNodeGeneratorWithTag { required VoidCallback onToggle, }) : super( tag: 'spoiler', - generator: ( - markdown.Element element, - MarkdownConfig config, - WidgetVisitor visitor, - ) { - return SpoilerSpanNode( - text: element.textContent, - backgroundColor: backgroundColor, - foregroundColor: foregroundColor, - outlineColor: outlineColor, - revealed: revealed, - onToggle: onToggle, - ); - }, + generator: + ( + markdown.Element element, + MarkdownConfig config, + WidgetVisitor visitor, + ) { + return SpoilerSpanNode( + text: element.textContent, + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + outlineColor: outlineColor, + revealed: revealed, + onToggle: onToggle, + ); + }, ); } @@ -591,35 +591,33 @@ class SpoilerSpanNode extends SpanNode { border: revealed ? Border.all(color: outlineColor, width: 1) : null, borderRadius: BorderRadius.circular(4), ), - child: - revealed - ? Row( - spacing: 6, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Symbols.visibility, size: 18).padding(top: 1), - Flexible(child: Text(text)), - ], - ) - : Row( - spacing: 6, - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Symbols.visibility_off, - color: foregroundColor, - size: 18, - ), - Flexible( - child: - Text( - 'spoiler', - style: TextStyle(color: foregroundColor), - ).tr(), - ), - ], - ), + child: revealed + ? Row( + spacing: 6, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Symbols.visibility, size: 18).padding(top: 1), + Flexible(child: Text(text)), + ], + ) + : Row( + spacing: 6, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Symbols.visibility_off, + color: foregroundColor, + size: 18, + ), + Flexible( + child: Text( + 'spoiler', + style: TextStyle(color: foregroundColor), + ).tr(), + ), + ], + ), ), ), ); @@ -634,19 +632,20 @@ class StickerGenerator extends SpanNodeGeneratorWithTag { required String baseUrl, }) : super( tag: 'sticker', - generator: ( - markdown.Element element, - MarkdownConfig config, - WidgetVisitor visitor, - ) { - return StickerSpanNode( - placeholder: element.textContent, - backgroundColor: backgroundColor, - foregroundColor: foregroundColor, - isEnlarged: isEnlarged, - baseUrl: baseUrl, - ); - }, + generator: + ( + markdown.Element element, + MarkdownConfig config, + WidgetVisitor visitor, + ) { + return StickerSpanNode( + placeholder: element.textContent, + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + isEnlarged: isEnlarged, + baseUrl: baseUrl, + ); + }, ); } diff --git a/lib/widgets/posts/post_filter.dart b/lib/widgets/posts/post_filter.dart index e3635a16..b05797fb 100644 --- a/lib/widgets/posts/post_filter.dart +++ b/lib/widgets/posts/post_filter.dart @@ -1,10 +1,12 @@ 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/pods/post/post_list.dart'; import 'package:material_symbols_icons/symbols.dart'; -class PostFilterWidget extends StatefulWidget { +class PostFilterWidget extends HookConsumerWidget { final TabController categoryTabController; final PostListQuery initialQuery; final ValueChanged onQueryChanged; @@ -19,79 +21,55 @@ class PostFilterWidget extends StatefulWidget { }); @override - State createState() => _PostFilterWidgetState(); -} - -class _PostFilterWidgetState extends State { - late bool? _includeReplies; - late bool _mediaOnly; - late String? _queryTerm; - late String? _order; - late bool _orderDesc; - late int? _periodStart; - late int? _periodEnd; - late int? _type; - late bool _showAdvancedFilters; - late TextEditingController _searchController; - - @override - void initState() { - super.initState(); - _includeReplies = widget.initialQuery.includeReplies; - _mediaOnly = widget.initialQuery.mediaOnly ?? false; - _queryTerm = widget.initialQuery.queryTerm; - _order = widget.initialQuery.order; - _orderDesc = widget.initialQuery.orderDesc; - _periodStart = widget.initialQuery.periodStart; - _periodEnd = widget.initialQuery.periodEnd; - _type = widget.initialQuery.type; - _showAdvancedFilters = false; - _searchController = TextEditingController(text: _queryTerm); - - widget.categoryTabController.addListener(_onTabChanged); - } - - @override - void dispose() { - widget.categoryTabController.removeListener(_onTabChanged); - _searchController.dispose(); - super.dispose(); - } - - void _onTabChanged() { - final tabIndex = widget.categoryTabController.index; - setState(() { - _type = switch (tabIndex) { - 1 => 0, - 2 => 1, - _ => null, - }; - }); - _updateQuery(); - } - - void _updateQuery() { - final newQuery = widget.initialQuery.copyWith( - includeReplies: _includeReplies, - mediaOnly: _mediaOnly, - queryTerm: _queryTerm, - order: _order, - periodStart: _periodStart, - periodEnd: _periodEnd, - orderDesc: _orderDesc, - type: _type, + Widget build(BuildContext context, WidgetRef ref) { + final includeReplies = useState(initialQuery.includeReplies); + final mediaOnly = useState(initialQuery.mediaOnly ?? false); + final queryTerm = useState(initialQuery.queryTerm); + final order = useState(initialQuery.order); + final orderDesc = useState(initialQuery.orderDesc); + final periodStart = useState(initialQuery.periodStart); + final periodEnd = useState(initialQuery.periodEnd); + final type = useState(initialQuery.type); + final showAdvancedFilters = useState(false); + final searchController = useTextEditingController( + text: initialQuery.queryTerm, ); - widget.onQueryChanged(newQuery); - } - @override - Widget build(BuildContext context) { + void updateQuery() { + final newQuery = initialQuery.copyWith( + includeReplies: includeReplies.value, + mediaOnly: mediaOnly.value, + queryTerm: queryTerm.value, + order: order.value, + periodStart: periodStart.value, + periodEnd: periodEnd.value, + orderDesc: orderDesc.value, + type: type.value, + ); + onQueryChanged(newQuery); + } + + useEffect(() { + void onTabChanged() { + final tabIndex = categoryTabController.index; + type.value = switch (tabIndex) { + 1 => 0, + 2 => 1, + _ => null, + }; + updateQuery(); + } + + categoryTabController.addListener(onTabChanged); + return () => categoryTabController.removeListener(onTabChanged); + }, [categoryTabController]); + return Card( margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Column( children: [ TabBar( - controller: widget.categoryTabController, + controller: categoryTabController, dividerColor: Colors.transparent, splashBorderRadius: const BorderRadius.all(Radius.circular(8)), tabs: [ @@ -108,20 +86,18 @@ class _PostFilterWidgetState extends State { Expanded( child: CheckboxListTile( title: Text('reply'.tr()), - value: _includeReplies, + value: includeReplies.value, tristate: true, onChanged: (value) { - // Cycle through: null -> false -> true -> null - setState(() { - if (_includeReplies == null) { - _includeReplies = false; - } else if (_includeReplies == false) { - _includeReplies = true; - } else { - _includeReplies = null; - } - }); - _updateQuery(); + final current = includeReplies.value; + if (current == null) { + includeReplies.value = false; + } else if (current == false) { + includeReplies.value = true; + } else { + includeReplies.value = null; + } + updateQuery(); }, dense: true, controlAffinity: ListTileControlAffinity.leading, @@ -131,14 +107,12 @@ class _PostFilterWidgetState extends State { Expanded( child: CheckboxListTile( title: Text('attachments'.tr()), - value: _mediaOnly, + value: mediaOnly.value, onChanged: (value) { - setState(() { - if (value != null) { - _mediaOnly = value; - } - }); - _updateQuery(); + if (value != null) { + mediaOnly.value = value; + } + updateQuery(); }, dense: true, controlAffinity: ListTileControlAffinity.leading, @@ -149,14 +123,12 @@ class _PostFilterWidgetState extends State { ), CheckboxListTile( title: Text('descendingOrder'.tr()), - value: _orderDesc, + value: orderDesc.value, onChanged: (value) { - setState(() { - if (value != null) { - _orderDesc = value; - } - }); - _updateQuery(); + if (value != null) { + orderDesc.value = value; + } + updateQuery(); }, dense: true, controlAffinity: ListTileControlAffinity.leading, @@ -173,24 +145,24 @@ class _PostFilterWidgetState extends State { borderRadius: BorderRadius.all(const Radius.circular(8)), ), trailing: Icon( - _showAdvancedFilters ? Symbols.expand_less : Symbols.expand_more, + showAdvancedFilters.value + ? Symbols.expand_less + : Symbols.expand_more, ), onTap: () { - setState(() { - _showAdvancedFilters = !_showAdvancedFilters; - }); + showAdvancedFilters.value = !showAdvancedFilters.value; }, ), - if (_showAdvancedFilters) ...[ + if (showAdvancedFilters.value) ...[ const Divider(height: 1), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (!widget.hideSearch) + if (!hideSearch) TextField( - controller: _searchController, + controller: searchController, decoration: InputDecoration( labelText: 'search'.tr(), hintText: 'searchPosts'.tr(), @@ -204,13 +176,11 @@ class _PostFilterWidgetState extends State { ), ), onChanged: (value) { - setState(() { - _queryTerm = value.isEmpty ? null : value; - }); - _updateQuery(); + queryTerm.value = value.isEmpty ? null : value; + updateQuery(); }, ), - if (!widget.hideSearch) const Gap(12), + if (!hideSearch) const Gap(12), DropdownButtonFormField( decoration: InputDecoration( labelText: 'sortBy'.tr(), @@ -222,7 +192,7 @@ class _PostFilterWidgetState extends State { vertical: 8, ), ), - value: _order, + value: order.value, items: [ DropdownMenuItem(value: 'date', child: Text('date'.tr())), DropdownMenuItem( @@ -231,10 +201,8 @@ class _PostFilterWidgetState extends State { ), ], onChanged: (value) { - setState(() { - _order = value; - }); - _updateQuery(); + order.value = value; + updateQuery(); }, ), const Gap(12), @@ -245,9 +213,9 @@ class _PostFilterWidgetState extends State { onTap: () async { final pickedDate = await showDatePicker( context: context, - initialDate: _periodStart != null + initialDate: periodStart.value != null ? DateTime.fromMillisecondsSinceEpoch( - _periodStart! * 1000, + periodStart.value! * 1000, ) : DateTime.now(), firstDate: DateTime(2000), @@ -256,11 +224,9 @@ class _PostFilterWidgetState extends State { ), ); if (pickedDate != null) { - setState(() { - _periodStart = - pickedDate.millisecondsSinceEpoch ~/ 1000; - }); - _updateQuery(); + periodStart.value = + pickedDate.millisecondsSinceEpoch ~/ 1000; + updateQuery(); } }, child: InputDecorator( @@ -278,9 +244,9 @@ class _PostFilterWidgetState extends State { suffixIcon: const Icon(Symbols.calendar_today), ), child: Text( - _periodStart != null + periodStart.value != null ? DateTime.fromMillisecondsSinceEpoch( - _periodStart! * 1000, + periodStart.value! * 1000, ).toString().split(' ')[0] : 'selectDate'.tr(), ), @@ -293,9 +259,9 @@ class _PostFilterWidgetState extends State { onTap: () async { final pickedDate = await showDatePicker( context: context, - initialDate: _periodEnd != null + initialDate: periodEnd.value != null ? DateTime.fromMillisecondsSinceEpoch( - _periodEnd! * 1000, + periodEnd.value! * 1000, ) : DateTime.now(), firstDate: DateTime(2000), @@ -304,11 +270,9 @@ class _PostFilterWidgetState extends State { ), ); if (pickedDate != null) { - setState(() { - _periodEnd = - pickedDate.millisecondsSinceEpoch ~/ 1000; - }); - _updateQuery(); + periodEnd.value = + pickedDate.millisecondsSinceEpoch ~/ 1000; + updateQuery(); } }, child: InputDecorator( @@ -326,9 +290,9 @@ class _PostFilterWidgetState extends State { suffixIcon: const Icon(Symbols.calendar_today), ), child: Text( - _periodEnd != null + periodEnd.value != null ? DateTime.fromMillisecondsSinceEpoch( - _periodEnd! * 1000, + periodEnd.value! * 1000, ).toString().split(' ')[0] : 'selectDate'.tr(), ), diff --git a/lib/widgets/posts/post_subscription_filter.dart b/lib/widgets/posts/post_subscription_filter.dart new file mode 100644 index 00000000..07d0216e --- /dev/null +++ b/lib/widgets/posts/post_subscription_filter.dart @@ -0,0 +1,157 @@ +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/pods/post/post_subscriptions.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +class PostSubscriptionFilterWidget extends HookConsumerWidget { + final List initialSelectedPublisherIds; + final ValueChanged> onSelectedPublishersChanged; + final bool hideSearch; + + const PostSubscriptionFilterWidget({ + super.key, + required this.initialSelectedPublisherIds, + required this.onSelectedPublishersChanged, + this.hideSearch = false, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedPublisherIds = useState>( + initialSelectedPublisherIds, + ); + final showSubscriptions = useState(false); + + final subscriptionsAsync = ref.watch(subscriptionsProvider); + + void updateSelection() { + onSelectedPublishersChanged(selectedPublisherIds.value); + } + + return Card( + margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Column( + children: [ + ListTile( + title: Text('filterBySubscriptions'.tr()), + leading: const Icon(Symbols.subscriptions), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(const Radius.circular(8)), + ), + trailing: Icon( + showSubscriptions.value + ? Symbols.expand_less + : Symbols.expand_more, + ), + onTap: () { + showSubscriptions.value = !showSubscriptions.value; + }, + ), + if (showSubscriptions.value) ...[ + const Divider(height: 1), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + subscriptionsAsync.when( + data: (subscriptions) { + if (subscriptions.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text('noSubscriptions'.tr()), + ), + ); + } + + return Column( + children: subscriptions.map((subscription) { + final isSelected = selectedPublisherIds.value + .contains(subscription.publisherId); + final publisher = subscription.publisher; + + return CheckboxListTile( + title: Text(publisher.name), + subtitle: + publisher.nick.isNotEmpty && + publisher.nick != publisher.name + ? Text(publisher.nick) + : null, + value: isSelected, + onChanged: (value) { + if (value == true) { + selectedPublisherIds.value = [ + ...selectedPublisherIds.value, + subscription.publisherId, + ]; + } else { + selectedPublisherIds.value = + selectedPublisherIds.value + .where( + (id) => + id != subscription.publisherId, + ) + .toList(); + } + updateSelection(); + }, + dense: true, + controlAffinity: ListTileControlAffinity.leading, + secondary: const Icon(Symbols.person), + ); + }).toList(), + ); + }, + loading: () => const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator(), + ), + ), + error: (error, stack) => Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text('errorLoadingSubscriptions'.tr()), + ), + ), + ), + if (subscriptionsAsync.hasValue && + subscriptionsAsync.value!.isNotEmpty) ...[ + const Gap(12), + Row( + children: [ + TextButton( + onPressed: () { + selectedPublisherIds.value = subscriptionsAsync + .value! + .map((s) => s.publisherId) + .toList(); + updateSelection(); + }, + child: Text('selectAll'.tr()), + ), + const Gap(8), + TextButton( + onPressed: () { + selectedPublisherIds.value = []; + updateSelection(); + }, + child: Text('selectNone'.tr()), + ), + ], + ), + ], + ], + ), + ), + ], + ], + ), + ); + } +}