♻️ 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 @freezed
sealed class SnSubscriptionStatus with _$SnSubscriptionStatus { sealed class SnPublisherSubscription with _$SnPublisherSubscription {
const factory SnSubscriptionStatus({ const factory SnPublisherSubscription({
required bool isSubscribed, required String accountId,
required String publisherId, required String publisherId,
required String publisherName, required SnPublisher publisher,
}) = _SnSubscriptionStatus; }) = _SnPublisherSubscription;
factory SnSubscriptionStatus.fromJson(Map<String, dynamic> json) => factory SnPublisherSubscription.fromJson(Map<String, dynamic> json) =>
_$SnSubscriptionStatusFromJson(json); _$SnPublisherSubscriptionFromJson(json);
} }
@freezed @freezed
@@ -92,8 +92,9 @@ sealed class ReactInfo with _$ReactInfo {
static String getTranslationKey(String templateKey) { static String getTranslationKey(String templateKey) {
final parts = templateKey.split('_'); final parts = templateKey.split('_');
final camelCase = final camelCase = parts
parts.map((p) => p[0].toUpperCase() + p.substring(1)).join(); .map((p) => p[0].toUpperCase() + p.substring(1))
.join();
return 'reaction$camelCase'; return 'reaction$camelCase';
} }
} }

View File

@@ -856,72 +856,81 @@ as int,
/// @nodoc /// @nodoc
mixin _$SnSubscriptionStatus { mixin _$SnPublisherSubscription {
bool get isSubscribed; String get publisherId; String get publisherName; String get accountId; String get publisherId; SnPublisher get publisher;
/// Create a copy of SnSubscriptionStatus /// Create a copy of SnPublisherSubscription
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline') @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(); Map<String, dynamic> toJson();
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,isSubscribed,publisherId,publisherName); int get hashCode => Object.hash(runtimeType,accountId,publisherId,publisher);
@override @override
String toString() { String toString() {
return 'SnSubscriptionStatus(isSubscribed: $isSubscribed, publisherId: $publisherId, publisherName: $publisherName)'; return 'SnPublisherSubscription(accountId: $accountId, publisherId: $publisherId, publisher: $publisher)';
} }
} }
/// @nodoc /// @nodoc
abstract mixin class $SnSubscriptionStatusCopyWith<$Res> { abstract mixin class $SnPublisherSubscriptionCopyWith<$Res> {
factory $SnSubscriptionStatusCopyWith(SnSubscriptionStatus value, $Res Function(SnSubscriptionStatus) _then) = _$SnSubscriptionStatusCopyWithImpl; factory $SnPublisherSubscriptionCopyWith(SnPublisherSubscription value, $Res Function(SnPublisherSubscription) _then) = _$SnPublisherSubscriptionCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
bool isSubscribed, String publisherId, String publisherName String accountId, String publisherId, SnPublisher publisher
}); });
$SnPublisherCopyWith<$Res> get publisher;
} }
/// @nodoc /// @nodoc
class _$SnSubscriptionStatusCopyWithImpl<$Res> class _$SnPublisherSubscriptionCopyWithImpl<$Res>
implements $SnSubscriptionStatusCopyWith<$Res> { implements $SnPublisherSubscriptionCopyWith<$Res> {
_$SnSubscriptionStatusCopyWithImpl(this._self, this._then); _$SnPublisherSubscriptionCopyWithImpl(this._self, this._then);
final SnSubscriptionStatus _self; final SnPublisherSubscription _self;
final $Res Function(SnSubscriptionStatus) _then; 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. /// 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( return _then(_self.copyWith(
isSubscribed: null == isSubscribed ? _self.isSubscribed : isSubscribed // ignore: cast_nullable_to_non_nullable accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as bool,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable as String,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,publisher: null == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable
as String, 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]. /// Adds pattern-matching-related methods to [SnPublisherSubscription].
extension SnSubscriptionStatusPatterns on SnSubscriptionStatus { extension SnPublisherSubscriptionPatterns on SnPublisherSubscription {
/// A variant of `map` that fallback to returning `orElse`. /// A variant of `map` that fallback to returning `orElse`.
/// ///
/// It is equivalent to doing: /// 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; final _that = this;
switch (_that) { switch (_that) {
case _SnSubscriptionStatus() when $default != null: case _SnPublisherSubscription() when $default != null:
return $default(_that);case _: return $default(_that);case _:
return orElse(); 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; final _that = this;
switch (_that) { switch (_that) {
case _SnSubscriptionStatus(): case _SnPublisherSubscription():
return $default(_that);} return $default(_that);}
} }
/// A variant of `map` that fallback to returning `null`. /// 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; final _that = this;
switch (_that) { switch (_that) {
case _SnSubscriptionStatus() when $default != null: case _SnPublisherSubscription() when $default != null:
return $default(_that);case _: return $default(_that);case _:
return null; 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) { switch (_that) {
case _SnSubscriptionStatus() when $default != null: case _SnPublisherSubscription() when $default != null:
return $default(_that.isSubscribed,_that.publisherId,_that.publisherName);case _: return $default(_that.accountId,_that.publisherId,_that.publisher);case _:
return orElse(); 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) { switch (_that) {
case _SnSubscriptionStatus(): case _SnPublisherSubscription():
return $default(_that.isSubscribed,_that.publisherId,_that.publisherName);} return $default(_that.accountId,_that.publisherId,_that.publisher);}
} }
/// A variant of `when` that fallback to returning `null` /// 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) { switch (_that) {
case _SnSubscriptionStatus() when $default != null: case _SnPublisherSubscription() when $default != null:
return $default(_that.isSubscribed,_that.publisherId,_that.publisherName);case _: return $default(_that.accountId,_that.publisherId,_that.publisher);case _:
return null; return null;
} }
@@ -1047,74 +1056,83 @@ return $default(_that.isSubscribed,_that.publisherId,_that.publisherName);case _
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _SnSubscriptionStatus implements SnSubscriptionStatus { class _SnPublisherSubscription implements SnPublisherSubscription {
const _SnSubscriptionStatus({required this.isSubscribed, required this.publisherId, required this.publisherName}); const _SnPublisherSubscription({required this.accountId, required this.publisherId, required this.publisher});
factory _SnSubscriptionStatus.fromJson(Map<String, dynamic> json) => _$SnSubscriptionStatusFromJson(json); factory _SnPublisherSubscription.fromJson(Map<String, dynamic> json) => _$SnPublisherSubscriptionFromJson(json);
@override final bool isSubscribed; @override final String accountId;
@override final String publisherId; @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. /// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false) @override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$SnSubscriptionStatusCopyWith<_SnSubscriptionStatus> get copyWith => __$SnSubscriptionStatusCopyWithImpl<_SnSubscriptionStatus>(this, _$identity); _$SnPublisherSubscriptionCopyWith<_SnPublisherSubscription> get copyWith => __$SnPublisherSubscriptionCopyWithImpl<_SnPublisherSubscription>(this, _$identity);
@override @override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return _$SnSubscriptionStatusToJson(this, ); return _$SnPublisherSubscriptionToJson(this, );
} }
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,isSubscribed,publisherId,publisherName); int get hashCode => Object.hash(runtimeType,accountId,publisherId,publisher);
@override @override
String toString() { String toString() {
return 'SnSubscriptionStatus(isSubscribed: $isSubscribed, publisherId: $publisherId, publisherName: $publisherName)'; return 'SnPublisherSubscription(accountId: $accountId, publisherId: $publisherId, publisher: $publisher)';
} }
} }
/// @nodoc /// @nodoc
abstract mixin class _$SnSubscriptionStatusCopyWith<$Res> implements $SnSubscriptionStatusCopyWith<$Res> { abstract mixin class _$SnPublisherSubscriptionCopyWith<$Res> implements $SnPublisherSubscriptionCopyWith<$Res> {
factory _$SnSubscriptionStatusCopyWith(_SnSubscriptionStatus value, $Res Function(_SnSubscriptionStatus) _then) = __$SnSubscriptionStatusCopyWithImpl; factory _$SnPublisherSubscriptionCopyWith(_SnPublisherSubscription value, $Res Function(_SnPublisherSubscription) _then) = __$SnPublisherSubscriptionCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
bool isSubscribed, String publisherId, String publisherName String accountId, String publisherId, SnPublisher publisher
}); });
@override $SnPublisherCopyWith<$Res> get publisher;
} }
/// @nodoc /// @nodoc
class __$SnSubscriptionStatusCopyWithImpl<$Res> class __$SnPublisherSubscriptionCopyWithImpl<$Res>
implements _$SnSubscriptionStatusCopyWith<$Res> { implements _$SnPublisherSubscriptionCopyWith<$Res> {
__$SnSubscriptionStatusCopyWithImpl(this._self, this._then); __$SnPublisherSubscriptionCopyWithImpl(this._self, this._then);
final _SnSubscriptionStatus _self; final _SnPublisherSubscription _self;
final $Res Function(_SnSubscriptionStatus) _then; 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. /// 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,}) { @override @pragma('vm:prefer-inline') $Res call({Object? accountId = null,Object? publisherId = null,Object? publisher = null,}) {
return _then(_SnSubscriptionStatus( return _then(_SnPublisherSubscription(
isSubscribed: null == isSubscribed ? _self.isSubscribed : isSubscribed // ignore: cast_nullable_to_non_nullable accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as bool,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable as String,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,publisher: null == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable
as String, 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 /// @nodoc

View File

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

View File

@@ -65,7 +65,7 @@ final class AppSettingsNotifierProvider
} }
String _$appSettingsNotifierHash() => String _$appSettingsNotifierHash() =>
r'8e6e901b8a91f9944e1f4dd5d96507e75cd1de81'; r'ee6b67190f3db5d8cb8a9e438a444e91685927d4';
abstract class _$AppSettingsNotifier extends $Notifier<AppSettings> { abstract class _$AppSettingsNotifier extends $Notifier<AppSettings> {
AppSettings build(); 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 @riverpod
Future<SnPublisher?> publisher(Ref ref, String? identifier) async { Future<SnPublisher?> publisherNullable(Ref ref, String? identifier) async {
if (identifier == null) return null; if (identifier == null) return null;
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
final resp = await client.get('/sphere/publishers/$identifier'); final resp = await client.get('/sphere/publishers/$identifier');
@@ -93,13 +93,9 @@ class EditPublisherScreen extends HookConsumerWidget {
submitting.value = true; submitting.value = true;
try { try {
final cloudFile = final cloudFile = await FileUploader.createCloudFile(
await FileUploader.createCloudFile(
ref: ref, ref: ref,
fileData: UniversalFile( fileData: UniversalFile(data: result, type: UniversalFileType.image),
data: result,
type: UniversalFileType.image,
),
).future; ).future;
if (cloudFile == null) { if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...'); 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 formKey = useMemoized(GlobalKey<FormState>.new, const []);
final nameController = useTextEditingController( final nameController = useTextEditingController(
@@ -194,8 +190,7 @@ class EditPublisherScreen extends HookConsumerWidget {
GestureDetector( GestureDetector(
child: Container( child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: child: background.value != null
background.value != null
? CloudImageWidget( ? CloudImageWidget(
fileId: background.value!, fileId: background.value!,
fit: BoxFit.cover, fit: BoxFit.cover,
@@ -238,14 +233,14 @@ class EditPublisherScreen extends HookConsumerWidget {
prefixText: '@', prefixText: '@',
), ),
readOnly: name != null, readOnly: name != null,
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
TextFormField( TextFormField(
controller: nickController, controller: nickController,
decoration: InputDecoration(labelText: 'nickname'.tr()), decoration: InputDecoration(labelText: 'nickname'.tr()),
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
TextFormField( TextFormField(
controller: bioController, controller: bioController,
@@ -255,8 +250,8 @@ class EditPublisherScreen extends HookConsumerWidget {
), ),
minLines: 3, minLines: 3,
maxLines: null, maxLines: null,
onTapOutside: onTapOutside: (_) =>
(_) => FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
DropdownButtonFormField<SnRealm>( DropdownButtonFormField<SnRealm>(
value: currentRealm.value, value: currentRealm.value,
@@ -267,8 +262,7 @@ class EditPublisherScreen extends HookConsumerWidget {
child: Text('individual'.tr()), child: Text('individual'.tr()),
), ),
...joinedRealms.maybeWhen( ...joinedRealms.maybeWhen(
data: data: (realms) => realms.map(
(realms) => realms.map(
(realm) => DropdownMenuItem( (realm) => DropdownMenuItem(
value: realm, value: realm,
child: Text(realm.name), child: Text(realm.name),
@@ -277,8 +271,7 @@ class EditPublisherScreen extends HookConsumerWidget {
orElse: () => [], orElse: () => [],
), ),
], ],
onChanged: onChanged: joinedRealms.isLoading
joinedRealms.isLoading
? null ? null
: (SnRealm? value) { : (SnRealm? value) {
currentRealm.value = value; currentRealm.value = value;
@@ -307,8 +300,7 @@ class EditPublisherScreen extends HookConsumerWidget {
currentRealm.value!.background?.id; currentRealm.value!.background?.id;
} }
}, },
label: label: Text(
Text(
currentRealm.value == null currentRealm.value == null
? 'syncPublisher' ? 'syncPublisher'
: 'syncPublisherRealm', : 'syncPublisherRealm',
@@ -317,8 +309,7 @@ class EditPublisherScreen extends HookConsumerWidget {
), ),
TextButton.icon( TextButton.icon(
onPressed: submitting.value ? null : performAction, onPressed: submitting.value ? null : performAction,
label: label: Text(
Text(
name == null ? 'create' : 'saveChanges', name == null ? 'create' : 'saveChanges',
).tr(), ).tr(),
icon: const Icon(Symbols.save), icon: const Icon(Symbols.save),

View File

@@ -50,10 +50,10 @@ final class PublishersManagedProvider
String _$publishersManagedHash() => r'ea83759fed9bd5119738b4d09f12b4476959e0a3'; String _$publishersManagedHash() => r'ea83759fed9bd5119738b4d09f12b4476959e0a3';
@ProviderFor(publisher) @ProviderFor(publisherNullable)
const publisherProvider = PublisherFamily._(); const publisherNullableProvider = PublisherNullableFamily._();
final class PublisherProvider final class PublisherNullableProvider
extends extends
$FunctionalProvider< $FunctionalProvider<
AsyncValue<SnPublisher?>, AsyncValue<SnPublisher?>,
@@ -61,23 +61,23 @@ final class PublisherProvider
FutureOr<SnPublisher?> FutureOr<SnPublisher?>
> >
with $FutureModifier<SnPublisher?>, $FutureProvider<SnPublisher?> { with $FutureModifier<SnPublisher?>, $FutureProvider<SnPublisher?> {
const PublisherProvider._({ const PublisherNullableProvider._({
required PublisherFamily super.from, required PublisherNullableFamily super.from,
required String? super.argument, required String? super.argument,
}) : super( }) : super(
retry: null, retry: null,
name: r'publisherProvider', name: r'publisherNullableProvider',
isAutoDispose: true, isAutoDispose: true,
dependencies: null, dependencies: null,
$allTransitiveDependencies: null, $allTransitiveDependencies: null,
); );
@override @override
String debugGetCreateSourceHash() => _$publisherHash(); String debugGetCreateSourceHash() => _$publisherNullableHash();
@override @override
String toString() { String toString() {
return r'publisherProvider' return r'publisherNullableProvider'
'' ''
'($argument)'; '($argument)';
} }
@@ -91,12 +91,12 @@ final class PublisherProvider
@override @override
FutureOr<SnPublisher?> create(Ref ref) { FutureOr<SnPublisher?> create(Ref ref) {
final argument = this.argument as String?; final argument = this.argument as String?;
return publisher(ref, argument); return publisherNullable(ref, argument);
} }
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is PublisherProvider && other.argument == argument; return other is PublisherNullableProvider && other.argument == argument;
} }
@override @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?> { with $FunctionalFamilyOverride<FutureOr<SnPublisher?>, String?> {
const PublisherFamily._() const PublisherNullableFamily._()
: super( : super(
retry: null, retry: null,
name: r'publisherProvider', name: r'publisherNullableProvider',
dependencies: null, dependencies: null,
$allTransitiveDependencies: null, $allTransitiveDependencies: null,
isAutoDispose: true, isAutoDispose: true,
); );
PublisherProvider call(String? identifier) => PublisherNullableProvider call(String? identifier) =>
PublisherProvider._(argument: identifier, from: this); PublisherNullableProvider._(argument: identifier, from: this);
@override @override
String toString() => r'publisherProvider'; String toString() => r'publisherNullableProvider';
} }

View File

@@ -33,7 +33,7 @@ part 'publisher_profile.g.dart';
class _PublisherBasisWidget extends StatelessWidget { class _PublisherBasisWidget extends StatelessWidget {
final SnPublisher data; final SnPublisher data;
final AsyncValue<SnSubscriptionStatus> subStatus; final AsyncValue<SnPublisherSubscription?> subStatus;
final ValueNotifier<bool> subscribing; final ValueNotifier<bool> subscribing;
final VoidCallback subscribe; final VoidCallback subscribe;
final VoidCallback unsubscribe; final VoidCallback unsubscribe;
@@ -208,16 +208,16 @@ class _PublisherBasisWidget extends StatelessWidget {
data: (status) => FilledButton.icon( data: (status) => FilledButton.icon(
onPressed: subscribing.value onPressed: subscribing.value
? null ? null
: (status.isSubscribed : (status != null
? unsubscribe ? unsubscribe
: subscribe), : subscribe),
icon: Icon( icon: Icon(
status.isSubscribed status != null
? Symbols.remove_circle ? Symbols.remove_circle
: Symbols.add_circle, : Symbols.add_circle,
), ),
label: Text( label: Text(
status.isSubscribed status != null
? 'unsubscribe' ? 'unsubscribe'
: 'subscribe', : 'subscribe',
).tr(), ).tr(),
@@ -366,13 +366,16 @@ Future<List<SnAccountBadge>> publisherBadges(Ref ref, String pubName) async {
} }
@riverpod @riverpod
Future<SnSubscriptionStatus> publisherSubscriptionStatus( Future<SnPublisherSubscription?> publisherSubscriptionStatus(
Ref ref, Ref ref,
String pubName, String pubName,
) async { ) async {
final apiClient = ref.watch(apiClientProvider); final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get("/sphere/publishers/$pubName/subscription"); 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 @riverpod

View File

@@ -168,13 +168,13 @@ const publisherSubscriptionStatusProvider =
final class PublisherSubscriptionStatusProvider final class PublisherSubscriptionStatusProvider
extends extends
$FunctionalProvider< $FunctionalProvider<
AsyncValue<SnSubscriptionStatus>, AsyncValue<SnPublisherSubscription?>,
SnSubscriptionStatus, SnPublisherSubscription?,
FutureOr<SnSubscriptionStatus> FutureOr<SnPublisherSubscription?>
> >
with with
$FutureModifier<SnSubscriptionStatus>, $FutureModifier<SnPublisherSubscription?>,
$FutureProvider<SnSubscriptionStatus> { $FutureProvider<SnPublisherSubscription?> {
const PublisherSubscriptionStatusProvider._({ const PublisherSubscriptionStatusProvider._({
required PublisherSubscriptionStatusFamily super.from, required PublisherSubscriptionStatusFamily super.from,
required String super.argument, required String super.argument,
@@ -198,12 +198,12 @@ final class PublisherSubscriptionStatusProvider
@$internal @$internal
@override @override
$FutureProviderElement<SnSubscriptionStatus> $createElement( $FutureProviderElement<SnPublisherSubscription?> $createElement(
$ProviderPointer pointer, $ProviderPointer pointer,
) => $FutureProviderElement(pointer); ) => $FutureProviderElement(pointer);
@override @override
FutureOr<SnSubscriptionStatus> create(Ref ref) { FutureOr<SnPublisherSubscription?> create(Ref ref) {
final argument = this.argument as String; final argument = this.argument as String;
return publisherSubscriptionStatus(ref, argument); return publisherSubscriptionStatus(ref, argument);
} }
@@ -221,10 +221,10 @@ final class PublisherSubscriptionStatusProvider
} }
String _$publisherSubscriptionStatusHash() => String _$publisherSubscriptionStatusHash() =>
r'634262ce519e1c8288267df11e08e1d4acaa4a44'; r'accf6a0cdf98f8b0474d94ac575e8b20448adc79';
final class PublisherSubscriptionStatusFamily extends $Family final class PublisherSubscriptionStatusFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<SnSubscriptionStatus>, String> { with $FunctionalFamilyOverride<FutureOr<SnPublisherSubscription?>, String> {
const PublisherSubscriptionStatusFamily._() const PublisherSubscriptionStatusFamily._()
: super( : super(
retry: null, retry: null,

View File

@@ -12,7 +12,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/screens/account/profile.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/alert.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/cloud_file_lightbox.dart'; import 'package:island/widgets/content/cloud_file_lightbox.dart';
@@ -64,14 +64,16 @@ class MarkdownTextContent extends HookConsumerWidget {
final matches = stickerPattern.allMatches(content); final matches = stickerPattern.allMatches(content);
// Content should only contain one sticker and nothing else (except whitespace) // Content should only contain one sticker and nothing else (except whitespace)
final contentWithoutStickers = final contentWithoutStickers = content
content.replaceAll(stickerPattern, '').trim(); .replaceAll(stickerPattern, '')
.trim();
return matches.length == 1 && contentWithoutStickers.isEmpty; return matches.length == 1 && contentWithoutStickers.isEmpty;
}, [content]); }, [content]);
final isDark = Theme.of(context).brightness == Brightness.dark; final isDark = Theme.of(context).brightness == Brightness.dark;
final config = final config = isDark
isDark ? MarkdownConfig.darkConfig : MarkdownConfig.defaultConfig; ? MarkdownConfig.darkConfig
: MarkdownConfig.defaultConfig;
final onMentionTap = useCallback((String type, String id) { final onMentionTap = useCallback((String type, String id) {
final fullPath = '/$type/$id'; final fullPath = '/$type/$id';
@@ -128,8 +130,7 @@ class MarkdownTextContent extends HookConsumerWidget {
), ),
), ),
TableConfig( TableConfig(
wrapper: wrapper: (child) => SingleChildScrollView(
(child) => SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: child, child: child,
), ),
@@ -203,8 +204,9 @@ class MarkdownTextContent extends HookConsumerWidget {
), ),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: color: Theme.of(
Theme.of(context).colorScheme.surfaceContainer, context,
).colorScheme.surfaceContainer,
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular(8), Radius.circular(8),
), ),
@@ -288,8 +290,7 @@ class _MetionInlineSyntax extends markdown.InlineSyntax {
"c" => 'chat', "c" => 'chat',
_ => '', _ => '',
}; };
final element = final element = markdown.Element('mention-chip', [markdown.Text(alias)])
markdown.Element('mention-chip', [markdown.Text(alias)])
..attributes['alias'] = alias ..attributes['alias'] = alias
..attributes['type'] = type ..attributes['type'] = type
..attributes['id'] = parts.last; ..attributes['id'] = parts.last;
@@ -373,7 +374,8 @@ class MentionChipGenerator extends SpanNodeGeneratorWithTag {
required void Function(String type, String id) onTap, required void Function(String type, String id) onTap,
}) : super( }) : super(
tag: 'mention-chip', tag: 'mention-chip',
generator: ( generator:
(
markdown.Element element, markdown.Element element,
MarkdownConfig config, MarkdownConfig config,
WidgetVisitor visitor, WidgetVisitor visitor,
@@ -440,15 +442,13 @@ class MentionChipSpanNode extends SpanNode {
builder: (context, ref, _) { builder: (context, ref, _) {
final userData = ref.watch(accountProvider(parts.last)); final userData = ref.watch(accountProvider(parts.last));
return userData.when( return userData.when(
data: data: (data) => ProfilePictureWidget(
(data) => ProfilePictureWidget(
file: data.profile.picture, file: data.profile.picture,
fallbackIcon: Symbols.person_rounded, fallbackIcon: Symbols.person_rounded,
radius: 9, radius: 9,
), ),
error: (_, _) => const Icon(Symbols.close), error: (_, _) => const Icon(Symbols.close),
loading: loading: () => const SizedBox(
() => const SizedBox(
width: 9, width: 9,
height: 9, height: 9,
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
@@ -460,15 +460,13 @@ class MentionChipSpanNode extends SpanNode {
builder: (context, ref, _) { builder: (context, ref, _) {
final pubData = ref.watch(publisherProvider(parts.last)); final pubData = ref.watch(publisherProvider(parts.last));
return pubData.when( return pubData.when(
data: data: (data) => ProfilePictureWidget(
(data) => ProfilePictureWidget( file: data.picture,
file: data?.picture,
fallbackIcon: Symbols.design_services_rounded, fallbackIcon: Symbols.design_services_rounded,
radius: 9, radius: 9,
), ),
error: (_, _) => const Icon(Symbols.close), error: (_, _) => const Icon(Symbols.close),
loading: loading: () => const SizedBox(
() => const SizedBox(
width: 9, width: 9,
height: 9, height: 9,
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
@@ -508,7 +506,8 @@ class HighlightGenerator extends SpanNodeGeneratorWithTag {
HighlightGenerator({required Color highlightColor}) HighlightGenerator({required Color highlightColor})
: super( : super(
tag: 'highlight', tag: 'highlight',
generator: ( generator:
(
markdown.Element element, markdown.Element element,
MarkdownConfig config, MarkdownConfig config,
WidgetVisitor visitor, WidgetVisitor visitor,
@@ -545,7 +544,8 @@ class SpoilerGenerator extends SpanNodeGeneratorWithTag {
required VoidCallback onToggle, required VoidCallback onToggle,
}) : super( }) : super(
tag: 'spoiler', tag: 'spoiler',
generator: ( generator:
(
markdown.Element element, markdown.Element element,
MarkdownConfig config, MarkdownConfig config,
WidgetVisitor visitor, WidgetVisitor visitor,
@@ -591,8 +591,7 @@ class SpoilerSpanNode extends SpanNode {
border: revealed ? Border.all(color: outlineColor, width: 1) : null, border: revealed ? Border.all(color: outlineColor, width: 1) : null,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: child: revealed
revealed
? Row( ? Row(
spacing: 6, spacing: 6,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -612,8 +611,7 @@ class SpoilerSpanNode extends SpanNode {
size: 18, size: 18,
), ),
Flexible( Flexible(
child: child: Text(
Text(
'spoiler', 'spoiler',
style: TextStyle(color: foregroundColor), style: TextStyle(color: foregroundColor),
).tr(), ).tr(),
@@ -634,7 +632,8 @@ class StickerGenerator extends SpanNodeGeneratorWithTag {
required String baseUrl, required String baseUrl,
}) : super( }) : super(
tag: 'sticker', tag: 'sticker',
generator: ( generator:
(
markdown.Element element, markdown.Element element,
MarkdownConfig config, MarkdownConfig config,
WidgetVisitor visitor, WidgetVisitor visitor,

View File

@@ -1,10 +1,12 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/post/post_list.dart'; import 'package:island/pods/post/post_list.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
class PostFilterWidget extends StatefulWidget { class PostFilterWidget extends HookConsumerWidget {
final TabController categoryTabController; final TabController categoryTabController;
final PostListQuery initialQuery; final PostListQuery initialQuery;
final ValueChanged<PostListQuery> onQueryChanged; final ValueChanged<PostListQuery> onQueryChanged;
@@ -19,79 +21,55 @@ class PostFilterWidget extends StatefulWidget {
}); });
@override @override
State<PostFilterWidget> createState() => _PostFilterWidgetState(); 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,
);
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);
} }
class _PostFilterWidgetState extends State<PostFilterWidget> { useEffect(() {
late bool? _includeReplies; void onTabChanged() {
late bool _mediaOnly; final tabIndex = categoryTabController.index;
late String? _queryTerm; type.value = switch (tabIndex) {
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, 1 => 0,
2 => 1, 2 => 1,
_ => null, _ => null,
}; };
}); updateQuery();
_updateQuery();
} }
void _updateQuery() { categoryTabController.addListener(onTabChanged);
final newQuery = widget.initialQuery.copyWith( return () => categoryTabController.removeListener(onTabChanged);
includeReplies: _includeReplies, }, [categoryTabController]);
mediaOnly: _mediaOnly,
queryTerm: _queryTerm,
order: _order,
periodStart: _periodStart,
periodEnd: _periodEnd,
orderDesc: _orderDesc,
type: _type,
);
widget.onQueryChanged(newQuery);
}
@override
Widget build(BuildContext context) {
return Card( return Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column( child: Column(
children: [ children: [
TabBar( TabBar(
controller: widget.categoryTabController, controller: categoryTabController,
dividerColor: Colors.transparent, dividerColor: Colors.transparent,
splashBorderRadius: const BorderRadius.all(Radius.circular(8)), splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
tabs: [ tabs: [
@@ -108,20 +86,18 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
Expanded( Expanded(
child: CheckboxListTile( child: CheckboxListTile(
title: Text('reply'.tr()), title: Text('reply'.tr()),
value: _includeReplies, value: includeReplies.value,
tristate: true, tristate: true,
onChanged: (value) { onChanged: (value) {
// Cycle through: null -> false -> true -> null final current = includeReplies.value;
setState(() { if (current == null) {
if (_includeReplies == null) { includeReplies.value = false;
_includeReplies = false; } else if (current == false) {
} else if (_includeReplies == false) { includeReplies.value = true;
_includeReplies = true;
} else { } else {
_includeReplies = null; includeReplies.value = null;
} }
}); updateQuery();
_updateQuery();
}, },
dense: true, dense: true,
controlAffinity: ListTileControlAffinity.leading, controlAffinity: ListTileControlAffinity.leading,
@@ -131,14 +107,12 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
Expanded( Expanded(
child: CheckboxListTile( child: CheckboxListTile(
title: Text('attachments'.tr()), title: Text('attachments'.tr()),
value: _mediaOnly, value: mediaOnly.value,
onChanged: (value) { onChanged: (value) {
setState(() {
if (value != null) { if (value != null) {
_mediaOnly = value; mediaOnly.value = value;
} }
}); updateQuery();
_updateQuery();
}, },
dense: true, dense: true,
controlAffinity: ListTileControlAffinity.leading, controlAffinity: ListTileControlAffinity.leading,
@@ -149,14 +123,12 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
), ),
CheckboxListTile( CheckboxListTile(
title: Text('descendingOrder'.tr()), title: Text('descendingOrder'.tr()),
value: _orderDesc, value: orderDesc.value,
onChanged: (value) { onChanged: (value) {
setState(() {
if (value != null) { if (value != null) {
_orderDesc = value; orderDesc.value = value;
} }
}); updateQuery();
_updateQuery();
}, },
dense: true, dense: true,
controlAffinity: ListTileControlAffinity.leading, controlAffinity: ListTileControlAffinity.leading,
@@ -173,24 +145,24 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
borderRadius: BorderRadius.all(const Radius.circular(8)), borderRadius: BorderRadius.all(const Radius.circular(8)),
), ),
trailing: Icon( trailing: Icon(
_showAdvancedFilters ? Symbols.expand_less : Symbols.expand_more, showAdvancedFilters.value
? Symbols.expand_less
: Symbols.expand_more,
), ),
onTap: () { onTap: () {
setState(() { showAdvancedFilters.value = !showAdvancedFilters.value;
_showAdvancedFilters = !_showAdvancedFilters;
});
}, },
), ),
if (_showAdvancedFilters) ...[ if (showAdvancedFilters.value) ...[
const Divider(height: 1), const Divider(height: 1),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (!widget.hideSearch) if (!hideSearch)
TextField( TextField(
controller: _searchController, controller: searchController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'search'.tr(), labelText: 'search'.tr(),
hintText: 'searchPosts'.tr(), hintText: 'searchPosts'.tr(),
@@ -204,13 +176,11 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
), ),
), ),
onChanged: (value) { onChanged: (value) {
setState(() { queryTerm.value = value.isEmpty ? null : value;
_queryTerm = value.isEmpty ? null : value; updateQuery();
});
_updateQuery();
}, },
), ),
if (!widget.hideSearch) const Gap(12), if (!hideSearch) const Gap(12),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'sortBy'.tr(), labelText: 'sortBy'.tr(),
@@ -222,7 +192,7 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
vertical: 8, vertical: 8,
), ),
), ),
value: _order, value: order.value,
items: [ items: [
DropdownMenuItem(value: 'date', child: Text('date'.tr())), DropdownMenuItem(value: 'date', child: Text('date'.tr())),
DropdownMenuItem( DropdownMenuItem(
@@ -231,10 +201,8 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
), ),
], ],
onChanged: (value) { onChanged: (value) {
setState(() { order.value = value;
_order = value; updateQuery();
});
_updateQuery();
}, },
), ),
const Gap(12), const Gap(12),
@@ -245,9 +213,9 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
onTap: () async { onTap: () async {
final pickedDate = await showDatePicker( final pickedDate = await showDatePicker(
context: context, context: context,
initialDate: _periodStart != null initialDate: periodStart.value != null
? DateTime.fromMillisecondsSinceEpoch( ? DateTime.fromMillisecondsSinceEpoch(
_periodStart! * 1000, periodStart.value! * 1000,
) )
: DateTime.now(), : DateTime.now(),
firstDate: DateTime(2000), firstDate: DateTime(2000),
@@ -256,11 +224,9 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
), ),
); );
if (pickedDate != null) { if (pickedDate != null) {
setState(() { periodStart.value =
_periodStart =
pickedDate.millisecondsSinceEpoch ~/ 1000; pickedDate.millisecondsSinceEpoch ~/ 1000;
}); updateQuery();
_updateQuery();
} }
}, },
child: InputDecorator( child: InputDecorator(
@@ -278,9 +244,9 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
suffixIcon: const Icon(Symbols.calendar_today), suffixIcon: const Icon(Symbols.calendar_today),
), ),
child: Text( child: Text(
_periodStart != null periodStart.value != null
? DateTime.fromMillisecondsSinceEpoch( ? DateTime.fromMillisecondsSinceEpoch(
_periodStart! * 1000, periodStart.value! * 1000,
).toString().split(' ')[0] ).toString().split(' ')[0]
: 'selectDate'.tr(), : 'selectDate'.tr(),
), ),
@@ -293,9 +259,9 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
onTap: () async { onTap: () async {
final pickedDate = await showDatePicker( final pickedDate = await showDatePicker(
context: context, context: context,
initialDate: _periodEnd != null initialDate: periodEnd.value != null
? DateTime.fromMillisecondsSinceEpoch( ? DateTime.fromMillisecondsSinceEpoch(
_periodEnd! * 1000, periodEnd.value! * 1000,
) )
: DateTime.now(), : DateTime.now(),
firstDate: DateTime(2000), firstDate: DateTime(2000),
@@ -304,11 +270,9 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
), ),
); );
if (pickedDate != null) { if (pickedDate != null) {
setState(() { periodEnd.value =
_periodEnd =
pickedDate.millisecondsSinceEpoch ~/ 1000; pickedDate.millisecondsSinceEpoch ~/ 1000;
}); updateQuery();
_updateQuery();
} }
}, },
child: InputDecorator( child: InputDecorator(
@@ -326,9 +290,9 @@ class _PostFilterWidgetState extends State<PostFilterWidget> {
suffixIcon: const Icon(Symbols.calendar_today), suffixIcon: const Icon(Symbols.calendar_today),
), ),
child: Text( child: Text(
_periodEnd != null periodEnd.value != null
? DateTime.fromMillisecondsSinceEpoch( ? DateTime.fromMillisecondsSinceEpoch(
_periodEnd! * 1000, periodEnd.value! * 1000,
).toString().split(' ')[0] ).toString().split(' ')[0]
: 'selectDate'.tr(), : '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()),
),
],
),
],
],
),
),
],
],
),
);
}
}