♻️ Refactored publisher subscription

This commit is contained in:
2025-12-23 00:23:25 +08:00
parent 33686b83e3
commit 0a179acb13
12 changed files with 563 additions and 414 deletions

View File

@@ -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<String, dynamic> json) =>
_$SnSubscriptionStatusFromJson(json);
factory SnPublisherSubscription.fromJson(Map<String, dynamic> 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';
}
}

View File

@@ -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<SnSubscriptionStatus> get copyWith => _$SnSubscriptionStatusCopyWithImpl<SnSubscriptionStatus>(this as SnSubscriptionStatus, _$identity);
$SnPublisherSubscriptionCopyWith<SnPublisherSubscription> get copyWith => _$SnPublisherSubscriptionCopyWithImpl<SnPublisherSubscription>(this as SnPublisherSubscription, _$identity);
/// Serializes this SnSubscriptionStatus to a JSON map.
/// Serializes this SnPublisherSubscription to a JSON map.
Map<String, dynamic> 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 extends Object?>(TResult Function( _SnSubscriptionStatus value)? $default,{required TResult orElse(),}){
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(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 extends Object?>(TResult Function( _SnSubscriptionStatus value) $default,){
@optionalTypeArgs TResult map<TResult extends Object?>(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 extends Object?>(TResult? Function( _SnSubscriptionStatus value)? $default,){
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(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 extends Object?>(TResult Function( bool isSubscribed, String publisherId, String publisherName)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(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 extends Object?>(TResult Function( bool isSubscribed, String publisherId, String publisherName) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(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 extends Object?>(TResult? Function( bool isSubscribed, String publisherId, String publisherName)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(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<String, dynamic> json) => _$SnSubscriptionStatusFromJson(json);
class _SnPublisherSubscription implements SnPublisherSubscription {
const _SnPublisherSubscription({required this.accountId, required this.publisherId, required this.publisher});
factory _SnPublisherSubscription.fromJson(Map<String, dynamic> 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<String, dynamic> 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

View File

@@ -158,20 +158,20 @@ Map<String, dynamic> _$SnPublisherStatsToJson(_SnPublisherStats instance) =>
'downvote_received': instance.downvoteReceived,
};
_SnSubscriptionStatus _$SnSubscriptionStatusFromJson(
_SnPublisherSubscription _$SnPublisherSubscriptionFromJson(
Map<String, dynamic> 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<String, dynamic>),
);
Map<String, dynamic> _$SnSubscriptionStatusToJson(
_SnSubscriptionStatus instance,
Map<String, dynamic> _$SnPublisherSubscriptionToJson(
_SnPublisherSubscription instance,
) => <String, dynamic>{
'is_subscribed': instance.isSubscribed,
'account_id': instance.accountId,
'publisher_id': instance.publisherId,
'publisher_name': instance.publisherName,
'publisher': instance.publisher.toJson(),
};
_SnPostEmbedView _$SnPostEmbedViewFromJson(Map<String, dynamic> json) =>

View File

@@ -65,7 +65,7 @@ final class AppSettingsNotifierProvider
}
String _$appSettingsNotifierHash() =>
r'8e6e901b8a91f9944e1f4dd5d96507e75cd1de81';
r'ee6b67190f3db5d8cb8a9e438a444e91685927d4';
abstract class _$AppSettingsNotifier extends $Notifier<AppSettings> {
AppSettings build();

View File

@@ -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<List<SnPublisherSubscription>>((
ref,
) async {
final client = ref.read(apiClientProvider);
final response = await client.get('/sphere/subscriptions');
return response.data
.map((json) => SnPublisherSubscription.fromJson(json))
.cast<SnPublisherSubscription>()
.toList();
});

View File

@@ -35,7 +35,7 @@ Future<List<SnPublisher>> publishersManaged(Ref ref) async {
}
@riverpod
Future<SnPublisher?> publisher(Ref ref, String? identifier) async {
Future<SnPublisher?> 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<FormState>.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<SnRealm>(
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),
),
],

View File

@@ -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<SnPublisher?>,
@@ -61,23 +61,23 @@ final class PublisherProvider
FutureOr<SnPublisher?>
>
with $FutureModifier<SnPublisher?>, $FutureProvider<SnPublisher?> {
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<SnPublisher?> 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<FutureOr<SnPublisher?>, 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';
}

View File

@@ -33,7 +33,7 @@ part 'publisher_profile.g.dart';
class _PublisherBasisWidget extends StatelessWidget {
final SnPublisher data;
final AsyncValue<SnSubscriptionStatus> subStatus;
final AsyncValue<SnPublisherSubscription?> subStatus;
final ValueNotifier<bool> 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<List<SnAccountBadge>> publisherBadges(Ref ref, String pubName) async {
}
@riverpod
Future<SnSubscriptionStatus> publisherSubscriptionStatus(
Future<SnPublisherSubscription?> 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

View File

@@ -168,13 +168,13 @@ const publisherSubscriptionStatusProvider =
final class PublisherSubscriptionStatusProvider
extends
$FunctionalProvider<
AsyncValue<SnSubscriptionStatus>,
SnSubscriptionStatus,
FutureOr<SnSubscriptionStatus>
AsyncValue<SnPublisherSubscription?>,
SnPublisherSubscription?,
FutureOr<SnPublisherSubscription?>
>
with
$FutureModifier<SnSubscriptionStatus>,
$FutureProvider<SnSubscriptionStatus> {
$FutureModifier<SnPublisherSubscription?>,
$FutureProvider<SnPublisherSubscription?> {
const PublisherSubscriptionStatusProvider._({
required PublisherSubscriptionStatusFamily super.from,
required String super.argument,
@@ -198,12 +198,12 @@ final class PublisherSubscriptionStatusProvider
@$internal
@override
$FutureProviderElement<SnSubscriptionStatus> $createElement(
$FutureProviderElement<SnPublisherSubscription?> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<SnSubscriptionStatus> create(Ref ref) {
FutureOr<SnPublisherSubscription?> 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<FutureOr<SnSubscriptionStatus>, String> {
with $FunctionalFamilyOverride<FutureOr<SnPublisherSubscription?>, String> {
const PublisherSubscriptionStatusFamily._()
: super(
retry: null,

View File

@@ -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,
);
},
);
}

View File

@@ -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<PostListQuery> onQueryChanged;
@@ -19,79 +21,55 @@ class PostFilterWidget extends StatefulWidget {
});
@override
State<PostFilterWidget> createState() => _PostFilterWidgetState();
}
class _PostFilterWidgetState extends State<PostFilterWidget> {
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<bool?>(initialQuery.includeReplies);
final mediaOnly = useState<bool>(initialQuery.mediaOnly ?? false);
final queryTerm = useState<String?>(initialQuery.queryTerm);
final order = useState<String?>(initialQuery.order);
final orderDesc = useState<bool>(initialQuery.orderDesc);
final periodStart = useState<int?>(initialQuery.periodStart);
final periodEnd = useState<int?>(initialQuery.periodEnd);
final type = useState<int?>(initialQuery.type);
final showAdvancedFilters = useState<bool>(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<PostFilterWidget> {
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<PostFilterWidget> {
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<PostFilterWidget> {
),
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<PostFilterWidget> {
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<PostFilterWidget> {
),
),
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<String>(
decoration: InputDecoration(
labelText: 'sortBy'.tr(),
@@ -222,7 +192,7 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
vertical: 8,
),
),
value: _order,
value: order.value,
items: [
DropdownMenuItem(value: 'date', child: Text('date'.tr())),
DropdownMenuItem(
@@ -231,10 +201,8 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
),
],
onChanged: (value) {
setState(() {
_order = value;
});
_updateQuery();
order.value = value;
updateQuery();
},
),
const Gap(12),
@@ -245,9 +213,9 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
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<PostFilterWidget> {
),
);
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<PostFilterWidget> {
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<PostFilterWidget> {
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<PostFilterWidget> {
),
);
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<PostFilterWidget> {
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(),
),

View File

@@ -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<String> initialSelectedPublisherIds;
final ValueChanged<List<String>> 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<List<String>>(
initialSelectedPublisherIds,
);
final showSubscriptions = useState<bool>(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()),
),
],
),
],
],
),
),
],
],
),
);
}
}