Compare commits
2 Commits
06f04eb3a5
...
c7f5b63fe5
Author | SHA1 | Date | |
---|---|---|---|
|
c7f5b63fe5 | ||
|
96c2f45c85 |
@@ -867,7 +867,7 @@
|
||||
"failedToLoadUserInfoNetwork": "It seems be network issue, you can tap the button below to try again.",
|
||||
"failedToLoadUserInfoUnauthorized": "It seems your session has been logged out or not available anymore, you can still try agian to fetch the user info if you want.",
|
||||
"okay": "Okay",
|
||||
"postDetails": "Post Details",
|
||||
"postDetail": "Post Detail",
|
||||
"postCount": {
|
||||
"zero": "No posts",
|
||||
"one": "{} post",
|
||||
|
@@ -829,7 +829,7 @@
|
||||
"failedToLoadUserInfoNetwork": "这看起来是个网络问题,你可以按下面的按钮来重试",
|
||||
"failedToLoadUserInfoUnauthorized": "看来您的会话已被注销或不再可用,如果您愿意,您仍然可以再次尝试获取用户信息。",
|
||||
"okay": "了解",
|
||||
"postDetails": "帖子详情",
|
||||
"postDetail": "帖子详情",
|
||||
"mimeType": "类型",
|
||||
"fileSize": "大小",
|
||||
"fileHash": "哈希",
|
||||
|
@@ -137,6 +137,7 @@ class ArticlesScreen extends ConsumerWidget {
|
||||
return DefaultTabController(
|
||||
length: feeds.length + 1,
|
||||
child: AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: const Text('Articles'),
|
||||
bottom: TabBar(
|
||||
@@ -192,11 +193,13 @@ class ArticlesScreen extends ConsumerWidget {
|
||||
},
|
||||
loading:
|
||||
() => AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(title: const Text('Articles')),
|
||||
body: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error:
|
||||
(err, stack) => AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(title: const Text('Articles')),
|
||||
body: Center(child: Text('Error: $err')),
|
||||
),
|
||||
|
@@ -8,6 +8,7 @@ import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/post/post_list.dart';
|
||||
import 'package:island/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
@@ -27,6 +28,49 @@ Future<SnPostTag> postTag(Ref ref, String slug) async {
|
||||
return SnPostTag.fromJson(resp.data);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<bool> postCategorySubscriptionStatus(
|
||||
Ref ref,
|
||||
String slug,
|
||||
bool isCategory,
|
||||
) async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
try {
|
||||
final resp = await apiClient.get(
|
||||
'/sphere/posts/${isCategory ? 'categories' : 'tags'}/$slug/subscription',
|
||||
);
|
||||
return resp.statusCode == 200;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _subscribeToCategoryOrTag(
|
||||
WidgetRef ref, {
|
||||
required String slug,
|
||||
required bool isCategory,
|
||||
}) async {
|
||||
final apiClient = ref.read(apiClientProvider);
|
||||
await apiClient.post(
|
||||
'/sphere/posts/${isCategory ? 'categories' : 'tags'}/$slug/subscribe',
|
||||
);
|
||||
// Invalidate the subscription status to refresh it
|
||||
ref.invalidate(postCategorySubscriptionStatusProvider(slug, isCategory));
|
||||
}
|
||||
|
||||
Future<void> _unsubscribeFromCategoryOrTag(
|
||||
WidgetRef ref, {
|
||||
required String slug,
|
||||
required bool isCategory,
|
||||
}) async {
|
||||
final apiClient = ref.read(apiClientProvider);
|
||||
await apiClient.post(
|
||||
'/sphere/posts/${isCategory ? 'categories' : 'tags'}/$slug/unsubscribe',
|
||||
);
|
||||
// Invalidate the subscription status to refresh it
|
||||
ref.invalidate(postCategorySubscriptionStatusProvider(slug, isCategory));
|
||||
}
|
||||
|
||||
class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
final String slug;
|
||||
final bool isCategory;
|
||||
@@ -41,6 +85,9 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
final postCategory =
|
||||
isCategory ? ref.watch(postCategoryProvider(slug)) : null;
|
||||
final postTag = isCategory ? null : ref.watch(postTagProvider(slug));
|
||||
final subscriptionStatus = ref.watch(
|
||||
postCategorySubscriptionStatusProvider(slug, isCategory),
|
||||
);
|
||||
|
||||
final postFilterTitle =
|
||||
isCategory
|
||||
@@ -50,58 +97,155 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(title: Text(postFilterTitle).tr()),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (isCategory)
|
||||
postCategory!.when(
|
||||
data:
|
||||
(category) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(category.categoryDisplayTitle).bold().fontSize(15),
|
||||
Text('A category'),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
error:
|
||||
(error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () => ref.invalidate(postCategoryProvider(slug)),
|
||||
),
|
||||
loading: () => ResponseLoadingWidget(),
|
||||
)
|
||||
else
|
||||
postTag!.when(
|
||||
data:
|
||||
(tag) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(tag.name ?? '#${tag.slug}').bold().fontSize(15),
|
||||
Text('A tag'),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
error:
|
||||
(error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () => ref.invalidate(postTagProvider(slug)),
|
||||
),
|
||||
loading: () => ResponseLoadingWidget(),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
body: Expanded(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
if (isCategory)
|
||||
SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 540),
|
||||
child: Card(
|
||||
margin: EdgeInsets.only(top: 8),
|
||||
child: postCategory!.when(
|
||||
data:
|
||||
(category) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
category.categoryDisplayTitle,
|
||||
).bold().fontSize(15),
|
||||
Text('A category'),
|
||||
const Gap(8),
|
||||
subscriptionStatus.when(
|
||||
data:
|
||||
(isSubscribed) =>
|
||||
isSubscribed
|
||||
? FilledButton.icon(
|
||||
onPressed: () async {
|
||||
await _unsubscribeFromCategoryOrTag(
|
||||
ref,
|
||||
slug: slug,
|
||||
isCategory: isCategory,
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Symbols.remove_circle,
|
||||
),
|
||||
label: Text('unsubscribe'.tr()),
|
||||
)
|
||||
: FilledButton.icon(
|
||||
onPressed: () async {
|
||||
await _subscribeToCategoryOrTag(
|
||||
ref,
|
||||
slug: slug,
|
||||
isCategory: isCategory,
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Symbols.add_circle,
|
||||
),
|
||||
label: Text('subscribe'.tr()),
|
||||
),
|
||||
error:
|
||||
(error, _) => Text(
|
||||
'Error loading subscription status',
|
||||
),
|
||||
loading: () => CircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
error:
|
||||
(error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry:
|
||||
() => ref.invalidate(
|
||||
postCategoryProvider(slug),
|
||||
),
|
||||
),
|
||||
loading: () => ResponseLoadingWidget(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 540),
|
||||
child: Card(
|
||||
margin: EdgeInsets.only(top: 8),
|
||||
child: postTag!.when(
|
||||
data:
|
||||
(tag) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
tag.name ?? '#${tag.slug}',
|
||||
).bold().fontSize(15),
|
||||
Text('A tag'),
|
||||
const Gap(8),
|
||||
subscriptionStatus.when(
|
||||
data:
|
||||
(isSubscribed) =>
|
||||
isSubscribed
|
||||
? FilledButton.icon(
|
||||
onPressed: () async {
|
||||
await _unsubscribeFromCategoryOrTag(
|
||||
ref,
|
||||
slug: slug,
|
||||
isCategory: isCategory,
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Symbols.add_circle,
|
||||
),
|
||||
label: Text('unsubscribe'.tr()),
|
||||
)
|
||||
: FilledButton.icon(
|
||||
onPressed: () async {
|
||||
await _subscribeToCategoryOrTag(
|
||||
ref,
|
||||
slug: slug,
|
||||
isCategory: isCategory,
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Symbols.remove_circle,
|
||||
),
|
||||
label: Text('subscribe'.tr()),
|
||||
),
|
||||
error:
|
||||
(error, _) => Text(
|
||||
'Error loading subscription status',
|
||||
),
|
||||
loading: () => CircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
error:
|
||||
(error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry:
|
||||
() => ref.invalidate(postTagProvider(slug)),
|
||||
),
|
||||
loading: () => ResponseLoadingWidget(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverGap(4),
|
||||
SliverPostList(
|
||||
categories: isCategory ? [slug] : null,
|
||||
tags: isCategory ? null : [slug],
|
||||
maxWidth: 540 + 16,
|
||||
),
|
||||
SliverGap(MediaQuery.of(context).padding.bottom + 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -266,5 +266,146 @@ class _PostTagProviderElement
|
||||
String get slug => (origin as PostTagProvider).slug;
|
||||
}
|
||||
|
||||
String _$postCategorySubscriptionStatusHash() =>
|
||||
r'407dc7fcaeffc461b591b4ee2418811aa4f0a63f';
|
||||
|
||||
/// See also [postCategorySubscriptionStatus].
|
||||
@ProviderFor(postCategorySubscriptionStatus)
|
||||
const postCategorySubscriptionStatusProvider =
|
||||
PostCategorySubscriptionStatusFamily();
|
||||
|
||||
/// See also [postCategorySubscriptionStatus].
|
||||
class PostCategorySubscriptionStatusFamily extends Family<AsyncValue<bool>> {
|
||||
/// See also [postCategorySubscriptionStatus].
|
||||
const PostCategorySubscriptionStatusFamily();
|
||||
|
||||
/// See also [postCategorySubscriptionStatus].
|
||||
PostCategorySubscriptionStatusProvider call(String slug, bool isCategory) {
|
||||
return PostCategorySubscriptionStatusProvider(slug, isCategory);
|
||||
}
|
||||
|
||||
@override
|
||||
PostCategorySubscriptionStatusProvider getProviderOverride(
|
||||
covariant PostCategorySubscriptionStatusProvider provider,
|
||||
) {
|
||||
return call(provider.slug, provider.isCategory);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'postCategorySubscriptionStatusProvider';
|
||||
}
|
||||
|
||||
/// See also [postCategorySubscriptionStatus].
|
||||
class PostCategorySubscriptionStatusProvider
|
||||
extends AutoDisposeFutureProvider<bool> {
|
||||
/// See also [postCategorySubscriptionStatus].
|
||||
PostCategorySubscriptionStatusProvider(String slug, bool isCategory)
|
||||
: this._internal(
|
||||
(ref) => postCategorySubscriptionStatus(
|
||||
ref as PostCategorySubscriptionStatusRef,
|
||||
slug,
|
||||
isCategory,
|
||||
),
|
||||
from: postCategorySubscriptionStatusProvider,
|
||||
name: r'postCategorySubscriptionStatusProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$postCategorySubscriptionStatusHash,
|
||||
dependencies: PostCategorySubscriptionStatusFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
PostCategorySubscriptionStatusFamily._allTransitiveDependencies,
|
||||
slug: slug,
|
||||
isCategory: isCategory,
|
||||
);
|
||||
|
||||
PostCategorySubscriptionStatusProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.slug,
|
||||
required this.isCategory,
|
||||
}) : super.internal();
|
||||
|
||||
final String slug;
|
||||
final bool isCategory;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<bool> Function(PostCategorySubscriptionStatusRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: PostCategorySubscriptionStatusProvider._internal(
|
||||
(ref) => create(ref as PostCategorySubscriptionStatusRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
slug: slug,
|
||||
isCategory: isCategory,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<bool> createElement() {
|
||||
return _PostCategorySubscriptionStatusProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PostCategorySubscriptionStatusProvider &&
|
||||
other.slug == slug &&
|
||||
other.isCategory == isCategory;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, slug.hashCode);
|
||||
hash = _SystemHash.combine(hash, isCategory.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin PostCategorySubscriptionStatusRef on AutoDisposeFutureProviderRef<bool> {
|
||||
/// The parameter `slug` of this provider.
|
||||
String get slug;
|
||||
|
||||
/// The parameter `isCategory` of this provider.
|
||||
bool get isCategory;
|
||||
}
|
||||
|
||||
class _PostCategorySubscriptionStatusProviderElement
|
||||
extends AutoDisposeFutureProviderElement<bool>
|
||||
with PostCategorySubscriptionStatusRef {
|
||||
_PostCategorySubscriptionStatusProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get slug => (origin as PostCategorySubscriptionStatusProvider).slug;
|
||||
@override
|
||||
bool get isCategory =>
|
||||
(origin as PostCategorySubscriptionStatusProvider).isCategory;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
|
@@ -83,6 +83,7 @@ class SliverPostList extends HookConsumerWidget {
|
||||
final bool isOpenable;
|
||||
final Function? onRefresh;
|
||||
final Function(SnPost)? onUpdate;
|
||||
final double? maxWidth;
|
||||
|
||||
const SliverPostList({
|
||||
super.key,
|
||||
@@ -98,6 +99,7 @@ class SliverPostList extends HookConsumerWidget {
|
||||
this.isOpenable = true,
|
||||
this.onRefresh,
|
||||
this.onUpdate,
|
||||
this.maxWidth,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -139,6 +141,15 @@ class SliverPostList extends HookConsumerWidget {
|
||||
|
||||
final post = data.items[index];
|
||||
|
||||
if (maxWidth != null) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth!),
|
||||
child: _buildPostItem(post),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return _buildPostItem(post);
|
||||
},
|
||||
),
|
||||
|
Reference in New Issue
Block a user